[Shader] Shader Cookbook 顶点函数[5]

56 篇文章 2 订阅

  shader这个术语源于一个事实,即C for Graphics (Cg)主要用于在三维(3D)模型上模拟真实的光照条件(阴影)。尽管如此,着色器现在远不止于此。它们不仅可以定义物体的外观,还可以完全重新定义它们的形状。如果你想学习如何通过着色器操作3D对象的几何形状,这一章是适合你的。

  在第二章,创建你的第一个着色器中,我们解释了3D模型不仅仅是三角形的集合。每个顶点都可以包含正确渲染模型本身所必需的数据。本章将探讨如何访问这些信息,以便在着色器中使用它。我们还将详细探讨如何通过使用Cg代码简单地变形一个物体的几何形状。

在本章中,你将学习以下食谱:

  • 在表面着色器中访问顶点颜色
  • 在一个表面着色器动画顶点
  • 挤压您的模型
  • 实现雪着色器
  • 实现体积爆炸

  在本章的最后,你将看到几种不同的方法,你可以通过着色器而不是通过操纵对象本身的网格来操纵3D对象的几何形状。


在表面着色器中访问顶点颜色

  让我们先来看看如何使用 Surface 着色器中的顶点函数来访问模型顶点的信息。它将武装我们的知识,开始利用包含在模型的顶点内的元素,以创建真正有用的和视觉上有吸引力的效果。

  顶点函数中的顶点可以返回我们需要注意的关于自身的信息。您实际上可以检索顶点的法线方向作为float3值,顶点的位置作为float3,您甚至可以在每个顶点中存储颜色值并返回该颜色为float4。这就是我们在这道食谱中要看的。我们需要了解如何存储颜色信息,并在一个表面着色器的每个顶点中检索这些存储的颜色信息。

准备

  为了编写这个着色器,我们需要准备一些资产

  要查看顶点的颜色,我们需要有一个模型,它的顶点应用了颜色。虽然你可以使用Unity来应用颜色,但你必须编写一个工具来允许个人应用颜色或编写一些脚本来实现颜色应用。

  在这个食谱的情况下,您可以使用3D建模工具,如Maya或Blender将颜色应用到我们的模型。在第07章| Models文件夹(VertexColorObj ect)中提供的示例代码中有一个可用的模型。fbx),你可以在本书的GitHub页面上获得 https: //github. com/PacktPublishing/
Unity-2021-Shaders-and-Effects-Cookbook-Fourth-Edition .

  下面的步骤将设置我们创建这个顶点着色器:

  1. 创建一个新的场景,并将导入的模型(VertexColorObj)放在场景中。
  2. 创建一个新的着色器(SimpleVertexColor)和材质(SimpleVertexColorMat)。
  3. 完成后,将着色器分配给材质,然后将材质分配给导入的模型。

你的场景现在看起来应该类似于下图
在这里插入图片描述

怎么做……

  随着我们的场景、着色器和材料的创建和准备就绪,我们可以开始为我们的着色器编写代码。在Unity编辑器的Project选项卡中双击它来启动着色器。请执行以下步骤:

  1. 因为我们正在创建一个非常简单的着色器,所以我们不需要在properties块中包含任何属性。我们仍将包括一个全局颜色Tint,只是为了保持与本书中其他着色器一致。用以下内容替换着色器属性块中的代码:
Properties
{
	_MainTint("Global Color Tint", Color) = (1, 1, 1, 1)
}
  1. 下一步告诉Unity我们将在着色器中包含一个顶点函数。为此,我们将用下面的代码替换#pragma语句的Standard fullforwardshadows部分:
CGPROGRAM
#pragma surface surf Lambert vertex: vert
  1. 和往常一样,如果我们在properties块中包含了属性,我们必须确保在CGPROGRAM语句中创建了相应的变量。在#pragma target 3. 0 下面输入以下代码。替换原来给定的参数:
float4 _MainTint;
  1. 现在我们将注意力转向Input结构体。添加一个新变量,以便surf()函数能够访问vert()函数提供的数据
struct Input
{
	float2 uv_MainTex;
	float4 vertColor;
};
  1. 现在,首先编写一个简单的vert()函数来访问存储在网格每个顶点中的颜色:
void vert(inout appdata_full v, out Input o)
{
	UNITY_INITIALIZE_OUTPUT(Input, o) ;
	o. vertColor = v. color;
}
  1. 最后,使用Input结构中的顶点颜色数据赋值给内置SurfaceOutput结构中的o. Albedo参数:
void surf (Input IN, inout SurfaceOutput o)
{
	o. Albedo = IN. vertColor. rgb * _MainTint. rgb;
}
  1. 随着我们的代码完成,重新进入Unity编辑器,让着色器编译。如果一切顺利,你应该会看到类似这样的内容:
    在这里插入图片描述
它是如何工作的……

  Unity为我们提供了一种访问附加着色器的模型顶点信息的方法。它使我们能够修改诸如顶点的位置和颜色等内容。有了这个配方,我们从Maya导入了一个网格(尽管几乎任何3D软件应用程序都可以使用),其中顶点颜色被添加到vert中。您会注意到,通过导入模型,默认材质将不会显示顶点颜色。我们实际上必须编写一个着色器来提取顶点颜色,并将其显示在模型的表面上。Unity在使用Surface Shaders时为我们提供了许多内置功能,这使得提取顶点信息的过程快速而有效。

  我们的第一个任务是告诉Unity,我们将在创建着色器时使用顶点函数。为此,我们将vertex: vert参数添加到CGPROGRAM的#pragma语句中。它会自动让Unity在编译着色器时查找一个名为vert()的顶点函数。如果它没有找到一个,Unity将抛出一个编译错误,并要求你添加一个vert()函数到你的着色器。

  这就把我们带到了下一步。我们必须实际编写vert()函数,如本菜谱的第5步所示。我们首先使用一个内置宏来确保0变量被初始化为0。如果它没有任何要求,如果你的目标是DirectX 11或以上

  通过这个函数,我们可以访问appdata_full内置数据结构。它的内置结构是顶点信息存储的地方。然后,我们通过添加代码o. vertColor = v. color将顶点颜色信息传递给Input结构体来提取顶点颜色信息.

  o变量表示Input结构,v变量表示appdata_full顶点数据。在本例中,我们只是从appdata_ full结构中获取颜色信息,并将其放入Input结构中。一旦顶点颜色在Input结构体中,就可以在surf()函数中使用它。在这个菜谱中,我们只是将o. Albedo参数的颜色应用到内置的SurfaceOutput结构。

有更多的…

  您还可以从垂直颜色数据访问第四个组件。如果您注意到,我们在Input结构中声明的vertColor变量是float4类型的。这意味着我们也传递了顶点颜色的alpha值。知道了这一点,你就可以利用它来存储第四个顶点通道,可以执行透明度等效果,或者给自己一个遮罩来混合两个纹理。是否真的需要使用第四个组件实际上取决于您和您的生产,但这里值得一提。

在表面着色器中制作顶点动画

  现在我们知道了如何按每个顶点访问数据,让我们扩展我们的知识集,以包括其他类型的数据和顶点的位置。

  使用顶点函数,我们可以访问网格中每个顶点的位置。它允许我们实际上修改每个单独的顶点,而着色器进行处理。

  在这个食谱中,我们将创建一个着色器,它将允许我们用正弦波修改网格上每个顶点的位置。这种技术可以用来创建动画的对象,如在海洋上的烟或波。

准备

让我们把我们的资产聚集在一起,这样我们就可以为我们的顶点着色器创建代码。

遵循以下步骤:

  1. 创建一个新的场景,并在场景的中心放置一个平面网格(GameObject | 3D Object | plane)

    创建的平面对象可能看起来是一个单独的四边形,但实际上,有121个我们将要移动的顶点。使用quad会带来意想不到的结果。要自己检查,从网格过滤器组件的检查器选项卡中选择平面对象右侧的圆圈,并注意在选择网格菜单中选择时显示的属性。
    在这里插入图片描述

  2. 创建一个新的着色器(VertexAnimation)和材质(VertexAnimationMat)。

  3. 最后,将着色器分配给材质,并将材质分配给平面网格。

你的场景应该如下所示:
在这里插入图片描述

怎么做……

  场景准备就绪后,让我们双击新创建的着色器,在代码编辑器中打开它。按以下步骤进行:

  1. 让我们从着色器开始,填充Properties块

    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) ) = 3. 16
    	_Amplitude ("Wave Amplitude", Range(-1, 1) ) = 1
    }
    
  2. 在#pragma语句中添加以下代码,通知Unity我们将使用一个顶点函数:

    CGPROGRAM
    #pragma surface surf Lambert vertex: vert
    
  3. 为了访问我们的属性赋予我们的值,在我们的CGPROGRAM块中声明一个相应的变量:

    sampler2D _MainTex;
    float4 _ColorA;
    float4 _ColorB;
    float _tintAmount;
    float _Speed;
    float _Frequency;
    float _Amplitude;
    float _OffsetVal;
    
  4. 使用顶点位置的修改作为一个垂直的颜色。这将允许我们给我们的对象着色:

    struct Input
    {
    	float2 uv_MainTex;
    	float3 vertColor;
    };
    
  5. 此时,使用正弦波和顶点函数执行顶点修改。在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) ;
    }
    
  6. 最后,通过在两种颜色之间执行lerp()函数来完成着色器,这样我们就可以对新网格的峰谷进行着色,并通过vertex函数进行修改:

    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;
    }
    
  7. 完成着色器的代码后,切换回Unity,让着色器编译。编译完成后,选择Material并将Base (RGB) Texture属性设置为UV Checker材质,包含在本书示例代码的 Chapter 07 | Textures 文件夹中。

  8. 从那里,将颜色A和颜色B分配到不同的颜色。修改之后,你应该会看到类似这样的内容:
    在这里插入图片描述
    玩这个游戏,你应该会注意到随着时间的推移着色器将如何影响给定平面对象的顶点!你可以在这里看到它的样子:
    在这里插入图片描述

它是如何工作的……

  这个特殊的着色器使用了与上一个配方相同的概念,只是这一次,我们要修改网格中顶点的位置。如果您不想装配简单的对象(如旗帜),然后使用框架结构或转换层次来动画它们,那么这是非常有用的。

  我们只需使用Cg语言内建的sin()函数创建一个正弦波值。计算完这个值后,我们将其添加到每个顶点位置的y值,创建一个波状效果。

  我们还修改了网格上的法线,以便根据正弦波值给它更真实的阴影。

  您将看到,利用Surface Shaders提供的内置顶点参数来执行更复杂的顶点效果是多么容易。


挤压你的模型

  游戏中的一大问题便是重复性。创造新内容是一项耗时的任务,当你必须面对数以千计的敌人时,他们很可能看起来都一样。给模型添加变化的一种相对便宜的技术是使用着色器改变模型的基本几何形状。这个配方将向你展示一种叫做正常挤压的技术,它可以用来创建一个更胖或更瘦的模型版本,如下面的Unity营演示士兵截图所示:

在这里插入图片描述
为了方便使用,我在本书的 Chapter 07 | Prefabs 文件夹下的示例代码中提供了士兵的预制。

准备

  对于这个配方,你需要访问你想要修改的模型所使用的着色器。一旦你有了它,我们会复制它以便我们可以安全地编辑它。这是它可以做到的:

  1. 找到你的模型正在使用的着色器,一旦选中,按Ctrl + d复制它。如果它只是使用标准着色器,就像在这个例子中,它也可以创建一个新的标准材质着色器。不管怎样,将这个新的着色器重命名为NormalExtrusion
  2. 复制模型的原始材质,并将克隆的着色器分配给它。
  3. 将新材料分配到模型(NormalExtrusionMat)并开始编辑它。

为了实现这个效果,你的模型应该有法线

怎么做……

要创建这个效果,首先修改复制的着色器,如下所示:

  1. 让我们开始添加一个属性到我们的着色器,它将被用来调节它的挤压。这里给出的范围从-0.0001到0.0001,但你可能需要根据自己的需要来调整:
    	_Amount ("Extrusion Amount", Range(-0. 0001, 0. 0001) ) = 0
    
  2. 将属性与它各自的变量配对:
    	float _Amount;
    
  3. 更改#pragma指令,使其现在使用顶点修改器。你可以通过在它的末尾添加vertex: function_name来做到这一点。在我们的例子中,我们调用了vert函数:
    	#pragma surface surf Standard vertex: vert
    
  4. 添加以下顶点修改器:
    	void vert (inout appdata_full v)
    	{
    		v. vertex. xyz += v. normal * _Amount;
    	}
    
  5. 现在着色器已经准备好了;你可以使用挤压量滑块在材料的检查器选项卡,使你的模型更瘦或更胖。同样,你也可以创建一个材质的克隆,以便每个角色都有不同的挤压量:
    在这里插入图片描述
