S04E18: 圆角矩形平台网格生成

836 阅读3分钟

说明

圆角矩形平台,就是将一个圆角矩形向上拉伸形成的平台。

几何

第一步,我们可以复用前面圆角矩形的代码,生成一个上表面;
第二步,我们重复一下,得到下表面,注意下表面需要将法线反转,UV 反转,还有三角形的顺序也要反转;
第三步,我们将下表面的最外圈抽出来,做为侧向的表面,并连接为三角形;

第三步中比较复杂的是侧面法线的计算,及侧面 UV 坐标的计算。下面我们来分别说明一下。

计算外圈法线时,我们需要先假设圆角矩形内部有个小矩形,然后对比圆角矩形上的点,如果该点的 xy 坐标超出了内部矩形,则将其拉到内部矩形上。然后我们将这个向量反向并归一化,就得到了该点处的法线。 在实际代码中,我们并没有对 xy 进行单独判断,还是使用了效率更高的 clamped() 函数,作用是一样的。

至于侧面的 UV 坐标,我们则需要先计算出周长,然后计算出圆角矩形上 8 个关键点的索引及对应周长,然后再分段进行累加处理。

比如上图,从 key0 开始,向 key1 增长,假设中间有 m 个点,点 p 对应的弧长就是 p.y 与 key0.y 坐标差值;而对于圆角处,则需要根据点 p 与 key1 处的索引差值,乘以每一段的弧长,再加上 key1 处已有的弧长。

最后,需要注意的是,为了 UV 贴图能完美环绕,我们在 x 轴正方向强制插入了两个点分别做为 UV 的起点和终点,并调整了弧长计算的起点,使其与 x 轴正方向对齐。

代码

public static func generateExtrudedRoundedRectPad(width: Float, height: Float, depth: Float, radius: Float, angularResolution: Int = 3, edgeXResolution: Int = 2, edgeYResolution: Int = 2, depthResolution: Int = 2, radialResolution: Int = 2, splitFaces: Bool = false, circleUV: 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 halfDepth = depth * 0.5
    
    let datas = generateRoundedRectPlaneDatas(width: width, height: height, radius: radius, angularResolution: angularResolution, edgeXResolution: edgeXResolution, edgeYResolution: edgeYResolution, radialResolution: radialResolution, circleUV: circleUV)
    let planePositionsCount = datas.meshPositions.count
    
    let topMeshPositions = datas.meshPositions.map({ p in
        return p + SIMD3<Float>(0, halfDepth, 0)
    })
    meshPositions.append(contentsOf: topMeshPositions)
    let bottomMeshPositions = datas.meshPositions.map({ p in
        return p + SIMD3<Float>(0, -halfDepth, 0)
    })
    meshPositions.append(contentsOf: bottomMeshPositions)
    
    normals.append(contentsOf: Array(repeating: SIMD3<Float>(0, 1, 0), count: planePositionsCount))
    normals.append(contentsOf: Array(repeating: SIMD3<Float>(0, -1, 0), count: planePositionsCount))
    
    textureMap.append(contentsOf: datas.textureMap)
    textureMap.append(contentsOf: datas.textureMap.map({ uv in
        return uv * SIMD2<Float>(-1, 1) + SIMD2<Float>(1, 0)
    }))
    
    indices.append(contentsOf: datas.indices)
    var reverseIndices: [UInt32] = []
    let bottomTriangleCount = datas.indices.count / 3
    for i in 1...bottomTriangleCount {
        reverseIndices.append(contentsOf: [
            datas.indices[i*3-1] + UInt32(planePositionsCount),
            datas.indices[i*3-2] + UInt32(planePositionsCount),
            datas.indices[i*3-3] + UInt32(planePositionsCount)])
    }
    indices.append(contentsOf: reverseIndices)
    
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 1, count: bottomTriangleCount * 2))
    }
    
    let angular = angularResolution > 2 ? angularResolution : 3
    let edgeX = edgeXResolution > 1 ? edgeXResolution : 2
    let edgeY = edgeYResolution > 1 ? edgeYResolution : 2
    let edgeDepth = depthResolution > 1 ? depthResolution : 2
    
    let widthHalf = width * 0.5
    let heightHalf = height * 0.5
    let minDim = (widthHalf < heightHalf ? widthHalf : heightHalf)
    let radius = radius > minDim ? minDim : radius
    let innerWidth = width - radius * 2
    let innerHeight = height - radius * 2
    
    // 强行在 x 轴正方向插入 2 个点,以使 UV 在此处发生突变
    let perLoop = (angular - 2) * 4 + (edgeX * 2) + (edgeY * 2) + 2
    let perimeter = innerWidth * 2 + innerHeight * 2 + .pi * radius * 2
    var bottomOutPositions = Array(bottomMeshPositions[(planePositionsCount - perLoop + (circleUV ? 0 : 2))...])
    if !circleUV {//start and end UVs are different, so add more points
        bottomOutPositions.insert(contentsOf: [SIMD3<Float>(widthHalf, -halfDepth, 0), SIMD3<Float>(widthHalf, -halfDepth, 0)], at: edgeY/2)
    }
    
    let depthInc = depth / Float(edgeDepth-1)
    let angularInc = .pi * radius * 0.5 / Float(angular - 1)
    let innerWidthHalf = widthHalf - radius
    let innerHeightHalf = heightHalf - radius
    
    // 计算关键点,即直线与圆弧连接处,共 8 个连接点
    let index1 = edgeY + 2
    let keyIndexes = [0,
                      index1 - 1,
                      index1 + angular - 2,
                      index1 + angular + edgeX - 3,
                      index1 + angular * 2 + edgeX - 4,
                      index1 + angular * 2 + edgeX + edgeY - 5,
                      index1 + angular * 3 + edgeX + edgeY - 6,
                      index1 + angular * 3 + edgeX * 2 + edgeY - 7
    ]
    // 关键点对应的周长
    let keyLengths = [0,
                      innerHeight,
                      innerHeight + .pi * radius * 0.5,
                      innerHeight + innerWidth + .pi * radius * 0.5,
                      innerHeight + innerWidth + .pi * radius,
                      innerHeight * 2 + innerWidth + .pi * radius,
                      innerHeight * 2 + innerWidth + .pi * radius * 1.5,
                      innerHeight * 2 + innerWidth * 2 + .pi * radius * 1.5,
    ]
    let topBottomPositionsCount = UInt32(planePositionsCount * 2)
    
    for j in 0..<edgeDepth {
        let jf = Float(j)
        let d = jf * depthInc
        let uvy = jf / Float(edgeDepth-1)
        let curLoop = j * perLoop
        let nextLoop = (j + 1) * perLoop
        
        for i in 0..<perLoop {
            let p = bottomOutPositions[i]
            meshPositions.append(p + SIMD3<Float>(0, d, 0))
            
            let inner = p.clamped(lowerBound: SIMD3<Float>(-innerWidthHalf, 0, -innerHeightHalf), upperBound: SIMD3<Float>(innerWidthHalf, 0, innerHeightHalf))
            let n = simd_normalize(p - inner)
            normals.append(n)
            
            var length: Float = -innerHeightHalf
            if i <= keyIndexes[1] {
                if i <= edgeY/2 {
                    length += perimeter
                }
                length += keyLengths[0] + abs(p.z - bottomOutPositions[0].z)
            } else if i <= keyIndexes[2] {
                length += keyLengths[1] + angularInc * Float(i - keyIndexes[1])
            } else if i <= keyIndexes[3] {
                length += keyLengths[2] + abs(p.x - bottomOutPositions[keyIndexes[2]].x)
            } else if i <= keyIndexes[4] {
                length += keyLengths[3] + angularInc * Float(i - keyIndexes[3])
            } else if i <= keyIndexes[5] {
                length += keyLengths[4] + abs(p.z - bottomOutPositions[keyIndexes[4]].z)
            } else if i <= keyIndexes[6] {
                length += keyLengths[5] + angularInc * Float(i - keyIndexes[5])
            } else if i <= keyIndexes[7] {
                length += keyLengths[6] + abs(p.x - bottomOutPositions[keyIndexes[6]].x)
            } else {
                length += keyLengths[7] + angularInc * Float(i - keyIndexes[7])
            }
            textureMap.append(SIMD2<Float>(length / perimeter, uvy))
            
            
            var prev = i - 1
            prev = prev < 0 ? (perLoop - 1) : prev
            let curr = i
            let next = (i + 1) % perLoop
            
            if j != edgeDepth - 1 {
                let i0 = UInt32(curLoop + curr) + topBottomPositionsCount
                let i1 = UInt32(curLoop + next) + topBottomPositionsCount
                
                let i2 = UInt32(nextLoop + curr) + topBottomPositionsCount
                let i3 = UInt32(nextLoop + next) + topBottomPositionsCount
                indices.append(contentsOf: [
                    i0, i2, i3,
                    i0, i3, i1
                ])
            }
        }
    }
    
    if splitFaces {
        materials.append(contentsOf: Array(repeating: 0, count: edgeDepth * perLoop * 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 .generate(from: [descr])
}