构建可视化数学函数图形
- 创立预制件。
- 实例化多个多维数据集。
- 显现数学函数。
- 创立表面着色器和着色器图。
- 为图形添加动画作用。
在这个教程中,咱们将带你进入数学世界,向你展现怎么运用Unity构建可视化数学函数图形。你将学习怎么创立预制件和多维数据集,并运用表面着色器和着色器图显现数学函数。咱们还将向你展现怎么运用脚本操控图形的动画作用,使你的图形更加生动。 这次咱们将运用游戏目标来构建图形,以便咱们能够显现数学公式。咱们还将使函数与时间相关,然后创立一个动画图。
本教程是运用 Unity 2020.3.6f1 制作的。
运用立方体显现正弦波。
创立多维数据集行
编程时,对数学的良好了解至关重要。在最基本的层面上,数学是对代表数字的符号的操作。求解方程归结为重写一组符号,使其成为另一组(一般更短)的符号集。数学规矩决议了怎么进行这种重写。
例如,咱们有函数 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 + 5 x 3 − 8 x 2 + 3 x 更难。咱们能够写下一些输入输出对,但这可能不会让咱们很好地把握它所代表的映射。咱们需求很多点,靠得很近。这终究将变成一个难以解析的数字海洋。相反,咱们能够将这些对解释为办法为 [ x f ( x ) ] 的二维坐标。这是一个 2D 矢量,其间顶部数字表明 X 轴上的水平坐标,底部数字表明 Y 轴上的笔直坐标。换句话说, y = f ( x )。咱们能够在表面上制作这些点。 假如咱们运用足够多的十分接近的点,咱们终究会得到一条线。结果是一个图形。
在 −2 和 2 之间运用 x 的图形,用 Desmos 制作。
查看图形能够快速让咱们了解函数的行为办法。这是一个便利的工具,所以让咱们在 Unity 中创立一个。咱们将从一个新项目开端。
预制件
经过在适当的坐标处放置点来创立图形。为此,咱们需求一个点的 3D 可视化。为此,咱们将简单地运用 Unity 的默许立方体游戏目标。向场景添加一个并命名为 Point 。删去其 [BoxCollider](http://docs.unity3d.com/Documentation/ScriptReference/BoxCollider.html)
组件,由于咱们不会运用物理引擎。
咱们将运用自界说组件来创立此多维数据集的许多实例并正确定位它们。为此,咱们将立方体转换为游戏目标模板。将多维数据集从层次结构窗口拖到项目窗口中。这将创立一个称为预制件的新资源。它是存在于项目中的预制游戏目标,而不是存在于场景中。
点预制件资源,一列和两列布局。
咱们用于创立预制件的游戏目标仍然存在于场景中,但现在是一个预制件实例。它在层次结构窗口中有一个蓝色图标,右侧有一个箭头。其查看器的标题还指示它是一个预制件,并显现更多控件。方位和旋转现在以粗体文本显现,表明实例的值掩盖预制件的值。您对实例所做的任何其他更改也将以这种办法指示。
点预制件实例。
挑选预制件资源时,其查看器将显现其根游戏目标和一个用于翻开预制件的大按钮。
预制件资源查看器。
单击 Open Prefab 按钮将使场景窗口显现一个只包括预制件目标层次结构的场景。您还能够经过实例的 Open 按钮、层次结构窗口中实例周围的向右箭头或双击项目窗口中的资源来抵达那里。当预制件具有杂乱的层次结构时,这很有用,但关于咱们的简单点预制件来说,状况并非如此。
预制件的层次结构窗口。
您能够经过层次结构窗口中预制件名称左边的箭头退出预制件的场景。
预制件是装备游戏目标的便捷办法。假如更改预制件资源,则任何场景中的一切实例都将以相同的办法更改。例如,更改预制件的份额也会更改仍在场景中的立方体的份额。可是,每个实例都运用自己的方位和旋转。此外,还能够修改游戏目标实例,这将掩盖预制件的值。请注意,预制件和实例之间的联系在播映方式下会中止。
咱们将运用脚原本创立预制件的实例,这意味着咱们不再需求场景中当时存在的预制件实例。因而,请经过 Edit / Delete ,指示的键盘快捷键或其层次结构窗口中的上下文菜单将其删去。
图形脚本
咱们需求一个 C# 脚原本生成带有点预制件的图形。创立一个并将其命名为 **Graph**
。
“脚本”文件夹中的图形 C# 资源。
咱们从扩展 [MonoBehaviour](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.html)
的简单类开端,以便它能够用作游戏目标的组件。为其供给一个可序列化的字段,以保存对用于实例化点的预制件的引证,名为 pointPrefab
。咱们需求访问 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
组件来定位点,因而请将其设置为字段的类型。
using UnityEngine;
public class Graph : MonoBehaviour {
[SerializeField]
Transform pointPrefab;
}
在场景中添加一个空的游戏目标,并将其命名为 Graph 。保证其方位和旋转为零,而且其份额为 1。将咱们的 **Graph**
组件添加到此目标。然后将咱们的预制件资源拖到图形的 Point Prefab 字段上。它现在包括对预制件的 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
组件的引证。
参考预制件制作游戏目标。
实例化预制件
实例化游戏目标是经过 [Object](http://docs.unity3d.com/Documentation/ScriptReference/Object.html).[Instantiate](http://docs.unity3d.com/Documentation/ScriptReference/Object.Instantiate.html)
办法完结的。这是 Unity 的 [Object](http://docs.unity3d.com/Documentation/ScriptReference/Object.html)
类型的揭露可用办法, **Graph**
经过扩展 [MonoBehaviour](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.html)
直接承继了该办法。 [Instantiate](http://docs.unity3d.com/Documentation/ScriptReference/Object.Instantiate.html)
办法克隆作为参数传递给它的任何 Unity 目标。关于预制件,它将导致将实例添加到当时场景中。让咱们在 **Graph**
组件唤醒时履行此操作。
public class Graph : MonoBehaviour {
[SerializeField]
Transform pointPrefab;
void Awake () {
Instantiate(pointPrefab);
}
}
假如咱们现在进入播映方式, Point 预制件的单个实例将在世界原点生成。它的名称与预制件的名称相同,并附加了 (Clone) 。
实例化的预制件,在场景窗口中向下查看 Z 轴。
要将点放置在其他地方,咱们需求调整实例的方位。 [Instantiate](http://docs.unity3d.com/Documentation/ScriptReference/Object.Instantiate.html)
办法为咱们供给了对它创立的任何内容的引证。由于咱们给了它一个 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
组件的引证,这便是咱们得到的报答。让咱们用一个变量来盯梢它。
void Awake () {
Transform point = Instantiate(pointPrefab);
}
在前面的教程中,咱们经过为枢轴的 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
的 localRotation
特点分配一个四元数来旋转时钟臂。更改方位的工作办法相同,仅仅咱们有必要将 3D 矢量分配给 localPosition
特点。
运用 [Vector3](http://docs.unity3d.com/Documentation/ScriptReference/Vector3.html)
结构类型创立 3D 矢量。例如,让咱们将点的 X 坐标设置为 1,将其 Y 和 Z 坐标保存为零。 [Vector3](http://docs.unity3d.com/Documentation/ScriptReference/Vector3.html)
有一个 right
特点,它为咱们供给了这样一个向量。运用它来设置点的方位。
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.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;
point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * 2f;
两个实例,X 坐标为 1 和 2。
循环代码
让咱们创立更多的点,直到咱们有十个点。咱们能够再重复八次相同的代码,但这将是十分低效的编程。理想状况下,咱们只编写一个点的代码,并指示程序多次履行它,略有不同。
**while**
句子可用于使代码块重复。将其应用于办法的前两个句子,并删去其他句子。
void Awake () {
while {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;
}
}
**while**
要害字后有必要跟圆括号内的表达式。 **while**
后边的代码块只需在表达式的核算结果为 true 时才会履行。之后,程序将循环回 **while**
句子。假如此刻表达式再次核算为 true,则将再次履行代码块。重复此操作,直到表达式的核算结果为 false。然后程序跳过 **while**
句子后边的代码块,并持续在其下方。
所以咱们有必要在 **while**
之后添加一个表达式。咱们有必要当心保证循环不会永远重复。无限循环会导致程序卡住,需求用户手动中止。编译的最安全的表达式仅仅 **false**
。
while (false) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;
}
限制循环能够经过盯梢咱们重复代码的次数来完结。咱们能够运用整数变量来盯梢这一点。它的类型是 **int**
。它将包括循环的迭代编号,因而将其命名为 i
。它的初始值为零。为了能够在 **while**
表达式中运用它,有必要在它上面界说它。
int i = 0;
while (false) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;
}
每次迭代,经过将数字设置为本身加 1,将数字添加 1。
int i = 0;
while (false) {
i = i + 1;
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;
}
现在, i
在第一次迭代开端时变为 1,在第2次迭代开端时变为 2,依此类推。可是 **while**
表达式在每次迭代之前都会被核算。因而,在第一次迭代之前 i
为零,在第2次迭代之前为1,依此类推。所以在第十次迭代之后 i
是十。此刻,咱们希望中止循环,因而其表达式的核算结果应为 false。换句话说,只需 i
小于 10,咱们就应该持续。在数学上,这表明为 i < 10 i < 10。它在代码中编写相同,运用 <
小于运算符。
int i = 0;
while (i < 10) {
i = i + 1;
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;
}
现在咱们将在进入游戏方式后取得十个立方体。但他们终究都处于相同的方位。要沿 X 轴将它们排成一行,请将 right
向量乘以 i
。
point.localPosition = Vector3.right * i;
沿 X 轴连续创立十个立方体。
请注意,现在第一个立方体的 X 坐标为 1,最后一个立方体的 X 坐标为 10。让咱们改变这一点,以便咱们从零开端,将第一个立方体定位在原点。咱们能够将一切点向左移动一个单位,办法是将 right
乘以 (i - 1)
而不是 i
.可是,咱们能够经过在块末尾、乘法后而不是开头添加 i
来跳过额外的减法。
while (i < 10) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
i = i + 1;
}
简练的语法运用
由于循环一定次数十分普遍,因而坚持循环代码简练很便利。一些句法糖能够协助咱们处理这个问题。
首要,让咱们考虑添加迭代次数。当履行 x = x * y
办法的操作时,能够将其缩短为 x *= y
。这适用于一切作用于两个操作数的运算符。
i += 1;
更进一步,当将数字递加或递减 1 时,能够缩短为 ++x
或 --x
。
++i;
赋值句子的一个特点是它们也能够用作表达式。这意味着您能够编写相似 y = (x += 3)
的内容。这会将 x
添加 3,并将结果也分配给 y
。这表明咱们能够在 **while**
表达式中添加 i
,然后缩短代码块。
while (++i < 10) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
}
可是,现在咱们在比较之前而不是之后递加 i
,这将导致更少的迭代。特别是关于这种状况,递加和递减运算符也能够放在变量之后,而不是变量之前。该表达式的结果是更改之前的原始值。
while (i++ < 10) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
}
尽管 **while**
句子适用于一切类型的循环,但还有一种代替语法特别合适遍历规划。它是 **for**
循环。它的工作办法相似于 **while**
,仅仅迭代器变量声明及其比较都包括在圆括号中,用分号分隔。
for (int i = 0; i++ < 10) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
}
这将产生编译器过错,由于还有第三部分用于在另一个分号之后递加迭代器,使其与比较分开。此部分在每次迭代结束时履行。
for (int i = 0; i < 10; i++) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
}
更改域
现在,咱们的点被赋予 X 坐标 0 到 9。运用函数时,这不是一个便利的规划。一般,0–1 的规划用于 X。或许,当运用以零为中心的函数时,规划为 −1–1。让咱们相应地从头定位咱们的观点。
沿两个单位长的线段放置十个立方体将导致它们重叠。为了避免这种状况,咱们将缩小它们的规划。默许状况下,每个立方体在每个维度中的巨细为 1,因而为了使它们合适,咱们有必要将它们的份额减小到 1/5 。咱们能够经过将每个点的局部份额设置为 [Vector3](http://docs.unity3d.com/Documentation/ScriptReference/Vector3.html).one
特点除以 5 来做到这一点。除法是运用 /
斜杠运算符完结的。
for (int i = 0; i < 10; i++) {
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
point.localScale = Vector3.one / 5f;
}
经过将场景窗口切换到忽略透视的正交投影,能够更好地了解立方体的相对方位。单击场景窗口右上角轴微件下的标注可在正交方式和透视方式之间切换。假如经过场景窗口工具栏关闭天空盒,则白色立方体也更简单看到。
小立方体,在没有天空盒的正交场景窗口中看到。
要使立方体再次组合在一起,请将它们的方位也除以五。
point.localPosition = Vector3.right * i / 5f;
这使得它们掩盖 0-2 规划。要将其转换为 −1–1 规划,请在缩放向量之前减去 1。运用圆括号指示数学表达式的操作次序。
point.localPosition = Vector3.right * (i / 5f - 1f);
从 −1 到 0.8。
现在第一个立方体的 X 坐标为 −1,而最后一个立方体的 X 坐标为 0.8。可是,立方体巨细为 0.2。由于立方体以它的方位为中心,第一个立方体的左边为 −1.1,而最后一个立方体的右侧为 0.9。为了用咱们的立方体整齐地填充 −1–1 规划,咱们有必要将它们向右移动半个立方体。这能够经过在除法之前将 0.5 添加到 i
来完结。
point.localPosition = Vector3.right * ((i + 0.5f) / 5f - 1f);
填充 −1–1 规划。
将向量提升到循环之外
尽管一切立方体都具有相同的份额,但咱们在循环的每次迭代中都会再次核算它。咱们不必这样做,规划是不变的。相反,咱们能够在循环之前核算一次,将其存储在 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;
}
}
咱们还能够为循环之前的方位界说一个变量。当咱们沿着 X 轴创立一条线时,咱们只需求调整循环内方位的 X 坐标。所以咱们不再需求乘以 [Vector3](http://docs.unity3d.com/Documentation/ScriptReference/Vector3.html).right
。
Vector3 position;
var scale = Vector3.one / 5f;
for (int i = 0; i < 10; i++) {
Transform point = Instantiate(pointPrefab);
position.x = (i + 0.5f) / 5f - 1f;
point.localPosition = position;
point.localScale = scale;
}
这将导致编译器过错,抱怨运用未赋值的变量。发生这种状况是由于咱们正在将 position
分配给某些内容,而咱们没有设置其 Y 和 Z 坐标。咱们能够经过开始将 position
设置为零向量,经过将 [Vector3](http://docs.unity3d.com/Documentation/ScriptReference/Vector3.html).zero
分配给它来处理此问题。
var position = Vector3.zero;
var scale = Vector3.one / 5f;
运用 X 界说 Y
这个主意是,咱们的立方体的方位被界说为 [ x f ( x ) 0 ],所以咱们能够运用它们来显现函数。此刻 Y 坐标始终为零,表明普通函数 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 等于 X。
一个不太明显的函数是 f ( x ) = x 的2次方 ,它界说了一条抛物线,其最小值为零。
position.y = position.x * position.x;
Y 等于 X 的平方。
创立更多多维数据集
虽然咱们在这一点上有一个功能图,但它很丑恶。由于咱们只运用十个立方体,所以建议的行看起来十分块状和离散。假如咱们运用更多更小的立方体,看起来会更好。
可装备不同分辨率
咱们能够使其可装备,而不是运用固定数量的多维数据集。要完成这一点,请将分辨率的可序列化整数字段添加到 **Graph**
。给它一个默许值 10,这便是咱们现在运用的。
[SerializeField]
Transform pointPrefab;
[SerializeField]
int resolution = 10;
可装备的分辨率。
现在,咱们能够经过查看器更改图形的分辨率来调整图形的分辨率。可是,并非一切整数都是有效的分辨率。至少他们有必要是积极的。咱们能够指示查看员强制履行咱们的处理方案规划。这是经过将 [Range](http://docs.unity3d.com/Documentation/ScriptReference/RangeAttribute.html)
特点附加到它来完结的。咱们能够将 resolution
的两个特点放在它们自己的方括号之间,或许组合在一个逗号分隔的特点列表中。让咱们做后者。
[SerializeField, Range]
int resolution = 10;
查看器查看字段是否附加了 [Range](http://docs.unity3d.com/Documentation/ScriptReference/RangeAttribute.html)
特点。假如是这样,它将束缚该值并显现一个滑块。可是,要履行此操作,它需求知道允许的规划。因而, [Range](http://docs.unity3d.com/Documentation/ScriptReference/RangeAttribute.html)
需求两个参数(如办法)来表明最小值和最大值。让咱们运用 10 和 100。
[SerializeField, Range(10, 100)]
int resolution = 10;
分辨率滑块设置为 50。
变量实例化
要运用装备的分辨率,咱们有必要更改实例化的多维数据集数量。现在,迭代量由 resolution
而不是始终 10
束缚,而不是在 [Awake](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Awake.html)
中循环固定次数。因而,假如分辨率设置为 50,咱们将在进入播映方式后取得 50 个立方体。
for (int i = 0; i < resolution; i++) {
…
}
咱们还有必要调整立方体的份额和方位,以使它们坚持在 −1-1 域内。咱们现在每次迭代有必要履行的每个步骤的巨细除以分辨率。将此值存储在变量中,并运用它来核算立方体的份额及其 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;
…
}
设置父级
以分辨率 50 进入播映方式后,许多实例化的立方体会显现在场景中,因而也会显现在项目窗口中。
点是根目标。
这些点当时是根目标,但它们是图形目标的子目标是有意义的。咱们能够在实例化点后建立这种联系,办法是调用其 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
组件的 SetParent
办法,向其传递所需的父级 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
。咱们能够经过 **Graph**
的 transform
特点获取图形目标的 [Transform](http://docs.unity3d.com/Documentation/ScriptReference/Transform.html)
组件,它承继自 [Component](http://docs.unity3d.com/Documentation/ScriptReference/Component.html)
。在循环块的末尾履行此操作。
for (int i = 0; i < resolution; i++) {
…
point.SetParent(transform);
}
点是图形的子项。
设置新的父目标后,Unity 将尝试将目标坚持在原始世界方位、旋转和缩放。在咱们的案例中,咱们不需求这个。咱们能够经过将 **false**
作为第二个参数传递给 SetParent
来宣布信号。
point.SetParent(transform, false);
上篇