它是如何工作的……

  表面着色器分为两个步骤。在之前的所有章节中,我们只探讨了它们的最后一个: surface函数。还有一个可以使用的函数:顶点修改器。它获取顶点的数据结构(通常称为appdata_full),并对其应用转换。这使我们可以自由地对模型的几何结构进行任何操作。通过在Surface Shader的#pragma指令中添加vertex: vert,我们向图形处理单元(GPU)发出这样一个函数存在的信号。你可以参考第8章,片段着色器和Grab Passes,学习如何在顶点和片段着色器中定义顶点修饰器。

  可以用来改变模型几何形状的最简单但有效的技术之一是正常挤压。它的工作原理是将顶点沿法线方向投影,并通过以下代码行完成:

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

  顶点的位置向顶点法线方向移动了_Amount个单位。如果_Amount过高,结果可能非常不愉快。但是,使用较小的值,您可以向模型添加许多变化。

有更多的…

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

using UnityEngine;
public class NormalExtruder : MonoBehaviour
{
	[Range(-0. 0001f, 0. 0001f) ]
	public float amount = 0;
	// Use this for initialization
	void Start()
	{
		Material = GetComponent<Renderer>() . sharedMaterial;
		Material newMaterial = new Material(material) ;
		newMaterial. SetFloat("_Amount", amount) ;
		GetComponent<Renderer>() . material = newMaterial;
	}
}
添加挤压映射

这项技术实际上还可以进一步改进。我们可以添加一个额外的纹理(或使用主纹理的alpha通道)来指示挤压量。它允许更好地控制哪些部分被提高或降低。下面的代码片段向您展示了如何实现这样的效果(与我们之前所做的主要区别是用粗体显示的):

Shader "CookbookShaders/Chapter 07/Normal Extrusion Map"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_ExtrusionTex("Extrusion map", 2D) = "white" {}
		_ Amount("Extrusion Amount", Range(-0. 0001, 0. 0001) ) = 0
	}
	SubShader
	{
		Tags{ "RenderType" = "Opaque" }
		CGPROGRAM
		#pragma surface surf Standard vertex: vert
		struct Input
		{
			float2 uv_MainTex;
		};
		float _Amount;
		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;
		}
		sampler2D _MainTex;
		void surf(Input IN, inout SurfaceOutputStandard o)
		{
			float4 tex = tex2D(_ExtrusionTex, IN. uv_MainTex) ;
			float extrusion = abs(tex. r * 2 - 1) ;
			o. Albedo = tex2D(_MainTex, IN. uv_MainTex) . rgb;
			o. Albedo = lerp(o. Albedo. rgb, float3(0, 0, 0) ,
			extrusion * _Amount / 0. 0001 * 1. 1) ;
		}
		ENDCG
	}
	Fallback "Diffuse"
}

  _ExtrusionTex的红色通道被用作正常挤压的乘法系数。值为0.5使模型不受影响;深色或浅色分别用于向内或向外挤压顶点。你应该注意,为了在顶点修改器中采样纹理,应该使用tex2Dlod而不是tex2D。

NOTE
在着色器中,颜色通道从0到1,尽管有时您也需要表示负值(如向内挤压)。在这种情况下,处理0.5表示0;考虑将较小的值设为负值,将较高的值设为正值。这正是法线所发生的情况,法线通常编码在红绿蓝(RGB)纹理中。UnpackNormal()函数用于将范围(0,1)中的值映射到范围(-1,+1)上。从数学上讲,这相当于tex。R * 2 -1。

  挤压贴图非常适合僵尸化角色,通过缩小皮肤来突出下面骨头的形状。下面的截图向你展示了一个健康的士兵如何只用一个着色器和挤压贴图就可以变成尸体。与前面的示例相比,您可能会注意到衣服是如何不受影响的。下面截图中使用的着色器也会使挤压区域变暗,让士兵看起来更加瘦弱:
在这里插入图片描述


实现雪着色器

  雪的模拟一直是游戏中的一个挑战。绝大多数游戏只是直接在模型的纹理中加入雪,这样他们的顶部看起来是白色的。但是,如果其中一个物体开始旋转呢?雪不仅仅是表面上的一点油漆;这是一种适当的材质积累,应该被这样对待。这个食谱向你展示了如何使用着色器给你的模型一个雪景。

  这一效果分两个步骤实现。首先,所有面向天空的三角形都使用白色。其次,它们的顶点被挤压,以模拟积雪积累的效果。你可以在以下截图中看到结果:
