【《Unity 2018 Shaders and Effects Cookbook》提炼总结】(十 一)Fragment Shaders and Grab Passes

到目前为止,我们依赖于Surface Shaders。他们旨在简化Shader编码的工作方式,为艺术加提供有意义的工具。如果我们想进一步推动我们对Shader的了解,我们需要冒险进入Vertex和Fragment Shaders领域。

介绍

与“Surface Shader”相比,Vertex和Fragment几乎没有关于确定光在表面上反射的物理属性的信息。他们缺乏表现力,他们用力量来弥补:Vertex和片元着色器不受物理约束的限制,非常适合非真实感效果。本文将重点介绍一种称为grab pass的技术,它允许这些Shader模拟变形。

理解 Vertex 和 Fragment Shaders

了解Vertex和Fragment Shaders如何工作的最佳方法是创建一个。接下来我们将展示如何编写其中一个Shader,这将简单地将纹理应用于模型并将其乘以给定的颜色,如下面的屏幕截图所示

请注意它的工作方式与Photoshop中的“乘法”滤镜的工作方式类似。那是因为我们将在那里完成相同的计算。

这里提供的Shader非常简单,它将用作所有其他Vertex和Fragment着色器的起始基础。

开始准备

a.创建一个新的Shader命名为Multiply。

b.创建一个新的Material(MultiplyMat)并把shader分配给它。

c.我们就用上一篇博文(十)系列的士兵,并将新材质附加到预制体的头部,头部可以在士兵物体的士兵子找到。

d.从那里,在Inspector选项卡中,向下滚动到Skinned Mesh Render组件,然后在Materials下,将Element0设置为新材质。最后,在Albedo(RGB)属性中,拖放Unity_soldier_Head_DIF_01纹理。以下屏幕截图应该有助于演示我们正在寻找的内容。(资源可在文末下载)

e.删除shader中所有的属性,用下面的代替

    Properties
    {
        _Color("Color", Color) = (1,0,0,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
    }

h.删除SubShader块中的代码并用下面的代替

SubShader
    {
        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            half4 _Color;
            sampler2D _MainTex;
            struct vertInput
            {
                float4 pos : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct vertOutput
            {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
            };

            vertOutput vert(vertInput input)
            {
                vertOutput o;
                o.pos = UnityObjectToClipPos(input.pos);
                o.texcoord = input.texcoord;
                return o;
            }

            half4 frag(vertOutput output) : COLOR
            {
                half4 mainColour = tex2D(_MainTex, output.texcoord);
                return mainColour * _Color;
            }
            ENDCG
        }
    }

i.保存Shader脚本并回到Unity编辑器,结束后,修改MultiplyMat材质的Color属性,看看我们得到了我们想要的结果。

它是如何运行的

在顾名思义,Vertex和Fragment Shaders分两步完成,该模型首先需要通过vertex函数,然后将结果输入到Fragment function。这两个函数都是使用#pragma指令分配的

#pragma vertex vert

#pragma fragment frag

这种情况下,他们简单称作vert 和frag。

从概念上讲,fragment和像素密切相关,fragment术语通常用于指绘制像素所需的数据集合,这也是Vertex和Fragment Shaders通常被称为Pixel Shaders的原因。

vertex函数

顶点函数将输入数据置于Shader中定义为vertInput的结构中:

struct vertInput  {
                float4 pos : POSITION;
                float2 texcoord : TEXCOORD0;
            };

它的名字完全是武断的,但它的内容不是。结构的每个字段必须用绑定语义进行修饰。这是Cg的一个特性,它允许我们标记变量,以便用某些数据初始化它们,例如法线向量和顶点位置。绑定语义POSITION表示当vertInput输入到顶点函数时,pos将包含当前顶点的位置。这类似于Surface Shader中appdata_full结构的顶点字段。主要区别在于pos以模型坐标(相对于3D对象)表示,我们需要手动转换为视图坐标(相对于屏幕上的位置)。

提示:

Surface Shader中的顶点函数只用于改变模型的几何形状。在Vertex和FragmentShader中,顶点函数是将模型的坐标投影到屏幕上所必需的。

这种转换背后的数学超出了本章的范围。但是,这种转换可以通过使用UnityObjectToClipPos函数来视线,该函数将在齐次坐标下从对象空间取一点到相机的裁剪空间。这是通过乘以模型-视图-投影矩阵来实现的,并且找到一个顶点在屏幕上的位置是至关重要的。

vertOutput o;
        o.pos = UnityObjectToClipPos(input.pos);

初始化的另一部分信息是textcoord,它使用TEXCOORD0绑定语义来获取第一个纹理的UV数据。不需要进一步处理,这个值可以直接传递给fragment函数(frag);

o.texcoord = input.texcoord;

Unity将为我们初始化vertInput,我们负责vertOutput的初始化。尽管如此,它的字段仍然需要绑定语义进行修饰。

struct vertOutput
            {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
            };
一旦顶点函数初始化了vertOutput,结构就会传递给fragment函数(frag)。这样可以对模型的主要纹理进行采样,并将其与所提供的颜色相乘。

如您所见,Vertex和Fragment Shader不了解材质的物理属性。这意味着材料不会受到光源的影响,并且与Surface Shader相比,它没有关于光如何反射以创建凸起表面的数据。它更接近图形GPU的架构。

提示:

Vertex和Fragment Shaders最令人困惑的一个方面是绑定语义,我们可以使用许多其他内容,其含义取决于上下文。

输入语义:

下表中的绑定语义可以在vertInput中使用,vertInputUnity为vertex function提供的结构。用这种语义装饰的字段将自动初始化:

输出语义:

下表中的绑定语义可以在vertInput中使用,vertInputUnity为vertex function提供的结构。用这种语义装饰的字段将自动初始化:

绑定时,在vertOutput中使用语义;它们不会自动保证字段将被初始化,恰恰相反,这是我们的责任。编译器将确保字段是用正确的数据初始化的。

如果由于某种原因,您需要一个包含不同类型数据的字段,我们可以使用许多可以用的TEXCOORD数据之一来修饰它,编译器将不允许不修饰字段。

使用抓取通道来绘制对象

如在前面的基于物理的渲染PBR配方的透明度中,我们已经看到如何使用材料透明,即使透明材质可以在场景上绘制,也不能改变在其下方绘制的内容。着意味着那些透明Shader不会产生扭曲,例如通常在玻璃或水中看到的扭曲。为了模拟它们,我们需要引入另一种称为抓取通道的技术。这允许我们访问到目前位置在屏幕上绘制的内容,以便Shader可以无限制地使用它(或更改它),为了学习如何使用抓取过程,我们将创建一种材料来抓取其后面渲染地内容并在屏幕上再次绘制它。矛盾地说,它是一个shader,使用几个操作来显示没有任何变化。

开始准备

a.创建一个shader命名为GrabShader之后我们将会初始化它。

b.创建一个material命名为GrabMat托管Shader。

c.将材料附加到几何平面,例如四边形,将它放在其他物体的前面,这样你就无法看透它.Shader完成后,四边形将显示为透明。

d.移走属性块的所有部分,这个Shader暂时不需要它们。

e.在SubShader部分,删除所有内容,并添加以下内容以确保将对象视为透明。

Tags { "Queue"="Transparent" }

h.然后,在下面添加一个 grab pass

GrabPass()

i.在GrabPass之后,我们需要添加一个这样的额外的Pass:

          Pass
    {
        CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
        sampler2D _GrabTexture;
        struct vertInput
        {
            float4 vertex : POSITION;
        };
        struct vertOutput
        {
            float4 vertex:POSITION;
            float4 uvgrab:TEXCOORD1;
        };
        vertOutput vert(vertInput v)
        {
            vertOutput o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uvgrab = ComputeGrabScreenPos(o.vertex);
            return o;
        }
        half4 frag(vertOutput i):COLOR
        {
            fixed4 col = tex2Dproj(_GrabTexture,UNITY_PROJ_COORD(i.uvgrab));
            return col + half4(0.5, 0, 0, 0);
        }
        ENDCG
    }

j.保存脚本回到Unity编辑器,回来后,我们应该注意到我们的材质现在按照我的意图工作。

它是如何工作的

保这个Shader不仅引入了抓取通道,还引入了Vertex和Fragment Shaders,出于这个原因,我们必须详细分析Shader。到目前为止,所有代码始终直接放在SubShader部分。这是因为我们的Shader只需要一个Pass。这次需要两个Pass。第一个是GrabPass{},它由GrabPass{}简单定义。其余代码放在第二个pass中,该pass包含在Pass块中。

第二个Pass与结构上第一个Shader没有什么区别,我们使用顶点函数vert来获取顶点的位置,然后我们在Fragment函数frag中给它一个颜色。不同之处在于vert计算另一个重要细节:GrabPass{}的UV数据。GrabPass{}自动创建一个纹理,可以参考如下:

sampler2D _GrabTexture;

为了对此纹理进行采样,我们需要其UV数据。ComputeGrabScreenPos函数返回我们以后可以使用的数据,以正确地采样抓取纹理。这是使用以下行在Fragment Shader中完成的:

fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab));

