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

4.3 KiB

title date weight
Shader Stage State 2021-01-28T12:15:13-08:00 3

We described shader modules earlier, now we get to put them into practice. A shader stage state has three fields: a shader module, an "entry point name", and a "uniform buffer" size.

Let's have a look at a simple shader example:

#version 450

layout(set = 3, binding = 0) uniform UniformBlock
{
    float time;
} Uniforms;

layout(location = 0) in vec2 fragCoord;

layout(location = 0) out vec4 FragColor;

void main()
{
    // Time varying pixel color
    vec3 col = 0.5 + 0.5 * cos(Uniforms.time + fragCoord.xyx + vec3(0, 2, 4));

    FragColor = vec4(col, 1.0);
}

This shader outputs for each pixel a color value dependent on a time value and the pixel's position on the screen.

Notice the "main" function here. This will be our "entry point". You can name your entry point function anything you want and pass it to the EntryPointName field. You can even have multiple entry points compiled in the same shader module! This is why it is called a "shader module" and not just a shader.

Refresh expects a particular set layout depending on the shader stage.

  • Vertex Sampler -> Set 0
  • Vertex Uniforms -> Set 2
  • Fragment Sampler -> Set 1
  • Fragment Uniforms -> Set 3

The "binding" count should start at 0 and increment by 1 for each binding of that type.

"Samplers" are shader objects that consist of a texture and a filter mode. You can use samplers to do something as simple as display an image, or as complex as sampling depth values to construct a shadow map.

Notice the "UniformBlock" above. This block describes "shader uniforms". Shader uniforms are used to pass in state to the shader that it needs to to do its job when that info doesn't come from the vertex shader.

To elaborate a bit: the graphics pipeline looks like this so far.

Vertex Input -> Vertex Shader -> Rasterizer -> Fragment Shader -> Framebuffer

Notice that our input into this pipeline comes from vertex input. But something like "elapsed time" applies to every pixel we need to shade and doesn't make sense to store with vertex data. So we pass that in as a uniform.

To define our "uniform buffer", we create a C# struct.

using MoonWorks.Math;
using System.Runtime.InteropServices;

namespace MyProject
{
    [StructLayout(LayoutKind.Sequential)]
    public struct ColorPhaseUniforms
    {
        public float Time;
        public Vector3 Padding;
    }
}

Why is that padding vector in there? SPIR-V shaders expect uniform data in 16-byte blocks. So if we just pass in a single float value, which is 4 bytes, SPIR-V will get confused. Also, for this reason, if you ever have a uniform value which is, for example, a Vector3, you will need to add an extra float value as padding. It's a little annoying, I know, but you'll get used to it.

Finally, StructLayout is a C# attribute that tells the runtime how it should lay out the data contained in the struct. Note that, at the end of the day, all data that lives in the computer is just a list of 0s and 1s, and how those 0s and 1s are interpreted depends on context. The layout you provide in the C# struct has to exactly match the order of fields you provide in the shader uniform block or the data will be interpreted incorrectly. It does not use the field names or anything like that. LayoutKind.Sequential means that the fields will appear in the exact order you list them in the struct definition. If you really want to, you can also specify exactly where the fields should go in byte order. by using the FieldOffset attribute above fields. If that makes no sense to you, don't worry about it and just use Sequential.

Back to our shader stage state. The final thing you need to provide is the size of the uniform block. For this I recommend using C#'s Marshal.SizeOf method, which will return the size of the provided struct type in bytes. This means that if you ever change your shader uniform struct in some way, you won't have to remember to update the numbers you provided to your shader stage states.

var myColorPhaseFragmentShaderModule = new ShaderModule(
    GraphicsDevice,
    "ColorPhaseFrag.spv"
);

var myFragmentShaderState = new ShaderStageState
{
    ShaderModule = myColorPhaseFragmentShaderModule,
    EntryPoint = "main",
    UniformBufferSize = Marshal.SizeOf<ColorPhaseUniforms>()
};