前言:

最近项目里有一个需求,要求某个单位(3D)一直显示在所有物体的最上层,哪怕他在其他物体后面。

经过一番折腾解决了,这里提供几种思路。这里先起一个Demo,如下图:

可以看到,黄色方块是在白色方块后面的。在Game视图中可以看到,黄色方块是被白色方块挡住了一部分。

现在我们用一些操作把黄色方块提到最前。

 

一、使用2个相机

这种可以说是最简单的了:

把主相机复制一份,然后剔除掉黄色方块的层。再在复制出来的相机里设置为Depth Only并且只照黄色方块的层:

可以看到这个黄方块立马就到最前来了,而且效果和预计一样的。

但是这里有以下几个问题:

1、这种情况黄色方块必须占用一个独立的层,不过这是个小问题;

2、多个相机性能会下降,也是小问题;

3、LWRP对多相机支持不好,很可能会出现各种渲染异常!(大问题!)

我就遇到LWRP的对多相机支持不好的情况,导致我最后不得不放弃此解决方案。我用的是 Unity 2018.4.23f1 ,LWRP是4.10.0 ,在部分设备上出现了显示异常。后面听说LWRP有升级后支持多相机了,并且看到网上有文章说可以解决LWRP不支持多相机的问题,不过这里不展开了。

 

二、修改ZTest/ZWrite

这个同样也在各种文章里面有讲解了,直接搜素ZTest就有很多讲解,这里就不赘述了。

那么我直接关掉ZTest,看看效果:

可以看到,不论是Scene视图还是Game视图,方块都挪到最前来了。看似非常完美,但并非如此:

如果我们增加一个绿色Cube,使用和黄色Cube一样的Shader,并且和黄色Cube重合一部分,再观察,问题就出来了:

我发现这两个方块交叠的部分没有正确显示,要么黄色方块最前,要么绿色方块最前(可以通过设置Render Queue 来改变顺序),而不能像C那样各自剔除一部分

原因也很简单:因为关闭了ZTest之后,方块的深度都无了,所以一旦出现这种交叠情况,GPU就无法剔除了。有时候这样没有关系,但有时候会导致模型闪烁(毕竟深度叠在一起了),就很严重,这就是我最后放弃这个方案的原因。

 

三、修改ClipPos

这个比较复杂,在Shader里面找到 UnityObjectToClipPos 这个函数调用,在其下面加一行代码:

		float _ZTestAddValue;

		vertexOutput vert(appdata_full input)
		{
			vertexOutput output;
			output.pos = UnityObjectToClipPos(input.vertex);

			//计算深度时额外增加一个值:
			output.pos.z+=_ZTestAddValue;	

			output.posInObjectCoords = input.texcoord;
			return output;
		}

_ZTestAddValue我是在属性面板定义的,他的意义稍后解释。这里先给随意给他一个值(比如0.5):

可以看到两个方块都显示在最上层,而且互相的剔除关系也正确了,这就是我们要达到的效果。

这里解释一下修改的原理:其实我修改的就是 SV_POSITION ,也就是物体的屏幕坐标。

 

关于 SV_POSITION 的简要解释和故障排除:

SV_POSITION的介绍网上有很多也不解释了,可以简单理解为屏幕坐标,我们修改的Z轴就是在屏幕坐标的深度值。

在不同底层 OpenGL或者 DirectX 其取值范围有所不同,在OpenGL里面是 【-1,1】,在DirectX 里面是 【0,1】。不过我们并不用在意这些,只需要知道他是一个相对值,表示距离相机的远近。因此我们修改其Z值就是修改深度值,在处理遮挡时把Z值增加使其更靠近屏幕,就达到了我们的“显示在最前”的效果了。

当然,肯定有人发现问题了。Z轴明明是越小离相机越近,为啥增加Z值反而使得物体更靠前了呢?

答案就是 :UNITY_REVERSED_Z  

Unity 为了计算深度时获得更高的精度,因此会有翻转 Z 轴的情况,所以会 Z 值变大反而距离屏幕越近。但是这个 UNITY_REVERSED_Z  并不是所有平台都一致的,详细看官方文档:

https://docs.unity3d.com/Manual/SL-PlatformDifferences.html

简单来说,在 DirectX 11, DirectX 12, PS4, Xbox One, Metal 平台下会翻转 Z 轴,其他平台则不会。虽然我并不很关心为什么,但是这会导致我再其他平台(如安卓)就不能 加,而是应该减一个值。所以之前的代码还要修改:

		float _ZTestAddValue;

		vertexOutput vert(appdata_full input)
		{
			vertexOutput output;

			float4 clipPos = UnityObjectToClipPos(input.vertex);
			//计算深度时额外增加一个值:			
        	#if UNITY_REVERSED_Z
                clipPos.z+=_ZTestAddValue;
        	    clipPos.z = min(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);//限定在范围内
        	#else
                clipPos.z-=_ZTestAddVal;
        	    clipPos.z = max(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);//限定在范围内
        	#endif
			output.pos = clipPos;

			output.posInObjectCoords = input.texcoord;
			return output;
		}

这样就能根据不同平台,正确地设置 Z 轴的值了。

 

_ZTestAddValue 的取值问题:

最后,再来谈谈关于Z轴附加值(_ZTestAddValue)的取值问题。

现在已经知道 这个 SV_POSITION 的Z轴是相对值,和相机距离物体的远近有直接关系。

当我们附加值 (_ZTestAddValue) 不改变的时候,改变相机和物体的距离,就会使得遮挡关系改变。可以理解为,如果相机够远 ,+0.1 就可以把黄色方块拉到前面来了,但是相机如果太近,就不足够了。

这里用一张网上的图:

可以看到在0附近(靠近相机)的地方取值范围更大,而越远变化越小。

因此 _ZTestAddValue 的值需要根据物体距离相机的远近来设定一个合适的值(距离相机近则大,远则小)。如果需要甚可以用代码动态修改。

 

完整代码

最后贴上完整的Shader代码:

Shader "Custom/TestShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
		_ZTestAddValue("ZTest Add Value",float)=0
        [Enum(UnityEngine.Rendering.CompareFunction)] _ZTest ("ZTest", Float) = 4
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
		ZTest[_ZTest]

        Pass
		{

		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "UnityCG.cginc"

		struct vertexOutput 
		{
			float4 pos : SV_POSITION;
			float4 posInObjectCoords : TEXCOORD0;
		};

		float _ZTestAddValue;

		vertexOutput vert(appdata_full input)
		{
			vertexOutput output;

			float4 clipPos = UnityObjectToClipPos(input.vertex);
			//计算深度时额外增加一个值:			
        	#if UNITY_REVERSED_Z
                clipPos.z+=_ZTestAddValue;
        	    clipPos.z = min(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);//限定在范围内
        	#else
                clipPos.z-=_ZTestAddVal;
        	    clipPos.z = max(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);//限定在范围内
        	#endif
			output.pos = clipPos;

			output.posInObjectCoords = input.texcoord;
			return output;
		}
		
        sampler2D _MainTex;
        fixed4 _Color;

		float4 frag(vertexOutput input) : COLOR
		{
			float4 col = tex2D(_MainTex, input.posInObjectCoords)*_Color;
			return col;
		}

			ENDCG
		} 
    }
    FallBack "Diffuse"
}

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