Skip to main content

Zhuangdong's Technical Art Introduction Course - Forward Rendering

4090 words·20 mins
Table of Contents

01 General Rendering Process
#

Rendering Pipeline
#

flowchart LR
    Model --> InputStructure
    InputStructure --> VertexShader
    VertexShader --> OutputStructure
    OutputStructure --> PixelShader

Model
#

Contains vertex information (e.g., v 1.0 1.0 -1.0, ID determined by order), triangle face information (e.g., f 5 3 1, numbers are vertex IDs), as well as UV, normals, vertex colors, etc.

Input Structure
#

Select the model information that needs to be used.

Vertex Shader
#

Process input information, convert each vertex position to screen space, and calculate/assign other per-vertex information (such as UV, vertex colors, normals, etc.).

Output Structure
#

Output specified vertex information.

Pixel Shader
#

Combine environment, lighting, camera, etc., to output the final rendering result.

Lighting Model
#

Taking the Lambert lighting model as an example, it is the result of the dot product of two vectors.

Vectors
#

For detailed explanation of vectors, please refer to this chapter:

Dot Product
#

The dot product result of the main light source vector and normal vector is in the [-1,1] range, determining the light-dark boundary.

L01_Lambert

Lambert
#

max(0, nDir·lDir), only takes positive values.

Half Lambert
#

Lambert * 0.5 + 0.5, makes the dark areas softer.

Code
#

float lambert = max(0.0, nDotl);
float2 uv = float2(lambert, 0.0);
float4 var_MainTex = tex2D(_MainTex, uv);
return float4(var_MainTex);

02 Case Studies
#

Normal Offset Creating Multiple Highlights
#

Using normal offset to create multiple highlight areas, enriching the highlight performance on object surfaces.

L02_Normaloffset02

L02_Normaloffset01

Screen UV & Depth Value
#

By multiplying screen UV with depth value, textures can be attached to object surfaces and always face the camera.

L02_Screendepth

Algorithm Combination
#

Texture Node
#

Multiply screen coordinates by tiling count, take the decimal part and limit it to the [-0.5, 0.5] range, then connect to the Length node.

L02_Noise

Power
#

Perform power operation on values to adjust the shape of highlights or other effects.

L02_Noise_Process

Length
#

Length = √(x^2 + y^2 + z^2), when x=y, the result is √0.5 when x=-0.5 or 0.5, and 0 when x=0, forming a periodic cycle.

L02_Length

03 Basic Code Material FlatCol
#

FlatCol represents a single-color rendering result. Previous cases were made using ShaderForge node method, but here we start implementing materials with code. Code-implemented materials have advantages in performance and flexibility because the final output instructions are more streamlined.

L03_FlatCol

Code Example
#

Shader "Zhuangdong/AP1/L02/Lambert_U_01" {
    Properties{}
    SubShader{
        Tags { "RenderType" = "Opaque" }
        Pass {
            Name "FORWARD"
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM//Shader compilation directive
            #pragma vertex vert//Declare vertex shader
            #pragma fragment frag//Declare fragment shader
            #include "UnityCG.cginc"//Include Unity built-in CG library
            #pragma multi_compile_fwdbase_fullshadows//Enable shadow functionality
            #pragma target 3.0//Specify shader model version
            //The above are similar to context, but with more functionality.
            //Declare vertex shader and fragment shader, so the vert, frag functions below can be enabled.
            //You can also choose to enable and disable features, such as enabling shadow functionality. And specify shader model version
            struct VertexInput { 
            //Import model vertex information and normal information
            //Words like POSITION NORMAL can be found in UNITY official Document to see their meaning
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            struct VertexOutput {
            //Convert the above model vertex information to vertex screen position
            //Convert model normal information to world space normal information
                float4 pos : SV_POSITION;
                float3 nDirWS : TEXCOORD0;
            };
            VertexOutput vert(VertexInput v) {
            //Fixed vertex shader writing: v2f vert (appdata v), Output vert (input variable name)
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos(v.vertex);//Convert model vertex position to vertex screen position
                o.nDirWS = UnityObjectToWorldNormal(v.normal);//Convert normal to world normal
                return o;
            }
            float4 frag(VertexOutput i) : COLOR{
            //Fixed fragment shader writing
                float3 nDir = i.nDirWS;
                float3 lDir = normalize(_WorldSpaceLightPos0.xyz);
                //0 represents directional light, xyz represents direction, 1 represents point light, xyz represents point light coordinates
                //normalize ensures rendering results don't go wrong
                float nDotl = (dot(nDir, lDir)*0.5+0.5);//halflambert
                float lambert = max(0.0, nDotl);
                //Some mobile devices treat 0 as 0.1 or similar decimals, so it's better to write 0.0, otherwise errors will occur.
                //Using max instead of clamp is because clamp limits both ends, max limits one end, more performance efficient, because dot product won't be greater than 1
                return float4(lambert, lambert, lambert, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Custom Material Cases:
#

  1. Use float4 to customize light direction, implementing custom Lambert model
  2. Lambert model multiplied by float4 can customize light color
  3. Lambert model multiplied by float can customize light intensity

L03_CustomLambert

04 Case Studies
#

SSS Material
#

The core idea of this case is to use textures and parameters together to control the color and range of light-dark boundaries. The first half of the texture U-axis is black, and the second half gradually changes from dark to bright. By adjusting parameters, the color and range near the light-dark boundary line can be changed arbitrarily. This effect is commonly used to simulate the translucency of biological skin, called SSS effect.

L04_SSS

Color Generated Mask
#

This case uses the step function to layer the lighting model. The step function can turn grayscale images into black and white masks. Use custom colors, mix R/G/B channels with Lambert respectively, generate different masks, and finally mix all masks for output.

L04_Colormask02

L04_Colormask01

Special Case
#

This case mixes textures and lighting models, then partitions the mixed results through Round to obtain custom results.

  1. Use two groups of textures with different Tiling values for mixing, then adjust grayscale to get the final texture
  2. Use Half Dir and normal dot product to get a special lighting model that can make lighting change with camera
  3. Mix grayscale image and lighting model, then process the result with round node to get only black and white boundaries
  4. Finally assign different colors to black and white areas respectively

L04_Customwork

Pre-Integrated Skin Shading
#

LUT (Look Up Texture) principle brief description

$$ D(\theta) = \frac{\int_{-\pi}^{\pi} \cos(\theta + x) \cdot R(2\sin(x/2))dx}{\int_{-\pi}^{\pi} R(2\sin(x/2))dx} $$

L04_LUT

05 Diffuse and Specular Reflection
#

Diffuse Reflection
#

Lambert, nDotl, direction independent. Real-world examples include movie screens.

Specular Reflection
#

Phong/Blinn-Phong, rDotv/nDoth, view direction dependent. Real-world examples include car paint.

L05_Phong

Common Vectors
#

  • nDir Normal direction
  • lDir Light direction
  • vDir View direction
  • rDir Light reflection direction, r = reflect(-l, n)
  • hDir Half-angle direction, vector between ldir and vdir
  • nDoth The closer hDir and nDir are, the closer the output value is to 1

Highlight Adjustment
#

Power = Value^Exp is a formula commonly used in Phong model, controlling highlight range through Exp

Case
#

Combining diffuse and specular reflection

// Fragment shader example
float3 nDir = normalize(i.nDirWS);
float3 lDir = _WorldSpaceLightPos0.xyz;
float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
float3 rDir = reflect(-lDir, nDir);
float rDotv = dot(rDir, vDir);
float nDotl = dot(nDir, lDir);
float Lambert = nDotl * 0.5 + 0.5;
float Phong = pow(max(0.0, rDotv), _SpecPow);
float3 FinalColor = _BaseColor * Lambert + Phong;
return float4(FinalColor, 1.0);

Note: TEXCOORD is equivalent to register slots reserved for developers in the GPU pipeline, default is float4

  • Register placeholder: Marks the input position of vertex data in the shader
  • Purpose: Pass UV and custom vector data

06 FakeEnvReflect/Grape/Painted Metal/BRDF
#

FakeEnvReflect
#

This case maps the grayscale values of cloud2 texture to U coordinates, implementing random grayscale distribution applied to lighting effects.

L06_FakeEnv

Grape
#

The case uses Lambert lighting model and LUTRampTex as diffuse reflection, while the highlight part uses Phong model. Finally, adjust color and highlight changes through cloud2 texture.

L06_Grape

Painted Metal
#

This case uses cloud2 to generate masks, lerp mixing different material performances. One part has specular reflection, the other doesn’t.

L06_PaintDrop

BRDF
#

Can be understood as a function that inputs light, view angle and surface parameters, outputs reflection distribution. Lambert and Phong are common BRDF models.

BRDFExplorer
#

Can customize parameters, create your own BRDF, and view source code of common Shaders.

07 3ColAmbient and Shadow
#

Three-Color Ambient Light
#

This case uses the three channels of normal to create Top/Side/Bottom three-layer masks. Used to simulate the influence of three different directions of ambient light on the lighting model.

float3 nDir = i.nDirWS;
float TopMask = max(0.0, nDir.g);
float BotMask = max(0.0, -nDir.g);
float MidMask = 1 - TopMask - BotMask;
float3 EnvCol = _TopCol * TopMask + _MidCol * MidMask + _BotCol * BotMask;
float AO = tex2D(_AoMap, i.uv);
float3 EnvLighting = EnvCol * AO;
return float4(EnvLighting, 1.0);

L07_3Col

Shadow
#

Add shadows to the lighting model

//Add shadows to Shader
struct VertexOutput { 
...
LIGHTING_COORDS(3,4)};//Add LIGHT_COORDS to VertexOutput
VertexOutput vert(VertexInput v) { 
VertexOutput o = (VertexOutput)0;
...
TRANSFER_VERTEX_TO_FRAGMENT(o)//Add TRANSFER_VERTEX_TO_FRAGMENT to vertex shader
...
return o;}
float4 frag(VertexOutput i) : COLOR{
float shadow = LIGHT_ATTENUATION(i);//Add LIGHT_ATTENUATION to fragment shader
...}

L07_Shadow

Lighting Composition
#

flowchart LR
    Lighting --> LightSource
    LightSource --> DiffuseHalfLambert
    LightSource --> SpecularPhong
    DiffuseHalfLambert --> OcclusionShadow
    SpecularPhong --> OcclusionShadow
    OcclusionShadow --> Result
    Lighting --> Environment
    Environment --> Diffuse1Col
    Environment --> SpecularCubemap
    Diffuse1Col --> OcclusionNoAO
    SpecularCubemap --> OcclusionNoAO
    OcclusionNoAO --> Result
    RimLight --> Result
    Emission --> Result

OldSchool case is the output result of the above lighting composition

L07_OldSchool

08 NormalMap Implementation Principle
#

L08_TBN

TBN: Tangent(Red), Bitangent(Blue), Normal(Green)

Normal maps record normal orientations in model space, which need to be converted to world space for normal display. Use TBN matrix to convert normal maps to world space. Conversion steps are as follows:

  1. Sample normal map and decode
  2. Construct TBN matrix
  3. Convert tangent space normal to world space
  4. Output world space normal
VertexOutput vert(VertexInput v) { 
    VertexOutput o = (VertexOutput)0;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv0 = v.uv0;
        o.posWS = mul(unity_ObjectToWorld, v.vertex);
        o.nDirWS = UnityObjectToWorldNormal(v.normal); 
        o.tDirWS = normalize( mul( unity_ObjectToWorld, float4( v.tangent.xyz, 0.0 ) ).xyz );//Tangent direction OStoWS
        o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);//Calculate bDir based on nDir tDir
        TRANSFER_VERTEX_TO_FRAGMENT(o)
    return o;
}
float4 frag(VertexOutput i) : COLOR{
    //Vectors
    float3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).rgb;
    float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);//TBN matrix
    float3 nDirWS = normalize(mul(nDirTS, TBN));//Convert to world space

09 Fresnel, Matcap, Cubemap
#

Fresnel
#

Edge glow phenomenon, formula is nDotv, outputs 1 when nDir and vDir are perpendicular, outputs 0 when coincident, visually appears as model edge highlighting

L09_Fresnel

Matcap
#

View space normal mapping BRDF rendering result, only suitable for static display, Zbrush preview interface uses Matcap.

L09_Matcap

The texture is circular because MatcapMap takes the RG channels of nDirVS as mapping values, and the normal vector satisfies the following formula:

$$ x^2 + y^2 + z^2 = 1 $$

When only taking xy, the mapped result will be a circle

L09_Matcap_Map

Cubemap
#

Panoramic image, mapping the environment around the camera to a Cube. Mipmap shows different levels of clarity

float3 vrDirWS = reflect(-vDirWS, nDirWS);
float3 cubemap = texCUBElod(_CubeMap, float4(vrDirWS, _CubemapMip));

L09_Cubemap

L09_Cubemap_Map

10 Live Q&A
#

Modified PBR
#

One of the origins of PBR is the generalization of surface parameters. The more Shader functions are added, the more panel parameters there are. Finding physical relationships between parameters to reduce panel parameters is one of the contents of PBR. Modifying based on PBR’s physics-based core is called modified PBR. Transplanting some PBR BRDF or texture names to traditional models is still traditional models, not called modified PBR.

The significance of mobile PBS physics-based core:

  1. Physics-based energy conservation
  2. Physics-based surface property generalization
  3. Microsurface theory

Changing UV
#

Normal maps record normal information in tangent space. The main and secondary tangent directions in tangent space can be intuitively understood as the UV axis directions of the texture at the surface. When the model doesn’t change but tangent space changes, the recorded normal information will also change.

11 Common Parameters
#

Common Parameters and Functions
#

  • Values, ranges
    • _Name (“Name”, float) = defaultVal
    • _Name (“Name”, range(min, max)) = defaultVal
    • _Name (“Name”, int) = defaultVal
  • Position, vector, color
    • _Name (“Name”, vector) = (xVal, yVal, zVal, wVal)
    • _Name (“Name”, color) = (rVal, gVal, bVal, aVal)
  • 2D, 3D textures, environment sphere
    • _Name (“Name”, 2d) = “defaultVal” {}
    • _Name (“Name”, 3d) = “defaultVal” {}
    • _Name (“Name”, cube) = “defaultVal” {}
  • [HideInInspector]
    • Purpose: Hide this parameter in the panel
    • Can be used for: Any parameter
    • [HideInInspector] _FakeLightDir(“Fake light direction”, vector) = (0.0, 1.0, 0.0, 1.0)
  • [NoScaleOffset]
    • Purpose: Disable texture TilingOffset panel
    • Can be used for: Texture parameters
    • [NoScaleOffset] _MainTex (“Main texture”, 2d) = “white” {}
  • [Normal]
    • Purpose: Mark this texture parameter as normal map to activate related self-check functions
    • Can be used for: 2D texture parameters
    • Example: [Normal] _NormTex (“Normal map”, 2d) = “bump” {}
  • [HDR]
    • Purpose: Used to set high dynamic range color values; such as: light colors, emission colors, etc.
    • Can be used for: Color parameters
    • [HDR] _EmitCol (“Emission color”, color) = (1.0, 1.0, 1.0, 1.0)
  • [Gamma]
    • Purpose: Used for color space conversion of color parameters; generally used for projects with Linear color space
    • Can be used for: Color parameters
    • Example: [Gamma] _EmitCol (“Emission color”, color) = (1.0, 1.0, 1.0, 1.0)
  • [PowerSlider(value)]
    • Purpose: Perform Power processing on range parameters before passing to Shader; correct some parameter adjustment feel
    • Example: [PowerSlider(0.5)] _SpecPow (“Specular power”, range(1,90)) = 30
  • [Header(Label)]
    • Purpose: Label, used for layout
    • Can be used for: Standalone use
    • Example: [Header(Texture)]
  • [Space(value)]
    • Purpose: Empty line, used for layout
    • Can be used for: Standalone use
    • Example: [Space(50)]

Common Parameter Types
#

  • fixed: 11-bit fixed point, -2.0 ~ 2.0, precision 1/256
  • half: 16-bit floating point, -60000 ~ 60000, precision about 3 decimal places
  • float: 32-bit floating point, -3.4E38 ~ 3.4E28, precision about 6, 7 decimal places
  • int: 32-bit integer, rarely used
  • bool: Boolean type, rarely used
  • Matrices:
    • float2x2, float3x3, float4x4, float2x3 and similar formats
    • half2x2, half3x3, half4x4, half2x3 and similar formats
  • Texture objects:
    • sampler2D: 2D texture
    • sampler3D: 3D texture
    • samplerCUBE: Cube texture

Parameter Usage Methods
#

In principle, prioritize using the lowest precision data type

Experience
#

  • World space positions and UV coordinates, use float
  • Vectors, HDR colors, use half; upgrade to float as needed
  • LDR colors, simple multipliers, can use fixed

Important Notes
#

  • Different platforms have different support for data types; generally automatic conversion, very rarely automatic conversion brings problems
  • On some platforms, data type precision conversion consumption is not small; so fixed should be used carefully
  • Discuss more with graphics developers

Accessible Vertex Input Data
#

  • POSITION Vertex position float3 float4
  • TEXCOORD0 UV channel 1 float2 float3 float4
  • TEXCOORD1 UV channel 2 float2 float3 float4
  • TEXCOORD2 UV channel 3 float2 float3 float4
  • TEXCOORD3 UV channel 4 float2 float3 float4
  • NORMAL Normal direction float3
  • TANGENT Tangent direction float4
  • COLOR Vertex color float4

Common Vertex Output Data (more customizable than the former)
#

  • pos Vertex position CS float4
  • uv0 General texture UV float2
  • uv1 LightmapUV float2
  • posWS Vertex position WS float3
  • nDirWS Normal direction WS half3
  • tDirWS Tangent direction WS half3
  • bDirWS Bitangent direction WS half3
  • color Vertex color fixed4

Common Vertex Shader Operations
#

Note: Unity2019.3.2f1 version

  • pos UnityObjectToClipPos(v.vertex);

  • uv0 o.uv0 = v.uv1; o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex_ST);

  • uv1 o.uv1 = v.uv1; o.uv1 = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;

  • posWS mul(unity_ObjectToWorld, v.vertex);

  • nDirWS UnityObjectToWorldNormal(v.normal);

  • tDirWS normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);

  • bDirWS normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);

  • color o.color = v.color;

  • Enable texture tiling

uniform sampler2D _MainTex; uniform float4 _MainTex_ST
...
VertexOutput vert(VertexInput v) { 
...
o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex);& o.uv0 = v.uv0 * _MainTex_ST.xy + _MainTex_ST.zw;
...}
  • Function modularization, code reuse

    • Original writing
    float TopMask = max(0.0, nDirWS.g);
    float BotMask = max(0.0, -nDirWS.g);
    float MidMask = 1 - TopMask - BotMask;
    float3 EnvCol = _TopCol * TopMask +
                    _MidCol * MidMask +
                    _BotCol * BotMask;
    
    • Modified version
    float3 TriColAmbient (float3 n, float3 uCol, float3 sCol, float3 dCol){
        float uMask = max(0.0, n.g);
        float dMask = max(0.0, -n.g);
        float sMask = 1.0 - uMask - dMask;
        float3 envCol = uCol * uMask +
                        sCol * sMask +
                        dCol * dMask;
        return envCol;
    }
    ...
    float3 envCol = TriColAmbient(nDirWS, _EnvUpCol, _EnvSideCol, _EnvDownCol);
    ...
    
    • Modularization

    Create Assets\Cgnic\MyCgnic.cgnic

    #ifndef MY_CGINC
    #define MY_CGINC
    float3 TriColAmbient (float3 n, float3 uCol, float3 sCol, float3 dCol){
    float uMask = max(0.0, n.g);
    float dMask = max(0.0, -n.g);
    float sMask = 1.0 - uMask - dMask;
    float3 envCol = uCol * uMask +
                    sCol * sMask +
                    dCol * dMask;
    return envCol;}
    #endif
    

    Add path under Shader CGPROGRAM

    ...
    CGPROGRAM
    ...
    #include "../cginc/MyCginc.cginc"
    ...
    float3 envCol = TriColAmbient(nDirWS, _EnvUpCol, _EnvSideCol, _EnvDownCol);
    

