S04E09: 胶囊体网格生成

250 阅读3分钟

说明

RealityKit 中已经自带了胶囊体,但我们可以写一个更多网格的,更加细分的几何体。

几何

胶囊体构造过程与圆柱体也是类似的,只是把上下平面换成了两个半球面。法线也稍有不同,但比较麻烦的是 UV 贴图与之前的不同,需要考虑是将胶囊体表面作为一个连续的表面进行贴图,还是将上下半球面与中间的圆柱面分开贴图。 分开计算其实比较简单,就是上中下分为三个部分,每个部分都有自己的 UV 和贴图;而合并成一个反而是需要按表面长度进行计算,我们可以计算出单边的总长度是 let totalSurfaceLength = height + .pi * radius 再根据上下半球的角度与中间段高度,在总长度中的占比:vPerCapvPerCyl ,进而得到一个完整的 UV 范围。

代码

public static func generateCapsule(radius: Float, height: Float, angularResolution: Int = 24, radialResolution: Int = 1, verticalResolution: Int = 1, 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 phi = angularResolution > 2 ? angularResolution : 3;
    let theta = radialResolution > 0 ? radialResolution : 1;
    let slices = verticalResolution > 0 ? verticalResolution : 1;
    
    let phif = Float(phi)
    let thetaf = Float(theta)
    let slicesf = Float(slices)
    
    let phiMax = Float.pi * 2.0
    let thetaMax = Float.pi * 0.5
    
    let phiInc = phiMax / phif
    let thetaInc = thetaMax / thetaf
    let heightInc = height / slicesf
    
    let halfHeight = height * 0.5
    let totalSurfaceLength = height + .pi * radius
    let vPerCap = thetaMax * radius / totalSurfaceLength
    let vPerCyl = height / totalSurfaceLength
    
    let perLoop = phi + 1
    let verticesPerCap = perLoop * (theta + 1)
    
    // top cap
    var textureOutMin: Float = vPerCap + vPerCyl
    var textureOutMax: Float = 1
    if splitFaces {
        textureOutMin = 0
    }
    for t in 0...theta {
        let tf = Float(t)
        let thetaAngle = tf * thetaInc
        let cosTheta = cos(thetaAngle)
        let sinTheta = sin(thetaAngle)
        
        for p in 0...phi {
            let pf = Float(p)
            let phiAngle = pf * phiInc
            let cosPhi = cos(phiAngle)
            let sinPhi = sin(phiAngle)
            
            let x = cosPhi * sinTheta
            let z = sinPhi * sinTheta
            let y = cosTheta
            
            
            meshPositions.append(SIMD3<Float>(radius * x, radius * y + halfHeight, radius * z))
            normals.append(SIMD3<Float>(x, y, z))
            textureMap.append(SIMD2<Float>(1 - pf / phif, map(input: thetaMax - thetaAngle, inMin: 0.0, inMax: thetaMax, outMin: textureOutMin, outMax: textureOutMax)))
            
            if(p != phi && t != theta) {
                let index = p + t * perLoop
                
                let tl = UInt32(index)
                let tr = tl + 1
                let bl = UInt32(index + perLoop)
                let br = bl + 1
                
                indices.append(contentsOf: [
                    tl, tr, br,
                    tl, br, bl
                ])
            }
        }
    }
    
    // bottom cap
    if splitFaces {//reverse at bottom
        textureOutMin = 1
        textureOutMax = 0
    } else {
        textureOutMin = 0
        textureOutMax = vPerCap
    }
    for t in 0...theta {
        let tf = Float(t)
        let thetaAngle = tf * thetaInc
        let cosTheta = cos(thetaAngle)
        let sinTheta = sin(thetaAngle)
        
        for p in 0...phi {
            let pf = Float(p)
            let phiAngle = pf * phiInc
            let cosPhi = cos(phiAngle)
            let sinPhi = sin(phiAngle)
            
            let x = cosPhi * sinTheta
            let z = sinPhi * sinTheta
            let y = -cosTheta
            
            
            meshPositions.append(SIMD3<Float>(radius * x, radius * y - halfHeight, radius * z))
            normals.append(SIMD3<Float>(x, y, z))
            textureMap.append(SIMD2<Float>(splitFaces ? pf / phif : 1 - pf / phif, map(input: thetaAngle - thetaMax, inMin: -thetaMax, inMax: 0, outMin: textureOutMin, outMax: textureOutMax)))
            
            if(p != phi && t != theta) {
                let index = verticesPerCap + p + t * perLoop
                
                let tl = UInt32(index)
                let tr = tl + 1
                let bl = UInt32(index + perLoop)
                let br = bl + 1
                
                indices.append(contentsOf: [
                    tl, bl, tr,
                    tr, bl, br
                ])
            }
        }
    }
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 1, count: theta * phi * 4))
    }
    
    if splitFaces {
        textureOutMin = 0
        textureOutMax = 1
    } else {
        textureOutMin = vPerCap
        textureOutMax = vPerCap + vPerCyl
    }
    for s in 0...slices {
        let sf = Float(s)
        let y = sf * heightInc
        
        for p in 0...phi {
            let pf = Float(p)
            let phiAngle = pf * phiInc
            let cosPhi = cos(phiAngle)
            let sinPhi = sin(phiAngle)
            
            let x = cosPhi
            let z = sinPhi
            
            meshPositions.append(SIMD3<Float>(radius * x, y - halfHeight, radius * z))
            normals.append(SIMD3<Float>(x, 0, z))
            textureMap.append(SIMD2<Float>(1 - pf / phif, map(input: sf, inMin: 0, inMax: slicesf, outMin: textureOutMin, outMax: textureOutMax)))
            
            if(p != phi && s != slices) {
                let index = verticesPerCap * 2 + p + s * perLoop
                
                let tl = UInt32(index)
                let tr = tl + 1
                let bl = UInt32(index + perLoop)
                let br = bl + 1
                
                indices.append(contentsOf: [
                    tl, bl, tr,
                    tr, bl, br
                ])
            }
        }
    }
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 0, count: slices * phi * 2))
    }
    
    
    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 MeshResource.generate(from: [descr])
}