S04E20: 均匀立方球体网格生成

1,140 阅读3分钟

说明

前一篇说过,当圆角立方体的半径等于边长一半时,就会变成一个球体。这个球体由 6 个面组成,每个面都是弯曲变形的,中间部分突出明显。我们希望对顶点分布进行调整,让点分布更均匀一些。

几何

如何调整这些顶点?这里我参考了知名 Unity 教程网站:CatLikeCoding 的两篇文章:

其中基本原理如下图所示,最早直接将立方体上的点向球心移动,会导致圆角处的点比较集中,而靠近 xyz 轴方向则比较稀疏。我们可以找到一个公式,让这些点在圆上分布更均匀一些。 文中的推导公式如下:

也就是说,我们只需要对最终得到的顶点进行如下操作即可:

var roundPositions: [SIMD3<Float>] = []
for p in meshPositions {
    let n = simd_normalize(p)
    let n2 = n * n  // 计算各分量的平方
    
    let x = n.x * sqrtf(1 - (n2.y + n2.z) / 2 + n2.y * n2.z / 3)
    let y = n.y * sqrtf(1 - (n2.x + n2.z) / 2 + n2.x * n2.z / 3)
    let z = n.z * sqrtf(1 - (n2.x + n2.y) / 2 + n2.x * n2.y / 3)
    
    let newN = simd_normalize(SIMD3<Float>(x, y, z))
    normals.append(newN)
    
    roundPositions.append(newN * radius)
}

最终结果,贴图的变形程度更小

代码

public static func generateCubeSphere(radius: Float, resolution: Int = 10, splitFaces: Bool = false) throws -> MeshResource {
    var descr = MeshDescriptor()
    var meshPositions: [SIMD3<Float>] = []
    var indices: [UInt32] = []
    var normals: [SIMD3<Float>] = []
    var textureMap: [SIMD2<Float>] = []
    var materials: [UInt32] = []
    
    
    let edge = resolution > 2 ? resolution : 3
    let edgeMinusOne = edge - 1
    let edgeMinusOnef = Float(edgeMinusOne)
    let edgeMinusOneSqr = edgeMinusOne * edgeMinusOne
    
    let edgeInc = 2 * radius / edgeMinusOnef
    let facePointCount = edge * edge
    
    // +X
    for j in 0..<edge {
        let startY = radius
        let startZ = radius
        let jf = Float(j)
        let uvy = jf / edgeMinusOnef
        for i in 0..<edge {
            let p = SIMD3<Float>(radius, startY - edgeInc * jf, startZ - edgeInc * Float(i))
            meshPositions.append(p)
            
            let uv = SIMD2<Float>(Float(i) / edgeMinusOnef, 1 - uvy)
            textureMap.append(uv)
            
            if j != edgeMinusOne && i != edgeMinusOne {
                let index = UInt32(i + j * edge)

                let tl = index
                let tr = tl + 1
                let bl = index + UInt32(edge)
                let br = bl + 1

                indices.append(contentsOf: [tl,bl,tr,
                                            tr,bl,br])
            }
        }
    }
    // -X
    for j in 0..<edge {
        let startY = radius
        let startZ = -radius
        let jf = Float(j)
        let uvy = jf / edgeMinusOnef
        for i in 0..<edge {
            let p = SIMD3<Float>(-radius, startY - edgeInc * jf, startZ + edgeInc * Float(i))
            meshPositions.append(p)

            let uv = SIMD2<Float>(Float(i) / edgeMinusOnef, 1 - uvy)
            textureMap.append(uv)
            
            if j != edgeMinusOne && i != edgeMinusOne {
                let index = UInt32(i + j * edge + facePointCount)
                
                let tl = index
                let tr = tl + 1
                let bl = index + UInt32(edge)
                let br = bl + 1
                
                indices.append(contentsOf: [tl,bl,tr,
                                            tr,bl,br])
            }
        }
    }
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 0, count: edgeMinusOneSqr * 4))
    }
    
    // +Y
    for j in 0..<edge {
        let startX = -radius
        let startZ = -radius
        let jf = Float(j)
        let uvy = jf / edgeMinusOnef
        for i in 0..<edge {
            let p = SIMD3<Float>(startX + edgeInc * Float(i), radius, startZ + edgeInc * jf)
            meshPositions.append(p)

            let uv = SIMD2<Float>(Float(i) / edgeMinusOnef, 1 - uvy)
            textureMap.append(uv)
            
            if j != edgeMinusOne && i != edgeMinusOne {
                let index = UInt32(i + j * edge + facePointCount * 2)

                let tl = index
                let tr = tl + 1
                let bl = index + UInt32(edge)
                let br = bl + 1

                indices.append(contentsOf: [tl,bl,tr,
                                            tr,bl,br])
            }
        }
    }
    // -Y
    for j in 0..<edge {
        let startX = radius
        let startZ = -radius
        let jf = Float(j)
        let uvy = jf / edgeMinusOnef
        for i in 0..<edge {
            let p = SIMD3<Float>(startX - edgeInc * Float(i), -radius, startZ + edgeInc * jf)
            meshPositions.append(p)
            
            let uv = SIMD2<Float>(Float(i) / edgeMinusOnef, 1 - uvy)
            textureMap.append(uv)
            
            if j != edgeMinusOne && i != edgeMinusOne {
                let index = UInt32(i + j * edge + facePointCount * 3)
                
                let tl = index
                let tr = tl + 1
                let bl = index + UInt32(edge)
                let br = bl + 1
                
                indices.append(contentsOf: [tl,bl,tr,
                                            tr,bl,br])
            }
        }
    }
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 1, count: edgeMinusOneSqr * 4))
    }
    
    // +Z
    for j in 0..<edge {
        let startY = radius
        let startX = -radius
        let jf = Float(j)
        let uvy = jf / edgeMinusOnef
        for i in 0..<edge {
            let p = SIMD3<Float>(startX + edgeInc * Float(i), startY - edgeInc * jf, radius)
            meshPositions.append(p)
            
            let uv = SIMD2<Float>(Float(i) / edgeMinusOnef, 1 - uvy)
            textureMap.append(uv)
            
            if j != edgeMinusOne && i != edgeMinusOne {
                let index = UInt32(i + j * edge + facePointCount * 4)

                let tl = index
                let tr = tl + 1
                let bl = index + UInt32(edge)
                let br = bl + 1

                indices.append(contentsOf: [tl,bl,tr,
                                            tr,bl,br])
            }
        }
    }
    // -Z
    for j in 0..<edge {
        let startY = radius
        let startX = radius
        let jf = Float(j)
        let uvy = jf / edgeMinusOnef
        for i in 0..<edge {
            let p = SIMD3<Float>(startX - edgeInc * Float(i), startY - edgeInc * jf, -radius)
            meshPositions.append(p)
            
            let uv = SIMD2<Float>(Float(i) / edgeMinusOnef, 1 - uvy)
            textureMap.append(uv)
            
            if j != edgeMinusOne && i != edgeMinusOne {
                let index = UInt32(i + j * edge + facePointCount * 5)
                
                let tl = index
                let tr = tl + 1
                let bl = index + UInt32(edge)
                let br = bl + 1
                
                indices.append(contentsOf: [tl,bl,tr,
                                            tr,bl,br])
            }
        }
    }
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 2, count: edgeMinusOneSqr * 4))
    }
    
    var roundPositions: [SIMD3<Float>] = []
    for p in meshPositions {
        let n = simd_normalize(p)
        let n2 = n * n
        
        let x = n.x * sqrtf(1 - (n2.y + n2.z) / 2 + n2.y * n2.z / 3)
        let y = n.y * sqrtf(1 - (n2.x + n2.z) / 2 + n2.x * n2.z / 3)
        let z = n.z * sqrtf(1 - (n2.x + n2.y) / 2 + n2.x * n2.y / 3)
        
        let newN = simd_normalize(SIMD3<Float>(x, y, z))
        normals.append(newN)
        
        roundPositions.append(newN * radius)
    }
    meshPositions = roundPositions
    
    descr.positions = MeshBuffers.Positions(meshPositions)
    descr.normals = MeshBuffers.Normals(normals)
    descr.textureCoordinates = MeshBuffers.TextureCoordinates(textureMap)
    descr.primitives = .triangles(indices)
    if !materials.isEmpty {
        descr.materials = MeshDescriptor.Materials.perFace(materials)
    }
    return try .generate(from: [descr])
}