This tutorial builds on top of Tutorial01 and demonstrates how to render an actual 3D object, a cube. It shows how to load shaders from files, create and use vertex, index and uniform buffers.
This tutorial uses a little bit more complicated vertex shader that reads two attributes from the input vertex buffer, a float3
position and a float4
color:
By convention, vertex shader inputs should be labeled as ATTRIBn, where n is the attribute number. The attributes must match the input layout defined in the pipeline state object. Note that if separate shader objects are not supported (this is only the case for old GLES3.0 devices), vertex shader output variable name must match exactly the name of the pixel shader input variable. If the variable has structure type (like in this example), the structure declarations must also be identical. The shader also uses a world-view-projection matrix defined in a constant (uniform) buffer called Constants
to transform vertex positions:
The full vertex shader source code is as follows:
Similar to Tutorial01, pixel (fragment) shader simply interpolates vertex colors.
In this tutorial, we will be using depth buffer, so besides color output, we need to specify the format of the depth output in the PSOCreateInfo
:
Also, we will enable back-face culling:
In this tutorial, we create shaders from files rather than from the source code strings. Diligent Engine accesses platform-specific files through IShaderSourceInputStreamFactory
interface. The engine provides default implementation for the interface that should be sufficient in most cases.
CreateDefaultShaderSourceStreamFactory
method optionally takes a semicolon-separated list of directories where source files will be looked up.
Notice the use of the SHADER_COMPILE_FLAG_PACK_MATRIX_ROW_MAJOR
flag:
By default, matrices are laid out in GPU memory in column-major order, which means that the first four values of a 4x4 matrix represent the first column, the next four values represent the second column, and so on. Using row-major layout is more intuitive as it matches the way matrices are defined in C++ code. The SHADER_COMPILE_FLAG_PACK_MATRIX_ROW_MAJOR
flag tells the shader compiler to use the row-major layout for matrices. Note that only row-major matrices are supported in WebGPU backend.
Other than a couple of differences mentioned above, vertex shader initialization is the same as in Tutorial01:
This time our shader uses a resource - a uniform buffer. So the first step is to create the buffer that will hold the transformation matrix. To create a buffer, populate BufferDesc
structure:
Usage and Bind flags are designed after D3D11 Usage and D3D11 Bind Flags.
Since our vertex shader reads attributes from the vertex buffer, the pipeline state must define how the attributes are fetched from the buffer. The two attributes are defined as follows:
Our shader has one variable that needs to be bound by the application, a uniform buffer Constants
. Shader variables can be assigned one of three types, static, dynamic, or mutable. Please read this post for details. If no explicit type is provided for a variable, default type will be used:
Constants
uniform buffer is a static resource variable. Static variables are bound directly to the pipeline state and cannot be changed once bound:
Notice that only the binding cannot be changed. The contents of the buffer is modifiable.
To create a vertex buffer, we first prepare the data to fill the buffer with. Our vertex layout corresponds to the following structure:
Our vertex buffer will contain 8 vertices. Every vertex will have position and color:
Similar to uniform buffer, to create a vertex buffer, we populate BufferDesc
structure. Since the data in the buffer will never change, we create the buffer with immutable usage (USAGE_IMMUTABLE
) and provide initial data to CreateBuffer()
:
Index buffer is initialized in a very similar fashion.
Since our fragment shader uses shader resources (constant buffer), we need to create a shader resource binding object that will manage all required resource bindings:
The second parameter tells the engine to initialize all static resources bindings in the created SRB object.
There are few changes that we need to make to our rendering procedure compared to Tutorial01. First, we need to update our transformation matrix. Since we created our constant buffer as dynamic buffer, it can be mapped. Diligent Engine provides MapHelper
template class that facilitates buffer mapping:
Second, we need to bind vertex and index buffer to the GPU pipeline. We use RESOURCE_STATE_TRANSITION_MODE_TRANSITION
to let the engine automatically transition the buffers to required states.
Next step is very important: we need to commit shader resources:
The first argument of CommitShaderResources()
is the shader resource binding object. The RESOURCE_STATE_TRANSITION_MODE_TRANSITION
tells the system that resources need to be transitioned to correct states by the engine. Transitioning resources introduces some overhead and can be avoided when it is known that resources are already in correct states.
Finally, this time the draw call is an indexed one:
We want the engine to verify that the states are correct, so we use DRAW_FLAG_VERIFY_ALL
flag.