Vertex Shaders & Transformations

Non-uniform scale skews normals: transforming them by the world matrix makes them non-perpendicular to the surface.

16.1 The world-view-projection transform

// common.hlsli
cbuffer PerFrame : register(b0)
{
    float4x4 gViewProj;
    float3   gCameraPos;
    float    gTime;
};
cbuffer PerObject : register(b1)
{
    float4x4 gWorld;
    float4x4 gWorldInvTranspose;     // for normals under non-uniform scale
};

struct VSIn
{
    float3 pos     : POSITION;
    float3 normal  : NORMAL;
    float4 tangent : TANGENT;        // w = handedness (±1)
    float2 uv      : TEXCOORD0;
};

struct VSOut
{
    float4 posH     : SV_Position;
    float3 posW     : POSITION_WS;
    float3 normalW  : NORMAL;
    float4 tangentW : TANGENT;
    float2 uv       : TEXCOORD0;
};

// Convention in this course: column vectors, mul(M, v), matrices uploaded as GLM/transposed DirectXMath.
VSOut VSMain(VSIn i)
{
    VSOut o;
    float4 posW  = mul(gWorld, float4(i.pos, 1.0));
    o.posW       = posW.xyz;
    o.posH       = mul(gViewProj, posW);
    o.normalW    = normalize(mul((float3x3)gWorldInvTranspose, i.normal));
    o.tangentW   = float4(normalize(mul((float3x3)gWorld, i.tangent.xyz)), i.tangent.w);
    o.uv         = i.uv;
    return o;
}

16.2 Building matrices in HLSL (normally done on the CPU with DirectXMath)

float4x4 Translation(float3 t)
{
    return float4x4(1, 0, 0, t.x,
                    0, 1, 0, t.y,
                    0, 0, 1, t.z,
                    0, 0, 0, 1);      // column-vector convention: translation in the 4th column
}

float3x3 RotationY(float a)
{
    float s, c; sincos(a, s, c);
    return float3x3( c, 0, s,
                     0, 1, 0,
                    -s, 0, c);
}

// Left-handed perspective, D3D depth [0, 1]
float4x4 PerspectiveLH(float fovY, float aspect, float zn, float zf)
{
    float y = 1.0 / tan(fovY * 0.5);
    float x = y / aspect;
    float q = zf / (zf - zn);
    return float4x4(x, 0, 0, 0,
                    0, y, 0, 0,
                    0, 0, q, -q * zn,
                    0, 0, 1, 0);
}

16.3 Why the inverse-transpose for normals?

Non-uniform scale skews normals: transforming them by the world matrix makes them non-perpendicular to the surface. The inverse-transpose of the upper 3×3 preserves perpendicularity. For rotation + uniform scale only, (float3x3)gWorld followed by normalize is enough.

16.4 Vertex animation

VSOut WavingFlagVS(VSIn i)
{
    float3 p = i.pos;
    float wave = sin(p.x * 4.0 + gTime * 3.0) * 0.1 * i.uv.x;   // anchored at uv.x = 0
    p.z += wave;
    // recompute normal from the analytic derivative dz/dx
    float dzdx = cos(p.x * 4.0 + gTime * 3.0) * 0.4 * i.uv.x;
    float3 n = normalize(float3(-dzdx, 0, 1));

    VSOut o = (VSOut)0;
    float4 posW = mul(gWorld, float4(p, 1));
    o.posW = posW.xyz; o.posH = mul(gViewProj, posW);
    o.normalW = normalize(mul((float3x3)gWorldInvTranspose, n));
    o.uv = i.uv;
    return o;
}

16.5 Billboards (camera-facing quads)

cbuffer Camera : register(b2) { float3 gCamRight; float pad0; float3 gCamUp; float pad1; };

float4 BillboardVS(uint vid : SV_VertexID, float3 center : POSITION, float size : PSIZE) : SV_Position
{
    float2 corner = float2(vid & 1, vid >> 1) * 2 - 1;          // 4-vertex strip: (-1,-1)(1,-1)(-1,1)(1,1)
    float3 posW = center + (gCamRight * corner.x + gCamUp * corner.y) * size;
    return mul(gViewProj, float4(posW, 1));
}