说明
圆角矩形平台,就是将一个圆角矩形向上拉伸形成的平台。
几何
第一步,我们可以复用前面圆角矩形的代码,生成一个上表面;
第二步,我们重复一下,得到下表面,注意下表面需要将法线反转,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])
}