这两个功能走的是完全不同的两条路径,在这份 Shader 里对应的代码也完全不同。
一、Cast Shadows(投射阴影)------ 由 ShadowCaster Pass 承担
被谁调用:URP 在渲染 Shadow Map 时(光源视角),会遍历场景里所有物体,找出 "有 ShadowCaster Pass 且 MeshRenderer.Cast Shadows = On" 的,单独跑一遍这个 Pass。
对应代码:
zmx_TFD_CustomToon_Code.shaderLines 82-151
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
Cull Back
HLSLPROGRAM
...
float4 GetShadowPositionHClip(ShadowAttributes IN)
{
float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
float3 normalWS = TransformObjectToWorldNormal(IN.normalOS);
...
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS));
...
}
...
half4 shadowFrag(ShadowVaryings IN) : SV_Target
{
return 0;
}
ENDHLSL
}
关键点:
- 这个 Pass 写入的是光源视角的 Shadow Map 纹理
ColorMask 0不输出颜色,return 0的片段输出也不重要,只要ZWrite On把深度写对就行ApplyShadowBias是为了减少自阴影 acne- 如果删掉这个 Pass → 物体不会出现在 Shadow Map 里 → 它投不出阴影,但依然会在 Forward Pass 里正常渲染
二、Receive Shadows(接收阴影)------ 由 Forward Pass 里的这段代码承担
被谁调用:常规渲染流程(相机视角),Forward Pass 采样别人写好的 Shadow Map,判断当前像素是否被挡住。
对应代码:
zmx_TFD_CustomToon_Code.shaderLines 37-76
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile_fragment _ _SHADOWS_SOFT
...
half4 frag(Varyings IN) : SV_Target
{
#if defined(MAIN_LIGHT_CALCULATE_SHADOWS)
float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS);
#else
float4 shadowCoord = float4(0, 0, 0, 0);
#endif
Light mainLight = GetMainLight(shadowCoord);
float lightAtten = mainLight.distanceAttenuation * mainLight.shadowAttenuation;
return half4((half3)lightAtten, 1.0);
}
关键点:
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS ...:开启阴影关键字,让 URP 知道这个 Shader 想接收阴影TransformWorldToShadowCoord(positionWS):把当前像素的世界坐标变换到 Shadow Map 空间GetMainLight(shadowCoord):内部会调用MainLightRealtimeShadow(shadowCoord)采样 Shadow Map,把采样结果放到mainLight.shadowAttenuation(0 = 完全在阴影中,1 = 完全被光照)- 最后
lightAtten = distanceAttenuation * shadowAttenuation就是"接收阴影"的结果
如果把 _MAIN_LIGHT_SHADOWS 改成默认关闭 → MAIN_LIGHT_CALCULATE_SHADOWS 宏不会定义 → shadowCoord = 0 → GetMainLight 里的 shadowAttenuation 永远返回 1 → 物体不接收阴影(但别人投到它身上的阴影不会显示)
三、两者的职责对照表
| 行为 | 相关 Pass / 代码 | 关键 API | 写入 / 读取 |
|---|---|---|---|
| Cast(投出阴影) | ShadowCaster Pass |
ApplyShadowBias + TransformWorldToHClip |
写入 光源的 Shadow Map |
| Receive(接收阴影) | Forward Pass 的 frag |
TransformWorldToShadowCoord + GetMainLight(shadowCoord) |
读取 已经写好的 Shadow Map |
四、组合效果
| Cast | Receive | 结果 |
|---|---|---|
| ✓ | ✓ | 能投阴影到别人身上,自己也显示别人/自己投过来的阴影(默认、最常见) |
| ✓ | ✗ | 像"完美白纸" ------ 挡光能让地面有影子,但自己身上没有影子 |
| ✗ | ✓ | 自己能显示影子(例如地板),但自己不挡光(角色看不到它的投影) |
| ✗ | ✗ | 和阴影系统完全无关(比如自发光特效片) |
Unity Inspector 上 Mesh Renderer 的两个勾:
Cast Shadows = On/Off→ 控制 URP 要不要调用你的ShadowCasterPassReceive Shadows = On/Off→ 控制 URP 要不要给你定义_MAIN_LIGHT_SHADOWS关键字
五、在你当前这份 Shader 里的体验
因为这个 Shader 的输出就是 lightAttenuation.xxx 灰度:
- Receive Shadows 的结果你能直接肉眼看见:被阴影遮住的地方
shadowAttenuation = 0,物体表面就是黑色;被照亮的地方是白色。整个物体像一张可视化的"阴影热力图"。 - Cast Shadows 的结果你要在别的物体上才能看到:比如把这个物体放到地面上方,地面上会出现它的影子。
这也是为什么它叫"Toon"------典型卡通渲染里常需要把 shadowAttenuation 这个硬边阴影值单独提取出来再做处理。
cs
// 从 ASE 版本 zmx_TFD_CustomToon.shader 翻写的手写 URP Unlit Shader
//
// ASE 节点图等价表达:
// LightAttenuation --> MasterNode(Forward).Color
// 即:
// Color.rgb = (mainLight.distanceAttenuation * mainLight.shadowAttenuation).xxx
Shader "Toon/zmx_TFD_CustomToon_Code"
{
Properties
{
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalPipeline"
"RenderType" = "Opaque"
"Queue" = "Geometry"
"UniversalMaterialType" = "Unlit"
}
LOD 100
Pass
{
Name "Forward"
Tags { "LightMode" = "UniversalForwardOnly" }
ZWrite On
ZTest LEqual
Cull Back
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
};
CBUFFER_START(UnityPerMaterial)
CBUFFER_END
Varyings vert(Attributes IN)
{
Varyings OUT;
VertexPositionInputs posInputs = GetVertexPositionInputs(IN.positionOS.xyz);
OUT.positionCS = posInputs.positionCS;
OUT.positionWS = posInputs.positionWS;
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
#if defined(MAIN_LIGHT_CALCULATE_SHADOWS)
float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS);
#else
float4 shadowCoord = float4(0, 0, 0, 0);
#endif
Light mainLight = GetMainLight(shadowCoord);
float lightAtten = mainLight.distanceAttenuation * mainLight.shadowAttenuation;
return half4((half3)lightAtten, 1.0);
}
ENDHLSL
}
// ShadowCaster: 让物体自身能投射阴影到其它物体的 Shadow Map 上
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
ZWrite On
ZTest LEqual
ColorMask 0
Cull Back
HLSLPROGRAM
#pragma vertex shadowVert
#pragma fragment shadowFrag
#pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
// URP ShadowCaster 需要这两个由引擎设置的变量:
// _LightDirection:主方向光 / Spot 的方向(世界空间)
// _LightPosition :点光源的位置(仅 Punctual 阴影使用)
float3 _LightDirection;
float3 _LightPosition;
struct ShadowAttributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct ShadowVaryings
{
float4 positionCS : SV_POSITION;
};
float4 GetShadowPositionHClip(ShadowAttributes IN)
{
float3 positionWS = TransformObjectToWorld(IN.positionOS.xyz);
float3 normalWS = TransformObjectToWorldNormal(IN.normalOS);
#if _CASTING_PUNCTUAL_LIGHT_SHADOW
float3 lightDirWS = normalize(_LightPosition - positionWS);
#else
float3 lightDirWS = _LightDirection;
#endif
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS));
// 防止阴影偏移后越过近裁剪面
#if UNITY_REVERSED_Z
positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#else
positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
#endif
return positionCS;
}
ShadowVaryings shadowVert(ShadowAttributes IN)
{
ShadowVaryings OUT;
OUT.positionCS = GetShadowPositionHClip(IN);
return OUT;
}
half4 shadowFrag(ShadowVaryings IN) : SV_Target
{
return 0;
}
ENDHLSL
}
// DepthOnly: 为需要 _CameraDepthTexture 的效果(SSAO / 软粒子 / 后处理等)提供深度
Pass
{
Name "DepthOnly"
Tags { "LightMode" = "DepthOnly" }
ZWrite On
ColorMask 0
Cull Back
HLSLPROGRAM
#pragma vertex depthVert
#pragma fragment depthFrag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct DepthAttributes
{
float4 positionOS : POSITION;
};
struct DepthVaryings
{
float4 positionCS : SV_POSITION;
};
DepthVaryings depthVert(DepthAttributes IN)
{
DepthVaryings OUT;
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
return OUT;
}
half4 depthFrag(DepthVaryings IN) : SV_Target
{
return 0;
}
ENDHLSL
}
}
FallBack "Hidden/Universal Render Pipeline/FallbackError"
}