[Shader] Shader Cookbook 片段着色器和Grab通道[6]

56 篇文章 3 订阅

  到目前为止,我们一直依赖于表面着色器。它们被设计用来简化着色器编码的工作方式,为艺术家提供有意义的工具。如果我们想进一步了解着色器的知识,我们需要冒险进入顶点着色器和片段着色器的领域。

  与表面着色器相比,顶点着色器和片段着色器几乎没有关于物理属性的信息,这些物理属性决定了光如何反射到表面上。他们缺乏表达能力,却用力量来弥补。顶点和片段着色器不受物理约束的限制,非常适合非真实感效果。本章将关注一种叫做Grab通道的技术,它允许这些着色器模拟变形。

在本章中,我们将介绍以下食谱:

  • 理解顶点和片段着色器
  • 使用抓取通道绘制物体后面
  • 实现玻璃着色器
  • 实现2D游戏的水着色器

理解顶点和片段着色器

  要了解顶点着色器和片段着色器的工作原理,最好的方法是自己创建一个。这个配方将向你展示如何编写这些着色器之一,它将简单地应用纹理到一个模型,并乘以一个给定的颜色,如下面的截图所示:
在这里插入图片描述
  注意它的工作原理与Photoshop中的Multiply滤镜工作原理相似。这是因为我们要做的计算和刚才做的一样!

  这里展示的着色器非常简单,它将被用作所有其他顶点和片段着色器的开始基础。

准备

对于这个食谱,我们需要一个新的着色器。遵循以下步骤:

  1. 新建一个着色器(Multiply)。
  2. 创建一个新的材质(MultiplyMat),并将着色器分配给它。
  3. 从Chapter 07 | Prefabs 文件夹中取出士兵预制件到场景中,并将新材料附加到预制件的头部。为此,从Hierarchy窗口中,选择Soldier对象的Soldier子对象。
  4. 从那里,在Inspector选项卡中,向下滚动到蒙皮网格渲染器组件,在Materials下面,设置Element 0 为 new materials:
    在这里插入图片描述
  5. 最后,在反照率(RGB)属性中,拖放Unity_soldier_Head_ DIF_01纹理。下面的截图展示了我们正在寻找的.
    在这里插入图片描述
怎么做……

  在之前的所有章节中,我们总是能够改装表面着色器。现在情况不同了,因为表面着色器和碎片着色器在结构上是不同的。我们需要实现以下更改:

  1. 删除着色器的所有属性,用以下内容替换它们:
    Properties
    {
    	_Color ("Color", Color) = (1, 0, 0, 1)
    	_MainTex ("Albedo (RGB) ", 2D) = "white" {}
    }
    
  2. 删除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
	}
}
FallBack "Diffuse"
  1. 保存着色器脚本并返回到Unity编辑器。完成后,修改MultiplyMat材质的Color属性。你会看到我们得到了我们想要的结果:
    在这里插入图片描述
    这也将是所有未来顶点和片段着色器的基础
它是如何工作的……

  顾名思义,顶点着色器和片段着色器分两个步骤工作。模型首先通过一个顶点函数;然后将结果输入到片段函数。这两个函数都是使用#pragma指令分配的:

#pragma vertex vert
#pragma fragment frag

在这种情况下,它们被简单地称为vert和frag

  从概念上讲,片段与像素密切相关; 术语片段通常用来指收集必要的数据来绘制一个像素。这也是为什么顶点着色器和片段着色器通常被称为像素着色器。

  顶点函数接受一个结构中的输入数据,该结构在着色器中被定义为vertInput:

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

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

NOTE
曲面着色器中的顶点函数只用于改变模型的几何形状。然而,在顶点或片段着色器中,顶点函数是必要的,以在屏幕上投影模型的坐标。

  这种转换背后的数学方法超出了本章的范围。然而,这种转换可以通过使用UnityObj ectToClipPos函数来实现,它将在齐次坐标中从对象的空间取一个点到相机的剪辑空间。这是通过乘以模型-视图-投影矩阵来实现的,这对于找到一个顶点在屏幕上的位置非常重要:

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

NOTE
更多关于ShaderLab内置的辅助函数的信息,请查看https: //docs. unity3d. com/Manual/SL-BuiltinFunctions. html

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

o.texcoord = input.texcoord;

  当Unity为我们初始化vertInput时,我们负责初始化vertOutput。尽管如此,它的字段仍然需要用绑定语义来装饰:

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

  一旦顶点函数初始化了vertOutput,该结构就被传递给片段函数(fragg)。这将对模型的主要纹理进行采样,并将其乘以所提供的颜色。

  正如你所看到的,顶点着色器和片段着色器不知道材质的物理属性。这意味着材料不具有与光源相同的效果,它没有关于光如何反射创建碰撞表面的数据与表面着色器相比;它更接近图形处理器的架构。

有更多的…

  顶点着色器和片段着色器最令人困惑的方面之一是绑定语义。你还可以使用其他许多词,它们的含义取决于上下文。

输入语义(Input semantics)
  下表中显示的绑定语义可以在vertInput中使用,vertInput是Unity提供给顶点函数的结构。用这些语义装饰的字段将自动初始化:

在这里插入图片描述
输出的语义(Output semantics)
  绑定时,vertOutput使用语义;它们不自动保证fields将被初始化。事实恰恰相反;这是我们的责任。编译器将尽力确保字段用正确的数据初始化:

在这里插入图片描述

NOTE
要了解更多关于剪辑空间、剪辑空间的含义以及DirectX和OpenGL之间的区别的信息,请查看https: //answers.unity. com/questions/1443941/shaders-what-is-clip-space. html

  如果出于任何原因,您需要一个包含不同类型数据的字段,您可以使用众多可用的TEXCOORD数据中的一个来装饰它。编译器不允许未修饰的字段。

另请参阅

  你可以参考NVIDIA参考手册来查看Cg中可用的其他绑定语义:http://developer.download.nvidia.com/cg/Cg_3.1/Cg-3.1_April2012_ReferenceManual.pdf


使用Grab通道在物体后面绘制

  在第6章“基于物理渲染”的“为PBR添加透明度”中,我们学习了如何使材料透明。即使透明材料可以覆盖一个场景,它也不能改变在它下面绘制的东西。这意味着那些透明着色器不能创造扭曲,如典型的看到在玻璃或水。为了模拟它们,我们需要引入另一种技术,称为 grab pass。它允许我们访问已经在屏幕上绘制的内容,以便着色器可以不受限制地使用它(或更改它)。为了学习如何使用 grab pass,我们将创建一个材料来抓取后面渲染的内容,并在屏幕上再次绘制它。矛盾的是,它是一个着色器,执行几个操作来显示完全没有变化。

准备

对于这个食谱,你需要做以下操作:

  1. 创建一个我们稍后将初始化的着色器(GrabShader)。
  2. 创建一个承载着色器的材质(GrabMat)。
  3. 将材料附着在一个flat的几何图形上,例如四边形。把它放在另一个物体的前面,这样你就不能透过它看到东西了。当着色器完成时,四边形就会显示为透明的:
    在这里插入图片描述
怎么做……

