S04E06: 圆角矩形平面网格生成

532 阅读3分钟

说明

圆角矩形就是普通的带有圆角的矩形。因为圆角边与矩形边圆角在位置上是连续的,但是斜率(导数)并不连续,没有统一的公式来确定每个点的位置,所以只能分段处理。

几何

我们对圆角矩形的边进行分段处理,如下图:先排列好最内圈红色的点,然后是黄色的圆角 0,接着是洋红色的点……直到排满一圈为止;然后就开始排列下一圈。 中心的大圆点,代表着其实有多个点组成,可以看做是半径为 0 的圆角矩形。这样一来,进行三角形连线时,就可以像原来一样当做扭曲的矩形处理了。

同样的,贴图 UV 坐标也有两种:环绕式,平铺式。需要注意的是:环绕式需要对 X 轴水平正方向进行特殊处理,以使 UV 坐标开始的点和最后一个点不相同。也就是需要在 X 轴正方向上,每一圈多插入两处点,这两个点位置坐标相同,但 UV 坐标不同。这样贴图出来才不会导致此处三角形出现错误的逆向插值。

代码

private static func angle2(a:simd_float2) -> Float {
    var theta = atan2f(a.y, a.x);
    if (theta < 0) { theta += .pi * 2.0; }
    return theta;
}

public static func generateRoundedRectPlane(width: Float, depth: Float, radius: Float, angularResolution: Int = 24, edgeXResolution: Int = 2, edgeYResolution: Int = 2, radialResolution: Int = 2, circleUV: Bool = true) throws -> MeshResource {
    var descr = MeshDescriptor()
    var meshPositions: [SIMD3<Float>] = []
    var indices: [UInt32] = []
    var textureMap: [SIMD2<Float>] = []
    
    let twoPi = Float.pi * 2.0
    let halfPi = Float.pi * 0.5

    let angular = angularResolution > 2 ? angularResolution : 3
    let angularMinusOne = angular - 1
    let angularMinusOnef = Float(angularMinusOne)
    
    let radial = radialResolution > 1 ? radialResolution : 2
    let radialf = Float(radial)
    let radialMinusOnef = radialf - 1.0
        
    let edgeX = edgeXResolution > 1 ? edgeXResolution : 2
    
    let edgeXMinusOne = edgeX - 1
    let edgeXMinusOnef = Float(edgeXMinusOne)
    
    let edgeY = edgeYResolution > 1 ? edgeYResolution : 2
    
    let edgeYMinusOne = edgeY - 1
    let edgeYMinusOnef = Float(edgeYMinusOne)

    let perLoop = (angular - 2) * 4 + (edgeX * 2) + (edgeY * 2) + (circleUV ? 2 : 0)

    let widthHalf = width * 0.5
    let depthHalf = depth * 0.5

    let minDim = (widthHalf < depthHalf ? widthHalf : depthHalf)
    let radius = radius > minDim ? minDim : radius
    
    for j in 0..<radial {
        let n = Float(j) / radialMinusOnef
        
        // +X, -Y -> +Y
        var start = SIMD2<Float>(widthHalf, -depthHalf + radius)
        var end = SIMD2<Float>(widthHalf, depthHalf - radius)
        for i in 0..<edgeY {
            let t = Float(i) / edgeYMinusOnef
            let pos = simd_mix(start, end, SIMD2<Float>(t, t))
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
                
                if i == edgeY/2 - 1 {//start and end UVs are different, so add more points
                    let newPos = SIMD2<Float>(pos.x * n, 0)
                    meshPositions.append(SIMD3<Float>(newPos.x, 0, newPos.y))
                    textureMap.append(SIMD2<Float>(1,uvy))

                    meshPositions.append(SIMD3<Float>(newPos.x, 0, newPos.y))
                    textureMap.append(SIMD2<Float>(0,uvy))
                }
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // corner 0
        for i in 1..<angularMinusOne {
            let t = Float(i) / angularMinusOnef
            let theta = t * halfPi
            let x = radius * cos(theta)
            let y = radius * sin(theta)
            let pos = SIMD2<Float>(widthHalf - radius + x, depthHalf - radius + y)
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // +Y, +X -> -X
        start = SIMD2<Float>(widthHalf - radius, depthHalf)
        end = SIMD2<Float>(-widthHalf + radius, depthHalf)
        for i in 0..<edgeX {
            let t = Float(i) / edgeXMinusOnef
            let pos = simd_mix(start, end, SIMD2<Float>(t, t))
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // corner 1
        for i in 1..<angularMinusOne {
            let t = Float(i) / angularMinusOnef
            let theta = t * halfPi + halfPi
            let x = radius * cos(theta)
            let y = radius * sin(theta)
            let pos = SIMD2<Float>(-widthHalf + radius + x, depthHalf - radius + y)
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // -X, +Y -> -Y
        start = SIMD2<Float>(-widthHalf, depthHalf - radius)
        end = SIMD2<Float>(-widthHalf, -depthHalf + radius)
        for i in 0..<edgeY {
            let t = Float(i) / edgeYMinusOnef
            let pos = simd_mix(start, end, SIMD2<Float>(t, t))
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // corner 2
        for i in 1..<angularMinusOne {
            let t = Float(i) / angularMinusOnef
            let theta = t * halfPi + .pi
            let x = radius * cos(theta)
            let y = radius * sin(theta)
            let pos = SIMD2<Float>(-widthHalf + radius + x, -depthHalf + radius + y)
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // -Y, -X -> +X
        start = SIMD2<Float>(-widthHalf + radius, -depthHalf)
        end = SIMD2<Float>(widthHalf - radius, -depthHalf)
        for i in 0..<edgeX {
            let t = Float(i) / edgeXMinusOnef
            let pos = simd_mix(start, end, SIMD2<Float>(t, t))
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        // corner 3
        for i in 1..<angularMinusOne {
            let t = Float(i) / angularMinusOnef
            let theta = t * halfPi + 1.5 * .pi
            let x = radius * cos(theta)
            let y = radius * sin(theta)
            let pos = SIMD2<Float>(widthHalf - radius + x, -depthHalf + radius + y)
            meshPositions.append(SIMD3<Float>(pos.x, 0, pos.y) * n)
            
            if circleUV {
                let angle = angle2(a: pos)
                let uvx = angle / twoPi
                let uvy = n
                textureMap.append(SIMD2<Float>(uvx,uvy))
            } else {
                textureMap.append(SIMD2<Float>(pos.x * n / width + 0.5, -pos.y * n / depth + 0.5))
            }
        }
        
        for i in 0..<perLoop {
            if j + 1 != radial {
                let currLoop = j * perLoop
                let nextLoop = (j + 1) * perLoop
                let next = (i + 1) % perLoop

                let i0 = UInt32(currLoop + i)
                let i1 = UInt32(currLoop + next)
                let i2 = UInt32(nextLoop + i)
                let i3 = UInt32(nextLoop + next)
                
                indices.append(contentsOf: [i3,i2,i0,
                                            i1,i3,i0
                ])
            }
        }
    }
    
    descr.primitives = .triangles(indices)
    descr.positions = MeshBuffer(meshPositions)
    descr.textureCoordinates = MeshBuffers.TextureCoordinates(textureMap)
    return try .generate(from: [descr])
}