news 2026/10/1 19:58:40

Unity自定义Shader阴影消失?彻底搞懂ShadowCaster实现投射与接收

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity自定义Shader阴影消失?彻底搞懂ShadowCaster实现投射与接收

很多人刚开始自己写Unity Shader时都会撞上一堵墙:从Asset Store拖下来的模型,换上自己写的Unlit Shader,地面上干干净净,影子没了。我当时也干过这事,翻来覆去调Lighting设置、检查Renderer,死活想不明白,最后才发现问题根本不在场景,而在Shader自己少写了一个Pass——也就是标题里提到的shadowCast。这篇就把"产生阴影和接收阴影"这件事彻底拆开,从阴影映射原理讲到可以直接抄的Shader代码,再聊几个我实际项目里踩过的坑。想搞懂Unity阴影原理、或者正被自定义Shader阴影问题卡住的人,这篇都能给你一个能落地的答案。

1. 阴影是怎么画出来的:Caster与Receiver的分工

很多人以为Unity的阴影是引擎"顺手"生成的,模型放那,影子自然就有。实际上实时阴影远没有这么智能,它本质上是一种"以空间换时间"的近似算法,背后是一整套Shadow Mapping流程。

1.1 Shadow Map先把"谁被光源照亮"变成一张深度图

实时阴影最常用的方案是Shadow Mapping,中文叫阴影映射。思路非常直白:从光源位置放一台"虚拟摄像机"朝场景拍一张深度图,记录它看到的每一个点的深度。这张深度图就是Shadow Map,可以理解为"从光源视角看过去,最前面那层物体离光源多远"。

到了正常渲染阶段,GPU渲染物体时会把当前片元也变换到光源空间,然后取出Shadow Map里对应位置的深度值做一次比较:如果当前片元离光源更远,说明它被某个更靠近光源的物体挡住了,于是判定为阴影;如果深度差不多或者更近,说明它直接暴露在光线下,就不在阴影里。就这么一个简单的思想,支撑起了当今绝大多数实时渲染引擎的阴影效果。

Unity里这套流程被高度封装,你看不到每次阴影渲染的具体过程,但幕后它就是按这个逻辑走的。方向光、聚光灯、点光源都走Shadow Map,只是投影矩阵和深度计算方式略有差异。需要注意的是,渲染Shadow Map时,引擎并不会把你屏幕上看到的模型完整画一遍,它只关心深度,颜色一概忽略。

1.2 Unity里Cast和Receive是两趟独立的Drawing

在Unity里,阴影被拆成两个独立环节:产生阴影(Cast)和接收阴影(Receive)。

产生阴影,对应的是引擎在渲染Shadow Map阶段,需要把物体画进深度图里。这一步必须执行Shader中的一个特殊Pass,也就是LightMode标记为ShadowCaster的Pass。如果Shader里找不到这个Pass,或者材质没有Fallback,物体在Shadow Map里就是透明的,自然不会产生阴影。

接收阴影,对应的是正常渲染物体时,在当前物体的Pass里采样Shadow Map,并和片元深度做比较,算出阴影衰减值。这一步通常发生在ForwardBase或ForwardAdd这样的光照Pass中,需要Shader里有一系列专门的宏参与。

这两件事是独立的,A物体可以只投射不接收,B物体也可以只接收不投射。Mesh Renderer上的Cast Shadows和Receive Shadows两个开关,分别控制着这两个方向。很多新手只注意了场景灯光里有没有开阴影,却忽略了Renderer和Shader两侧的状态,这就是阴影莫名其妙消失的头号原因。

环节实际发生的阶段依赖的Shader PassRenderer开关
产生阴影渲染Shadow Map时LightMode=ShadowCaster的PassCast Shadows
接收阴影正常渲染物体时光照Pass(ForwardBase/ForwardAdd)Receive Shadows

理解了这个分工,就明白为什么自定义Shader最容易出问题——它可能只有普通的颜色Pass,根本没有为"产生阴影"这件事准备任何东西。

2. 物体不投影的根本原因:Shader里没有ShadowCaster Pass

先看一个最常见的场景:你用Shader Graph或者手写代码新建了一个Unlit Shader,模型显示正常,颜色也对,但投射阴影选项怎么开都没效果。说破大天,就是因为这个Shader压根没有ShadowCaster Pass。

2.1 症状:物体凭空消失的阴影

在Unity内置渲染管线里,一个很简单的Unlit Shader可能长这样:

Shader "Custom/MyUnlit" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return fixed4(1, 0, 0, 1); } ENDCG } } }

这个Shader没有任何LightMode标记,也没有Fallback。Unity渲染Shadow Map时拿着这个材质找ShadowCaster Pass,找遍SubShader都找不到,于是模型就不参与Shadow Map渲染。屏幕上模型红彤彤地站着,地上却连个轮廓都没有。

这时候如果你打开Frame Debugger看阴影图渲染那一步,会发现DrawCall列表里压根没有这个物体的条目。可惜大多数人在排查时第一反应是调灯光强度、调Shadow Distance,绕了一大圈,问题其实就出在Shader自己。

2.2 Fallback能救急,但救不了顶点动画

遇到这种情况,很多人的第二个反应是:给Shader加一行Fallback。

Fallback "VertexLit"

加上之后,Unity在找不到ShadowCaster Pass时会回退到Fallback指定的Shader,从那里找ShadowCaster Pass继续执行。于是阴影出来了,问题看起来解决了。

但这种救急方案在后头很容易埋雷。最典型的坑有两个:

第一,顶点动画对不上。如果你的Shader在顶点阶段做了位移,比如草随风摆动、旗帜飘动、角色衣服跟着骨骼动画顶点的逻辑变化,而Fallback的内置Shader没有这一段顶点位移逻辑,阴影就会按照模型原始姿态去投,出现"影子在空中""影子和模型分离"这种诡异画面。

第二,镂空裁剪失效。树的树叶、铁栅栏这类需要用clip()裁剪透明像素的材质,用了Fallback后,内置ShadowCaster里没有你的clip逻辑,阴影直接变成一个实心方块。远处看一棵镂空的树,地面阴影却是一团圆乎乎的实心黑影,非常出戏。

实际上Fallback的正确使用场景是:你这个Shader真的没有特殊裁剪和顶点动画,只是一个普通的实体模型。但只要你动了顶点位置,或者在片段阶段做了clip,就必须考虑写自定义ShadowCaster Pass。

3. 手写ShadowCaster Pass:让阴影跟着顶点动画和透明边缘走

既然Fallback靠不住,那就自己把ShadowCaster Pass写出来。这一步听着玄乎,实际代码量非常小,关键是要理解里面每个宏在干什么。

3.1 最基础的ShadowCaster Pass模板

内置管线中,一个最常见的ShadowCaster Pass长这样:

Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct v2f { V2F_SHADOW_CASTER; }; v2f vert (appdata_base v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } fixed4 frag (v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG }

你不用管V2F_SHADOW_CASTER展开后到底是什么结构,只需要知道它是为一个ShadowCaster专门准备的顶点到片段的数据结构。TRANSFER_SHADOW_CASTER_NORMALOFFSET这个宏做了两件事:把顶点从模型空间变换到裁剪空间,同时叠加一个基于法线方向的偏移量。这个偏移量就是Normal Bias,它可以把阴影表面稍微往光源方向"推"一点,避免阴影粉刺(画面上一闪一闪的密集黑点)。

#pragma multi_compile_shadowcaster也很重要,它会在不同平台和不同灯光类型下编译出对应的Shader变体。Unity的ShadowCaster在处理方向光、聚光灯、点光源时,有些计算路径不一样,这个编译指令帮你把变体一次性备齐。

片段阶段几乎不用做计算,直接返回0即可,因为Shadow Map只关心深度值,不关心颜色。我把这段代码塞进一个带Properties的完整Shader里,需要时直接复制用。

3.2 把Clipping放进来:镂空阴影的正确姿势

如果你的材质需要镂空效果,ShadowCaster Pass就多了一个操作:在片段阶段执行和主Pass一致的clip。拿树叶来举例,主Pass会采样透明度贴图,使用clip(alpha - _Cutoff)把透明部分丢掉。ShadowCaster里也必须做同样的事情,否则又回到透明部分也投实心阴影的老路。

sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; struct v2f { V2F_SHADOW_CASTER; float2 uv : TEXCOORD1; }; v2f vert (appdata_base v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 tex = tex2D(_MainTex, i.uv); clip(tex.a - _Cutoff); SHADOW_CASTER_FRAGMENT(i) }

关于TEXCOORD的编号,需要留个心眼。V2F_SHADOW_CASTER本身已经占了一组坐标,所以我这里把uv放在TEXCOORD1,避免冲突。clip函数一旦执行,满足条件的片元会被直接丢弃,不会写入Shadow Map。这样镂空部分在阴影里也是镂空的,阳光穿过树叶缝隙洒下来的影子才真实。

3.3 顶点动画怎么让阴影跟着动

顶点动画场景下,ShadowCaster的修改更简单粗暴:在vert阶段把主Pass里那套顶点偏移逻辑再执行一遍。

比如一棵被风吹动的草,主Pass里对顶点做了这样的弯曲:

float3 offset = float3(sin(_Time.y + v.vertex.x * 2.0), 0, 0) * _WindStrength; v.vertex.xyz += offset;

那么在ShadowCaster的vert里,你必须在调用TRANSFER_SHADOW_CASTER_NORMALOFFSET之前,先做一模一样的顶点位移。这样阴影的轮廓才会跟随草的实际弯曲状态。很多项目里草来回摆,地面阴影却像钉子一样钉在原地,就是因为这步缺失。

但这里引出一个新问题,也是很多做了多年开发的老手偶尔也会翻车的地方:即使ShadowCaster里做了顶点偏移,如果Renderer的包围盒没有跟着扩大,Unity在视锥剔除阶段可能把物体整个丢掉,阴影自然也没了。这个坑我在后面专门讲。

4. 接收阴影的写法:在光照Pass里采样Shadow Map

Shader能产生阴影只是第一步,真正让物体表面变暗的是接收阴影环节。如果接收端没写对,物体依然一片光亮,阴影效果同样出不来。

4.1 几个宏搞定阴影采样:SHADOW_COORDS / TRANSFER_SHADOW / SHADOW_ATTENUATION

内置管线的接收阴影逻辑,其实被AutoLight.cginc封装得非常友好。你需要在光照Pass中做三件事。

第一,在顶点到片元的结构体中声明阴影坐标:

struct v2f { float2 uv : TEXCOORD0; SHADOW_COORDS(1) float4 pos : SV_POSITION; };

SHADOW_COORDS(1)展开后是一个_ShadowCoord成员,存放在TEXCOORD1。这里不要和UV冲突,UV占TEXCOORD0,阴影坐标就从TEXCOORD1开始。

第二,在顶点Shader中通过TRANSFER_SHADOW宏把坐标传过去:

v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); TRANSFER_SHADOW(o) return o; }

这个宏内部会根据平台差异,正确计算Shadow Map采样用的屏幕坐标或光源空间坐标。

第三,在片段Shader中调用SHADOW_ATTENUATION得到阴影衰减值:

fixed4 frag (v2f i) : SV_Target { fixed shadow = SHADOW_ATTENUATION(i); // shadow为0表示完全在阴影里,1表示完全在光照下 return fixed4(shadow, shadow, shadow, 1); }

SHADOW_ATTENUATION返回的就是一个0到1的黑白因子,你可以直接把它乘进颜色里。对于没有复杂光照计算的Unlit Shader,这招尤其好用——不需要引入完整的光照模型,就能让物体"吃到"来自方向光的阴影。

4.2 一个能用的接收阴影Shader示例

下面给一个完整的、在Unity内置渲染管线里可以直接跑通的接收阴影Shader,它包含最简单的漫反射光照,同时接收主光源方向光的阴影:

Shader "Custom/ShadowReceiverDemo" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Color", Color) = (1,1,1,1) } SubShader { Pass { Tags { "LightMode" = "ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; SHADOW_COORDS(2) }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal = UnityObjectToWorldNormal(v.normal); TRANSFER_SHADOW(o) return o; } fixed4 frag (v2f i) : SV_Target { fixed shadow = SHADOW_ATTENUATION(i); fixed3 worldNormal = normalize(i.worldNormal); fixed3 lightDir = normalize(_WorldSpaceLightPos0.xyz); fixed ndotl = saturate(dot(worldNormal, lightDir)); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb; fixed3 diffuse = _LightColor0.rgb * ndotl * shadow; fixed4 tex = tex2D(_MainTex, i.uv); fixed3 finalColor = (ambient + diffuse) * tex.rgb * _Color.rgb; return fixed4(finalColor, 1); } ENDCG } } }

这段代码里,#pragma multi_compile_fwdbase同样关键。它告诉编译器要生成ForwardBase需要的一系列变体,其中就包括是否启用阴影。如果漏掉这个编译指令,Unity会自动跳过阴影关键字的变体,SHADOW_ATTENUATION展开后很可能等于一个空值,最终画出来就是完全没有阴影。

如果你的Shader是纯Unlit,不想算光照,那你完全可以把上面代码里的Diffuse部分去掉,只保留ambient和shadow相乘的效果。但注意,纯Unlit接收阴影在严格意义上是一种"手动画上去"的模拟,物体之间如果有半透明穿插,效果只能说够用,不是物理级正确。

5. URP迁移:ShadowCaster从CG到HLSL的变化

Unity官方已经把重心全面转到URP/HDRP上,新项目很少有人再写内置管线Shader了。URP虽然保留了LightMode=ShadowCaster这套Tag,但内部的写法从CG宏变成了一套更显式的HLSL函数。这一节单独展开,方便迁移项目的朋友少踩坑。

5.1 URP的ShadowCaster:Tag没变,语言和宏换了

URP里手写ShadowCaster Pass,结构上依然是一个带Tags { "LightMode" = "ShadowCaster" }的Pass。不同之处在于,不再用TRANSFER_SHADOW_CASTER_NORMALOFFSET这种封装宏,而是自己封装一个GetShadowPositionHClip函数,调用URP阴影库里的ApplyShadowBias。

下面是一个URP下带顶点动画和裁切能力的ShadowCaster模板:

