Last active
June 27, 2024 09:13
-
-
Save Matt54/850540e5610a22e5bd161cf66fdae8fb to your computer and use it in GitHub Desktop.
A floating bubble (morphing transparent sphere) RealityView created from a LowLevelMesh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment