S04E11: 李萨如曲线体网格生成

465 阅读2分钟

说明

Meta公司(原Facebook)开发布会的时候,宣传视频里有这么一段三维动画:

这就是李萨如曲线 (Lissajous Curve)的一种,大家所说的 Meta Logo 就是李萨如曲线的一种形式:即有 2 个完整波动周期( 2*2pi)的李萨如曲线。

几何

从直观上来说,Meta Logo 其实就是对圆环体的扭曲变形,也就是对原来的圆环高度坐标,添加 2* 2pi 个周期的波动就可以了。

但是,如果直接对圆环上的所有点直接添加 y 坐标波动,会让圆管变扁。因此,我们还是需要利用切线,来计算新的法线,进而得到粗细均匀的圆环网格。

首先,我们修改圆环中心点坐标的计算,添加 y 坐标的计算 let centerY = sin(2 * slice) * height * 0.5 。然后在计算切线时,仍然是求导即可。希望大家还记得复合函数求导的公式,这里我们需要对复合函数进行求导得到 tangentY = cos(2 * slice) * 2 * height * 0.5

复合函数求导链式法则:f[(g(x))]' = f'[g(x)] * g'(x),也就是说:sin'(2x) = cos(2x) * 2;sin'(3x) = cos(3x) * 3;sin'(4x) = cos(4x) * 4

最后需要注意的是,由于求出的切线长度并不是 1,所以需要另外进行归一化处理。

当然,这个波动周期也可以是 0,1,2,3,4……等其他数字

代码

public static func generateLissajousCurveTorus(minorRadius: Float, majorRadius: Float, height: Float, cycleTimes: Int = 2, minorResolution :Int = 24, majorResolution: Int = 96) throws -> MeshResource {
    var descr = MeshDescriptor()
    var meshPositions: [SIMD3<Float>] = []
    var indices: [UInt32] = []
    var normals: [SIMD3<Float>] = []
    var textureMap: [SIMD2<Float>] = []
    
    let slices = majorResolution > 3 ? majorResolution : 4
    let angular = minorResolution > 2 ? minorResolution : 3

    let cycleTimesf = Float(cycleTimes)
    let slicesf = Float(slices)
    let angularf = Float(angular)

    let limit = Float.pi * 2.0
    let sliceInc = limit / slicesf
    let angularInc = limit / angularf

    let perLoop = angular + 1
    
    for s in 0...slices {
        let sf = Float(s)
        let slice = sf * sliceInc
        let cosSlice = cos(slice)
        let sinSlice = sin(slice)
        
        let centerX = cosSlice * majorRadius
        let centerY = sin(cycleTimesf * slice) * height * 0.5
        let centerZ = sinSlice * majorRadius
        let center = SIMD3<Float>(centerX, centerY, centerZ)
        
        let tangentN = simd_normalize(SIMD3<Float>(-sinSlice, cos(cycleTimesf * slice) * cycleTimesf * height * 0.5 / majorRadius, cosSlice))
        let vectorN = SIMD3<Float>(cosSlice, 0, sinSlice)
        for a in 0...angular {
            let af = Float(a)
            let angle = af * angularInc
            
            let normal = simd_act(simd_quatf(angle: angle, axis: tangentN), vectorN)

            meshPositions.append(center + normal * minorRadius)
            normals.append(normal)
            textureMap.append(SIMD2<Float>(af / angularf, sf / slicesf))
            
            if (s != slices && a != angular) {
                let index = a + s * perLoop

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

                indices.append(contentsOf: [
                    tl, tr, bl,
                    tr, br, bl
                ])
            }
        }
    }
    
    descr.positions = MeshBuffers.Positions(meshPositions)
    descr.normals = MeshBuffers.Normals(normals)
    descr.textureCoordinates = MeshBuffers.TextureCoordinates(textureMap)
    descr.primitives = .triangles(indices)
    
    return try MeshResource.generate(from: [descr])
}