找回密码
 立即注册
首页 业界区 业界 【URP】Unity[法线贴图]原理与实践

【URP】Unity[法线贴图]原理与实践

丘奕奕 6 天前
【从UnityURP开始探索游戏渲染】专栏-直达
法线贴图(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核心原理

    • 切线空间转换‌:通过TBN矩阵将法线从切线空间转到世界空间
    • 光照计算‌:转换后的法线与光源方向点积决定漫反射强度

完整示例代码

以下URP Shader实现法线贴图与基础光照:

  • 顶点着色器‌:计算世界空间法线和切线
  • 片段着色器‌:采样法线贴图并通过TBN矩阵转换
  • 光照模型‌:采用Lambert漫反射计算
  • NormalMapShader.shader
    1. Shader "Custom/NormalMapShader" {
    2.     Properties {
    3.         _MainTex("Albedo", 2D) = "white" {}
    4.         _NormalMap("Normal Map", 2D) = "bump" {}
    5.         _NormalScale("Normal Scale", Range(0,2)) = 1
    6.     }
    7.     SubShader {
    8.         Tags { "RenderPipeline"="UniversalPipeline" }
    9.         HLSLINCLUDE
    10.         #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    11.         ENDHLSL
    12.         Pass {
    13.             HLSLPROGRAM
    14.             #pragma vertex vert
    15.             #pragma fragment frag
    16.             struct Attributes {
    17.                 float4 positionOS : POSITION;
    18.                 float2 uv : TEXCOORD0;
    19.                 float3 normalOS : NORMAL;
    20.                 float4 tangentOS : TANGENT;
    21.             };
    22.             struct Varyings {
    23.                 float4 positionCS : SV_POSITION;
    24.                 float2 uv : TEXCOORD0;
    25.                 float3 normalWS : TEXCOORD1;
    26.                 float4 tangentWS : TEXCOORD2;
    27.             };
    28.             sampler2D _MainTex;
    29.             sampler2D _NormalMap;
    30.             float _NormalScale;
    31.             Varyings vert(Attributes IN) {
    32.                 Varyings OUT;
    33.                 VertexPositionInputs posInput = GetVertexPositionInputs(IN.positionOS.xyz);
    34.                 OUT.positionCS = posInput.positionCS;
    35.                 OUT.uv = IN.uv;
    36.                 VertexNormalInputs normInput = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
    37.                 OUT.normalWS = normInput.normalWS;
    38.                 OUT.tangentWS = float4(normInput.tangentWS, IN.tangentOS.w);
    39.                 return OUT;
    40.             }
    41.             half4 frag(Varyings IN) : SV_Target {
    42.                 float4 normalSample = tex2D(_NormalMap, IN.uv);
    43.                 float3 tangentNormal = UnpackNormalScale(normalSample, _NormalScale);
    44.                 float3 normalWS = IN.normalWS;
    45.                 float3 tangentWS = IN.tangentWS.xyz;
    46.                 float3 bitangentWS = cross(normalWS, tangentWS) * IN.tangentWS.w;
    47.                 float3x3 TBN = float3x3(tangentWS, bitangentWS, normalWS);
    48.                 float3 finalNormal = mul(tangentNormal, TBN);
    49.                 Light mainLight = GetMainLight();
    50.                 float NdotL = saturate(dot(finalNormal, mainLight.direction));
    51.                 half3 albedo = tex2D(_MainTex, IN.uv).rgb;
    52.                 half3 diffuse = albedo * NdotL * mainLight.color;
    53.                 return half4(diffuse, 1);
    54.             }
    55.             ENDHLSL
    56.         }
    57.     }
    58. }
    复制代码
数据结构定义


  • 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中添加额外光照循环
<blockquote>
【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册