法线贴图(Normal Mapping)是一种通过修改表面法线方向来模拟凹凸细节的纹理技术,无需增加模型几何复杂度,显著提升渲染效率同时保持视觉真实感。
解决的问题
- 性能优化:用低多边形模型配合法线贴图替代高模,减少计算开销
- 细节增强:通过RGB通道存储法线方向,模拟表面凹凸、划痕等微观结构
- 动态光照响应:每个像素的法线独立参与光照计算,实现更真实的明暗变化
历史发展节点
- 1998年:首次由Crytek在游戏《Far Cry》中大规模应用
- 2004年:成为DirectX 9标准特性,进入主流游戏引擎
- 2018年:Unity URP管线整合法线贴图标准化工作流,支持移动端优化
- 2022年:HLSL语法改进,分离纹理对象与采样器声明
生成与使用流程
生成方法
- 高模烘焙:通过ZBrush等工具将高模细节烘焙到低模法线贴图
- 程序生成:Substance Designer等工具从高度图转换生成
- 手动绘制:Photoshop使用滤镜生成基础法线纹理
详细存储原理参看了解具体如何计算和存储的。
URP实现步骤
- 纹理导入
- 类型设为
Default,勾选Bump Map自动切换模式 - 压缩格式推荐
BC5 (DXT5nm)或BC7
- 类型设为
- 材质配置
- Shader选择:URP > Lit 或 Simple Lit
法线贴图拖拽至Normal Map插槽
调整Normal Scale参数控制凹凸强度(0.5-1.5为常用范围
- Shader选择:URP > Lit 或 Simple Lit
- Shader核心原理
- 切线空间转换:通过TBN矩阵将法线从切线空间转到世界空间
- 光照计算:转换后的法线与光源方向点积决定漫反射强度
完整示例代码
以下URP Shader实现法线贴图与基础光照:
-
顶点着色器:计算世界空间法线和切线
-
片段着色器:采样法线贴图并通过TBN矩阵转换
-
光照模型:采用Lambert漫反射计算
-
NormalMapShader.shader
|------------------------------------------------------------------------------------|
|Shader "Custom/NormalMapShader" {|
|Properties {|
|_MainTex("Albedo", 2D) = "white" {}|
|_NormalMap("Normal Map", 2D) = "bump" {}|
|_NormalScale("Normal Scale", Range(0,2)) = 1|
|}|
|SubShader {|
|Tags { "RenderPipeline"="UniversalPipeline" }|
|HLSLINCLUDE|
|#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"|
|ENDHLSL|
|Pass {|
|HLSLPROGRAM|
|#pragma vertex vert|
|#pragma fragment frag|
|struct Attributes {|
|float4 positionOS : POSITION;|
|float2 uv : TEXCOORD0;|
|float3 normalOS : NORMAL;|
|float4 tangentOS : TANGENT;|
|};|
|struct Varyings {|
|float4 positionCS : SV_POSITION;|
|float2 uv : TEXCOORD0;|
|float3 normalWS : TEXCOORD1;|
|float4 tangentWS : TEXCOORD2;|
|};|
|sampler2D _MainTex;|
|sampler2D _NormalMap;|
|float _NormalScale;|
|Varyings vert(Attributes IN) {|
|Varyings OUT;|
|VertexPositionInputs posInput = GetVertexPositionInputs(IN.positionOS.xyz);|
|OUT.positionCS = posInput.positionCS;|
|OUT.uv = IN.uv;|
|VertexNormalInputs normInput = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);|
|OUT.normalWS = normInput.normalWS;|
|OUT.tangentWS = float4(normInput.tangentWS, IN.tangentOS.w);|
|return OUT;|
|}|
|half4 frag(Varyings IN) : SV_Target {|
|float4 normalSample = tex2D(_NormalMap, IN.uv);|
|float3 tangentNormal = UnpackNormalScale(normalSample, _NormalScale);|
|float3 normalWS = IN.normalWS;|
|float3 tangentWS = IN.tangentWS.xyz;|
|float3 bitangentWS = cross(normalWS, tangentWS) * IN.tangentWS.w;|
|float3x3 TBN = float3x3(tangentWS, bitangentWS, normalWS);|
|float3 finalNormal = mul(tangentNormal, TBN);|
|Light mainLight = GetMainLight();|
|float NdotL = saturate(dot(finalNormal, mainLight.direction));|
|half3 albedo = tex2D(_MainTex, IN.uv).rgb;|
|half3 diffuse = albedo * NdotL * mainLight.color;|
|return half4(diffuse, 1);|
|}|
|ENDHLSL|
|}|
|}|
|}|
数据结构定义
- Attributes结构体:声明顶点输入数据
- positionOS:模型空间顶点位置
- uv:纹理坐标
- normalOS:模型空间法线
- tangentOS:模型空间切线(含手性信息)
- Varyings结构体:定义顶点到片段的传递数据
- positionCS:裁剪空间位置
- normalWS:世界空间法线(通过URP内置函数转换)
- tangentWS:世界空间切线(保留手性分量)
顶点着色器实现
- 核心流程:
- 调用GetVertexPositionInputs转换模型空间到裁剪空间
- 通过GetVertexNormalInputs计算世界空间法线和切线
- 保持原始UV坐标传递
片段着色器实现
- 法线贴图处理:
- float4 normalSample = tex2D(_NormalMap, IN.uv); float3 tangentNormal = UnpackNormalScale(normalSample, _NormalScale);
- 使用UnpackNormalScale函数解压法线贴图(范围从[0,1]映射到[-1,1])并应用强度参数。
- TBN矩阵构建:
- float3x3 TBN = float3x3(tangentWS, bitangentWS, normalWS); float3 finalNormal = mul(tangentNormal, TBN);
- 通过切向量、副法线和法线构建正交基,将切线空间法线转换到世界空间。
光照计算:
- Light mainLight = GetMainLight(); float NdotL = saturate(dot(finalNormal, mainLight.direction)); half3 diffuse = albedo * NdotL * mainLight.color;
- 采用Lambert漫反射模型,计算法线与光源方向的点积作为光照强度因子。
关键函数说明
- GetVertexPositionInputs:URP内置函数,处理顶点位置变换
- UnpackNormalScale:URP提供的法线贴图解压函数
- GetMainLight:获取场景主光源信息(需配合URP的Lightweight Render Pipeline使用)
小结
- 坐标空间转换:完整实现模型空间→世界空间→切线空间的转换链
- 光照模型:基于物理的简单漫反射计算
- 性能优化:使用half类型减少内存占用,适合移动端
- 扩展性:通过_NormalScale参数可动态调整法线贴图强度
实际项目应用
- 角色模型:增强皮肤皱纹或服装褶皱细节
- 环境场景:表现砖墙缝隙或金属表面划痕
- 性能权衡 :移动端建议使用
Simple Lit简化版着色器
关键注意事项:
- 确保模型具有正确的UV和切线数据
- 避免sRGB模式导入法线贴图
- 多光源场景需在Shader中添加额外光照循环