本章将涵盖一些在今天的游戏开发阴影管道中发现的更常见的扩散技术。让我们想象一个立方体,它被均匀地涂成白色,在一个有定向光的3D环境中。即使在每张脸上使用的颜色是相同的,它们也会有不同的白色深浅,这取决于光线从哪个方向来,以及我们从哪个角度看它。这种额外的真实感是通过使用着色器在3D图形中实现的,着色器是一种特殊的程序,主要用于模拟光的工作原理。一个木制立方体和一个金属立方体可能共享相同的3D模型,但使它们看起来不同的是它们使用的着色器。
本章将向你介绍如何在Unity中编写着色器。如果你之前没有着色器的使用经验,本章将告诉你着色器是什么,它们是如何工作的,以及如何定制它们。在本章结束时,您将学习如何构建执行基本操作的基本着色器。本章还涵盖了一些调试信息,以帮助在你的着色器中出现错误。有了这些知识,你将能够创建几乎任何表面着色器。
在本章中,我们将介绍以下食谱:
在Unity中,当我们创建一个GameObject时,我们会通过使用组件附加额外的功能。每个游戏对象都需要有一个Transform组件;Unity中已经包含了一些组件,我们在编写MonoBehaviour扩展脚本时也会创建自己的组件。
作为游戏一部分的所有对象都包含若干影响其外观和行为的组件。脚本决定对象的行为方式,而呈现程序决定对象在屏幕上的显示方式。Unity提供了几个渲染器,这取决于我们试图可视化的对象类型;每个3D模型通常都有一个附加的MeshRenderer组件。一个对象应该只有一个渲染器,但是渲染器本身可以包含多个材质。每个材质都是单个着色器的包装,这是3D图形食物链中的最后一个环。这些组件之间的关系如下图所示:
注意,上面的图提到了Cg代码。Cg只是内置渲染器的默认设置。URP/HDRP默认使用HLSL代码。
理解这些组件之间的差异对于理解着色器是如何工作的至关重要。
要开始使用这个配方,你需要让Unity运行,并必须使用内置渲染器打开一个项目。(在我的例子中,我使用的是3D模板。如果你使用的是Unity Hub 3,请转到Core | 3D。)正如我们之前提到的,Unity项目已经包含在这本烹饪书中,所以你可以使用它,并简单地添加自定义着色器,当你逐步通过每个食谱。一旦你做到了这一点,你将准备进入实时 shading 的美妙世界!
NOTE:
如果你正在使用本烹饪书附带的Unity项目,你可以打开第2章|场景|起点场景,而不是完成准备部分,因为它已经设置好了。
在我们创建第一个着色器之前,让我们创建一个小场景来使用:
2. 一旦你创建了场景,创建一个平面作为地面,进入Unity编辑器,从顶部菜单栏选择GameObject | 3D Object | plane:
3. 接下来,在Hierarchy选项卡中选择对象,然后进入Inspector选项卡。从那里,右键单击Transform组件并选择Reset Property | Position选项
这将重置对象的Position属性为0,0,0,这是我们的游戏世界的中心
NOTE
一种快速的方法是在Hierarchy窗口中选择对象时,通过按Ctrl + D键复制对象。您可以通过Inspector窗口的顶部文本框重命名对象。
确认你有方向光(它应该在Hierarchy选项卡中)。如果没有,你可以通过选择GameObject | Light | Directional Light来添加它,使它更容易看到你的变化和你的着色器对光的反应。
本书的示例代码可以在名为Chapter 2的文件夹中找到。这个文件夹保存了本章的所有代码。为了组织你的代码,在Unity编辑器的项目标签中创建一个文件夹,右键单击,并选择 Create | Folder。将文件夹重命名为Chapter 2。
生成场景后,我们可以开始编写着色器:
NOTE
你会看到Unity已经用一些基本代码填充了我们的着色器。默认情况下,它会给你一个基本的着色器,在反照率(RGB)属性中接受一个纹理。我们将修改这个基础代码,以便您可以学习如何快速开始开发自定义着色器。
Shader "CookbookShaders/Chapter 02/StandardDiffuse"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Albedo (RGB) ", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0, 1) ) = 0. 5
_Metallic ("Metallic", Range(0, 1) ) = 0. 0
}
SubShader
{
// Rest of file…
NOTE
要将材质分配给对象,你可以简单地从Project选项卡中单击并拖动材质到场景中的对象。你也可以在Unity编辑器中将材料拖到对象的Inspector选项卡中来分配它。
下面的截图显示了我们目前所做的:
在这一点上没有太多要看的,但我们的着色器开发环境已经建立,这意味着我们可以开始修改着色器,以满足我们的需要。
Unity让着色器环境的运行变得非常简单。只需点击几下,就可以开始了。有很多关于表面着色器本身的背景元素。Unity采用了Cg着色器语言,并通过为你做大量繁重的Cg代码使其更有效地编写。Surface Shader语言是一种基于组件的编写着色器的方法。处理纹理坐标和转换矩阵等任务已经为您完成,因此您不必再从头开始。在过去,我们必须开始一个新的着色器和重写大量的代码一遍又一遍。当你获得更多的经验与Surface Shaders,你会想要探索更多的Cg语言的底层函数,以及Unity是如何处理所有低级图形处理单元(GPU)的任务。
NOTE
Unity项目中所有的对象都独立于它们所在的文件夹引用。我们可以从编辑器内部移动着色器和材质,而不会有断开任何连接的风险。然而,文件不应该从编辑器之外移动,因为Unity将无法更新它们的引用。
所以,通过简单地将着色器的路径名更改为我们选择的名称,我们已经在Unity环境中获得了基本的漫反射着色器,以及光和阴影,只需要更改一行代码!
内置着色器的源代码通常隐藏在Unity中。你不能像用自己的着色器那样从编辑器中打开它。要了解更多关于在哪里找到Unity的大部分内置Cg功能的信息,请访问Unity安装目录(可以在Unity Hub的安装部分看到,如果你已经安装了它;选择安装(Unity Hub 3中的齿轮图标)旁边的三个点,并选择在资源管理器中显示选项:
从安装位置,导航到 Editor | Data | CGIncludes文件夹:
在这个文件夹中,你可以找到Unity附带的着色器的源代码。随着时间的推移,它们发生了很大的变化;你可以访问Unity下载存档(https: //unity3d。如果你需要访问在不同版本的Unity中使用的着色器的源代码。选择正确的版本后,在下拉列表中选择内置shaders,如下面的截图所示:
在这一点上有三个重要的东西:UnityCG.cginc,Lighting.
cginc 和 UnityShaderVariables.cginc。我们当前的着色器正在使用所有这些对象。在第12章,高级着色技术中,我们将探索如何使用CGInclude作为一种模块化的着色器编码方法。
着色器的属性对于着色器管道非常重要,因为你使用它们来让着色器的艺术家或用户分配纹理和调整着色器的值。属性允许你在材质的Inspector选项卡中暴露GUI元素,而不必使用单独的编辑器,这为我们提供了调整着色器的可视化方法。在选择的IDE中打开着色器后,查看第三到第九行。它被称为脚本的Properties块。目前,它有一个名为_MainTex的纹理属性
如果你看你的材质应用了这个着色器,你会注意到在Inspector选项卡中有一个纹理GUI元素。这几行代码在我们的着色器为我们创建这个GUI元素。再说一次,Unity在编码和更改属性所需的时间方面使这个过程非常高效。
让我们看看这是如何在我们当前的着色器StandardDiffuse中工作的,通过创建一些属性和学习更多关于相关语法的知识。对于这个例子,我们将改装我们之前创建的着色器。它将只使用它的颜色和一些我们可以直接从Inspector选项卡更改的其他属性,而不是使用纹理。首先复制StandardDiffuse着色器。你可以从检查器选项卡中选择它并按Ctrl + d。它将创建一个名为StandardDiffuse 1的副本。继续并将其重命名为StandardColor。
NOTE
你可以在着色器的第一行给它一个更友好的名字。例如,着色器“CookbookShaders/StandardDiffuse”告诉Unity调用这个着色器StandardDiffuse并将其移动到一个名为CookbookShaders的组中。正如我们在前面的例子中所做的那样,添加其他组的工作原理与项目中的文件夹工作原理类似。如果你使用Ctrl + D复制一个着色器,你的新文件将共享相同的名称。为了避免混淆,确保你改变了每个新的着色器的第一行,以便它在这个和未来的食谱中使用唯一的别名。
一旦StandardColor着色器准备好了,我们就可以开始改变它的属性了:
Shader "CookbookShaders/Chapter 02/StandardColor"
_MainTex ("Albedo (RGB) ", 2D) = "white" {}
sampler2D _MainTex;
fixed4 c = _Color;
就像你可能习惯于在c#和其他编程语言中编写代码时使用浮点类型作为填充点值一样,fixed用于固定点值,并且是在编写着色器时使用的类型。您可能还会看到使用了half类型,它与float类型类似,但占用了一半的空间。它在节省内存方面很有用,但在如何显示它方面不太精确。我们将在第9章“移动着色器调整”的“使着色器更高效的技术”中详细讨论这一点。
fixed4中的4表示颜色是一个单独的变量,它包含四个固定值:红色、绿色、蓝色和alpha。你将在下一章,第三章,使用表面着色器中了解更多关于这是如何工作的以及如何修改这些值的细节。
Properties
{
_Color("Color", Color) = (1, 1, 1, 1)
_AmbientColor("Ambient Color", Color) = (1, 1, 1, 1)
_Glossiness("Smoothness", Range(0, 1) ) = 0. 5
_Metallic("Metallic", Range(0, 1) ) = 0. 0
}
_MySliderValue ("This is a Slider", Range(0, 10) ) = 2. 5
NOTE
属性属于着色器,与它们相关的值存储在材质中。相同的着色器可以安全地在许多不同的材料之间共享。另一方面,改变材质的属性会影响当前使用它的所有对象的外观。
每个Unity着色器都有它在代码中寻找的内置结构。属性块是Unity所期望的功能之一。这背后的原因是给你,着色器程序员,一种快速创建GUI元素直接绑定到你的着色器代码的方法。你在properties块中声明的这些属性(变量)可以在你的着色器代码中使用,以改变值,颜色和纹理。定义属性的语法如下:
让我们来看看下面发生了什么。当您第一次开始编写一个新属性时,您需要给它一个变量名。变量名将是你的着色器代码将用于从GUI元素获取值的名称。这为我们节省了很多时间,因为我们不需要自己设置这个系统。
属性的下一个元素是检查器GUI名称和属性的类型,它们包含在括号中。检查器GUI名称是当用户与着色器交互和调整时,将出现在材质的检查器选项卡中的名称。类型是此属性将要控制的数据类型。我们可以在Unity着色器中为属性定义许多类型。
下表描述了我们可以在着色器中使用的变量类型:
最后是默认值。它只是将此属性的值设置为您在代码中放置的值。因此,在前面的示例图中,_AmbientColor属性的默认值(属于Color类型)被设置为1,1,1,1。因为这是一个Color属性,期望颜色为RGBA或float4或r, g, b, a = x, y, z, w,这个Color属性在创建时被设置为白色。
NOTE
默认值只在第一次将着色器分配给新材质时设置。之后,使用材料的值。更改默认值不会影响使用着色器的现有材质的值。当然,这是一件好事,但经常被遗忘。所以,如果你改变了一个值,但注意到有些东西没有改变,这可能是导致这种情况的原因。
这些属性记录在Unity手册中的http: //docs. unity3d.com/Documentation/Components/SL-Properties. html
现在我们已经创建了一些属性,让我们将它们连接到着色器,以便我们可以使用它们作为调整,使材质过程更具互动性。我们可以使用材质的Inspector选项卡中的Properties值,因为我们已经为属性本身附加了一个变量名,但在着色器代码中,在开始通过变量名调用值之前,你必须设置一些东西。
下面的步骤告诉你如何在一个表面着色器中使用属性:
// Inside the Properties block
_MainTex ("Albedo (RGB) ", 2D) = "white" {}
// Below the CGPROGRAM line
sampler2D _MainTex;
// Inside of the surf function
fixed4 c = tex2D (_MainTex, IN. uv_MainTex) * _Color;
Properties
{
_Color("Color", Color) = (1, 1, 1, 1)
_AmbientColor("Ambient Color", Color) = (1, 1, 1, 1)
_Glossiness("Smoothness", Range(0, 1) ) = 0. 5
_Metallic("Metallic", Range(0, 1) ) = 0. 0
_MySliderValue("This is a Slider", Range(0, 10) ) = 2. 5
}
float4 _AmbientColor;
float _MySliderValue;
void surf(Input IN, inout SurfaceOutputStandard o)
{
// We can then use the properties values in our
// shader
fixed4 c = pow((_Color + _AmbientColor) ,
_MySliderValue) ;
// Albedo comes from property values given from
// slider and colors
o. Albedo = c. rgb;
// Metallic and smoothness come from slider
// variables
o. Metallic = _Metallic;
o. Smoothness = _Glossiness;
o. Alpha = c. a;
}
Shader "CookbookShaders/Chapter 02/ParameterExample"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1)
_AmbientColor("Ambient Color", Color) =
(1, 1, 1, 1)
_Glossiness("Smoothness", Range(0, 1) ) = 0. 5
_Metallic("Metallic", Range(0, 1) ) = 0. 0
_MySliderValue("This is a Slider",
Range(0, 10) ) = 2. 5
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 200
CGPROGRAM
float4 _AmbientColor;
float _MySliderValue;
// 基于物理的标准照明模型,并在所有光类型上启用阴影
#pragma surface surf Standard
fullforwardshadows
// 使用着色器模型3。0目标,以获得更好的外观照明
#pragma target 3. 0
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
// //为这个着色器添加实例化支持。你需要在使用着色器的材料上检查“启用实例化”。参见docs. unity3d. com/Manual/GPUInstancing. htmlHTML以获取关于实例化的更多信息。
// #pragma
// instancing_optionsassumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// 在这里放置更多的每个实例属性
UNITY_INSTANCING_BUFFER_END(Props)
void surf(Input IN, inoutSurfaceOutputStandard o)
{
// 然后我们可以在我们的着色器中使用属性值
fixed4 c = pow((_Color + _AmbientColor) ,
_MySliderValue) ;
// 反照率来自滑块和颜色的属性值
o. Albedo = c. rgb;
// 金属质感和平滑度来自滑块变量
o. Metallic = _Metallic;
o. Smoothness = _Glossiness;
o. Alpha = c. a;
}
ENDCG
}
FallBack "Diffuse"
}
NOTE
pow(arg1, arg2)函数是一个内置函数,它将执行幂的等效数学函数。因此,arg1参数是我们想要取其幂的值,而arg2参数是我们想要取其幂的值。
要了解更多关于pow()函数的信息,请查看Cg教程。这是一个很好的免费资源,你可以用它来学习更多关于阴影的知识。这里还有Cg着色语言中所有可用函数的词汇表:http: //http. developer. nvidia. com/CgTutorial/cg_
tutorial_appendix_e. html
当你保存并返回到Unity时,着色器将会编译。现在,我们需要创建一个材料,将使用我们的新着色器。从项目窗口,转到第02章|材料文件夹,复制前面的一个材料,并将新创建的材料重命名为ParameterExample
在Inspector窗口中,将着色器更改为CookbookShaders | Chapter 02 | ParameterExample选项。然后,在场景视图中,通过拖放材料到球体的顶部,并释放鼠标,为场景中的球体分配材料:
这样做之后,修改材质的参数,看看它如何影响场景中的对象
下面的截图显示了在材质的Inspector选项卡中使用我们的属性来控制材质的颜色和饱和度所得到的结果:
当你在Properties块中声明一个新属性时,你就允许着色器从材质的Inspector选项卡中检索调整后的值。该值存储在属性的变量名部分。在本例中,_AmbientColor、_Color和_MySliderValue是我们存储调整值的变量。
为了能够使用SubShader块中的值,您需要创建三个与属性的变量名相同的新变量。它会自动在这两者之间建立一个链接,这样他们就知道他们必须处理相同的数据。此外,它声明了我们想要存储在SubShader变量中的数据类型,这将在我们在后面的章节中优化着色器时派上用场。一旦你创建了SubShader变量,你就可以使用surf()函数中的值。在本例中,我们希望将_Color和_AmbientColor变量加在一起,并将其取材料的Inspector选项卡中的_MySliderValue变量的幂值。绝大多数的着色器开始是标准着色器,并被修改,直到他们匹配所需的外观。有了这些,我们已经为任何需要一个扩散组件的表面着色器创建了基础。
NOTE
Material 是Asset。这意味着当你的游戏在编辑器中运行时,对它们所做的任何更改都是永久的。如果您错误地更改了属性的值,您可以使用Ctrl + Z撤消它。
和其他编程语言一样,Cg不允许出现错误。因此,如果你的代码中有一个打印错误,你的着色器将无法工作。当这种情况发生时,你的材料将被渲染成无阴影的洋红色:
当脚本无法编译时,Unity将阻止你的游戏被导出甚至执行。相反地,着色器中的错误并不会阻止游戏的执行。如果其中一个着色器是紫红色的,那么是时候调查问题出在哪里了。如果你选择了涉及的着色器,你会在它的Inspector选项卡中看到错误列表:
NOTE
着色器错误也显示在控制台窗口中。
尽管显示了引发错误的行,但这很少意味着这是必须修复的行。前面截图中显示的错误信息是通过从SubShader{}块中删除sampler2D _MainTex变量而生成的。但是,当第一行尝试访问这样的变量时,会引发错误。查找和查找代码的错误是一个称为调试的过程。以下是你应该检查的最常见的错误:
TIP
由着色器引发的错误信息可能非常具有误导性,特别是由于它们严格的语法限制。如果你对它们的意思有疑问,最好上网搜索一下。在Unity论坛上,有很多开发者可能曾经遇到过(并解决过)你的问题。
更多关于如何掌握表面着色器及其属性的信息可以在第3章,使用表面着色器中找到。
如果你想知道着色器在充分发挥其潜力时可以做什么,请参阅第12章,高级着色技术,该书将涵盖一些最高级的技术。