【URP】[法线贴图]为什么主要是蓝色的?
【从UnityURP开始探索游戏渲染】专栏-直达
法线贴图呈现蓝紫色调(尤其以蓝色为主)是由其存储原理、切线空间坐标系设计及颜色编码规则共同决定的。
核心原因:法线向量的存储规则
法线向量的物理范围
法线是单位向量,每个分量(X, Y, Z)的取值范围为 [-1, 1],分别代表切线空间中的方向:
- X(红色通道):左右偏移(左为负,右为正)
- Y(绿色通道):上下偏移(下为负,上为正)
- Z(蓝色通道):垂直表面的方向(指向外部为正)。
颜色空间的映射限制
图像颜色值范围是 [0, 1](对应0~255),因此需要进行转换:
RGB=(Normalxyz+1)/2
- 默认法线方向:当表面完全垂直(无倾斜)时,法线向量为 (0, 0, 1)。
- 转换结果:
- R=20+1=0.5 (128)
- G=20+1=0.5 (128)
- B=21+1=1 (255)
- 最终颜色为 (128, 128, 255),即 蓝紫色(蓝色占主导)。
现实模型的主导方向
- 大多数模型表面(如墙面、地面)以垂直方向为主(Z≈1),因此蓝色通道值接近255,而XY通道接近128(中性灰),整体呈现蓝色基调。
颜色变化的场景解释
颜色表现 | 对应的法线方向 | 表面形态 |
---|---|---|
深蓝色 (0,0,1) | 完全垂直向外 | 平坦表面(如地板) |
蓝紫色 (0.5,0.5,1) | 轻微倾斜 | 缓坡、弧形表面 |
青色/绿色 (低R,高G,中B) | 明显上/下倾斜(Y≠0) | 边缘、陡坡 |
红色/粉色 (高R,中G,中B) | 明显左/右倾斜(X≠0) | 侧壁、凹凸边缘 |
💎 示例:墙面法线贴图中,砖缝凹陷处因法线指向侧方(X/Y增大),可能呈现红绿色调,但整体仍以蓝紫色为基底。
️ 技术实现验证
生成与解码逻辑
-
生成法线贴图:通过公式
color = (normal + 1) / 2
将高模法线烘焙为贴图。 -
Shader解码:在着色器中逆向计算还原法线向量:此步骤是光照计算的基础。
glsl vec3 normal = texture(normalMap, uv).rgb * 2.0 - 1.0; // [0,1] → [-1,1]
切线空间的意义
法线贴图通常在 切线空间(Tangent Space)中定义:
- 以顶点法线为Z轴,切线为X轴,副切线为Y轴构建坐标系。
- 优势:无论模型如何旋转,法线方向始终相对于表面本地坐标,确保凹凸效果稳定。
常见误区澄清
- 误区1:蓝色是人为设定的美术风格。真相:蓝色是数学映射的必然结果,由垂直方向(0,0,1)的编码规则决定。
- 误区2:法线贴图的颜色代表凹凸高度。真相:它存储的是方向而非高度,凹凸感通过光照模拟实现。
实际应用案例
- Unity 工作流:将法线贴图拖入材质球的 Normal Map 插槽,通过
UnpackNormal()
函数解码(内置管线见UnityCG.cginc
,URP管线UnpackNormalScale()
见Packing.hlsl)。 - 效果增强:调整 Normal Scale 参数控制凹凸强度(值>1增强凸起,<1弱化)。
URP中的法线贴图
法线贴图设置流程
- 导入法线贴图
- 纹理类型设为"Default/Normal map"
- 压缩格式推荐BC5(DXT5nm)或BC7
- 勾选"sRGB"选项确保正确色彩空间转换
- 创建URP材质
- 使用Shader路径:
Universal Render Pipeline/Lit
- 将法线贴图拖拽到Normal Map插槽
- 调整Normal Scale参数(建议0.5-1.5)
- 使用Shader路径:
完整Shader代码实现
-
NormalMapURP.shader
Shader "Custom/URPNormalMap" {Properties{_BaseMap("Albedo", 2D) = "white" {}_BaseColor("Color", Color) = (1,1,1,1)_NormalMap("Normal Map", 2D) = "bump" {}_NormalScale("Normal Scale", Range(0,2)) = 1_Metallic("Metallic", Range(0,1)) = 0_Smoothness("Smoothness", Range(0,1)) = 0.5}SubShader{Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline"}HLSLINCLUDE#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"TEXTURE2D(_BaseMap);SAMPLER(sampler_BaseMap);TEXTURE2D(_NormalMap);SAMPLER(sampler_NormalMap);CBUFFER_START(UnityPerMaterial)float4 _BaseMap_ST;half4 _BaseColor;half _Metallic;half _Smoothness;half _NormalScale;CBUFFER_ENDstruct Attributes{float4 positionOS : POSITION;float3 normalOS : NORMAL;float4 tangentOS : TANGENT;float2 uv : TEXCOORD0;};struct Varyings{float4 positionCS : SV_POSITION;float2 uv : TEXCOORD0;float3 normalWS : TEXCOORD1;float4 tangentWS : TEXCOORD2;float3 positionWS : TEXCOORD3;};ENDHLSLPass{Name "ForwardLit"Tags { "LightMode"="UniversalForward" }HLSLPROGRAM#pragma vertex vert#pragma fragment fragVaryings vert(Attributes input){Varyings output;VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);output.positionCS = vertexInput.positionCS;output.positionWS = vertexInput.positionWS;output.uv = TRANSFORM_TEX(input.uv, _BaseMap);output.normalWS = normalInput.normalWS;output.tangentWS = float4(normalInput.tangentWS, input.tangentOS.w);return output;}half4 frag(Varyings input) : SV_Target{// 采样基础贴图half4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;// 采样和解压法线贴图half4 normalSample = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv);half3 normalTS = UnpackNormalScale(normalSample, _NormalScale);// 构建TBN矩阵half3 bitangentWS = cross(input.normalWS, input.tangentWS.xyz) * input.tangentWS.w;half3x3 TBN = half3x3(input.tangentWS.xyz, bitangentWS, input.normalWS);half3 normalWS = TransformTangentToWorld(normalTS, TBN);// 光照计算Light mainLight = GetMainLight();half3 lightDir = normalize(mainLight.direction);half NdotL = saturate(dot(normalWS, lightDir));half3 diffuse = baseColor.rgb * NdotL * mainLight.color;// 高光计算half3 viewDir = normalize(_WorldSpaceCameraPos - input.positionWS);half3 halfVec = normalize(lightDir + viewDir);half NdotH = saturate(dot(normalWS, halfVec));half specular = pow(NdotH, _Smoothness * 256) * _Metallic;half3 finalColor = diffuse + specular * mainLight.color;return half4(finalColor, baseColor.a);}ENDHLSL}} }
关键实现说明
- 法线解压:使用
UnpackNormalScale
函数处理法线贴图数据,支持强度调节 - TBN矩阵:通过切线、副切线和法线构建转换矩阵,将切线空间法线转到世界空间
- 光照模型:采用Blinn-Phong模型计算漫反射和高光
- URP适配:使用URP特有的
GetVertexPositionInputs
等函数替代传统Shader写法
常见问题解决方案
- 法线效果异常:检查切线空间计算是否正确,确保模型导入时勾选"Calculate Tangents"
- 性能优化:移动端可考虑在切线空间计算光照减少矩阵运算
- 多光源支持:需添加AdditionalLights Pass处理额外光源
总结
法线贴图的蓝色基调本质是垂直方向向量(0,0,1)经归一化映射后的颜色表达,这种方法平衡了存储效率与光照计算需求,是3D渲染中模拟表面细节的核心技术,直观的颜色样式只是数据可视化的一种直观显示。
【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)