S&box Wiki

Shading Model

What is a shading model

A shading model is what controls the lighting & shading of your surface. By default, Two different shading models are provided, ShadingModelStandard which is the default, and ShadingModelValveStandard. Not only can you use one of these pre-defined shading models, but you can also create your own shading model to shade your surface the way you want.

Using built-in shading models

Invoking and using your shading model is done by calling the FinalizePixelMaterial function, which is defined automatically when you include the common/pixel.hlsl file within your pixel shader.

By default, if you do not provide a shading model to the FinalizePixelMaterial call, it will use the ShadingModelStandard. For example:

// PixelInput, Material return FinalizePixelMaterial( i, m );

If you wish to use the ShadingModelValveStandard, it will need to be passed in as the third argument to FinalizePixelMaterial like so.

ShadingModelValveStandard sm; // PixelInput, Material, Shading Model return FinalizePixelMaterial( i, m, sm );

Creating a shading model

Structure

All shading models contain 4 basic methods which need to be implemented. Init, which is called once the shading model is initialized for the first time. Direct is called for every single direct light which is currently visible, Indirect is called once to calculate indirect lighting & PostProcess which allows tweaking of the shading after everything is done, for example adding fog. To begin implementing any of these methods, you will need to create a class & inherit from ShadingModel. An empty shading model can be seen below:

class ShadingModelExample : ShadingModel { // // Execute before anything // void Init( const PixelInput pixelInput, const Material material ) { } // // Executed for every direct light // LightShade Direct( const LightData light ) { LightShade shade; shade.Diffuse = float3( 0.0f, 0.0f, 0.0f ); shade.Specular = float3( 0.0f, 0.0f, 0.0f ); return shade; } // // Executed for indirect lighting, combine ambient occlusion, etc. // LightShade Indirect() { LightShade shade; shade.Diffuse = float3( 0.0f, 0.0f, 0.0f ); shade.Specular = float3( 0.0f, 0.0f, 0.0f ); return shade; } // // Applying any post-processing effects after all lighting is complete // float4 PostProcess( float4 vColor ) { return vColor; } };

Light Data

Type Name Description
float3 Color The color is an RGB value in the linear sRGB color space.
float3 LightDir The normalized light vector, in world space (direction from the current fragment's position to the light).
float NdotL The dot product of the shading normal (with normal mapping applied) and the light vector. This value is equal to the result of saturate(dot(getWorldSpaceNormal(), lightData.l)). This value is always between 0.0 and 1.0. When the value is <= 0.0, the current fragment is not visible from the light and lighting computations can be skipped.
float3 PositionWs The position of the light in world space.
float Attenuation Attenuation of the light based on the distance from the current fragment to the light in world space. This value between 0.0 and 1.0 is computed differently for each type of light (it's always 1.0 for directional lights).
float Visibility Visibility factor computed from shadow maps or other occlusion data specific to the light being evaluated. This value is between 0.0 and 1.0.

Usage

Once the shading model is created, it can be used like any other shading model. It's first declared and then passed into FinalizePixelMaterial like so:

float4 MainPs( PixelInput i ) : SV_Target0 { Material m = GatherMaterial( i ); ShadingModelExample sm; return FinalizePixelMaterial( i, m, sm ); }

Finished Example

image.png
PS { #include "common/pixel.hlsl" // // Main // class ShadingModelExample : ShadingModel { float3 Albedo; float3 NormalWs; float3 ViewRayWs; // // Consumes a material and converts it to the internal shading parameters, // That is more easily consumed by the shader. // // Inherited classes can expand this to whichever shading model they want. // void Init( const PixelInput pixelInput, const Material material ) { // Keep track of our albedo & normal Albedo = material.Albedo; NormalWs = material.Normal; float3 PositionWithOffsetWs = pixelInput.vPositionWithOffsetWs.xyz; float3 PositionWs = PositionWithOffsetWs + g_vCameraPositionWs; // View ray in World Space ViewRayWs = CalculatePositionToCameraDirWs( PositionWs ); } // // Executed for every direct light // LightShade Direct( const LightData light ) { // Shading output LightShade shade; // Compute our shadow mask float flShadow = light.NdotL * light.Visibility * light.Attenuation; // Make it hard instead of a soft transition flShadow = step(0.001f, flShadow); // Calculate everything we need for specular float3 vHalfAngleDirWs = normalize(ViewRayWs + light.LightDir); float flNdotH = dot( vHalfAngleDirWs.xyz, NormalWs ); // Sharpen our specular float flSpecular = pow(flNdotH * flShadow, 100.0f); // Smooth it out a little bit flSpecular = smoothstep(0.005, 0.01, flSpecular); // Diffuse lighting for the current light shade.Diffuse = saturate(flShadow * light.Color) * g_flTintColor; // Calculate our specular for the current light shade.Specular = flSpecular * Albedo * g_flTintColor; return shade; } // // Executed for indirect lighting, combine ambient occlusion, etc. // LightShade Indirect() { LightShade shade; // Get a flat average ambient float3 vAmbientCube[6]; SampleLightProbeVolume( vAmbientCube, float3(0,0,0) ); // Light with our ambient color float3 flColor = 0.0f; for(int i = 0; i < 6; i++) flColor += vAmbientCube[i] * (1.0f / 6.0f); shade.Diffuse = flColor * Albedo; // No specular shade.Specular = 0.0f; return shade; } float4 PostProcess( float4 vColor ) { // We don't need any post processing! return vColor; } }; float4 MainPs( PixelInput i ) : SV_Target0 { Material m = GatherMaterial( i ); ShadingModelExample sm; return FinalizePixelMaterial( i, m, sm ); } }