S04E15:正十二面体网格生成

1,019 阅读5分钟

说明

正十二面体,每个面是由正五边形组成,每个顶点被 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])
}