这是抓取纹理并将其应用于屏幕的正确位置的标准方式。如果一切都已正确完成,那么该Shader将简单地克隆几何体后面地克隆几何体后面渲染地内容。我们将在下面地内容学习如何用于创建水和玻璃等材质。

更多

每次使用带有GrabPass{}的材质时,Unity都必须将屏幕渲染为纹理。此操作非常昂贵,并且限制了我们可以在游戏中使用的GrabPass实例的数量。Cg提供略有不同的版本:

GrabPass {"TextureName"}

这一行不仅允许我们为纹理指定名称,还可以与具有名为TextureName的GrabPass的所有材质共享纹理。这意味着如果你有十种材质,Unity只会做一个GrabPass并与所有这些材质共享纹理。该技术的主要问题是它不允许可堆叠的效果。如果我们使用这种技术制作玻璃杯,我们无法一个接一个地叠上玻璃。

实现玻璃Shader

玻璃是一种非常复杂的材质,在基于物理的渲染的PBR配方的添加透明度中,我们试着做过它。我们已经知道如何使我们的眼镜半透明,以完美地展示它背后地物体,并适用于许多应用。然而,大多数眼镜并不完美。例如,如果我们透过污渍玻璃窗观察,我们可能会在看到它们时发现扭曲或变形。下面我们就学习如何实现这种效果,这种效果背后地想法是使用带有GrabPass的Vertex和FragmentShader,然后对抓取纹理进行采样,稍微改变其UV数据以产生失真。

开始准备

a.创建一个新的Vertex和Fragment Shader,可以直接利用上一个案例的Cube 复制几个。并复制上面的Shader改名为WindowShader。

b.创建将使用WindowShader的材质命名为WindowMat。

c.将材质指定为模拟玻璃的四边形或其他平面几何体。

d.在它后面放置一些物体,我们可以看到一些失真效果。

e.创建包含以下内容的属性块

Properties {
        _MainTex("Base (RGB) Tran(A)",2D) = "white"{}
        _Colour("Colour",Color) = (1,1,1,1)
        _BumpMap("Noise text",2D) = "bump" {}
        _Magnitude("Magnitude",Range(0,1)) = 0.05
    }

h.在第二个pass加入下面相关的变量:

sampler2D _MainTex; fixed4 _Colour;
sampler2D _BumpMap; float _Magnitude

i.在纹理信息种加入输入和输出结构:

float2 texcoord:TEXCOORD0;

j.将UV数据从输入传输到输出结构

    vertOutput vert(vertInput v)
                {
                    vertOutput o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uvgrab = ComputeGrabScreenPos(o.vertex);
                    o.texcoord = v.texcoord;
                    return o;
                }

k.使用下面的Fragment函数

    half4 frag(vertOutput i) :COLOR
                {
                    half4 mainColour = tex2D(_MainTex,i.texcoord);
                    half4 bump = tex2D(_BumpMap, i.texcoord);
                    half2 distortion = UnpackNormal(bump).rg;
                    i.uvgrab.xy += distortion * _Magnitude;

                    fixed4 col = tex2Dproj(_GrabTexture,UNITY_PROJ_COORD(i.uvgrab));
                    return col * mainColour * _Colour;
                }

l.材质是透明的,因此我们改变SubShader块种的标签为

Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" =  "Opaque" }

m.现在剩下的就是设置玻璃的纹理,并使用法线贴图来替换抓取纹理。

它是如何运行的

这个shader使用的核心是抓取传递以获取已在屏幕上呈现的内容。发生失真的部分在Fragment函数中。这里,解压缩法线贴图并用于偏移抓取纹理的UV数据:

half4 bump = tex2D(_BumpMap,i.texcoord);

half2 distortion = UnpackNormal(bump).rg;

i.uvgrab.xy += distortion * _Magnitude;

_Magnitude滑块用于确定效果的强度。

更多

这种效果非常通用,它抓取屏幕并根据法线贴图创建失真。没有理由不应该用它来模拟更有趣的事情。许多游戏使用爆炸或其他科幻设备的变形。这种材质可以应用于球体,并且使用不同的法线贴图,它可以完美地模拟爆炸地热浪。

为2D游戏实现水Shader

上一个Shader介绍地Glass Shader是静态的,它的失真永远不变。只需几处更改即可将其转换为动画素材,因此非常适合以水为特色的2D游戏。这使用了类似于前面Vertex Function中所示的技术,该技术名为“在Surface Shader中设置动画顶点”。

开始准备

该Shader基于使用抓取通道绘制背后对象,Shader中描述的Vertex和Fragment Shaders,因为它将严重依赖于GrabPass。

a.创建一个新的Vertex和Fragment Shader。我们可以直接copy前面一个名叫GrabShader的Shader,然后改名为WaterShader.

b.创建一个材质并把Shader赋予它。

c.将材质指定为代表2D水的平面几何体。为了使这个效果起作用,我们应该在它后面渲染一些东西,这样我们就可以看到像水一样的位移。

 d.这个shader需要噪声纹理,用于获取伪随机值。选择无缝噪声纹理非常重要,例如由可平铺的2D Perlin噪声生成的纹理。这可确保在将材质应用于大型物体时,我不会看到任何不连续性。为了使此效果起作用,必须在重复模式下导入纹理。如果你像要一个平滑和连续的水看,你也应该从Inspector将它设置为Bilinear.这些设置可确保从Shader中正确采样纹理。

e.要创建此动画效果,我们可以从重新设置Shader开始。添加下面的属性

        Properties 
    {
        _NoiseTex("Noise text", 2D) = "white" {}
        _Colour ("Colour", Color) = (1,1,1,1)
        _Period ("Period", Range(0,50)) = 1
        _Magnitude ("Magnitude", Range(0,0.5)) = 0.05
        _Scale ("Scale", Range(0,10)) = 1
    }

h.将它们各自的变量添加到Shader的第二个Pass:

sampler2D _GrabTexture;
            sampler2D _NoiseTex;
            fixed4 _Colour;
            float _Period;
            float _Magnitude;
            float _Scale;