Pass { Name "ShadowCaster" Tags { "LightMode" = "ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull Off HLSLPROGRAM #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; float4 GetShadowPositionHClip(Attributes input) { float3 positionWS = TransformObjectToWorld(input.positionOS.xyz); float3 normalWS = TransformObjectToWorldNormal(input.normalOS); // 这里插入顶点动画,比如草的弯曲 positionWS.x += sin(_Time.y + positionWS.x * 2.0) * 0.1; float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection)); #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; } Varyings ShadowPassVertex(Attributes input) { Varyings output; output.positionCS = GetShadowPositionHClip(input); output.uv = TRANSFORM_TEX(input.uv, _MainTex); return output; } half4 ShadowPassFragment(Varyings input) : SV_TARGET { fixed4 tex = tex2D(_MainTex, input.uv); clip(tex.a - _Cutoff); return 0; } ENDHLSL }

URP的ShadowCaster会用到_LightDirection这个变量,它来自Shadow库,不需要自己在Properties里声明。ApplyShadowBias的作用相当于内置管线里Normal Bias那一步,会对裁剪空间坐标做偏移。ColorMask 0的意思是Shadow Pass里不输出任何颜色,只写深度,这和内置管线里frag返回0是同一个思路。

5.2 URP里接收阴影要自己组装ShadowCoord

URP里接收阴影和内置管线最大的不同是,不再有SHADOW_COORDS这类宏,你得自己声明阴影坐标并手动传给光照函数。

以URP的一个自定义Unlit Shader为例,接收阴影的套路是这样的:

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _SHADOWS_SOFT #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 normalWS : TEXCOORD1; float2 uv : TEXCOORD2; }; Varyings vert(Attributes input) { Varyings output; output.positionWS = TransformObjectToWorld(input.positionOS.xyz); output.positionCS = TransformWorldToHClip(output.positionWS); output.normalWS = TransformObjectToWorldNormal(input.normalOS); output.uv = input.uv; return output; } half4 frag(Varyings input) : SV_Target { float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS); Light mainLight = GetMainLight(shadowCoord); float shadow = mainLight.shadowAttenuation; half4 color = tex2D(_MainTex, input.uv); color.rgb *= shadow; return color; }

URP里如果你用的是标准Lit Shader,阴影接收不用你操心,官方已经处理好了。但如果你写的是一个高度定制的Shader,就需要手动把TransformWorldToShadowCoord和GetMainLight接起来。需要注意,_MAIN_LIGHT_SHADOWS和_MAIN_LIGHT_SHADOWS_CASCADE这两组multi_compile必须写上,否则当URP开启级联阴影时,你的Shader因为没有对应变体,阴影坐标计算会出错或者干脆不采样。

URP不同小版本API有差异,比如早期版本里ApplyShadowBias可能叫UnityApplyLinearShadowBias,发现编译报错时直接去Packages缓存里翻对应版本的源码,照着官方Lit Shader的ShadowCaster Pass改是最稳的。

6. 实战中容易踩的阴影坑与调试方法

原理讲完,代码给出,最后分享几个我在实际项目中反复踩过的坑。每一个都真实消耗过我的时间,写出来帮大家少走弯路。

6.1 Mesh包围盒把阴影"剪"掉了

这是顶点动画类Shader最容易踩的隐形坑。你给草写了顶点弯曲,主Pass阴影看着也正常,但只要草离镜头远一点,或者风向导致摆动幅度突然变大,整棵草突然消失,地面阴影也跟着没了。

原因在于,Unity在渲染前会先用Mesh Renderer的包围盒做视锥剔除。Shader里对顶点的偏移是发生在GPU端的,Unity CPU端完全不知道顶点位移到了哪里,它只拿原始网格的包围盒判断"这个物体是否在视野内"。一旦顶点偏移幅度超过原始包围盒范围,CPU认为物体不可见,直接跳过渲染,阴影自然也一起消失。

解决办法是在脚本里手动扩展包围盒:

MeshRenderer mr = GetComponent<MeshRenderer>(); Bounds bounds = mr.bounds; bounds.Expand(expandSize); mr.bounds = bounds;

expandSize至少取值要覆盖顶点最大位移幅度,最好留一定余量。这个操作要在Awake或Start里执行,并且如果顶点动画幅度极大,还要考虑每帧更新或者用足够大的固定包围盒。否则你会看到远处物体的阴影时有时无,非常迷惑。

6.2 阴影粉刺和Bias参数的搭配

画面表面出现密密麻麻的黑色闪烁小点,这就是Shadow Acne,阴影粉刺。原因是Shadow Map记录的是离散的深度值,当表面几乎平行于光源方向时,相邻像素的采样深度和自己投影到光源空间的深度差距极小,深度比较的误差就被放大了,造成"自己误判挡住自己"。

解决办法有三板斧:

第一,在Light组件上适当调大Bias和Normal Bias值。Bias是深度偏移,让阴影判断放宽一点;Normal Bias是法线偏移,让采样坐标往法线方向挪一点。

第二,增加Shadow Map的分辨率。Project Settings里可以调Shadow Resolution,分辨率越高,深度值越精细,粉刺就越不明显。

第三,在ShadowCaster里用好Normal Offset。内置管线中TRANSFER_SHADOW_CASTER_NORMALOFFSET已经帮你做了这件事,URP里的ApplyShadowBias同样内置了normal bias。不要为了追求代码简化把这步省掉。

不过Bias也不是越大越好。Bias太大会出现Peter Panning,效果是物体的阴影和物体本身明显分离,看起来像飘在空中。这个平衡没有标准答案,只能根据场景里灯光的角度和物体尺寸实测。

6.3 Frame Debugger:判断阴影到底画没画

如果阴影还是出不来,不要再瞎猜了,直接打开Frame Debugger看绘制事件。

路径是:Window -> Analysis -> Frame Debugger。开启后,可以看到Unity每一帧渲染的所有事件。翻到ShadowMap相关的DrawCall,比如"RenderShadowMap"或"Shadow Depth",点进去查看里面绘制的Mesh列表。如果你的物体出现在列表里,说明Caster侧没问题;如果找不到,问题大概率在Shader的ShadowCaster Pass上。如果Caster侧正常,但最终画面还是没阴影,再去检查接收端的Pass代码以及Renderer的Receive Shadows开关。

这个方法比我肉眼排查快得多,是判断阴影链路故障的黄金手段。

6.4 透明物体和双面阴影的特殊处理

半透明材质在阴影上是一个老大难问题。Shadow Map只存深度不存颜色,它没办法像颜色渲染那样做真正的半透明混合。所以Transparent类型的Shader投出的阴影,默认是一个不透明的实心形状。如果你让一个玻璃杯投阴影,地面上的影子往往是全黑的,不透明。

合理的变通方案有三种:

第一种,对镂空效果使用clip而不是真正的透明混合,树叶、铁丝网、栅栏都走这个路线,阴影效果很自然。

第二种,如果物体本质上不透明但形状单薄,比如一面墙壁的片面模型,记得把Mesh Renderer的Cast Shadows设为Two Sided。这样Unity会专门对背面做一次ShadowCaster渲染,让单面模型也能投出完整阴影,避免出现"只投一半"的怪影。

第三种,如果实在需要半透明阴影,就得靠自定义Projector或者另外一张低透明度贴图做装饰,这已经属于假阴影范畴,不属于ShadowCaster Pass本身的能力。

双面阴影还有个隐藏性能点:开启Two Sided后,Shadow Map渲染量会翻倍,因为要额外画背面。移动端上如果场景里大量使用这个选项,建议评估一下GPU开销。


这一篇围绕shadowCast把产生阴影和接收阴影的地基打扎实了。下一篇我打算接着写阴影在视觉表现上的进阶,比如自定义软阴影、Cascade级联阴影的调参、以及移动端阴影性能优化的思路。这些东西如果开头没打好底子,后面越调越懵,所以基础部分值得花点时间捋清楚。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 19:55:40

UltraEdit关联modelsim调试:Verilog仿真环境配置与排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 19:55:20

深入UDP:报文头、IP分片与C#分包组包实战

1. 为什么先聊UDP&#xff1a;一个“不可靠协议”撑起了半个互联网做网络排查做到今天&#xff0c;我手机里存得最多的不是TCP抓包&#xff0c;反而是UDP那一堆看上去“没头没尾”的数据报。之所以这么说&#xff0c;是因为TCP出了问题往往有重传、有状态、有日志可循&#xff…

作者头像 李华
网站建设 2026/10/1 19:54:45

准备你的行囊——用 TaoToken 搭建 NASM + UltraEdit 汇编开发环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 19:53:11

编辑预览正常,导出却变了?排查 Canvas 尺寸与绘制顺序

图片编辑器里&#xff0c;预览看起来没有问题&#xff0c;下载后却出现文字位置不对、图层被遮住&#xff0c;或透明区域变成白色。遇到这类现象&#xff0c;我会先把“显示出来的画面”和“被编码的像素”拆开检查&#xff0c;而不是立即怀疑 toBlob。 本文以我维护的图片猫&…

作者头像 李华
网站建设 2026/10/1 19:53:10

claude code 配置 Github mcp server:把 settings 改到 TaoToken 的完整步骤

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华