项目中在开发角色面板或一些带有模型的活动面板的时候往往会需要将模型显示在界面的某些UI之间,常规做法是单开一台相机照模型渲到RenderTexture再赋值到UITexture上,然后使用NGUI组件的depth来控制显示先后顺序,但在某些的情况下,这种方式的处理反而会变得很复杂,很幸运我最近就遇到这样的情况
策划的需求如下:模型要显示在背景与背包之上,在名字板与一些介绍信息之下,而我们的有几个模型比如特殊,比如自带一个"屏风",而“屏风”是有半透效果的,(半透是要和背景颜色混合的)所以如果使用RendTexture的方式,照它的相机必须连带着背景一起照,重点是“模型必须在背包之前”,那么背包也要作为背景在另一个相机下一起照,而背包是受UICamera影响具有“点击”等操作事件的,所以此法无解。
有没有办法就让模型在NGUI之间渲染呢,答案是有的
要解决这个问题,我们首先要弄明白NGUI的是怎么渲染出来的,在这之前我们要引入几个影响渲染最终渲染顺序的名词
1.Camera
2.SortingLayers (点某个GameObject,在Inspector->Tag->AddTag...->SortingLayers进行设置,默认Default)
3.SortingOrder (Render的属性,继承Render的组件都具备)
4.RenderQueue (Shader的属性,详见 Unity渲染顺序(Queue,ZWrite,ZTest))
5.ZTest & ZWrite (GPU渲染管线的一环,详见 Unity渲染顺序(Queue,ZWrite,ZTest))
它们之间的优先级是这样的,Camera的depth优先级最高,在同一个相机下,优先级排序为 SortingLayers 高于 SortingOrder ,高于RenderQueue,但在它们渲染时GPU根据ZTest和ZWrite的配置决定最终的渲染顺序,这个话比较难理解,举个例子,有两个MeshRender A和B,B在A前面(B距离相机更近),其中A的SortingOrder为0,B的SortingOrder为1,根据SortingOrder的排序代码会先先画A会,然后画B,但是如果A和B都开启了深度测试,且都写入了深度缓冲,GPU在做深度测试时发现B的深度小于A,所以改成了先画B再画A,但是如果A和B都设置ZWrite Off,GPU在做深度测试时,由于没有参考的深度信息,所以直接通过深度测试,于是这个问题就变成了由SortingOrder的排序决定最终的渲染顺序,而NGUI就是这么做的,截图如下:
当SoringOrder相同,使用RenderQueue进行排序,由于NGUI支持透明,所以它的起始RenderQueue设为3000,具体的代码可自行翻阅UIPanel源码,至于同一个UIDrawCall(即同一个Atlas的多个UISprite怎么通过NGUI的Depth来区分的)是怎么区分先后顺序的?
原理是通过调整多个同一个UIDrawCall的UIGeometry绘制的顺序,而所谓的Depth值只是用来进行排序的依据,在NGUI中,同一个UIDrawCall代表一个MeshRender,使用同一个MeshFilter创建的Mesh,可使用不超过65000个顶点,其中每N个顶点形成一个UIGemetry,比如Simple的UISprite,使用4个顶点,Sliced的UISprite(九宫格)使用36个顶点...,(当然还有它们的uv信息和tangent以及normal等等),另外同一个UIDrawCall使用相同的材质和贴图(画同一个UIAtlas的一部分),所以NGUI可以实现动态合批并减少动态开辟内存
下面是我写的验证代码(自己打了一个图集,手撸了一个简易的Sprite,其中的uv信息都是事先知道的,Shader比较简单就是一个普通的图片采样,关闭了深度写入)
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SpriteTest : MonoBehaviour { [SerializeField] private MeshFilter mMeshFilter; [SerializeField] private MeshRenderer mMeshRender; [SerializeField] private Texture2D mTexture; private bool mSwitch; private void Fill1(Mesh mesh) { mesh.vertices = new Vector3[] { new Vector3(-0.5f, -0.5f, 0), new Vector3(-0.5f, 0.5f, 0), new Vector3(0.5f, 0.5f, 0), new Vector3(0.5f, -0.5f, 0) ,new Vector3(-0.2f, -0.2f, 0), new Vector3(-0.2f, 0.8f, 0), new Vector3(0.8f, 0.8f, 0), new Vector3(0.8f, -0.2f, 0) }; mesh.triangles = new int[] { 0,1,2,2,3,0 ,4,5,6,6,7,4, }; mesh.uv = new Vector2[] { new Vector2(0, 0), new Vector2(0, 0.8f), new Vector2(0.4f, 0.8f), new Vector2(0.4f, 0) ,new Vector2(0.4f, 0), new Vector2(0.4f, 0.8f), new Vector2(0.8f, 0.8f), new Vector2(0.8f, 0) }; } private void Fill2(Mesh mesh) { mesh.vertices = new Vector3[] { new Vector3(-0.2f, -0.2f, 0), new Vector3(-0.2f, 0.8f, 0), new Vector3(0.8f, 0.8f, 0), new Vector3(0.8f, -0.2f, 0) ,new Vector3(-0.5f, -0.5f, 0), new Vector3(-0.5f, 0.5f, 0), new Vector3(0.5f, 0.5f, 0), new Vector3(0.5f, -0.5f, 0) }; mesh.triangles = new int[] { 0,1,2,2,3,0 ,4,5,6,6,7,4, }; mesh.uv = new Vector2[] { new Vector2(0.4f, 0), new Vector2(0.4f, 0.8f), new Vector2(0.8f, 0.8f), new Vector2(0.8f, 0) ,new Vector2(0, 0), new Vector2(0, 0.8f), new Vector2(0.4f, 0.8f), new Vector2(0.4f, 0) }; } private Material CreateMaterial() { Material newMat = new Material(Shader.Find("Custom/Sprite")); var oldMat = mMeshRender.sharedMaterial; if (oldMat != null) newMat.CopyPropertiesFromMaterial(oldMat); return newMat; } private void Start() { var meshObject = new GameObject("Mesh", typeof(MeshFilter), typeof(MeshRenderer)); if (mMeshRender == null) mMeshRender = meshObject.GetComponent<MeshRenderer>(); if (mMeshFilter == null) mMeshFilter = meshObject.GetComponent<MeshFilter>(); if (mMeshFilter.sharedMesh == null) { mMeshFilter.sharedMesh = new Mesh(); Fill1(mMeshFilter.sharedMesh); } if (mMeshRender.sharedMaterial == null) { mMeshRender.sharedMaterial = CreateMaterial(); mMeshRender.sharedMaterial.SetTexture("_MainTex", mTexture); mMeshRender.sharedMaterial.renderQueue = 3000; } } void OnGUI() { GUI.skin.button.fontSize = 25; if (GUI.Button(new Rect(0, 0, 150, 70), "切换")) { mSwitch = !mSwitch; if (mSwitch) { Fill2(mMeshFilter.sharedMesh); } else { Fill1(mMeshFilter.sharedMesh); } } } }
效果如下,点击按钮可以切换两个Sprite的先后
(其实如果不关闭深度写入,由于NGUI的各个组件都摆在一起(Z轴都是0)还会造成Z-fighting的现象,大家可以自行尝试)
至此,NGUI的渲染顺序的原理已经弄明白了,那么关于标题的问题我相信已经有些解决方案了,这里我说一下自己的做法。
1.首先我在两个UI之间夹入一张UITexture(每个UITexture是单独的一个UIDrawCall),然后通过UIPanel获取所有的UIDrawCall,筛选出这个UITexture的,然后得到它SortingOrder与RenderQueue,将其赋值给模型
using UnityEngine; using System.Collections; public class ZTestApp : MonoBehaviour { public UIPanel mPanel; public GameObject mModel; void Start() { } void SetModelRQ(int sortingOrder, int renderQueue) { var renders = mModel.GetComponentsInChildren<Renderer>(); foreach (Renderer render in renders) { render.sortingOrder = sortingOrder; foreach (var mat in render.materials) { mat.renderQueue = renderQueue; } } } void Update() { for (int i = 0; i < mPanel.drawCalls.Count; i++) { var drawCall = mPanel.drawCalls[i]; Debug.LogFormat("index:{0} name:{1} sortingOrder:{2} renderQueue:{3}", i, drawCall.name, drawCall.sortingOrder, drawCall.renderQueue); if (mPanel.drawCalls[i].name == "middle") { SetModelRQ(mPanel.drawCalls[i].sortingOrder, mPanel.drawCalls[i].renderQueue); } } } }
2.将模型放到UI界面的后面
这一步最重要,如果模型放到前面,最终模型会跑到最前面,不会被黄色面板挡住,原理如下
a.根据SortingOrder的排序,先画褐色的背景,此时深度缓存中没有数据,深度测试通过,但褐色的面板不写入深度缓存
b.SortingOrder排第二位是模型,可以画模型,此时深度缓存中没有数据,深度测试通过,模型写入深度缓存
c.继续画前面的黄色面板,此时深度缓存中有模型的数据,但是黄色面板深度在模型之前,深度测试通过,所以黄色面板最后被画出来(如果模型放到整个UI最前面,黄色面板在做深度测试时,发现自己的深度比模型低,会被GPU改成先画黄色面板再画模型)
下面是显示结果与FrameDebuger的截图
3.最后我们将中间的红色UITexture的alpha值设置成1就行了
此法不会影响NGUI自身之前的排序,所以维护成本比较低,唯一要注意的就是要在Update中实时找夹层UITexture的UIDrawcall,原因是在实际项目中界面自身的显隐会导致UIPanel的Rebuild,会导致UIDrawall发生变化
测试工程: https://pan.baidu.com/s/17Dq7AModT5A5ht6zlA5nkA 提取码: 1jf9
文章评论