i.为vertex 函数定义以下输入和输出结构

    struct vertInput 
            {
                float4 vertex : POSITION;
                fixed4 color : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct vertOutput 
            {
                float4 vertex : POSITION;
                fixed4 color : COLOR;
                float2 texcoord : TEXCOORD0;

                float4 worldPos : TEXCOORD1;
                float4 uvgrab : TEXCOORD2;
            };

j.该Shader需要知道每个片段的空间的确切位置。为此,请将vertex函数更新为以下内容:

    vertOutput vert(vertInput v) 
            {
                vertOutput o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.color = v.color;
                o.texcoord = v.texcoord;

                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.uvgrab = ComputeGrabScreenPos(o.vertex);
                
                return o;
            }

k.使用以下Fragment 函数:

  

    fixed4 frag (vertOutput i) : COLOR 
            {
                float sinT = sin(_Time.w / _Period);

                float distX = tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5;
                float distY = tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5;

                float2 distortion = float2(distX, distY);

                i.uvgrab.xy += distortion * _Magnitude;

                fixed4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));

                return col * _Colour;
            }

l.保存脚本并返回Unity编辑器。然后,选择我们的WatMat并应用噪声纹理。然后,调整水Material中的属性,并注意它如何修改它背后的东西。

它是如何运行的

这个Shader与“实现玻璃Shader”中介绍的Shader非常相似。主要区别在于这是一种动画材质;位移不是从法线贴图生成的,而是考虑当前时间以创建常量动画。替换抓取纹理的UV数据的代码看起来相当复杂;让我们试着了解它是如何生成的。其背后的想法是使用正弦函数使水振荡。这种影响需要随着时间的推移而发展;为实现此效果,Shader生成的失真取决于使用内置变量_Time检索的当前时间。_Period变量确定正弦波的周期,这意味着波的出现速度有多块:

float2 distortion = float2( sin(_Time.w/_Period),  sin(_Time.w/_Period) ) – 0.5;

这段代码的问题在于你的X和Y轴上有相同的位移;结果,整个抓取纹理将以圆周运动旋转,看起来不像水。我们显然需要为此添加一些随机性。向Shader添加随机行为的最常用方法是包含噪声纹理。现在的问题是找到一种在看似随机的位置采样纹理的方法。避免看到明显的正弦曲线模式的最佳方法是使用正弦波作为_NosieTex纹理的UV数据中偏移量。

float sinT = sin(_Time.w / _Period);

float2 distortion = float2(    tex2D(_NoiseTex, i.texcoord / _Scale + float2(sinT, 0) ).r - 0.5,    tex2D(_NoiseTex, i.texcoord / _Scale + float2(0, sinT) ).r - 0.5 );

_Scale变量确定波的大小。这个解决方案更接近最终版本但是有一个严重的问题 - 如果水四边形移动,UV数据跟随它,我们可以看到水波跟随材料而不是固定在背景上。要解决这个问题,我们需要使用当前片段的世界位置作为UV数据的初始位置:

float sinT = sin(_Time.w / _Period); float2 distortion = float2(    tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5,    tex2D(_NoiseTex, i.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5 );

i.uvgrab.xy += distortion * _Magnitude;

结果是令人愉快的无缝失真,不会在任何明确的方向上移动。我们还可以通过将失真分解为更小的步骤来提高代码的可读性:

float sinT = sin(_Time.w / _Period);
float distX = tex2D(_NoiseTex, i.worldPos.xy / _Scale +  float2(sinT, 0) ).r - 0.5; float distY = tex2D(_NoiseTex, i.worldPos.xy / _Scale +  float2(0, sinT) ).r - 0.5;
float2 distortion = float2(distX, distY);

i.uvgrab.xy += distortion * _Magnitude;

这是我们应该在最终结果中看到的。

以上均基于Unity2018.1.0f源码可见我的GitHub:

 https://github.com/xiaoshuivv/ShadersUnity2018.1.0f.git

猜你喜欢

转载自blog.csdn.net/qq_39218906/article/details/90482751
今日推荐