创建图形(Building a Graph)

Posted 大哥大嫂过年好啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建图形(Building a Graph)相关的知识,希望对你有一定的参考价值。

   原文 https://catlikecoding.com/unity/tutorials/basics/building-a-graph/

   创建一个预制

    实例化多个方块

    演示一个数学模型函数

    创建一个surface shader和shader graph

    使图形动起来

unity 2020.3.6f1

1 用方块组成线(Creating a Line of Cubes)

编程时对数学有很好的理解是必不可少的。在最基本层面上,数学是对数字的处理。求解方程归结为重写一组符号,使其成为另一组--通常更短--的符号集。数学规则决定了如何进行这种重写。

比如我们有一个函数 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很容易理解。f(x)=(x-1)^4+5x^3-8x^2+3x就比较难了。我们可以写下一些输入-输出对,但是这并不能让我们理解它代表的映射。我们需要非常多的,集中的点。最终会导致一个数字海洋,难于解析。然而,我们可以将这些对解释为\\begin{bmatrix} x\\\\ f(x) \\end{bmatrix}这种形式的2维坐标。这是一个二维向量,上面的数字表示横坐标,x轴,下面的数字表示竖坐标,y轴。另一种写法,y=f(x)。我们可以在一个面上绘制这些点。如果我们用了足够多非常靠近的点,我们就得到一条线。最终结果将是一个图形(graph)。

            

                                       Graph with xx between −2 and 2, made with Desmos.

观察图形可以快速的让我们对这个函数行为有所了解。这是一个方便的工具,所以让我们在unity中制作一个。

1.1 预制件

通过在适当的坐标处放置点来创建图形。为此,我们需要可视化的3D的点。我们使用unity的默认cube对象作为点。在scene中添加一个并命名为point。移除 BoxCollider 组件,因为我们不使用物理。

我们将使用自定义的组件来创建非常多的cube的实例,并把他们放置在正确的位置。为了做到这些我们需要把cube转化为一个游戏模板。将cube从层级窗口拖拽到项目窗口。这将创建一个资源,被称为预制件。这是一个存在于项目中的游戏对象的预制,而不是场景中。

我们用来制作预制体的游戏对象依然存在场景中,但是现在是一个预制的实例了。在层级窗口里他有一个蓝色的图标,右边是一个箭头。它的inspector头部也指示出它是一个预制件,并且显示了一些控制器。它的position和rotation现在是黑体,表明实例的这些值是预制的重载。

                                                         Point prefab instance.

当选择预制资源时,它的inspector会显示根游戏对象和一个大按钮,用来打开预制。

点击Open Prefab按钮,scene窗口会显示一个只包含预制层级的场景。你也可以通过实例的Open按钮,hierarchy窗口中实例右侧的箭头,或者双击project窗口里的资源打开这个场景。当预制的层级比较复杂时这么做很有用,但我们的例子非常简单。

                                                Hierarchy window for our prefab.

你可以通过hierarchy窗口里预制名字左侧的箭头退出预制场景。

用预制体配置游戏对象非常的方便。当你修改预制资源,它在所有场景里的实例都将做相同的修改。比如,修改预制的缩放也会修改场景里cube的缩放。但是,每个实例使用它自己的position和rotation。当然实例也可以修改,并覆盖预制里的值。注意预制和实例在play模式中是不关联的。

我们会使用脚本来创建预制的实例,也就是说我们不在需要现在场景里的实例。所以我们把它删除掉。

1.2 图形组件(Graph Component)

我们创建一个c#脚本,用我们的point预制生成一个图形。

                                                       Graph C# asset in Scripts folder.

我们从一个简单的,继承自MonoBehaviour的类开始,这样它就可以作为游戏对象的组件来使用。给它一个序列化的字段来引用预制件,命名为pointPrefab。我们通过Transform 组件来定位点。


using UnityEngine;

public class Graph : MonoBehaviour {

	[SerializeField]
	Transform pointPrefab;
}

在场景里添加一个空游戏对象,命名为Graph。将position和rotation设为0,scale设为1。将我们的Graph 组件添加到这个对象。然后将我们的预制件拖到graph的Point Prefab字段。现在它就有了一个预制的Transform组件的引用。

1.3 实例化预制(Instantiating Prefabs)