在这里插入图片描述

重要提示
请记住,这个食谱的目的不是创建一个逼真的雪效果。它提供了一个很好的起点,但这取决于你如何创建正确的纹理和找到正确的参数,使其适合你的游戏。

准备

这个效果完全基于着色器。我们需要做到以下几点:

  1. 为雪效果创建一个新的着色器(SnowShader)。
  2. 为着色器(SnowMat)创建一个新材质。
  3. 将新创建的材质分配给你想要被雪覆盖的对象,并分配一个颜色,这样就更容易分辨雪的位置了:
    在这里插入图片描述
怎么做……

要创建一个雪景效果,打开你的着色器,做以下改变:

  1. 将着色器的属性替换为以下属性:
    	_Color("Main Color", Color) = (1. 0, 1. 0, 1. 0, 1. 0)
    	_MainTex("Base (RGB) ", 2D) = "white" {}
    	_Bump("Bump", 2D) = "bump" {}
    	_Snow("Level of snow", Range(-1. 0, 1. 0) ) = 0. 5
    	_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
    
  2. 对应的相关变量:
    	sampler2D _MainTex;
    	sampler2D _Bump;
    	float _Snow;
    	float4 _SnowColor;
    	float4 _Color;
    	float4 _SnowDirection;
    	float _SnowDepth;
    
  3. 将Input结构替换为以下结构:
    	struct Input
    	{
    		float2 uv_MainTex;
    		float2 uv_Bump;
    		float3 worldNormal;
    		INTERNAL_DATA
    	};
    
  4. 用下面的函数替换surface函数。它会把模型的雪部分涂成白色:
    	void surf(Input IN, inout SurfaceOutputStandard o)
    	{
    		half4 c = tex2D(_MainTex, IN.uv_MainTex) ;
    		o. Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump) ) ;
    		if (dot(WorldNormalVector(IN, o.Normal) , _SnowDirection.xyz) >= _Snow)
    		{
    			o.Albedo = _SnowColor.rgb;
    		}
    		else
    		{
    			o.Albedo = c.rgb * _Color;
    		}
    		o. Alpha = 1;
    	}
    
  5. 配置#pragma指令,使其使用顶点修饰符:
    	#pragma surface surf Standard vertex: vert
    
  6. 添加下面的顶点修改器,可以挤压覆盖在雪中的顶点:
    	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;
    		}
    	}
    
  7. 现在你可以使用材质的Inspector选项卡来选择你的模型要覆盖多少以及雪的厚度:
    在这里插入图片描述
它是如何工作的……

这个着色器分两步工作:

  • 给表面上色
  • 改变几何形状

给表面上色
  第一步改变面向天空的三角形的颜色。它影响法线方向类似于_SnowDirection的所有三角形。正如在第5章,理解照明模型中所看到的,可以使用点积来比较单位向量。当两个向量正交时,它们的点积为0;当它们平行时,它是1(或-1)。 _Snow属性用于决定它们应该如何对齐,以便被视为面向天空。

  如果你仔细观察表面函数,你会发现我们并没有直接点画出法线和雪的方向。这是因为它们通常在不同的空间中定义。雪的方向用世界坐标表示,而物体法线通常是相对于模型本身。如果我们旋转模型,它的法线不会改变,这不是我们想要的。为了解决这个问题,我们需要将法线从对象坐标转换为世界坐标。这是通过WorldNormalVector()函数完成的,如下面的代码片段所示:

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

  这个着色器简单地将模型着色为白色; 更高级的一个应该初始化SurfaceOutputStandard结构与纹理和参数从一个真实的雪材料。

改变几何形状

  这个着色器的第二个效果是改变几何形状来模拟雪的积累。首先,我们通过测试Surface函数中使用的相同条件来识别哪些三角形被涂成白色。不幸的是,这一次我们不能依赖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;
}

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

另请参阅

如果你需要高质量的雪效果和道具,你也可以在Unity资产商店检查这些资源:

  • 一个更复杂的版本雪着色器在这个配方可以在https: //assetstore.unity. com/packages/vfx/shaders/winter-suite-13927
  • 一个非常现实的道具和材料的雪景环境可以在 https: //assetstore. unity. com/packages/3d/environments/winter-pack-13316

实现体积爆炸

  游戏开发的艺术是现实主义和效率之间的巧妙交易。对于爆炸来说尤其如此;它们是许多游戏的核心,但它们背后的物理原理往往超出现代机器的计算能力。爆炸本质上不过是非常热的气体球;因此,正确模拟它们的唯一方法便是将流体模拟整合到游戏中。正如你所想象的,这对于运行时应用来说是不可行的,许多游戏只是用粒子来模拟它们。当一个物体爆炸时,通常只是简单地实例化火焰、烟雾和碎片颗粒,以便它们一起可以获得可信的结果。不幸的是,这种方法不太现实,而且很容易发现。有一种中间技术可以用来实现更真实的效果: 体积爆炸。这个概念背后的想法是,爆炸不像一堆粒子; 他们正在进化3D对象,而不仅仅是臃肿的二维(2D)纹理。

准备

按照以下步骤完成这个食谱:

  1. 为这个效果创建一个新的着色器(VolumetricExplosion).
  2. 创建一个新的材质来承载着色器(VolumetricExplosionMat)。
  3. 把材料粘在一个球体上。你可以直接从编辑器中创建一个,导航到GameObject | 3D Object | Sphere

在这里插入图片描述

TIP
这个方法适用于标准的Unity球体,但如果你需要大爆炸,你可能需要使用高聚球体。实际上,顶点函数只能修改网格的顶点。所有其他点将使用附近顶点的位置进行插值。更少的顶点意味着更低的爆炸分辨率。

  1. 对于这个配方,你还需要一个渐变纹理,在渐变中,你的爆炸将拥有所有的颜色。你可以使用GNU图像处理程序(GIMP)或Photoshop创建一个这样的纹理:
    在这里插入图片描述

NOTE
你可以在本书提供的示例代码中的Chapter 07 | Textures中找到这个图像(explosionRamp)。

  1. 一旦你有了图片,就把它导入Unity。然后,从它的检查器选项卡,确保过滤模式设置为双线性和包装模式设置为Clamp。这两个设置确保了斜坡纹理的平滑采样:
    在这里插入图片描述
  2. 最后,你需要一个噪声纹理。你可以在网上搜索免费的噪音纹理。最常用的是用柏林噪声产生的。我已经在第07章|纹理文件夹中包含了一个例子供你使用:
    在这里插入图片描述
怎么做……

这个效果分为两个步骤: 一个顶点函数改变几何形状,一个表面函数给它正确的颜色。步骤如下:

  1. 删除当前属性,并添加以下属性到着色器:
    Properties
    {
    	_RampTex("Color Ramp", 2D) = "white" {}
    	_RampOffset("Ramp offset", Range(-0. 5, 0. 5) ) = 0
    	_NoiseTex("Noise Texture", 2D) = "gray" {}
    	_Period("Period", Range(0, 1) ) = 0. 5
    	_Amount("_Amount", Range(0, 1. 0) ) = 0. 1
    	_ClipRange("ClipRange", Range(0, 1) ) = 1
    }
    
  2. 添加它们的相对变量,这样着色器的Cg代码就可以访问它们了
    	sampler2D _RampTex;
    	half _RampOffset;
    	sampler2D _NoiseTex;
    	float _Period;
    	half _Amount;
    	half _ClipRange;
    
  3. 改变Input结构,让它接收渐变纹理的UV数据:
    struct Input
    {
    	float2 uv_NoiseTex;
    };
    
  4. 添加以下顶点函数:
    void vert(inout appdata_full v)
    {
    	float disp = tex2Dlod(_NoiseTex, float4(v. texcoord. xy, 0, 0)).r;
    	float time = sin(_Time[3] * _Period + disp * 10) ;
    	v.vertex.xyz += v.normal * disp * _Amount * time;
    }
    
  5. 添加以下表面函数:
void surf(Input IN, inout SurfaceOutput o)
{
	float3 noise = tex2D(_NoiseTex, IN. uv_NoiseTex) ;
	float n = saturate(noise. r + _RampOffset) ;
	clip(_ClipRange - n) ;
	half4 c = tex2D(_RampTex, float2(n, 0. 5) ) ;
	o.Albedo = c.rgb;
	o.Emission = c.rgb*c. a;
}
  1. 我们在#pragma指令中指定顶点函数,添加nolightmap参数,以防止Unity为我们的爆炸添加真实的照明:
    #pragma surface surf Lambert vertex: vert nolightmap
    
  2. 最后一步是选择材质,从它的Inspector选项卡中,在相对的插槽中添加两个纹理
    在这里插入图片描述
  3. 这是动画材料,意味着它会随着时间的推移而演变。你可以在编辑器中通过点击场景窗口中的Always Refresh来观察材质的变化:
    在这里插入图片描述
