Unity Shader实战:手把手教你实现Lambert漫反射(逐顶点 vs 逐像素 vs 半兰伯特)
Unity Shader实战:Lambert漫反射的三种实现方式深度解析
在3D游戏开发中,光照效果直接影响场景的真实感和视觉体验。Lambert漫反射作为最基础的光照模型之一,其实现方式的选择往往决定了渲染质量和性能消耗。本文将带您深入探索Unity中三种Lambert漫反射的实现路径:逐顶点光照、逐像素光照以及半兰伯特优化方案。
1. 光照模型基础与数学原理
漫反射光照遵循Lambert余弦定律,其核心公式为:
float diffuse = max(0, dot(N, L));其中N是表面法线,L是光源方向向量。这个简单的点积运算背后蕴含着重要的物理意义——表面接收到的光强与入射角余弦成正比。
在Unity中,我们需要考虑几个关键参数:
| 参数 | 说明 | 典型取值 |
|---|---|---|
_LightColor0 | 主光源颜色 | 由场景光源决定 |
_WorldSpaceLightPos0 | 光源位置 | 自动传入Shader |
UNITY_LIGHTMODEL_AMBIENT | 环境光颜色 | 可在Lighting面板设置 |
注意:在Shader中获取正确的法线方向需要特别注意坐标空间转换。模型空间法线需要转换到世界空间才能与光源方向正确计算。
2. 逐顶点光照实现
逐顶点光照(Per-Vertex Lighting)是最基础的实现方式,其特点是光照计算在顶点着色器中完成,然后通过插值传递给片元着色器。
Shader "Custom/VertexLambert" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; fixed3 diffuse : COLOR0; }; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; // 逐顶点光照计算 float3 worldNormal = UnityObjectToWorldNormal(v.normal); float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); float ndotl = max(0, dot(worldNormal, lightDir)); o.diffuse = _LightColor0.rgb * ndotl; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); col.rgb *= i.diffuse; return col; } ENDCG } } }这种实现方式的优缺点对比:
优点:
- 计算开销最小
- 适合低端设备
- 对简单几何体效果尚可
缺点:
- 高模表面会出现明显色阶
- 无法表现细腻的光影过渡
- 法线贴图效果受限
3. 逐像素光照实现
逐像素光照(Per-Pixel Lighting)将计算推迟到片元着色器阶段,能产生更精确的光照效果。
Shader "Custom/PixelLambert" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD1; }; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.worldNormal = UnityObjectToWorldNormal(v.normal); return o; } fixed4 frag (v2f i) : SV_Target { // 逐像素光照计算 float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); float ndotl = max(0, dot(normalize(i.worldNormal), lightDir)); fixed3 diffuse = _LightColor0.rgb * ndotl; fixed4 col = tex2D(_MainTex, i.uv); col.rgb *= diffuse + UNITY_LIGHTMODEL_AMBIENT.rgb; return col; } ENDCG } } }性能对比数据:
| 指标 | 逐顶点 | 逐像素 |
|---|---|---|
| 顶点着色器指令数 | 15 | 8 |
| 片元着色器指令数 | 5 | 12 |
| 适合场景 | 移动端简单模型 | PC端高模/法线贴图 |
提示:在移动平台上,可以针对不同设备性能采用LOD策略,高端设备使用逐像素光照,低端设备回退到逐顶点方案。
4. 半兰伯特优化技术
Valve公司在《半条命2》中提出的半兰伯特(Half Lambert)技术,通过修改光照计算公式解决了传统Lambert在背光面过暗的问题:
float halfLambert = dot(N, L) * 0.5 + 0.5;完整Shader实现:
fixed4 frag (v2f i) : SV_Target { float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); float ndotl = dot(normalize(i.worldNormal), lightDir); float halfLambert = ndotl * 0.5 + 0.5; fixed3 diffuse = _LightColor0.rgb * halfLambert; fixed4 col = tex2D(_MainTex, i.uv); col.rgb *= diffuse; return col; }半兰伯特特别适合的风格化渲染场景:
- 卡通风格游戏
- 需要突出角色轮廓的场景
- 低对比度艺术风格
实际项目中,我们经常根据需求调整半兰伯特公式的参数:
// 可调节的半兰伯特变体 float halfLambert = pow(ndotl * _Scale + _Offset, _Power);在材质面板暴露这些参数,可以让美术人员动态调整光照效果:
Properties { _Scale ("Scale", Range(0,1)) = 0.5 _Offset ("Offset", Range(0,1)) = 0.5 _Power ("Power", Range(0.1,5)) = 1.0 }5. 实战性能优化技巧
在真实项目中使用Lambert光照时,有几个实用技巧可以提升效果和性能:
多光源处理策略:
- 主光源使用逐像素计算
- 附加光源使用逐顶点或球谐光照
- 对静态物体使用光照贴图
// 示例:简单多光源支持 #pragma multi_compile_fwdbase #pragma multi_compile_fwdadd移动端优化方案:
- 使用
approxview指令简化视角向量计算 - 对低端设备关闭动态阴影
- 合并光照计算与纹理采样指令
// 移动端优化版光照计算 half3 lightDir = normalize(_WorldSpaceLightPos0.xyz); half ndotl = saturate(dot(i.worldNormal, lightDir));在URP/HDRP管线中,Lambert计算已经被整合到PBR光照模型中,但理解其原理对于自定义Shader开发仍然至关重要。
