数学曲面

  • 创立函数库。
  • 运用托付和枚举类型。
  • 运用网格显现2D函数。
  • 在三维空间中界说曲面。

它是构建图形教程的延续,所以咱们不会开端一个新项目。这一次,咱们将能够显现多个和更杂乱的功用。

【Unity】探索数学国际:运用Unity构建可视化数学函数图形(一)(上) – 掘金 ()

本教程运用Unity 2020.3.6f1制造。

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

组合几个波以创立杂乱曲面

函数库

完结上一个教程后,咱们有一个点图,显现了一个动画正弦波,而在播映形式。还能够示出其他数学函数。你能够改动代码,函数也会沿着改动。您乃至能够在Unity编辑器处于播映形式时履行此操作。履行将暂停,保存当时游戏状态,然后再次编译脚本,最终重新加载游戏状态并继续游戏。这称为热更新。不是所有的东西都能热更新,但咱们的图表能够。

虽然在播映形式期间更改代码可能很便利,但在多个功用之间来回切换并不是一种便利的办法。假如咱们能够经过图形的装备选项来更改功用,那就更好了。

库类

咱们能够在 **Graph** 中声明多个数学函数,但让咱们将该类专用于显现函数,使其不知道切当的数学方程。这是关注点的专门化和别离的一个比如。

创立一个新的 **FunctionLibrary** C#脚本,并将其放在 Scripts 文件夹中,紧挨着 **Graph** 。您能够运用菜单选项创立新资源或仿制并重命名 **Graph** 。在任何一种情况下,都要清除文件内容,并从运用 UnityEngine 开端,并声明一个不扩展任何内容的空的 **FunctionLibrary** 类。

using UnityEngine;
public class FunctionLibrary {}

这个类不会是一个组件类型。咱们也不会创立它的目标实例。相反,咱们将运用它来供给一个可揭露拜访的办法调集,这些办法表明数学函数,类似于Unity的 [Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html)

为了表明这个类不被用作目标模板,将其标记为静态,在 **class** 之前写入 **static** 关键字。

public static class FunctionLibrary {}

函数

咱们的第一个函数将是与 **Graph** 当时显现的相同的正弦波。咱们需求为它创立一个办法。这与创立 [Awake](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Awake.html)[Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) 办法的作业原理相同,仅仅咱们将其命名为 Wave

public static class FunctionLibrary {
void Wave () {}
}

默许情况下,办法是实例办法,这意味着有必要在目标实例上调用它们。为了让它们直接在类等级作业,咱们有必要将其标记为静态,就像 **FunctionLibrary** 自身相同。

static void Wave () {}

为了使它能够揭露拜访,也给予它一个 **public** 拜访修饰符。

public static void Wave () {}

该办法将表明咱们的数学函数 f(x,t)= sin((x + t))。这意味着它有必要发生一个成果,该成果有必要是一个浮点数。因而,函数的回来类型应该是 **float** ,而不是 **void**

public static float Wave () {}

接下来,咱们有必要将这两个参数添加到办法的参数列表中,就像数学函数相同。仅有的区别是咱们有必要在每个参数前面写上类型,即 **float**

public static float Wave (float x, float t) {}

现在咱们能够将核算正弦波的代码放入办法中,运用其 xt 参数。

public static float Wave (float x, float t) {
Mathf.Sin(Mathf.PI * (x + t));
}

最终一步是显式地指出办法的成果是什么。由于这是一个 **float** 办法,所以当它完结时,它有必要回来一个 **float** 值。咱们经过写 **return** 来表明这一点,后边跟着成果应该是什么,这是咱们的数学核算。

public static float Wave (float x, float t) {
return Mathf.Sin(Mathf.PI * (x + t));
}

现在能够在 **Graph**.[Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) 中调用这个办法,运用 position.xtime 作为其参数的参数。其成果可用于设置点的Y坐标,而不是显式的数学方程。

void Update () {
float time = Time.time;
for (int i = 0; i < points.Length; i++) {
Transform point = points[i];
Vector3 position = point.localPosition;
position.y = FunctionLibrary.Wave(position.x, time);
point.localPosition = position;
}
}

隐式运用类型

咱们将在 **FunctionLibrary** 中运用 [Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html).PI[Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html).Sin[Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html) 中的其他办法。假如咱们能够编写这些,而不必一向显式地说到类型,那就太好了。咱们能够经过在 **FunctionLibrary** 文件的顶部添加另一个 **using** 句子来完结这一点,其间额定的 **static** 关键字后跟显式的 UnityEngine.[Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html) 类型。这使得该类型的所有常量和静态成员都能够运用,而无需显式提及该类型自身。

using UnityEngine;
using static UnityEngine.Mathf;
public static class FunctionLibrary { … }

现在咱们能够经过省略 [Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html) 来缩短 Wave 中的代码。

public static float Wave (float x, float z, float t) {
return Sin(PI * (x + t));
}

第二个功用

让咱们添加另一个函数办法。这次咱们将运用多个正弦波来创立一个稍微杂乱一点的函数。开端仿制 Wave 办法并将其重命名为 MultiWave

public static float Wave (float x, float t) {
return Sin(PI * (x + t));
}
public static float MultiWave (float x, float t) {
return Sin(PI * (x + t));
}

咱们将保留已有的正弦函数,但添加一些额定的东西。为了简化操作,在回来之前将当时成果赋给一个 y 变量。

public static float MultiWave (float x, float t) {
float y = Sin(PI * (x + t));
return y;
}

添加正弦波杂乱度的最简略办法是添加另一个频率加倍的正弦波。这意味着它的变化速度是正弦函数的两倍,这是经过将正弦函数的参数乘以2来完结的。一起,咱们将这个函数的成果折半。这使新正弦波的形状与旧正弦波相同,但巨细折半。

float y = Sin(PI * (x + t));
y += Sin(2f * PI * (x + t)) / 2f;
return y;

这给出了数学函数

f(x,t)=sin((x+t))+sin(2(x+t))2f(x,t)= sin((x + t))+\frac {sin(2 (x + t))} {2}

。由于正弦函数的正负极值都是1和−1,因而这个新函数的最大值和最小值可能是1.5和−1.5。为了保证咱们坚持在-1-1规模内,咱们应该将总和除以1.5。

return y / 1.5f;

除法比乘法需求更多的作业,所以一般来说,乘法比除法更好。然而,像 1f / 2f2f * [Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html).PI 这样的常量表达式现已被编译器简化为一个数字。所以咱们能够重写代码,只在运行时运用乘法。咱们有必要保证首先运用操作次序和括号来削减常数部分。

y += Sin(2f * PI * (x + t)) * (1f / 2f);
return y * (2f / 3f);

咱们也能够直接写 0.5f 而不是 1f / 2f ,可是1.5的逆不能用十进制表明法精确地写,所以咱们将继续运用 2f / 3f ,编译器将其简化为具有最大精度的浮点表明。

y += 0.5f * Sin(2f * PI * (x + t));

现在运用这个函数代替 **Graph**.[Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) 中的 Wave ,看看它是什么姿态。

position.y = FunctionLibrary.MultiWave(position.x, time);

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

 两个正弦波之和

你能够说,一个较小的正弦波现在跟从一个较大的正弦波。咱们也能够让较小的一个沿着较大的一个滑动,例如经过将较大的波浪的时刻折半。成果将是一个函数,它不只跟着时刻的推移而滑动,而且会改动形状。现在需求四秒钟来重复该形式。

float y = Sin(PI * (x + 0.5f * t));
y += 0.5f * Sin(2f * PI * (x + t));

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

变形波

在编辑器中选择函数

接下来咱们能够做的是添加一些代码,以便能够操控 **Graph** 运用哪个办法。咱们能够运用滑块来完结这一点,就像图的分辨率相同。由于咱们有两个函数可供选择,因而咱们需求一个规模为0-1的可序列化整数字段。将其命名为 function ,以便它操控的内容清楚明了。

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

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

现在咱们能够在 [Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) 的循环中查看 function 。假如为零,则图形应显现 Wave 。为了做出选择,咱们将运用 **if** 句子,后边跟着一个表达式和一个代码块。它的作业原理类似于 **while** ,但它不会循环回去,因而该块要么被履行,要么被跳过。在这种情况下,测验是 function 是否等于零,这能够用 == 持平运算符来完结。

void Update () {
float time = Time.time;
for (int i = 0; i < points.Length; i++) {
Transform point = points[i];
Vector3 position = point.localPosition;
if (function == 0) {
position.y = FunctionLibrary.Wave(position.x, time);
}
point.localPosition = position;
}
}

咱们能够在if-block后边加上 **else** 和另一个块,假如测验失利,它就会被履行。在这种情况下,图形应显现 MultiWave

if (function == 0) {
position.y = FunctionLibrary.Wave(position.x, time);
}
else {
position.y = FunctionLibrary.MultiWave(position.x, time);
}

这使得能够经过图形的查看器操控功用,即便咱们处于播映形式。

纹波函数

让咱们向库中添加第三个函数,它能够发生类似波纹的效果。咱们经过使正弦波远离原点来创立它,而不是总是以相同的方向行进。咱们能够依据它到中心的间隔来做这件事,这是X的绝对值。在 [Mathf](http://docs.unity3d.com/Documentation/ScriptReference/Mathf.html).Abs 的帮助下,在一个新的 **FunctionLibrary**.Ripple 办法中只核算这个值。将间隔存储在 d 变量中,然后回来它。

public static float Ripple (float x, float t) {
float d = Abs(x);
return d;
}

为了显现它,将 **Graph**.function 的规模添加到2,并在 [Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) 中为 Wave 办法添加另一个块。咱们能够经过在 **else** 之后直接写入另一个 **if** 来链接多个条件块,因而它成为一个else-if块,当 function 等于1时应该履行。然后为涟漪添加一个新的 **else** 块。

[SerializeField, Range(0, 2)]
…
void Update () {
float time = Time.time;
for (int i = 0; i < points.Length; i++) {
Transform point = points[i];
Vector3 position = point.localPosition;
if (function == 0) {
position.y = FunctionLibrary.Wave(position.x, time);
}
else if (function == 1) {
position.y = FunctionLibrary.MultiWave(position.x, time);
}
else {
position.y = FunctionLibrary.Ripple(position.x, time);
}
point.localPosition = position;
}
}

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

Absolute X.

回到 **FunctionLibrary**.Ripple ,咱们运用间隔作为正弦函数的输入,并将其作为成果。具体来说,咱们将运用 y = sin(4 d),其间 d =| x|我的天||所以涟漪在图的域中上下屡次。

public static float Ripple (float x, float t) {
float d = Abs(x);
float y = Sin(4f * PI * d);
return y;
}

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

间隔的正弦

成果很难从视觉上解说,由于Y变化太大。咱们能够经过下降波的振幅来削减它。可是涟漪并没有固定的振幅,它会跟着间隔的添加而减小。所以让咱们把咱们的函数变成

y=sin(4d)1+10dy=\frac {sin(4 d)} {1 + 10 d}
float y = Sin(4f * PI * d);
return y / (1f + 10f * d);

画龙点睛的一笔是动画涟漪。为了使它向外流动,咱们有必要从传递给正弦函数的值中减去时刻。让咱们运用 t,因而终究函数变为

y=sin((4d−t))1+10dy=\frac {sin((4 d − t))} {1 + 10 d}
float y = Sin(PI * (4f * d - t));
return y / (1f + 10f * d);

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

   涟漪型动画

管理办法

一系列条件块适用于两个或三个函数,但当试图支持更多函数时,它会变得非常快。假如咱们能够依据一些规范向咱们的库恳求对办法的引证,然后重复调用它,那就便利多了。

托付

能够经过运用托付来获取对办法的引证。托付是一种特别类型,它界说了某个目标能够引证哪种办法。数学函数办法没有规范的托付类型,但咱们能够自己界说。由于它是一个类型,咱们能够在它自己的文件中创立它,但由于它是专门为咱们的库的办法界说的,咱们将在 **FunctionLibrary** 类中界说它,使其成为内部或嵌套类型。

若要创立托付类型,请仿制 Wave 函数,将其重命名为 **Function** 并将其代码块替换为分号。这界说了一个没有完结的办法签名。然后,咱们经过将 **static** 关键字替换为 **delegate** ,将其转化为托付类型。

public static class FunctionLibrary {
public delegate float Function (float x, float t);
…
}

现在咱们能够引进一个 GetFunction 办法,它在给定索引参数的情况下回来一个 **Function** ,运用与循环中运用的相同的if-else逻辑,仅仅在每个块中咱们回来恰当的办法而不是调用它。

public delegate float Function (float x, float t);
public static Function GetFunction (int index) {
if (index == 0) {
return Wave;
}
else if (index == 1) {
return MultiWave;
}
else {
return Ripple;
}
}

接下来,咱们运用这个办法在 **Graph**.[Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) 的开头,根据 function 获取一个函数托付,并将其存储在一个变量中。由于这段代码不在 **FunctionLibrary** 中,所以咱们有必要将嵌套的托付类型称为 **FunctionLibrary**.**Function**

void Update () {
FunctionLibrary.Function f = FunctionLibrary.GetFunction(function);
…
}

然后在循环中调用托付变量而不是显式办法。

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

代理托付数组

咱们现已简化了 **Graph**.[Update](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.Update.html) ,但咱们只将if-else代码移到了 **FunctionLibrary**.GetFunction 。咱们能够完全脱节这段代码,将其替换为索引数组。开端将 functions 数组的静态字段添加到 **FunctionLibrary** 。此数组仅供内部运用,因而不要将其揭露。

public delegate float Function (float x, float t);
static Function[] functions;
public static Function GetFunction (int index) { … }

咱们总是要把相同的元素放在这个数组中,这样咱们就能够显式地界说它的内容作为它声明的一部分。这是经过在花括号之间分配逗号分隔的数组元素序列来完结的。最简略的是一个空列表。

static Function[] functions = {};

这意味着咱们当即得到一个数组实例,但它是空的。更改它,使其包括对咱们的办法的托付,次序与前面相同。

static Function[] functions = { Wave, MultiWave, Ripple };

GetFunction 办法现在能够简略地索引数组以回来恰当的托付。

public static Function GetFunction (int index) {
return functions[index];
}

枚举

一个整数滑块能够作业,可是0代表波函数等等并不明显。假如咱们有一个包括函数称号的下拉列表,会更清楚。咱们能够运用枚举来完结这一点。

枚举能够经过界说 **enum** 类型来创立。咱们将再次在 **FunctionLibrary** 中履行此操作,这次将其命名为 **FunctionName** 。在这种情况下,类型称号后边是一个大括号内的标签列表。咱们能够运用数组元素列表的副本,但没有分号。请注意,这些都是简略的标签,它们不引证任何东西,尽管它们遵循与类型称号相同的规矩。咱们有责任坚持两份名单共同。

public delegate float Function (float x, float t);
public enum FunctionName { Wave, MultiWave, Ripple }
static Function[] functions = { Wave, MultiWave, Ripple };

现在将索引参数 GetFunction 替换为类型为 **FunctionName** 的name参数。这表明参数有必要是有效的函数名。

public static Function GetFunction (FunctionName name) {
return functions[name];
}

枚举能够被认为是语法糖。默许情况下,枚举的每个标签都表明一个整数。第一个标签对应0,第二个标签对应1,依此类推。所以咱们能够用姓名来索引数组。可是,编译器会诉苦枚举不能隐式转化为整数。咱们有必要显式地履行此强制转化。

return functions[(int)name];

最终一步是将 **Graph**.function 字段的类型更改为 **FunctionLibrary**.**FunctionName** 并删除其 [Range](http://docs.unity3d.com/Documentation/ScriptReference/RangeAttribute.html) 属性。


[SerializeField]
FunctionLibrary.FunctionName function;

**Graph** 的查看器现在显现一个包括函数称号的下拉列表,大写单词之间添加了空格。

【Unity】游戏数学:使用Unity构建可视化数学函数图形(二)(上)

  函数下拉列表。

【Unity】游戏数学:运用Unity构建可视化数学函数图形(二)(下) – 掘金 ()

【Unity】探索数学国际:运用Unity构建可视化数学函数图形(一)(上) – 掘金 ()

上篇