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.
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.
Screen UV & Depth Value#
By multiplying screen UV with depth value, textures can be attached to object surfaces and always face the camera.
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.
Power#
Perform power operation on values to adjust the shape of highlights or other effects.
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.
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.
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:#
- Use float4 to customize light direction, implementing custom Lambert model
- Lambert model multiplied by float4 can customize light color
- Lambert model multiplied by float can customize light intensity
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.
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.
Special Case#
This case mixes textures and lighting models, then partitions the mixed results through Round to obtain custom results.
- Use two groups of textures with different Tiling values for mixing, then adjust grayscale to get the final texture
- Use Half Dir and normal dot product to get a special lighting model that can make lighting change with camera
- Mix grayscale image and lighting model, then process the result with round node to get only black and white boundaries
- Finally assign different colors to black and white areas respectively
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} $$
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.
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.
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.
Painted Metal#
This case uses cloud2 to generate masks, lerp mixing different material performances. One part has specular reflection, the other doesn’t.
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);
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
...}
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
08 NormalMap Implementation Principle#
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:
- Sample normal map and decode
- Construct TBN matrix
- Convert tangent space normal to world space
- 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
Matcap#
View space normal mapping BRDF rendering result, only suitable for static display, Zbrush preview interface uses 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
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));
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:
- Physics-based energy conservation
- Physics-based surface property generalization
- 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.
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