在1月31日我们进行了Unity着色器训练营第三期的直播,今天Unity技术经理鲍健运将带领大家回顾这次直播的内容。由于篇幅限值,本文将分为上下二篇,本篇主要介绍替换着色器(Replacement Shader)的方法。

Unity着色器训练营的知识点与内容是循序渐进的,在概念上有一定的关联,所以后面一旦提到过往的功能点的时候,希望也能帮助大家能够回想起相应的知识点。

着色器训练营往期内容回顾
Unity 着色器训练营(1):着色器入门
Unity 着色器训练营(2): MVP转换和法线贴图
Shader着色器代码辅助工具

024516gyzxwzw0llldzcec.png
图 01

图01便是替换着色器呈现的Demo场景完成后的效果,我们可以看到Scene视图中所看到的,与Game视图看到的完全不一样,有一种潜行类游戏的透视效果。在实现如此效果之前,我们先做另外一个有趣的Shader作为引子,写一个显示深度的Shader。

那么如何获取深度呢?如何计算它呢?其实主要还是在摄像机上做文章!

024517iwq4peeqnnkpvhrq.png
图 02

看到图02这个场景,可能各位会比较眼熟,因为这个正是第二期直播中所讲到的MVP转换中的投影空间转换。这里略有不同的是,这里所展示的是视图空间(View Space)的坐标系。其实也就是的对应到屏幕上显示,应该的绿色向上为Y轴,红色向右为X轴,而指向摄像机的蓝色是Z轴。箭头的方向,实际就是数值递增的方向,从这个图中我们可以发现,其实指向摄像机前方的方向是Z越来越小。而这个Z轴的值,正好与我们要获取的深度值强相关。

024518jr0zfcr0rrgraa3x.png
图 03

在UnityCG.cginc中有个顶点转换的方法,叫做UnityObjectToViewPos,它的作用是将将对象空间中的点转换为视图空间的点。我这边就以这把尺子来显示转换之后,Z的数值,是越来越小的,但是这结果不是我们所期望的。

024519ohdvyd9dh4vhloh8.png
图 04

为了获得正向的深度值,我们就一定要对于这个方法的结果取负数值,这样就有深度的从小到大的表达了。但是这里我们还需要进行一定的补充,因为最后我们希望的结果,深度最好能以颜色的方式展现出来,这样值域就需要在0~1之间了。

024520jaguqu55u5zvteeu.png
图 05

将深度值域变为0到1之间,换而言之就需要将近剪裁平面与远剪裁平面之间的距离值换算到0~1。有一个很重要的参数可以帮助我们(_ProjectionParams),这个参数也是来自于UnityCG.cginc文件。它有四个内容值,分别对应的是x是1.0(如果当前使用翻转投影矩阵进行渲染则为-1.0),y是相机的近剪裁平面,z是相机的远剪裁平面,w是1 / FarPlane,即远剪裁平面的倒数值。

024521sb71uqx7x5vq5xmi.png
图 06

因为之前换算出来的距离值乘以远剪裁平面的值,其结果就是从近到远0到1的比例值,也就是我们想要的结果。从图中的展示我们大致可以看到通过-UnityObjectToViewPos * _ProjectionParams.w的计算,这里假设远剪裁平面为10米,可以得到深度值转换到0到1值域的结果。现在准备正式撰写显示深度效果的Shader脚本了。

024522iinj3znd7nej0trd.png
图 07

打开我们的Demo起始场景:它是一个简单的茶室,有两面围墙,简单的双色地板,中间有一张桌子和三张椅子,桌子上还有一个茶壶。

在Project项目视图中,通过Create → Shader → Unlit Shader新建一个无光照顶点/片元着色器,重命名ShowDepth,基本撰写如下:
[C#] 纯文本查看 复制代码Shader Shader Course/03/Replacement/ShowDepth { Properties { _Color(Color, color) = (1, 1, 1, 1) } SubShader { Tags { RenderType=Opaque } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; float depth: DEPTH; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.depth = -UnityObjectToViewPos(v.vertex).z * _ProjectionParams.w; return o; } half4 _Color; fixed4 frag (v2f i) : SV_Target { float invert = 1 - i.depth; return fixed4(invert, invert, invert, 1) * _Color; } ENDCG } } }

值得注意的是,v2f结构体中添加了depth深度值参数,并在vert顶点函数中通过o.depth = -UnityObjectToViewPos(v.vertex).z * _ProjectionParams.w;获取深度。在片元函数中,以invert方式获得灰度值,我们希望显示能够明亮些,因为全0是黑色的,所以用1减去i.depth来获取。最后以乘以_Color的方式混合颜色,从而得到渐变的颜色值。

如何将ShowDepth统一替换场景中所有材质的Shader呢?这就需要调用Camera的名为SetReplacementShader的方法去实现。现在在Project视图中创建一个C#脚本,命名为ReplacementShaderCameraEffect,具体内容如下:
[C#] 纯文本查看 复制代码using System.Collections; using System.Collections.Generic; using UnityEngine; [ExecuteInEditMode] public class ReplacementShaderCameraEffect : MonoBehaviour { public Shader ReplacementShader; void OnEnable() { if (ReplacementShader != null) { GetComponent<Camera>().SetReplacementShader(ReplacementShader, RenderType); } } void OnDisable() { GetComponent<Camera>().ResetReplacementShader(); } }

开头的[ExecuteInEditMode],其作用是编辑模式下执行的脚本。在OnEnable()中,即在该脚本激活状态下执行SetReplacementShader,进行Shader的替换,替换用的Shader就是ReplacementShader,而替换的依据就是第二个参数。通过这个字符串我们可以定位替换子Shader,比如这里参照RenderType,主Shader的会询问替换Shader的RenderType是啥?替换Shader的RenderType为Opaque,而主Shader下有许多子Shader,其中RenderType为Opaque的就会被替换Shader更换掉。在OnDisable()中,当该脚本进入关闭状态时将已经替换好的Shader在换回来。

回到Demo场景,我们为Main Camera添加组件Replacement Shader Camera Effect,设置Replacement Shader为ShowDepth。如下图 08所示:

024523do1flwz8r5zofeww.png
图 08

在重新激活该脚本,并调节Camera的Clipping Plane → Far的值从1000变为75后,我们就可以获得如下图09 较为理想的画面效果了:

024523u9oz909e5bv0e8oo.png
图 09

场景中现有的所有材质的Shader都是不透明的,如果我们将其中一个使用透明的Shader会是如何呢?这里我们可以找“茶壶”对象入手尝试一下。

024524qkg34hg6dogqjo6r.png
图 10

024524mygy00l7ygelg77g.png
图 11

原本其所使用的材质Green,使用的是Standard Shader,指定的渲染类型是Opaque(不透明),这里更换为Glass,同样是Standard Shader,但是渲染类型是Transparent(透明)。当再次重新激活Replacement Shader Camera Effect脚本后,有趣的事情发生了:茶壶在Game视图不见了。

024524hrzkke8eqxbyel8w.png
图 12

这是为何?主要原因就在与我们的Replacement Shader,替换着色器只有一个Subshader,其RenderType是Opaque。这里并没有额外的子Shader来负责替换RenderType为Transparent的,只要复制RenderType是Opaque一份,重新修改RenderType为Transparent就能显示出来了。

024525iq9b2bbyhbwhoq9r.png
图 13

虽然茶壶再现了,但是不是真正透明的。这是为什么呢?因为这是深度写入(ZWrite)对于半透明的影响。对于不透明物体,由于强大的深度缓冲(Depth Buffer)的存在,我们可以不考虑它们的渲染顺序也能得到正确的排序效果。在实时渲染中,深度缓冲是用于解决可见性问题的,它可以决定哪个物体或其某个部分的渲染前后、遮挡与否。它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,如果打开深度测试,它会将其深度值和深度缓冲中的值进行比较。但是,需要表现透明效果时,单纯以深度值与深度缓冲判断就不行了,因此需要关闭深度写入功能(ZWrite)。现将这个子Shader做如下修改:
[C#] 纯文本查看 复制代码 SubShader { Tags { RenderType=Transparent } ZWrite Off Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } half4 _Color; fixed4 frag (v2f i) : SV_Target { return _Color; } ENDCG } }

除了添加ZWrite Off关闭深度写入,从而便于进行透明度混合之外,还写下Blend SrcAlpha OneMinusSrcAlpha,它主要是将Source的透明度与1减去Target的透明度进行的混合,比较传统的混合方式将原图与目标背景进行一个混合,产生半透明的渐变效果。因为不需要考虑深度,原有depth相关的删除,随后返回输出的就是给定Color。

024526w5rh8zrjomtmxhrm.png
图 14

重新激活脚本,我们便可以得到有透明茶壶,基本上我们也实现了显示深度的效果。

回到Replacement Shader Camera Effect脚本,SetReplacementShader第二个选项可以指定各种类型进行操作,但是如果你觉得有许多都要替换,不单单是RenderType或者其他的,这的第二个参数可以直接使用双引号作为缺省,Unity会自动找到匹配的替换Shader和主Shader的内容进行替换。因此那句代码可以修改为:
[C#] 纯文本查看 复制代码GetComponent<Camera>().SetReplacementShader(ReplacementShader, );

其实一开始Demo所展示的效果,是接近于重复渲染(Overdraw)的效果。你可以点击Scene视图左上角的渲染选项(默认是Shaded)选择Overdraw,就可以呈现出这样的效果:

024527ogg48oho6nw46sgs.png
图 15

大家应该会发现越是高亮的部分,越是被重复渲染过,也就是被反复混合描绘的部分。现在可以新建一个Unlit Shader,重命名Overdraw,代码如下:

[C#] 纯文本查看 复制代码Shader Shader Course/03/Replacement/OverDraw { SubShader { Tags { Queue=Transparent } ZTest Always ZWrite Off Blend One One Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } half4 _OverDrawColor; fixed4 frag (v2f i) : SV_Target { return _OverDrawColor; } ENDCG } } }

1、这里删除Properties,我们会通过外部代码来直接修改颜色。

2、将RenderType改为Queue,也就是将Shader生效放在透明的渲染队列上,即渲染的时机。

3、使用ZTest Always,就是始终进行深度测试的含义,它将不会考虑深度缓冲的情况,结合ZWrite Off深度写入的关闭,这个Shader影响下的将会全是半透明的物体。

4、改写Blend为Blend One One,就是让源颜色与目标颜色完全通过混合,不考虑透明色的情况,一旦有叠加的情况,颜色就会愈发高亮,趋近于白色。

5、Return颜色的部分,思路与ShowDepth第二个子Shader基本一致。

Replacement Shader Camera Effect脚本也会做一定的调整:添加OverDrawColor变量和OnValidate方法。作用是修改OverDraw的颜色,这个调用只在加载脚本或检查器中的值发生更改时调用此函数(仅在编辑器中调用)。还可使用此功能来验证你的MonoBehaviours的数据。

024527xa5j7dzvavii7p5x.png
图 16

我们回到编辑器界面,先把Scene视图左上角的设置调整回Shaded。将ReplacementShaderCameraEffect的Shader替换为OverDraw,将Over Draw Color设置为Black,然后重新激活这个脚本。这个时候可以发现,二个视图中所展示的效果已经与一开始所呈现给大家的基本一致了。

替换着色器方法是个非常有趣的功能,可以帮助开发者表现更为丰富的画面效果,希望这部分的介绍可以帮助到各位开阔思路,做出更多赏心悦目的作品。更多Unity技术直播内容回顾请关注Unity Connect平台!
着色器, 直播锐亚教育

锐亚教育 锐亚科技 unity unity教程