【《Unity 2018 Shaders and Effects Cookbook》提炼总结】(十)Vertex Functions上(水波,拉伸贴图,雪的效果)

一.Vertex Function函数

1.介绍

之前我们解释了说3D模型不仅是三角形的集合,每个顶点都包括正确渲染模型本身所必须的数据。本章讲探讨如何访问此信息以便在shader中使用它。我们还将详细探索如何使用Cg代码简化对象的几何形状。

2.在Surface Shader中访问顶点颜色

了解如何使用Surface Shader中的顶点函数访问模型顶点的信息。这将是我们掌握知识,开始利用模型顶点中包含的元素,以创造真正有用和视觉上吸引人的效果。

vertex函数中的顶点可以返回我们需要知道的有关自身的信息。实际上,我们可以将顶点的法线方向检索为float3,将顶点的位置检索为float3,甚至可以将颜色值存储在每个顶点中,并将盖颜色返回为float4,我们需要了解如何存储颜色信息并在Surface Shader每个顶点内检索存储的颜色信息。

3.开始准备

为了查看顶点的颜色,我们需要有一个颜色应用于其顶点的模型,尽管你可以使用Unity来应用颜色,但你必须编写一个工具以运行个人应用颜色或编写一些脚本来实现颜色应用。

我们可以使用一个3D模型工具类似于Maya或者Blender来为我们的模型应用颜色。稍后文章末尾我也会提供项目链接。

创建一个Vertex Shader我们需要做以下步骤:

a.创建一个新的scene并导入我们前面提到的模型在场景中。

b.创建一个新的shader(SimpleVertexColor)和Material(SimpleVertexColorMat)

c.完成以上给步骤后,把shader赋予material然后把material赋予导入的模型.

效果图如截图所示

d.双击打开Shader,因为我们创建的是一个非常简单的shader,所以我们不需要包括任何属性在我们的属性框。我们仍然会包括一个Global Color Tint,仅仅为了与之前该系列的Shader保持一致。

e.下一部要告知Unity我们将会包括一个vertex函数在我们的shader中:

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert

h.和往常一样,如果我们的属性框有属性,我们必须确保创建一个相应的变量在我们的CGPROGRAM部分声明,

在#pragma语句下面输入以下代码

        float4 _MainTint; 

i.我们现在将注意力转向Input struct,我们需要为我们的添加一个新的变量,以便我们的surf()函数访问我们的vert()给我们的数据:

struct Input {
            float2 uv_MainTex;
            float4 vertColor;
        };

j.现在我们可以写我们简单的vert()函数来访问存储在网格每个顶点中的颜色。

       void vert(inout appdata_full v, out Inout o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.vertColor = v.color;
        }

k.最后我们可以使用Input结构中的顶点颜色数据分配给内置SurfaceOutput结构中的o.Albedo 参数。

void surf(Input IN, inout SurfaceOutput o)
        {
            o.Albedo = IN.vertColor.rgb * _MainTint.rgb;
        }

l.通过这些代码的完成,我们现在可以重新进入Unity编辑器让Shader编译,如果这些都做完了我们将看到以下的效果。

4.它是如何工作的

通Unity为我们提供了一种访问着色器附加到的模型的顶点信息的方法。这使我们有能力修改诸如顶点的位置和颜色之类的东西。

我们从Maya导入一个网格(虽然几何可以使用任何3D软件应用程序),其中顶点颜色被添加到Verts,我们会注意到,通过导入模型,默认材质将不会显示顶点颜色。我们实际上必须编写一个shader来提取颜色并将其显示在模型的表面上。Unity在使用Surface Shaders时为我们提供许多内置功能,这使得快速有效的提取此信息的过程成为可能。我们的第一个任务时告诉Unity我们将在创建shader时使用顶点函数,我们通过将vertex:vert参数添加到CGPROGRAM的#pragma语句来完成此操作。这回自动使Unity在编译器时查找名为ver()的顶点函数。如果找不到,Unity将抛出一个编译错误,并要求你向着色器添加一个vert()函数。这将我们带到下一步。我们必须实际编写vert()函数的代码,如上所述。如果它没有任何要求,我们首先使用内置宏来确保0变量来初始化为0,如果你是定位DirectX 11或更高版本。

通过有这些函数,我们可以访问叫做appdata_full内置数据结构,此内置结构是存储顶点信息的位置。因此,我们通过添加代码o.vertColor  = v.color将它传递给Input结构来提取顶点颜色信息。

