HLSL Shaders

Sub Surface Scattering (SS)

This is a real-time subsurface scattering approximation that uses two ingredients: wrap lighting (a soft diffuse falloff) and a light-space depth map that estimates how much material light had to pass through, then attenuates it with Beer's law.

Jade material created using SS.

Wrap lighting: NdotL_wrap = (NdotL + WrapFactor) / (1.0 + WrapFactor) shifts the diffuse falloff so light "wraps" past the terminator instead of hard-clipping at NdotL = 0. It's not physically SSS but classic Half-Lampert.
  • entryDepth comes from the depth map: the distance from the light to the closest surface at that texel, where light first hits the object.
  • fragDepth is this fragment's own distance from the light.
  • totalDepth is the gap between them an estimate of how much material light would have to travel through to reach this point (useful for points on the far side of the mesh, like the inside of an ear or a thin fold).
  • absorption = exp(-totalDepth * Absorption) Beer-Lambert law states the further light travels through the material, the more it's absorbed, so thin areas glow and thick areas go dark.

...
// Depth Map
texture DepthMap;
sampler2D DepthSampler = sampler_state
{
    Texture = ;
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = None;
    AddressU = Clamp;
    AddressV = Clamp;
};

struct VertexShaderInput
{
    float4 Position : POSITION;
    float3 Normal : NORMAL;
};

struct VertexShaderOutput
{
    float4 Position : POSITION;
    float3 WorldPosition : TEXCOORD0;
    float3 Normal : TEXCOORD1;
    float4 LightPosition : TEXCOORD3;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPos = mul(input.Position, World);
    output.WorldPosition = worldPos.xyz;
    output.Normal = normalize(mul(input.Normal, (float3x3) WorldInverseTranspose));

    float4 viewPos = mul(worldPos, View);
    output.Position = mul(viewPos, Projection);

    // Transform to Light Space
    float4 lightView = mul(worldPos, LightViewMatrix);
    output.LightPosition = mul(lightView, LightProjectionMatrix);
    
    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float2 uv = input.LightPosition; // Find the position of light on the UV
    
    float4 sample = tex2D(DepthSampler, uv).a;          // Get the depth from the depth map; 'a' component
    float3 N = normalize(sample.rgb * 2.0f - 1.0f);     // [0, 1] to [-1, 1]
    float entryDepth = sample.a;
    
    // Vectors
    //float3 N = normalize(input.Normal);
    float3 L = normalize(LightPosition - input.WorldPosition);  // Light Direction Vector
    float3 V = normalize(CameraPosition - input.WorldPosition); // View Direction Vector
    float NdotL = dot(N, L);                                    // Dot product between the normal and the light
    
    // Light Wrapping y = (x + wrap) / (1 + wrap)
    float NdotL_wrap = (NdotL + WrapFactor) / (1.0 + WrapFactor);
    float wrap_diffuse = max(0, NdotL_wrap);
    
    // Light Scatter
    float4 scatterColor = float4(ScatterColor, 1.0);
    float scatter = smoothstep(0.0, ScatterWidth, NdotL_wrap) * smoothstep(ScatterWidth * 2.0, ScatterWidth, NdotL_wrap);
    
    // Light Absorption
    float fragDepth = length(mul(float4(input.WorldPosition, 1), LightViewMatrix).xyz);
    float totalDepth = max(0, fragDepth - entryDepth);
    float absorption = exp(-totalDepth * Absorption);   // Beer's Law = e^(-sigma * d)
    
    // Light Components
    float3 ambient = AmbientColor.rgb * AmbientIntensity;
    float3 diffuse = DiffuseColor.rgb * DiffuseIntensity * wrap_diffuse;
    
    // Get the final color
    float3 color = (ambient + diffuse) + scatter * scatterColor.rgb;
    color *= absorption;
    
    return float4(color, 1.0);
}

...

Bump Map

Bump Map & Refraction Shader

bumpNormal = normalize(N + h_u·T + h_v·B)

where T/B/N are the interpolated tangent, binormal, and normal at that point on the surface, and h_u/h_v come straight from the normal map texel. Nudge the base normal toward the tangent and binormal directions, scaled by how much the surface detail leans in each direction."

...
// Normal Map Sampler
sampler NormalMapSamplerLinear = sampler_state      // MipMap = on
{
    texture = ;
    magfilter = LINEAR;         // None, POINT, LINEAR, Anisotropic
    minfilter = LINEAR;
    mipfilter = LINEAR;
    AddressU = Wrap;            // Clamp, Mirror, MirrorOnce, Wrap, Border
    AddressV = Wrap;
};

sampler NormalMapSamplerNone = sampler_state        // MipMap = off
{
    texture = ;
    magfilter = none; // None, POINT, LINEAR, Anisotropic
    minfilter = none;
    mipfilter = none;
    AddressU = Wrap; // Clamp, Mirror, MirrorOnce, Wrap, Border
    AddressV = Wrap;
};

// Skybox Sampler
samplerCUBE SkyBoxSampler = sampler_state
{
    texture = ;
    magfilter = LINEAR;
    minfilter = LINEAR;
    mipfilter = LINEAR;
    AddressU = Mirror;
    AddressV = Mirror;
};

// Input for verticies
struct VertexShaderInput
{
    float4 Position : POSITION0;        // These are registers
    float2 TexCoord : TEXCOORD0;
    float4 Normal : NORMAL0;
    float4 Binormal : BINORMAL0;
    float4 Tangent : TANGENT0;
};

// Ouput for verticies
struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TexCoord : TEXCOORD0;
    float3 Normal : TEXCOORD1;
    float3 Tangent : TEXCOORD2;
    float3 Binormal : TEXCOORD3;
    float3 WorldPos : TEXCOORD4;
};

// Bump Map Self Shadowing
float SselfStep2(float ndot1)
{
    if (ndot1 > 0)
        return 1;
    else
        return 0;
}

float SselfStep3(float ndot1)
{
    float c = 0.125;
    if (ndot1 > c)
        return 1;
    else if (ndot1 > 0)
        return ndot1 / c;
    else
        return 0;
}

...

/// ******* Refractive Bump Mapping ********
VertexShaderOutput RefractiveVertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output; // output with diffuse applied to send to GPU
    
    // Matrix multiplication sequence (Projection * (View * (Object * World)))
    float4 worldPosition = mul(input.Position, World); // Point position x', y', x', w'
    float4 viewPosition = mul(worldPosition, View); // View Point x", y", z", w"
    output.Position = mul(viewPosition, Projection);
    
    output.Normal = normalize(mul(input.Normal, World).xyz); // Normal Vector
    output.Tangent = normalize(mul(input.Tangent, World).xyz); // Tangent Vector
    output.Binormal = normalize(mul(input.Binormal, World).xyz);
    
    output.WorldPos = worldPosition.xyz;
    output.TexCoord = input.TexCoord * float2(NormalMapRepeatU, NormalMapRepeatV);
    return output;
}

