尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity渲染排序深度解析:MeshRenderer的SortingLayer与Order in Layer实战

Unity渲染排序深度解析:MeshRenderer的SortingLayer与Order in Layer实战 做Unity项目的时候最让人挠头的往往不是玩法逻辑而是渲染排序。MeshRenderer的渲染排序问题看起来简单实际坑起来能让人怀疑人生3D角色明明站在塔后面却被塔盖住粒子特效明明发射了却被建筑模型挡住UI稍微一偏移3D模型就穿到面板上。这些问题的根源基本都指向SortingLayer和Order in Layer这两个看似不起眼的属性。这篇文章我从原理讲到实操把MeshRenderer的渲染排序机制、设置方法以及我这些年踩过的坑一次说清楚适合在2D与3D混合渲染、数字孪生、小游戏UI嵌入模型、角色与场景穿插等场景里摸爬滚打的开发者。1. 渲染排序的本质一条管线里的先后之争1.1 Unity完整的渲染顺序判断链很多新手上来就急着调参数连排序规则都没搞清楚结果改了一圈还是不对。我建议先花五分钟把Unity的渲染顺序判断链刻在脑子里后面所有操作都是有据可依的。Unity在同一个相机下渲染物体时大致会按下面这条链子来判定谁先谁后优先级判断项规则备注1Camera Depth相机深度深度值小的先渲染后渲染的会覆盖先渲染的多相机项目里这是最高优先级2Sorting Layer排序层Sorting Layers列表中排在后方的层会被绘制在更前面全局生效MeshRenderer和SpriteRenderer统一比较3Order in Layer层内序号同一层内数值越大越后渲染显示越靠上这是日常调得最频繁的参数4Render Queue渲染队列Shader中Queue标签的数值Background1000、Geometry2000、Transparent3000等材质级别影响透明与不透明分组5深度 / 距离不透明物体按离相机近到远绘制透明物体按离相机远到近绘制需要配合ZBuffer与ZWrite使用这条链子我记错过好几次特别是Render Queue和SortingLayer到底谁优先。网上很多资料说法不一实际用Frame Debugger抓一次队列就能看明白在Renderer级别上SortingLayer和Order in Layer的判断会优先于Render Queue。也就是说你给MeshRenderer设置了一个更靠后的SortingLayer即便它的Shader是Geometry队列也可能被另一个Transparent队列的Sprite盖在底下结果取决于层序和排序值而不是Queue数值本身。换成人话Render Queue是“材质内部的分组”SortingLayer和Order是“整个Renderer在屏幕上的站位”。站位高的哪怕它是半透明材质也能稳稳压住站位低的。但要注意这不代表Render Queue没用——透明和不透明物体之间的混合、Depth写入、遮挡关系依然要受Shader队列和ZBuffer约束。1.2 MeshRenderer为什么经常被忽略排序能力很多开发者潜意识里觉得“渲染排序是SpriteRenderer和Canvas的事”MeshRenderer就老老实实按3D深度来。这是一个很大的误区。从Unity 5.x开始MeshRenderer组件上就带上了Sorting Layer和Order in Layer只是一直藏在Additional Settings折叠区域里不刻意去找根本注意不到。在MeshRenderer的Inspector面板往下翻你会看到Sorting Layer和Order in Layer这两个字段默认值分别是Default和0。这意味着不主动设置时所有MeshRenderer都会挤在Default层上Order全部为0接下来只能靠RenderQueue和深度关系来定显示顺序。而SpriteRenderer、ParticleSystemRenderer这些2D渲染器天生就有自己的SortingLayer和Order所以当3D模型和2D物体混在一起的时候就会出现“明明按照深度应该被遮挡结果模型强行显示在精灵上面”的诡异现象。这个差异的根源在于渲染体系的历史包袱2D渲染从诞生起就强调“贴图叠放”需要明确的前后关系3D渲染则依赖深度缓冲来处理遮挡默认不需要美术去手动排顺序。但当2D、3D混用或者你想用3D模型去模拟2D立绘、用UI盖住模型局部的时候两种体系的判断规则就会打架。理顺的办法并不是绕开MeshRenderer的排序属性而是主动把它纳入和SpriteRenderer、Canvas统一的排序体系里。2. MeshRenderer与SortingLayer的设置实操2.1 在Inspector面板完成基础配置先讲最直接的操作。选中场景里的3D物体在MeshRenderer组件的最下方找到Additional Settings里面有Sorting Layer和Order in Layer两个选项。Sorting Layer下拉菜单里默认只有Default想添加自定义层点下拉框底部的Add Sorting Layer就会跳到Tags Layers窗口的Sorting Layers列表。点右下角的加号新增一层然后填名字就行。这里有个很关键的细节新增的层永远追加在列表底部列表顺序代表渲染顺序——排在前面的层先渲染排在后面的层后渲染、显示在前。这个顺序是不允许拖动调整的所以项目初期一定要做好规划否则后期想插一层在中间就只能改所有相关物体的层引用。Order in Layer相对简单纯数字比较同一SortingLayer下数值越大越靠前显示。它可以是任何整数但我不建议用魔法数字乱填最好有一个全局的排序值规划表。比如层级典型对象Order范围Background3D背景模型、远景建筑-100 ~ 0Ground地面、道路、地表细节0 ~ 50Character角色、NPC、玩家100 ~ 200Effect技能特效、粒子、半透明Mesh300 ~ 500Overlay3D模型的描边、高亮框600 ~ 800实际项目里我见过不少团队因为没人管Order角色、建筑、特效全挤在0最后只能用改Shader Queue或者调相机深度的方式硬撑。与其这样不如花十分钟建个排序表贴墙上大家都按约定来。同一个排序层内的差值建议至少留出20以上的间隔给玩法动态调整留够空间。2.2 用C#脚本动态控制排序静态场景可以靠Inspector拖但实际项目中大量排序是要在运行时动态改的。比如角色走进建筑背后你的排序逻辑必须跟着坐标变化。C#里控制MeshRenderer排序非常直接using UnityEngine; public class SortingController : MonoBehaviour { private Renderer _renderer; [SerializeField] private string targetLayerName Character; [SerializeField] private int baseOrder 100; private void Awake() { _renderer GetComponentRenderer(); ApplySorting(targetLayerName, baseOrder); } public void ApplySorting(string layerName, int order) { // 方法一按名字设置直观但每次会做字符串查找 _renderer.sortingLayerName layerName; _renderer.sortingOrder order; } public void ApplySortingById(string layerName, int order) { // 方法二先缓存ID再设置避免运行时反复按名称查找 int layerId SortingLayer.NameToID(layerName); _renderer.sortingLayerID layerId; _renderer.sortingOrder order; } }这里有几个容易踩的细节。第一代码里设置的是Renderer层面的SortingLayer和Order它会影响这个MeshRenderer上挂的所有材质因为一个Renderer的排序信息是一份。如果你想对同一个Mesh的多个子材质单独控制排序那必须把模型拆成多个MeshRenderer或者改用不同RenderQueue的材质靠Shader队列去区分。第二sortingLayerID和sortingLayerName相比ID方式在运行时性能更好因为字符串到ID的转换在底层有查找开销。一般我会在Awake里把初始层级定好Update里只改sortingOrder这样最省。第三很多做2D游戏的同学会用一个经典技巧根据物体在场景中的Y坐标动态计算order模仿2D游戏的纵深感void Update() { // 越靠下Y越小的角色越应该显示在前面所以order越大 _renderer.sortingOrder Mathf.RoundToInt(-transform.position.y * 10f) baseOrderOffset; }这个技巧用在3D模型和2D场景混排里同样有效只要你把基准面设好乘以一个合适的系数。但要注意每帧改sortingOrder会打断一部分批处理和排序优化几千个物体同时这么做会有压力。一般只对少数关键角色和交互对象使用静态建筑别这么搞。2.3 创建与管理自定义SortingLayer的团队协作前面说了SortingLayer列表只能追加不能重排这对团队协作是个隐患。两个分支同时在各自编辑器里添加了不同名字的层合到同一个项目后排序索引就会乱而且这种问题在代码里很难一眼看出来。合理的做法是把SortingLayer的规划和程序代码一样对待用常量类统一管理。毕竟SortingLayer的ID在代码里就是整数不同的层对应不同的int值一旦索引错位所有引用它的物体都会受影响。我在项目里会写一个静态类来统一暴露这些常量public static class SortingLayers { public const string Background Background; public const string Ground Ground; public const string Character Character; public const string Effect Effect; public const string Overlay Overlay; } public static class SortingOrders { public const int CharacterBase 100; public const int EffectBase 300; public const int OverlayBase 600; }同时规定新增SortingLayer必须走一条固定流程先更新规划表再在编辑器里添加最后把常量加入代码并提交。这样即便有人在不同分支上加了同一个名字的层合并时也能靠常量检查出冲突。对了还有一个容易被忽略的点切换场景或动态加载AB包时如果某个Prefab引用了不存在的SortingLayerUnity并不会报错而是会静默地落到Default层。线上项目里排序突然失效排查半天最后发现是加载顺序导致SortingLayer还没被注册这种情况我遇到过不止一次。3. 实战场景从“排不上”到“指哪打哪”3.1 场景一3D角色与2D场景融合第一个典型场景是2D场景里混入3D角色。很多独立游戏为了省钱省力场景直接用2D美术素材拼角色却用3D模型做骨骼动画。这时候3D角色本身是MeshRenderer默认排在Default层0而2D场景里的建筑、装饰都带着各自的SortingLayer和Order。结果就是角色站在房子后面画面却显示角色压在房子上。解决思路很简单把场景里的每个3D建筑也当作2D“图层”来安排给建筑的MeshRenderer挂上SortingLayer和Order然后让角色的Order按照和建筑的相对位置动态变化。举个例子一个建筑在角色前方更靠屏幕下方时建筑的Order设成80角色Order设成100角色就能盖在建筑上当角色绕到建筑后方把角色的Order改成60角色就被建筑盖住了。这个“绕到后方”的判断在2D视角下通常不看深度而是看坐标和碰撞体区域的包含关系。一个比较稳的写法是用角色的世界坐标跟每个建筑的Bounds做判断一旦角色中心进入建筑区域且Y坐标低于某个阈值就降低角色排序private void UpdateSorting() { bool behindBuilding false; for (int i 0; i _buildings.Count; i) { Bounds bounds _buildings[i].GetComponentRenderer().bounds; Vector3 pos transform.position; if (bounds.Contains(new Vector3(pos.x, pos.y, bounds.center.z))) { behindBuilding true; break; } } _renderer.sortingOrder behindBuilding ? 60 : 100; }这段逻辑的关键是Bounds中心不会太离谱否则角色明明站旁边也会被当作“进入”区域。我自己的体会是不管逻辑怎么变排序值一定不要只设0和1两档最好给每个可相互遮挡的物体根据其“屏幕垂直位置”连续计算。比如用transform.position.y映射一个基础排序值再叠加当前是否在建筑后方的修正值这样画面过渡会自然很多。3.2 场景二模型与UI元素的穿插处理第二个场景是3D模型和UI穿插。最典型的诉求是界面上有一个圆形的“角色展示框”3D角色模型要放在框里但希望模型的头发或者武器能超出框的边缘压在UI元素上。这种情况下模型和UI之间并没有真正的3D遮挡关系优先级完全靠排序控制。默认情况下Screen Space - Overlay模式的Canvas在渲染顺序上几乎总是最后绘制所有3D模型都会被UI盖住。想让模型盖住UI简单粗暴的方法是给Canvas设置SortingLayer和Order但Overlay模式本身不受Renderer排序的严格约束。我实际项目中用的比较多的是把Canvas改成Screen Space - Camera模式绑定一个专门的UICamera然后再去调Canvas的SortingLayer和Order。举个例子UICanvas的SortingLayer设为UIOrder设为200角色模型的MeshRenderer的SortingLayer设为UIOrder设为300。这样角色模型就会在UI之后绘制自然能从UI框边缘伸出来。要注意如果你希望UI框本身还盖住模型的一部分那UI框不能和模型共用同一个Canvas和Order得把“外框”和“底层背景”拆成两个Canvas或者用两张Image分层排。模型Order在背景之上、在边框之下效果就出来了。这种做法的代价是UI渲染从纯粹的Overlay变成了一个相机下的世界空间绘制会带来一些阴影和透明排序的变化但比起用RenderTexture去渲染3D模型再贴到UI上性能和实现成本还是低很多。热搜词里有人搜“Unity摄像机跟随”其实这个方案里UICamera的深度设置就很关键UICamera只渲染UI层Depth大于主相机而主相机要Culling Mask里勾掉UI层。3.3 场景三透明特效与场景的叠加顺序第三个场景是半透明特效和场景模型的叠加。很多特效都用粒子系统或半透明Mesh实现而粒子系统的Renderer上也有SortingLayer和Order in Layer它本质是ParticleSystemRenderer。你可以像控制MeshRenderer一样给粒子设置一个比场景建筑更高的Order让技能光效压在建筑前面。但这里有个真正的深坑透明物体的排序并不按网格表面到相机的实际距离来算而是按Renderer包围盒的中心点来算。也就是说一个很大的3D球体特效它的包围盒中心可能在场景深处而它的边缘已经延伸到角色跟前了。这时候透明排序会认为它“离相机远”先绘制然后角色的不透明部分再绘制看起来就是特效被角色切成两半。我处理这个问题的经验是三步走。第一步尽量缩小透明Mesh的包围盒必要时把大特效拆成几个小块各自挂排序值。第二步强制指定透明物体的Order in Layer让特效和角色在同一图层下有个明确的前后关系而不是依赖中心点距离。第三步也是很多人忽略的检查材质上的ZWrite是否关闭。透明材质一般应该关闭ZWrite否则透明体在深度缓冲里写入后后续物体可能会被错误遮挡。想明白“透明排序看中心点、不透明排序看深度测试”这两条规则之间的差异场景里的排序问题就解决了一大半。4. 常见问题与排查技巧实录4.1 排序无效先查这四个地方我在社区和群里给人看排序问题至少一半是下面这四个低级原因造成的。你调了半天没效果先别怀疑引擎有bug按顺序排查第一个地方多个相机。只要场景里有两个相机且物体被不同相机渲染那么相机Depth的优先级会直接压过SortingLayer。A相机里排序再靠后的物体只要B相机的Depth更大且渲染范围包含它照样能盖过去。遇到这种问题先把相机分层和Depth理清楚再谈物体排序。第二个地方Shader的RenderQueue。前面说过Renderer的Sorting优先级高于RenderQueue但如果你给一个物体挂的材质Queue是4000Overlay另一个是2000Geometry在相同SortingLayer和Order的情况下Queue 4000的物体永远最后画。有时候你调Order调不出效果是因为两个物体的SortingLayer和Order完全相同引擎默认按Queue和深度来跟你心里预期的顺序不一致。第三个地方动态合批和静态合批。合批是会改变渲染顺序的尤其静态合批会把多个Mesh合并到一个批次里合并后这部分物体作为一个整体参与排序批内顺序你控制不了。如果你发现某个模型单独看排序是对的和旁边的物体组合后就不对了多半就是合批在捣鬼。第四个地方没有考虑Canvas的渲染模式。当你处理UI和3D模型的穿插时第一步就是确认Canvas的模式。Screen Space - Overlay的Canvas基本不参与常规SortingLayer比较你给3D模型调一万的Order也盖不住它。除非改成Camera模式或者用一个专门的排序层否则在这个方向上努力就是浪费时间。为了方便记忆我整理了一个排查速查表现象优先检查项大概率原因模型始终被UI盖住Canvas Render ModeOverlay模式下常规排序无效模型排序有时对有时错相机Depth、合批多相机或合批干扰设置了Order没变化SortingLayer名是否拼错字符串索引失效静默落入Default透明特效被模型切成两半包围盒中心、ZWrite透明排序按中心点计算同一个Renderer下不同材质排序乱材质RenderQueue受限于单一Renderer排序信息4.2 透明物体排序“翻车”的底层原因透明物体的排序单位是Renderer排序依据是Renderer.bounds的中心到相机距离。这跟不透明物体完全不同不透明物体排序再乱深度测试会兜底透明物体一旦画错顺序混合结果就是错的而且很难看。很多开发者给一个建筑挂上很大的玻璃面或者给角色加一个长条光柱特效就会遇到排序不稳定。因为一个长条光柱的bounds中心可能在角色胸部的位置而光柱顶端已经伸到相机前了底端还在脚底。按中心点距离排序结果跟肉眼看到的前后关系完全对不上。处理这类问题我推荐几个实战有效的办法。第一把长条或大范围特效的Renderer替换成多个小Renderer每个小块负责一小段每块单独设Order。第二不用Sprites或Standard透明Shader而是写一个特殊透明Shader把Renderer的排序彻底交给SortingLayer和Order无视深度距离。第三当几个透明物体的前后关系几乎永远不变时直接在它们的Order in Layer上设置两个相差足够大的数字比如100和110强制稳定顺序尽量不要让引擎去“智能”排序。顺带一提Bounds不仅仅影响透明排序还影响相机的裁剪判定。如果MeshRenderer的包围盒因为动画或顶点蒙皮计算太大哪怕物体实际很小相机也可能因为包围盒在视野里而多渲染这个物体带来性能浪费。所以透明Mesh和动画模型的包围盒大小值得定期检查和校正。4.3 阴影、透明混合与跨平台排序的连带问题排序问题经常和阴影一起出现。我见过一个项目给3D建筑设了很高的Order in Layer让它显示在角色前方结果建筑的阴影却“穿”过角色投射到角色前面。原因很简单阴影投射和阴影接收跟SortingLayer、Order in Layer没有直接关系它由光源和ShadowCaster Pass决定。你可以在MeshRenderer的Cast Shadows里选择Off或者把建筑的ShadowCastingMode设为Off来避免阴影干扰视线。透明物体的阴影更麻烦。一个半透明Mesh如果开着Cast Shadows它会往阴影贴图里写入不透明的阴影看起来好像一个实心板子投下了黑影。合理做法是半透物体一律关闭Cast Shadows或者用专门的ShadowCaster Pass。跨平台方面WebGL和微信小游戏这类环境下的透明排序通常和编辑器一致但移动端的GPU驱动和一部分低端安卓机会在深度精度上有所不同同一套排序在编辑器看着正常真机上可能闪烁或顺序颠倒。我的经验是在项目早期就制定“少用透明混合、多用Order硬排序”的原则能大幅降低真机上的意外。发布前一定要在目标真机上用Frame Debugger跑一遍关键表现别只在编辑器里自嗨。另外Unity的开发版本之间排序行为也有差异比如某些版本对Renderer.bounds的计算方式调整过会导致同一场景在两个版本下排序结果不同。如果团队中有人升级版本、有人没升级很容易出现“你本地是好的合完我的代码就乱了”的诡异情况。遇到这种问题先统一编辑器版本再排查排序逻辑。收尾前再分享一个实用习惯写了这么多最后说一个我最近两年一直在用的小习惯给角色的排序值做“正负区间”管理而不是盲目往上加。很多人以为排序值越大越靠前就给所有交互物体设500、800、1000……最后数值膨胀到几万完全失控。我的做法是先确定一个基准值比如角色为1000所有能被角色遮挡的物体统一放在950到1000之间所有能遮挡角色的物体放在1000到1050之间特效和UI再往上预留空间。这样每次调整只需要在一个小范围内微调谁压谁一目了然。再有就是在项目里开启Frame Debugger把排序问题当作渲染流程的一部分去观察。你看到的每一个Draw Call都带有一行排序相关的信息比如Sorting Layer、Order、Queue、还有计算出来的距离。手动调之前先在那里确认当前顺序比对着代码瞎猜高效得多。我自己排查排序问题一半的时间都花在Frame Debugger上它比任何文档都直观。
返回列表