Unity自定义地形Surface Shader添加法线贴图故障排查
问题:Unity自定义地形Surface Shader添加法线贴图后地形变黑、主纹理失效
我在为Unity地形编写自定义CG Surface Shader时遇到了麻烦——目标是给地形加上法线贴图,但应用Shader后没有报错,地形却直接变黑了,主纹理完全显示不出来,法线贴图也没任何效果(已经附上Unity编辑器材质截图)。想请教各位:如何在自定义地形Shader中正确添加法线贴图?
以下是我的Shader代码:
Shader "Custom/Terrain" { Properties { // Zexture und Scale-Slot im Material erzeugen testTexture("Texture", 2D) = "white"{} testScale("Scale", Float) = 1 _NormalMap ("Normal Map", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 // SHADER-Language = CG CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf Standard fullforwardshadows // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 const static int maxLayerCount = 8; // Max Amount of colors the terrain can have const static float epsilon = 1E-4; int layerCount; float3 baseColours[maxLayerCount]; float baseStartHeights[maxLayerCount]; // ZONES FOR COLORS OF TERRAIN float baseBlends[maxLayerCount]; // BLENDS FoR COLORS/TEXTUREN float baseColourStrength[maxLayerCount]; float baseTextureScales[maxLayerCount]; // Texture size float minHeight; //min. Hight of Map float maxHeight; //max. Hight of Map sampler2D testTexture; // Textur Slot float testScale; // Scale of the texture sampler2D _NormalMap; UNITY_DECLARE_TEX2DARRAY(baseTextures); // Texture-Array from TextureData(another Script) definieren struct Input { float3 worldPos; // Position on Map float3 worldNormal; // Normales of Map float2 uv_Diffuse; INTERNAL_DATA }; float inverseLerp(float a, float b, float value) // a = min.value, b = max.value, value = surrent value { return saturate((value - a)/(b-a)); } // --> TRIPLANAR MAPPING float3 triplanar (float3 worldPos, float scale, float3 blendAxes, int textureIndex) { float3 scaledWorldPos = worldPos/ scale; float3 xProjection = UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaledWorldPos.y,scaledWorldPos.z, textureIndex)) * blendAxes.x; float3 yProjection = UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaledWorldPos.x,scaledWorldPos.z, textureIndex)) * blendAxes.y; float3 zProjection = UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaledWorldPos.x,scaledWorldPos.y, textureIndex)) * blendAxes.z; return xProjection + yProjection + zProjection; // Mix textures } // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader. // https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing. // #pragma instancing_options assumeuniformscaling UNITY_INSTANCING_CBUFFER_START(Props) // put more per-instance properties here UNITY_INSTANCING_CBUFFER_END void surf (Input IN, inout SurfaceOutputStandard o) { float heightPercent = inverseLerp(minHeight, maxHeight, IN.worldPos.y); float3 blendAxes = abs(IN.worldNormal); blendAxes /= blendAxes.x + blendAxes.y + blendAxes.z; for (int i = 0; i < layerCount; i++) { float drawStrenght = inverseLerp(-baseBlends[i]/2 - epsilon, baseBlends[i]/2, heightPercent - baseStartHeights[i]); float3 baseColour = baseColours[i] * baseColourStrength[i]; // Textur float3 textureColour = triplanar(IN.worldPos, baseTextureScales[i], blendAxes, i) * (1-baseColourStrength[i]); o.Albedo = o.Albedo * (1 - drawStrenght) + (baseColour + textureColour) * drawStrenght; o.Normal = UnpackNormal( tex2D(_NormalMap, IN.uv_Diffuse)); } } ENDCG } FallBack "Diffuse" }
我正在努力学习CG编程,希望得到大家的帮助。鸣谢Sebastian Lague。
解决方案
先帮你梳理下代码里的几个核心问题,这些问题直接导致了地形变黑和法线失效:
1. 循环内重复覆盖输出值的错误
你在surf函数的循环里,每次迭代都直接覆盖o.Normal和o.Albedo,而不是做渐变混合。比如法线部分,最后只会保留最后一个图层的法线(如果layerCount为0,法线会是默认的无效值);Albedo的混合逻辑也因为第一次循环时o.Albedo是默认黑色,导致前面的图层无法正确叠加到最终颜色里。
2. 法线贴图未适配三平面映射
你当前用固定的uv_Diffuse坐标采样法线贴图,但地形主纹理是用三平面映射采样的,两者UV不匹配会导致法线效果错乱,甚至和主纹理完全脱节。
3. UV命名不符合Unity约定
Input结构体里的uv_Diffuse和Properties里的主纹理testTexture名称不对应,Unity无法自动关联正确的UV坐标(虽然你主要用纹理数组的三平面,但修正后更符合规范)。
修改后的完整Shader代码
Shader "Custom/Terrain" { Properties { testTexture("Texture", 2D) = "white"{} testScale("Scale", Float) = 1 _NormalMap ("Normal Map", 2D) = "bump" {} _NormalScale ("Normal Strength", Float) = 1.0 // 新增法线强度控制参数 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 const static int maxLayerCount = 8; const static float epsilon = 1E-4; int layerCount; float3 baseColours[maxLayerCount]; float baseStartHeights[maxLayerCount]; float baseBlends[maxLayerCount]; float baseColourStrength[maxLayerCount]; float baseTextureScales[maxLayerCount]; float minHeight; float maxHeight; sampler2D testTexture; float testScale; sampler2D _NormalMap; float _NormalScale; UNITY_DECLARE_TEX2DARRAY(baseTextures); struct Input { float3 worldPos; float3 worldNormal; float2 uv_testTexture; // 修正UV命名,和Properties里的纹理名对应 INTERNAL_DATA }; float inverseLerp(float a, float b, float value) { return saturate((value - a)/(b-a)); } // 新增三平面法线采样函数,和主纹理采样逻辑对齐 float3 triplanarNormal(float3 worldPos, float scale, float3 blendAxes, sampler2D normalMap, float normalScale) { float3 scaledWorldPos = worldPos / scale; // 三个轴向上的法线采样 float3 xNormal = UnpackNormal(tex2D(normalMap, float2(scaledWorldPos.y, scaledWorldPos.z))) * blendAxes.x; float3 yNormal = UnpackNormal(tex2D(normalMap, float2(scaledWorldPos.x, scaledWorldPos.z))) * blendAxes.y; float3 zNormal = UnpackNormal(tex2D(normalMap, float2(scaledWorldPos.x, scaledWorldPos.y))) * blendAxes.z; // 混合法线并应用强度缩放 float3 blendedNormal = normalize(xNormal + yNormal + zNormal); blendedNormal.z /= normalScale; return normalize(blendedNormal); } // 原三平面颜色采样函数保留,重命名更清晰 float3 triplanarColour(float3 worldPos, float scale, float3 blendAxes, int textureIndex) { float3 scaledWorldPos = worldPos/ scale; float3 xProjection = UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaledWorldPos.y,scaledWorldPos.z, textureIndex)) * blendAxes.x; float3 yProjection = UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaledWorldPos.x,scaledWorldPos.z, textureIndex)) * blendAxes.y; float3 zProjection = UNITY_SAMPLE_TEX2DARRAY(baseTextures, float3(scaledWorldPos.x,scaledWorldPos.y, textureIndex)) * blendAxes.z; return xProjection + yProjection + zProjection; } UNITY_INSTANCING_CBUFFER_START(Props) UNITY_INSTANCING_CBUFFER_END void surf (Input IN, inout SurfaceOutputStandard o) { float heightPercent = inverseLerp(minHeight, maxHeight, IN.worldPos.y); float3 blendAxes = abs(IN.worldNormal); blendAxes /= blendAxes.x + blendAxes.y + blendAxes.z; // 初始化输出值,避免第一次循环的黑色初始值 o.Albedo = float3(0,0,0); float3 accumulatedNormal = float3(0,0,1); // 默认向上的法线 for (int i = 0; i < layerCount; i++) { float drawStrength = inverseLerp(-baseBlends[i]/2 - epsilon, baseBlends[i]/2, heightPercent - baseStartHeights[i]); float3 baseColour = baseColours[i] * baseColourStrength[i]; float3 textureColour = triplanarColour(IN.worldPos, baseTextureScales[i], blendAxes, i) * (1-baseColourStrength[i]); // 正确混合Albedo:用lerp逐步叠加图层颜色 o.Albedo = lerp(o.Albedo, baseColour + textureColour, drawStrength); // 混合法线:只有当前图层有绘制强度时才参与混合 if (drawStrength > epsilon) { float3 layerNormal = triplanarNormal(IN.worldPos, baseTextureScales[i], blendAxes, _NormalMap, _NormalScale); // 转换切线空间法线到世界空间,再和累积法线混合 layerNormal = WorldNormalVector(IN, layerNormal); accumulatedNormal = normalize(lerp(accumulatedNormal, layerNormal, drawStrength)); } } // 最后赋值累积后的法线 o.Normal = accumulatedNormal; } ENDCG } FallBack "Diffuse" }
关键修改点说明
- 修复循环覆盖问题:初始化Albedo为黑色、法线为默认向上方向,用
lerp逐步混合每个图层的颜色和法线,避免直接覆盖导致的图层丢失。 - 三平面法线采样:新增
triplanarNormal函数,和主纹理用完全一致的三平面映射逻辑采样法线贴图,保证UV匹配,还加入法线强度参数控制凹凸效果。 - 法线空间转换:用
WorldNormalVector把切线空间的法线转换到世界空间,确保光照计算正确。 - 规范UV命名:把
uv_Diffuse改成uv_testTexture,符合Unity自动关联UV的约定。
修改后记得在材质面板给_NormalMap赋值,调整_NormalScale可以控制法线的凹凸强度,这样应该就能正常显示主纹理和法线效果了。
内容的提问来源于stack exchange,提问作者M_NEN




