Unity 极致性能假阴影(Fake Shadow)方案原理技术剖析新手教程

Unity 极致性能假阴影(Fake Shadow)方案原理技术剖析新手教程
Unity 极致性能假阴影Fake Shadow方案原理技术剖析新手教程适用场景手机游戏 / DOTS 大批量渲染 / 需要高性能阴影但不想开实时阴影的项目目标读者刚接触 Unity 渲染的新人能看懂 C# 和 Shader 基础即可一、什么是假阴影为什么需要它假阴影Fake Shadow就是用简单的手段画一个阴影出来而不是让 Unity 实时计算光照和阴影贴图。在场景力大量角色同时投射阴影时性能开销会很大所以往往会用假阴影来替代。常见的应用场景有肉鸽游戏、俯视角射击游戏、MMORPG等。为什么不用实时阴影在 Unity 中开启Shadowmap会带来这些开销需要额外渲染一遍或几遍深度图到阴影贴图阴影贴图有分辨率限制远了模糊、近了锯齿DOTS 下大批量角色同时投射阴影 → GPU 承受不住你的项目现状URP-Performant.asset中MainLightShadowsSupported 0实时阴影已经关了。但角色脚下没有阴影会显得飘在空中缺少落地的真实感。假阴影的代价一个面片 一张纹理 一个 ShaderDraw Call 几乎不增加性能开销可以忽略不计。参考效果这是自己的技术项目Demo上实现的效果之前用到了并且写过的是高效的HUD方案也是类似的技术感兴趣的可以翻看之前的文档教程实现。效果对比方案性能效果真实感实现难度额外 Mesh实时阴影 (Shadowmap)低★★★★低勾选即可不需要① 地面 Shader 直接绘制极高★★★低不需要② 圆斑阴影 (额外 Quad)极高★★极低一个 Quad/人③ 投影平面阴影高★★★★中不需要二、方案一直接在地面 Plane 上绘制阴影零额外 Mesh ★推荐原理不需要额外创建任何 Mesh直接修改地面 Plane 的 Shader在其中计算每个像素到角色的距离距离近的画一个黑色半透明圆。为什么这是最优解对你的俯视角射击项目来说✅零额外 Mesh— 不需要 Quad不需要额外 GameObject✅零额外 Draw Call— 阴影和地面在一次渲染中完成✅完美贴合地面— 不存在 Z-fighting不需要抬高✅DOTS 友好— 用 ComputeBuffer 传位置数组即可步骤 1编写地面 Shader含阴影找到你地面 Plane 当前的 Shader或者新建一个。下面是一个完整示例一个带简单纹理的地面 Shader加上阴影绘制逻辑。在Assets/Res/Shader/下新建GroundWithShadow.shaderShader Custom/GroundWithShadow { Properties { _MainTex(Ground Texture, 2D) white {} _Color(Shadow Color, Color) (0, 0, 0, 0.5) _ShadowRadius(Shadow Radius, Float) 1.0 } SubShader { Tags { RenderPipeline UniversalPipeline } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 4.5 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float3 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; // 传递世界坐标给 Fragment }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _MainTex_ST; CBUFFER_START(UnityPerMaterial) float4 _Color; float _ShadowRadius; int _ShadowCount; CBUFFER_END // 所有角色的世界位置 — 由 C# 每帧传入 StructuredBufferfloat3 _ShadowPositions; Varyings vert(Attributes v) { Varyings o; o.positionCS TransformObjectToHClip(v.positionOS); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.worldPos mul(UNITY_MATRIX_M, float4(v.positionOS, 1.0)).xyz; return o; } half4 frag(Varyings i) : SV_Target { // 1. 采样地面本身的纹理 half4 col SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv); // 2. 计算阴影遍历所有角色 float shadowAlpha 0.0; for (int j 0; j _ShadowCount; j) { float2 delta i.worldPos.xz - _ShadowPositions[j].xz; float dist length(delta); float strength 1.0 - saturate(dist / _ShadowRadius); strength smoothstep(0, 1, strength); shadowAlpha max(shadowAlpha, strength); } // 3. 混合阴影 col.rgb lerp(col.rgb, col.rgb * _Color.rgb, shadowAlpha * _Color.a); return col; } ENDHLSL } } }核心思路Fragment Shader 中拿到当前像素的世界坐标worldPos然后跟所有角色的位置算距离。距离 ShadowRadius 的像素就被涂暗。步骤 2把 Shader 赋给地面 Plane在 Project 窗口右键 →Create→Material命名为GroundShadowMatShader 选Custom/GroundWithShadow把GroundShadowMat拖给场景中的地面 Plane调参数Shadow Color黑色Alpha 0.3~0.5Shadow Radius根据你的角色大小调整一般 0.5~2.0步骤 3C# 脚本 — 每帧把角色位置传给 ShaderusingUnityEngine;publicclassShadowUpdater:MonoBehaviour{publicMaterialgroundMaterial;// 地面的材质 (GroundShadowMat)publicTransform[]shadowSources;// 所有需要阴影的角色 TransformprivateVector3[]positions;privateComputeBuffershadowBuffer;// 传给 GPU 的缓冲区privatestaticreadonlyintShadowPositionsShader.PropertyToID(_ShadowPositions);privatestaticreadonlyintShadowCountShader.PropertyToID(_ShadowCount);voidStart(){positionsnewVector3[1024];// 最多 1024 个角色}voidUpdate(){intcountMathf.Min(shadowSources.Length,1024);for(inti0;icount;i)positions[i]shadowSources[i].position;// 创建或更新 ComputeBufferif(shadowBuffernull||shadowBuffer.count!count){shadowBuffer?.Release();shadowBuffernewComputeBuffer(count,12);// 12 sizeof(float3)}shadowBuffer.SetData(positions,0,0,count);groundMaterial.SetBuffer(ShadowPositions,shadowBuffer);groundMaterial.SetInt(ShadowCount,count);}voidOnDestroy(){shadowBuffer?.Release();shadowBuffernull;}}步骤 4在场景中挂脚本在 Hierarchy 中创建一个空 GameObject取名ShadowManager挂上ShadowUpdater脚本把GroundShadowMat拖进groundMaterial槽把所有角色玩家、敌人拖进shadowSources列表运行场景地面就会在每个角色脚下自动画出黑色圆形阴影。DOTS 版本在 DOTS 项目中角色不是GameObject而是Entity。你需要通过EntityManager查询所有角色的位置usingUnity.Entities;usingUnity.Mathematics;publicclassDotsShadowUpdater:MonoBehaviour{publicMaterialgroundMaterial;privateEntityManagerentityManager;privateComputeBuffershadowBuffer;voidStart(){entityManagerWorld.DefaultGameObjectInjectionWorld.EntityManager;}voidUpdate(){// 查询所有带 LocalTransform 的 EntityvarqueryentityManager.CreateEntityQuery(typeof(LocalTransform));vartransformsquery.ToComponentDataArrayLocalTransform(Unity.Collections.Allocator.Temp);intcountmath.min(transforms.Length,1024);varpositionsnewVector3[count];for(inti0;icount;i)positions[i]transforms[i].Position;// 传给 GPUif(shadowBuffernull||shadowBuffer.count!count){shadowBuffer?.Release();shadowBuffernewComputeBuffer(count,12);}shadowBuffer.SetData(positions,0,0,count);groundMaterial.SetBuffer(Shader.PropertyToID(_ShadowPositions),shadowBuffer);groundMaterial.SetInt(Shader.PropertyToID(_ShadowCount),count);transforms.Dispose();}voidOnDestroy(){shadowBuffer?.Release();}}效果改进思路阴影随高度变淡在frag中加上角色 Y 轴判断跳起来阴影变淡// 在遍历循环中角色离地越高阴影越淡 float heightFade saturate(1.0 - _ShadowPositions[j].y / 5.0); strength * heightFade;阴影微微偏移在角色位置加上_LightDir.xz * offset模拟光的方向大角色大阴影每个角色传入不同的半径把StructuredBufferfloat3改成StructuredBufferfloat4w 存半径三、方案二圆斑阴影Blob Shadow— 用额外 Quad原理在角色脚下放一个半透明的黑色圆形/椭圆形面片Quad平躺在地面上。适合场景方案一不方便修改地面 Shader 时比如地面是商店买的复杂材质场景不是纯平面有起伏地形刚开始学习想最快看到效果步骤 1准备阴影纹理在 Unity 中创建一张 32×32 的 PNG中心白色不透明边缘黑色透明导入设置Texture Type DefaultAlpha Source Input Texture Alpha或者用附录中的代码动态生成。步骤 2创建阴影 Shader在Assets/Res/Shader/下新建FakeShadowBlob.shaderShader Custom/FakeShadowBlob { Properties { _MainTex(Shadow Texture, 2D) white {} _Color(Shadow Color, Color) (0, 0, 0, 0.5) _Falloff(Edge Softness, Range(0.001, 1)) 0.3 } SubShader { Tags { RenderType Transparent Queue Geometry1 RenderPipeline UniversalPipeline } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 4.5 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float3 positionOS : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float2 uv : TEXCOORD0; float4 positionCS : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_START(UnityPerMaterial) float4 _Color; float _Falloff; CBUFFER_END Varyings vert(Attributes v) { Varyings o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); o.positionCS TransformObjectToHClip(v.positionOS); o.uv v.uv; return o; } half4 frag(Varyings i) : SV_Target { half alpha SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv).r; alpha pow(alpha, _Falloff); return half4(_Color.rgb, _Color.a * alpha); } ENDHLSL } } }步骤 3创建阴影面片Hierarchy 右键 →3D Object→QuadRotation 设为(90, 0, 0)平躺赋值FakeShadowBlob材质把阴影纹理拖进_MainTex把 Quad 拖成角色的子物体Position 设为(0, 0.01, 0)关于 Z-fighting必须抬高 0.01~0.05 单位阴影面片和地面完全叠合会闪烁。或者更省事Shader 中添加Offset -1, -1指令。缩放适配不同大小的角色需要不同大小的阴影publicclassShadowScaler:MonoBehaviour{voidUpdate(){// 从角色碰撞体获取半径floatradiusGetComponentInParentSphereCollider().radius;transform.localScaleVector3.one*radius*0.8f;}}四、方案三投影平面阴影Planar Shadow— 形状匹配原理把角色的网格沿着光线方向压扁到地面上阴影形状和角色轮廓一致。原理把角色的网格沿着光线方向压扁到地面上。效果很真实但每个角色需要多一次 Draw Call。什么时候用方案二角色形状不规则比如有武器突出圆斑阴影看起来不自然场景是斜视角或 3D 视角能看到阴影形状角色数量不多几十个以内核心思路对每个顶点做一次投影矩阵变换把顶点压到地面平面y0上顶点世界位置 角色位置 光线方向 * (角色位置.y / 光线方向.y)Shader 实现新建FakeShadowPlanar.shaderShader Custom/FakeShadowPlanar { Properties { _Color(Shadow Color, Color) (0, 0, 0, 0.4) _FadeDistance(Fade Distance, Float) 10 } SubShader { Tags { RenderType Transparent Queue Geometry1 RenderPipeline UniversalPipeline } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off ZTest LEqual Cull Off Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 4.5 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float3 positionOS : POSITION; }; struct Varyings { float4 positionCS : SV_POSITION; float alpha : TEXCOORD0; }; CBUFFER_START(UnityPerMaterial) float4 _Color; float _FadeDistance; CBUFFER_END // 光源方向世界空间归一化 // 在俯视角游戏中光从斜上方打下来效果最好 static float3 _LightDir normalize(float3(-0.5, -1, -0.3)); Varyings vert(Attributes v) { Varyings o; float3 worldPos mul(UNITY_MATRIX_M, float4(v.positionOS, 1.0)).xyz; // 投影到 y0 平面地面 // 公式投射点 顶点位置 - (顶点位置.y / 光线方向.y) * 光线方向 float t worldPos.y / (-_LightDir.y); float3 groundPos worldPos t * _LightDir; // 高度越高阴影越淡距离衰减 float heightFade saturate(1.0 - worldPos.y / _FadeDistance); o.positionCS TransformWorldToHClip(groundPos); o.alpha _Color.a * heightFade; return o; } half4 frag(Varyings i) : SV_Target { return half4(_Color.rgb, i.alpha); } ENDHLSL } } }用法说明这个 Shader 直接复用角色的原始网格不需要额外做面片在角色 Prefab 上添加一个子物体挂同样网格 FakeShadowPlanar材质通过_FadeDistance控制角色离地多高时阴影完全消失模拟远距离模糊对于 DOTS GPU Instancing记得勾选Enable GPU Instancing局限只投影到 y0 平面。如果地面有起伏就不准确了阴影始终是硬边除非你另外加 fade适合俯视角游戏不适合有复杂地形的场景五、你的项目推荐方案根据你的项目情况来分析你的项目特征俯视角射击→ 方案一的平躺 Quad 就足够DOTS/ECS 架构→ 需要 GPU Instancing 兼容URP-Performant→ 追求极致性能大量敌人→ 需要极低的开销推荐首选方案一地面 Shader 直接绘制修改你地面 Plane 的 Shader加上阴影遍历逻辑一个 Draw Call 搞定地面 所有阴影DOTS 版用DotsShadowUpdater查询 Entity 位置传给 Shader零额外 Mesh、零额外 Draw Call如果地面是商店买的复杂材质不方便改选方案二圆斑阴影 Quad。如果需要阴影形状匹配角色轮廓比如角色拿武器选方案三投影平面阴影。六、避坑指南1. Z-fighting阴影闪烁阴影面和地面在同一平面时会产生闪烁。解决阴影 Quad 抬高0.01~0.05单位或者 Shader 中用Offset -1, -1在 SubShader 或 Pass 中Offset -1, -12. 阴影渲染顺序不对确保阴影在实体之后渲染Queue Geometry1在 Geometry 之后或Queue Transparent在透明队列一起渲染但要小心和 UI 的排序3. DOTS 中阴影不跟随在 DOTS 中如果你是用Entities Graphics渲染阴影 Quad 需要作为角色 Prefab 的一部分一起Bake。或者你用 Hybrid 方式在EnemySpawnSystem中实例化阴影 GameObject 并每帧同步位置// 在 EnemyMovementSystem 或类似系统中// 每帧把敌人的位置同步给阴影 GameObjectvarshadowLookupSystemAPI.GetComponentLookupShadowTag(true);foreach(var(transform,enemyPos)inSystemAPI.QueryRefRWLocalTransform,RefROEnemyData()){// 阴影位置 敌人位置 稍微抬高防止 Z-fighting// 如果你用 Hybrid 方式直接设置 GameObject 的 transform}更简单的做法把阴影做成 Unity 的常规 GameObject非 DOTS在LateUpdate中通过EntityManager查询位置然后设置阴影位置。虽然每帧同步有微小开销但敌人数量不多时完全可接受。4. 阴影闪烁 / 轻微抖动如果阴影和地面有 Z-fighting抬高 0.01或者用Offset指令或者稍微缩小阴影不要和地面模型完全重叠5. 批处理Batching被打断每个阴影用不同的材质实例会导致合批失败。解决办法所有角色共用同一个材质实例阴影的大小、透明度在 Shader 中用Per-instance数据UNITY_VERTEX_INPUT_INSTANCE_ID控制DOTS 下注意不要用MaterialPropertyBlock会打断 Instancing七、效果调试 Checklist阴影出现在角色正下方角色跳起时阴影逐渐变淡阴影边缘柔和没有硬边阴影不会穿透地面或闪烁多个角色各自有独立的阴影GPU Instancing 正常合批Frame Debugger 中查看移动端帧率没有明显下降附录 A代码生成阴影纹理不想准备图片可以用 C# 在运行时生成usingUnityEngine;publicstaticclassShadowTextureGenerator{publicstaticTexture2DCreateBlobShadowTexture(intresolution32){Texture2DtexnewTexture2D(resolution,resolution,TextureFormat.RGBA32,false);tex.nameBlobShadow_Generated;Color[]pixelsnewColor[resolution*resolution];floatcenter(resolution-1)*0.5f;floatinvRadius2.0f/resolution;for(inty0;yresolution;y){for(intx0;xresolution;x){floatdx(x-center)*invRadius;floatdy(y-center)*invRadius;floatdistMathf.Sqrt(dx*dxdy*dy);floatalphaMathf.Clamp01(1.0f-dist);alphaMathf.Lerp(0,1,alpha*alpha);// Gamma 校正让边缘更软pixels[y*resolutionx]newColor(1,1,1,alpha);}}tex.SetPixels(pixels);tex.Apply();tex.wrapModeTextureWrapMode.Clamp;returntx;}}在Start中调用ShadowTextureGenerator.CreateBlobShadowTexture()赋值给材质的_MainTex即可。附录 B快速参考 — 各方案 Shader 代码位置方案文件路径① 地面 Shader 直接绘制Assets/Res/Shader/GroundWithShadow.shader② 圆斑阴影额外 QuadAssets/Res/Shader/FakeShadowBlob.shader③ 投影平面阴影Assets/Res/Shader/FakeShadowPlanar.shader总结假阴影是用微不足道的开销换一个可信度提升的经典手段。对于你的 DOTS 俯视角射击项目方案一地面 Shader 直接绘制是最干净的做法——不改动 Prefab、不增加 Mesh、不增加 Draw Call。如果地面不方便改 Shader降级到方案二圆斑阴影 Quad同样简单高效。