MoonWorks-docs/content/Graphics/Resources/GraphicsPipeline/VertexInputState.md

4.7 KiB

title date weight
Vertex Input State 2021-01-28T12:55:51-08:00 9

Our vertex input state will tell the graphics pipeline how to interpret the vertex data in a buffer so that it can pass this data to the vertex shader.

Vertex input state is comprised of two sub-structures: "vertex bindings" and "vertex attributes".

First, let's have a look at an example vertex shader:

#version 450

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inTexCoord;

layout(location = 0) out vec2 fragCoord;

void main() {
	gl_Position = vec4(inPosition, 1.0);
	fragCoord = inTexCoord;
}

Every vertex shader must output a gl_Position, which is a 4-component vector. This value determines where the vertex will appear in the viewport. In this example, we just pass the input position right through, but a lot of the time you will be "transforming" this position by multiplying an input by a matrix. The intricacies of 3D projection are far too complex for the scope of this tutorial, so you should read more about that over here if you want to know more.

We also define a custom output value, a 2-component vector we call fragCoord. This value will be consumed by the fragment shader in the pipeline.

Now for the relevant bits. Notice that we have two inputs: a 3-component vector called inPosition, and a 2-component vector called inTexCoord. This is the shape of the data that lives in our vertex buffer. Now we need to tell the graphics pipeline about this.

A "vertex binding" describes a binding index, an "input rate", and a "stride".

The cleverer among you have probably figured out at this point that you could have multiple vertex bindings in the same shader. Why would you do this? The reason is that this allows you to perform a technique called "instancing" which can be a massive speedup when drawing the same data over and over. You can read about instancing here.

The "input rate" describes whether the binding is read in per-vertex or per-instance. If you don't understand what any of this means, don't worry about it yet, this is advanced usage.

Finally, the "stride" value is just the total size of the binding input.

So, for our example shader, the binding looks like this:

var myVertexBindings = new VertexBinding[]
{
    new VertexBinding
    {
        binding = 0,
        inputRate = VertexInputRate.Vertex,
        stride = Marshal.SizeOf<PositionTextureVertex>()
    }
};

We also have a convenient shortcut function that attempts to generate a binding given a vertex struct.

var myVertexBindings = new VertexBinding[]
{
	VertexBinding.Create<PositionTextureVertex>()
};

We need one "vertex attribute" per input value we have defined in our vertex shader, so in this specific case we need two. A "vertex attribute" contains binding value, location, a format, and an offset. The binding index corresponds to the binding index we provided in the vertex binding. Next, notice the location values in our vertex shader. This tells the pipeline that first it will read in the position value, and then it will read in the texcoord value. Next we provide the format. Our position is a vec3 and our texcoord is a vec2, so our formats will be VertexElementFormat.Vector3 and VertexElementFormat.Vector2 respectively. Finally, the offset tells the pipeline the positions of these values relative to the vertex structure, so these values will be 0 and sizeof(float) * 3, respectively. (Note that we can call sizeof here instead of Marshal.SizeOf because float is a built-in C# value.)

For our example shader, the vertex attributes look like this:

var myVertexAttributes = new VertexAttributes[]
{
    new VertexAttribute
    {
        binding = 0,
        location = 0,
        format = VertexElementFormat.Vector3,
        offset = 0
    },
    new VertexAttribute
    {
        binding = 0,
        location = 1,
        format = VertexElementFormat.Vector2,
        offset = sizeof(float) * 3
    }
};

Similar to above, we have a shortcut function for this initialization.

var myVertexAttributes = new VertexAttributes[]
{
	VertexAttribute.Create<PositionTextureVertex>("Position", 0),
	VertexAttribute.Create<PositionTextureVertex>("Texture", 1)
};

Now we can put these arrays in our vertex input state struct.

var myVertexInputState = new VertexInputState
{
    VertexBindings = myVertexBindings,
    VertexAttributes = myVertexAttributes
};

That's it for the vertex input state. If you're slightly mystified right now, don't worry, we'll talk more about this when we discuss commands and binding buffers to the pipeline.