法线Normal是图形学的核心概念之一。它本质上是垂直于表面的一个方向向量用来告诉 GPU 这个点的表面朝哪个方向。一、为什么法线这么重要光照计算最核心的公式是朗伯余弦定律漫反射亮度 max(0, dot(N, L))其中N是法线L是指向光源的方向。dot 积的结果就是 cosθθ 越小表面正对光源亮度越高θ 越大斜射/背光亮度趋向 0。没有法线就无从计算任何光照。二、三种坐标空间的法线注意底部那行提示——法线矩阵Normal Matrix是个经典陷阱。普通顶点可以直接乘 M 矩阵但法线不行。如果物体做了非均匀缩放x 方向拉伸y 方向不变直接乘 M 会让法线偏转导致光照错误。正确做法是乘M 的转置逆矩阵。URP 中Unity 已经帮你封装好了// 顶点着色器中 float3 normalWS TransformObjectToWorldNormal(v.normalOS); // 等价于 float3 normalWS mul((float3x3)unity_WorldToObject, v.normalOS); // 已转置三、法线贴图用贴图存储法线几何网格上的法线是逐顶点的精度取决于顶点数量。想在低面数模型上表现砖块缝隙、皮肤毛孔这种细节就需要法线贴图Normal Map。法线贴图把**切线空间Tangent Space**的法线偏移量编码进纹理颜色。所谓切线空间就是以每个顶点为原点、以表面走向为基准建立的局部坐标系由三个轴组成TTangent切线— 沿 UV 的 U 方向BBitangent副切线— 沿 UV 的 V 方向NNormal法线— 垂直表面向外使用切线空间的好处是贴图可以复用——同一张砖块法线贴图贴到墙面、贴到地面都能正确响应光照。四、在 URP Shader 中写法线下面是完整的 URP 自定义 Lit Shader 中处理法线的核心代码片段// 顶点着色器输出 struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float4 tangentWS : TEXCOORD1; // w存bitangent符号 float2 uv : TEXCOORD2; }; // 顶点着色器 Varyings vert(Attributes v) { Varyings o; o.positionCS TransformObjectToHClip(v.positionOS); // 法线和切线变换到世界空间 VertexNormalInputs normalInput GetVertexNormalInputs(v.normalOS, v.tangentOS); o.normalWS normalInput.normalWS; o.tangentWS float4(normalInput.tangentWS, v.tangentOS.w); // w ±1 o.uv TRANSFORM_TEX(v.uv, _BaseMap); return o; } // 片元着色器 half4 frag(Varyings i) : SV_Target { // 1. 从法线贴图采样解码到 [-1,1] half4 normalSample SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv); half3 normalTS UnpackNormalMap(normalSample); // 切线空间法线 // 2. 重建 TBN 矩阵 float3 bitangentWS cross(i.normalWS, i.tangentWS.xyz) * i.tangentWS.w; float3x3 TBN float3x3(i.tangentWS.xyz, bitangentWS, i.normalWS); // 3. 切线空间 → 世界空间 float3 normalWS TransformTangentToWorld(normalTS, TBN); normalWS normalize(normalWS); // 4. 光照计算Lambert Light mainLight GetMainLight(); half NdotL saturate(dot(normalWS, mainLight.direction)); half3 albedo SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb; return half4(albedo * mainLight.color * NdotL, 1.0); }五、顶点法线 vs 法线贴图视觉差异六、几个容易踩的坑问题原因解决方法法线贴图看起来像凹进去Y轴G通道方向翻转导入设置里勾Flip Green Channel模型缩放后光照错误非均匀缩放破坏法线方向用法线矩阵URP 内置函数已处理法线贴图完全没效果未提供切线数据模型导入设置 →Import Tangents颜色全是绿色/紫色贴图 sRGB 设置错误法线贴图导入时取消勾sRGB缝隙处法线突变UV 接缝处切线不连续让美术在接缝处做硬边Hard Edge七、一句话总结法线 表面朝哪个方向的单位向量是所有光照计算的基础输入。顶点法线决定基本形状法线贴图用纹理空间模拟微观凹凸二者通过 TBN 矩阵统一到世界空间参与光照计算。在 URP Shader 里GetVertexNormalInputs()处理顶点法线UnpackNormalMap()TransformTangentToWorld()处理贴图法线这两个函数组合几乎覆盖了 99% 的使用场景。文章已完整交付共包含4 张可视化图表法线基础概念、三种坐标空间变换、TBN 矩阵切线空间、顶点法线 vs 法线贴图对比完整 URP HLSL 代码示例从顶点着色器到片元着色器覆盖法线变换全流程常见坑点速查表5 个高频问题及修复方法如果想继续深挖这几个方向可以接着展开PBR 中法线如何配合粗糙度roughness影响高光Mikktspace 算法切线空间的工业标准视差贴图Parallax Mapping——法线贴图的升级版模拟更强的深度感
Unity Shader URP 什么是法线
法线Normal是图形学的核心概念之一。它本质上是垂直于表面的一个方向向量用来告诉 GPU 这个点的表面朝哪个方向。一、为什么法线这么重要光照计算最核心的公式是朗伯余弦定律漫反射亮度 max(0, dot(N, L))其中N是法线L是指向光源的方向。dot 积的结果就是 cosθθ 越小表面正对光源亮度越高θ 越大斜射/背光亮度趋向 0。没有法线就无从计算任何光照。二、三种坐标空间的法线注意底部那行提示——法线矩阵Normal Matrix是个经典陷阱。普通顶点可以直接乘 M 矩阵但法线不行。如果物体做了非均匀缩放x 方向拉伸y 方向不变直接乘 M 会让法线偏转导致光照错误。正确做法是乘M 的转置逆矩阵。URP 中Unity 已经帮你封装好了// 顶点着色器中 float3 normalWS TransformObjectToWorldNormal(v.normalOS); // 等价于 float3 normalWS mul((float3x3)unity_WorldToObject, v.normalOS); // 已转置三、法线贴图用贴图存储法线几何网格上的法线是逐顶点的精度取决于顶点数量。想在低面数模型上表现砖块缝隙、皮肤毛孔这种细节就需要法线贴图Normal Map。法线贴图把**切线空间Tangent Space**的法线偏移量编码进纹理颜色。所谓切线空间就是以每个顶点为原点、以表面走向为基准建立的局部坐标系由三个轴组成TTangent切线— 沿 UV 的 U 方向BBitangent副切线— 沿 UV 的 V 方向NNormal法线— 垂直表面向外使用切线空间的好处是贴图可以复用——同一张砖块法线贴图贴到墙面、贴到地面都能正确响应光照。四、在 URP Shader 中写法线下面是完整的 URP 自定义 Lit Shader 中处理法线的核心代码片段// 顶点着色器输出 struct Varyings { float4 positionCS : SV_POSITION; float3 normalWS : TEXCOORD0; float4 tangentWS : TEXCOORD1; // w存bitangent符号 float2 uv : TEXCOORD2; }; // 顶点着色器 Varyings vert(Attributes v) { Varyings o; o.positionCS TransformObjectToHClip(v.positionOS); // 法线和切线变换到世界空间 VertexNormalInputs normalInput GetVertexNormalInputs(v.normalOS, v.tangentOS); o.normalWS normalInput.normalWS; o.tangentWS float4(normalInput.tangentWS, v.tangentOS.w); // w ±1 o.uv TRANSFORM_TEX(v.uv, _BaseMap); return o; } // 片元着色器 half4 frag(Varyings i) : SV_Target { // 1. 从法线贴图采样解码到 [-1,1] half4 normalSample SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv); half3 normalTS UnpackNormalMap(normalSample); // 切线空间法线 // 2. 重建 TBN 矩阵 float3 bitangentWS cross(i.normalWS, i.tangentWS.xyz) * i.tangentWS.w; float3x3 TBN float3x3(i.tangentWS.xyz, bitangentWS, i.normalWS); // 3. 切线空间 → 世界空间 float3 normalWS TransformTangentToWorld(normalTS, TBN); normalWS normalize(normalWS); // 4. 光照计算Lambert Light mainLight GetMainLight(); half NdotL saturate(dot(normalWS, mainLight.direction)); half3 albedo SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb; return half4(albedo * mainLight.color * NdotL, 1.0); }五、顶点法线 vs 法线贴图视觉差异六、几个容易踩的坑问题原因解决方法法线贴图看起来像凹进去Y轴G通道方向翻转导入设置里勾Flip Green Channel模型缩放后光照错误非均匀缩放破坏法线方向用法线矩阵URP 内置函数已处理法线贴图完全没效果未提供切线数据模型导入设置 →Import Tangents颜色全是绿色/紫色贴图 sRGB 设置错误法线贴图导入时取消勾sRGB缝隙处法线突变UV 接缝处切线不连续让美术在接缝处做硬边Hard Edge七、一句话总结法线 表面朝哪个方向的单位向量是所有光照计算的基础输入。顶点法线决定基本形状法线贴图用纹理空间模拟微观凹凸二者通过 TBN 矩阵统一到世界空间参与光照计算。在 URP Shader 里GetVertexNormalInputs()处理顶点法线UnpackNormalMap()TransformTangentToWorld()处理贴图法线这两个函数组合几乎覆盖了 99% 的使用场景。文章已完整交付共包含4 张可视化图表法线基础概念、三种坐标空间变换、TBN 矩阵切线空间、顶点法线 vs 法线贴图对比完整 URP HLSL 代码示例从顶点着色器到片元着色器覆盖法线变换全流程常见坑点速查表5 个高频问题及修复方法如果想继续深挖这几个方向可以接着展开PBR 中法线如何配合粗糙度roughness影响高光Mikktspace 算法切线空间的工业标准视差贴图Parallax Mapping——法线贴图的升级版模拟更强的深度感