Shaders

Shaders are small programs used to influence the pipeline in a programmable way. The most common shader types are the following:

  • Vertex shader: The first, and required, shader in a conventional rendering pipeline. Determines where on the screen a vertex is, and passes data on to other shaders that follow the vertex shader.
  • Pixel shader: The last shader in a conventional rendering pipeline. The primary purpose of a pixel shader is to assign a color to an individual pixel. The pixel shader is not strictly required, but rendering without it will not render any color. However, depth will be rendered with or without pixel shader, although the pixel shader can influence the depth value output.

The shaders are made in a language called HLSL or High-Level Shading Language. These are compiled to a bytecode format for use by the API using an application called fxc or in the application using the d3dcompiler library.

Initializing a vertex and pixel shader are done as follows:

result = renderer->device->lpVtbl->CreateVertexShader (
    renderer->device,
    vsAsset->data,
    vsAsset->size,
    NULL,
    &renderer->vs
);
if ( !SUCCEEDED ( result ) )
    RETURN_ERROR(-1, "CreateVertexShader failed (0x%08X)", result );

result = renderer->device->lpVtbl->CreatePixelShader (
    renderer->device,
    psAsset->data,
    psAsset->size,
    NULL,
    &renderer->ps
);
if ( !SUCCEEDED ( result ) )
    RETURN_ERROR(-1, "CreatePixelShader failed (0x%08X)", result );

It should be noted that vsAsset and psAsset are custom objects made for this specific application, containing the bytecode created by fxc.

The (simplified) shaders themselves are as follows:

struct ApplicationToVertex
{
    float3 position : POSITION;
    float2 texcoord : TEXCOORD;
};

struct VertexToPixel
{
    float4 position : SV_POSITION;
    float2 texcoord : TEXCOORD;
};

cbuffer PerFrameConstantsVS : register(b0)
{
    float4x4 mvpMat;
};

VertexToPixel mainVS ( ApplicationToVertex IN )
{
    VertexToPixel OUT;
    OUT.position = mul ( mvpMat, float4 ( IN.position, 1.0f ) );
    OUT.texcoord = IN.texcoord;
    return OUT;
}

The above is the vertex shader. For every vertex being rendered, mainVS is being called with properties we are passing from the application. The screen position is then determined by multiplying the position by a model-view-projection matrix. The mathematics for this are outside of the scope of this article, but what should be known is that this matrix is made to convert a position in model-space to clip-space. The clip-space coordinates are then converted to a screen position.

In addition to the position, we are passing a “texcoord” without doing anything with it. This is a parameter we are going to use in the pixel shader, but in order to get it in the pixel shader, we need to pass the parameter in the vertex shader. For any pixel rendered between the vertices, the value of texcoord will be interpolated proportional to the distance of the vertices. What it is used for will be covered in a moment.

struct VertexToPixel
{
    float4 position : SV_POSITION;
    float2 texcoord : TEXCOORD;
};

Texture2D<float4> Tex : register(t0);
SamplerState Sampler : register(s0);

float4 mainPS ( VertexToPixel IN ) : SV_TARGET
{
    return Tex.Sample ( Sampler, IN.texcoord );
}

The above is the pixel shader. For every pixel being rendered, mainPS is being called. This function will take in the interpolated value in between the vertices. The texture specified by Tex and Sampler is a texture and way to sample (get the color at a certain position from) the texture. The texcoord property will be used in order to specify the position where to sample the color from the texture. The color result from this is directly returned in our case, but can be manipulated in any number of ways if desired.