这个脚本的原作者是 Madore ,原脚本是用 Perl 写的,我改写成了 Python,主要的改动就是用算法生成 120-cell 的所有顶点,而不是直接罗列一大堆坐标。
脚本会在当且目录下生成若干 .pov 文件,然后用 Povray 渲染即可。
原理就是利用的 Gnomonic Projection,对一个 4 维空间中的点,从原点将(4维空间中的)球面的北半球投影到与北极相切的(3 维)平面上,将南半球投影到与南极相切的(3 维)平面上。白色的柱子表示北半球的顶点投影的效果,红色的细线表示南半球上的顶点投影后的效果。
Python 代码如下:
1 #coding=utf-8 2 3 4 #------------ 5 #程序会在当且目录下生成若干 .pov (default=1000)文件, 渲染命令为 6 #for i in `seq 0 999`; do povray +I `printf frame%03d.pov $i` render.ini;done 7 8 #这里 render.ini 如下: 9 #Width = 640 10 #Height = 480 11 #Display = off 12 #Pause_when_Done = off 13 #Bounding_Threshold = 3 14 #Test_Abort=On 15 #Test_Abort_Count=100 16 #Quality=11 17 #Antialias_Depth=3 18 #Antialias=On 19 #Antialias_Threshold=0.1 20 #Jitter_Amount=0.5 21 #Jitter=On 22 #------------- 23 24 25 import numpy as np 26 from numpy import pi, sin, cos 27 import itertools 28 29 SQRT5 = np.sqrt(5) 30 PHI = (1+SQRT5)/2 31 32 33 num_frames = 1000 34 user_trans = True 35 36 37 def negate(vector, index): 38 u = list(vector) 39 u[index] = -u[index] 40 return tuple(u) 41 42 def plus_minus(vector): 43 pool = set([vector]) 44 for i in range(len(vector)): 45 new_set = set([negate(vector,i) for vector in pool]) 46 pool = pool.union(new_set) 47 return pool 48 49 def perm_parity(vector, parity): 50 u = list(vector) 51 if len(vector) == 2: 52 if parity == 0: 53 return set([vector]) 54 else: 55 swap = (vector[1],vector[0]) 56 return set([swap]) 57 58 pool = set() 59 for index, value in enumerate(u): 60 rest = tuple(u[:index]+u[index+1:]) 61 rest_set = perm_parity(rest, (index+parity)%2) 62 for rest_tuple in rest_set: 63 pool.add(tuple([value]+list(rest_tuple))) 64 return pool 65 66 67 def even_perm(vector): 68 return perm_parity(vector,0) 69 70 def plus_minus_even_perm(vector): 71 pool = plus_minus(vector) 72 new_pool = set() 73 for v in pool: 74 for pv in even_perm(v): 75 new_pool.add(pv) 76 return new_pool 77 78 def plus_minus_all_perm(vector): 79 pool = plus_minus(vector) 80 new_pool = set() 81 for v in pool: 82 for pv in itertools.permutations(v): 83 new_pool.add(pv) 84 return new_pool 85 86 vertices = [] 87 88 vertices += plus_minus_all_perm((2,2,0,0)) 89 vertices += plus_minus_all_perm((1,1,1,SQRT5)) 90 vertices += plus_minus_all_perm((1/PHI**2,PHI,PHI,PHI)) 91 vertices += plus_minus_all_perm((1/PHI,1/PHI,1/PHI,PHI**2)) 92 93 vertices += plus_minus_even_perm((0,1/PHI**2,1,PHI**2)) 94 vertices += plus_minus_even_perm((0,1/PHI,PHI,SQRT5)) 95 vertices += plus_minus_even_perm((1/PHI,1,PHI,2)) 96 vertices = np.array(vertices) 97 edge_length = 3 - SQRT5 98 99 edges = [] 100 for i in xrange(600): 101 for j in xrange(i+1,600): 102 u = vertices[i] 103 v = vertices[j] 104 dist = np.linalg.norm(u-v) 105 if abs(dist-edge_length) < 1e-8: 106 edges.append([i,j]) 107 108 print('there are %d vertices, %d edges of the polytope'%(len(vertices),len(edges))) 109 110 def gram_schimdt(M): 111 """ 112 The QR decompostion. Q will be columnly orthogonal. 113 """ 114 Q,R = np.linalg.qr(M) 115 return Q 116 117 M = np.random.normal(size=(4,4)) 118 M = gram_schimdt(M) 119 120 for k_frame in range(num_frames): 121 T = np.eye(4) 122 123 ctheta = cos(2*pi*k_frame/num_frames) 124 stheta = sin(2*pi*k_frame/num_frames) 125 T[[0,0,3,3],[0,3,0,3]] = [ctheta,stheta,-stheta,ctheta] 126 M_i = np.dot(M,T) 127 128 rotated = np.zeros((len(vertices),4)) 129 for i in range(len(vertices)): 130 rotated[i] = np.dot(vertices[i],M_i) 131 132 f = open('frame%03d.pov'%(k_frame),'w') 133 content = """camera { location <0,0,0> look_at <1,0,0> up <0,0,1> right <0,1.5,0> } 134 light_source { <0,0,0>, 1 } 135 #declare Std = texture { pigment { color <1,1,1> } finish { ambient .25 diffuse .4 phong .35 reflection .1 } } 136 #declare Far = texture { pigment { color <1,0.5,0.5> } finish { ambient .4 diffuse .0 } }\n""" 137 f.write(content) 138 for e in edges: 139 u = rotated[e[0]] 140 v = rotated[e[1]] 141 142 p0 = u[0:3] / u[3] 143 p1 = v[0:3] / v[3] 144 dir1 = p1 - p0 145 dir1 /= np.linalg.norm(dir1) 146 147 if user_trans == True: 148 q0 = u[0:3] / u[0] 149 q1 = v[0:3] / v[0] 150 if ((u[0]<0) != (v[0]<0)): 151 qdir = np.array([1,q1[1]-q0[1],q1[2]-q0[2]]) 152 qdir /= np.linalg.norm(qdir[1:3]) 153 f.write('cylinder { <10000,%.5f,%.5f>, <10000,%.5f,%.5f>, 20 texture { Far } }\n'%(1.e4*q0[1],1.e4*q0[2],1.e4*(q0[1]-10*qdir[1]),1.e4*(q0[2]-10*qdir[2]))) 154 f.write('cylinder { <10000,%.5f,%.5f>, <10000,%.5f,%.5f>, 20 texture { Far } }\n'%(1.e4*q1[1],1.e4*q1[2],1.e4*(q1[1]+10*qdir[1]),1.e4*(q1[2]+10*qdir[2]))) 155 else: 156 f.write('cylinder { <10000,%.5f,%.5f>, <10000,%.5f,%.5f>, 20 texture { Far } }\n'%(1.e4*q0[1],1.e4*q0[2],1.e4*q1[1],1.e4*q1[2])) 157 158 if (u[3] < 0 and v[3] < 0): 159 continue 160 if u[3] < 0: 161 p1 = p0 - 1.e4 * dir1 162 elif v[3] < 0: 163 p0 = p1 + 1.e4 * dir1 164 f.write('cylinder { <%.5f,%.5f,%.5f>, <%.5f,%.5f,%.5f>, 0.01 texture { Std } }\n'%( p0[0], p0[1], p0[2], p1[0], p1[1], p1[2])) 165 166 f.close() 167