
做Unity项目只要画面里同时出现两个以上对象就迟早要碰“谁的绘制优先级更高”这个问题2D角色的箭头是不是被技能特效挡住了全屏UI里的伤害数字为什么突然被模型穿透3D角色站在灌木丛后面头发到底该不该被树叶边缘罩住这些看着都是美术或表现问题实际上全是Unity渲染顺序在幕后定生死。我早期做消除类小游戏时就因为多加了一层级联UI导致伤害数字层级错乱排查了一整晚最后发现是RenderQueue、Sorting Order和深度缓冲三个维度一起夹击的结果。这篇内容我打算把Unity渲染顺序彻底讲透包括RenderQueue、Sorting Layer、深度缓冲、Camera Depth的优先级划分并穿插一些实战踩坑过程。适合正在做2D、UI、3D混合项目经常被“谁盖谁”困扰的中初级Unity开发者也适合想优化绘制批次、减少状态切换的性能控。搞懂这套机制你调层级、调穿插、调特效顺序时就不需要再靠试错一次就能定位问题。1. 渲染顺序到底由什么决定先说结论Unity里最终绘制顺序不是靠场景面板里的Hierarchy上下位置决定的也不是靠脚本执行先后决定的而是由一套独立于游戏对象逻辑的规则来判定。很多新手习惯在Hierarchy里把想显示在后面的物体往下拖拖完之后发现画面纹丝不动就是这个原因——你不小心在跟引擎的排序机制较劲。1.1 三个排序维度及其优先级我把Unity判定“谁先画谁后画”的规则拆成三个维度按优先级从高到低排列维度作用范围判定方式常见使用场景RenderQueue渲染队列Shader/Material级别ShaderLab里Tags { Queue ... }的值从小到大区分不透明、透明、背景、OverlaySorting Layer Order in LayerSpriteRenderer/CanvasRenderer等2D渲染器每层有全局编号同层内Order数字大的后画2D角色、UI、技能指示器、特效深度缓冲ZTest/ZWrite像素级当前片元深度与缓冲已有深度比较3D物体的前后遮挡关系这里要特别注意深度缓冲并不决定“谁先画谁后画”它决定的是“后画的能不能覆盖先画的”。真正控制绘制顺序的第一优先级是RenderQueue第二优先级是2D相关的Sorting Layer和Order。如果同一个物体既不是Sprite也不是Canvas那它基本只靠RenderQueue和深度缓冲来工作。1.2 为什么游戏引擎不做“从上到下扫描”搜索热词里有一条“dom从上到下顺序渲染是不是更快”这个疑问我在Web开发者朋友那边也听过。浏览器渲染DOM树时确实有明确的文档顺序但Unity这类实时3D引擎完全不同。图形API本质上是“提交一批绘制命令GPU按命令顺序处理”引擎为了减少状态切换会对命令重新排列。比如同一批物体材质相同就尽量挨在一起画不透明物体大多不排序反正深度缓冲会兜底透明物体才需要严格按距离从远到近。打个比方餐厅打饭不是按客人进门顺序叫号而是按窗口分类排队——拿包子的人去包子窗口拿饮料的人去饮料窗口最终每个窗口内部再按先后顺序出餐。游戏引擎里的“窗口”就是RenderQueue、Sorting Layer、材质状态这些维度。所以你手动调整Hierarchy上下顺序本质上是想通过“进门顺序”控制“出餐顺序”当然经常失效。2. RenderQueue才是全局总开关任何一个可渲染物体最终都要经过Shader里的Queue标签来分拣。这个标签决定了物体被划进哪个大组大组之间的优先级是“一刀切”的Background组永远先画接着是Geometry不透明几何体、AlphaTest、Transparent透明组、Overlay最后画的覆盖层。2.1 内置队列的数值体系ShaderLab里最常见的写法是这样的Shader Custom/MyShader { SubShader { Tags { Queue Transparent } // ... } }Unity内置的队列数值大致如下队列名称数值典型用途Background1000天空盒、远景背景Geometry2000默认不透明物体、场景静态网格AlphaTest2450带Alpha Test的植物、头发Transparent3000半透明物体、粒子、UI元素Overlay4000镜头光晕、最终覆盖层你可以用Renderer.material.renderQueue 3000;在脚本里动态修改单个物体的实际队列值也可以写成Tags { Queue Geometry1 }做到微调。这个“1”的做法很实用比如角色身上有些装饰物想排在不透明主体的后面一点又不想完全丢进透明队列就可以用Geometry1。2.2 不透明物体不排序透明物体必须从后往前默认情况下不透明物体会被归到Geometry队列这个队列内部的绘制顺序基本不按距离排序而是按材质、纹理、渲染状态做合批优化。你可能会疑惑如果不排序那后画的大楼会不会把先画的大楼盖住不会。因为每个不透明片元写入前都会做深度测试深度值更远的片元直接丢弃这样一来即使后画只要几何位置在后面也无法覆盖前面的物体。这是深度缓冲的功劳不是排序的功劳。透明物体就不一样。透明材质默认会关闭深度写入ZWrite Off因为如果每个半透明片元都写深度那后续需要覆盖的半透明物体全被堵死了。没有深度写入就必须靠CPU端对物体中心到相机的距离排序从远到近依次绘制否则就会出现“后面的玻璃挡住了前面的玻璃”这种错乱。但这里有个很大的坑距离排序用的是物体包围盒中心点不是物体表面。当一个长条形的半透明物体横跨场景两端距离远近差异较大时中心点排序可能完全判断错。2.3 半透明穿插的实战解法遇到透明物体相互穿插、排序错误我常用的处理方案有三个把大的透明物体拆成多个小Renderer让每个小物体的中心点都更接近实际表面远近距离判断更准确。在Project Settings Graphics里调整Transparency Sort Mode可以把它改成Orthographic或自定义轴让透明排序按某个固定轴而不是相机方向尤其适合2D风格战斗场景。干脆用两个Pass做深度占位先用一个不透明的、不写颜色的Pass把深度写入再用透明Pass绘制半透明部分后续物体就无法穿透上来。方案3在写水面、冰面、能量罩时特别常用。我给一个简化的Shader伪代码Shader Custom/WaterWithDepth { SubShader { // 深度占位 Pass不写颜色 Pass { Tags { Queue Geometry2 } ColorMask 0 ZWrite On } // 透明主体 Pass { Tags { Queue Transparent } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off } } }这样水面既能挡住它后面的物体自己又是半透明的表现稳定很多。2.4 渐隐、水墨、二次元特效里的Queue坑热词里有一条“unity脚本控制逐渐消失”很多人在脚本里直接改material.color.a结果物体一点反应没有。原因很简单材质当前还是不透明模式不透明材质的Alpha值在默认光照模型里基本不影响渲染只有把Shader的Surface Type改成Transparent也就是Queue变成TransparentAlpha才会参与混合。在URP里操作更直观Shader面板上把Surface从Opaque切到Transparent即可但背后引擎还会同时改掉RenderQueue和ZWrite状态。水墨晕开特效、二次元角色描边这类需求就更讲究顺序了。水墨特效通常是粒子系统粒子材质必须放在Transparent队列且关闭深度写入不然它会把后面的角色妆容给“挖空”。二次元描边我习惯用两个Pass第一个Pass专门沿法线方向扩一圈写描边第二个Pass画正常身体两个Pass写在同一个Shader里执行顺序按代码从上到下这样描边会被身体自然覆盖区域遮挡不会出现描边穿透到身体前面的奇怪结果。3. 2D与UI的排序控制Sorting Layer和Order in Layer如果项目里只有3D模型排序规则相对简单一旦混入Sprite、UI、粒子问题就来了Sprite本身也是Renderer它到底怎么跟3D模型比优先级答案是看Sorting Layer和Order in Layer这两个字段在2D/UI体系里优先级极高可以被视为“RenderQueue之外的第二个排序维度”。3.1 Sorting Layer的全局定义在Project Settings Graphics里有一个Sorting Layers列表从上到下依次由底层往顶层排列你在列表里新增的层靠下就越靠后渲染。实际操作时层名尽量不要乱起我用过一套固定命名BackGround、Ground、Character、Effect、Indicator、UI。这样做的好处是所有团队成员写代码时都能准确选择层名不会出现 “我想让特效显示在角色前面但不知道该选哪层”的纠结。技能攻击指示器热词里那条“unity skill attack indicators”就是个典型场景角色脚下有圆形攻击范围头顶有蓄力条攻击特效在命中瞬间爆发。如果指示器、蓄力条、特效都在默认的Default层就只能靠Order in Layer硬调稍微加一个特效就可能被盖住。更合理的做法是把指示器单独放在Indicator层这个层排在Effect下面、UI上面既保证指示器显示在角色和战斗特效之上又不会被血条、面板等UI遮挡。3.2 Order in Layer与SpriteRenderer的Z坐标同一层内的Sprite按Order in Layer数字从小到大绘制数字越大越靠前。如果Order完全相同Unity会拿Sprite距离相机的远近来比离相机更近的优先显示。在正交相机下这就是Z轴数值的比较。这里要特别提醒不要为了方便把所有Sprite都设成同一个Order然后再去拖Z轴。Z轴参与的是“相机距离排序”如果两个Sprite在空间上交错结果可能很不直观。我踩过的坑是做一个2D俯视角物品拾取提示提示箭头和金币堆叠在同一个位置Z轴微调了一晚上也没稳定最后老老实实把箭头设成Order 10、金币设成Order 5世界立刻清净了。2D项目里能用Order解决的就别依赖Z轴Z轴留给真正的空间位置判断。3.3 Canvas渲染顺序的特殊规则与常见坑Canvas有三种渲染模式对层级的影响差异巨大Screen Space - Overlay永远最后绘制无视深度缓冲和相机裁剪适合全屏UI、对话框、广告位。缺点是无法被3D物体遮挡也不参与后处理。Screen Space - CameraCanvas被放到指定相机的裁剪空间里通过Plane Distance控制离相机远近参与相机的深度排序。World SpaceCanvas像一块世界里的面板可以被模型遮挡也受光照影响常用于HUD、血条跟随。多个Canvas同时存在时先按Camera Depth排序再按sortingOrder和sortingLayer。但有个经典坑如果多个Canvas使用同一套图集和材质动态合批可能把它们的绘制顺序强行合并导致你改Canvas上的sortingOrder不生效。我遇到过一次两个血条面板一个在后一个在前改Order后画面纹丝不动最后发现是两个Canvas的CanvasRenderer被批处理合并了给其中一个Canvas换了个材质或关掉动态批处理才解决。从Figma导出的UI进Unity后层级乱也多半是这个道理你不是把素材拖错顺序了而是Canvas自身的渲染模式和sortingOrder没有规划好。导入后统一在Canvas根节点把Sorting Order设成固定值再在内部用RectTransform的上下关系控制显示会比层层嵌套Canvas靠谱得多。4. 3D深度缓冲与遮挡关系到了3D场景决定“谁被谁挡住”的主角已经不再是排序指令而是深度缓冲。如果前面半透明问题让很多人头疼那不透明物体的深度管理相对简单但也需要理解几个致命细节。4.1 ZWrite与ZTest的关系每个片元在写入屏幕前GPU都会做一次深度测试只有测试通过的片元才会执行颜色混合和深度写入。两者可以通过ShaderLab来控制Pass { ZWrite On ZTest LEqual // ... }常见组合的用途如下组合用途ZWrite On / ZTest LEqual默认不透明物体能正常遮挡ZWrite Off / ZTest LEqual透明物体需要从后往前排序ZWrite Off / ZTest Greater描边或体积光扩边显示在物体表面外侧ZWrite On / ZTest Always特效后处理永远绘制但也会污染深度一个常见的误区是给透明材质开启ZWrite On想让透明物体被遮挡得更“干净”。但这样会导致后续透明物体在深度测试时全军覆没半透明遮挡链直接断裂。正确的做法是用前面提到的深度占位Pass而不是简单打开ZWrite。4.2 多相机叠加的排序方式游戏里一个场景往往不止一个相机主相机负责世界UI相机负责面板小地图相机负责俯视。相机的执行顺序取决于Camera组件上的Depth值数值小的先渲染大的后渲染。后渲染的相机会默认覆盖整个屏幕除非Clear Flags设置为Depth Only。热词里那条“unity摄像机跟随”跟这个也有关系。如果你的跟随相机挂在角色下面而场景里还有一个小地图相机切记别把小地图相机的Depth值调到比UI相机还大否则小地图会盖住主UI。我见过的惯用做法是世界主相机Depth0小地图Depth1但Clear Flags设为Depth OnlyUI相机Depth2。记住Clear Flags里Solid Color会直接拿底色盖掉之前相机的画面多个相机叠加时优先用Skybox或Depth Only。4.3 阴影与渲染顺序是两套独立Pass热词里有“unity阴影问题”不少人以为阴影错乱是渲染顺序引起的。其实阴影用的是ShadowMap流程它在正式绘制前会把场景按光源视角渲染一遍深度这个阶段只跑所有物体的ShadowCaster Pass。如果材质的Shader没有包含ShadowCaster Pass物体就不会出现在阴影图上哪怕它本身绘制顺序再正常也白搭。排查“模型没有阴影”的顺序应该是先看光源是否开启阴影再查Shader里有没有LightMode ShadowCaster的Pass最后看Renderer的Cast Shadows属性是不是设成了Off。URP里如果用了自定义Shader又没接阴影直接在材质面板开启Receive Shadows也无效因为你缺少那个Pass。至于“unity模型遮挡剔除插件”Unity自带的Occlusion Culling往往就被很多人忽略了。这套机制在做遮挡剔除烘焙后会在渲染前剔除被建筑挡住的整个网格。它和深度缓冲不冲突一个是粗粒度剔除一个是像素级精确遮挡。实际项目中优先开内置的不用急着买第三方插件。4.4 Z-fighting闪烁的根源两个面完全重合时深度缓冲无法判断谁前谁后同一像素反复被两个面交替写入画面就会闪烁。这种“打架”在数字孪生、贴花、路面白线上特别常见。解决办法首先是避免面重合其次可以给Shader加上深度偏移Pass { Offset -1, -1 }Offset的作用是把片元深度往相机方向拉一点让重合的面有一个明确的先后。这个参数不是越大越好太大会导致物体被错误地拉穿其他遮挡物我通常从-0.5、-1开始试。5. 常见渲染顺序问题的排查与实战修正这里我整理了一张速查表覆盖我这些年遇到的最常见的排序问题。遇到问题时可以照着表里从前往后排查大部分情况能在十分钟内定位。5.1 问题速查表症状可能原因解决方案Sprite被3D模型挡住Sprite与模型都在Geometry队列模型深度更近给Sprite设更高的Sorting Layer半透明物体穿插错乱透明排序用中心点距离长物体判断不准拆分Mesh、改Transparency Sort ModeUI盖不住特效Canvas RenderMode不是Overlay或相机Depth低于特效相机UI相机Depth调到最大Overlay优先多个Canvas的sortingOrder不生效动态合批把渲染状态合并了调整材质或关闭动态批处理淡出效果没反应材质还在Opaque模式Alpha不参与混合切到Transparent/Fade模式物体没有阴影Shader缺少ShadowCaster Pass给材质补上ShadowCaster Pass两个重叠面闪烁深度精度冲突Z-fighting拉开间距或加Offset5.2 实战让Sprite稳定显示在模型前面热词里那句“sprite renderer在模型前渲染”我提供一个完整的实操流程。在场景中创建一个带Sprite的物体位置移动到3D模型附近。初始状态下Sprite很可能被模型遮挡因为两者都在Geometry队列模型表面的深度更近。选中Sprite物体在SpriteRenderer组件里找到Sorting Layer把它从Default调整为更高的一层比如Character层之上的Effect层。如果想让Sprite盖在所有3D物体之上可以给它单独建一个层叫AlwaysOnTop什么都不放只给这个Sprite用。如果Sprite是UI图标更稳的做法是直接用UI Canvas的Overlay模式彻底脱离3D空间。需要注意如果你给Sprite设了很高的Sorting Layer但它的材质仍是Transparent那它还是参与透明绘制逻辑可能被别的透明物体穿插。这种情况下要检查材质队列和ZWrite设置确保它跟透明批次里的其他对象处于合适的关系。5.3 顺带说清LayerMask与RenderingLayerMask热词里有人问“unity中的layermask与renderinglayermask的区别是什么”。这两个东西名字很像但完全不是一回事。LayerMask是传统Unity的层遮罩用来做物理射线检测、相机Culling Mask过滤它决定“哪些物体参与物理检测、哪些物体被相机看到”本身不参与渲染顺序。RenderingLayerMask是SRP时代引入的渲染层遮罩主要用来控制Decal、Light Layer这类渲染特性判断“这个贴花是否投射到这个物体上”“这个灯光会照亮哪些物体”。它们跟渲染顺序都没有直接关系。如果你遇到模型不接收贴花想的是“是不是被其他物体挡住了”其实是RenderingLayerMask里的Decal Layer不匹配。而Camera的Culling Mask用的还是传统LayerMask两套遮罩混着用就会出现“物体明明被相机看到了特效却就是贴不上”的怪象。5.4 实战处理“半透明遮挡但又被深度正确挡住”这个需求常见于游戏里的大型能量护罩它应该是半透明的能看到后面的建筑但建筑边缘又要明确地被护罩边缘压住不能整面穿透。我的做法是给护罩材质加两个Pass。第一个Pass单独负责“把ZTest打开、ZWrite打开、ColorMask 0”相当于在深度缓冲里画了一个不透明的护罩轮廓。第二个Pass正常绘制半透明护罩本身ZWrite Off。这样GPU先知道护罩在哪里再绘制护罩颜色结果建筑被护罩的边缘部分正确遮蔽而透过护罩中央看建筑时又会呈现半透明的混合效果。这个方案在URP、内置管线下都能跑只是URP里要注意RenderObjects Feature不要把这个depth pass二次排序。6. 优化渲染顺序的一些个人经验6.1 排序方式直接影响性能不只是表现很多人只关心排序对不对不关心排序多不多。渲染顺序一旦频繁变化引擎的合批和状态切换就会爆炸。同一个场景里如果把大量Sprite的sortingOrder在运行时来回改或者动态把物体的renderQueue从Geometry改到TransparentUnity会不断拆批、重新上传顶点数据帧率立刻给你颜色看。我有一个习惯能在编辑器里定好的顺序绝不让脚本动态改能在材质里写好的Queue绝不在代码里赋值。运行时做排序的代价远高于看似“灵活”的收益。限制物体排序变化的次数比你去优化Shader效率更见效尤其是在移动端。热词里那条“dom从上到下顺序渲染是不是更快”的疑惑放在Unity里就变成只要渲染状态切换少、批次合并好顺序本身快不快不是关键关键是别为排序付出额外的重组代价。6.2 SortingGroup让组合排序变得可控一堆Sprite需要作为一个整体跟周围物体比较优先级时单独调每个Sprite的Order会很痛苦。比如一个2D角色由身体、武器、披风三个Sprite组成你想让整个角色比地面物品层高又比天气雨效低。如果只调每个子Sprite加一个新特效就得全盘重调。这时可以用Sorting Group组件挂在角色根节点上。Sorting Group会给整组一个统一的Sorting Layer和Order值组内子Sprite的相对顺序还在但跟外部物体比较时整组被视为一个单位。最典型的应用是2D战斗阵营里的整支小队编队以及纸娃娃系统。注意SortingGroup不要嵌套太深两层以内足够嵌套太多容易让人搞不清优先级来源。6.3 我现在的默认配置最后分享一套目前我起新项目时最先定下来的默认规则。场景Sorting Layers从上到下固定为BackGround、Ground、Character、Effect、Indicator、UI。所有透明特效材质统一走Transparent队列并且ZWrite Off。UI主Canvas固定为Screen Space - Overlay任何小地图、血条、指示器的Canvas按Camera Depth排好然后sortingOrder由根节点统一控制。这个配置我用过好几个项目表现稳定排查问题也快。以后只要有人跑来问“为什么我的Sprite跑到模型前面了”或“为什么半透明水墙把后面的角色切掉了”我先问一句他的Sorting Layer和Queue设置问题基本就能解开一半。渲染顺序不是玄学它只是三个维度叠加后的确定性结果理解透之后调起来真的很快。