如图。角色被其他物体挡住时,能够看到其基本的轮廓,就像医院的X光一样。所以这种透视效果又被叫做X光。
在说X光的实现原理之前,需要掌握两个储备知识,①是渲染顺序,②是深度测试。
渲染队列用来控制渲染顺序。所谓渲染顺序,即先渲染哪个物体,再渲染哪个物体。
我们可以通过修改Shader中的Queue标签或者材质球中的Render Queue来设置渲染队列,值越小越先渲染。需要注意,如果更改了材质球上的Render Queue值,则该物体的渲染队列以材质球上的Renderer Queue值为准。比如我们这里就将X光物体的材质设置为了Geometry+1。
这里的Geometry是Unity中内置的不透明物体的渲染队列,值为2000。Geometry+1,就是2001,也就是说场景中所有渲染队列值≤2000的物体渲染完毕后,才渲染我们的X光物体。Unity中默认的渲染队列值如下。
为了更好的理解这个渲染队列的作用,我们刻意去Frame Debugger看看。Frame Debugger展示了当前一帧画面是如何一步一步被绘制出来的。
然后点击Enable即可查看本帧是如何被渲染出来。
我们先看看我们的测试场景,场景中就3个物体。 其中Ground和Wall01物体使用的是标准着色器,渲染队列为2000。Robot Kyle使用的Shader是我们后面会讲的X Ray Shader。
同时相机需要关闭HDR与MSAA并将Clear Flags设置为Solid Color,平行光关闭阴影。修改这两项的参数主要是减少额外的Draw Call,方便我们看Frame Debugger,理解渲染队列的作用。
我们将Robot Kyle使用的材质的Render Queue设置为2001,即Geometry+1,然后点击Frame Debugger的Enable。
Frame Debugger的右侧展示了该相机是如何一步一步渲染的,左侧的数字即为Draw Call数,右侧为渲染某物体时用到的Shader的状态。
根据Frame Debugger其绘制流程如下:
由于Robot Kyle使用的XRay材质,其渲染队列为2001,而Ground和Wall01的材质的渲染队列为2000,所以先渲染Ground和Wall01物体,再渲染Robot Kyle。
然后,我们将Robot Kyle使用的XRay材质的渲染队列改为1999,即Geometry-1,再来看看Frame Debugger。
可以看到,此时先渲染的Robot Kyle,再渲染的Ground和Wall01。
这就是渲染队列的作用,用来控制物体的渲染顺序。
有同学可能会问,那相同渲染队列的物体的渲染顺序是怎么判断的呢,比如我们这个例子中的Ground和Wall01的渲染队列均为2000,为什么会先渲染Wall0再渲染Ground呢?
对于不透明物体,一般是按照距离相机从前往后进行渲染。而对于半透明物体,是按照距离相机从后往前的顺序进行渲染。
至于这个距离相机的距离是怎么计算的,是取物体的世界坐标与相机的世界坐标的距离呢,还是取模型中的某一个顶点的世界坐标与相机的世界坐标的距离呢,说实话我也没弄清楚,Untiy官方文档也没提到这个细节,欢迎有知道的同学留言。
至于不透明物体,为什么是从前往后,而半透明物体是从后往前,要把这个问题说清楚就不是一时半会儿的事情了。简单提下,不透明物体从前往后渲染由于GPU的EarlyZ技术的存在可以减少Over Draw,而半透明物体从后往前渲染是因为半透明物会关闭深度写入,这样做才能保证颜色在混合时是正确的。
①GPU有一个深度缓冲区(Z Buffer),里面存储的是相机通过该像素点能看到的最近的物体的表面的深度值,所谓的深度值即相机与该物体对应点的距离值,深度值范围一般为0~1,位数为24位。深度值越小,表示距离相机越近。
②我们可以在Shader中选择是否开启深度测试。
③如果Shader中开启了深度测试,那么渲染某个物体时,就需要比较深度缓冲区的值与该物体对应的深度值,如果满足某个规则(这个规则也是我们在Shader中指定的,如小于),我们就认为深度测试通过。
④只有深度测试通过了,才能选择是否进行深度写入(即将该物体的深度值写入到深度缓冲区中)。没有通过深度测试是没有权利进行深度写入的。
举个例子,假设物体A的顶点A1与物体B的顶点B1对应同一个像素,A1的深度值为0.2,B1的深度值为0.5。先渲染你物体A,再渲染物体B。物体A物体B的Shader中都开启了深度测试和深度写入,深度比较规则为小于。那么先渲染A时,由于此时深度缓冲区还没有值,所以A1顶点对应的像素深度测试通过,然后将0.2写入深度缓冲区。物体A渲染完成后再渲染物体B,此时发现B1顶点对应像素的深度缓冲区值为0.2,而0.5>0.2,测试不通过,B1顶点对应的像素颜色将不能输出到屏幕上。
一般使用深度测试的方式来实现。有2个步骤,①先将所有不透明物体全部渲染完成之后再渲染我们的X光物体,②我们的X光物体要渲染两次,第一次渲染先判断物体是否被其他物体挡住,若挡住就渲染物体的边缘,若没挡住则不渲染,而且要关闭深度写入,第二次渲染物体就按正常的渲染。
知道了原理,我们再看看怎么写代码。
步骤①先将所有不透明物体全部渲染完成之后再渲染我们的X光物体:
这个很简单,我们上面已经知道可以使用Unity提供的渲染队列来控制场景中各个物体的渲染顺序。我们这里将X光Shader的渲染队列设置值为2001,即Geometry+1。
步骤②我们的X光物体要渲染两次,第一次渲染先判断物体是否被其他物体挡住,若挡住就渲染物体的轮廓,若没挡住则不渲染,而且要关闭深度写入,第二次渲染物体就按正常的渲染流程,即有物体挡住挡住时则不渲染,没有物体挡住时则渲染
要渲染两次,那就是Shader中有两个Pass(在前向渲染中,一个Pass就代表一次完成的渲染流程,或者说就是经过一次完整的渲染流水线)。第一个Pass要判断是否被其他物体挡住,这简单,在步骤①中我们已经渲染了所有不透明物体,如果深度缓冲区的深度值(存储的是距离相机最近的物体的深度值,深度值越小表示距离相机越近)小于x光物体的深度值,则说明X光物体被其他物体挡住。直接使用ZTest Greater即可实现。
ZTest Greater
如果X光物体被其他物体挡住了则要渲染物体的轮廓,这里的轮廓怎么找出来?一般使用边缘光的方式,即当视线方向与模型表面的法线的点积为0时就认为是物体的边缘,是物体的边缘则输出X光颜色。
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldNormalDir = normalize(worldNormal);
fixed3 worldViewDir = normalize(WorldSpaceViewDir(v.vertex));
float rim = 1.0 - dot(worldNormalDir, worldViewDir);
o.color = _RayColor * pow(rim, _RayPower);
Shader "Custom/XRay"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_MainColor ("Main Color", color) = (1.0, 0, 0, 1.0)
_RayColor ("Ray Color", color) = (1.0, 0, 0, 1.0)
_RayPower ("Ray Power", Range(0, 3)) = 1.0
}
SubShader
{
LOD 100
CGINCLUDE
#include "UnityCG.cginc"
struct appdataXRay
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2fXRay
{
float4 vertex : SV_POSITION;
fixed4 color : TEXCOORD0;
};
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _MainColor;
fixed4 _RayColor;
float _RayPower;
v2fXRay vertXRay (appdataXRay v)
{
v2fXRay o;
o.vertex = UnityObjectToClipPos(v.vertex);
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldNormalDir = normalize(worldNormal);
fixed3 worldViewDir = normalize(WorldSpaceViewDir(v.vertex));
float rim = 1.0 - dot(worldNormalDir, worldViewDir);
o.color = _RayColor * pow(rim, _RayPower);
return o;
}
fixed4 fragXRay (v2fXRay i) : SV_Target
{
return i.color;
}
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col * _MainColor;
}
ENDCG
// 两个Pass顺序不能交换,一定要先渲染X光再正常渲染物体
Pass
{
Tags { "RenderType" = "Opaque" "Queue"="Geometry+1" }
ZTest Greater
ZWrite Off
Blend SrcColor OneMinusSrcColor
CGPROGRAM
#pragma vertex vertXRay
#pragma fragment fragXRay
ENDCG
}
Pass
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
项目源码:https://pan.baidu.com/s/1U3T5udbDLYk0maWXu1jW-g
提取码:du3x
博主个人博客本文链接。