o变量表示我们的Input结构,v变量是我们的appdata_full顶点数据。再这种情况下,我们只是从appdata_full结构中获取颜色信息并将其放入Input结构中,一旦顶点颜色在我们的Input结构中,我们就可以在我们的surf() 函数中使用它。在这份shader的情况下,我们只是将o.Albedo参数的颜色应用于内置的SurfaceOutput结构。

5.更多

你还可以从顶点颜色数据访问第四个组件。如果你注意到,我们在Input结构中声明的vertColor变量属于Float4类型。这意味着我们也传递了顶点颜色的alpha值。了解了这一点后,我们可以将它存储第四个顶点颜色以执行透明效果或为自己添加一个蒙版来混合两个纹理。着取决于你和你的产品,以确定你是否真的需要使用第四个组件,但这里值得一提。

二.在Surface Shader中设置顶点动画

现在我们知道如何基于每个顶点访问数据,让我们扩展我们的知识集以包括其他类型的数据和顶点的位置,使用顶点函数,我们可以访问网格中每个顶点的位置。这运行我们在着色器进行处理实际修改每个单独的顶点,接下来,我们将使用正弦波修改网格上每个顶点的位置。该技术可用于诸如海洋上的旗帜或波浪之类的物体创建动画。

1.开始准备

a.创建一个新的场景并放置一个平面网格在场景中间。(GameObject | 3D Objects | Plane)。

     创建的Plane对象可能看起来像是一个四边形,实际上有121个顶点,我们将要移动它们。使用四边形会产生意想不到的结果。效果图如图右下角所示。

选择这个Plane对象,在属性框中双击“MeshFilter”属性。可见上图效果。

b.创建一个Shader命名为VertexAnimation创建一个material命名为VertexAnimationMat。

c.最后,将Shader指定给material,讲material指定给平面网格。

d.让我们从shader开始填充属性块

       Properties {
        _MainTex("Base (RGB)",2D) = "white"{}
        _tintAmount("Tint Amount",Range(0,1)) = 0.5
        _ColorA("Color A",Color) = (1,1,1,1)
        _ColorB("Color B",Color) = (1,1,1,1)
        _Speed("Wave Speed",Range(0.1,80)) = 5
        _Frequency("Wave Frequency",Range(0,5)) = 2
        _Amplitude("Wave Amplitude",Range(-1,1)) = 1
    }

e.我们现在需要告诉Unity我们讲通过在#pragma语句中添加一下内容来使用vertex 函数。

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert

h.为了访问我们的属性给我们的值,我们需要在CGPROGRAM块中声明一个相应的变量

sampler2D _MainTex;
        float4 _ColorA;
        float4 _ColorB;
        float _tintAmount;
        float _Speed;
        float _Frequency;
        float _Amplitude;
        float _OffsetVal;

i.我们讲使用vertex位置修改作为vertex颜色。这将允许我们为我们的对象着色。

struct Input {  float2 uv_MainTex;  float3 vertColor; }

此时,我们可以使用正弦波和顶点函数执行顶点修改。在Input结构后输入以下代码:

    void vert(inout appdata_full v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            float time = _Time * _Speed;
            float waveValueA = sin(time + v.vertex.x * _Frequency) * _Amplitude;
            v.vertex.xyz = float3(v.vertex.x, v.vertex.y + waveValueA, v.vertex.z);
            v.normal = normalize(float3(v.normal.x + waveValueA, v.normal.y, v.normal.z));
            o.vertColor = float3(waveValueA, waveValueA, waveValueA);
        }

j.最后,我们通过在两种颜色之间执行lerp()函数来完成shader,这样我们就可以通过顶点函数修改新网格的峰和谷。

void surf (Input IN, inout SurfaceOutput o) {
            half4 c = tex2D(_MainTex, IN.uv_MainTex);
            float3 tintColor = lerp(_ColorA, _ColorB, IN.vertColor).rgb;
            o.Albedo = c.rgb * (tintColor * _tintAmount);
            o.Alpha = c.a;
        }

k.回到Unity中等待Shader编译,效果图应该如下所示:(这里的贴图是我随意找的)

2.它是如何工作的

这个特殊的shader使用的理念和上一个shader很相似,除此之外我们一致修改网格中顶点的位置,这种做法非常有用在我们使用简单的对象的时候,然后并不像使用骨骼结构或变换层次结构对它们进行动画处理。我们只需使用Cg语言中内置的sin()函数创建一个正弦波值。计算此值后,我们将其添加到每个顶点位置的y值,从而创建类似于波浪的效果。我们还修改了网格上的法线,以便根据正弦波值给出更逼真的阴影。通过利用Surface Shaders为我们提供的内置顶点参数,我们法线执行更复杂的顶点效果是多么容易。

三.挤压你的模型

