S04E12: 正四面体网格生成

550 阅读2分钟

说明

正多面体,也叫 柏拉图多面体,共有五种正多面体:即是正四面体、正六面体、正八面体、正十二面体和正二十面体。接下来我们从正四面体开始,用代码构造全部 5 个正多面体系列。

类型英文名面数棱数顶点数每面边数每顶点棱数
正4面体Tetrahedron46433
正6面体Hexahedron612843
正8面体Octahedron812634
正12面体Dogecahedron12302053
正20面体Icosahedron20301235

几何

第一步,根据网络百科可得知正四面体有以下几何关系: 第二步,根据这些几何关系,求出四个点的坐标:

let a: Float = 4 * radius / sqrtf(6)//棱长
let r = radius / 3 //内切球半径
let bz = sqrtf(2) * 2 * r
let points: [SIMD3<Float>] = [
    SIMD3<Float>(0, radius, 0),
    SIMD3<Float>(a/2, -r, -sqrtf(2)*r),
    SIMD3<Float>(0, -r, bz),
    SIMD3<Float>(-a/2, -r, -sqrtf(2)*r)
]
//第三步修改这里,重复三次,以便对应不同的法线,让面显示出棱角
//meshPositions.append(contentsOf: points + points + points)

第三步,确定哪三个点组成三角形,注意逆时针旋转才是正面。但这里有个问题:如果两个面共用顶点,那也会共用法线,这样两个面就无法形成锋利的锐角。所以我们需要修改的第二步将顶点重复三次,并重新建立索引。

let index: [UInt32] = [
    0, 2, 1,
    0, 3, 2,
    0, 1, 3,
    2, 3, 1
]
var countDict: [UInt32:Int] = [:]
for ind in index {
    let count = countDict[ind] ?? 0
    indices.append(ind + UInt32(pointCount * count))
    countDict[ind] = count + 1
}

因为前面的顶点是 4 个点按顺序重复三次,那么我们在建立索引时,也需要统计每个点用过几次了。如果点 a 已经被用过 2 次了,那么下次再用到时,应该使用第三轮中的点:也就是点 a 原本的索引 ind,加上每轮点数 pointCount * 2 。

第四步,求出各个面的法线,这里可以将每个面的顶点取出,求平均值也就是中心点坐标,归一化之后做为整个面的法线,注意顺序要和顶点一致。

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 = simd_normalize((v0 + v1 + v2) / 3)
    normals[Int(i0)] = faceNormal
    normals[Int(i1)] = faceNormal
    normals[Int(i2)] = faceNormal
}

第五步,这一步是可选的,细分后贴图更细腻,但不影响几何形状。我们可以做一个平面的细分,也就是把每个面的三角形细分成 4 个小三角形,取各个边的中心,然后连接成新的小三角形。注意也要同时增加这三个点的法线。

第六步,我们直接根据点的位置,计算一下贴图坐标 UV。这里,我们根据正四面体的外接球,来计算贴图坐标:假设有个外接球,先把图贴在球体上,再计算正四面体各顶点在球体上的坐标来得到贴图 UV。由于正四面体顶点受限,不能任意增加顶点作为起点和终点,无法实现完美的环绕贴图,所以这里贴图采用了对称模式。

代码

/// 正四面体,radius 为外接球半径,res 三角面剖分次数
public static func generateTetrahedron(radius: Float, res: Int = 0) throws -> MeshResource {
    let pointCount = 4
    var triangles = 4
    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 a: Float = 4 * radius / sqrtf(6)//棱长
    let r = radius / 3 //内切球半径
    let bz = sqrtf(2) * 2 * r
    let points: [SIMD3<Float>] = [
        SIMD3<Float>(0, radius, 0),
        SIMD3<Float>(a/2, -r, -sqrtf(2)*r),
        SIMD3<Float>(0, -r, bz),
        SIMD3<Float>(-a/2, -r, -sqrtf(2)*r)
    ]
    meshPositions.append(contentsOf: points + points + points)
    
    let index: [UInt32] = [
        0, 2, 1,
        0, 3, 2,
        0, 1, 3,
        2, 3, 1
    ]
    var countDict: [UInt32:Int] = [:]
    for ind in index {
        let count = countDict[ind] ?? 0
        indices.append(ind + UInt32(pointCount * count))
        countDict[ind] = count + 1
    }
    
    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 = simd_normalize((v0 + v1 + v2) / 3)
        normals[Int(i0)] = faceNormal
        normals[Int(i1)] = faceNormal
        normals[Int(i2)] = faceNormal
    }
    
    for _ in 0..<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)
    descr.primitives = .triangles(indices)
    return try MeshResource.generate(from: [descr])
}