说明
正十二面体,每个面是由正五边形组成,每个顶点被 3 个面共享。
几何
第一步,查找正十二面体的几何公式,可以看出正十二面体计算要复杂很多;
第二步,计算顶点坐标时,也要复杂一点;
let phi = (1.0 + sqrtf(5)) * 0.5
let a = (sqrtf(5) - 1) / sqrtf(3) * radius //棱长
let h = phi / sqrtf(3) * radius //中交球半径
let w = a / 2
let v = radius / sqrtf(3)
let points = [
SIMD3<Float>(0.0, h, w),
SIMD3<Float>(0.0, h, -w),
SIMD3<Float>(0.0, -h, w),
SIMD3<Float>(0.0, -h, -w),
SIMD3<Float>(h, -w, 0.0),
SIMD3<Float>(h, w, 0.0),
SIMD3<Float>(-h, -w, 0.0),
SIMD3<Float>(-h, w, 0.0),
SIMD3<Float>(-w, 0.0, -h),
SIMD3<Float>(w, 0.0, -h),
SIMD3<Float>(-w, 0.0, h),
SIMD3<Float>(w, 0.0, h),
SIMD3<Float>(v, v, v),//12
SIMD3<Float>(-v, v, v),
SIMD3<Float>(-v, v, -v),
SIMD3<Float>(v, v, -v),
SIMD3<Float>(v, -v, v),//16
SIMD3<Float>(-v, -v, v),
SIMD3<Float>(-v, -v, -v),
SIMD3<Float>(v, -v, -v),
]
//三个面共用顶点,故重复 3 次
meshPositions.append(contentsOf: points + points + points)
这里计算后的坐标顺序如下图:
第三步,构建正五边形,除了三角形和四边形,我们还可以直接向 RealityKit 中写入多边形来构建几何体。
let index: [UInt32] = [
0, 1, 14, 7, 13,
1, 0, 12, 5, 15,
3, 2, 17, 6, 18,
2, 3, 19, 4, 16,
4, 5, 12, 11, 16,
5, 4, 19, 9, 15,
7, 6, 17, 10, 13,
6, 7, 14, 8, 18,
8, 9, 19, 3, 18,
9, 8, 14, 1, 15,
11, 10, 17, 2, 16,
10, 11, 12, 0, 13
]
var countDict: [UInt32:Int] = [:]
for ind in index {
let count = countDict[ind] ?? 0
indices.append(ind + UInt32(pointCount * count))
countDict[ind] = count + 1
}
第四步,原始法线计算,基本逻辑还是 5 个点求和得到中心点作为法线。
// 计算法线,同时当 res > 0 时,细分一次,将正五边形分成 5 个小三角形
var newIndices1: [UInt32] = []
for i in 0..<pentagons {
let ai = 5 * i
let bi = 5 * i + 1
let ci = 5 * i + 2
let di = 5 * i + 3
let ei = 5 * i + 4
let i0 = indices[ai]
let i1 = indices[bi]
let i2 = indices[ci]
let i3 = indices[di]
let i4 = indices[ei]
let v0 = meshPositions[Int(i0)]
let v1 = meshPositions[Int(i1)]
let v2 = meshPositions[Int(i2)]
let v3 = meshPositions[Int(i3)]
let v4 = meshPositions[Int(i4)]
let faceCenter = (v0 + v1 + v2 + v3 + v4) / 5
let faceNormal = simd_normalize(faceCenter)
normals[Int(i0)] = faceNormal
normals[Int(i1)] = faceNormal
normals[Int(i2)] = faceNormal
normals[Int(i3)] = faceNormal
normals[Int(i4)] = faceNormal
if res > 0 {
meshPositions.append(faceCenter)
normals.append(faceNormal)
let center = UInt32(vertices + i)
newIndices1.append(contentsOf: [
i0, i1, center,
i1, i2, center,
i2, i3, center,
i3, i4, center,
i4, i0, center
])
}
}
if res > 0 {
vertices += pentagons
indices = newIndices1
}
第五步,细分平面,这里我们需要先将正五边形细分一次,成为 5 个小的等腰三角形,再按原来逻辑进行三角形的细分。这个第一步操作,与法线计算类似,所以都放在第四步中计算。
无法直接细分的意思是,无法将正五边形直接分成若干个小的正五边形,所以只能先添加一个中心点,分成 5 个小的等腰三角形。
第六步,贴图计算与前面相同。但需要注意的是,当需要细分平面时,我们使用的是三角形;当不需要细分平面时,我们使用的是正五边形(其实 RealityKit 还是会把五边形优化成若干个三角形,但和我们自己细分不同的是,它不会添加新的顶点)

代码
/// 正十二面体,radius 为外接球半径,res 五边形面剖分次数
public static func generateDogecahedron(radius: Float, res: Int = 0) throws -> MeshResource {
let pointCount = 20
let pentagons = 12
var vertices = pointCount * 3
var descr = MeshDescriptor()
var meshPositions: [SIMD3<Float>] = []
var indices: [UInt32] = []
var normals: [SIMD3<Float>] = Array(repeating: .zero, count: vertices)
var textureMap: [SIMD2<Float>] = []
let phi = (1.0 + sqrtf(5)) * 0.5
let a = (sqrtf(5) - 1) / sqrtf(3) * radius //棱长
let h = phi / sqrtf(3) * radius //中交球半径
let w = a / 2
let v = radius / sqrtf(3)
let points = [
SIMD3<Float>(0.0, h, w),
SIMD3<Float>(0.0, h, -w),
SIMD3<Float>(0.0, -h, w),
SIMD3<Float>(0.0, -h, -w),
SIMD3<Float>(h, -w, 0.0),
SIMD3<Float>(h, w, 0.0),
SIMD3<Float>(-h, -w, 0.0),
SIMD3<Float>(-h, w, 0.0),
SIMD3<Float>(-w, 0.0, -h),
SIMD3<Float>(w, 0.0, -h),
SIMD3<Float>(-w, 0.0, h),
SIMD3<Float>(w, 0.0, h),
SIMD3<Float>(v, v, v),//12
SIMD3<Float>(-v, v, v),
SIMD3<Float>(-v, v, -v),
SIMD3<Float>(v, v, -v),
SIMD3<Float>(v, -v, v),//16
SIMD3<Float>(-v, -v, v),
SIMD3<Float>(-v, -v, -v),
SIMD3<Float>(v, -v, -v),
]
meshPositions.append(contentsOf: points + points + points)
let index: [UInt32] = [
0, 1, 14, 7, 13,
1, 0, 12, 5, 15,
3, 2, 17, 6, 18,
2, 3, 19, 4, 16,
4, 5, 12, 11, 16,
5, 4, 19, 9, 15,
7, 6, 17, 10, 13,
6, 7, 14, 8, 18,
8, 9, 19, 3, 18,
9, 8, 14, 1, 15,
11, 10, 17, 2, 16,
10, 11, 12, 0, 13
]
var countDict: [UInt32:Int] = [:]
for ind in index {
let count = countDict[ind] ?? 0
indices.append(ind + UInt32(pointCount * count))
countDict[ind] = count + 1
}
var newIndices1: [UInt32] = []
for i in 0..<pentagons {
let ai = 5 * i
let bi = 5 * i + 1
let ci = 5 * i + 2
let di = 5 * i + 3
let ei = 5 * i + 4
let i0 = indices[ai]
let i1 = indices[bi]
let i2 = indices[ci]
let i3 = indices[di]
let i4 = indices[ei]
let v0 = meshPositions[Int(i0)]
let v1 = meshPositions[Int(i1)]
let v2 = meshPositions[Int(i2)]
let v3 = meshPositions[Int(i3)]
let v4 = meshPositions[Int(i4)]
let faceCenter = (v0 + v1 + v2 + v3 + v4) / 5
let faceNormal = simd_normalize(faceCenter)
normals[Int(i0)] = faceNormal
normals[Int(i1)] = faceNormal
normals[Int(i2)] = faceNormal
normals[Int(i3)] = faceNormal
normals[Int(i4)] = faceNormal
if res > 0 {
meshPositions.append(faceCenter)
normals.append(faceNormal)
let center = UInt32(vertices + i)
newIndices1.append(contentsOf: [
i0, i1, center,
i1, i2, center,
i2, i3, center,
i3, i4, center,
i4, i0, center
])
}
}
if res > 0 {
vertices += pentagons
indices = newIndices1
}
if res > 1 {
var triangles = pentagons * 5
for _ in 1..<res {
let newTriangles = triangles * 4
let newVertices = vertices + triangles * 3
var newIndices: [UInt32] = []
var pos: SIMD3<Float>
for i in 0..<triangles {
let ai = 3 * i
let bi = 3 * i + 1
let ci = 3 * i + 2
let i0 = indices[ai]
let i1 = indices[bi]
let i2 = indices[ci]
let v0 = meshPositions[Int(i0)]
let v1 = meshPositions[Int(i1)]
let v2 = meshPositions[Int(i2)]
let faceNormal = normals[Int(i0)]
normals.append(contentsOf: [faceNormal, faceNormal, faceNormal])
// a
pos = (v0 + v1) * 0.5
meshPositions.append(pos)
// b
pos = (v1 + v2) * 0.5
meshPositions.append(pos)
// c
pos = (v2 + v0) * 0.5
meshPositions.append(pos)
let a = UInt32(ai + vertices)
let b = UInt32(bi + vertices)
let c = UInt32(ci + vertices)
newIndices.append(contentsOf: [
i0, a, c,
a, i1, b,
a, b, c,
c, b, i2
])
}
indices = newIndices
triangles = newTriangles
vertices = newVertices
}
}
for i in 0..<meshPositions.count {
let p = meshPositions[i]
let n = p
textureMap.append(SIMD2<Float>(abs(atan2(n.x, n.z)) / .pi, 1 - acos(n.y/radius) / .pi))
}
descr.positions = MeshBuffers.Positions(meshPositions)
descr.normals = MeshBuffers.Normals(normals)
descr.textureCoordinates = MeshBuffers.TextureCoordinates(textureMap)
if res == 0 {
descr.primitives = .polygons(Array(repeating: 5, count: pentagons), indices)
} else {
descr.primitives = .triangles(indices)
}
return try MeshResource.generate(from: [descr])
}