A floating bubble (morphing transparent sphere) RealityView created from a LowLevelMesh

June 25, 2024 ยท View on GitHub

import RealityKit import SwiftUI

struct BubbleRealityView: View { @State private var currentEntity: Entity? @State private var morphFactor: Float = 0.0 @State private var frameDuration: TimeInterval = 0.0 @State private var lastUpdateTime = CACurrentMediaTime()

static let animationFrameDuration: TimeInterval = 1.0 / 120.0
private let timer = Timer.publish(every: animationFrameDuration, on: .main, in: .common).autoconnect()

var body: some View {
    GeometryReader3D { proxy in
        RealityView { content in
            let size = content.convert(proxy.frame(in: .local), from: .local, to: .scene).extents
            let radius = Float(0.5 * size.x)
            currentEntity = try! createSphereEntity(morphFactor: 0, radius: radius)
            if let entity = currentEntity {
                content.add(entity)
            }
        } update: { content in
            let size = content.convert(proxy.frame(in: .local), from: .local, to: .scene).extents
            let radius = Float(0.5 * size.x)
            if let modelComponent = try? getModelComponent(morphFactor: morphFactor, radius: radius) {
                currentEntity?.components.set(modelComponent)
            }
        }
        .onReceive(timer) { _ in
            let currentTime = CACurrentMediaTime()
            frameDuration = currentTime - lastUpdateTime
            lastUpdateTime = currentTime
            let morphAmount = Float(frameDuration)
            morphFactor = morphFactor + morphAmount*1.125
        }
    }
}

func createSphereEntity(morphFactor: Float, radius: Float = 0.5) throws -> Entity {
    let modelComponent = try getModelComponent(morphFactor: morphFactor, radius: radius)
    
    let entity = Entity()
    entity.name = "Sphere"
    entity.components.set(modelComponent)
    entity.scale *= 0.25
    return entity
}

func getModelComponent(morphFactor: Float, radius: Float = 0.5) throws -> ModelComponent {
    let lowLevelMesh = try sphereMesh(morphFactor: morphFactor, radius: radius)
    let resource = try MeshResource(from: lowLevelMesh)
    
    var material = PhysicallyBasedMaterial()
    material.baseColor.tint = .init(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
    material.roughness.scale = 0.0
    material.faceCulling = .front
    material.blending = .transparent(opacity: 0.1)

    return ModelComponent(mesh: resource, materials: [material])
}

func sphereMesh(morphFactor: Float, radius: Float = 0.5) throws -> LowLevelMesh {
    let latitudeBands = 30
    let longitudeBands = 50
    
    let vertexCount = (latitudeBands + 1) * (longitudeBands + 1)
    let indexCount = latitudeBands * longitudeBands * 6
    
    var desc = MyVertex.descriptor
    desc.vertexCapacity = vertexCount
    desc.indexCapacity = indexCount
    
    let mesh = try LowLevelMesh(descriptor: desc)
    
    mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in
        let vertices = rawBytes.bindMemory(to: MyVertex.self)
        var vertexIndex = 0
        
        for latNumber in 0...latitudeBands {
            let theta = Float(latNumber) * Float.pi / Float(latitudeBands)
            let sinTheta = sin(theta)
            let cosTheta = cos(theta)
            
            for longNumber in 0...longitudeBands {
                let phi = Float(longNumber) * 2 * Float.pi / Float(longitudeBands)
                let sinPhi = sin(phi)
                let cosPhi = cos(phi)
                
                var x = cosPhi * sinTheta
                var y = cosTheta
                var z = sinPhi * sinTheta
                
                let scaleFactorX: Float = 1.0 + 0.04 * sin(morphFactor * 1.0)
                let scaleFactorY: Float = 1.0 + 0.08 * cos(morphFactor * 1.5)
                let scaleFactorZ: Float = 1.0 + 0.12 * sin(morphFactor * 2.0)
                
                x *= scaleFactorX
                y *= scaleFactorY
                z *= scaleFactorZ
                
                let noiseScale: Float = 0.0375
                let noiseValueX = smoothNoise(x: morphFactor + theta, y: phi) * noiseScale
                let noiseValueY = smoothNoise(x: morphFactor + phi, y: theta) * noiseScale
                let noiseValueZ = smoothNoise(x: morphFactor + theta + phi, y: theta) * noiseScale
                
                x += noiseValueX
                y += noiseValueY
                z += noiseValueZ

                let position = SIMD3<Float>(x, y, z) * radius
                let color = 0xFFFFFFFF
                vertices[vertexIndex] = MyVertex(position: position, color: UInt32(color))
                vertexIndex += 1
            }
        }
    }
    
    mesh.withUnsafeMutableIndices { rawIndices in
        let indices = rawIndices.bindMemory(to: UInt32.self)
        var index = 0
        
        for latNumber in 0..<latitudeBands {
            for longNumber in 0..<longitudeBands {
                let first = (latNumber * (longitudeBands + 1)) + longNumber
                let second = first + longitudeBands + 1
                
                indices[index] = UInt32(first)
                indices[index + 1] = UInt32(second)
                indices[index + 2] = UInt32(first + 1)
                indices[index + 3] = UInt32(second)
                indices[index + 4] = UInt32(second + 1)
                indices[index + 5] = UInt32(first + 1)
                
                index += 6
            }
        }
    }
    
    let meshBounds = BoundingBox(min: [-radius, -radius, -radius], max: [radius, radius, radius])
    mesh.parts.replaceAll([
        LowLevelMesh.Part(
            indexCount: indexCount,
            topology: .triangle,
            bounds: meshBounds
        )
    ])
    
    return mesh
}

func smoothNoise(x: Float, y: Float) -> Float {
    return 0.2 * sin(x * 2.0) + 0.2 * cos(y * 2.0)
}

struct MyVertex {
    var position: SIMD3<Float> = .zero
    var color: UInt32 = .zero
    
    static var vertexAttributes: [LowLevelMesh.Attribute] = [
        .init(semantic: .position, format: .float3, offset: MemoryLayout<Self>.offset(of: \.position)!),
        .init(semantic: .color, format: .uchar4Normalized_bgra, offset: MemoryLayout<Self>.offset(of: \.color)!)
    ]


    static var vertexLayouts: [LowLevelMesh.Layout] = [
        .init(bufferIndex: 0, bufferStride: MemoryLayout<Self>.stride)
    ]


    static var descriptor: LowLevelMesh.Descriptor {
        var desc = LowLevelMesh.Descriptor()
        desc.vertexAttributes = MyVertex.vertexAttributes
        desc.vertexLayouts = MyVertex.vertexLayouts
        desc.indexType = .uint32
        return desc
    }
}

}

#Preview { BubbleRealityView() }