游戏中最大的问题之一就是重复。创建新内容是一项耗时的任务,当你不得不面对成千上万的敌人时,很有可能他们看起来都是一样的,为模型添加变化的相对被便衣的技术时使用改变其基本几何的shader。下面我们就学习一种普通挤出技术,可用于创建模型的chubbier或skinnier版本,如Unity阵营演示中士兵的。

3.开始准备

我们需要访问要更改的模型使用的Shader,一旦我们拥有它,我们复制它,以便我们可以安全地编辑它。

         找到模型正在使用地Shader,一旦选中,按Ctrl+D复制它。如果它只是使用Standard Shader,如本例所示,也可以只创建一个新的标准材质,如法线,和Albedo图将会自动转移,无论使用哪种方式,重命名这个新的Shader为NormalExtrusion.

        重新测试这个模型地原始材质并同样设置克隆地Shader。

        给模型设定新的材质并开始编辑它。

为了使此效果起作用,我们的模型应具有法线。

a.让我们首先在我们的shader中添加一个属性,该属性将用于调整其拉伸,此处显示的范围从-0.0001到0.0001,我们也可以根据自己的需要进行调整。

    _Amount ("Extrusion Amount", Range(-.0001, .0001)) = 0

b.配备该属性相关的变量

   float _Amount;

c.更改#pragma指令,使其现在使用顶点修饰符,我们可以通过在其末尾添加vertex:function_name来完成更嗨操作。在下面的例子中,我们调用vert:function:

 #pragma surface surf Standard vertex:vert

e.添加以下顶点修改器

void vert (inout appdata_full v)
{

     v.vertex.xyz += v.normal + _Amount;

}

h.这个Shader已经写好了,我们可以在面板中滑动Amount变量使模型更加纤薄或更加丰满。此外,我们可以随意创建材质的克隆,以便为每个角色创建不同的拉伸量。

4.它是如何工作的

Surface Shaders分两步完成。我们使用了函数: 顶点修改器。它采用顶点的数据结构(通常称为appdata_full)并对其应用变换。这使我们可以自由地使用模型的几何形状进行任何操作,我们通过将vertex:vert添加到Surface shader的#pragma指令向GPU发信号通知存在这样的函数。可用于改变模型几何形状的最简单但有效的技术之一称为正常挤压。它通过沿其法线方向投影顶点来工作。这时通过以下代码行来完成的;

 v.vertex.xyz += v.normal * _Amount;

顶点的位置由_Amount单位朝向顶点法线移位。如果_Amount太高,结果可能会不太乐观。但是,如果值较小,可以为模型添加大量变体。

5.更多

如果你有多个敌人,并希望每个敌人都有自己的重量,你必须为每个敌人创建一个不同的材料。这是必要的,因为材质通常在模型之间共享,而更改材质将改变所有的模型。有几种方法可以做到这一点;最快的是创建一个自动为您执行此操作的脚本。一下脚本一旦附加到具有渲染器的对象,将复制其第一个材质并自动设置_Amount属性。

using UnityEngine;

public class NormalExtruder :MonoBehaviour
{

      [Range(-0.0001f,0.0001f)]

      public float amount = 0;

      void Start()
      {

            Material material = GetComponent<Renderer>().sharedMaterial;

            Material newMaterial = new Material(material);

            newMaterial.SetFloat("_Amount",amount);

            GetComponent<Renderer>().material = newMaterial;

       }

}

6.添加拉伸贴图

实际上可以进一步改进该技术。我们可以添加额外的纹理(或使用主要纹理的alpha通道)来指示挤出量。这允许更好的控制升高或降低哪些部件。以下代码向我们展示了如何实现这样的效果(与之前我们所做的主要区别在于粗体字体)

sampler2D _ExtrusionTex;    

void vert(inout appdata_full v)  

 {  

   float4 tex = tex2Dlod (_ExtrusionTex, float4(v.texcoord.xy,0,0));  

   float extrusion = tex.r * 2 - 1;  

   v.vertex.xyz += v.normal * _Amount * extrusion;  

 }

注意:在着色器中,颜色通道从零到一,尽管有时我们也需要表示负值(例如向内挤出)。在这种情况下,将0.5视为零;考虑将较小的值视为负数,将较高的值视为正值。这正是法线所发生的情况,它通常以RGB纹理编码。

UnpackNormal()函数用于映射范围(-1,+1)范围(0,1)中的值。从数学上讲,这相当于tex.r * 2 -1.

挤出贴图非常适合通过收缩皮肤来突出字体,以突出下方骨骼的形状。以下屏幕截图显示了如何使用shader和拉伸贴图讲健康的士兵变成尸体。与前一个示例相比,我们可能会注意到衣服是如何不受影响的。以下截图中使用的着色器还会使拉伸区域变暗,从而为士兵提供更加憔悴的外观:

四.制作一个雪Shader

雪的模拟一直是游戏中的挑战。绝大数游戏只是直接在模型的纹理中包含雪,因此它们的顶部看起来是白色的。但是,如果其中一个对象开始旋转怎么办?我们应该认为雪不仅是表面的少量漆画,它是一种适当的材料积累。

我们接下来要做这部分,想做出这种效果要分为两部分,首先,白色用于面向天空的所有三角形。其次,它们的顶点被挤压以模拟积雪的影响。

我们并非旨在创建逼真的雪景效果,而是提供一个起点,后续的效果可以根据美术找到合适的问题进行相应的调整。

a. 创建一个shader命名为SnowShader

b.为该Shader创建一个新的材质名为为SnowMat

c.将新创建的材质指定给要下雪的对象并指定颜色,以便更容易分辨出雪的位置

d.用以下文本替换属性块。

        _Color ("Main Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Bump("Bump",2D) = "bump" {}
        _Snow("Level of snow",Range(1,-1)) = 1
        _SnowColor("Color of snow",Color) = (1.0,1.0,1.0,1.0)
        _SnowDirection("Direction of snow",Vector) = (0,1,0)
        _SnowDepth("Depth of snow",Range(0,1)) = 0

e.完成它们对应的相关变量


        sampler2D _MainTex;
        sampler2D _Bump;
        float _Snow;
        float4 _SnowColor;
        float4 _Color;
        float4 _SnowDirection;
        float _SnowDepth;

h.用下面的文本替换Input结构体

    struct Input {
            float2 uv_MainTex;
            float2 uv_Bump;
            float3 worldNormal;
            INIERNAL_DATA
        };

i.用下面的表面函数来替换表面函数。它会将模型白雪皑皑的部分着色。

    void surf (Input IN, inout SurfaceOutputStandard o) {
            half4 c = tex2D(_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal(tex2D(_Bump, IN._Bump));
            if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >= _Snow)
            {
                o.Albedo = _SnowColor.rgb;
            }
            else
            {
                o.Albedo = c.rgb * _Color;
            }
            o.Alpha = 1;
        }

j.配置#pragma指令,使其使用顶点修饰符

#pragma surface surf Standard vertex:vert

k.添加以下顶点修改器,它们可以拉伸覆盖在雪中的顶点。

void vert(inout appdata_full v)
        {
            float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);
            if (dot(v.normal, sn.xyz) >= _Snow)
            {
                v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
            }
        }

l.我们现在可以使用材质的“Inspector”选项开来选择要覆盖模型的数量以及应该有多厚的雪。效果图如下截图所示

2.它是如何运行的

       1.表面着色

第一步改变面向天空的三角形的颜色,它影响所有的三角形,其法线方向类似于_SnowDirection.如前面所讲,我们可以使用点积来比较单位矢量。当两个向量正交时,它们的点积为零,当它们平行时,它是一。_Snow属性用于决定它们应该如何对齐以便被视为面向天空。如果仔细观察表面函数,可以看出我们没有直接点积法线和雪方向。这是因为它们通常定义在不同的空间中。雪方向以世界坐标表示,而物体法线通常相对于自己。如果我们旋转模型,它的法线并不会随之改变,这并不是我们想要的。解决这个问题,我们需要将法线从它们的对象坐标转为世界坐标。这可以通过WorldNormalVector()函数完成的。如下代码所示:

if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >= _Snow)
            {
            o.Albedo = _SnowColor.rgb;
            }
            else
            {
            o.Albedo = c.rgb * _Color;
            }

      这个着色器只是将模型着色为白色;更高级的应该使用来自真实雪材料的纹理和参数初始化SurfaceOutputStandard结构

       2.改变几何形状

该着色器的第二个效果会改变几何体以模拟积雪。首先,我们通过测试表面函数中使用的相同条件来识别哪些三角形已经着色为白色。不幸的是,这次我们不能依赖于WorldNormalVector(),因为SurfaceOutputStandard 结构尚未在顶点修改器中初始化。我们使用另一种方法将_SnowDirection转换为对象坐标。

float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

然后我们可以挤出几何体来模拟积雪。
                if (dot(v.normal, sn.xyz) >= _Snow)
                {
                    v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;
                }

再一次,这是一个非常基本的效果,我们可以使用纹理贴图来更精确地控制雪地积累,或者给出一种奇特的,不均匀的外观。

写到这儿已经非常长,所有打算把文章分为上下篇,项目链接将会放在下篇。

猜你喜欢

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