目录
1 创建一排立方体
1.1 预制体
1.2 Graph组件
1.3 实例化预制体
1.4 代码循环
1.5 精简语法
1.6 更改域
1.7 向量放到循环外
1.8 使用X定义Y
2 创建更多立方体
2.1 可变的分辨率
2.2 可变的实例
2.3 设置父节点
3 给视图上色
3.1 创建表面着色器
3.2 基于世界位置上色
3.3 通用渲染管线(URP)
3.4 创建着色器视图(Shader Graph)
4 视图动画
4.1 保持对点的追踪
4.2 更新Points
4.3 展示正弦波
4.4 钳位颜色
本文重点内容: 1、创建预制体 2、实例化多个立方体 3、展示数学函数 4、创建surface shader和shader graph 5、让视图动起来
这是关于学习使用Unity的基础知识的系列教程中的第二篇。这次,我们将使用游戏对象来构建视图,从而可以显示数学公式。我们还将让函数与时间相关,从而创建动画视图。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2019.4.10f1制作。
(用立方体展示正弦波)
1 创建一排立方体
学习编程时需要对数学有很好的了解。从根本上讲,数学是对代表数字的符号的操作。解方程归结为重写一组符号,让它变成了另一组(通常较短的)符号。数学规则决定了如何进行这种重写。
例如,函数f(x)=x 1。我们可以用一个数字代替它的x参数,比如3。得到f(3)=3 1=4 。我们提供了3作为输入参数,并以4作为输出。我们可以说这个函数将3映射到4。更简短的写法是输入-输出对,比如(3,4)。我们可以创建许多对形式(x,f(x)) ,例如(5,6)和(8,9)、(1,2)和(6,7)。但是,当我们按输入数字对这些对排序时,就更容易理解这个函数。(1,2)(2,3)(3,4)等等。
函数f(x)=x 1很容易理解。
则比较难。我们可以写下一些输入-输出对,但这可能不会让我们很好地掌握它所代表的映射。我们需要很多紧密相连的点才行。但这最终将成为一个难以解析的数字海。相反,我们可以将这些对解释为二维坐标上的形式
。
这是一个二维矢量上面的数字代表X轴上的横坐标,下面的数字代表Y轴上的纵坐标。也就是y=f(x)我们可以在表面上画出这些点。如果我们使用足够多的非常接近的点我们就得到一条直线。结果就是一个视图。
(x在-2~2之间的视图,用Desmos 制作) https://www.desmos.com/calculator/di84egsf7a)
查看视图可以让我们快速了解函数的行为。这是非常方便的工具,因此让我们在Unity中创建一个。如上一教程的第一部分所述,我们将从一个新项目开始。
1.1 预制体
一般通过将点放置在适当的坐标上来创建视图。为此,我们需要让点形成3D的可视化。为此,我们将仅使用Unity的默认立方体游戏对象。将一个添加到场景并将其命名为Point。删除其BoxCollider组件,因为我们不使用物理。
立方体是可视化视图的最佳的选择吗? 也可以使用粒子系统或线段,但是单个立方体是最简单的。
我们将使用一个自定义组件来创建此立方体的许多实例并正确放置它们。为此,我们将立方体变成游戏对象模板。将立方体从层次结构窗口拖到项目窗口中。这将创建一种新资产,称为预制件。它是项目中而不是场景中存在的预制游戏对象。
(Point 预制件资产,一列和两列的对比)
我们用来创建预制件的游戏对象仍然存在于场景中,但现在是预制实例。它在层次结构窗口中具有一个蓝色图标,并在其右侧具有一个箭头。检查器的标题还表明它是预制件,并显示更多控件。现在,位置和旋转以粗体显示,表明实例的值覆盖了预制件的值。你对实例所做的任何其他更改也将以这种方式显示。
(Point 预制件实例)
选择预制资产时,检查员将显示其根游戏对象和一个大按钮以打开预制体。
(预置体资产的检视器)
单击Open Prefab按钮将使场景窗口显示一个仅包含预制对象层次结构的场景。还可以通过实例的Open按钮,层次结构窗口中实例旁边的向右箭头或在项目窗口中双击资产来到达那里。当预制件具有复杂的层次结构时,这很有用,但对于我们的简单点式预制件而言并非如此。
(预制体在层级窗口的显示)
你可以通过层次结构窗口中其名称左侧的箭头退出预制件的场景。
为何预制场景的背景为深蓝色? 默认情况下,预制场景中的天空盒以及其他一些东西都被禁用。你可以通过场景窗口的工具栏进行配置,就像常规场景窗口一样。可以通过下拉菜单切换天空盒,该下拉菜单看起来像是一个堆栈,上面有一个星号。请注意,当你跳入和退出预制模式时,场景工具栏设置将会更改。
预制件是配置游戏对象的便捷方法。如果更改预制资产,则其在任何场景中的所有实例都将以相同的方式更改。例如,更改预制件的比例也会更改仍在场景中的立方体的比例。但是,每个实例使用其自己的位置和旋转。此外,可以修改游戏对象实例,从而覆盖预制的值。请注意,在播放模式下,预制件与实例之间的关系会断开。
我们将使用脚本来创建预制实例,这意味着我们不再需要当前场景中的预制实例。因此,可以通过Edit / Delete,也可以使用键盘快捷键或层次结构窗口中的上下文菜单将其删除。
1.2 Graph组件
我们需要一个C#脚本让这些Point预制体生成视图。创建一个并将其命名为Graph。
(Graph C#资产 在 Scripts 文件夹下)
我们从扩展MonoBehaviour的简单类开始,以便可以将其用作游戏对象的组件。给它一个可序列化的字段,以保存对用于实例化点的预制的引用,名为pointPrefab。我们需要访问Transform组件以定位Point,因此请确定字段的类型。将其显式设置为默认值,以避免编译器警告。
将一个空的游戏对象添加到场景中,并将其命名为Graph。确保其位置和旋转为零,并且其比例为1。将Graph组件添加到该对象。然后将我们的预制资产拖到视图的Point Prefab字段上。现在,它具有对预制件的Transform组件的引用。
(Graph 游戏对象)
1.3 实例化预制体
实例化游戏对象是通过Object.Instantiate方法完成的。这是Unity的对象类型的公开可用方法,而Graph是通过继承MonoBehaviour间接继承的。Instantiate方法将克隆作为参数传递给它的所有Unity对象。对于预制件来说,会创建实例添加到当前场景。让我们在Graph组件唤醒时执行此操作。
什么是MonoBehaviour的完整继承链? MonoBehaviour继承自Behaviour,它继承了Component,后者又继承自Object。
如果我们进入播放模式,则将在世界原点生成一个Point预制件实例。它的名称与预制的名称相同,并附加(Clone)。
(实例化预置,在scene窗口,向下看Z轴)
在播放模式下可以打开场景窗口吗? 是的,但是进入播放模式后,Unity始终将游戏窗口强制置于前台。如果游戏窗口与场景窗口共享一个面板,则该场景窗口将被隐藏。但是你仍可以在播放模式下切换回场景窗口。另外,你可以配置编辑器布局,以便同时显示一个或多个游戏和场景窗口。请记住,Unity必须渲染所有这些窗口,因此打开的越多,速度就越慢。
要将Point放置在其他位置,我们需要调整实例的位置。实例化方法为我们提供了对其创建内容的引用。因为我们给它提供了对Transform组件的引用,所以我们会得到返回。我们用一个变量来跟踪它。
在上一教程中,我们通过为pivot的Transform的localRotation属性分配了四元数来旋转时钟指针。更改位置的工作方式相同,不同之处在于,我们需要为localPosition属性分配3D向量。
使用Vector3结构类型创建3D向量。例如,将点的X坐标设置为1,将其Y和Z坐标保持为零。Vector3具有正确的属性,可为我们提供这样的向量。用它来设置点的位置。
(立方体向右一个单位)
现在进入播放模式时,我们仍然得到一个立方体,只是位置略有不同。让我们实例化第二个实例,并将其放在向右的另一个步长上。这可以通过将右向量乘以2来完成。重复实例化和定位,然后将乘积添加到新代码中。
我们可以用结构乘以数字吗? 正常情况下不能,但是可以定义这种功能。这是通过创建具有特殊语法的方法来完成的,因此可以将其视为乘法来调用。在这种情况下,看似简单的乘法实际上是一种方法调用,类似于Vector3.Multiply(Vector3.right,2f),结果是一个等于等于其所有分量加倍的右向量的向量。
话虽如此,只有在方法严格符合该操作符的原始含义的情况下,才应将它们用作该操作符。在向量的时候,一些数学运算符是定义明确的,因此不定义对它们而言是比较好的。
此代码将产生编译器错误,因为我们尝试两次定义point变量。如果我们想使用另一个变量,我们需要给它起一个不同的名字。或者,重用我们已经拥有的变量。一旦完成对第一个point的引用,就不需要再保留引用,因此可以将新点分配给相同的变量。
(2个示例,X的坐标分为为1和2)
1.4 代码循环
如果要创建更多的点,比如10。我们可以将相同的代码再重复8次,但这将是非常低效的编程。理想情况下,只有细微的变化的话,我们应该只为一个point编写代码,并指示程序执行多次。
while语句可用于代码块重复。将其应用于我们方法的前两个语句,然后删除其他语句。
while关键字必须在圆括号内后跟一个表达式。仅当表达式的计算结果为true时,while后面的代码块才会执行。之后,程序将循环回到while语句。如果此时表达式再次求值为true,则将再次执行代码块。重复此操作,直到表达式的值为假。然后程序跳过while语句之后的代码块,并在其下面继续。
所以我们必须在while后面加上一个表达式。要小心的确保循环不会永远重复。无限循环会使程序卡住,需要用户手动终止。可以编译的最安全的表达式就是false。
我们可以在循环内定义point吗? 是的。尽管代码被重复,但我们只定义了一次变量。就像我们之前手动完成的那样,它在循环的每个迭代中都可以重用。
可以通过跟踪重复代码的次数来限制循环。我们可以使用整数变量来对此进行跟踪。它的类型是int。它包含循环的迭代次数,因此我们将其命名为i。初始值为零。为了能够在while表达式中使用它,必须在它上面定义。
每次迭代,通过将其设置为自身加1,将数字增加1。
现在i在第一次迭代开始时变成1,在第二次迭代开始时变成2,以此类推。但是while表达式在每次迭代之前求值。在第一次迭代之前i是0,在第二次迭代之前它是1,以此类推。在第10次迭代之后,i是10。此时,我们希望停止循环,因此它的表达式的值应该为false。换句话说,只要i小于10,我们就应该继续。数学上,它表示为 i<10。它在代码中也是这样写的,使用了< 操作符。
进入游戏模式后,我们将得到10个立方体。但是它们最终都在相同的位置。沿着X轴把它们排成一行用i乘以正确的向量。
(10个立方体沿着X轴排成一排)
注意,当前第一个立方体以X坐标为1结束,最后一个立方体以10结束。让我们对其进行更改,使我们从零开始,将第一个立方体定位在原点。我们可以将所有点向左移动一个单位,方法是向右乘以(i-1)而不是i。但是,我们可以通过在块的末尾(在乘法之后)而不是在开始时增加i来跳过额外的减法。
1.5 精简语法
因为循环一定次数非常普遍,所以保持循环的代码简洁是很方便的。一些语法糖可以帮助我们。
首先,让我们考虑增加迭代次数。当执行x = x y形式的运算时,可以将其缩短为x = y。这适用于对两个操作数起作用的所有运算符。
更进一步,将数字递增或递减1时,可以将其缩短为 x或--x。
赋值语句的一个属性是它们也可以用作表达式。这意味着你可以编写类似y =(x = 3)的内容。这会使x增加3,并将其结果也分配给y。这意味着我们也可以在while表达式内增加i,从而缩短代码块。
但是,现在我们在比较之前而不是之后增加i,这样可以减少迭代次数。特别是对于现在的情况,可以将递增和递减运算符放在变量之后,而不是放在变量之前。该表达式的结果是更改前的原始值。
尽管while语句适用于所有类型的循环,但还有另一种语法特别适合在范围内进行迭代。这是for循环。它的工作原理与while相同,只是迭代器变量声明及其比较都包含在圆括号中,并用分号分隔。
这将产生编译器错误,因为在另一个分号之后还有第三部分用于递增迭代器,使它与比较分开。该部分在每次迭代结束时执行。
为什么在for循环中使用i 而不是 i? 由于增量表达式不用于其他任何内容,因此我们使用哪个版本都没有关系。我们还可以使用i = 1或i = i 1。 经典的for循环的格式为(int i = 0; i < someLimit; i )。你将会在许多程序和脚本中遇到该代码片段。
1.6 更改域
当前,我们的点的X坐标为0~9。在使用函数时,这不是一个方便的范围。通常,X的范围为0~1。或者在使用以零为中心的函数时,范围为-1~1。让我们重新定位point。
沿两个两个单位长的线段放置十个立方体将导致它们重叠。为防止这种情况,我们将减小其缩放。默认情况下,每个立方体在每个维度上的大小均为1,因此要使其适合,我们必须将其比例缩小为2/10=1/5。我们可以通过将每个点的local scale设置为Vector3.one属性除以五来实现。用/斜杠运算符进行除法。
通过将场景窗口切换为不考虑透视的正射投影,可以更好地了解立方体的相对位置。单击场景窗口右上角轴小部件下的标签,可在正交和透视模式之间切换。如果通过场景窗口工具栏关闭天空盒,那么白色立方体也更容易查看。
(小立方体,在正焦模式下,没有天空盒)
要再次将立方体重新组合在一起,请将其位置也除以5。
这使它们覆盖0~2范围。要将其变为−1~1范围,请在缩放向量之前减去1。使用圆括号指示数学表达式的运算顺序。
(-1~0.8)
现在,第一个立方体具有X坐标-1,而最后一个立方体具有X坐标0.8。但是,立方体大小为0.2。由于立方体以其位置为中心,因此第一个立方体的左侧为-1.1,而最后一个立方体的右侧为0.9。要用我们的立方体整齐地填充-1–1范围,我们需要将它们向右移动半个立方体。这可以通过在除以i之前将0.5加到i上来完成。
(-1~1的范围)
1.7 向量放到循环外
尽管所有的立方体都具有相同的比例,但我们在循环的每次迭代中都再次对其进行计算。我们不需要这么做,因为缩放是不变的。相反,我们可以在循环之前计算一次,将其存储在scale变量中,然后在循环中使用它。
我们还可以在循环之前的位置定义一个变量。当沿着X轴创建一条线时,只需要调整循环内位置的X坐标即可。因此,我们不必再乘以Vector3.right。
我们可以单独更改矢量的分量吗? Vector3结构具有三个float字段:x,y和z。这些字段是公开的,因此我们可以对其进行更改。
如果我们说x= 3,然后x= 5,我们给x 分配了一个不同的数字,而没有把数字3修改成5。然而,Unity的矢量类型是可变的。这样做是为了方便和性能,因为单个向量组件通常是独立操作的。
要了解如何使用可变向量,可以考虑使用Vector3来替代使用三个单独的float值的便捷方法。你可以独立访问它们,也可以将它们复制并分配为一个组。
这将导致编译器错误,提示未分配变量的使用。发生这种情况是因为我们在分配时尚未设置其Y和Z坐标。可以通过将Vector3.zero赋给它,将位置初始设置为零向量来解决此问题。
1.8 使用X定义Y
这个想法是把我们的立方体的位置定义为
这样我们就可以用它们来显示一个函数。现在Y坐标总是0,表示一个函数f(x)=0。为了显示一个不同的函数,我们需要确定循环内部的Y坐标,而不是在它之前。首先让Y = X,表示函数f(X)= X。
(Y=X)
一个稍微不那么明显的函数是f (x)=x的平方,它定义了一个最小值为0的抛物线。
(Y=X的平方)
2 创建更多立方体
尽管此时我们有一个函数视图,但这很丑陋。由于我们仅使用十个立方体,因此看起来非常块状且离散。如果我们使用更多和更小的立方体,看起来会更好。
2.1 可变的分辨率
我们可以使它成为可配置项,而不是使用固定数量的立方体。为此,向视图中添加一个可序列化的整数字段。默认值为10,也就是我们现在使用的值。
(可配置的分辨率)
现在,我们可以通过检视器更改视图的分辨率。但是,并非所有整数都是有效的分辨率。至少他们必须是正的。我们可以指示检视器强制执行我们设定的范围。这是通过将Range属性附加到它来完成的。我们既可以将分辨率的两个属性放在自己的方括号之间,也可以将它们合并在一个逗号分隔的属性列表中。让我们使用后者。
检查器检查字段是否附加了Range属性。如果是这样,它将限制该值并显示一个滑块。但是,为此,它需要知道允许的范围。因此,Range需要两个参数(如方法)作为最小值和最大值。让我们使用10和100。
(分辨率滑块设置为50)
这是否保证分辨率限制为10–100? Range属性所做的所有操作都是指示检查器使用具有该范围的滑块。因此,我们可以编写代码来为其分配超出范围的值,但是我们不会这样做。
2.2 可变的实例
为了利用配置后的分辨率,我们也需要更改实例化的立方体的数量。现在,迭代次数不再是在Awake中循环固定的10,而是由分辨率来决定。因此,如果将分辨率设置为50,则进入播放模式后,我们将获得50个立方体。
……是什么意思? 这表明我省略了一些未更改的代码。
我们还需要调整立方体的比例和位置,以使其保持在-1~1的域内。现在,每次迭代必须执行的每个步长的大小除以分辨率,再除以2。将此值存储在变量中,并用它来计算立方体的比例及其X坐标。
(使用50的分辨率)
2.3 设置父节点
进入分辨率为50的播放模式后,场景中以及项目窗口中都会显示许多实例化的立方体。
(这些点都是根对象)
这些点当前是根对象,但是将它们作为视图对象的子节点是有意义的。我们可以在实例化一个点之后,通过调用其Transform组件的SetParent方法,并为其传递所需的父Transform来建立此关系。可以通过从Component继承来的Graph的transform属性来获取视图对象的Transform组件。在循环块的末尾执行此操作。
(现在他们都是Graph的子节点了)
设置新的父对象后,Unity会尝试将对象保持在其原始世界位置,旋转和比例。在我们的例子中,我们不需要这个。我们可以通过将false作为第二个参数传递给SetParent来标识。
3 给视图上色
白色的视图看起来不太漂亮。我们可以使用另一种纯色,但这也不是很有意思。使用点的位置确定其颜色更有趣。
调整每个立方体颜色的直接方法是设置其材质的颜色属性。我们可以在循环中做。由于每个立方体将获得不同的颜色,这意味着我们最终将为每个对象获得一个唯一的材质实例。而且,当以后为视图制作动画时,我们也需要一直调整这些材质。尽管此方法有效,但效率不是很高。如果我们可以使用直接使用位置作为其颜色的单一材质,那就更好了。不幸的是,Unity没有这种材质。因此,我们需要自己做。
3.1 创建表面着色器
GPU运行着色器程序以渲染3D对象。Unity的材质资产确定使用哪个着色器,并允许配置其属性。我们需要创建一个自定义着色器以获得所需的功能。通过Assets/ Create / Shader / Standard Surface创建一个,并将其命名为“ Point Surface”。
(Point Surface Shader 资产)
现在,我们有了一个着色器资产,你可以像脚本一样打开它。我们的着色器文件包含定义表面着色器的代码,该表面着色器使用的语法与C#不同。它包含一个表面着色器模板,我们将删除所有内容并从头开始创建一个最小的着色器。
表面着色器如何工作? Unity提供了一个框架来快速生成执行默认照明计算的着色器,你可以通过调整某些值来影响该着色器。这种着色器称为表面着色器。不幸的是,它们仅适用于默认渲染管道。稍后我们将介绍通用渲染管道。
Unity具有自己的着色器语法,总体上大致类似于C#,但是它是多种语言的混合体。它以Shader关键字开头,后跟一个字符串,用于定义着色器的菜单项。字符串用双引号引起来。我们使用Graph/Point Surface。之后是着色器内容的代码块。
着色器可以具有多个子着色器,每个子着色器由SubShader关键字定义,后跟一个代码块。但我们只需要一个。
在子着色器下,我们还希望通过编写FallBack“ Diffuse”向标准的漫反射着色器添加一个后备。
表面着色器的子着色器需要一个用CG和HLSL的混合体(两种着色器语言)编写的代码段。该代码必须用CGPROGRAM和ENDCG关键字括起来。
需要的第一个语句是编译器指令,称为编译指示。它写为#pragma,后接指令。在我们的示例中,我们需要#pragma surface ConfigureSurface Standard fullforwardshadows,它指示着色器编译器生成具有标准照明并完全支持阴影的表面着色器。ConfigureSurface是指用于配置着色器的方法,我们需要创建该方法。
pragma是什么意思? pragma一词来自希腊语,指的是一项行动或需要完成的事情。许多编程语言都使用它来发出特殊的编译器指令。
我们遵循#pragma target 3.0指令,该指令为着色器的target 级别和质量设置了最小值。
我们将根据点的世界位置为其着色。为了使它在表面中起作用,我们需要为配置函数定义输入结构。它需要被写为struct Input,后跟一个代码块,然后是一个分号。在块内,我们声明一个结构域,特别是float3 worldPos。它将包含渲染内容的世界位置。float3类型是Vector3结构的等效着色器。
这是否意味着移动图形会影响其颜色? 是的。使用这种方法,只有当我们将Graph对象保留在原处时,着色才是正确的:在世界原点,没有旋转,并且缩放为1。
立方体越大,颜色过渡就越明显。
在下面,我们定义了ConfigureSurface方法,尽管在着色器中它始终被称为函数。这是带有两个参数的void方法。首先是具有我们刚刚定义的Input类型的输入参数。第二个参数是表面配置数据,类型为SurfaceOutputStandard。
第二个参数必须在其类型前面写有inout关键字,这表明它用于函数的结果。
现在,我们有了一个功能良好的着色器,为其创建了一个材质,称为Point Surface。通过在其检查器标题中的Shader下拉列表中选择Graph / Point Surface,将其设置为使用我们的着色器。
(Point surface 材质 )
该材质目前是固体磨砂黑。我们可以通过将surface设置为Surface来使它看起来更像默认材质。平滑度在我们的配置函数中为0.5。在着色器中,我们不必在浮点值上添加f后缀。
现在,该材质不再是完全无光泽的。你可以在检查器标题的小型材质预览中或底部的可调整大小的预览中看到此内容。
(smoothness设置为一半之后的材质预览)
我们也可以使平滑度可配置,就像为它添加一个字段并在函数中使用它一样。默认样式是在着色器配置选项前加下划线并大写下一个字母,因此我们将使用_Smoothness。
为了使此配置选项出现在编辑器中,我们需要在子着色器之前的着色器顶部添加一个Properties块。在其中写入_Smoothness,然后((“ Smoothness”,Range(0,1))= 0.5。这将为它提供Smoothness标签,将其显示为范围为0-1的滑块,并将其默认设置为0.5。
(可配置的Smoothness)
使我们的Cube预制资产使用此材质代替默认材质。
(黑色的点)
3.2 基于世界位置上色
要调整点的颜色,我们需要修改surface.Albedo。由于反照率和世界的位置都有三个组成部分,我们可以直接将其用于反照率。
(上色了的点)
现在,世界X位置控制该点的红色分量,Y位置控制该绿色分量,而Z控制蓝色。但是我们图的X域是-1~1,负颜色分量没有意义。因此,我们需要将位置减半,然后加½以使颜色适合于域。可以一次对所有三个维度执行此操作。
为了更好地判断颜色是否正确,我们来改变一下视图。我们显示函数
这使得Y也从-1~1。
(有点蓝的曲线)
结果是带蓝色的,因为所有立方体面的Z坐标都接近零,这使它们的蓝色分量接近0.5。我们可以通过在设置反照率时仅包括红色和绿色通道来消除蓝色。这可以在着色器中完成,只需分配给surface.Albedo.rg并仅使用input.worldPos.xy。
当红色加绿色导致黄色时,这将使点从左下角的黑色附近开始,随着Y最初比X的增加快而变为绿色,随着X的增加而变为黄色,随着X的增加而稍微变为橙色,最后随着明亮而结束 右上方的黄色。
(从绿到黄)
3.3 通用渲染管线(URP)
除了默认的渲染管道外,Unity还具有通用和高清渲染管线,简称URP和HDRP。两种渲染管道都有不同的功能和限制。当前的默认渲染管道仍可运行,但其功能集已冻结。几年后,URP可能会成为默认值。因此,让我们的视图也可用于URP。
如果你尚未使用过URP,请转到程序包管理器并安装已针对你的Unity版本验证的最新Universal RP程序包。在我的例子里是7.3.1。
(安装URP包)
在包管理器中哪里可以找到URP? 确保已将包过滤器设置为Unity Registry而非In Project。然后搜索通用或向下滚动列表,直到找到它。
这不会自动使Unity使用URP。首先,我们必须通过Assets / Create / Rendering / Universal Render Pipeline / Pipeline Asset (Forward Renderer)为其创建资产。我将其命名为URP。这还将自动为渲染器创建另一个资产,在我的例子中为URP_Renderer。
(URP资产)
接下来,转到项目设置的Graphics部分,并将URP资产分配给Scriptable Renderer Pipeline Settings 字段。
(使用URP)
若要稍后切换回默认渲染管道,只需将Scriptable Renderer Pipeline Settings设置为None。这只能在编辑器中完成,不能在内置的独立应用程序中更改渲染管道。
HDRP呢? HDRP是更为复杂的渲染管线。我不会在教程中介绍它。
3.4 创建着色器视图(Shader Graph)
我们当前的材质仅适用于默认渲染管道,不适用于URP。因此,当使用URP时,会将其替换为Unity的错误材质,即粉红色。
(立方体变为粉红色)
我们需要为URP创建一个单独的着色器。可以自己写一个,但是目前很难,并且在升级到较新的URP版本时可能会失败。最好的方法是使用Unity的Shader Graph包以可视方式设计着色器。URP依赖于此软件包,因此它会与URP软件包一起自动安装。
通过Assets/ Create / Shader / PBR Graph 创建一个新的着色器视图,并将其命名为“ Point URP”。PBR代表基于物理的渲染。
(Point URP 着色器视图 资产)
可以通过在项目窗口中双击其资产或通过按其检查器中的Open Shader Editor按钮来打开该图形。这将为其打开一个着色器图形窗口,该窗口可能是一个大型主预览窗口。你可以通过拖动主预览的右下角来调整其大小。具有资产名称的另一个面板(称为黑板)也是如此。两者也可以通过工具栏隐藏。
(默认的PBR着色器视图,有master,blackboard和main preview)
着色器视图由代表数据或操作的节点组成。PRB主节点是用于配置着色器的主要节点,类似于ConfigureSurface函数的surface参数。例如,默认情况下,其Smoothness值固定为0.5。要使其成为可配置的着色器属性,请在Point URP背板面板上按加号按钮,然后选择Vector1。然后,双击面板中出现的圆形按钮,左侧带有一个绿点。将其重命名为Smoothness。将其下的默认值设置为0.5。确保已启用其Exposed切换选项,因为这可控制材质是否为其获取着色器属性。要使其显示为滑块,请将其Mode更改为Slider。
(带有平滑属性的黑板)
Reference是什么意思? 它是生成的着色器代码内部使用的名称。这通常无关紧要。
接下来,将圆角的Smoothness 按钮从黑板上拖到视图中的空白处。这将为视图添加一个平滑度节点。通过将其一个点拖到另一个点,将其连接到PRB主节点的Smoothness输入。这将在它们之间创建一个链接。
(Smoothness 连接上了)
现在,你可以通过Save Asset工具栏按钮保存视图,并创建一个使用它的名为Point URP的材质。着色器的菜单项是“ Shader Graphs/ Point URP”。然后,使Point预制件使用该材质代替Point Surface。
(URP材质使用了 shader graph)
要给point上色,我们需要从位置节点开始。通过在视图的空白部分打开上下文菜单并从中选择New Node来创建一个。选择Input/ Geometry / Position 或仅搜索Position。
(世界位置节点)
现在,我们有了一个位置节点,默认情况下将其设置为世界空间。你可以通过将鼠标悬停在其上时按下出现的向上箭头来折叠其预览可视化效果。
使用相同的方法创建Multiply 和Add节点。使用这些将位置的XY分量缩放0.5,然后加0.5,同时将Z设置为零。然后将结果连接到主节点的Albedo。
(设置颜色的 shader graph)
如果将鼠标悬停在节点上,则可以通过按节点右上角显示的箭头来压缩节点的视觉大小。隐藏所有未连接到另一个节点的输入和输出。这会消除很多混乱。
(压缩 shader graph)
保存着色器资产后,我们现在在播放模式下获得与使用默认渲染管线时相同的着色点。除此之外,调试更新器会在播放模式下出现在单独的DontDestroyOnLoad场景中。这用于调试URP,可以忽略。
(URP在Player模式下的调试器)
现在,你可以使用默认渲染管道或URP。从一种切换到另一种之后,你还需要更改Point预制件的材质,否则将变成粉红色。如果你对从视图生成的着色器代码感到好奇,可以通过PBR主节点的Show Generated Code上下文菜单项对其进行检查。
4 视图动画
显示静态图还不错,但如果有动画的视图更有趣。让我们添加对动画功能的支持。这是通过将时间作为附加函数参数来实现的,使用f(x,t) 形式的函数,而不仅仅是f(x),其中t 是时间。
4.1 保持对点的追踪
要为视图制作动画,我们需要随着时间的推移调整其点的位置。我们可以通过删除所有点并在每次Update时创建新点来实现,但这是一种非常低效的方式。最好是继续使用相同的点,并在每次更新时调整其位置。为了实现这一点,我们将使用一个字段来引用我们的points。将点字段添加到Transform类型的Graph中。
该字段使我们可以引用单个点,但是我们需要访问所有点。可以通过将空方括号放在其类型后面,将其转换为数组。
oints字段现在是对数组的引用,该数组的元素为Transform类型。数组是对象,而不是简单的值。我们必须显式创建这样的对象,并使我们的领域引用它。这是通过编写new后跟数组类型来完成的,因此在本例中为new Transform []。在循环之前,在Awake中创建数组,并将其分配给点。
创建数组时,我们必须指定其长度。这定义了它有多少个元素,创建后就不能更改。构造数组时,长度写在方括号内。使它等于视图的分辨率。
现在,我们可以使用指向点的引用来填充数组。通过在数组引用后面的方括号之间写入其索引来访问数组元素。数组索引从第一个元素的零开始,就像循环的迭代计数器一样。因此,我们可以使用它来分配给适当的数组元素。
现在,我们遍历points数组。因为数组的长度与分辨率相同,所以我们也可以使用它来约束循环。为此,每个数组都有一个Length属性,因此我们可以使用它。
4.2 更新Points
为了调整视图的每一帧,我们需要在Update方法中设置点的Y坐标。因此,不再需要在Awake中计算它们。仍然可以在此处设置X坐标,因为我们不会更改它们。
就像Awake一样,添加带有for循环的Update方法,但是在其代码块中还没有任何代码。
我们将通过获取对当前数组元素的引用并将其存储在变量中来开始循环的每次迭代。
之后,我们获取该点的位置。
现在,我们可以像之前一样基于X设置位置的Y坐标。
因为位置是一个结构,所以我们只调整了局部变量的值。要将其应用到该points,需要再次设置其位置。
我们不能直接设置point.localPosition.y吗? 如果localPosition是一个公共字段,那么我们可以直接设置该点位置的Y坐标。但是,localPosition是一个属性。它会将向量值的副本传递给我们,或复制我们分配给它的值。因此,我们最终要调整一个局部矢量值,该值根本不会影响到该点的位置。由于我们没有先将其明确存储在变量中,因此该操作将毫无意义,并且会产生编译器错误。
4.3 展示正弦波
从现在开始,在播放模式下,视图的点在每一帧中都被定位。我们还没有注意到这一点,因为它们总是在相同的位置结束。我们需要在函数中加入时间来改变它。然而,简单地添加时间将导致功能上升和迅速消失的视野。为了防止这种情况发生,我们必须使用一个在固定范围内变化的函数。sin函数最理想,我们用f(x)=sin(x)。我们可以使用Mathf.Sin方法计算。
(X的正弦波 -1~1)
什么是Mathf? 它是UnityEngine命名空间中的结构,其中包含数学函数和常量的集合。由于它可与浮点数一起使用,因此其类型名被赋予了f后缀。
正弦波在-1和1之间振荡。它每2π重复一次,发音为two pie,这意味着ts的周期大约是6.28。由于我们的图形的X坐标在-1和1之间,我们目前看到的重复模式不到三分之一。从整体上看,它的尺度是X乘以π,所以我们最后得到f(X)=sin(πX) 。我们可以使用Mathf.PI的近似常数。
( sineπX)
什么是正弦波和π?
角度θ以弧度表示,对应于沿单位圆的圆周行进的距离。在中点,行进距离等于π,大约为3.14。因此,整个圆周的长度为2π。换句话说,π是圆的周长与其直径之比。
要使这个函数具有动画效果,请在计算正弦函数之前将当前游戏时间添加到X。它是通过Time.time找到的。如果我们也将时间缩放为π,那么函数将每两秒重复一次。因此,使用f(x,t)=sin(π(x t))其中t 是经过的时间。这将使正弦波随着时间的推移而前进,使它在负的X方向上移动。
(正弦波动画)
因为对于每次循环迭代,Time.time的值都相同,所以我们可以将其放在循环外部。
4.4 钳位颜色
正弦波的振幅为1,这意味着我们的点所达到的最低和最高位置分别是-1和1。但是,由于这些点是具有一定大小的立方体,因此它们会稍微超出此范围。因此,我们可以得到绿色成分为负或大于1的颜色。尽管这并不明显,但我们还是要正确钳位颜色以确保它们保持在0–1范围内。
我们可以通过将生成的颜色传递给saturate函数来为表面着色器执行此操作。这是一项特殊功能,可将所有组件钳位为0-1。这是着色器中的常见操作,称为饱和度,因此得名。
可以在带有Saturate节点的着色器图中完成相同的操作。
(在shader graph 使用 Saturated )
下一个章节是数字表面。
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials