Unity3D Shader 法线与基础光照 Unity3D Shader 法线与基础光照前言前面的文章里我们已经反复提到“法线”这个词。比如顶点法线外扩做描边光照方向和法线方向做点乘法线贴图会改变表面受光表现那法线到底是什么为什么一旦涉及光照几乎就一定要用到法线这一篇我们从最基础的角度出发讲清楚法线是什么光照为什么和法线有关dot点乘在光照里代表什么如何写一个最基础的 Lambert 漫反射 Shader什么是法线Normal法线可以简单理解为一个表面“朝向哪里”的方向。它通常是一个单位向量。例如一个朝上的平面它的法线可能是(0,1,0)也就是说这个面朝向 Y 轴正方向。为什么法线和光照有关因为光照最基本的问题就是光线打到这个表面时它到底是正面迎光还是斜着迎光还是背光而判断这件事最直接的方法就是看表面的法线方向看光照方向这两个方向越一致说明表面越正对光源通常就越亮。越偏离通常就越暗。dot 点乘为什么这么重要在 Shader 光照中最常见的一行代码就是dot(normal,lightDir)点乘可以理解成两个方向有多接近。它的结果范围大致是1完全同方向0互相垂直-1完全相反在光照里通常会写成saturate(dot(normal,lightDir))意思是小于 0 的部分直接压成 0大于 1 的部分压成 1这样就得到一个 0~1 的受光强度。最基础的漫反射思想最经典的基础光照模型之一就是Lambert 漫反射。它的核心就是diffusemax(0,dot(N,L))其中N法线方向NormalL光照方向Light Direction当表面正对光源时dot接近 1物体更亮当表面转到侧面时dot接近 0物体更暗这就是最基础的“明暗变化”。在 Shader 中如何拿到法线Mesh 顶点输入里通常这样声明float3 normal:NORMAL;例如structappdata{float4 vertex:POSITION;float3 normal:NORMAL;};这表示从模型数据中读取法线。但要注意这里的法线通常是对象空间法线。而做光照时我们通常更常用世界空间法线。因此往往要转换一下float3 worldNormalUnityObjectToWorldNormal(v.normal);为什么光照计算常用世界空间因为灯光方向通常在世界空间下更容易理解摄像机位置是世界空间物体在场景中的朝向变化也更方便统一计算只要参与同一次运算的数据都在同一个空间结果才是可靠的。一个最基础的 Lambert 光照 Shader下面写一个最基础的版本支持主方向光包含环境光 漫反射使用法线和光照方向做点乘ShaderCustom/SimpleLambert{Properties{_Color(Main Color,Color)(1,1,1,1)}SubShader{Pass{Tags{LightModeForwardBase}CGPROGRAM#pragmavertex vert#pragmafragment frag#pragmamulti_compile_fwdbase#includeUnityCG.cginc#includeLighting.cgincstructappdata{float4 vertex:POSITION;float3 normal:NORMAL;};structv2f{float4 pos:SV_POSITION;float3 worldNormal:TEXCOORD0;};fixed4 _Color;v2fvert(appdata v){v2f o;o.posUnityObjectToClipPos(v.vertex);o.worldNormalUnityObjectToWorldNormal(v.normal);returno;}fixed4frag(v2f i):SV_Target{fixed3 Nnormalize(i.worldNormal);fixed3 Lnormalize(_WorldSpaceLightPos0.xyz);fixed3 ambientUNITY_LIGHTMODEL_AMBIENT.xyz*_Color.rgb;fixed ndotlsaturate(dot(N,L));fixed3 diffuse_LightColor0.rgb*_Color.rgb*ndotl;returnfixed4(ambientdiffuse,_Color.a);}ENDCG}}}代码拆解1 从模型里读取法线float3 normal:NORMAL;2 把法线转到世界空间o.worldNormalUnityObjectToWorldNormal(v.normal);3 获取主光方向fixed3 Lnormalize(_WorldSpaceLightPos0.xyz);这里我们主要讨论方向光Directional Light的基础情况。4 计算漫反射强度fixed ndotlsaturate(dot(N,L));这一步就是核心法线越朝向光源 → 值越大法线越背离光源 → 值越小5 乘上灯光颜色和材质颜色fixed3 diffuse_LightColor0.rgb*_Color.rgb*ndotl;6 加上一点环境光fixed3 ambientUNITY_LIGHTMODEL_AMBIENT.xyz*_Color.rgb;如果没有环境光背光面会黑得太死。效果会是什么样这个 Shader 的效果特点是朝向光源的一面更亮背向光源的一面更暗比纯色 Unlit 更有体积感这就是最基础的“受光”感觉。Half-Lambert让阴影面别那么黑标准 Lambert 在某些卡通或风格化场景里会显得阴影过硬。一个常见改法是floathalfLambertdot(N,L)*0.50.5;再做一次限制halfLambertsaturate(halfLambert);这样背光面不会一下掉到纯黑会显得更“柔和”。很多早期卡通渲染教程都会用这种方法。法线方向不对会发生什么如果法线错误最直接的表现就是明暗反了某些面不该亮却很亮旋转模型后受光异常所以当你发现光照不对时除了看代码也要怀疑模型法线是否正确空间是否统一有没有忘记normalize为什么几乎总要normalize因为法线和光照方向参与点乘时我们通常希望比较的是“方向”而不是长度。所以会习惯这样写fixed3 Nnormalize(i.worldNormal);fixed3 Lnormalize(_WorldSpaceLightPos0.xyz);不归一化虽然有时“看起来也能跑”但结果可能会失真。常见坑1 法线和光照方向不在同一个空间例如法线在对象空间光照方向在世界空间直接点乘就是错的。2 忘了归一化点乘结果会不稳定。3 以为法线就是顶点位置方向法线是“表面朝向”不是顶点坐标。4 背光面全黑以为 Shader 写错了很多时候只是因为你只写了 Lambert没加环境光。总结这一篇最重要的核心只有一句基础光照本质上就是“表面朝向”和“光照方向”的关系。在 Shader 中最关键的公式就是saturate(dot(N,L))你可以先记住下面这几个点NORMAL用来接收模型法线光照计算常用世界空间法线dot(N, L)表示受光强度Lambert是最基础的漫反射模型环境光可以避免背光面死黑后面你继续学Blinn-Phong 高光Fresnel法线贴图PBR其实都是在这个基础上不断往上加东西。