要使用grab pass,请遵循以下步骤:

  1. 删除属性部分,因为这个着色器不会使用它。
  2. 在SubShader部分,删除所有内容,这样你就剩下了下面的着色器代码
    Shader "CookbookShaders/Chapter 08/GrabShader"
    {
    	SubShader
    	{
    	}
    	FallBack "Diffuse"
    }
    
  3. 接下来,添加以下到SubShader部分,以确保对象被视为透明的:
    Tags{ "Queue" = "Transparent" }
    
  4. 然后,在下面,添加一个grab pass:
    GrabPass{ }
    
  5. 在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;
	};
	// Vertex function
	vertOutput vert(vertInput v)
	{
		vertOutput o;
		o. vertex = UnityObjectToClipPos(v. vertex) ;
		o. uvgrab = ComputeGrabScreenPos(o. vertex) ;
		return o;
	}
	// Fragment function
	half4 frag(vertOutput i) : COLOR
	{
		fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i. uvgrab) ) ;
		return col + half4(0. 5, 0, 0, 0) ;
	}
	ENDCG
}
	
  1. 保存脚本并返回到Unity编辑器。在这一点上,你应该看到你的材料现在以你想要的方式工作:
    在这里插入图片描述
它是如何工作的……

  到目前为止,我们的代码总是直接放在SubShader部分。这是因为我们之前的着色器只需要一次Pass。这一次,需要两个Pass。第一个是GrabPass{},它由GrabPass{}定义。其余的代码放在第二次Pass中,它包含在pass块中。

  第二个Pass在结构上与本章第一个配方中展示的着色器没有什么不同;我们使用顶点函数,vert,来获得顶点的位置,然后我们在Fragment函数中给它一个颜色。不同之处在于,vert计算了另一个重要的细节:GrabPass{}的UV数据。GrabPass{}自动创建一个可以引用如下的纹理:

sampler2D _GrabTexture;

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

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

  这是获取纹理并将其应用到屏幕正确位置的标准方法。如果一切都做得正确,这个着色器将简单地克隆在几何图形后面渲染的东西。在下面的食谱中,我们将如何使用这种技术来创建水和玻璃等材料。

有更多的…

  每次你使用GrabPass{}材质时,Unity将不得不渲染屏幕到纹理。这个操作非常昂贵,并且限制了你可以在游戏中使用的GrabPass实例的数量。Cg提供了稍微不同的变化:

GrabPass {"TextureName"}

  这一行不仅允许你给纹理命名,还允许你与所有有一个名为TextureName的GrabPass的材料共享纹理。这意味着如果你有10个材质,Unity将只执行一个GrabPass,并与所有材质共享纹理。这个技术的主要问题是它不允许可以堆叠的效果。如果你用这种方法制作一个玻璃杯,你就不可能有两个玻璃杯一个接一个。


实现玻璃着色器

  玻璃是一种非常复杂的材料; 不应该感到惊讶的是,其他食谱已经创建了着色器来模拟它,如在第6章,基于物理渲染的添加透明度的PBR食谱。我们已经知道如何使我们的眼镜半透明,以完美地显示其背后的物体,这在许多应用中都是有效的。然而,大多数眼镜都不是完美的。例如,如果你透过彩色玻璃窗看,你可能会注意到变形。这张食谱将教你如何达到这种效果。这个效果背后的想法是使用一个顶点和片段着色器与GrabPass,然后通过改变它的UV数据来采样抓取纹理创建一个失真。你可以在下面的截图中看到这个效果。在这里,我们使用了Unity的标准资产中的玻璃着色纹理:
在这里插入图片描述

准备

这个食谱的设置类似于上一个食谱:

  1. 创建一个新的顶点和片段着色器。你可以从复制我们在前一个食谱中使用的方法开始,使用Grab pass来绘制对象的后面,作为一个基础,选择它并按Ctrl + D复制它。复制完成后,将其名称更改为WindowShader。
  2. 创建一个材质,将使用着色器(WindowMat)。
  3. 将材质分配到一个四边形或另一个平坦的几何体,将模拟您的玻璃。
  4. 在它后面放一些物体,这样你就可以看到扭曲效果了。
    在这里插入图片描述
怎么做……

