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

查看:1146 |回复:1 | 2019-11-13 10:11:56

您需要 登录 才可以下载或查看,没有账号?注册

x
本帖最后由 kanisen 于 2019-11-13 14:04 编辑

985704748cccc8a1178e01ade8d4eb408d68e7dd81f25-wgIgkD.jpg





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

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

Unity Shader训练营:入门篇(上)

Unity Shader训练营:入门篇(下)

Unity 着色器训练营(2): MVP转换和法线贴图

Shader着色器代码辅助工具




图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,基本撰写如下:



  1. Shader "Shader Course/03/Replacement/ShowDepth"
  2. {
  3.        Properties
  4.        {
  5.               _Color("Color", color) = (1, 1, 1, 1)
  6.        }
  7.        SubShader
  8.        {
  9.               Tags { "RenderType"="Opaque" }

  10.               Pass
  11.               {
  12.                      CGPROGRAM
  13.                      #pragma vertex vert
  14.                      #pragma fragment frag
  15.                     
  16.                      #include "UnityCG.cginc"

  17.                      struct appdata
  18.                      {
  19.                             float4 vertex : POSITION;
  20.                      };

  21.                      struct v2f
  22.                      {
  23.                             float4 vertex : SV_POSITION;
  24.                             float depth: DEPTH;
  25.                      };

  26.                      v2f vert (appdata v)
  27.                      {
  28.                             v2f o;
  29.                             o.vertex = UnityObjectToClipPos(v.vertex);
  30.                             o.depth = -UnityObjectToViewPos(v.vertex).z * _ProjectionParams.w;
  31.                             return o;
  32.                      }
  33.                     
  34.                      half4 _Color;
  35.                      fixed4 frag (v2f i) : SV_Target
  36.                      {
  37.                             float invert = 1 - i.depth;
  38.                             return fixed4(invert, invert, invert, 1) * _Color;
  39.                      }
  40.                      ENDCG
  41.               }
  42.        }
  43. }
点击此处复制文本


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

如何将ShowDepth统一替换场景中所有材质的Shader呢?这就需要调用Camera的名为SetReplacementShader的方法去实现。现在在Project视图中创建一个C#脚本,命名为ReplacementShaderCameraEffect,具体内容如下:

  1. Shader "Shader Course/03/IBL/Diffuse"
  2. {
  3.        Properties
  4.        {
  5.               _MainTex("Albedo Texture", 2D) = "white" {}
  6.               [NoScaleOffset]
  7.               _NormalTex("Normal Texture", 2D) = "bump" {}
  8.                 _IBLTexCube("IBL Cubemap", Cube) = "black" {}
  9.        }

  10.        SubShader
  11.        {
  12.               Pass
  13.               {
  14.                      Tags{ "LightMode" = "ForwardBase" }

  15.                      CGPROGRAM
  16.                      #pragma vertex vert
  17.                      #pragma fragment frag
  18.            
  19.                      #include "UnityCG.cginc"

  20.                      struct appdata
  21.                      {
  22.                             float4 vertex : POSITION;
  23.                             float3 normal : NORMAL;
  24.                             float4 tangent : TANGENT;
  25.                             float2 uv : TEXCOORD0;
  26.                      };

  27.                      struct v2f
  28.                      {
  29.                             float4 vertex : SV_POSITION;
  30.                             float2 uv : TEXCOORD0;
  31.                             half3 wNormal : TEXCOORD1;
  32.                             half3 wTangent : TEXCOORD2;
  33.                             half3 wBitangent : TEXCOORD3;
  34.                      };

  35.                      sampler2D _MainTex;
  36.                      half4 _MainTex_ST;
  37.                      sampler2D _NormalTex;
  38.                     samplerCUBE _IBLTexCube;

  39.                      v2f vert(appdata v)
  40.                      {
  41.                             v2f o;

  42.                             o.vertex = UnityObjectToClipPos(v.vertex);
  43.                             o.uv = TRANSFORM_TEX(v.uv, _MainTex);
  44.                             o.wNormal = UnityObjectToWorldNormal(v.normal);
  45.                             o.wTangent = UnityObjectToWorldNormal(v.tangent.xyz);
  46.                             o.wBitangent = cross(o.wNormal, o.wTangent) *
  47.                                    v.tangent.w * unity_WorldTransformParams.w;

  48.                             return o;
  49.                      }
  50.            
  51.                     #define DIFFUSE_MIP_LEVEL 5

  52.             half3 SampleTexCube(samplerCUBE cube, half3 normal, half mip)
  53.             {
  54.                  return texCUBElod(cube, half4(normal, mip));
  55.             }
  56.            
  57.                      fixed4 frag(v2f i) : SV_Target
  58.                      {
  59.                             half3 albedoColor = tex2D(_MainTex, i.uv);
  60.                             half3 normalTex = tex2D(_NormalTex, i.uv) * 2 - 1;   
  61.                
  62.                             half3 N = normalize(i.wTangent * normalTex.r +
  63.                                    i.wBitangent * normalTex.g + i.wNormal * normalTex.b);
  64.                
  65. half3 indirectDiffuse = SampleTexCube(_IBLTexCube, N, DIFFUSE_MIP_LEVEL);
  66. half3 diffuse = albedoColor * indirectDiffuse;
  67.                
  68.                      half4 color = 0;
  69.                      color.rgb = diffuse;
  70.                
  71.                             return color;
  72.                      }
  73.                      ENDCG
  74.               }
  75.        }
  76. }
点击此处复制文本


开头的[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做如下修改:



  1. SubShader
  2.        {
  3.               Tags { "RenderType"="Transparent" }

  4.               ZWrite Off
  5.               Blend SrcAlpha OneMinusSrcAlpha

  6.               Pass
  7.               {
  8.                      CGPROGRAM
  9.                      #pragma vertex vert
  10.                      #pragma fragment frag
  11.                     
  12.                      #include "UnityCG.cginc"

  13.                      struct appdata
  14.                      {
  15.                             float4 vertex : POSITION;
  16.                      };

  17.                      struct v2f
  18.                      {
  19.                             float4 vertex : SV_POSITION;
  20.                      };

  21.                      v2f vert (appdata v)
  22.                      {
  23.                             v2f o;
  24.                             o.vertex = UnityObjectToClipPos(v.vertex);
  25.                             return o;
  26.                      }
  27.                     
  28.                      half4 _Color;

  29.                      fixed4 frag (v2f i) : SV_Target
  30.                      {
  31.                             return _Color;
  32.                      }
  33.                      ENDCG
  34.               }
  35.        }
点击此处复制文本


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



图14




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

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



  1. GetComponent<Camera>().SetReplacementShader(ReplacementShader, "");
点击此处复制文本


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



图15




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



  1. Shader "Shader Course/03/Replacement/OverDraw"
  2. {
  3.        SubShader
  4.        {
  5.               Tags { "Queue"="Transparent" }

  6.               ZTest Always
  7.               ZWrite Off
  8.               Blend One One

  9.               Pass
  10.               {
  11.                      CGPROGRAM
  12.                      #pragma vertex vert
  13.                      #pragma fragment frag
  14.                     
  15.                      #include "UnityCG.cginc"

  16.                      struct appdata
  17.                      {
  18.                             float4 vertex : POSITION;
  19.                      };

  20.                      struct v2f
  21.                      {
  22.                             float4 vertex : SV_POSITION;
  23.                      };

  24.                      v2f vert (appdata v)
  25.                      {
  26.                             v2f o;
  27.                             o.vertex = UnityObjectToClipPos(v.vertex);
  28.                             return o;
  29.                      }
  30.                     
  31.                      half4 _OverDrawColor;

  32.                      fixed4 frag (v2f i) : SV_Target
  33.                      {
  34.                             return _OverDrawColor;
  35.                      }
  36.                      ENDCG
  37.               }
  38.        }
  39. }
点击此处复制文本


  • 这里删除Properties,我们会通过外部代码来直接修改颜色。
  • 将RenderType改为Queue,也就是将Shader生效放在透明的渲染队列上,即渲染的时机。
  • 使用ZTest Always,就是始终进行深度测试的含义,它将不会考虑深度缓冲的情况,结合ZWrite Off深度写入的关闭,这个Shader影响下的将会全是半透明的物体。
  • 改写Blend为Blend One One,就是让源颜色与目标颜色完全通过混合,不考虑透明色的情况,一旦有叠加的情况,颜色就会愈发高亮,趋近于白色。
  • Return颜色的部分,思路与ShowDepth第二个子Shader基本一致。

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



图16




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

替换着色器方法是个非常有趣的功能,可以帮助开发者表现更为丰富的画面效果,希望这部分的介绍可以帮助到各位开阔思路,做出更多赏心悦目的作品。








评分

参与人数 1活跃度 +12 展开 理由
胖纸Jeremy + 12 【喜欢】看到这么好的帖如同回到初恋的年代!

查看全部评分

2019-11-13 10:11:56  
 赞 赞 0

使用道具 登录

1个回答,把该问题分享到群,邀请大神一起回答。
2#
6666666点赞!
回复 收起回复
2019-11-13 13:41:50   回复
 赞 赞 0

使用道具 登录

CG 游戏行业专业问题

手机游戏引擎手游引擎Unity3D技术
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表