第七章:顶点的魔力-Vertex Magic《Unity Shaders and Effets Cookbook》
Unity Shaders and Effets Cookbook
《着色器和屏幕特效制作攻略》
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
—— Kenny Lammers
第七章:顶点的魔力
介绍
第1节. 在表面着色器中访问顶点颜色
1.1、准备工作
1.2、如何实现...
1.3、实现原理...
1.4、另请参阅
第2节. 在表面着色器中对顶点进行动画处理
1.1、准备工作
1.2、如何实现...
1.3、实现原理...
第3节. 将顶点颜色应用于地形
1.1、准备工作
1.2、如何实现...
1.3、实现原理...
I like this book!
第七章:顶点的魔力
在本章节中,你会学习到:
- 在表面着色器中访问顶点颜色
- 在表面着色器中对顶点进行动画处理
- 将顶点颜色应用于地形
介绍
对于想要把物体实时渲染到屏幕上,使用 Shader 一定是我们的首选而且它是至关重要的。着色器能够给物体的表面创建非常复杂的光照解决方案,但是我们也可以使用着色器来实际修改物体的顶点。这样做最大的好处是,对于修改物体的顶点,使用着色器是非常高效的。
顶点函数对于发送到 图形处理单元 (GPU) 的每个顶点执行一次。它的工作是从其 3D 局部空间中获取顶点,并转换到2D屏幕空间的正确位置上渲染它。使用顶点函数,我们可以修改顶点位置、颜色和 UV 坐标等元素。顶点函数完成对顶点的修改后,它会移动到 surf() 函数,从而会应用到每像素上。
使用顶点着色器,我们可以对模型进行强大的控制,比如创建海洋上的波浪或挥舞的旗帜等效果,或者使用顶点颜色对模型进行着色。在本章中,我们将学习如何在表面着色器中使用顶点函数。
第1节. 在表面着色器中访问顶点颜色
让我们在本章开始时,先看一下如何使用表面着色器中的顶点函数访问模型顶点的信息。这会有助于我们去掌握,使用模型顶点中包含的元素知识去创建真正有用且具有视觉吸引力的效果。
顶点函数中的顶点可以返回我们需要注意的有关自身的信息。实际上,你可以检索顶点的法线方向作为 float3 值,将顶点的位置检索为 float3,甚至可以将颜色值存储在每个顶点中,并将该颜色返回为 float4。这就是我们将在本小节中介绍的内容。我们需要学习如何在表面着色器的每个顶点内存储颜色信息并检索存储的颜色信息。
1.1、准备工作
为了编写此着色器,我们需要准备一些资产。我们按照以下步骤为创建此顶点着色器来做准备:
- 1. 为了查看顶点的颜色,我们需要一个带有顶点颜色的模型。虽然你可以使用 Unity 来应用颜色,但你需要写一个工具来允许个人去添加顶点颜色,或者写一些脚本去实现颜色应用程序。在本小节中,我们只是使用了Maya软件在模型上添加的顶点颜色。
- 2. 新建场景,把导入的模型放入场景中。
- 3. 创建新的着色器和材质球。完成后,把着色器赋予给材质球,然后将再材质球赋予给导入的模型。
场景如下 图1.1 所示:
图1.1
1.2、如何实现...
有了场景、着色器和材质球,我们就可以开始为着色器编写代码了。在Unity编辑器的项目(Project) 选项卡中双击着色器,就可以启动它了。
- 步骤1. 由于我们正在创建一个非常简单的着色器,所以我们不需要在 Properties 块中添加属性。我们只需要一个全局颜色属性,只是为了与本书中的其他着色器保持一致。在着色器的 属性(Properties) 块中输入以下代码:
Properties
{_MainTint("Diffuse Tint", color) = (1,1,1,1)
}
- 步骤2. 下一步告诉 Unity 我们要在着色器中包含一个顶点函数:
CGPROGRAM
#pragma surface surf Lambert vertex:vert
- 步骤3. 像往常一样,如果我们在 Properties 块中包含属性,我们就必须确保在 CGPROGRAM 语句中创建相应的变量。在 #pragma 语句下方输入以下代码:
// 将属性链接到CG程序
half4 _MainTint;
- 步骤4. 在 Input 结构体中,我们需要添加一个新变量。以便 surf() 函数可以访问到的 vert() 函数给我们的数据:
struct Input
{float4 vertColor;
};
- 步骤5. 现在我们可以编写简单的 vert() 函数,来获取访问存储在网格体每个顶点中的颜色权限:
void vert(inout appdata_full v, out Input o)
{ o.vertColor = v.color;
}
- 步骤6. 最后,我们可以使用 Input 结构中的顶点颜色数据分配给内置的 SurfaceOutput 结构中的 o.Albedo 参数:
void surf (Input IN, inout SurfaceOutput o)
{o.Albedo = IN.vertColor.rgb * _MainTint.rgb;
}
- 步骤7. 代码完成后,我们现在可以重新进入 Unity 编辑器并让着色器编译。如果一切顺利,您应该会看到类似于下 图1.2 的内容:
图1.2
1.3、实现原理...
Unity 为我们提供了一种访问模型顶点信息的方式,并附加给了 Shader 。这让我们能够修改顶点的位置和颜色等内容。在本小节中,我们导入的网格体是使用 Maya 软件制作的(我们也可以使用任意的 3D 软件应用程序来制作网格体),在 Maya 软件中,顶点的颜色已被添加到顶点中。您会注意到,通过导入模型,默认的材质球不会显示顶点颜色。事实上我们必须要自定义一个着色器来提取顶点颜色并将其显示在模型表面上。Unity 在使用表面着色器时为我们提供了许多内置功能,这使得提取此顶点信息的过程变得快速高效。
我们的首要任务是告诉 Unity 我们在创建着色器时将使用顶点函数。我们通过将 vertex:vert 参数添加到 CGPROGRAM 的 #pragma 语句中来做到这一点。这会让 Unity 在编译着色器时自动查找到名为 vert 的顶点函数。如果找不到,Unity 将会报误,并要求您将 vert 函数添加到着色器中。
这就把我们带到了下一步。我们实际上必须要编写 vert() 函数,如 步骤 5 所示。通过拥有这个函数,我们可以访问名为 appdata_full 的内置数据结构体。这个内置结构是存储顶点信息的地方。因此,我们通过添加代码 o.vertColor = v.color 将顶点颜色信息传递给我们的 Input 结构体来提取顶点颜色信息。
o 变量代表我们的 Input 结构体,v 变量是我们的 appdata_full 顶点数据。在此案例中,我们只是简单的从 appdata_full 结构体中获取颜色信息并将其放入我们的 Input 结构体中。一旦顶点颜色进入我们的 Input 结构体,我们就可以在 surf() 函数中使用它。在本节中,我们只需将颜色应用于内置 SurfaceOutput 结构中的 o.Albedo 参数即可。
1.4、另请参阅
还可以从顶点颜色数据中访问第四个组件。如果你注意到,我们在 Input 结构中声明的 vertColor 变量的类型是 float4。这意味着我们还传入了顶点颜色的 alpha 值。了解了这一点,你就可以利用它来存储第四个顶点颜色值,去执行透明度等效果,或者给自己多一个蒙版来混合两种纹理。这实际上取决于你和你的项目组来确定是否真的需要使用第四个组件,但在这里值得提及一下。
在 Unity 4 中,我们现在能够将着色器定位到 Directx 11。这很棒,但这意味着着色器的编译过程现在有点挑剔。意思是我们需要在着色器中再包含一行代码,以正确初始化顶点信息的输出。如果在你的 Shader 中使用 Directx 11 时,下面的代码展示了顶点函数的写法:
void vert(inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input, o);
o.vertColor = v.color;
}
通过包含此代码行,顶点着色器将不会引发任何警告,这写警告指的是它无法正确编译Directx 11。
第2节. 在表面着色器中对顶点进行动画处理
现在我们知道了如何在每个顶点的基础上访问数据,让我们扩展我们的知识集,来了解其他类型的数据和顶点的位置。
使用顶点函数,我们可以访问网格中每个顶点的位置。这可以让我们去修改每个单独的顶点,而着色器则进行处理。
在本小节中,我们将创建一个着色器,该着色器允许我们修改具有正弦波的网格体上每个顶点的位置。此技术可用于制作旗帜或海洋上的海浪等物体来创建动画。
1.1、准备工作
我们需要准备一些资产,以便为顶点着色器创建代码:
- 1. 创建一个新的场景,并在场景中心放置一个平面网格。
- 2. 然后创建一个新的着色器和材质球。
- 3. 最后,将着色器赋予给材质球,再将材质球赋予给平面网格。
场景应类似于以下 图2.1 :
图2.1
1.2、如何实现...
场景准备就绪后,让我们双击新创建的着色器在 MonoDevelop 中打开它:
- 步骤1. 首先在 Properties 块中输入新的属性值:
Properties
{_MainTex("Main Map", 2D) = "white"{}_TintAmont("Tint Amont", 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
}
- 步骤2. 我们现在需要通过在 #pragma 语句中添加来告诉 Unity 我们将使用顶点函数:
CGPROGRAM
#pragma surface surf Lambert vertex:vert
- 步骤3. 为了访问属性块中的值,我们需要在 CGPROGRAM 块中声明一个相应的变量:
// 将属性链接到CG程序
sampler2D _MainTex;
float4 _ColorA;
float4 _ColorB;
float _TintAmont;
float _Speed;
float _Frequency;
float _Amplitude;
float _OffsetVal;
- 步骤4. 我们使用顶点位置改作为顶点颜色。这会允许我们为物体进行着色:
// 确保在struct中获得纹理的uv
struct Input
{float2 uv_MainTex;float3 vertColor;
};
- 步骤5. 此时,我们可以使用正弦波和顶点函数来对顶点进行修改。在 Input 结构体后输入以下代码:
void vert(inout appdata_full v, out 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);o.uv_MainTex = v.texcoord;}
- 步骤6. 最后,我们通过在两种颜色之间执行插值函数来完成 Shader ,这样我们就可以对新网格的峰和谷着色,这些都是通过我们的顶点函数来修改:
void surf (Input IN, inout SurfaceOutput o)
{float4 c = tex2D (_MainTex, IN.uv_MainTex);float3 tintColor = lerp(_ColorA, _ColorB, IN.vertColor).rgb;o.Albedo = c.rgb * (tintColor * _TintAmont);o.Alpha = c.a;}
完成着色器的代码后,切换回 Unity 并让着色器编译。编译完成后,你应该会看到类似于以下屏幕截图的内容,如 图2.2 :
图2.2
1.3、实现原理...
这个特定的着色器用了与上一个配方相同的概念,只是这次我们修改了网格体中顶点的位置。如果你对简单的物体不想做绑定、不想使用骨架结构或变换层次结构为它们设置动画,这个方法就非常的实用(比如旗帜的动画)。
我们只需使用 Cg 语言中内置的 sin() 函数创建一个正弦波值。计算出该值后,我们将其添加到每个顶点位置的 y 值上,从而创建波浪状效果。
我们还对网格上的法线进行了一些修改,只是为了根据正弦波值为其提供更逼真的着色。
你会看到,通过利用表面着色器为我们提供的内置顶点参数来执行更复杂的顶点效果是多么容易。
第3节. 将顶点颜色应用于地形
顶点信息最常见的用途之一是创建更逼真的地形或环境。这是通过使用 RGBA 顶点颜色的每个通道混合到不同的纹理中来完成的。这非常有效,因为不需要导入另一个纹理就可以混合到其他纹理中。我们几乎会在所有涉及户外地形和结构的游戏中看到这种技术。
这个特定技术会展示一种更高级的执行此混合的方法,即使用灰度图像或高度图为顶点混合添加更多细节。
1.1、准备工作
让我们花点时间搭建一下场景,并收集一些我们需要的纹理贴图:
- 1. 创建一个新场景,然后导入带顶点颜色的网格。我们在此示例中使用的三维软件是 Maya。
- 2. 将导入的网格体放入新场景中,并创建一盏平行光。
- 3. 最后,创建一个新的着色器和材质球。然后,将 着色器(Shader) 赋予给材质球,再将材质赋予给导入的网格体。
1.2、如何实现...
创建新场景后,双击着色器以在 MonoDevelop 中将其打开。
- 步骤1. 让我们创建所需的属性,以便让此着色器的用户更好地控制最终的视觉效果:
Properties
{_MainTex("Base (RGB)", 2D) = "white"{}_SecondaryTex("Secondary Texture", 2D) = "white"{}_HeightMap("HeightMap", 2D) = "white"{}_Value("Value", Range(1, 20)) = 3
}
- 步骤2. 之后,我们需要告诉 Unity 我们将使用以下代码在表面着色器中包含一个顶点函数:
CGPROGRAM
#pragma surface surf Lambert vertex:vert
- 步骤3. 然后,让我们创建变量,将我们的 CGPROGRAM 语句连接到我们的 Properties 块:
// 将属性链接到CG程序
sampler2D _MainTex;
sampler2D _SecondaryTex;
sampler2D _HeightMap;
float _Value;
- 步骤4. 由于我们会使用更多的纹理和顶点颜色,因此我们需要使用更多参数填充 Input 结构体:
struct Input
{float2 uv_MainTex;float2 uv_SecondaryTex;float3 vertColor;
};
- 步骤5. 然后我们需要创建顶点函数。这很简单,因为我们在这个着色器中要做的就是获取顶点颜色并将其传递给 Input 结构体:
void vert(inout appdata_full v, out Input o)
{o.vertColor = v.color.rgb;o.uv_MainTex = v.texcoord;o.uv_SecondaryTex = v.texcoord2;
}
- 步骤6. 现在我们可以将注意力转向 surf() 函数。在这里,我们需要先对纹理进行采样,以便为此功能的混合部分做好准备:
// 采样纹理贴图
half4 base = tex2D (_MainTex, IN.uv_MainTex);
half4 secondTex = tex2D (_SecondaryTex, IN.uv_SecondaryTex);
float4 height = tex2D (_HeightMap, IN.uv_MainTex);
- 步骤7. 然后,我们想要根据顶点颜色的红色通道和高度图处理混合值:
// 计算混合
float redChannel = 1 - IN.vertColor.r;
float rHeight = 1 - height.r * redChannel;
float invertHeight = 1 - height.r;
float finalHeight = (invertHeight * redChannel) * 4;
float finalBlend = saturate(rHeight + finalHeight);
- 步骤8. 下一步是计算顶点混合的衰减值,以便我们可以为纹理混合添加一个细节级别:
// 让我们创建更多关于顶点颜色如何混合的细节,无论是非常硬还是非常软
float hardness = ((1 - IN.vertColor.g) * _Value) + 1;
finalBlend = pow(finalBlend, hardness);
- 步骤9. 最后,我们需要使用最终的混合值对两个纹理进行插值,并将颜色传递给我们的 SurfaceOutput 结构体:
// 输出最终颜色值
float3 finalColor = lerp(base.rgb, secondTex.rgb, finalBlend);
当着色器完成编译后,你应该会看到类似于下 图3.1 中的渲染结果:
图3.1
1.3、实现原理...
这个着色器肯定有点复杂,但你会注意到我们不需要对顶点函数本身做太多事情。我们只是将顶点颜色传递给 surf() 函数,这样我们就可以对顶点颜色执行逐像素操作。正如您可能注意到的那样,这样做的原因是顶点颜色本身确实为我们提供了足够的视觉细节来创建非常令人信服的混合。默认情况下,顶点颜色会创建非常块状的混合,只能通过向网格体添加更多顶点来修复,这样的可行度是有限的。
因此,我们采用顶点颜色并将其与灰度图像相乘,灰度图像是我们想要混合另一种纹理类型的基础纹理的高度。通过步骤 7 中的算法运行顶点颜色和高度图,我们可以在混合中添加另一个级别的视觉细节,以伪造一种纹理类型混合到基础纹理类型的效果。在我们的例子中,雪的纹理正在融入我们基础石材纹理中的小裂缝中。
这种技术最近在《神秘海域》和《战争机器》等游戏中流行起来,现在可供您在游戏项目中使用!
这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。
作者:Kenny Lammers