让我们从编辑顶点着色器和片段着色器开始:

  1. 在脚本的SubShader部分上面,创建一个Properties块,其中包含以下项目:

    Shader "CookbookShaders/Chapter 08/WindowShader"
    {
    Properties
    {
    	_MainTex("Base (RGB) Trans (A) ", 2D) = "white" {}
    	_Colour("Colour", Color) = (1, 1, 1, 1)
    	_BumpMap("Noise text", 2D) = "bump" {}
    	_Magnitude("Magnitude", Range(0, 1) ) = 0. 05
    	}
    	SubShader
    	{
    		Tags{ "Queue" = "Transparent" }
    		// Rest of WindowShader. shader
    
  2. 在第二次Pass中添加它们的变量:

    	sampler2D _MainTex;
    	fixed4 _Colour;
    	sampler2D _BumpMap;
    	float _Magnitude;
    
  3. 将纹理信息添加到vertInput和vertOutput结构中:

    	float2 texcoord : TEXCOORD0;
    
  4. 将UV数据从输入传输到输出结构:

    // Vertex function
    	vertOutput vert(vertInput v)
    	{
    		vertOutput o;
    		o. vertex = UnityObj ectToClipPos(v. vertex) ;
    		o. uvgrab = ComputeGrabScreenPos(o. vertex) ;
    		o. texcoord = v. texcoord;
    		return o;
    	}
    	```
    
  5. 使用以下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;
    	}
    
  6. 这个材质是透明的,所以我们需要改变SubShader块的标签部分

    Tags
    {
    	"Queue" = "Transparent"
    	"IgnoreProjector" = "True"
    	"RenderType" = "Transparent"
    }
    
  7. 保存脚本并返回到Unity编辑器。在场景窗口中,你可能看不到任何东西。我们现在需要做的是为玻璃设置纹理,并使用法线贴图来替换GrabPass (你可以在本书示例代码的 Chapter 08 | Textures 文件夹中找到一个例子):
    在这里插入图片描述

它是如何工作的……

 ; 这个着色器使用一个Grab pass 来获取已经呈现在屏幕上的内容。变形发生的部分是在片段函数中。在这里,一个法线映射被解压缩并用于偏移抓取纹理的UV数据。

	half4 bump = tex2D(_BumpMap, i. texcoord) ;
	half2 distortion = UnpackNormal(bump) . rg;
	i. uvgrab. xy += distortion * _Magnitude;

_Magnitude滑块用于确定效果的强度:
在这里插入图片描述

有更多的…

  这种效应非常普遍; 它抓住屏幕,根据法线贴图产生失真。我们没有理由不使用它来模拟更有趣的事情。许多游戏使用爆炸或其他科幻设备的扭曲。这种材质可以应用在球体上,通过不同的法线贴图,它可以完美地模拟爆炸的热浪。


实现2D游戏的水着色器

  我们在前一个配方中介绍的玻璃着色器是静态的;它的失真从未改变。只需要做一些改变就能将其转换成动画材料,这使得它非常适合以水为特色的2D游戏。这使用了一个类似的技术,在第7章,顶点函数,在一个表面着色器的动画顶点配方中:
在这里插入图片描述

准备

  这个配方是基于顶点和片段着色器描述在使用Grab pass绘制后面的对象配方,因为它将严重依赖于GrabPass。让我们开始吧:

  1. 创建一个新的顶点和片段着色器。你可以开始复制一个使用在前面的食谱,使用 Grab pass 来绘制后面的对象,作为一个基础(GrabShader),选择它并按Ctrl + D复制它。复制完成后,将其名称改为2DWaterShader。
  2. 创建一个材质,将使用着色器(2DWaterMat)。
  3. 将材质分配到一个平坦的几何将代表你的2D水。为了让这个效果起作用,你应该在它后面渲染一些东西,这样你就可以看到水一样的位移:
    在这里插入图片描述
  4. 这个配方需要一个噪声纹理,用来获取伪随机值。你必须选择一个无缝的噪声纹理,比如由可平铺的2D柏林噪声生成的纹理,如下面的截图所示:
    在这里插入图片描述
      这可以确保当材料被应用到一个大的物体上时,你不会看到任何不连续。为了实现这个效果,必须导入纹理并将其Wrap Mode属性设置为Repeat。如果你想为你的水平滑和连续的外观,你也应该设置过滤模式为双线性从检查器窗口。这些设置确保纹理从着色器中正确采样。
怎么做……

  要创建这个动画效果,你可以从改装着色器开始。遵循以下步骤:

  1. 添加以下属性:
    Properties
    {
    	_NoiseTex("Noise text", 2D) = "white" {}
    	_Colour("Colour", Color) = (0. 67, 1, 0. 96, 1)
    	_Period("Period", Range(0, 50) ) = 1
    	_Magnitude("Magnitude", Range(0, 0. 5) ) = 0. 05
    	_Scale("Scale", Range(0, 10) ) = 1
    }
    
  2. 将它们各自的变量添加到第二次pass 着色器中:
    	#include "UnityCG. cginc"
    	sampler2D _GrabTexture;
    	sampler2D _NoiseTex;
    	fixed4 _Colour;
    	float _Period;
    	float _Magnitude;
    	float _Scale;
    
  3. 为顶点函数定义以下输入和输出结构:
    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;
    };
    
  4. 这个着色器需要知道每个片段空间的确切位置。要做到这一点,更新顶点函数如下:
    // Vertex function
    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;
    }
    
  5. 使用以下Fragment函数:
    fixed4 frag(vertOutput i) : COLOR
    {
    	float sinT = sin(_Time.w / _Period) ;
    	float2 xyOverScale = i.worldPos.xy / _Scale;
    	float2 xCoords = xyOverScale + float2(sinT, 0) ;
    	float2 yCoords = xyOverScale + float2(0, sinT) ;
    	float distX = tex2D(_NoiseTex, xCoords) . r - 0. 5;
    	float distY = tex2D(_NoiseTex, yCoords) . 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;
    }
    
  6. 保存脚本并返回到Unity编辑器。然后,选择你的水材料(WatMat),并应用噪声纹理。然后,调整水材料的属性,注意它是如何改变它背后的东西的:
    在这里插入图片描述
它是如何工作的……

  这个着色器非常类似于在实现玻璃着色器配方中介绍的着色器。主要的区别是,这是一个动画材料;位移不是从法线映射生成的,而是考虑到当前时间来创建恒定动画。替换抓取纹理的UV数据的代码似乎相当复杂;让我们试着理解它是如何产生的。它背后的思想是一个正弦函数被用来使水振荡。这种影响需要随着时间的推移而演变;为了达到这个效果,由着色器产生的失真取决于当前时间,通过内置的_Time变量检索。_Period变量决定了正弦信号的周期,这意味着波出现的速度:

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

  这个代码的问题是在x轴和y轴上有相同的位移; 因此,整个抓取纹理将以圆形运动旋转,这看起来一点也不像水。我们需要添加一些随机性。

  向着色器添加随机行为最常见的方法是包含噪声纹理。现在的问题是找到一种方法在看似随机的位置对纹理进行采样。避免看到明显的正弦波模式的最好方法是在_NoiseTex纹理的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) ;
float2 xyOverScale = i.worldPos.xy / _Scale;
float2 xCoords = xyOverScale + float2(sinT, 0) ;
float2 yCoords = xyOverScale + float2(0, sinT) ;
float distX = tex2D(_NoiseTex, xCoords).r - 0. 5;
float distY = tex2D(_NoiseTex, yCoords).r - 0. 5;
float2 distortion = float2(distX, distY) ;
i.uvgrab.xy += distortion * _Magnitude;

  这就是您应该在最终结果中看到的结果

NOTE
与所有这些特效一样,没有完美的解决方案。这个配方展示了一种你可以用来创造水一样失真的技术,但鼓励你玩它,直到你找到一个符合你的游戏美学的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值