12 Ogre Case
#

This case references https://support.steampowered.com/kb/3081-QUXN-6209/dota-2-workshop-item-shader-masks?l=finnish , combined with actual resources to create a typical character Shader.

L12_Ogre

Code Example
#

Shader "Zhuangdong/AP1/L06/L06_ogre_Feedback" {
    Properties{
        [Header(Texture)]
        _MainTex ("RGB:Base color A:Transparency texture", 2D) = "White" {}
        [Normal] _NormalMap ("RGB:Normal map", 2D) = "bump" {}
        _DetailMap ("RGB:Detail texture A:Detail mask", 2D) = "black" {}
        _MetalnessMask ("Metal mask", 2D) = "black" {}
        _SelfIllMask ("SelfIll emission mask", 2D) = "black" {}
        _SpecTex ("RGB:Spec highlight texture", 2D) = "gray" {}
        _RimLight ("Rim edge light mask", 2D) = "black" {}
        _BaseTintMask ("Tint base color mask", 2D) = "White" {}
        _SpecularExponent ("SpecExpo specular reflection exponent", 2D) = "White" {}
        _DiffuseWarp ("Diffuse diffusion mask", 2D) = "black" {}
        _CubeMap ("RGB:Cube environment texture", Cube) = "_Skybox" {}
        _FresnelWarp("Fresnel texture R:FCol G:FRim B:FSpec", 2D) = "black" {}
        [Header(Diffuse)]
        _LightCol ("Main light color", Color) = (1.0,1.0,1.0,1.0)
        [Space(10)]
        _TopCol ("Top color", Color) = (0.47,0.96,1,1)
        _MidCol ("Middle color", Color) = (0.46,0.7,0.45,1)
        _BotCol ("Bottom color", Color) = (0.75,0.39,0.39,1)
        _EnvDiffInt ("EnvDiff ambient diffuse intensity", Range(0.0, 5.0)) = 0.2
        [Header(Specular)]
        _SpecPow("Highlight power", Range(0.0, 90.0)) = 5
        _SpecInt("Highlight intensity", Range(0.0, 10.0)) = 5
        [Space(10)]
        _EnvSpecint ("Envspec ambient specular intensity", Range(0.0, 10.0)) = 0.2
        [Header(SelfIll)]
        _SelfIllInt ("SelfIll emission intensity", Range(0, 10)) = 1
        [HDR]_RimCol ("Rim light color", Color) = (1.0,1.0,1.0,1.0) 
        _RimInt ("Rim light intensity", Range(0.0, 3.0)) = 1.0
        [HideInInspector]
        _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
        [HideInInspector]
        _Color ("Main Color", Color) = (1,1,1,1)//Fallback Require
    }
    SubShader{
        Tags {
            "RenderType" = "Opaque"
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode" = "ForwardBase"
            }
            Cull Off
            CGPROGRAM 
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc" 
            #include "AutoLight.cginc"//Automatically handle lighting attenuation for shadow processing
            #include "Lighting.cginc"//Mainly used to access ambient main directional light related data
            #pragma multi_compile_fwdbase_fullshadows //Enable shadow functionality
            #pragma target 3.0 
            //Texture
            uniform sampler2D _MainTex;
            uniform sampler2D _NormalMap;
            uniform sampler2D _DetailMap;
            uniform sampler2D _MetalnessMask;
            uniform sampler2D _SelfIllMask;
            uniform sampler2D _SpecTex;
            uniform sampler2D _RimLight;
            uniform sampler2D _BaseTintMask;
            uniform sampler2D _SpecularExponent;
            uniform sampler2D _DiffuseWarp;
            uniform samplerCUBE _CubeMap;
            uniform sampler2D _FresnelWarp;
            //Diffuse
            uniform half3 _LightCol;
            uniform fixed3 _TopCol;
            uniform fixed3 _MidCol;
            uniform fixed3 _BotCol;
            uniform fixed  _EnvDiffInt;
            //Specular
            uniform half _SpecPow;
            uniform half _SpecInt;
            uniform half _EnvSpecint;
            //SelfIll
            uniform fixed _SelfIllInt;
            uniform half3 _RimCol;
            uniform half _RimInt;
            uniform half _Cutoff;
            struct VertexInput {
            //Check Document for meaning
                float4 vertex  : POSITION; 
                float2 uv0     : TEXCOORD0;
                float3 normal  : NORMAL; 
                float4 tangent : TANGENT; 
            };
            struct VertexOutput { 
            //Normal information
                float4 pos : SV_POSITION; 
                float2 uv0 : TEXCOORD0;  
                float3 posWS : TEXCOORD1; 
                float3 nDirWS : TEXCOORD2;  
                float3 tDirWS : TEXCOORD3;  
                float3 bDirWS : TEXCOORD4;  
                LIGHTING_COORDS(5,6)
            };
            VertexOutput vert(VertexInput v) { 
                VertexOutput o = (VertexOutput)0;
                    o.pos = UnityObjectToClipPos(v.vertex); 
                    o.uv0 = v.uv0;
                    o.posWS = mul(unity_ObjectToWorld, v.vertex); 
                    o.nDirWS = UnityObjectToWorldNormal(v.normal);
                    o.tDirWS = normalize( mul( unity_ObjectToWorld,
                     float4( v.tangent.xyz, 0.0 ) ).xyz );
                    o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
                    TRANSFER_VERTEX_TO_FRAGMENT(o)
                return o;
            }
            float4 frag(VertexOutput i) : COLOR{ 
                //Vectors
                half3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).rgb;
                half3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
                half3 nDirWS = normalize(mul(nDirTS, TBN));
                half3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
                half3 vrDirWS = reflect(-vDirWS, nDirWS);
                half3 lDirWS = _WorldSpaceLightPos0.xyz; 
                //Represents point light coordinates, normalization is safer, dot product results will be more correct
                half3 lrDirWS = reflect(-lDirWS, nDirWS);
                float shadow = LIGHT_ATTENUATION(i);
                //Dot products
                half  rDotv = dot(vDirWS, lrDirWS);//Phong
                half  nDotl = dot(nDirWS, lDirWS);//Lambert
                half  ndotv = dot(nDirWS, vDirWS);
                //Sample textures
                half4 var_MainTex = tex2D(_MainTex, i.uv0);
                half4 var_DetailMap = tex2D(_DetailMap, i.uv0);
                half  var_MetalnessMask = tex2D(_MetalnessMask, i.uv0);
                half var_SelfIllMask = tex2D(_SelfIllMask, i.uv0);
                half var_SpecTex = tex2D(_SpecTex, i.uv0);//specInt
                half  var_RimLight = tex2D(_RimLight, i.uv0);
                half  var_BaseTintMask = tex2D(_BaseTintMask, i.uv0);
                half  var_SpecularExponent = tex2D(_SpecularExponent, i.uv0);//specSize
                half3 var_Cubemap = texCUBElod(_CubeMap, float4(vrDirWS, lerp(8.0, 0.0, 
                var_MetalnessMask))).rgb;
                half3 var_FresnelWarp = tex2D(_FresnelWarp, ndotv);
                //Extract information
                half3 BaseCol = var_MainTex.rgb;
                half  Opacity = var_MainTex.a;
                half  MetalMask = var_MetalnessMask;
                half  RimLightInt = var_RimLight;
                half  TintMask = var_BaseTintMask;
                half  SpecExp = var_SpecularExponent;
                half3 EnvCube = var_Cubemap;
                half  SpecInt = var_SpecTex;
                half  EmitInt = var_SelfIllMask;
                //Lighting model
                    half3 DiffCol = lerp(BaseCol, half3(0.0,0.0,0.0), MetalMask);
                    //The closer to metal, the weaker the diffuse reflection
                    half3 SpecCol = lerp(BaseCol, half3(0.3,0.3,0.3), TintMask) * SpecInt;
                    //Determine highlight color based on TintMask
                    //0.3 is an empirical value, this value multiplied by highlight intensity specInt gives a comfortable highlight color texture
                    //Fresnel
                    half3 Fresnel = lerp(var_FresnelWarp, 0.0, MetalMask);
                    //The higher the metalness, the less obvious the Fresnel phenomenon
                    half  FreCol = Fresnel.r; //No practical use
                    half  FreRim = Fresnel.g; //Use Fresnel for rim light
                    half  FreSpec = Fresnel.b; //Use Fresnel for specular reflection
                    //Main light diffuse reflection
                    half  HalfLambert = nDotl * 0.5 + 0.5;//Halflambert
                    half3 var_DiffuseWarp = tex2D(_DiffuseWarp, half2(HalfLambert, 0.2));
                    //Sample Ramptexture
                    half3 DirDiff = DiffCol * var_DiffuseWarp * _LightCol;
                    //Main light specular reflection
                    half Phong = pow(max(0.0, rDotv), SpecExp * _SpecPow);
                    half Spec = Phong * max(0.0,nDotl);
                    Spec = max(Spec, FreSpec);
                    //Will have a glossy visual effect, strong Fresnel phenomenon mixed with Phong
                    Spec = Spec * _SpecInt;
                    //After multiplying by SpecInt, most of the previous Spec effects will disappear, only the range specified by SpecInt will have highlights
                    //The original Shader integrates all specular reflections together and finally does max(flMetalnessMask, flSpecWarp)
                    //Here the author splits specular reflections, so both sides need to do a max at the final calculation
                    half3 DirSpec = SpecCol * Spec * _LightCol;
                    //Ambient diffuse reflection
                    float TopMask = max(0.0, nDirWS.g);
                    float BotMask = max(0.0, -nDirWS.g);
                    float MidMask = 1 - TopMask - BotMask;
                    float3 EnvCol = _TopCol * TopMask + 
                                    _MidCol * MidMask + 
                                    _BotCol * BotMask;
                    float3 EnvDiff = DiffCol * EnvCol * _EnvDiffInt;
                    //The video author only used single color, here I directly copied the previous case code without modification
                    //Ambient specular reflection
                    half ReflectInt = max(FreSpec, MetalMask) * SpecInt;
                    //Metal parts have maximum MetalMask, non-metal parts have maximum FreSpec
                    //This way non-metal has strong Fresnel phenomenon, metal parts have little, instead have strong reflection phenomenon
                    half3 EnvSpec = SpecCol * ReflectInt * EnvCube * _EnvSpecint;
                    //Rim light
                    half3 RimLight = _RimCol * FreRim * RimLightInt * max(0.0, nDirWS.g) * _RimInt;
                    //Rim light only appears on top, so need to use normal.g
                    //FreRim defines Fresnel phenomenon, RimLightInt defines intensity range, RimInt defines intensity
                    //Emission
                    float3 emission = EmitInt * DiffCol * _SelfIllInt;
                    //Final
                    half3 FinalRGB = (DirDiff + DirSpec) * shadow + EnvDiff + EnvSpec + RimLight + 
                    emission;
                clip(Opacity - _Cutoff);//Delete all less than _Cutoff, keep those greater than
                return float4(FinalRGB, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Legacy Shaders/Transparent/Cutout/VertexLit"
    //Note: Using FallBack requires declaring a _Color: color, even if you don't use it
}

Main Issues
#

  • Fallback setting: Specify to a Shader that supports transparency textures (such as Legacy Shaders/Transparent/Cutout/VertexLit).
  • Cull Off: Turn off backface culling to prevent model back faces from being clipped.

Texture Explanation
#

Non-shared textures:
#

  • Color
  • MetalnessMask
  • Normal
  • RimMask
  • SelfIllumMask
  • SpecularExponent
  • SpecularMask
  • TintByBaseMask
  • Translucency

Shared textures
#

  • Cubemap
  • DiffuseWarp RampTex
  • FresnelWarpColor
  • FresnelWarpRim
  • FresnelWarpSpec

Texture merging
#

  • Color: RGB: Color A: Opacity
  • Mask1: R: SpecInt G: RimInt B: TintMask A: SpecPow
  • Mask2: R:FresnelCol G: FresnelRim B: FresnelSpec

Texture Description
#

  • MetalnessMask: Metal mask, controls metal areas
  • SpecularMask: Highlight intensity mask
  • RimlightMask: Edge light mask
  • BaseTintMask: Highlight tinting mask, metal highlight color determined by ColorMap
  • FresnelTex: Three RampTex, mapped to Fresnel lighting model
  • Other textures such as ColorMap, Transparency, NormalMap, SelfIlluminationMask, SpecularExponent, DiffuseMask, DetailMask have been encountered in previous cases, or are not used in this case, not detailed here

Source Code Explanation
#

  • RimLightScale, SpecScale: Fixed values, different for different characters, mainly used for rim light and highlight calculations.

  • SpecularScale, RimLightScale and other parameters that exist in source code, the author removed them without affecting the final effect and simplified the code.

  • Fresnel and rim light: Different characters have different RimLightScale settings, mostly numbers greater than 1. Based on this, through FresnelWarp texture and RimMask to control the intensity and range of edge highlights, exaggerated cartoon edge effects can be achieved.

  • FresnelWarpSpec is called flSpecWarp in the original Shader, used to define the character’s ambient specular reflection.

  • Highlight and metal control: Metal parts use cubemap reflection, non-metal parts use highlight texture control. Through cSpecular *= max(flMetalnessMask, flSpecWarp); to achieve highlight switching between metal and non-metal.

  • Main light specular reflection: Dota2 uses an algorithm more suitable for top-down view, core code as follows:

    vec3 R = reflect (V, N);
    float RdotL = clamp(dot(L, -R), 0, 1);
    

    From this we can know that the closer R and L are, the brighter the light. Compared to traditional Phong, it’s more suitable for Dota2’s god’s-eye view.

Lighting Composition
#

flowchart LR
    Lighting --> LightSource
    LightSource --> DiffuseHalfLambert
    LightSource --> SpecularPhong
    DiffuseHalfLambert --> OcclusionShadow
    SpecularPhong --> OcclusionShadow
    OcclusionShadow --> Result
    Lighting --> Environment
    Environment --> Diffuse1Col
    Environment --> SpecularCubemap
    Diffuse1Col --> OcclusionNoAO
    SpecularCubemap --> OcclusionNoAO
    OcclusionNoAO --> Result
    RimLight --> Result
    Emission --> Result

Experience Summary
#

The video case splits the original code and integrates it into its own system, making it easier to understand and extend. Beginners tend to ignore the role of Fresnel textures, it’s recommended to refer more to Dota2 original Shader and

Related

Zhuangdong_Course_Note_LightingMap
3802 words·18 mins
Zhuangdong_Course_Note_VFXShader
4105 words·20 mins
EssenceOfLinearAlgebra - 01
512 words·3 mins