数学曲面
- 创建函数库。
- 使用委托和枚举类型。
- 使用网格显示2D函数。
- 在三维空间中定义曲面。
它是构建图形教程的延续,所以我们不会开始一个新项目。这一次,我们将能够显示多个和更复杂的功能。
【Unity】探索数学世界:使用Unity构建可视化数学函数图形(一)(上) - 掘金 (juejin.cn)
本教程使用Unity 2020.3.6f1制作。
组合几个波以创建复杂曲面
函数库
完成上一个教程后,我们有一个点图,显示了一个动画正弦波,而在播放模式。还可以示出其他数学函数。你可以改变代码,函数也会沿着改变。您甚至可以在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) {}
现在我们可以将计算正弦波的代码放入方法中,使用其 x 和 t 参数。
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.x 和 time 作为其参数的参数。其结果可用于设置点的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;
这给出了数学函数
。由于正弦函数的正负极值都是1和−1,因此这个新函数的最大值和最小值可能是1.5和−1.5。为了保证我们保持在-1-1范围内,我们应该将总和除以1.5。
return y / 1.5f;
除法比乘法需要更多的工作,所以一般来说,乘法比除法更好。然而,像 1f / 2f 和 2f * [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);
两个正弦波之和
你可以说,一个较小的正弦波现在跟随一个较大的正弦波。我们也可以让较小的一个沿着较大的一个滑动,例如通过将较大的波浪的时间减半。结果将是一个函数,它不仅随着时间的推移而滑动,而且会改变形状。现在需要四秒钟来重复该模式。
float y = Sin(PI * (x + 0.5f * t));
y += 0.5f * Sin(2f * PI * (x + t));
变形波
在编辑器中选择函数
接下来我们可以做的是添加一些代码,以便可以控制 **Graph** 使用哪个方法。我们可以使用滑块来实现这一点,就像图的分辨率一样。由于我们有两个函数可供选择,因此我们需要一个范围为0-1的可序列化整数字段。将其命名为 function ,以便它控制的内容显而易见。
[SerializeField, Range(10, 100)]
int resolution = 10;
[SerializeField, Range(0, 1)]
int function;
现在我们可以在 [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;
}
}
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;
}
距离的正弦
结果很难从视觉上解释,因为Y变化太大。我们可以通过降低波的振幅来减少它。但是涟漪并没有固定的振幅,它会随着距离的增加而减小。所以让我们把我们的函数变成
float y = Sin(4f * PI * d);
return y / (1f + 10f * d);
画龙点睛的一笔是动画涟漪。为了使它向外流动,我们必须从传递给正弦函数的值中减去时间。让我们使用 π t,𝜋𝑡因此最终函数变为
float y = Sin(PI * (4f * d - t));
return y / (1f + 10f * d);
涟漪型动画
管理方法
一系列条件块适用于两个或三个函数,但当试图支持更多函数时,它会变得非常快。如果我们可以根据一些标准向我们的库请求对方法的引用,然后重复调用它,那就方便多了。
委托
可以通过使用委托来获取对方法的引用。委托是一种特殊类型,它定义了某个对象可以引用哪种方法。数学函数方法没有标准的委托类型,但我们可以自己定义。因为它是一个类型,我们可以在它自己的文件中创建它,但由于它是专门为我们的库的方法定义的,我们将在 **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构建可视化数学函数图形(二)(下) - 掘金 (juejin.cn)
【Unity】探索数学世界:使用Unity构建可视化数学函数图形(一)(上) - 掘金 (juejin.cn)
上篇