Skip to content

Instantly share code, notes, and snippets.

@Matt54
Last active July 7, 2024 20:07
Show Gist options
  • Save Matt54/535dbf2f6c0149ed24204b9cbd65000e to your computer and use it in GitHub Desktop.
Save Matt54/535dbf2f6c0149ed24204b9cbd65000e to your computer and use it in GitHub Desktop.
Sun with rays entity created from many low poly spheres at varying opacity and radius + different rotation animation along each axis
import RealityKit
import SwiftUI
struct GlowingLowPolySphere: View {
@State private var opacity: Float = 1.0
@State private var isForward: Bool = false
@State private var rotationAngles: SIMD3<Float> = [0, 0, 0]
@State private var modulationTimer: Timer?
@State private var time: Double = 0.0
@State private var lastRotationUpdateTime = CACurrentMediaTime()
@State private var rootEntity: Entity?
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)
let entity = glowingSphereEntity(radius: radius)
content.add(entity)
entity.components.set(OpacityComponent(opacity: opacity))
rootEntity = entity
} update: { content in
if let entity = content.entities.first {
entity.components.set(OpacityComponent(opacity: opacity))
}
}
}
.onAppear {
startOpacityTimer()
}
}
private func startOpacityTimer() {
Timer.scheduledTimer(withTimeInterval: 1/120.0, repeats: true) { _ in
if isForward {
opacity += 0.01
if opacity >= 1.0 {
isForward = false
}
} else {
opacity -= 0.01
if opacity <= 0.0 {
isForward = true
}
}
let currentTime = CACurrentMediaTime()
let frameDuration = currentTime - lastRotationUpdateTime
self.time += frameDuration
// Rotate along all axis at different rates for a wonky rotation effect
rotationAngles.x += Float(frameDuration * 1.5)
rotationAngles.y += Float(frameDuration * 0.7)
rotationAngles.z += Float(frameDuration * 0.45)
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
}
}
func glowingSphereEntity(radius: Float) -> Entity {
let sphereEntity = Entity()
let numSpheres = 50
for i in 0..<numSpheres {
let fraction = Float(i) / Float(numSpheres)
let sphereRadius = radius * (1.0 - fraction * 1.0)
let opacity = pow(fraction, 3) // Quadratic exaggerates effect
let sphere = Entity()
sphere.components.set(getModelComponent(radius: sphereRadius, opacity: opacity))
sphereEntity.addChild(sphere)
}
sphereEntity.scale *= scalePreviewFactor
return sphereEntity
}
func getModelComponent(radius: Float, opacity: Float) -> ModelComponent {
var material = UnlitMaterial()
material.color.tint = .yellow
material.blending = .transparent(opacity: .init(floatLiteral: opacity))
material.faceCulling = .back
let sphereMesh = try! MeshResource.generateSpecificSphere(radius: radius, latitudeBands: 8, longitudeBands: 8)
return ModelComponent(mesh: sphereMesh, materials: [material])
}
}
#Preview {
GlowingLowPolySphere()
}
var isPreview: Bool {
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
var scalePreviewFactor: Float = isPreview ? 0.3 : 1.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
}
}
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 = 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)
let x = cosPhi * sinTheta
let y = cosTheta
let z = sinPhi * sinTheta
let position = SIMD3<Float>(x, y, z) * radius
let color = 0xFFFFFFFF // White color for simplicity
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
)
])
// Print the number of triangles
let triangleCount = indexCount / 3
print("Number of triangles: \(triangleCount)")
return try MeshResource(from: mesh)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment