光照计算
光照计算由三个重要的组成决定:
- 光源数据:光源数据受光源类型影响,平行光、点光、聚光灯是不一样的。灯光数据的传递方式由RenderPath来决定。
- 光源方向LightDir
- 衰减范围Attenuation
- 光源颜色LightColor
- 模型表面材质结构:
- 顶点法线
- 法线贴图(像素级法线)
- 光滑度
- PBR理论框架——描述自然界大多数物体的表面材质结构
- 观察方向
渲染路径
前向渲染
Unity的Built-in渲染管线和URP渲染管线都是前向渲染。但是URP使用的是forward+渲染,能够使用多盏灯光。
在前向渲染中,每个模型根据自己受光的灯光数量执行一个Pass,如果一个模型除了主光源(平行光)外还受4个其他光源的影响,那么这个模型就要多重复渲染4次,多产生4个DrawCall
Unity对于精细的逐像素光照是有限制的,在Project Setting——Quality里面,Pixel Light Count指定了逐像素光照的光源数量。其中
超出这个数量的灯光会使用逐顶点的光照,并且逐顶点的光照会在ForwardBase这个Pass里面绘制。在Unity中,会根据灯光的Intensity来排序需要逐像素光照的灯光,当然也可以在灯光的Render Mode中设置Important。
在前向渲染中,主光源(平行光)和超出了pixel Light Count的其他光源都会在ForwardBase这个Pass里面绘制,在pixel限制内的逐个像素光源会在ForwardAdd这个Pass里面绘制
综上,在前向渲染的Shader中,需要两个Pass
- ForwardBase
- 主光源(平行光),以及超出pixel数量的灯光都作为逐顶点灯光传入
- SH(球谐光照)、Light Map(预烘焙的光照贴图)、Reflection Probe等计算均在这个Pass里面完成。
- ForwardAdd
- Pixel Light Count数量范围内的点光、平行光,每个灯光的计算,均会调用这个Pass进行,计算的结果通过Blend One One叠加起来
这里的灯光都是Realtime模式,先不讨论Mixed和Baked模式的灯光。
延迟渲染
UE4和Unity的HDRP渲染管线都是延迟渲染,这种渲染方式无视场景中的灯光个数。
想要在Unity中开启延迟渲染,选择场景中的main Camera,然后再在Render Path中选择Deffered。
我们可以打开FrameDebugger来观察延迟渲染管线的流程:

延迟渲染有两个重要的阶段,生成GBuffer和Lighting
在延迟渲染管线中,使用了MRT(Multi Render Target)技术,场景中的每个模型对应的Shader都可以输出多个信息到不同的Layer当中。在上图中:
- RT0代表的是当前选中的模型的Diffuse + Occlusion信息,ARGB32
- RT1代表的是当前选中的模型的Metallic信息(Specular color,smoothness),ARGB32
- RT2代表的是当前选中的模型的World Space Normal信息
- RT3代表的是当前选中的模型的Emission + lighting + lightmaps + reflection probes信息,根据摄像机是否是HDR,占用的内存大小不同
- RT4代表的是前选中的模型的Light occlusion信息,这个RT只有在开启了Shadow Mask或者Distance Shadow Mask模式才会看到。
- Depth代表的是当前选中的模型的Depth + Stencil信息
在前向渲染中,每个Shader只输出最终的颜色信息,没有延迟渲染中这么丰富。
在GBuffer中拿到足够的模型信息之后,在Lighting阶段只需要让场景中的每个灯光直接计算就好了,每个灯光计算一次然后叠加起来即可。
Phong光照
ForwardBase Pass
现在实现一下前向渲染下的Phong光照,前面说过,前向渲染需要两个Pass,在第一个ForwardBasePass中需要先添加一些东西:
1 2 3 4 5 6 7 8 9 10 11
| Pass { Tags {"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #pragme multi_compile_fwdbase #include "UnityCG.cginc" #include "AutoLight.cginc" }
|
添加的这三行是Unity引入光照必备的,记住即可

上图的N是法线方向,L是从当前顶点到光源的方向。R是当前灯光反射的方向(Unity提供了reflect函数可以快速计算)V是从当前顶点看向摄像机的方向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| Shader "KerryTA/Code/201PhongLighting" { Properties { _MainTex ("Texture", 2D) = "white" {} _AOMap ("AOMap", 2D) = "white" {} _SpecMask ("SpecMask", 2D) = "white" {} _Shininess("Shininess", Range(0.01, 100)) = 1.0 _SpecIntensity("SpecIntensity", Range(0.1,5)) = 1.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 100
Pass { Tags {"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include "UnityCG.cginc" #include "AutoLight.cginc"
struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 normal_dir : TEXCOORD1; float3 pos_world : TEXCOORD2; };
sampler2D _MainTex; float4 _MainTex_ST; sampler2D _AOMap; sampler2D _SpecMask;
float4 _LightColor0; float _Shininess; float _SpecIntensity;
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz); o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz; return o; }
half4 frag (v2f i) : SV_Target { half4 base_color = tex2D(_MainTex, i.uv); half4 ao_color = tex2D(_AOMap, i.uv); half4 spec_mask = tex2D(_SpecMask, i.uv);
half3 normal_dir = normalize(i.normal_dir); half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world); half3 light_dir = normalize(_WorldSpaceLightPos0.xyz); half NdotL = dot(normal_dir, light_dir); half3 diffuse_color = max(0.0, NdotL) * _LightColor0.xyz * base_color.xyz; half3 reflect_dir = reflect(-light_dir, normal_dir); half RDotV = dot(view_dir, reflect_dir); half3 spec_color = pow(max(0.0, RDotV),_Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;
half3 ambient_color = UNITY_LIGHTMODEL_AMBIENT.rgb * base_color.rgb; half3 final_color = (diffuse_color + spec_color + ambient_color) * ao_color; return half4(final_color, 1.0); } ENDCG } } }
|
高光遮罩贴图可以从PBR贴图中的Roughness贴图反相得到
ForwardAdd Pass
在ForwardAdd Pass中需要先添加一些东西:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Pass { Tags {"LightMode" = "ForwardAdd"} Blend One One CGPROGRAM #pragma vertex vert #pragma fragment frag、 #pragma multi_compile_fwdadd #include "UnityCG.cginc" #include "AutoLight.cginc" }
|
点光源或聚光灯需要自己的衰减系数attenuation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| Pass { Tags {"LightMode" = "ForwardAdd"} Blend One One CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdadd #include "UnityCG.cginc" #include "AutoLight.cginc"
struct appdata { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 normal_dir : TEXCOORD1; float3 pos_world : TEXCOORD2; };
sampler2D _MainTex; float4 _MainTex_ST; sampler2D _AOMap; sampler2D _SpecMask;
float4 _LightColor0; float _Shininess; float _SpecIntensity;
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.normal_dir = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz); o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz; return o; }
half4 frag (v2f i) : SV_Target { half4 base_color = tex2D(_MainTex, i.uv); half4 ao_color = tex2D(_AOMap, i.uv); half4 spec_mask = tex2D(_SpecMask, i.uv);
half3 normal_dir = normalize(i.normal_dir); half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world); #if defined (DIRECTIONAL) half3 light_dir = normalize(_WorldSpaceLightPos0.xyz); half attenuation = 1.0; #elif defined (POINT) half3 light_dir = normalize(_WorldSpaceLightPos0.xyz - i.pos_world); half distance = length(_WorldSpaceLightPos0.xyz - i.pos_world); half range = 1.0 / unity_WorldToLight[0][0]; half attenuation = saturate((range - distance) / range); #endif half NdotL = dot(normal_dir, light_dir); half3 diffuse_color = max(0.0, NdotL) * _LightColor0.xyz * base_color.xyz * attenuation; half3 reflect_dir = reflect(-light_dir, normal_dir); half RDotV = dot(view_dir, reflect_dir); half3 spec_color = pow(max(0.0, RDotV),_Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb * attenuation;
half3 final_color = (diffuse_color + spec_color) * ao_color; return half4(final_color, 1.0); } ENDCG }
|
此Shader中点光源的衰减计算方法已过时,Unity使用的是采样渐变图来计算灯光衰减,这里依然使用旧方法便于理解。
在ForwardAddPass中不需要加算Ambient光。
Blin-Phong高光
Blin-Phong是对Phong光照模型的高光部分的优化,它不再使用reflect
这个比较消耗性能的函数。而是将当前像素到灯光的向量和当前像素到摄像机的向量相加并归一化,得到一个“半角向量”,然后让这个半角向量和当前像素的法向量点乘,用来做高光。
1 2 3 4 5
| half3 half_dir = normalize(light_dir + view_dir); half NdotH = dot(normal_dir, half_dir);
half3 spec_color = pow(max(0.0, NdotH),_Shininess) * _LightColor0.xyz * _SpecIntensity * spec_mask.rgb;
|