找回密码
 立即注册
查看: 364|回复: 0

Unity基础 03_数学曲面

[复制链接]
发表于 2022-12-21 10:41 | 显示全部楼层 |阅读模式
原文地址:Mathematical Surfaces

  • 创建一个函数库。 使用委托和枚举类型。 用网格显示2D函数。 在3D空间中定义曲面。
  • 这是关于学习使用Unity的基础知识的系列教程中的第三篇。这是构建图表教程的延续,因此我们不会开始新的项目。这一次,我们将使显示多个更复杂的函数成为可能
方程库


  • 完成上一个教程后,我们有了一个点阵图,显示了在播放模式下的动画正弦波。也可以显示其他数学函数。你可以改变代码,功能也会随之改变。你甚至可以在Unity编辑器处于播放模式时这样做。执行将被暂停,当前游戏状态被保存,然后脚本被再次编译,最后游戏状态被重新加载,游戏继续。这就是所谓的热重装。不是所有东西都能经受住热重装,但我们的图表做到了。它将切换到新功能的动画,而不知道有什么变化
  • 虽然在播放模式下更改代码很方便,但在多个功能之间来回切换并不方便。如果我们可以通过图形的配置选项来改变功能,那就更好了
Library Class

  • 我们可以在 Graph 中声明多个数学函数,但是让我们将这个类专用于显示一个函数,让它不知道确切的数学方程。这是专门化和关注点分离的一个例子。
  • 创建一个新的FunctionLibrary C#脚本,并将其放在Graph旁边的Scripts文件夹中。您可以使用菜单选项来创建新资源,或者复制并重命名图形。在这两种情况下,清除文件内容,从使用UnityEngine开始,并声明一个不扩展任何内容的空FunctionLibrary类



空 FunctionLibrary 类


  • 这个类不是一个组件类型。我们也不会创建它的对象实例。相反,我们将使用它来提供表示数学函数的公共可访问方法的集合,类似于Unity的 Mathf。 为了表示该类不被用作对象模板,通过在class之前编写static关键字,将其标记为static



表示数学函数的公共可访问方法的集合

Function Method

  • 我们的第一个函数将是图中所示的正弦波。我们需要为它创建一个方法。这与创建Awake或Update方法的工作原理相同,只是我们将它命名为Wave



正弦波


  • 默认情况下,方法是实例方法,这意味着它们必须在对象实例上调用。为了让它们直接在类级别工作,我们必须将它标记为static,就像 FunctionLibrary 本身一样



在对象实例上调用


  • 为了让它可以公开访问,也要给它一个public access修饰符



可以公开访问


  • 这个方法将代表我们的数学函数 f(x,t) = sin(π(x+t))。这 意味着它 必须产生一个结果,这个结果必须是一个浮点数。所以函数的返回类型需要是float,而不是void



返回类型需要是 float


  • 接下来,我们必须将这两个参数添加到方法的参数列表中,就像数学函数一样。唯一不同的是,我们必须在每个参数前面写类型,也就是float



两个形参数


  • 现在我们可以将计算正弦波的代码放入方法中,使用它的x和t参数



正弦波方法


  • 最后一步是明确指出方法的结果是什么。因为这是一个浮点方法,所以它必须在完成后返回一个浮点值。我们通过在return后面加上结果应该是什么来表明这一点,这是我们的数学计算



return


  • 现在可以在Graph中调用这个方法。更新,使用 position.x 和 time 作为其参数的自变量。其结果可用于设置点的Y坐标,而不是显式数学方程


Implicitly using a Type

  • 我们将使用Mathf.PI,Mathf.Sin,以及函数库中Mathf at lot的其他方法。如果我们可以不用一直明确地提到类型就能写这些就好了。我们可以通过在FunctionLibrary文件的顶部添加另一个using语句来实现这一点,该语句带有extra static关键字,后跟显式UnityEngine。Mathf类型。这使得该类型的所有常量和静态成员都可用,而无需显式提及该类型本身



  • 现在我们可以通过省略Mathf来缩短Wave中的代码


方程二


  • 让我们添加另一个函数方法。这一次我们将使用不止一个正弦波来制作一个稍微复杂一点的函数。首先复制Wave方法,并将其重命名为MultiWave



  • 我们将保留现有的正弦函数,但要添加一些额外的东西。为了简单起见,在返回当前结果之前,将它赋给一个y变量



  • 增加正弦波复杂性的最简单方法是增加另一个两倍频率的正弦波。这意味着它的变化速度是两倍,这是通过将正弦函数的自变量乘以2来实现的。同时,我们将这个函数的结果减半。这使得新正弦波的形状与旧正弦波保持一致,但大小减半



  • 这给了我们数学函数 f(x,t)=sin(π(x+t))+sin(2π(x+t))2。由于正弦函数的正负极值都是1和1,因此这个新函数的最大值和最小值可能是1.5和1.5。为了保证保持在1–1范围内,我们应将总和除以1.5



  • 除法比乘法需要更多的工作,所以经验法则是乘法比除法更好。然而,像1f / 2f和2f * Mathf这样的常数表达式。PI 已经被编译器简化为一个数字。所以我们可以重写代码,只在运行时使用乘法。我们必须使用操作顺序和括号,确保先减少常数部分



  • 我们也可以直接写0.5f,而不是1f / 2f,但是1.5的倒数不能用十进制记数法精确地写出,所以我们将继续使用2f / 3f,编译器将其简化为最大精度的浮点表示



  • 你可以说一个较小的正弦波现在跟随一个较大的正弦波。我们也可以让较小的一个沿着较大的一个滑动,例如通过将较大的波浪的时间减半。结果将是一个函数,它不仅随着时间的推移而滑动,还会改变它的形状。现在重复这个模式需要4秒钟


Editor中选择方程


  • 接下来我们可以做的是添加一些代码来控制Graph使用哪种方法。我们可以用一个滑块来做这件事,就像图表的分辨率一样。因为我们有两个函数可供选择,所以我们需要一个范围为0–1的可序列化整数字段。把它命名为函数,这样它控制什么就很明显了



Function slider.


  • 现在我们可以检查更新循环中的函数。如果为零,则图形应该显示为波形。为了做出选择,我们将使用if语句,后跟一个表达式和一个代码块。这类似于while,只是它不循环返回,所以要么执行要么跳过这个块。在这种情况下,测试是函数是否等于零,这可以用==等于运算符来完成



  • 我们可以在if块后面加上else和另一个块,如果测试失败,就会执行这个块。在这种情况下,图形应该显示多波



  • 这使得通过图形的检查器控制功能成为可能,同时我们也处于播放模式
涟漪方程


  • 让我们为我们的库添加第三个函数,一个产生涟漪效应的函数。我们通过使一个正弦波远离原点,而不是总是在同一个方向上传播,来创建它。我们可以根据到中心的距离来计算,这是x的绝对值。在Mathf的帮助下,只从计算x开始。Abs,在新的函数库中。波纹法。将距离存储在一个d变量中,然后返回它



  • 要显示它,请将Graph.function的范围增加到2,并在Update中为Wave方法添加另一个块。我们可以通过在else之后直接写另一个if来链接多个条件块,因此它成为一个else-if块,当function等于1时应该执行该块。然后为波纹添加一个新的else块





Absolute X.


  • 返回 FunctionLibrary.Ripple,我们使用距离作为正弦函数的输入,并将其作为结果。具体来说,我们将使用 y=sin(4πd),其中d=|x|因此,纹波在图形域中会多次上下波动



  • 结果很难从视觉上解释,因为Y变化太大。我们可以通过降低波的振幅来降低它。但是波纹没有固定的振幅,它随着距离的增加而减小。所以让我们把我们的函数变成 y=sin(4πd) / (1+10d)



  • 点睛之笔是动画涟漪。为了让它向外流动,我们必须从传递给正弦函数的值中减去时间。让我们使用 πt所以最后一个函数变成 y=sin(π(4d-t)) /(1+10d)


管理方程


  • 一系列条件块适用于两个或三个函数,但是当试图支持更多的函数时,它会变得难以处理。如果我们可以根据某些标准向我们的库请求对某个方法的引用,然后重复调用它,那就方便多了。
委派<Delegates>

  • 通过使用委托可以获得对方法的引用。委托是一种特殊的类型,它定义了什么样的方法可以被引用。我们的数学函数方法没有标准的委托类型,但是我们可以自己定义。因为它是一种类型,我们可以在它自己的文件中创建它,但是因为它是专门为我们的库的方法而创建的,我们将在FunctionLibrary类中定义它,使它成为内部或嵌套类型
  • 要创建Wave函数副本的委托类型,请将其重命名为function,并用分号替换其代码块。这定义了一个没有实现的方法签名。然后,我们通过用delegate替换static关键字,将其转换为委托类型



  • 现在我们可以引入一个GetFunction方法,它返回一个给定了索引参数的函数,使用我们在循环中使用的相同if-else逻辑,除了在每个块中我们返回适当的方法而不是调用它



  • 接下来,我们使用这个方法在图的开头获得一个函数委托。基于函数进行更新,并将其存储在变量中。因为这段代码不在FunctionLibrary中,所以我们必须将嵌套的委托类型称为FunctionLibrary.Function



  • 然后在循环中调用委托变量,而不是显式方法


An Array of Delegates

  • 我们简化了图表。更新了很多,但是我们只把if-else代码移到了FunctionLibrary.GetFunction,用索引一个数组来代替就可以完全去掉这段代码。首先向FunctionLibrary添加一个函数数组的静态字段。此数组仅供内部使用,因此不要将其公开



  • 我们总是将相同的元素放在这个数组中,所以我们可以在声明中显式地定义它的内容。这是通过在花括号之间指定一个逗号分隔的数组元素序列来实现的。最简单的是空列表



  • 这意味着我们立即获得一个数组实例,但它是空的。更改它,使它包含我们方法的委托,顺序和以前一样


Enumerations:

  • 整数滑块是可以的,但是0代表波函数之类的就不明显了。如果我们有一个包含函数名的下拉列表,那就更清楚了。我们可以使用枚举来实现这一点。
  • 可以通过定义枚举类型来创建枚举。我们将再次在FunctionLibrary中这样做,这次将其命名为FunctionName。在这种情况下,类型名后面是花括号内的标签列表。我们可以使用数组元素列表的副本,但是不带分号。注意,这些是简单的标签,它们不引用任何东西,尽管它们遵循与类型名相同的规则。我们有责任保持两个列表的一致性



  • 现在用FunctionName类型的name参数替换GetFunction的index参数。这表明参数必须是有效的函数名



  • 枚举可以被认为是语法上的糖。默认情况下,枚举的每个标签表示一个整数。第一个标签对应0,第二个标签对应1,依此类推。所以我们可以用这个名字来索引数组。但是,编译器会抱怨枚举不能隐式转换为整数。我们必须显式地执行这种类型转换



  • 最后一步是将Graph.function字段的类型更改为FunctionLibrary。FunctionName并移除其Range属性
  • Graph的检查器现在显示一个包含函数名的下拉列表,在大写单词之间添加了空格


https://www.zhihu.com/video/1579819105657524225
添加另一个维度


  • 到目前为止,我们的图表只包含一条直线的点。我们将一维值映射到其他1D值,尽管如果你考虑时间,它实际上是将2D值映射到1D值。所以我们已经将高维输入映射到1D值。就像我们增加时间一样,我们也可以增加额外的空间维度
3D Colors



Adjusted Multiply and Add node inputs.

Incorporating Z

  • 在波函数中使用Z的最简单的方法是使用X和Z的和,而不仅仅是X,这样会产生一个斜波



  • 多波最直接的变化是让每个波使用一个独立的维度。让小一点的用z



  • 我们还可以添加第三个波,它沿着XZ对角线传播。让我们使用和wave一样的Wave,除了时间减慢到四分之一。然后将结果除以2.5,使其保持在1–1范围内



  • 最后,为了使波纹在XZ平面上向各个方向扩散,我们必须计算两个维度上的距离。在数学的帮助下,我们可以使用毕达哥拉斯定理。Sqrt方法


https://www.zhihu.com/video/1580119748582264832
Leaving the Grid

  • 通过使用X和Z来定义Y,我们能够创建描述各种曲面的函数,但是它们总是与XZ平面相关联。没有两个点可以同时具有相同的X和Z坐标和不同的Y坐标。这意味着我们表面的曲率是有限的。它们的斜坡不能变得垂直,也不能向后折叠。要做到这一点,我们的函数不仅要输出Y,还要输出X和z
  • 如果我们的函数输出3D位置而不是1D值,我们可以用它们来创建任意的表面。例如,函数f(x,z)=[x,0,z]描述了XZ平面,而函数f(x,z)=[x,z,0]描述了XY平面
  • 因为这些函数的输入参数不再需要与最终的X和Z坐标相对应,所以命名它们不再合适 英语字母表的第24个字母 x和 z z.相反,它们用于创建参数化曲面,通常被命名为 英语字母表中第二十一个字母 u和 英语字母表中第二十二个字母 动词 (verb的缩写)所以我们会得到这样的函数



  • 调整我们的函数委托类型以支持这种新方法。唯一需要的改变是用Vector3替换它的float返回类型,但是我们也要重命名它的参数



  • 我们还必须相应地调整函数方法。我们将简单地直接使用U和V来表示X和z。不需要调整参数名,只需要它们的类型与委托相匹配,但是我们这样做是为了保持一致。如果你的代码编辑器支持的话,你可以快速重构——重命名参数和其他东西,这样它就可以通过菜单或者上下文菜单选项在任何地方被重命名
  • 从波开始。让它首先声明一个Vector3变量,然后设置它的组件,然后返回它。我们不必给向量一个初始值,因为我们在返回它之前设置了它的所有字段



  • 然后给多波和波纹同样的待遇



  • 因为点的X和Z坐标不再是常数,我们也不再依赖于它们在Graph.Update中的初始值。我们可以通过用Awake中使用的循环替换Update中的循环来解决这个问题,只是我们现在可以直接将函数结果分配给点的位置


注意,我们只需要在z变化时重新计算v。这确实需要我们在循环开始之前设置它的初始值


还要注意,因为Update现在使用分辨率,所以在播放模式下更改分辨率会使图形变形,将网格拉伸或挤压成矩形。
我们不再需要在Awake中初始化位置,所以我们可以使这个方法简单很多。我们可以只设置点的比例和父值


Creating a Sphere

  • 为了证明我们确实不再局限于每个(X,Z)坐标对一个点,让我们创建一个定义球面的函数。为此,向FunctionLibrary添加一个球体方法。还要将它的条目添加到FunctionName枚举和functions数组中。从总是返回原点开始



  • 创建球体的第一步是绘制一个圆,平放在XZ平面上。我们可以使用  [sin(πu),0,cos(πu)]对于这一点,仅仅依靠 英语字母表中第二十一个字母 u



  • 我们现在有多个完美重叠的圆。我们可以沿着Y轴挤压它们 英语字母表中第二十二个字母 这给了我们一个无盖的圆柱体





A cylinder.


  • 我们可以通过将X和Z缩放某个值来调整圆柱体的半径 英语字母表中第十八个字母 r.如果我们使用 r=cos(π2v)那么圆柱体的顶部和底部会折叠成单点





A cylinder with collapsing radius.


  • 这使我们接近一个球体,但是圆柱体半径的减少还不是圆形的。这是因为圆是由正弦和余弦组成的,在这一点上我们只用余弦来表示它的半径。等式的另一部分是Y,它目前仍然等于 英语字母表中第二十二个字母 动词 (verb的缩写)为了完成这个循环,我们必须使用  y=sin(π2v)





A sphere.

Creating a Torus

  • 最后,我们给函数库添加一个圆环面。复制球体,将其重命名为圆环,并将其半径设置为1。还要更新名称和函数数组



  • 我们可以把球体变成圆环,方法是把它的垂直半圆彼此拉开,然后把它们变成完整的圆。 s=1/2+rcos(π/2 v)





Sphere pulled apart.



A self-intersecting spindle torus.


  • 因为我们已经将球体拉开了半个单位,这产生了一个自相交的形状,称为纺锤环面。如果我们把它拉开一个单位,我们会得到一个不自相交的环面,但也没有洞,这就是所谓的喇叭环面。所以我们把球体拉开多远会影响圆环的形状。具体来说,它定义了圆环体的主半径。另一个半径是小半径,决定了环的厚度。 s=r2cos(πv)+r1。然后使用0.75作为长半径,0.25作为短半径,以保持点在1–1范围内





A ring torus.


  • 现在,我们有两个半径来玩,使一个更有趣的圆环。例如,我们可以通过使用



  • 同时也通过使用扭转环



  • 现在,您已经有了一些使用描述曲面的非平凡函数以及如何可视化它们的经验。您可以试验自己的函数,以便更好地理解它是如何工作的。有许多看似复杂的参数曲面可以用几个正弦波创建


https://www.zhihu.com/video/1580119423985303552

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 00:52 , Processed in 0.093641 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表