资讯动态

Unity透明物体投射阴影全解析:Shader、Shadow Proxy与URP实战

发布时间:2026/10/4 6:40:00 来源:尧图企业网站定制
入行做渲染也有些年头了隔三差五就会被问一个问题Unity里让透明物体投射阴影该怎么做我第一次接到类似需求时也以为很简单——把材质Cast Shadows打开不就行了实际一测试就傻眼半透明物体要么没有影子要么影子直接从“实心体”穿过投到地面上还是一坨浓黑一点看不出透明质感。这个问题说穿了是Unity默认阴影机制和渲染队列在打架不看透这一点改起来纯靠瞎试。这篇东西我按自己的实际排查过程来写从为什么失败、怎么用Shader解决、到不动Shader也能出影子的保底方案再讲透明物体接收阴影的坑最后附上我踩过的坑汇总。适合正在Shader和阴影设置里头秃的Unity开发者无论你是做数字孪生、VR比如Pico4开发还是普通3D场景思路都能直接用。1. 为什么透明物体默认投射不了阴影1.1 阴影映射依赖深度透明物体偏偏“不占座”要讲清楚这个问题得先把Unity的阴影原理摊开。现实世界里的影子不区分物体是玻璃还是砖头光穿不过去就有影。但Unity的Shadow Map不是这么想的它先把场景从光源视角渲染一遍深度图再在相机渲染时把每个片元变换到光源空间里和这张深度图做对比从而判断某个位置是否被挡住。这套机制默认只关心“不透明物体写入深度”而Unity对Transparent队列的默认处理是不写深度、不参与深度测试带来的遮挡判断自然也不会被记进Shadow Map。于是一个带标准Transparent材质的Cube哪怕你打开了Cast Shadows多数情况下没有任何反应。为什么会这样因为Unity的阴影投射依赖一个特殊的Pass叫ShadowCaster。这个Pass在渲染管线里只负责从光源视角输出深度不参与正常相机画面。Unity内置的透明Shader大多没有提供这个Pass或者即使提供了RenderType和Queue不对管线也不会正常调用它。你从材质Inspector里看到的“Cast Shadows”本质上只是一个材质参数Shader内部如果没有对应的Pass勾了也白勾。很多人在这一步就开始怀疑版本问题、平台问题其实方向从一开始就偏了。1.2 “透明阴影”需求先分三类做这个需求最容易翻车的就是上来就找“一个万能开关”。实际项目里“透明物体投射阴影”至少能拆成三种完全不同的诉求半透明物体自身要在地面或墙面上形成阴影。比如玻璃门、塑料外壳、纱帘、半透明塑料板。镂空透明物体需要按Alpha镂空形状投影。比如铁丝网、绿植叶片、带花纹的格栅。透明物体需要接收别人的阴影。比如半透明玻璃台面上要能看到杯子的影子落在上面。这三种诉求背后的原理和代码路径并不完全相同。前两种靠ShadowCaster Pass或Shadow Proxy就能解决第三种还涉及ZWrite、混合和阴影采样顺序处理起来更麻烦。所以动手之前一定要先跟策划或美术确认一句“你到底要哪种效果”这句话能帮你省掉一大半返工时间。我见过太多人做了一半发现需求根本是另一个方向气得摔键盘。2. 主流解法给透明Shader补一个ShadowCaster Pass2.1 内置渲染管线一个可直接用的ShadowCaster Pass如果你的Shader代码自己手里有最干净的做法就是给透明Shader额外加一个Pass专门负责投射阴影。这个思路适用于内置渲染管线也就是传统的Built-in管线。先看一段我常用在玻璃、水晶、半透明塑料上的Shader核心代码Shader Custom/TransparentShadowCaster { Properties { _MainTex (Main Texture, 2D) white {} _Color (Color, Color) (1,1,1,1) _Cutoff (Alpha Cutoff, Range(0,1)) 0.5 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off // 正常的透明渲染Pass Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * _Color; return col; } ENDCG } // 阴影投射Pass Pass { Tags { LightModeShadowCaster } ZWrite On ColorMask 0 CGPROGRAM #pragma vertex vertShadow #pragma fragment fragShadow #pragma multi_compile_shadowcaster #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; float _Cutoff; struct v2f { V2F_SHADOW_CASTER; float2 uv : TEXCOORD1; }; v2f vertShadow (appdata_base v) { v2f o; o.uv TRANSFORM_TEX(v.texcoord, _MainTex); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } fixed4 fragShadow (v2f i) : SV_Target { fixed alpha tex2D(_MainTex, i.uv).a; clip(alpha - _Cutoff); SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }其实这段代码的关键点就两个。一是多了一个只用深度、不写颜色的ShadowCaster Pass二是用主纹理的Alpha做clip。如果你要的是镂空阴影比如铁丝网或被风刮动的树叶就把Cutoff调到一个合适的阈值Alpha小于阈值的片元直接丢弃影子自然跟着缺口走。如果你要的是普通玻璃、塑料壳那种全轮廓影子不需要clipfragShadow里直接返回影子就是整个物体的轮廓。注意这个Pass里ColorMask 0的意思是我完全不往颜色缓冲里写东西只写深度所以它不会干扰透明物体本来的视觉表现。有一点我必须提前说不要试图在ShadowCaster Pass里用Blend把阴影调成半透明。我见过有同事在ShadowCaster里写混合结果不是影子整体变灰就是产生一堆闪烁。Shadow Map本质上是“有遮挡/无遮挡”的深度信息不是RGBA图它不记录透明度。真要做多层透明阴影得用后面说到的Projector伪影子方案。2.2 半透明影子的“透明度”到底怎么表达这里要单独拉出来说因为这是新手最容易钻牛角尖的地方。很多美术以为既然是半透明物体投射出来的阴影也应该半透明最好还能泛着点玻璃的冷色调。可惜实时Shadow Map这一层做不到这种“半透明深度”。它只回答一个问题从光源看过去这个像素的前面到底有没有挡住它的东西。有就是阴影没有就没有阴影。所以透明物体投射出来的影子天然是实心的。那游戏里那些看起来“比较淡”的影子是怎么来的两种可能一是光源的Shadow Strength参数调低了或者用的是软阴影阴影边缘模糊视觉上就显得淡二是阴影贴图分辨率不高在远距离上产生了类似半透明的过渡感。如果你真想做一个“半透明物体的半透明影”最可控的办法不是改ShadowCaster而是在地面上叠一张带透明度的贴花或Projector纹理用美术手绘的模糊形状去模拟。这个方法我们后面细说。2.3 URP和Shader Graph下的做法使用URP的读者也别慌思路完全一致只是shadow caster的宏不太一样。URP里如果手写Shader需要在SubShader里加一个LightMode为ShadowCaster的Pass并在内部用URP封装的GetShadowCasterVertex和GetShadowCasterFragment来完成深度输出和偏移。我贴一个精简版的关键片段Pass { Name ShadowCaster Tags { LightMode ShadowCaster } ZWrite On ZTest LEqual ColorMask 0 Cull Off HLSLPROGRAM #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #pragma multi_compile _ _CASTING_PUNCTUAL_LIGHT_SHADOW #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl float4 _ShadowBias; float3 _LightDirection; float3 _LightPosition; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings ShadowPassVertex(Attributes input) { Varyings output; float3 positionWS TransformObjectToWorld(input.positionOS.xyz); float3 normalWS TransformObjectToWorldNormal(input.normalOS); #if _CASTING_PUNCTUAL_LIGHT_SHADOW float3 lightDirectionWS normalize(_LightPosition - positionWS); #else float3 lightDirectionWS _LightDirection; #endif output.positionCS GetShadowCasterVertex(positionWS, normalWS, lightDirectionWS, _ShadowBias); output.uv input.uv; return output; } half4 ShadowPassFragment(Varyings input) : SV_TARGET { float alpha SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a; clip(alpha - _Cutoff); return 0; } ENDHLSL }如果你不想手写HLSL用Shader Graph更省事。创建一个Unlit Shader Graph把Surface改成Transparent然后勾选Alpha Clip Threshold暴露出一个AlphaClipThreshold属性。只要开了Alpha ClipURP会自动为这个Shader生成ShadowCaster变体。之后在材质Inspector里确保Cast Shadows是On再配合管线Asset里的Shadow Settings镂空阴影基本就能出来了。这个技巧特别适合用Shader Graph搭材质、又不熟悉Pass语法的团队。3. 不改Shader的Shadow Proxy兜底方案3.1 Shadow Proxy思路让一个“隐形人”帮你投影现实项目里经常碰到的场景是透明物体用的是第三方插件生成的Shader或者Shader是ASE、Shader Graph打包成asset的你根本没有源码去加Pass。这时候Shadow Proxy是最快的兜底方案。思路很简单。原透明物体还是正常渲染我另外复制一个完全相同的模型放在同一个位置给它换一个“只输出阴影、不输出颜色”的材质这样它在相机里是隐形的但它能像普通不透明物体一样参与Shadow Map渲染把阴影投到地面上。做代理材质的时候我一般就用上面那套带ShadowCaster Pass的自定义Shader或者更简单一点创建一个Unlit材质把颜色设为纯黑把Surface Type设为Opaque再在MeshRenderer里单独关闭Receive Shadows。不过要注意代理物体只是用来产生阴影不能让它影响场景里的遮挡关系所以它的Renderer最好放在单独的Layer里既方便管理也方便在特定的相机里剔除。3.2 用脚本批量生成影子代理如果一个一个手动摆代理那就太折磨人了。更推荐用一个小脚本批量生成顺便解决同步问题。下面这段代码是我实际项目里简化过的版本可以放在编辑器菜单里调用也可以挂到根节点上运行using System.Collections.Generic; using UnityEngine; public class ShadowProxyGenerator : MonoBehaviour { public Material shadowMaterial; [ContextMenu(Generate Shadow Proxies)] public void Generate() { foreach (var renderer in GetComponentsInChildrenRenderer()) { if (renderer.sharedMaterial null) continue; // 跳过已经生成的代理 if (renderer.transform.name.EndsWith(_shadow_proxy)) continue; if (renderer is MeshRenderer meshRenderer) { var meshFilter meshRenderer.GetComponentMeshFilter(); if (meshFilter ! null) { var proxy new GameObject(renderer.name _shadow_proxy); proxy.transform.SetParent(renderer.transform); proxy.transform.localPosition Vector3.zero; proxy.transform.localRotation Quaternion.identity; proxy.transform.localScale Vector3.one; var proxyMeshFilter proxy.AddComponentMeshFilter(); proxyMeshFilter.sharedMesh meshFilter.sharedMesh; var proxyRenderer proxy.AddComponentMeshRenderer(); proxyRenderer.sharedMaterial shadowMaterial; proxyRenderer.shadowCastingMode UnityEngine.Rendering.ShadowCastingMode.On; proxyRenderer.receiveShadows false; } } else if (renderer is SkinnedMeshRenderer skinnedMeshRenderer) { var proxy new GameObject(renderer.name _shadow_proxy); proxy.transform.SetParent(renderer.transform); proxy.transform.localPosition Vector3.zero; proxy.transform.localRotation Quaternion.identity; proxy.transform.localScale Vector3.one; var proxyRenderer proxy.AddComponentSkinnedMeshRenderer(); proxyRenderer.sharedMesh skinnedMeshRenderer.sharedMesh; proxyRenderer.sharedMaterial shadowMaterial; proxyRenderer.bones skinnedMeshRenderer.bones; proxyRenderer.rootBone skinnedMeshRenderer.rootBone; proxyRenderer.shadowCastingMode UnityEngine.Rendering.ShadowCastingMode.On; proxyRenderer.receiveShadows false; } } } }注意因为代理是原物体的子物体所以原物体移动、旋转、缩放时代理会跟着动不需要额外写LateUpdate同步。但如果你用的是SkinnedMeshRenderer还带动画代理虽然跟着骨骼走阴影Pass的顶点动画处理和蒙皮网格性能会比普通Mesh多不少负担这种情况下我更建议用方案一直接改Shader。3.3 Projector伪影子柔和的“假透明”投影当需求只是“地上有一团模糊的深色印子”比如树荫、玻璃下的暗部、或人物脚下的环境阴影用真实的Shadow Map往往过度设计。这时候Projector伪影子反而更香。具体做法是在场景里放一个Projector组件给它一个带透明纹理的材质纹理直接用一张径向渐变的软边贴图调整投影角度、范围、透明度就能得到一团柔和的影子。这种影子不依赖任何光源也不会因为物体旋转而变形非常适合做展示类项目的“氛围影”。它的缺点也很明显只能投到平面或近似平面上物体一动Projector不会自动跟随有些场景里影子会“穿透墙壁”如果是多光源复杂环境它不会随灯光明暗变化看起来像一张贴纸。所以我一般把它当作美术遮挡工具而不是真正的光照阴影。如果你要的效果是“物理正确的透明物体阴影”还是回到ShadowCaster方案。4. 透明物体接收阴影反过来的坑4.1 接收阴影也需要深度基准如果只是“把阴影投到地面上”上面几节已经够用了。真正磨人的是让透明物体本身接收阴影。比如一块全息屏幕、玻璃桌面上面要呈现杯子的影子。接收阴影本质上是Shader在fragment阶段采样阴影贴图再乘上一个阴影因子。听起来不复杂但透明物体有个先天毛病它默认ZWrite Off不写入深度。这导致阴影采样时的深度比较变得很微妙半透明物体又经常被RenderQueue排到所有不透明物体之后于是容易出现阴影穿透、自己穿自己、影子忽闪忽闪等问题。在URP的Lit Shader里虽然Surface Type设成Transparent后可以勾选Receive Shadows但实际效果受ZWrite影响很大经常出现“影子在透明物体上透过去了”或者“透明物体本身亮度不对”的诡异画面。这里要有一个觉悟透明物体接收阴影从来不是勾一个复选框就能完美解决的它需要你在渲染顺序、混合模式、深度写入之间做取舍。4.2 最稳的接收方案半透明阴影接收板在我的实际项目里最稳的方案不是硬啃Shader而是给透明物体加一层跟随的“阴影接收板”。做法很直白找一块和透明物体需要展示阴影表面贴合的平面或曲面给它一个带Shadowmap采样能力的深色半透明材质把它叠在透明物体受光的那一侧。这样影子仿佛落在了半透明幕布上视觉上玻璃里就有影子了。这块幕布通常要做成只有几个毫米厚度ZWrite开Receive Shadows开颜色偏黑透明度按需求调。好处是绕过了复杂深度排序适合展览展示、数字孪生这类对实时性要求高的场景。缺点是额外多一个面片不适合任意角度的复杂形状。4.3 URP里想要真实接收效果怎么救如果非要在URP里让透明材质本身接收阴影就比较考验Shader功底了。用URP/Lit是最快的路但要注意几个细节。第一将Surface Type设为Transparent后把Render Queue手动调到Geometry1而不是Transparent这样它能参与不透明物体的深度测试阴影比较不容易穿模。第二透明度不要太低否则混合后的阴影亮度本身也会跟着变淡。第三如果是自己写Shader通常是拿屏幕空间阴影坐标去SampleShadowmap再和主纹理颜色做Lerp。这个过程在URP里有很多细节比如要正确声明ShadowSamplingData稍有参数不对就容易阴影接缝闪烁。没有十足把握的话建议还是走Shader Graph加Sample Shadowmap节点或者干脆用接收板方案省心又可控。5. 踩坑实录与效率技巧5.1 高频问题速查表这里整理了一份我实际排查透明阴影问题时常用的速查表很多问题其实都是同一两个原因反复出现。现象根本原因解决办法透明物体勾了Cast Shadows但完全没有影子Shader没有ShadowCaster Pass或Queue/RenderType不匹配给Shader补ShadowCaster Pass或换Shadow Proxy镂空纹理的影子仍然是一整块ShadowCaster Pass里没有按Alpha clip在frag里加clip(alpha - _Cutoff)并正确关联主纹理影子是实心黑看不出透明感Shadow Map本身不记录透明度用阴影强度、软阴影、Projector伪影子来模拟URP里透明Shader无法投射阴影材质没有ShadowCaster变体或管线Asset里Shadow相关项被关闭用Shader Graph开Alpha Clip检查URP Asset中的Shadows配置透明物体接收阴影时出现穿模/闪烁ZWrite Off导致深度基准不稳定调Render Queue、开ZWrite或使用接收板Shadow Proxy跟着动画角色一起跑歪SkinnedMeshRenderer的骨骼同步不完整尽量改Shader方案或单独处理rootBone和bones引用移动端/VR上影子突然消失或距离很近才出现Shadow Distance设置过小或阴影贴图分辨率不足调整Shadow Distance和Shadow Resolution或改用烘焙5.2 移动端和VRPico4等的经验如果你正在做Pico4开发或Quest一体机项目透明物体投射阴影这件事必须加上性能视角。一体机的GPU远远没有桌面显卡那么从容实时阴影本身就是消耗大户。我通常的做法是在项目初期就确认阴影方案到底是实时Shadowmap还是烘焙。如果场景以室内展示为主尽量用Lightmap加Shadowmask把动态物体的实时阴影距离压到15米以内阴影贴图分辨率用1024或2048就够再高帧率容易崩。透明物体的ShadowCaster Pass会增加一次额外的深度pass如果场景里透明物体数量很多Draw Call会跟着涨。这时候用Shadow Proxy批量设置材质比每个Shader都去扩Pass更好管理。还要注意一个细节半透明物体往往伴随高Overdraw如果阴影Pass里还有复杂的纹理采样移动端帧率会很敏感。你可以试在Pico设备上把阴影质量档位做成可调比如提供“低/中/高”三个选项分别对应阴影贴图分辨率、阴影距离和是否启用透明阴影。5.3 我的操作顺序与留退路原则最后分享一下我这几年的处理顺序算是一种个人习惯。看到“透明物体投射阴影”需求我第一件事不是去写Shader而是先确定需求类别。能改Shader就优先ShadowCaster Pass改不了Shader就用Shadow Proxy只想要一团软软的假阴影就用Projector要接收阴影就加接收板或改Lit透明材质。这套顺序帮我省了很多时间也让我在团队里至少看起来像个靠谱的人。还有个小习惯我一直保留在材质上留一个开关用MaterialPropertyBlock或Shader keyword控制是否启用阴影Pass。这样项目后期可以按平台、按距离、按场景复杂度动态裁剪效果。调阴影最怕不是做不出来而是做出来后满场景都是、美术一看又全关掉留个退路对双方都友好。毕竟透明物体投影这个需求在不同项目里标准差异太大了能快速开关才是最实在的。说来也怪Unity阴影问题看起来是个渲染算法问题做多了就发现大多时候是工程管理问题需求没说清、渲染管线没确认、Shader没有兜底方案。我踩过最多的一次是在一个玻璃材质上折腾了三天最后一问美术只想要地面上有个模糊的深色印子我直接用Projector五分钟就交差了。所以接到这个需求先别急着写代码把“影子给谁看、什么形状、要不要跟灯光走”这三件事问清楚再说。希望这篇能让你少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