自定义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
相关产品推荐
相关产品推荐

