You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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.Normalo.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

火山引擎 最新活动