Created
August 6, 2024 02:16
-
-
Save Matt54/f309f45d0012ff5ca2ee619f40a734a4 to your computer and use it in GitHub Desktop.
RealityView resembling a fireball spell created from many low opacity spheres swarming around with add blend mode applied
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 FireballCirclesAddBlendView: View { | |
@State var rootEntity: Entity? | |
@State var sphereTargets: [Entity: SIMD3<Float>] = [:] | |
@State private var rotationAngles: SIMD3<Float> = [0, 0, 0] | |
@State private var modulationTimer: Timer? | |
@State private var lastRotationUpdateTime = CACurrentMediaTime() | |
var body: some View { | |
RealityView { content in | |
let rootEntity = await FireballCirclesAddBlendView.createRootEntity() | |
content.add(rootEntity) | |
initializeTargets(for: rootEntity) | |
self.rootEntity = rootEntity | |
} | |
.onAppear { startTimer() } | |
.onDisappear { stopTimer() } | |
} | |
private func startTimer() { | |
modulationTimer = Timer.scheduledTimer(withTimeInterval: 1/120.0, repeats: true) { _ in | |
moveChildEntities() | |
rotateRootEntity() | |
} | |
} | |
private func stopTimer() { | |
modulationTimer?.invalidate() | |
modulationTimer = nil | |
} | |
private func moveChildEntities() { | |
guard let rootEntity else { return } | |
let movementSpeed: Float = 0.00025 | |
for child in rootEntity.children { | |
if let target = sphereTargets[child] { | |
let direction = normalize(target - child.position) | |
child.position += direction * movementSpeed | |
if distance(child.position, target) < movementSpeed { | |
sphereTargets[child] = FireballCirclesAddBlendView.generateRandomPosition(bound: 0.025) | |
} | |
} | |
} | |
} | |
private func rotateRootEntity() { | |
let currentTime = CACurrentMediaTime() | |
let frameDuration = currentTime - lastRotationUpdateTime | |
// Rotate along all axis at different rates for a wonky rotation effect | |
let scaleFactor = 1.0 | |
rotationAngles.x += Float(frameDuration * 1.5 * scaleFactor) | |
rotationAngles.y += Float(frameDuration * 0.75 * scaleFactor) | |
rotationAngles.z += Float(frameDuration * 0.5 * scaleFactor) | |
let rotationX = simd_quatf(angle: rotationAngles.x, axis: [1, 0, 0]) | |
let rotationY = simd_quatf(angle: rotationAngles.y, axis: [0, 1, 0]) | |
let rotationZ = simd_quatf(angle: rotationAngles.z, axis: [0, 0, 1]) | |
rootEntity?.transform.rotation = rotationX * rotationY * rotationZ | |
lastRotationUpdateTime = currentTime | |
} | |
static func createRootEntity() async -> Entity { | |
let rootEntity = Entity() | |
// create model component for reuse with all circles | |
let radius: Float = 0.01 | |
let sphereMesh = try! MeshResource.generateSpecificSphere(radius: radius, latitudeBands: 6, longitudeBands: 10) | |
let material = await generateAddMaterial(color: .orange) | |
let modelComponent = ModelComponent(mesh: sphereMesh, materials: [material]) | |
// create many circles | |
let entityCount = 400 | |
for _ in 0..<entityCount { | |
let entity = Entity() | |
entity.position = generateRandomPosition(bound: 0.1) | |
entity.components.set(modelComponent) | |
rootEntity.addChild(entity) | |
} | |
return rootEntity | |
} | |
static func generateAddMaterial(color: UIColor) async -> UnlitMaterial { | |
var descriptor = UnlitMaterial.Program.Descriptor() | |
descriptor.blendMode = .add | |
let prog = await UnlitMaterial.Program(descriptor: descriptor) | |
var material = UnlitMaterial(program: prog) | |
material.color = UnlitMaterial.BaseColor(tint: color) | |
material.blending = .transparent(opacity: 0.075) | |
return material | |
} | |
private func initializeTargets(for rootEntity: Entity) { | |
for child in rootEntity.children { | |
sphereTargets[child] = FireballCirclesAddBlendView.generateRandomPosition(bound: 0.025) | |
} | |
} | |
static func generateRandomPosition(bound: Float) -> SIMD3<Float> { | |
let x = Float.random(in: -bound...bound) | |
let y = Float.random(in: -bound...bound) | |
let z = Float.random(in: -bound...bound) | |
return SIMD3<Float>(x, y, z) | |
} | |
} | |
#Preview { | |
FireballCirclesAddBlendView() | |
} | |
extension MeshResource { | |
static func generateSpecificSphere(radius: Float, latitudeBands: Int = 10, longitudeBands: Int = 10) throws -> MeshResource { | |
let vertexCount = (latitudeBands + 1) * (longitudeBands + 1) | |
let indexCount = latitudeBands * longitudeBands * 6 | |
var desc = MyVertexWithNormal.descriptor | |
desc.vertexCapacity = vertexCount | |
desc.indexCapacity = indexCount | |
let mesh = try LowLevelMesh(descriptor: desc) | |
mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in | |
let vertices = rawBytes.bindMemory(to: MyVertexWithNormal.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) | |
let x = cosPhi * sinTheta | |
let y = cosTheta | |
let z = sinPhi * sinTheta | |
let position = SIMD3<Float>(x, y, z) * radius | |
let normal = -SIMD3<Float>(x, y, z).normalized() | |
vertices[vertexIndex] = MyVertexWithNormal(position: position, normal: normal) | |
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 try MeshResource(from: mesh) | |
} | |
} | |
struct MyVertexWithNormal { | |
var position: SIMD3<Float> = .zero | |
var normal: SIMD3<Float> = .zero | |
static var vertexAttributes: [LowLevelMesh.Attribute] = [ | |
.init(semantic: .position, format: .float3, offset: MemoryLayout<Self>.offset(of: \.position)!), | |
.init(semantic: .normal, format: .float3, offset: MemoryLayout<Self>.offset(of: \.normal)!), | |
] | |
static var vertexLayouts: [LowLevelMesh.Layout] = [ | |
.init(bufferIndex: 0, bufferStride: MemoryLayout<Self>.stride) | |
] | |
static var descriptor: LowLevelMesh.Descriptor { | |
var desc = LowLevelMesh.Descriptor() | |
desc.vertexAttributes = MyVertexWithNormal.vertexAttributes | |
desc.vertexLayouts = MyVertexWithNormal.vertexLayouts | |
desc.indexType = .uint32 | |
return desc | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment