前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity基础系列(三)——数学表面(数字雕刻)

Unity基础系列(三)——数学表面(数字雕刻)

作者头像
放牛的星星
发布2020-07-21 10:48:35
1.5K0
发布2020-07-21 10:48:35
举报
文章被收录于专栏:壹种念头壹种念头

本文要点: 支持多函数方法 使用委托和枚举。 用网格显示2D函数。 定义三维空间中的表面。

本教程是上一篇 构建视图 的延续,将会展示更多更复杂的函数和功能。

本篇教程使用的Unity版本最低为2017.1。

(将几个波浪组合在一起,形成复杂的形状)

1 不同函数之间的切换

在完成前面的教程之后,我们有一个基于线条的视图,并在游戏模式下显示一个正弦波动画。当然还可以通过修改代码来显示其他数学函数。甚至可以在Unity编辑器处于播放模式时执行修改操作。如果这样的话,Unity会暂停执行,保存当前的游戏状态,然后重新编译脚本,最后重新加载游戏状态并恢复播放。

然而在播放模式下,并不是所有的东西都能通过重新编译的,但我们这次的教程就可以。它将会切换到新的动画,好像什么都没有发生过。

虽然在播放模式中修改代码很方便,但在多个函数之间来回切换却并不方便。我们其实可以简单地更改图形的配置选项来完成我们想要的效果。

下面就看看怎么做吧。

1.1 函数方法

如果要让视图同时支持多个函数,那就需要先把所有的函数写出来。但是,循环遍历视图点的相关代码并不关心会使用哪个函数。所以,其实不需要对每个单独的函数重复循环的代码。而只要提取数学相关的部分,并将其放在自己的方法中即可。

Graph 中添加一种新的方法,来包含正弦函数的代码。就像建 “Awake” 和 “Update” 方法一样,但我们将其命名为SineFunction。

我们想让这个函数表示我们的数学公式f(x,t)=sin(π(x+t))。要实现这个目的,就必须要得到一个结果输出,因为它是一个浮点数.所以,函数的类型也必须是浮点数,而不是void。

该函数还需要参数,而现在它是一个空参数的函数。若要添加x参数,需要把其放在方法名称后面的括号内。就像这个函数本身一样,它的参数也必须在它们前面定义好类型。

因为函数是用浮点数计算,并且返回浮点数,所以参数也必须是浮点数。

同样要添加t参数,以及它的类型。参数声明必须用逗号分隔。

现在参数有了,就可以使用它的x和t参数将计算相关的代码放在函数里了。

最后一步是需要显式地返回该方法计算的结果。因为这是一个计算浮点类型的方法,所以它必须在完成时返回一个浮点数。

通过显式的return关键字,返回计算后的结果,这是所谓的数学计算了。

现在可以在Update中调用此方法了,使用position.x和Time.time作为函数的参数。和直接计算得到的结果一样,它返回的结果可以用来设置点的Y坐标。

注意,因为每次循环中调用时,Time.time都是相同的。所以可以优化一下,用在循环之前用变量存储起来这样只需要检索一次值就可以了。

1.2 第二个函数

现在已经创建了一个函数方法,下面可以再创建一个。这一次,将使用多个正弦来做一个稍微复杂一些的函数。

复制SineFunction方法开始,并将新方法重命名为MultiSineFunction。

第二个函数会保留我们已经拥有的正弦函数,但是在计算中添加一些额外的东西。为了简化理解,在返回之前,将当前的计算结果赋值给临时变量y。

要增加正弦波的复杂性,最简单的方法是增加另一个频率加倍的正弦波。如果我们要把变化速度变为原来的两倍,只需要将正弦函数的参数乘以2就可以。同时,我们还可以把这个函数的结果减半。最后得到的正弦波的形状保持不变,但只有先前的一半大小。

这个给定的函数我们也可以用公式来表达 。

当正弦函数的正极值和负极值均为1和?1时,这个新函数的最大值和最小值将分别为1.5和?1.5。为了保证我们保持在?1-1的范围内,我们应该把整件事除以1.5,这相当于乘以2/3 。

用这个函数代替Update里的SineFunction,看看它是什么样子的。

(多正弦效果)

你可以看做是一个较小的正弦波正在跟随一个更大的正弦波。或者我们可以让它变成一个较小波动的逐步向较大的波动过渡的效果,只需要加倍一下时间因素。它不仅会随着时间的推移而滑动,还会改变它的形状。因为正弦波重复,它每隔两秒钟就会返回相同的形状。

1.3 在编辑器里选择函数

接下来我们要实现的就是如果通过编辑器里的视图参数来控制函数调用。而实现方式可以用滑块,就像视图的分辨率一样。由于我们有两个函数可供选择,我们需要一个范围为0-1的公共整数字段。

(函数滑块)

我们可以在Update中使用if-else块来控制调用哪个函数。如果滑块设置为0,我们将使用SineFunction。否则,我们将使用MultiSineFunction。

这样我们就可以通过视图的检查器面板来控制函数调用,即使是在运行模式也可以。

1.4 静态方法

虽然SineFunction和MultiSineFunction是Graph的一部分,但它们实际上是自给自足的.因为它们只依靠传递参数和数学来完成自己的工作(它们需要依赖Mathf,可以把它简单地看作是数学)。此外,他们不需要访问Graph里任何其他方法或字段。这意味着我们如果把它们放到另一个类或结构中,它们仍然可以正常工作。

因此,我们可以为函数方法创建一个单独的类,并将它们全部放在那里。然而,由于Graph是使用这些方法的唯一地方,所以好像也没有足够的理由去这么干。

默认情况下,方法和字段与类或结构类型的特定对象或值实例相关联。大部分时候是这样的,但还有一种情况例外。通过将 static 关键字放在方法或字段定义的前面可以解除方法和实例之间的关联。

这些方法仍然是graph的一部分,但它们现在直属于类,而不再是对象实例。如果我们公开了静态方法,就可以在任何地方调用它们,比如Graphs.SineFunction(0f,0f),就像Mathf.Sin(0f)一样。在Graph 类自身调用的时候,不需要显式地添加类型前缀,所以我们现有的代码仍然可以工作。

把方法是静态的有什么意义?

至少到现在来看还没有真正的意义,但我们很快就会用到它了。

现在这么写,只是表明这些方法是独立的。因为静态方法与对象实例没有关联,所以编译的代码不必追踪分析调用方法的对象。这意味着静态方法调用的速度要快一些,但基本是微不足道。

1.5 委托

一个简单的if-else块适用于两个函数,但是如果要支持更多的函数时,它会变得难以处理。但如果可以使用一个变量来存储想要调用的方法的引用,那岂不是就非常方便了。通过委托类型可以很容易达成。委托是一种特殊类型,它定义可以引用哪种方法。我们前面定义的数学函数方法没有标准的类型,但是我们可以自己定义它。为此,创建一个新的C#脚本资产并将其命名为GraphFunction。

(GraphFunction 脚本)

为什么要新建一个脚本?

实际上,在Graph里定义委托类型是没问题的,但是将每个类型放在自己的脚本中可以清楚地表明它们是独立的。在一些较大类型的项目中,仅在另一种类型的上下文中使用的小类型的时候。才会嵌套在这些类型里。

去掉此脚本中的默认代码。保留 UnityEngine命名空间,然后定义一个名为GraphFunction的公共委托类型。这与类或结构定义不同,它后面必须是分号。

委托类型定义可用于该类型的方法的形式。该定义表示的是方法的签名,定义改类型的函数的返回类型和参数列表。在本次的例子中,这些方法的返回类型是Float,并且有两个参数,这两个参数都是浮点的。此签名应定义将用于GraphFunction委托类型,他们的名字不重要,但它们的类型必须正确。

现在,可以在Graph.Update中,在循环之前声明一个GraphFunction变量。之后,就可以像调用方法一样调用这个变量。这样就可以摆脱循环中的if-else代码了。

但是,现在必须在循环之前放置一个if-else块,将对适当方法的引用分配给GraphFunction变量。

1.6 委托数组

虽然现在已经将if-etc块移出循环体了,但仍然没有消除它。可以用索引数组来替换,从而完全摆脱它。既然已经有了GraphFunction类型,就可以把这种类型的函数数组字段添加到Graph之中。

因为一定要在这个数组中放置相同的元素,所以可以显式地将其内容定义为其声明的一部分。通过在花括号之间分配数组元素序列来完成的。最简单的是一个空序列。

这么定义,意味着着可以立即得到一个数组实例,但是是空的。修改一下,以便它能包含引用这两个函数方法。

因为这个数组总是相同的,所以没有必要为每个视图实例创建一个。相反,为Graph 类型本身定义一次就行,就像之前定义的函数方法一样是静态的。

接下来,使用Update中的数组,使用Function 实例字段对其进行索引。在此之后,我们终于可以删除if-else代码段了。

1.7枚举

整数滑块可以实现目的,但0表示正弦函数和1表示多正弦函数并不明显,理解起来也有困难。如果有一个包含有意义的名字的下拉列表,是不是就更清楚了?枚举可以做到。

枚举可以通过定义枚举类型来创建。创建一个新的C#脚本资产来包含该类型,命名为GraphFunctionName。

(GraphFunctionName 脚本)

枚举的最小定义与类相同,但并不能替代类。

枚举名称后面的块包含逗号分隔的标签列表。这些字符串遵循与类型名称相同的规则和约定。作为函数的名称,使用 Sine 和MultiSine。

接下来,用具有新GraphFunctionName类型的另一个 function 字段替换Graph的 function 整数字段。

枚举可以被认为是语法糖。默认情况下,枚举的每个标签表示一个整数。第一个标签对应于0,第二个标签对应于1,依此类推。因此,这里可以继续使用枚举字段对数组进行索引。但是,编译器会警告枚举不能隐式转换为整数。在Update中将其用作索引时,我们必须显式地执行此强制转换。

现在已经开始使用枚举来选择要使用的函数了。当检查器显示枚举时,它将创建一个下拉列表,其中包含该枚举类型的所有标签。我们可以在下来列表里选择使用哪个标签,只要我们确保GraphFunctionName的标签和Graph.function的内容匹配即可。

(函数下拉列表)

2 添加另一个维度

到目前为止,都是在使用传统的线性视图。它们将一个一维值映射到另外一维值,如果考虑到时间,它实际上是将二维值映射为一维值。所以现在已经实现在把一个高维输入映射到一个一维值上去了。既然可以增加事件维度,是否可以增加空间上的维度呢?

之前的示例都是使用X维作为函数的空间输入。Y维用于显示输出。这里可以让Z作为第二个空间维用于输入。如果要让它可视化的话,就需要升级我们的着色器,使它使用Z坐标设置蓝色通道。这可以通过计算albedo时用RGB和xyz替换Rg和XY的方式来实现。

2.1 调整函数

为了支持函数的第二个非时间输入,在GraphFunction委托类型的x参数之后添加一个z参数。

还需要将参数添加到视图的两个静态函数中,虽然他们目前还没有使用额外的维度来参与计算,但委托的参数个数和类型必须匹配。

要让整个调用链都正确的话,在调用Update中的函数方法时,还必须提供position的Z坐标作为第二个参数。

2.2 创建点网格

要显示Z维度,就必须将现在的点构成的线转换为点构成的网格。这可以通过创建多条线来实现,每条线沿Z偏移一个步长即可。这里对Z使用与X相同的范围,就像现在的点一样,我们可以创建很多条线,这就要求我们点的数量要是现在的平方倍数。接下来,在Awake的时候调整点数组的创建,以便能够容纳更多的点。

当我们根据分辨率每次迭代增加X坐标时,简单地创建更多的点只会产生一条更长的线。我们必须调整初始化的循环体以展示第二个维度。

(一条非常长的线)

首先,显式地跟踪X坐标。正确的做法是在for循环中声明和递增一个x变量,就像i迭代器变量一样。为此,循环定义的声明部分和增量部分可以转换为逗号分隔的列表。

每次完成一行处理之后,需要将x重新设置为零。当x变得等于分辨率时,一行就完成了,所以可以在循环顶部的if块来处理这个问题。然后用x代替i来计算X坐标。

接下来,每一行必须沿Z维偏移。这也可以通过向for循环中添加一个z变量来完成。这个变量不能每次迭代递增。相反,只有当我们移到下一行时,它才会增加,对于下一行,我们已经有了一个if块。然后设置位置的Z坐标,就像它的X坐标,用z代替x。

现在创建出来的是一个由点组成的正方形网格,而不是一条单独的线了。因为函数仍然只依赖于X维,它看起来就像是原来的点被挤压成线。

(网格上的正弦函数)

因为现在有很多点被放置在一个小空间里,所以很可能点之间会互相投下阴影。默认方向光的Y旋转被设置为?30°,这将导致在正方向上看图时出现大量可见阴影。为了更好地看到颜色,可以旋转光线以获得更令人愉悦的阴影,比如使用30°的正Y旋转,或者简单地禁用阴影。

(灯光在Y轴旋转了30度)

为什么帧率下降了很多?

与前一个线性视图相比,网格包含更多的点。50的分辨率,它就有2500点。100的分辨率,它有1万点。如果只是单独一个窗口你的电脑应该没问题,但当游戏和场景窗口同时可见时,它可能会很有些困难,因为这会使工作量增加一倍。

2.3 双循环

虽然当前创建网格布局的方法是可以达到效果了,但是if块的使用还是很尴尬。在二维上循环的一个更易读的方法是在每个维度上使用一个单独的循环。为此,删除旧的for循环声明和if块,以遍历Z的for循环取代它,然后在该循环中创建另一个循环,用于X。在第二个嵌套循环中创建点。这个效果其实就是在X上循环多次,在每一行之后增加Z,就像以前一样。

不再需要i迭代器变量来结束循环了,但它仍然需要索引点数组。在外部循环中定义它,但在内循环中增加它。通过这种方式,它在整个过程中都是已知的,并且在每一个点上都会增加。

注意,Z坐标只在外部循环的每一次迭代中更改。这意味着不用在内部循环中去计算它。这样就可以将其提升一级,以减少重复工作。

哪个维度放在外部循环进行遍历会造成影响吗?

我用Z做外循环,用X做内循环。结果会和前面的保持一致。这意味着网格是通过沿X方向创建点行来构造的,而行是沿Z偏移的。你也可以反过来使用X作为外部循环,Z作为内循环。在这种情况下,网格是通过沿Z创建逐行点,沿X偏移来构造的。只有创建点的顺序不同,其他一切都是相同的。

2.4 纳入Z维度

现在有一个输入点的2D网格,所以可以利用这个新的第二维。但是首先,要给π定义一个局部常量,这样就不必一直写Mathf.PI这样的方式了。因为它经常用到,所以会简化我们的代码。

创建一个使用X和Z作为输入的新函数,而不是调整现有的两个函数。为此创建一个方法,名为Sine2DFunction。它表示函数f(x,z,t)=sin(π(x+z+t))。

将此方法添加到函数数组中,将其直接放在SineFunction之后。

使用Sine2D将其添加到GraphFunctionName中。

(数字正弦波)

在播放模式中使用这个函数时,你会看到熟悉的正弦波,但它是沿着XZ对角线而不是沿着X直线方向的,这是因为我们使用x+z而不是仅仅x作为正弦函数的输入。

使用这两个维度的另一种更有意思的方法是组合两个独立的正弦波,每个维度一个。简单地将它们加在一起,然后将结果减半,以便输出保持在?1-1范围内。给出函数。

为了使代码易于阅读,这里将使用y变量并将其拆分为三行。

(每个维度单独的正弦波)

为什么使用*=0.5来取代/=2呢?

这两种方法在数学上是等价的,但乘法指令比除法指令快。如果在循环中执行大量的计算,这是一个简单的优化。本教程没有必要,但这是一个很好的习惯。

现在为多正弦函数创建一个2D变体实现。为了实现效果,会使用单个主波,两个次波,每维一次,因此我们得到了函数表示

其中M 代表主波,Sx 表示基于x的二次波,而Sz是基于z z的二次波。

使用

,所以主波是一个缓慢移动的对角线波。第一次波为Sx=sin(π(x+t))是沿X的法线波,第三波为Sz=sin(2π(z+2t)),双频快速运动。

主波M大,是Sx Sx振幅的四倍。因为Sz的频率和速度是其他二次波的两倍,所以我们会给它一半的振幅。这导致函数

,它必须除以5.5才能规范为?1-1范围。为此创建一个MultiSine2DFunction方法。

将其添加到 functions 数组中。

并给它命名为MultiSine2D。

(二维多正弦,合并三个波)

2.5 创建连漪

后面的教程里,我们开始弄点好玩的2D效果。再创建一个2D函数,这一次它代表了一个表面上的动画涟漪。让波纹向四面八方扩散,这样就得到了一个圆形的图案。首先,可以根据到圆点的距离来创建一个正弦波。这个距离可以用勾股定理(毕达哥拉斯定理)求出,即a2+b2=c2,其中c是直角三角形的斜边,a和b是它的两个直角边。

在XZ平面上的二维点,一个三角形的斜边就是原点和那个点之间的直线,而X和Z坐标就是2个直角边的距离。因此,我们每个点与原点之间的距离是

(勾股定理求斜边)

添加一个Ripple函数方法并让它计算距离,使用Mathfs.qrt计算平方根并作为输出。

将此方法附加到 functions 数组中。

然后把名字添加到枚举中。

(点到圆点的距离)

差不多就是一个圆锥形,中间为零,并且随着距离的增加而线性增加。因为这些点离原点最远,所以它在网格角附近的位置最高。在拐角处,距离是√2,大约是1.4142。

为了产生涟漪,我们必须使用f(x,z,t)=sin(πD),其中D是距离。

(到圆点的正弦距离)

光这样的话,还不足以产生波形,所以让我们可以把它的频率提高四倍。

(增加频率之后)

现在已经有些波纹的雏形了,但现在的波动太极端了。可以通过减小波的振幅来解决这个问题。如果所有的点都用一个振幅来做,效果只是现在效果的缩小版,所以我们让它和距离挂钩。例如,我们可以用1/10D 作为振幅。这会让波纹离其圆点越来越远,模仿了涟漪的行为,因为只是模仿,所以不用完全符合物理。然而,如果除以距离的话,在原点就会造成除0的错误。为了防止这种情况,这里用用1/(1+10D)代替。

(通过距离缩放振幅)

最后,将时间添加到正弦波中,使其具有动画效果。因为波纹应该向外移动,所以是减去t而不是加。

(连漪动画)

3 摆脱网格

通过使用X和Z来定义Y,可以创建大量能够展示表面的函数,但是它们却总是与XZ平面相连。如果两个点有相同的X和Z坐标,那它就不能再拥有相同的Y坐标了,这是2D的局限,意味着表面的曲率是有限定的。它们的边不能垂直,也不能向后折叠。要避免这点,现在,函数不仅要输出Y,还要输出X和Z。

3.1 3D函数

如果函数要输出3D位置而不是一维值的话,就可以使用它们来创建任意的表面。例如,函数f(x,z)=(x,0,z)描述XZ平面,而函数f(x,z)=(x,z,0)则描述XY平面。由于这些函数的输入参数不再对应于最后的X和Z坐标,因此不再适合将它们命名为x和z。相反,它们通常被命名为u和v 。因此,我们可以得到像f(u,v)=[u+v,uv,u/v]这样的函数。调整我们的 GraphFunction 的委托以支持这一新方法。其实唯一需要的修改的就是用Vector 3替换浮点返回类型,但也要重命名它的参数。

原来的正弦函数现在必须定义为f(u,v,t)=[u,sin(π(u+t)),v]。但是,由于没有调整X和Z,所以可以保持正弦函数的参数名不变。但现在它必须返回一个向量类型了,直接使用x和z作为其X和Z坐标,同时计算Y坐标。

对Sine2DFunction进行同样的更改。

调整其他三种函数方法。

因为点的X和Z坐标不再是常数,所以我们不能再依赖Update中它们的初始值了。相应的,必须提供新的U和V输入,这可以用一个双循环来完成。然后,将函数方法的结果直接分配到点的位置,因此不再需要索引它了。

由于这种新方法不再依赖于原始位置,因此我们不再需要在“Awake”中初始化它们了,可以简化该函数。用一个简单的循环来初始化所有的点并保持它们的位置不变。

3.2 创建圆柱体

为了证明示例不再局限于每个(X,Z)坐标的一个点,添加一个 Cylinder函数方法来创建一个定义圆柱的功能。从 始终返回原点开始。

将此方法添加到 functions 数组。并像之前一样将其名称添加到GraphFunctionName。后面不再重复提及这个步骤了。

圆柱体是一个扩展的圆,所以先从圆圈开始。正如前面的教程所提到的,2D圆上的所有点都可以通过[sin(θ),cos(θ)]来定义,θ从0到2π。在XZ平面上创建一个圆,需要函数f(U)=[sin(πu),0,cos(πu)]。

(一个环)

因为函数还没有使用v,所以所有使用相同v输入的点最终都位于完全相同的位置。所以效果看起来只是一条线。如果要看这条线是如何绕成一个圆的,让Y等于u即可。

(沿着Y轴的圆)

这表明这条线从[0,-1,-1]开始,沿着顺时针方向弯曲,与函数的输入一致。要把它变成一个实际的圆柱体,就需要让Y等于v。

(圆柱体)

目前使用单位圆可以作为我们的圆柱体的基础,但其实可以不用这么复杂。通过对正弦和余弦的幅值进行缩放,可以调节圆的半径。一般情况下,函数变成f(u,v)=[Rsin(πu),v,Rcos(πv)],其中R 是圆的半径。调整方法,使它使用一个显式半径为1。

如果使用不同的振幅参数会怎样?

x和y的振幅不一致会使结果变为一个椭圆。

可以用任何其他半径,甚至不是常数都可以。例如,可以沿u改变半径,使用另一个正弦波,比如R=1+sin(6πu)/5。

(六边形的圆柱体,分辨率100)

这个结果会导致圆柱体变得不稳定。此时圆圈已经变成了一个星型的圆柱了。表面沿着一个波浪图案绕着圆圈,前后移动了六次。还可以使半径依赖于v,例如R=1+sin(2πv)/5。在这种情况下,圆柱体的每个环都有一个恒定的半径,但半径随圆柱的长度而变化。

(用V替代U)

更有意思的是使用u和v来创建一个对角线波,它最终会绕着圆柱体旋转。添加t,让它动起来。最后,为了确保半径不超过1,将其基线降至4/5。

(动起来的圆柱)

3.3 创建球体

现在已经知道怎么创建圆柱体了,下面看看怎么创建球体。复制 Cylinder 方法并将其重命名为Sphere。看看,怎么把圆柱体变成一个球体呢?用R=cos(πv/2)将圆柱体顶部和底部的半径减小到零。

(有点球的样子了)

有点接近了,但光用圆柱体半径减小的方式还不能完全变成球体。这是因为圆是由正弦和余弦组成的,我们现在只使用余弦。方程的另一部分是Y,它现在仍然等于v。要完成圆,Y必须等于sin(πv/2)。

(球体)

现在已已经得到了一个球体,是由一个通常被称为UV球体的图案所创造。虽然这种方法创建了一个正确的球体,但请注意,点的分布并不均匀,因为这个球是通过变半径的叠加圆来创建的。在球体的两极,它们的半径为零。为了能够控制球体的半径,必须调整现在的公式。用f(u,v)=[Ssin(πu),Rsin(πv2),Scos(πu)],其中S=Rcos(πv/2),R 是半径。这种方法使使球体半径的变为动画。对u和v分别使用正弦波,R=4/5+sin(π(6u+t))/10+sin(π(4v+t))/10。

(脉冲球)

3.4 创建一个环面

最后一个效果,把一个球变成一个圆环。复制Sphere并将其重命名为Torus,然后删除球体半径的代码。

通过把球体分开来创造圆环,就像抓住它的把柄一样,在XZ平面上,把它拉向各个方向。所以可以通过向S添加一个常量值来做到这一点,例如1/2。

(球被扯开了)

现在我们有了半个环面,只占了环的外部部分。要完成环面,我们必须用v来描述整个圆,而不是半个圆。这可以通过使用πv而不是πv/2来实现。

一个自交的主轴环。

因为我们把球体分开了半个单位,这就产生了一个自相交的形状,这就是所谓的主轴环面。如果我们用一个单位把它拆开,我们就会得到一个没有自相交的环面,但也没有一个洞,也就是喇叭环。那么,需要把球体拉得多远,才会影响圆环的形状呢?具体来说,一个变量定义了环面的主要半径,我们将用R1来指定。因此,我们的函数成为f(u,v)=[Ssin(πu),sin(πv),Scos(πu)],其中S=cos(πv)+R1。

(喇叭环)

使R1大于1会在环面中部打开一个洞,这将使它成为一个环环面(ring torus)。但这假设我们环绕环的圆总是半径1,即环的二次半径R2,如果我们使用函数f(u,v)=[Ssin(πu),R2sin(πv),Scos(πu)],其中S=R2cos(πv)+R1。把R1保持在1,把R2降到5。

(环环面)

现在,我们有两个半径可玩了,这可以做更多更有趣的环面。一个相当简单,但仍然有意思的方法是添加一个u波到R1和v波到R2,两个动画,同时确保环面符合?1-1范围内。

现在,你已经掌握了一些处理描述3D表面函数的经验,以及如何将它们变的可视化。所以可以尝试写自己的函数,以便更好地掌握它的工作原理。简单几个正弦波就能创造出许多看似复杂的形状。

这之后,您可以继续下一章 构建分形。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档