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 漫反射。
它的核心就是:
diffuse=max(0,dot(N,L))其中:
N:法线方向(Normal)L:光照方向(Light Direction)
当表面正对光源时:
dot接近 1- 物体更亮
当表面转到侧面时:
dot接近 0- 物体更暗
这就是最基础的“明暗变化”。
在 Shader 中如何拿到法线
Mesh 顶点输入里,通常这样声明:
float3 normal:NORMAL;例如:
structappdata{float4 vertex:POSITION;float3 normal:NORMAL;};这表示从模型数据中读取法线。
但要注意:
这里的法线通常是对象空间法线。
而做光照时,我们通常更常用世界空间法线。
因此往往要转换一下:
float3 worldNormal=UnityObjectToWorldNormal(v.normal);为什么光照计算常用世界空间
因为:
- 灯光方向通常在世界空间下更容易理解
- 摄像机位置是世界空间
- 物体在场景中的朝向变化,也更方便统一计算
只要参与同一次运算的数据都在同一个空间,结果才是可靠的。
一个最基础的 Lambert 光照 Shader
下面写一个最基础的版本:
- 支持主方向光
- 包含环境光 + 漫反射
- 使用法线和光照方向做点乘
Shader"Custom/SimpleLambert"{Properties{_Color("Main Color",Color)=(1,1,1,1)}SubShader{Pass{Tags{"LightMode"="ForwardBase"}CGPROGRAM#pragmavertex vert#pragmafragment frag#pragmamulti_compile_fwdbase#include"UnityCG.cginc"#include"Lighting.cginc"structappdata{float4 vertex:POSITION;float3 normal:NORMAL;};structv2f{float4 pos:SV_POSITION;float3 worldNormal:TEXCOORD0;};fixed4 _Color;v2fvert(appdata v){v2f o;o.pos=UnityObjectToClipPos(v.vertex);o.worldNormal=UnityObjectToWorldNormal(v.normal);returno;}fixed4frag(v2f i):SV_Target{fixed3 N=normalize(i.worldNormal);fixed3 L=normalize(_WorldSpaceLightPos0.xyz);fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*_Color.rgb;fixed ndotl=saturate(dot(N,L));fixed3 diffuse=_LightColor0.rgb*_Color.rgb*ndotl;returnfixed4(ambient+diffuse,_Color.a);}ENDCG}}}代码拆解
1 从模型里读取法线
float3 normal:NORMAL;2 把法线转到世界空间
o.worldNormal=UnityObjectToWorldNormal(v.normal);3 获取主光方向
fixed3 L=normalize(_WorldSpaceLightPos0.xyz);这里我们主要讨论方向光(Directional Light)的基础情况。
4 计算漫反射强度
fixed ndotl=saturate(dot(N,L));这一步就是核心:
- 法线越朝向光源 → 值越大
- 法线越背离光源 → 值越小
5 乘上灯光颜色和材质颜色
fixed3 diffuse=_LightColor0.rgb*_Color.rgb*ndotl;6 加上一点环境光
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz*_Color.rgb;如果没有环境光,背光面会黑得太死。
效果会是什么样
这个 Shader 的效果特点是:
- 朝向光源的一面更亮
- 背向光源的一面更暗
- 比纯色 Unlit 更有体积感
这就是最基础的“受光”感觉。
Half-Lambert:让阴影面别那么黑
标准 Lambert 在某些卡通或风格化场景里,会显得阴影过硬。
一个常见改法是:
floathalfLambert=dot(N,L)*0.5+0.5;再做一次限制:
halfLambert=saturate(halfLambert);这样背光面不会一下掉到纯黑,会显得更“柔和”。
很多早期卡通渲染教程都会用这种方法。
法线方向不对会发生什么
如果法线错误,最直接的表现就是:
- 明暗反了
- 某些面不该亮却很亮
- 旋转模型后受光异常
所以当你发现光照不对时,除了看代码,也要怀疑:
- 模型法线是否正确
- 空间是否统一
- 有没有忘记
normalize
为什么几乎总要normalize
因为法线和光照方向参与点乘时,我们通常希望比较的是“方向”,而不是长度。
所以会习惯这样写:
fixed3 N=normalize(i.worldNormal);fixed3 L=normalize(_WorldSpaceLightPos0.xyz);不归一化虽然有时“看起来也能跑”,但结果可能会失真。
常见坑
1 法线和光照方向不在同一个空间
例如:
- 法线在对象空间
- 光照方向在世界空间
直接点乘就是错的。
2 忘了归一化
点乘结果会不稳定。
3 以为法线就是顶点位置方向
法线是“表面朝向”,不是顶点坐标。
4 背光面全黑,以为 Shader 写错了
很多时候只是因为你只写了 Lambert,没加环境光。
总结
这一篇最重要的核心只有一句:
基础光照,本质上就是“表面朝向”和“光照方向”的关系。
在 Shader 中最关键的公式就是:
saturate(dot(N,L))你可以先记住下面这几个点:
NORMAL用来接收模型法线- 光照计算常用世界空间法线
dot(N, L)表示受光强度Lambert是最基础的漫反射模型- 环境光可以避免背光面死黑
后面你继续学:
- Blinn-Phong 高光
- Fresnel
- 法线贴图
- PBR
其实都是在这个基础上不断往上加东西。