You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Unity GLSL Shader投射阴影与surf方法输出不匹配问题

问题描述

我为精灵表制作动画精灵编写了自定义Shader(本项目不采用Unity动画播放器的原因暂不赘述)。该Shader运行正常,但投射的阴影对应整个精灵表,而非surf方法中显示的当前帧。例如,若精灵表包含4帧,投射的会是4个独立阴影而非当前帧的阴影。
阴影异常示例
我可以通过修改材质属性中的“Tile”和“Offset”值修正阴影,但这需要CPU端逻辑更新并与Shader同步。

问题

能否仅在Shader中实现正确的阴影投射?

附原Shader代码
Shader "Custom/Pixel_Sprite_Diffuse"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _Cutoff("Cutoff", Range(0, 1)) = 0.5
        [ToggleOff]  _SpecularHighlights ("Specular Highlights", Float) = 1.000000
        [ToggleOff]  _GlossyReflections ("Glossy Reflections", Float) = 1.000000
        
        cell_x_count("Cell Count X", float) = 1
        cell_y_count("Cell Count Y", float) = 1
        frame_count("Frame Count", float) = 1
        speed("Speed", float) = 1
    }
    SubShader
    {
        Tags{ "RenderType" = "TransparentCutout" "Queue" = "Transparent"} 
        ZWrite Off

        //Tags{ "RenderType" = "Transparent" "Queue" = "TransparentCutout"} 
        //ZWrite Off Blend SrcAlpha OneMinusSrcAlpha 
        LOD 300

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard alphatest:_Cutoff  

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        float _SpecularHighlights;
        float _GlossyReflections;
        fixed4 _Color;
        float frame_count;
        float cell_x_count;
        float cell_y_count;
        float speed;
        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)


        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            int currentIndex = (int)
                    (((float)_Time * speed * 20) % frame_count);

            float2 cellSize
                = float2(
                    1.0 / cell_x_count,
                    1.0 / cell_y_count
                );

            const float2 offset = float2
            (
                 ((float)currentIndex % cell_x_count) / cell_x_count,
                  1 - (1 * floor(2.0 * (float)currentIndex * cellSize.x * cellSize.y))
            );


            const float2 cellCoord =
                float2(
                    IN.uv_MainTex.x * cellSize.x,
                    -(1 - IN.uv_MainTex.y) * cellSize.y
            );

            const float2 samplePoint = offset + cellCoord;
            float4 color =  tex2D(_MainTex, samplePoint);

            if( color.a < .01)
            {
                discard;
            }

            o.Albedo = color.rgb;
            o.Alpha = color.a;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
        }
        
        ENDCG
    }
    FallBack "Standard"
}

我此前已实现通过脚本更新材质的“tile”和“offset”来模拟动画的方案,但设置不如纯Shader简便。

解决方案

问题根源在于:Unity默认的阴影投射Pass会直接使用材质的_MainTex Tile/Offset参数采样纹理,不会执行你在surf函数中自定义的帧UV计算逻辑,因此阴影会采样整个精灵表。

要在纯Shader中解决这个问题,只需在SubShader中添加一个自定义的阴影投射Pass,复用你在surf里的UV计算逻辑,对纹理进行采样并处理alpha裁剪,确保阴影仅基于当前帧生成。

修改后的完整Shader代码如下:

Shader "Custom/Pixel_Sprite_Diffuse"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _Cutoff("Cutoff", Range(0, 1)) = 0.5
        [ToggleOff]  _SpecularHighlights ("Specular Highlights", Float) = 1.000000
        [ToggleOff]  _GlossyReflections ("Glossy Reflections", Float) = 1.000000
        
        cell_x_count("Cell Count X", float) = 1
        cell_y_count("Cell Count Y", float) = 1
        frame_count("Frame Count", float) = 1
        speed("Speed", float) = 1
    }
    SubShader
    {
        Tags{ "RenderType" = "TransparentCutout" "Queue" = "Transparent"} 
        ZWrite Off

        LOD 300

        // 自定义阴影投射Pass
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            #pragma multi_compile_shadowcaster

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Cutoff;
            float frame_count;
            float cell_x_count;
            float cell_y_count;
            float speed;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                V2F_SHADOW_CASTER;
                float2 uv : TEXCOORD1;
            };

            v2f vert(appdata v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // 复用surf中的帧UV计算逻辑
                int currentIndex = (int)(((float)_Time * speed * 20) % frame_count);
                float2 cellSize = float2(1.0 / cell_x_count, 1.0 / cell_y_count);

                float2 offset = float2(
                    ((float)currentIndex % cell_x_count) / cell_x_count,
                    1 - floor((float)currentIndex / cell_x_count) * cellSize.y
                );

                float2 cellCoord = float2(
                    i.uv.x * cellSize.x,
                    -(1 - i.uv.y) * cellSize.y
                );

                float2 samplePoint = offset + cellCoord;
                fixed4 color = tex2D(_MainTex, samplePoint);

                // Alpha裁剪,和surf保持一致
                clip(color.a - _Cutoff);

                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }

        CGPROGRAM
        #pragma surface surf Standard alphatest:_Cutoff  
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        float _SpecularHighlights;
        float _GlossyReflections;
        fixed4 _Color;
        float frame_count;
        float cell_x_count;
        float cell_y_count;
        float speed;

        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            int currentIndex = (int)(((float)_Time * speed * 20) % frame_count);
            float2 cellSize = float2(1.0 / cell_x_count, 1.0 / cell_y_count);

            const float2 offset = float2(
                 ((float)currentIndex % cell_x_count) / cell_x_count,
                  1 - floor((float)currentIndex / cell_x_count) * cellSize.y
            );

            const float2 cellCoord = float2(
                IN.uv_MainTex.x * cellSize.x,
                -(1 - IN.uv_MainTex.y) * cellSize.y
            );

            const float2 samplePoint = offset + cellCoord;
            float4 color = tex2D(_MainTex, samplePoint);

            if( color.a < .01)
            {
                discard;
            }

            o.Albedo = color.rgb;
            o.Alpha = color.a;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
        }
        
        ENDCG
    }
    FallBack "Standard"
}

说明:

  • 添加了名为ShadowCaster的Pass,指定LightMode为ShadowCaster,这是Unity识别阴影投射Pass的关键标签
  • 在阴影Pass的片段着色器中,完全复用了你在surf中的帧UV计算逻辑,确保采样的是当前帧的纹理区域
  • 保持了和surf一致的Alpha裁剪逻辑,确保阴影的透明区域和渲染结果匹配
  • 修正了原代码中offset计算的错误(原代码中floor(2.0 * ...)逻辑有误,改为floor(currentIndex / cell_x_count)来正确计算行索引)

内容的提问来源于stack exchange,提问作者Marco Flores

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 18:44:53