1. 项目背景与核心需求拆解1.1 为什么需要Projector来做阴影投射在Unity场景里做阴影大多数人第一反应是直接用实时阴影也就是在光源组件里把Shadow Type打开让引擎自己算。这个方案在中小型场景里确实省事但一旦场景规模上去、光源数量变多性能开销就会变得非常难看。尤其是移动端项目实时阴影几乎是性能杀手Draw Call和Overdraw都会明显上升。那用Lightmap烘焙行不行行但烘焙的代价是静态的。场景里一旦有动态物体经过或者光源需要移动烘焙出来的阴影就对不上了。而且烘焙一张高质量Lightmap的时间成本很高迭代起来很痛苦。Projector这个组件就是在这种夹缝里生存的。它本质上是一个投影器可以把一张贴图按照指定的方向和范围投射到场景表面上。你可以把它理解成一个幻灯片放映机它把一张带透明通道的阴影贴图打到地面、墙面、模型表面上从视觉上模拟出阴影效果。它不参与光照计算不走实时阴影管线纯粹是贴图投影所以性能开销非常可控。这个方案最适合的场景包括固定视角的塔防游戏、俯视角策略游戏、需要大量角色阴影但不想开实时阴影的移动端项目、以及需要特殊形状阴影比如云影、树影、窗户栅格影的效果实现。如果你正在做这类项目Projector方案值得认真研究一下。1.2 Projector阴影的核心原理Projector的工作方式和相机有点像但方向反过来。相机是把三维场景拍成二维图像Projector是把一张二维贴图照回三维场景。它有一个投影范围通常用一个视锥体或者正交盒子来定义范围内的物体表面会接收到这张贴图。具体到阴影投射我们需要一张带Alpha通道的阴影贴图。贴图上有阴影的地方Alpha值高或者低取决于混合模式没有阴影的地方透明。Projector把这张贴图投射到场景模型表面后通过合适的混合模式就能让模型表面出现暗色区域看起来就像被什么东西挡住了光。这里有个关键点Projector投射的是贴图不是真正的几何阴影。所以它不会因为场景物体的遮挡关系自动变化。也就是说如果场景里多了一个箱子挡在光源和地面之间Projector不会自动在地面上多出一块阴影。你需要手动控制Projector的开关或者贴图切换来模拟这种变化。这是它最大的局限也是使用时最容易踩坑的地方。1.3 适用场景与不适用场景对比场景类型是否适合Projector原因固定视角塔防非常适合视角固定阴影贴图可以预先做好性能极佳俯视角策略适合阴影方向统一可以用一张通用贴图移动端大量角色适合比实时阴影省太多角色脚下用简单圆形阴影即可第一人称探索不太适合视角变化大Projector边缘容易穿帮动态光源场景不适合光源一动投影方向就对不上了需要精确遮挡不适合Projector不感知遮挡关系需要额外逻辑2. 阴影贴图的制作与参数设计2.1 阴影贴图的基本要求Projector用的阴影贴图和普通贴图不太一样。它需要一张带Alpha通道的RGBA贴图通常用PNG格式。RGB通道决定投影出来的颜色Alpha通道决定透明度。做阴影的话RGB一般填纯黑或者深灰色Alpha通道控制哪里显示阴影、哪里透明。贴图的尺寸不用太大256x256或者512x512就够用了。因为Projector投射到场景表面后贴图会被拉伸太高的分辨率反而浪费显存。我一般用512x512边缘用柔和的渐变过渡这样投影出来的阴影边缘不会太硬。贴图的边缘处理很关键。如果贴图边缘是硬切的黑边投影到地面上就会看到明显的方形边界非常穿帮。解决办法是在贴图四周留出足够的透明过渡区域让阴影从中心向外逐渐淡出。用Photoshop或者任何绘图工具把阴影形状画在中间四周用软笔刷擦出渐变即可。2.2 不同阴影形状的制作思路圆形阴影是最常用的角色脚下、道具底部都可以用。制作方法很简单新建一个正方形画布用椭圆选框工具画一个圆填充黑色然后羽化边缘羽化半径根据贴图尺寸来定512的贴图用30到50像素的羽化就差不多。长条形阴影适合柱子、树木这类细长物体。画一个细长的椭圆或者矩形同样做羽化处理。注意长条阴影的方向要和Projector的投射方向匹配不然投影出来方向是歪的。特殊形状阴影比如窗户栅格、树叶缝隙可以用图案叠加或者蒙版来做。这类阴影通常用于营造氛围比如阳光透过窗户在地面上形成的光斑和阴影交替效果。制作时先把基础形状画好然后用图案画笔或者蒙版叠加出细节。注意贴图的Alpha通道必须是独立的不能依赖RGB的亮度来做透明。有些绘图工具默认导出时Alpha是拼合的需要在导出设置里确认存储透明度或者Alpha通道选项是打开的。2.3 贴图导入Unity的参数设置贴图导入Unity后在Inspector面板里有一堆参数要调。最重要的几个Texture Type选Default或者Sprite都行Projector不挑但选Default更通用。Alpha Source选From Gray Scale或者Input Texture Alpha取决于你的贴图是怎么做的。如果Alpha是独立通道选Input Texture Alpha。Alpha Is Transparency这个要勾上不然边缘会有黑边。Wrap Mode选Clamp不要选Repeat。因为Projector投射时如果贴图边缘是Repeat模式超出范围的部分会重复贴图导致地面出现莫名其妙的重复阴影。Filter Mode选Bilinear阴影边缘会柔和一些。Point模式太硬Trilinear没必要。Max Size512或者1024足够了再大就是浪费。这些参数看着琐碎但每一个都会影响最终效果。我见过有人贴图边缘一直有黑边查了半天发现是Alpha Is Transparency没勾。也见过地面阴影重复出现结果是Wrap Mode选成了Repeat。这些坑我都踩过你直接按上面的设置来就行。3. Projector组件的配置与材质设置3.1 Projector组件的核心参数详解在场景里新建一个空物体Add Component搜索Projector添加上去。这个组件有几个关键参数Orthographic勾选后投影范围是正交盒子不勾是透视视锥。做阴影一般勾上因为正交投影的阴影方向一致不会出现近大远小的透视变形。但如果你要模拟点光源的阴影比如灯泡在桌子正上方那就不勾用透视模式。Orthographic Size正交模式下投影盒子的半高。这个值决定了阴影投射的范围大小。比如你想让阴影覆盖角色周围2米的范围Orthographic Size就设1左右。具体数值要根据场景比例来调没有固定标准。Near Clip Plane / Far Clip Plane投影的最近和最远距离。Near Clip Plane不要设0设0.1或者0.2避免投影器自身位置出现异常。Far Clip Plane根据你需要投射的距离来定比如从天花板投射到地面距离3米那Far Clip Plane至少设3.5。Aspect Ratio投影的宽高比。默认是1也就是正方形。如果你想让阴影在某个方向上拉长可以调这个值。比如角色阴影想稍微扁一点Aspect Ratio设1.2。Ignore Layers这个非常重要。Projector默认会投射到所有层上包括角色自身。如果你不想让角色自己身上出现阴影贴图就要把角色的Layer加到Ignore Layers里。通常我会建一个专门的Layer叫ShadowReceiver只让地面和场景模型接收投影角色和特效都不接收。3.2 投影材质的选择与自定义ShaderProjector需要一个材质来定义投影的混合方式。Unity自带几个Projector材质在Effects文件夹里可以找到。常用的有Projector Light做加法混合适合做光斑。Projector Multiply做乘法混合适合做阴影因为乘法会让底色变暗。Projector Shadow专门做阴影的但效果比较基础。我一般不用自带的而是自己写一个简单的Shader。原因有两个一是自带Shader的混合模式不够灵活二是自己写的可以加一些额外控制比如根据距离淡出、根据角度调整强度。下面是一个基础的Projector阴影Shader用的是Multiply混合Shader Custom/ProjectorShadow { Properties { _ShadowTex (Cookie, 2D) gray {} _Intensity (Intensity, Range(0, 1)) 0.5 } Subshader { Tags { QueueTransparent } Pass { ZWrite Off Offset -1, -1 Blend DstColor Zero CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct v2f { float4 pos : SV_POSITION; float4 uvShadow : TEXCOORD0; }; float4x4 unity_Projector; sampler2D _ShadowTex; float _Intensity; v2f vert (appdata_base v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uvShadow mul(unity_Projector, v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 tex tex2Dproj(_ShadowTex, i.uvShadow); fixed4 col; col.rgb lerp(fixed3(1,1,1), tex.rgb, _Intensity); col.a tex.a * _Intensity; return col; } ENDCG } } }这个Shader的核心逻辑是用unity_Projector矩阵把顶点坐标转换到投影空间然后用tex2Dproj采样贴图。Blend DstColor Zero是乘法混合让投影区域变暗。_Intensity控制阴影强度0就是没阴影1就是全黑。提示Offset -1, -1是为了避免投影面和接收面在同一深度时产生Z-fighting闪烁。这个偏移值可以根据实际情况微调。3.3 材质参数的实际调试经验把Shader挂到材质上然后把材质拖到Projector组件的Material槽里。这时候场景里应该能看到投影效果了。如果看不到检查几个地方第一Projector的Far Clip Plane是不是太近了导致投影范围没覆盖到目标表面。第二Ignore Layers是不是把接收投影的层给排除了。第三材质的Shader有没有编译错误在Console里看一下。调试阴影强度的时候_Intensity不要设太高。0.3到0.6之间比较自然太黑了会显得很假。真实阴影的边缘是有过渡的不是一刀切的黑色。如果贴图本身边缘羽化做得好强度低一点反而更真实。还有一个细节Projector的旋转角度会影响投影形状。如果Projector是斜着投射的阴影会被拉长。这是正常的透视效果但如果你不想要这种拉长就把Projector的旋转和光源方向对齐或者用正交模式减少变形。4. 完整实操流程与场景搭建4.1 场景准备与层级规划先规划好场景的层级结构。我一般会建这么几个空物体来组织_Projectors放所有Projector物体。_ShadowReceivers放需要接收阴影的场景模型。_ShadowCasters放投射阴影的角色或道具逻辑上的不是必须。层级规划的好处是后面用代码控制Projector开关或者批量调整参数时可以直接遍历父物体下的所有子物体不用一个个找。接收阴影的模型Layer设成ShadowReceiver。这个Layer要在Projector的Ignore Layers里排除掉意思是不要忽略这一层也就是要投射到这一层上。Projector的Ignore Layers逻辑是列表里的层会被忽略不在列表里的层会接收投影。所以要把不需要接收投影的层比如角色、UI、特效加进去。4.2 创建Projector并配置参数新建空物体命名比如ShadowProjector_Player位置放在角色正上方。旋转角度根据光源方向来定如果光源是从左上角打过来的Projector就朝右下角倾斜。添加Projector组件参数按下面的来参数值说明Orthographic勾选正交投影阴影不变形Orthographic Size1.5覆盖角色周围3米范围Near Clip Plane0.1避免自身位置异常Far Clip Plane10足够投射到地面Aspect Ratio1正方形投影Ignore Layers角色层、UI层、特效层这些层不接收投影材质用上面写的Custom/ProjectorShadow贴图用做好的圆形阴影贴图。Intensity先设0.5后面根据效果微调。把Projector的位置和角色绑定可以直接把Projector作为角色的子物体这样角色移动时阴影跟着走。但要注意如果角色有缩放Projector也会被缩放投影范围会跟着变。如果不想这样就用代码每帧同步Projector的位置不设为子物体。4.3 让阴影跟随角色移动的代码实现如果Projector是角色的子物体位置同步是自动的但旋转和缩放可能会受角色影响。更稳妥的做法是用一个独立的Projector每帧把它的XZ平面位置同步到角色脚下Y轴保持固定高度。using UnityEngine; public class ShadowFollower : MonoBehaviour { public Transform target; public float height 5f; public float followSpeed 10f; void LateUpdate() { if (target null) return; Vector3 targetPos new Vector3(target.position.x, height, target.position.z); transform.position Vector3.Lerp(transform.position, targetPos, followSpeed * Time.deltaTime); } }把这个脚本挂到Projector物体上target拖角色进去。height设成Projector离地面的高度比如5米。followSpeed控制跟随的平滑度设10左右比较跟手不会太飘。用LateUpdate而不是Update是因为角色的位置可能在Update里被修改LateUpdate能保证在角色移动之后再同步Projector位置避免阴影和角色之间出现一帧的延迟。4.4 多个角色共用Projector的优化方案如果场景里有几十个角色每个角色一个ProjectorDraw Call会上去。虽然Projector本身开销不大但数量多了也扛不住。优化方案有两种第一种是合并Projector。如果角色比较集中可以用一个大的Projector覆盖所有角色贴图用一张包含多个阴影的图。但这样阴影位置就固定了角色移动时阴影不会跟着动只适合静态或者小范围移动的场景。第二种是用Command Buffer或者Graphics.DrawMesh手动绘制。这个方案复杂一些但性能最好。核心思路是把所有角色的阴影贴图合并到一张大图里然后用一个Projector投射出去。不过这个方案需要写不少代码适合对性能有极致要求的项目。对于大多数项目我建议先用每个角色一个Projector的方案跑起来看Profiler。如果Draw Call在可接受范围内就不用折腾。如果确实高了再考虑合并方案。过早优化是万恶之源这句话在Unity性能优化里同样适用。5. 常见问题与排查技巧实录5.1 阴影边缘出现硬切或黑边这是最常见的问题。原因通常有三个贴图边缘没有做羽化、Alpha Is Transparency没勾、或者Wrap Mode设成了Repeat。排查顺序先看贴图本身在图片查看器里放大边缘看有没有渐变过渡。如果没有回绘图工具重新做羽化。如果贴图没问题看Unity导入设置确认Alpha Is Transparency勾上了Wrap Mode是Clamp。这两个都对了再看材质的Blend模式Multiply模式对边缘比较敏感可以换成自定义Shader里的lerp方式边缘会柔和很多。还有一个隐藏原因Projector的Orthographic Size设得太小导致贴图边缘正好落在投影范围的边界上被硬切了。解决办法是把Orthographic Size调大一点让贴图边缘在投影范围内部不要贴边。5.2 阴影投射到了不该投射的物体上比如角色自己身上出现了阴影贴图或者UI上出现了阴影。这是因为Ignore Layers没配好。Projector的Ignore Layers逻辑容易搞反。记住列表里的层会被忽略不在列表里的层会接收投影。所以要把所有不想接收投影的层加进去。角色层、UI层、特效层、水层这些通常都要加。如果加了之后还是投射到了角色身上检查角色的Layer是不是真的设成了你加进去的那个层。有时候角色父物体是一个层子物体是另一个层Projector会按子物体的层来判断。确保所有不需要接收投影的物体其Layer都在Ignore Layers列表里。5.3 阴影闪烁或Z-fighting两个面在同一深度时会出现闪烁这是Z-fighting。Projector的投影面和接收面如果深度太接近就会闪。解决办法是在Shader里加Offset。上面给的Shader里有Offset -1, -1这个就是用来偏移深度的。如果还闪把值改成-2, -2或者-3, -3。偏移值越大投影面越靠前越不容易和接收面冲突。但偏移太大也会导致投影穿透其他物体需要平衡。另一个办法是把Projector的Near Clip Plane调大一点让投影从离接收面稍远的地方开始。比如Near Clip Plane从0.1改成0.5投影的起始深度就远了不容易和接收面重叠。5.4 阴影在移动端表现异常移动端对Shader的支持和PC不太一样。有些在PC上正常的Shader到移动端就出问题。常见的是tex2Dproj在部分移动GPU上不支持或者精度不够导致阴影边缘有锯齿。解决办法是用tex2D代替tex2Dproj手动做透视除法。把o.uvShadow mul(unity_Projector, v.vertex)改成先算float4 uv mul(unity_Projector, v.vertex)然后在fragment里用uv.xy / uv.w来采样。这样兼容性更好。精度问题的话把Shader里的fixed4改成half4或者float4提高精度。移动端GPU对fixed精度的支持参差不齐用half或float更保险。5.5 常见问题速查表问题现象可能原因排查方向阴影边缘硬切贴图未羽化 / Wrap Mode错误检查贴图边缘和导入设置阴影投射到角色身上Ignore Layers未配置把角色层加入Ignore Layers阴影闪烁Z-fighting加Offset或调大Near Clip Plane移动端阴影异常Shader兼容性 / 精度不足换tex2D手动除法 / 提高精度阴影位置偏移Projector位置未同步用LateUpdate同步位置阴影颜色太深Intensity过高降低Intensity到0.3-0.6投影范围不够Far Clip Plane太近调大Far Clip Plane6. 进阶技巧与性能优化建议6.1 用Render Texture动态生成阴影贴图静态贴图只能做固定形状的阴影。如果想让阴影根据场景实时变化比如角色举起武器时阴影形状跟着变可以用Render Texture。思路是在角色上方放一个正交相机朝下拍只拍角色的阴影投射层。相机的输出给一张Render Texture这张Render Texture再作为Projector的贴图。这样角色动阴影贴图就跟着动。这个方案的开销比静态贴图大因为多了一个相机渲染。但比实时阴影还是省很多。适合角色动作丰富、对阴影精度要求不高的项目。配置步骤新建一个Camera设为OrthographicSize和Projector的Orthographic Size一致。相机的Culling Mask只勾选角色层Clear Flags设为Solid Color背景色设白色因为阴影贴图是黑底白透明白色背景对应透明。然后新建一张Render Texture拖到相机的Target Texture上。最后把这张Render Texture拖到Projector材质的_ShadowTex上。6.2 用Shader Graph做更灵活的阴影效果如果项目用的是URP或者HDRP可以用Shader Graph来做Projector的Shader比手写HLSL直观很多。Shader Graph里有Projector相关的节点可以直接连。用Shader Graph的好处是可以很方便地加一些效果比如根据距离淡出、根据视角角度调整强度、叠加噪声让阴影边缘不规则。这些效果手写HLSL也能做但Shader Graph可视化编辑调起来更快。不过要注意URP下的Projector和内置管线不太一样。URP没有内置的Projector组件需要用Decal Projector或者自己写一个。Decal Projector是URP专门做贴花投影的用法和Projector类似但参数和Shader写法不同。如果项目是URP建议直接用Decal Projector省事。6.3 性能对比与选型建议把Projector阴影和实时阴影做个对比对比项Projector阴影实时阴影Draw Call每个Projector 1-2个每个光源多个内存占用一张贴图阴影贴图RT移动端性能很好一般到差动态遮挡不支持支持阴影精度取决于贴图取决于分辨率适用场景固定视角、大量角色动态光源、精确遮挡选型建议如果项目是移动端、视角固定、角色数量多优先用Projector。如果项目是PC端、光源动态、需要精确遮挡用实时阴影。如果两者都有可以混合使用主角用实时阴影小怪用Projector阴影。6.4 一些踩过的坑和实操心得第一个坑Projector的贴图不要用带Mipmap的。Mipmap在远处会降低贴图分辨率导致阴影边缘模糊甚至消失。在贴图导入设置里把Generate Mip Maps关掉。第二个坑Projector的旋转角度不要和接收面平行。如果Projector垂直朝下接收面是水平地面投影是正常的。但如果接收面是斜面Projector垂直朝下投影会被拉长。解决办法是让Projector的朝向和接收面的法线方向有一定角度或者用多个Projector分别投射不同斜面。第三个坑场景切换时Projector的材质会丢。如果Projector是动态加载的材质要手动赋值不能依赖Inspector里的引用。用Resources.Load或者Addressables加载材质然后projector.material loadedMaterial。第四个坑Projector在Scene视图里能看到投影范围线框但Game视图里看不到阴影。这通常是材质的Shader编译错误或者贴图的Alpha通道有问题。在Console里看有没有报错没有的话检查贴图的Alpha通道是不是全白或者全黑。第五个坑多个Projector重叠时阴影会叠加变黑。因为Multiply混合是累乘的两个0.5的阴影叠在一起就是0.25更黑。解决办法是控制Projector的投射范围不要重叠或者用Stencil Buffer做遮罩让重叠区域只算一次。Stencil方案复杂一些但效果最好。7. 不同项目类型下的适配方案7.1 塔防游戏的阴影方案塔防游戏视角固定通常是俯视角或者斜45度。这种场景最适合Projector阴影。所有角色的阴影可以用一张统一的圆形贴图Projector的旋转角度和光源方向一致所有角色共用同一个Projector参数。优化做法把所有角色的阴影合并到一个Projector里。用一个大的Projector覆盖整个战场贴图用一张包含所有角色阴影位置的大图。角色移动时更新贴图上对应位置的阴影。这个方案Draw Call极低但需要写代码动态更新贴图。如果角色数量不多20个以内也可以每个角色一个Projector性能也扛得住。7.2 移动端角色阴影的轻量方案移动端最省事的方案是每个角色脚下放一个面片Quad面片上贴一张圆形阴影贴图用透明混合。这个方案连Projector都不需要Draw Call就是一个面片。缺点是阴影只能贴在地面上如果地面有起伏面片会穿模。Projector方案比面片方案好的地方在于它能贴合地面起伏。如果移动端项目的地面是平坦的用面片就够了。如果地面有坡度用Projector。如果地面既有坡度又有高低差Projector的Far Clip Plane要调好确保投影能覆盖到低处的地面。7.3 室内场景的窗户光影效果室内场景经常需要模拟阳光透过窗户在地面上形成的光斑和阴影。这个效果用Projector做非常合适。贴图做成窗户栅格的形状Projector放在窗户位置朝室内投射。关键点是Projector的旋转角度要和阳光方向一致。如果阳光是从左上角斜射进来Projector就朝右下角倾斜。倾斜角度根据太阳高度角来算一般45度左右比较自然。贴图的颜色不要用纯黑用深灰色或者带一点蓝色的暗色模拟环境光的影响。Intensity设0.4到0.5让光斑和阴影的对比不要太强烈。如果场景里有多个窗户每个窗户一个Projector贴图可以共用但位置和旋转要分别调。7.4 大世界场景的阴影分级策略大世界场景里玩家周围用实时阴影远处用Projector阴影再远处直接不显示阴影。这个分级策略能平衡效果和性能。具体做法以玩家为中心半径20米内用实时阴影20到50米用Projector阴影50米外关掉阴影。Projector阴影的贴图分辨率可以低一些因为远处看不清细节。切换距离可以根据设备性能调整高端机可以拉远低端机拉近。实现上用一个管理器脚本每帧计算每个角色和玩家的距离根据距离切换阴影模式。实时阴影的开关在光源组件上控制Projector的开关在Projector组件上控制。切换时注意不要突然跳变可以用淡入淡出过渡。8. 收尾的一些个人体会Projector做阴影这个方案我从几年前开始用中间踩了不少坑也总结了一些经验。最大的体会是不要指望Projector能替代实时阴影它就是一个特定场景下的优化手段。用对了地方效果和性能都很好用错了地方问题一堆。我一般会在项目前期就确定阴影方案。如果是移动端项目直接上Projector不要犹豫。如果是PC端项目先试实时阴影性能不够再考虑Projector。如果是主机项目实时阴影基本够用Projector主要用来做特殊效果。还有一个建议阴影贴图不要只用一张。准备几张不同形状、不同强度的贴图根据场景切换。比如角色在室内用柔和的圆形阴影在室外用硬一点的阴影。切换贴图比调参数方便效果也更可控。最后分享一个小技巧Projector的材质可以做成Material Variant不同场景用不同的Variant共用同一个Shader。这样改Shader的时候所有Variant一起更新不用一个个改材质。Unity的Material Variant功能在2021版本之后就有了用起来很方便。