查看原文
其他

Unity着色器训练营(3) - 替换着色器方法(上)

2018-02-26 Unity Unity官方平台

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


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


着色器训练营往期内容回顾

 

图 01

 

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

 

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

 

图 02

 

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

 

图 03

 

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

 

图 04

 

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

 

图 05

 

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


图 06

 

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

 

图 07

 

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

 

在Project项目视图中,通过Create → Shader → Unlit Shader新建一个无光照顶点/片元着色器,重命名ShowDepth,基本撰写如下:

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;

                   &nbsp 44 35511 44 15645 0 0 6989 0 0:00:05 0:00:02 0:00:03 6990;        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,具体内容如下: 

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所示:

 

图 08

 

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

 

图 09

 

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


图 10

 

图 11

 

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


图 12

 

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

 

图 13

 

虽然茶壶再现了,但是不是真正透明的。这是为什么呢?因为这是深度写入(ZWrite)对于半透明的影响。对于不透明物体,由于强大的深度缓冲(Depth Buffer)的存在,我们可以不考虑它们的渲染顺序也能得到正确的排序效果。在实时渲染中,深度缓冲是用于解决可见性问题的,它可以决定哪个物体或其某个部分的渲染前后、遮挡与否。它的基本思想是:根据深度缓存中的值来判断该片元距离摄像机的距离,如果打开深度测试,它会将其深度值和深度缓冲中的值进行比较。但是,需要表现透明效果时,单纯以深度值与深度缓冲判断就不行了,因此需要关闭深度写入功能(ZWrite)。现将这个子Shader做如下修改:

       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。

 

图 14

 

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

 

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

GetComponent<Camera>().SetReplacementShader(ReplacementShader, "");

 

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


图 15

 

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

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的数据。

 

图 16

 

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

 

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

 

推荐阅读


点击“阅读原文”访问更多技术内容!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存