【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_END struct 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; }; ENDHLSL Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag Varyings 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開始探索游戲渲染】專欄-直達
(歡迎點贊留言探討,更多人加入進來能更加完善這個探索的過程,??)

法線貼圖呈現藍紫色調是由其存儲原理決定的。在切線空間中,法線向量(X,Y,Z)的取值范圍為[-1,1],通過RGB=(Normal+1)/2轉換為[0,1]的顏色值。默認垂直方向法線(0,0,1)映射為(128,128,255)的藍紫色。由于大多數模型表面以垂直方向為主,因此法線貼圖整體呈現藍色基調。在著色器中通過UnpackNormalScale函數解碼還原法線向量,配合TBN矩陣實現光照計算。這種編碼方式高效地存儲了表面細節信息,藍色基調是數學映射的必然結果,而非人為設定的美術風格。
浙公網安備 33010602011771號