float4 RefractivePixelShaderFunction(VertexShaderOutput input) : SV_Target
{
    float3 texColor;
    if (MipMap == 0)        // MipMap
        texColor = tex2D(NormalMapSamplerNone, input.TexCoord).xyz;
    else
        texColor = tex2D(NormalMapSamplerLinear, input.TexCoord).xyz;
    
    texColor -= 2.0 * float3(0.5, 0.5, 0.5);
    
    // Bump Height; adjust x,y,z comp to change height of map
    texColor.x *= (1 + 0.2 * (BumpHeight - 5));
    texColor.y *= (1 + 0.2 * (BumpHeight - 5));
    texColor.z *= (1 + 0.2 * (BumpHeight - 5));
    
    // Vectors
    float3 L = normalize(LightPosition - input.WorldPos.xyz); // Light vector
    float3 N = normalize(input.Normal); // Normal
    float3 T = normalize(input.Tangent); // Tangent
    float3 B = normalize(input.Binormal); // Binormal
    
    // N' = norm(N + h_u(N_n x P_v) + h_v(P_u x N_n))
    float3 bumpNormal = normalize(N + texColor.x * T + texColor.y * B);
    
    float Sself = 1; //default is 1
    if (SelfShadow == 1)
        Sself = SselfStep2(dot(N, L));
    else if (SelfShadow == 2)
        Sself = SselfStep3(dot(N, L));
    
    float4 LightColor = 1;
    
    // Calc ambient
    float4 ambient = AmbientColor * AmbientIntensity;
    
    // Calc diffuse component
    float diffuse = Sself * saturate(dot(L, bumpNormal));
    float4 decalColor = diffuse * DiffuseColor * DiffuseIntensity;
    
    // Calc specular component
    float3 V = normalize(CameraPosition - input.WorldPos);
    
    // Calc view vector
    float3 H = normalize(L + V); //halfway vector
    float specular = saturate(dot(H, bumpNormal));
    specular = Sself * pow(specular, Shininess * 3);
    decalColor += specular * SpecularColor * SpecularIntensity;
    
    decalColor.a = 1;
    
    //float3 reflectNorm = normalize(mul(bumpNormal, WorldInverseTranspose).xyz); // Normal Vector
    float3 I = normalize(input.WorldPos.xyz - CameraPosition); // Incident Vector
    
    float3 R = refract(I, bumpNormal, reflectivity);
    
    // texCUBE -> Calculates the mapping coordinates on the Skybox
    float4 refractedColor = texCUBE(SkyBoxSampler, R);
    
    // Linearly interpolate the model texture and the env map texture by a float value
    return lerp(decalColor, refractedColor, 0.5);
}

Toon

This toon shader implements cel shading - collapsing a continuous lighting term into a small number of flat color bands instead of a smooth gradient. The vertex shader here does nothing but transform the vertex and pass world normal straight through, all the actual shading decision happens in the pixel shader.

Toon shader.

N, V, L, and R are built exactly the same way as a Phong shader, R = reflect(-L, N) is the mirror-reflection of the light off the surface. This shader takes that same dot(V, R) as the Phong specular value and runs it through a four-way ladder that snaps it to one of four fixed grayscale outputs — black, 0.25, 0.5, or 1.0 — with no interpolation between them.
// Input for verticies
struct VertexShaderInput
{
    float4 Position : POSITION;
    float4 Normal : NORMAL;
};

// Ouput for verticies 
struct VertexShaderOutput
{
    float4 Position : POSITION;
    float4 Color : COLOR;
    float4 Normal : TEXCOORD0;
    float4 WorldPosition : TEXCOORD1;
};

...

VertexShaderOutput ToonVertexShaderFunction(VertexShaderInput input)       // Calculate only the data
{
    VertexShaderOutput output; // output with diffuse applied to send to GPU
    
    float4 worldPosition = mul(input.Position, World); // Point position x', y', x', w'
    float4 viewPosition = mul(worldPosition, View); // View Point x", y", z", w"
    output.Position = mul(viewPosition, Projection);
    
    // Used as input for the pixel shader function
    output.WorldPosition = worldPosition;
    output.Normal = input.Normal;
    output.Color = 0;
    
    return output;
}
float4 ToonPixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float3 N = normalize(mul(input.Normal, WorldInverseTranspose).xyz); // Normal Vector
    float3 V = normalize(CameraPosition - input.WorldPosition.xyz); // View Vector
    float3 L = normalize(lightPosition) * lightIntensity; // Light Vector
    float3 R = reflect(-L, N); // Reflection Vector

    // Simple color palette that will change based on the angle between the view vector and reflection vector
    float D = dot(V, R);
    if (D < -0.7)
    {
        return float4(0, 0, 0, 1); // Color 1: Black
    }
    else if (D < 0.2)
    {
        return float4(0.25, 0.25, 0.25, 1); // Color 2: Dark Grey
    }
    else if (D < 0.6)
    {
        return float4(0.5, 0.5, 0.5, 1); // Color 3: Light Grey
    }
    else
    {
        return float4(1, 1, 1, 1); // Color 4: White
    }
}

Reflection

Reflection shader.

Refraction

Refraction Shader

Phong Shading

Phong shading model.