通过Object.Instantiate 函数实例化游戏对象。这是Unity的Object 类型的共有函数,而Graph 通过MonoBehaviour简介的继承ObjectInstantiate 方法克隆任意传给他的unity对象。如果给他预制体,它将再当前场景中添加一个实例。让我们在Graph组件唤醒时完成这些。

public class Graph : MonoBehaviour {

	[SerializeField]
	Transform pointPrefab;
	
	void Awake () {
		Instantiate(pointPrefab);
	}
}

如果我们进入play模式,一个实例会在世界的原点生成出来。他的名字是预制体的名字上 (Clone)后缀。

                               Instantiated prefab, looking down the Z axis in the scene window.

为了放置这个点我们需要修改实例的位置。Instantiate方法会返回一个它创建的对象的引用。因为我们传给它一个Transform组件,所以我们将的到一个Transform组件。让我们将它赋值给一个变量。

void Awake () {
		Transform point = Instantiate(pointPrefab);
	}

我们将一个3D向量赋值给TransformlocalPosition 属性用以改变其位置。

3D向量是通过Vector3结构体来创建的。例如,我们将我们的点的x坐标设为1,保持y和z坐标为0。 Vector3有一个right属性等同于这个向量。我们使用它来设置点的位置

Transform point = Instantiate(pointPrefab);
		point.localPosition = Vector3.right;

                                                       Cube one unit to the right.

进入play模式时我们依然得到一个方块,不过位置稍微不同。让我实例化另一个方块并放置到再右边一步。为此我们将right向量乘以2。

void Awake () {
		Transform point = Instantiate(pointPrefab);
		point.localPosition = Vector3.right;

		Transform point = Instantiate(pointPrefab);
		point.localPosition = Vector3.right * 2f;
	}

这段代码将产生一个编译错误,因为我们试图的定义point变量两次。如果要使用另一个变量我们必须给他一个不同的名字。或者我们可以重新使用已有的变量。我们直接将新的点赋值给那个变量。

Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;

//Transform point = Instantiate(pointPrefab);
point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * 2f;

                                         Two instances, with X coordinates 1 and 2.

1.4 编码循环(Code Loops)

让我们创建更多的点,直到我们有十个。我们可以再将代码重复八次,但这么做非常的没有效率。理想情况是,我们只需要写一个点的代码,然后通知程序执行数次,每次稍作修改。

        for (int i = 0; i < 10; i++) {
			Transform point = Instantiate(pointPrefab);
			point.localPosition = Vector3.right * i;
		}

1.6 修改域(Changing the Domain)

目前,我们的点的x坐标是0到9。这不是一个函数里方便的区间。通常,x的区间是0-1。或者当函数以0位中心时,区间是 -1 到 1 。让我们重新放置我们的点。

将十个方块放到两个单位长的线段上会发生重叠,因此我们要降低他们的scale 。每个方块的默认尺寸是1,所以为了使它们合适我们降低他们scale到\\frac{2}{10} = \\frac{1}{5}。为此,我们可以设置每个点的local scale为Vector3.one属性除以5。

        for (int i = 0; i < 10; i++) {
			Transform point = Instantiate(pointPrefab);
			point.localPosition = Vector3.right * i;
			point.localScale = Vector3.one / 5f;
		}

将scene窗口切换到正交投影,你可以更好的观察方块的位置关系。点击scene窗口右上角的轴工具下面的标记可以切换正交和透视模式。

                           Small cubes, seen in orthographic scene window without skybox

为了将方块重新放到一起,把他们的position也除以5

point.localPosition = Vector3.right * i / 5f;

此时他们的区间是0--2。为了转换到-1 -- 1,在乘以向量前减1。

point.localPosition = Vector3.right * (i / 5f - 1f);

                                                           From −1 to 0.8.

现在,第一个方块的x坐标是-1,但是最后一个的x坐标是0.8。然而方块的尺寸是0.2。由于方块的中心点就是他的位置,第一个方块的左侧是-1.1,最后一个方块的右侧是0.9。为了准确的填充-1 -- 1的区间,我们将全部方块整体右移半个方块的距离。为此,我们可以在 i 被除之前加上0.5

point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);

                                                               Filling the −1–1 range.

1.7 将向量置于循环之上(Hoisting the Vectors out of the Loop)

虽然所有方块都有相同的scale,但我们在每个循环迭代里都重复计算了一遍。我们不必这么做,scale是不变的,我们可以在循环前计算他,将其存储在一个变量里,再循环里使用它。

    void Awake () {
		var scale = Vector3.one / 5f;
		for (int i = 0; i < 10; i++) {
			Transform point = Instantiate(pointPrefab);
			point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);
			point.localScale = scale;
		}
	}

我们也可以在循环前定义一个position的变量。由于我们是创建一个沿着x轴的线,我们只需要在循环里修改x坐标。因此我们不在需要乘以Vector3.right

        Vector3 position;
		var scale = Vector3.one / 5f;
		for (int i = 0; i < 10; i++) {
			Transform point = Instantiate(pointPrefab);
			//point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);
			position.x = (i + 0.5f) / 5f - 1f;
			point.localPosition = position;
			point.localScale = scale;
		}

这将产生一个编译错误,提示使用了一个为初始化的变量。这是因为我们没有设置position的Y和Z就将它负值给其他变量。我们可以通过将position设为零向量修复这个问题。

        //Vector3 position;
		var position = Vector3.zero;
		var scale = Vector3.one / 5f;

1.8 用X定义Y(Using X to Define Y)

目前Y坐标一直是0,可以用函数 f(x)=0 来表示。为了显示一个不同的函数,我们必须在循环里面设置Y坐标。让我们先把Y和X相等,表示函数f(x)=x

        for (int i = 0; i < 10; i++) {
			Transform point = Instantiate(pointPrefab);
			position.x = (i + 0.5f) / 5f - 1f;
			position.y = position.x;
			point.localPosition = position;
			point.localScale = scale;
		}

                                                                   Y equals X.

一个不太明显的函数是f(x)=x^{2},定义了一个最低点为0的抛物线

position.y = position.x * position.x;

                                                             Y equals X squared.

2 创建更多方块(Creating More Cubes)

虽然此时我们有了一个函数图形,但很丑。因为我们只使用了十个方块,使这条线看起来像不连续的方块。如果我们使用更多更小的方块,它看起来会好很多。

21 可变的分辨率(Variable Resolution)

使用一个固定的数量不如将其改为可配置的,为此我们在Graph中添加一个序列化整型字段。给他一个默认值10,也就是我们当前使用的值。

    [SerializeField]
	Transform pointPrefab;

	[SerializeField]
	int resolution = 10;

                                                              Configurable resolution.

现在我们就可以通过inspector修改图形的分辨率。但是并不是所有的整数都是有效的分辨率,它必须是正整数,我们可以为其限制一个范围。为此我们添加一个Range 属性(attribute )。

    [SerializeField, Range]
	int resolution = 10;

inspector会检查字段是否有 Range 属性,如果有,它会限制其值并显示一个滑动条。然而它也需要知道允许的范围。所以 Range 需要两个参数--就像函数--最小值和最大值。我们使用10和100

    [SerializeField, Range(10, 100)]
	int resolution = 10;

                                                         Resolution slider set to 50.

2.2 可修改实例化(Variable Instantiation)

修改Awake中的循环,将数量改为我们的分辨率字段

        for (int i = 0; i < resolution; i++) {
			…
		}

我们还需要修改方块的scale和position来使其保持在-1 -- 1区间内。每次迭代时,我们做的间隔现在是2除以分辨率。用一个变量保存这个值并使用它计算方块的x坐标。

        float step = 2f / resolution;
		var position = Vector3.zero;
		var scale = Vector3.one * step;
		for (int i = 0; i < resolution; i++) {
			Transform point = Instantiate(pointPrefab);
			position.x = (i + 0.5f) * step - 1f;
			…
		}

                                                                Using resolution 50.

2.3 设置父物体(Setting the Parent)

使用50分辨率进入play模式后会产生很多个方块。这些点现在都是跟对象,但将他们作为图形对象的子对象是很有意义的。通过调用Transform组件的SetParent函数并将父物体的Transform.传递给它,我们可以在实例化点后建立这个关系。我们可以通过Graph的transform属性得到图形对象的Transform组件。在循环块的最后完成这些。

        for (int i = 0; i < resolution; i++) {
			…
			point.SetParent(transform);
		}

                                                      Points are children of the graph.

当建立一个新的父子关系时,Unity会试图保持子物体原来的世界坐标的位置、旋转和缩放。在我们例子中不需要这样,所以我们传递false作为SetParent的第二个参数。

point.SetParent(transform, false);

3 给图像上色(Coloring the Graph)

一个白色的图像并不好看。我们可以使用另一个颜色,但那也很没趣。让点的位置决定其颜色会有趣的多。

改变颜色的一个直接的方式就是设置它的材质的颜色属性。我们可以在循环里做这些。由于每个方块的颜色都不一样,这意味着每个对象都有一个唯一的材质实例。同时当图形做动画时我们也要一直修改那些材质。这虽然有效,但却没有效率。如果有一个材质能直接使用位置来设置它的颜色就好了,但不幸的是,Unity并没有这样的材质。所以让我们自己做一个。

3.1 创建一个 Surface Shader

GPU运行着色器来渲染3D对象。Unity的材质资源决定了使用哪个着色器,并允许配置它的属性。我们需要创建一个自定义着色器来实现我们想要的功能。通过 Assets / Create / Shader / Standard Surface Shader  创建一个并命名为Point Surface.

                               Shader grouped with prefab in Point folder, one and two column layout.

我们现在拥有了一个着色器资源,你可以像脚本一样打开它。我们的着色器文件包含了定了表面着色器的代码,其语法不同于c#。它包含了一个表面着色器的模板,但我们全删掉并从创建一个最简单的着色器开始。

Unity有其自己的着色器语法,总体上有点像c#但其实是不同语言的组合。它的开头是Shader 关键字,紧接着是一个定义了着色器在菜单位置的字符串。字符串在双引号里面。然后是着色器内容的代码块。

Shader "Graph/Point Surface" {}

着色器可以有多个子着色器,每个由SubShader 关键字加代码段定义。我们只需要一个。

Shader "Graph/Point Surface" {

	SubShader {}
}

子着色器下面我们还想添加一个标准漫反射着色器作为回滚,写作FallBack "Diffuse"。

Shader "Graph/Point Surface" {
	
	SubShader {}
	
	FallBack "Diffuse"
}

表面着色器的子着色器需要一个由CG和HLSL两种语言混合的代码段,这段代码必须被CGPROGRAM 和ENDCG 关键字包起来。

    SubShader {
		CGPROGRAM
		ENDCG
	}

第一个需要声明的是一条编译指令,被叫做progma。它的写法是#pragma后面跟着一个指令。我们的例子中是 #pragma surface ConfigureSurface Standard fullforwardshadows ,这条指令命令着色器编译器生成一个带有标准光照和阴影的表面着色器。ConfigureSurface 就是我们马上要创建的,用来配置着色器的函数。

        CGPROGRAM
		#pragma surface ConfigureSurface Standard fullforwardshadows
		ENDCG

接着我们添加#pragma target 3.0 指令,它指定了最低的着色器目标等级和质量。

        CGPROGRAM
		#pragma surface ConfigureSurface Standard fullforwardshadows
		#pragma target 3.0
		ENDCG

我们打算基于位置给我们的点上色。因此我们要为我们的配置函数定义一个输入结构体。它被写作struct Input后面跟着一个代码块和分号。在代码块中我们声明一个字段,float3 worldPos。它将包含被渲染的东西的世界坐标。float3 在shader中等同于Vector3 结构体。

        CGPROGRAM
		#pragma surface ConfigureSurface Standard fullforwardshadows
		#pragma target 3.0

		struct Input {
			float3 worldPos;
		};
		ENDCG

接着我们定义我们的ConfigureSurface 函数,它是一个返回空并有两个参数的函数。第一个是一个输入参数,也就是我们刚定义的Input 类型的参数。第二个参数是表面配置数据,类型是SurfaceOutputStandard  。

        struct Input {
			float3 worldPos;
		};

		void ConfigureSurface (Input input, SurfaceOutputStandard surface) {}

第二个参数前面必须有一个inout 关键字,说明它同时被传入函数并作为结果传出。

void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {}

现在我们为着色器创建一个材质,命名为Point Surface 。为它设置我们的着色器。

                                                           Point surface material.

目前我们的材质还是无光泽的黑色。我们可以在我们的配置函数中设置surface.Smoothness=0.5,以使其看起来更像默认材质。在写着色器代码时,我们不必再float类型前添加 f 后缀。

        void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {
			surface.Smoothness = 0.5;
		}

现在材质看起来有光泽了。在inspector 的顶部有个小的预览窗口,在底部还有一个可缩放的预览窗口。

                                             Material preview with average smoothness.

我们也可以使光滑度可配置化,比如添加一个字段并在函数中使用。默认风格是下划线前缀加首字母大写,我们就是用_Smoothness

		float _Smoothness;

		void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {
			surface.Smoothness = _Smoothness;
		}

为了在editor中显示配置项,我们必须在着色器顶部,也就是子着色器上方添加一个Properties 块。在里面写下_Smoothness ,跟着是("Smoothness", Range(0,1)) = 0.5 。这会暴露出一个Smoothness 标签,是一个0 -- 1 范围的滑动条,默认值是0.5

Shader "Graph/Point Surface" {

	Properties {
		_Smoothness ("Smoothness", Range(0,1)) = 0.5
	}
	
	SubShader {
		…
	}
}

                                                          Configurable smoothness.

将我们的方块的材质换成这个材质,这些点会变成黑色。

                                                                     Black points.

3.2 基于世界坐标着色(Coloring Based on World Position)

要调整点的颜色我们必须修改surface.Albedo 。由于反射率(albedo)和世界坐标都是三维的,我们可以直接将位置赋值给反射率。

		void ConfigureSurface (Input input, inout SurfaceOutputStandard surface) {
			surface.Albedo = input.worldPos;
			surface.Smoothness = _Smoothness;
		}

                                                                 Colored points.

现在世界坐标的X就控制点的红色,Y控制绿色,Z控制蓝色。但是我们图形的X域是-1 -- 1,而颜色的负值没有任何意义。因此我们要将点映射到合适的区间。

surface.Albedo = input.worldPos * 0.5 + 0.5;

为了更好的观察颜色是否正确,我们将Graph.Awake修改为函数f(x)=x^3,此时Y的区间也是-1到1

position.y = position.x * position.x * position.x;

                                                                     X cubed, bluish.

结果出现蓝色是因为所有方块的Z坐标都是0,这导致他们的蓝色值是0.5。我们可以在设置反射率时只包含红色和绿色以消除蓝色的影响。在着色器中,只是用 input.worldPos.xy 来给 surface.Albedo.rg 赋值。这样蓝色值就一直是0。

surface.Albedo.rg = input.worldPos.xy * 0.5 + 0.5;

因为红色加绿色是黄色,所以点从左下角的黑色开始,逐渐变绿因为Y的增长快于X,然后变黄因为X追了上来,然后变橙色因为X快速增长,最后在右上变为明黄色。

                                                     X cubed, from green to yellow.

3.3 通用渲染管线(Universal Render Pipeline)

除了默认渲染管线,Unity还有通用和高清渲染管线,简称URP和HDRP。两种渲染管线有不同的特点和局限。当前的默认渲染管线是功能化的(functional),但是他的特征集是固定的(its feature set is frozen)。数年内URP可能将变成默认的,所以让我们将图形改为URP。

如果你还没有使用URP,在 package manager 安装Universal RP package 。我这里是10.4.0

                                                            URP package installed.

这并不会使Unity自动使用URP。我们首先要通过Assets / Create / Rendering / Universal Render Pipeline / Pipeline Asset (Forward Renderer) 创建一个资源,我命名为URP。同时还会自动创建一个renderer资源,我这里命名为URP_Renderer

                                   URP assets in separate folder, one and two column layout.

在 project settings 的 Graphics 部分,将URP资源赋值给Scriptable Renderer Pipeline Settings

                                                                  Using the URP.

若要换回默认渲染管线也很简单,设置Scriptable Renderer Pipeline Settings 为none 即可。注意渲染管线只能在editor模式切换,不能打包后切换。

3.4 创建一个Shader Graph(Creating a Shader Graph)

我们的材质只能在默认渲染管线下工作,所以使用URP时我们的材质被替换为Unity的默认错误材质,不透明的紫红色。

                                                      Cubes have become magenta.

我们必须另创建一个URP着色器。我们可以自己写一个,但那比较困难并且当URP升级到新版后可能会不支持。最好的方式是使用Unity's shader graph package 来可视化的编辑着色器。URP依赖于这个包,所以它会自动的和URP一起安装。

 通过Assets / Create / Shader / Universal Render Pipeline / Lit Shader Graph 创建一个新的shader graph,命名为Point URP

                                 Point URP shader graph asset, one and two column layout.

双击project 窗口里的graph资源或点击inspector窗口的 Open Shader Editor 按钮打开 shader graph 窗口,我们可能会看到凌乱的节点和面板。那些是 黑板(blackboard),图形检视( graph inspector),和主预览面板(main preview panels),这些都可以缩放并通过工具栏隐藏。还有两个连接点:Vertex node 和 Fragment node。这两个是用来配置shader graph的输出的。

                                         Default lit shader graph with everything visible.

一个shader graph是由多个代表数据和操作的节点构成的。目前Fragment 节点的Smoothness 值是0.5。为了让其成为一个可配置的属性,点击Point URP 的底板(backboard )的加号按钮,选择float 命名为Smoothness 。这将在黑板添加一个代表属性的圆按钮。选中它,切换到graph inspector的Node Settings 表单来看一下可配置的属性。

                                            Smoothness property with default settings.

Reference 是属性在内部的命名。这和我们如何命名表面着色器代码的_Smoothness 属性字段是一样的,所以让我们在这里也做出同样的命名。然后设置default 为0.5。要确保Exposed 是被选中的,因为它控制着材质是否显示着色器的属性。最后,将Mode 改为 Slider,来使其显示为一个滑动条。

                                                    Smoothness property configured.

接着,将圆按钮拖拽到graph的一个空地,以添加一个smoothness 节点。将它和PRB Master 节点的Smoothness 输入链接起来,通过拖拽其中一个的圆点到另一个。

                                                    Smoothness connected.

现在你可以通过Save Asset 工具栏按钮保存graph并创建一个材质使用它。材质的shader菜单项是Shader Graphs / Point URP 。然后让Point 预制使用这个新的材质。

                                             Material for URP using our shader graph.

3.5 节点编程(Programming with Nodes)

为了给点着色我们必须先建一个位置节点。在空白处打开上下文菜单并选择New Node 。选择Input / Geometry / Position 或者搜索Position

                                                            World position node.

我们现在拥有了一个位置节点,默认被设置为世界空间。将鼠标放在它上面时会出现一个向上的箭头,点击箭头可以收起预览窗口。

用同样的方法创建一个Multiply 和一个Add 节点。使用他们将位置的XY乘以0.5然后再加0.5,同时Z设为0。这些节点会根据与其相连的节点适配类型。所以先将节点相连并填写他们的输入,然后将结果连接到FragmentBase Color input

                                                      Colored shader graph.

如果你想Multiply 和  Add 看起来更紧凑,将鼠标放到它们上面,点击右上角出现的箭头,会隐藏他们的输入和未连接到其他节点的输出。这会使画面更整洁。你还可以通过Vertex 和Fragment 节点的上下文菜单删除他们的组件。这样你就可以隐藏所有你不关心的东西并使它们保持默认值。

                                                             Compacted shader graph.

保存着色器资源后,我们现在拥有了和使用默认渲染管线时一样的彩色的点。另外,一个 debug updater 还会出现在play模式的 DontDestroyOnLoad下面,这是用来调试URP的,可以忽略。

                                               URP debug updater in play mode.

现在你既可以使用默认渲染管线也可以使用URP。在切换渲染管线后你必须也切换点的材质,否则它将是紫红色的。

4 使图形动起来(Animating the Graph)

显示一个静态的图形很有用,但是一个动态的图形看起来更有趣。所以让我们添加动画函数。这可以通过添加时间参数来实现,使用函数f(x,t)代替f(x),t是时间。

4.1 跟踪点(Keeping Track of the Points)

为了使图形动起来,我们必须根据时间调整点。我们可以通过删除所有点并在每个update中再创建,但是这非常的低效。比较好的方法是保留这些点,并在update中调整他们的位置。为此我们需要使用一个字段来引用我们的点。添加一个 points 字段,类型是  Transform.

    [SerializeField, Range(10, 100)]
	int resolution = 10;
	
	Transform points;

这个字段允许我们引用一个点,但是我们需要所有的点。通过在其类型后天添加中括号,我们可以将我们的字段转为数组。

Transform[] points;

points字段现在是一个数组的引用,它的每个元素都是 Transform类型。数组是一个对象,而不是值。我们必须创建出一个同类型的对象并让我们的字段引用它。

        points = new Transform[];
		for (int i = 0; i < resolution; i++) {
			…
		}

当创建数组时,我们需要指定长度。

points = new Transform[resolution];

现在我们可以用我们的点填充数组。通过索引来访问数组的每个元素。索引从0开始。

        points = new Transform[resolution];
		for (int i = 0; i < resolution; i++) {
			Transform point = Instantiate(pointPrefab);
			points[i] = point;
			…
		}

因为数组的长度和分辨率是一样的,我们可以使用长度约束我们的循环。

        points = new Transform[resolution];
		for (int i = 0; i < points.Length; i++) {
			…
		}

4.2 更新点(pdating the Points)

为了每帧调整图形,我们需要在update函数中设置点的Y坐标。所以我们不在Awake中计算他们,我们仍然可以在Awake中设置X坐标因为我们不会修改他们。

		for (int i = 0; i < points.Length; i++) {
			Transform point = points[i] = Instantiate(pointPrefab);
			position.x = (i + 0.5f) * step - 1f;
//			position.y = position.x * position.x * position.x;
			…
		}

在Update函数中添加一个代码块为空的for循环

	void Awake () {
		…
	}
	
	void Update () {
		for (int i = 0; i < points.Length; i++) {}
	}

我们以获取当前数组元素的引用并将其存到变量里作为开始。

		for (int i = 0; i < points.Length; i++) {
			Transform point = points[i];
		}

然后我们获取点的本地坐标并存到变量里

		for (int i = 0; i < points.Length; i++) {
			Transform point = points[i];
			Vector3 position = point.localPosition;
		}

现在我们可以基于X设置Y坐标,像之前一样。

		for (int i = 0; i < points.Length; i++) {
			Transform point = points[i];
			Vector3 position = point.localPosition;
			position.y = position.x * position.x * position.x;
		}

因为位置是一个结构体,我们只修改了本地变量的值。若要点应用这些修改,我们必须再将他们负值给点的位置。

		for (int i = 0; i < points.Length; i++) {
			Transform point = points[i];
			Vector3 position = point.localPosition;
			position.y = position.x * position.x * position.x;
			point.localPosition = position;
		}

4.3 显示一个正弦波形(Showing a Sine Wave)

到目前为止,在play模式中,我们的点每帧都会被重新放置。我们还观察不到是因为每次都别放置到同样的位置。我们必须将其与时间联系起来。然而简单的增加时间会使函数上升并迅速的从视野中消失。为避免这样我们必须使用一个在固定区间变换的函数。正弦函数就非常理想,所以我们使用f(x)=sin(x)。我们可以使用Mathf.Sin函数来计算。

position.y = Mathf.Sin(position.x);

                                                      The sine of X, from −1 to 1.

正弦波形是在-1和1之间震动。它每2\\pi单位重复一次,也就是说它的周期差不多是6.28。由于我们的X坐标是在-1 和 1之间,我们现在能看到的还不到三分之一周期。为了看一个整周期我们将X乘以\\pi

position.y = Mathf.Sin(Mathf.PI * position.x);

                                                                   The sine of πX.

为了让函数动起来,将当前时间加到X。当前时间就是 Time.time 。

position.y = Mathf.Sin(Mathf.PI * (position.x + Time.time));

因为Time.time 每个循环迭代都是一样的,所以我们将其放到循环上面。

		float time = Time.time;
		for (int i = 0; i < points.Length; i++) {
			Transform point = points[i];
			Vector3 position = point.localPosition;
			position.y = Mathf.Sin(Mathf.PI * (position.x + time));
			point.localPosition = position;
		}

4.4 Clamping the Colors

因为正弦的振幅是1, 所以我们点的位置的最低点和最高点是-1 和 1。然而,由于点是有尺寸的方块,所以他们会超过一点这个区间。因此我们获得的颜色的绿色部分会是负数或超过1。让我们将颜色值限制在0--1。

在表面着色器中我们可以通过添加saturate 函数实现。这是一个特殊的函数,它限制输入的参数到0--1,这是一个常见的函数。

surface.Albedo.rg = saturate(input.worldPos.xy * 0.5 + 0.5);

在shader graph 中使用 Saturate node.

以上是关于创建图形(Building a Graph)的主要内容,如果未能解决你的问题,请参考以下文章

创建图形(Building a Graph)

A revolutionary architecture for building a distributed graph

TF Graph与代码不对应

House Building---hdu5538(求表面积水题)

graph engine

如何使用graph_tool获得晶格图的X Y坐标