在三维欧氏空间里,有且仅有五种正多面体:正四面体、立方体、正八面体、正十二面体、正二十面体。一般介绍正多面体的文献中,只会强调立方体和正四面体互为对偶,正十二面体和正二十面体互为对偶,正四面体与自身对偶。这里“对偶”的意思是一种操作:连接多面体的每个面中心,形成新的多面体。正四面体的面心一连就是正八面体,其余类似。这篇文章想做的是为大家展示五种正多面体可以形成一个变换的循环:从正四面体到正八面体,再到正二十面体,乃至正十二面体、立方体,最后回到正四面体。
最后的成果应该是一个循环的动画,一边旋转一边展示内接形成新的正多面体。因为要展示,各面就该是半透明的。因为要循环,必要时需要旋转、缩放,如此才能够前后衔接形成"无穷循环"的效果。这需要精确计算相关缩放的比例,旋转的角度等等。
变换函数
这里要定义的变换函数就是按上文给出的变换顺序,根据一个多面体的顶点坐标生成另一个多面体顶点坐标的函数。正如我们知道的,正多面体除了定点坐标这个几何属性之外,还有很多组合属性。
诸如哪两个顶点组成一条棱,哪几个顶点组成一个面这样的属性不会因为顶点坐标的变化而变化,只要给顶点坐标编号,那些属性就是固定的数据。Mathematica 把这些都存在了 PolyhedronData 这个函数里,我希望能够复用这里面的信息。
这需要做到保证变换函数生成的坐标的顺序与 PolyhedronData 提供的坐标顺序一致:把两组坐标的编号标出后,可以用刚体变换加缩放让这两组编号重合。先定义绘制多边形及各顶点坐标的函数:
然后就可以给出与 PolyhedronData 提供的各个正多面体的顶点编号:
从四面体生成八面体
把四面体变换成八面体是很容易的,把各条棱中点连起来即可。不过根据四面体棱的顺序生成的中点编号和八面体编号不一致,这点可以从下图对比标准八面体的顶点编号看出来。红色的就是棱的编号。
这个定义就很简单了,先算出各条边对应的顶点编号并铺平,然后放在定义里:
然后为了对比验证顶点编号和标准多面体一致,需要定义一个按顺序绘制生成坐标点及编号的函数:
对比结果是正确的:
从八面体生成二十面体
正八面体有十二条棱,每条棱上取一点连起来能组成二十面体,但未必是正二十面体。要得到正二十面体,必须按黄金分割比来取,而且因为比例不是 1/2,所以棱的顶点次序不能任意指定,而要一致。
一致的目标就是给每条棱一个方向,保证每个面上的棱都是首尾相接。为了做到这一点,我就根据十二条棱人工指定了,我想不到特别简洁的用 Mathematica 表述的算法。
按这个顺序生成的顶点次序与标准编号对比如下,所以要根据对照,找出调整后的顺序。
然后就可以定义变换函数:
可以发现现在编号是正确的了:
从二十面体生成十二面体
从正二十面体到正十二面体的变换是很容易的,因为它们互为对偶。把一个正二十面体的面心连起来就是正十二面体了。首先求各面顶点编号:
与标准多面体顶点编号对比:
然后即可根据对比定义变换函数:
验证编号正确性:
从十二面体生成立方体
这个变换很简单,只要连接正十二面体的八个顶点即可。当然,这样的连接也是很对称的:十二面体的每个面都有一条棱经过,组成立方体的十二条棱。可以直接给出定义:
然后可以验证定点编号:
从立方体生成四面体
这就更容易了,和之前的一样,可以直接给出定义:
然后验证顶点编号:
缩放和旋转计算
设想中的动画涉及了缩放和旋转。如何选择每个阶段的缩放比例,如何决定旋转的形式,这都需要计算和决策。
缩放
从正四边形开始,每个变换都是用内接的方式生成,每个都比原来的小,所以设想中的动画涉及到缩放。问题是我没想好该以什么标准五个正多面体“等大”。棱长相等肯定不是一个选择,体积相等很难感知。
我能想到的就是三个球面的等大:外接球,内切球以及切边球。这三个球面的半径分别是顶点,面和棱到体心的距离。不论选择哪一个,都有一些基本的计算要做。
首先是各个变换下,外接多面体和内接多面体之间,顶点到体心距离的比例:
最大要扩大 1.732 倍,最小的不扩张,它们的乘积刚好是 3,然后再算一下棱中点到体心的距离,首先要定义一个辅助函数:
于是棱中点到体心距离的比例就是:
这个比较均匀,乘积也是 3,然后可以算面心距离的比例:
面心显然是不太合适的,有两次变换的内切球居然相等,要按它缩放,那两次就根本不会变了。最后结论:缩放到棱心距离相等。
旋转
设想中,动画的旋转有两种。一种是展示台一般的 360 度绕垂直轴旋转,还有呢则是因为生成的内接多边形可能是歪斜的,不在它们最“典型”的位置上,这需要沿着水平轴旋转到最佳位置。
转到典型位置相对容易计算些,绕垂直轴旋转的难点在于动画的最后一帧要和第一帧能够衔接上,这才能保证循环往复无穷无尽,所以这个垂直的转速就非常有讲究了。让我们先易后难。
在计算前,先要定义一个能显示两重多面体及对应顶点的函数,这样可以通过操控三维图形直观感受旋转该如何计算和选择。
这就是两种嵌套的多面体及其各自的顶点编号:
虽然从某种意义上来讲正多面体是很对称的,但为了展示的需要,在动画的每个阶段都需要把正多面体旋转到一个“正位”。这个旋转只考虑垂直方向,也就是旋转轴垂直于 z 轴的旋转,而不考虑绕 z 轴的旋转。至于正位,只是一个约定而已。
这里约定面为三角形的多面体正位都是顶点朝上,而立方体和正十二面体的正位都是面朝上。下面就是根据这个正位计算旋转的角度,只有正二十面体变换成正十二面体因为是对偶操作,不需要旋转。
立方体内含四面体的旋转
立方体的正位就是各条棱与右手正交标架平行,八个顶点坐标很好算:
这样的立方体,棱心距为:
内含四面体的旋转只要把顶点 4 转到朝上即可,旋转角度的余弦值为:
旋转轴向量为:
旋转效果如下:
旋转并放大到棱心距相等后的四面体坐标就是:
可以验证棱心距没有变:
四面体内含八面体的旋转
之前变换得到的四面体,顶点 4 朝上,那么根据下图,只要把顶点 2 旋转到朝上就是八面体的正位,旋转角度的余弦值为:
旋转轴向量为:
旋转效果如下:
旋转并缩放后的正八面体顶点坐标为:
可以验证棱心距不变:
八面体内含二十面体的旋转
之前变换得到的四面体,顶点 4 朝上,那么根据下图,只要把顶点 2 旋转到朝上就是八面体的正位,旋转角度的余弦值为:
旋转轴向量为:
旋转效果为:
旋转并缩放后的正二十面体顶点坐标为:
可以验证棱心距没有变化:
二十面体内含十二面体的坐标
旋转后处于正位的正二十面体顶点 8 朝上。从正二十面体变换到正十二面体是对偶操作,只要正二十面体在正位,那么变换得到的正十二面体也在正位,不需要旋转调整姿势。所以要计算的只是变换后的正十二面体的坐标:
可以验证棱心距并没有变化:
这样得到的对偶多面体,最上一层顶点编号是 1 9 10 14 15。
十二面体内含立方体的旋转
处于正位的正十二面体 1 9 10 14 15 这个面朝上,而内含的立方体的旋转则是把其中 1 9 18 4 这个面转到朝上即可。旋转到正位的角度余弦值是:
旋转轴向量是:
旋转效果为:
旋转并缩放后的坐标是:
可以验证棱心距并没有变化:
水平旋转
变换后处于正位的立方体和最开始的立方体并不重合,而是有一个角度差。要想动画循环往复,需要观察者或者立方体作水平旋转。该立方体在 x y 平面的投影如下:
歪斜角度刚好是:
所以只要最后观察者少绕 30 度即可。
动画
说到动画这里就要做一下决策和规定了:每次从一个多面体开始到完全变成另一个多面体,这个成为一阶段。每个阶段呢又分为三个小阶段:原多面体一边放大一边变透明,同时内含多面体开始连接棱并放大,到原多面体完全透明消失内含多面体连接完成为一小阶段;内含多面体旋转到正位,与此同时面从透明到半透明为第二小阶段;静置一段时间面从半透明变为不透明是第三小阶段。
在多面体变换时,摄像机镜头将不断围绕多面体,展示其各方面。三个小阶段的时间长短可以任意安排,只有最后一个大阶段的第三个阶段时间是要精心计算的,因为摄像头对准立方体使得图像和第一幅完全一致。所以动画我也打算分两个部分来做:自身的变换和镜头的运动,各自做好后再合在一起。
自身变换的动画
首先要定义一个根据编号给出外接多面体坐标的函数:
然后要定义一个辅助的线性缩放函数:
最后还需要定义一个绘制多面体各条棱的函数:
第一个小阶段:生成内接多面体
有了这三个辅助函数,定义第一个小阶段的动画函数就容易了:
考虑到旋转和放大,绘制范围要尽量找个能包住全部点的,所以得找个距离最大的:
这样绘图就很容易了:
第二个小阶段:旋转调整姿势
首先要定义五个阶段中需要旋转的角度:
然后是旋转轴向量:
这样就可以定义旋转的动画了:
绘图如下:
第三个小阶段:静置
第三个小阶段多面体本身并没有变化,所以只要显示变换后的多面体并随时间减少多面体的透明度即可。
三个小阶段的整合
三个小阶段的整合可以指定不同小阶段的时长,在这里用比例足矣,整体的时间参数还是在 [0, 1] 区间内。这就需要定义一个反向映射时间参数的函数:
然后就可以开始定义每个阶段的动画函数了:
五个阶段的整合
五个阶段的整合理论上需要指定 15 个时间段,我这里就不做那么细致的调整了,只指定三个。
镜头运动
镜头的运动一方面是为了全方位展示多面体的变换,另一方面是为了能让最后一个镜头和第一个镜头重合起来。所以总共旋转的角度是完整圈数的角度减去 30 度。最后导出成为完整的动图。