它是如何工作的……

  如果你正在阅读这个食谱,你应该已经熟悉了表面着色器和顶点修饰器的工作原理。这种效应背后的主要思想是以一种看似混乱的方式改变球体的几何形状,就像发生在真实的爆炸中一样。下面的截图向您展示了这样的爆炸在编辑器中的样子。你可以看到原来的网格已经严重变形:
在这里插入图片描述
  顶点函数是本章“挤压你的模型”中介绍的普通挤压技术的变体。不同的是,这里的挤压量是由时间和噪声纹理决定的。

TIP
当你在Unity中需要随机数时,你可以依赖random。Range()函数。在着色器中没有获得随机数的标准方法,所以最简单的方法是采样一个噪声纹理。

没有标准的方法可以做到这一点,所以只举个例子:

float time = sin(_Time[3] *_Period + disp. r*10);

  内置的_Time[3]变量用于从着色器内部获取当前时间,而dis . r噪声纹理的红色通道用于确保每个顶点独立移动。sin()函数使顶点上下移动,模拟爆炸的混乱行为。然后,正常挤压发生:

v.vertex. xyz += v.normal * disp.r * _Amount * time;

  你应该尝试这些数字和变量,直到找到你满意的移动模式。

  最后部分的效果是由曲面函数实现的。在这里,噪声纹理用于从斜面纹理中采样一个随机的颜色。然而,还有两个方面值得注意。第一个是引入_RampOffset。它的使用迫使爆炸从纹理的左侧或右侧取样颜色。当数值为正时,爆炸的表面往往呈现出更多的灰色色调——这正是它溶解时的情况。您可以使用_RampOffset来确定爆炸中应该有多少火或烟。在曲面函数中引入的第二个方面是使用clip()。clip()所做的是从渲染管道中剪辑(删除)像素。当用负值调用时,不会绘制当前像素。这个效果由_ClipRange控制,它决定了体积爆炸的哪些像素将是透明的。

  通过控制_RampOffset和_ClipRange,您可以完全控制并确定爆炸的行为和溶解方式。

有更多的…

  本配方中提供的着色器使球体看起来像爆炸。如果您真的想使用它,您应该将它与一些脚本结合使用,以最大限度地利用它。最好的方法是创造一个爆炸对象,并将其制作成预制件,这样你就可以在需要的时候重复使用它。您可以通过将球体拖回到Project窗口中来做到这一点。完成此操作后,您可以使用Instantiate()函数创建任意数量的爆炸。

  然而,值得注意的是,所有使用相同材质的物体都具有相同的外观。如果你同时进行多次爆炸,它们就不应该使用相同的材质。当你实例化一个新的爆炸时,你也应该复制它的材料。你可以用下面这段代码轻松做到这一点:

GameObj ect explosion = Instantiate(explosionPrefab) as GameObject;
Renderer = explosion. GetComponent<Renderer>() ;
Material = new Material(renderer. sharedMaterial) ;
renderer. material = material;

  最后,如果你想以一种现实的方式使用这个着色器,你应该附加一个脚本来改变它的大小,_RampOffset和_ClipRange根据你想要重建的爆炸类型

另请参阅

  要使爆炸变得逼真,还有很多工作要做。这个方法只创建了一个空壳; 在它内部,爆炸实际上是空的。一个简单的改进方法是在里面创建粒子。然而,你只能做到这一步。短片(http://unity3d.com/pages/butterfly)。(游戏邦注:由Unity Technologies与Passion Pictures和Nvidia合作制作)便是一个完美的例子。它基于改变球体几何形状的相同概念,但它使用一种称为体射线铸造的技术来渲染它。简而言之,它呈现的几何图形好像是满的。你可以在下面的截图中看到一个例子:
在这里插入图片描述

  • 如果您正在寻找高质量的爆炸,请查看Pyro Technix ( https: //assetstore. unity. com/packages/vfx/shaders/pyro-
    technix-16925)。它包括体积爆炸,并将它们与真实的冲击波结合起来。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值