Skip to content

Instantly share code, notes, and snippets.

@Matt54
Created July 20, 2025 12:40
Show Gist options
  • Save Matt54/205950b77ffdfae93d114927c76636ec to your computer and use it in GitHub Desktop.
Save Matt54/205950b77ffdfae93d114927c76636ec to your computer and use it in GitHub Desktop.
RealityKit LowLevelMesh Glowing Torus Spinner
import RealityKit
public struct AnimatingTorusSystem: System {
public init(scene: RealityKit.Scene) {}
public func update(context: SceneUpdateContext) {
let entities = context.entities(matching: Self.query,
updatingSystemWhen: .rendering)
for torus in entities.compactMap({ $0 as? LoopingTorusEntity }) {
torus.update(deltaTime: context.deltaTime)
}
}
static let query = EntityQuery(where: .has(AnimatingTorusEntityComponent.self))
}
public struct AnimatingTorusEntityComponent: Component {
public init() {}
}
import RealityKit
import SwiftUI
class LoopingTorusEntity: Entity {
var mesh: LowLevelMesh?
var meshData: LoopingTorusMeshData
var opacity: Float
var startpointEntity: Entity?
var endpointEntity: Entity?
var isResetting: Bool = false
var material: UnlitMaterial?
var baseColor: UIColor = UIColor(red: 0.5, green: 0.875, blue: 0.5, alpha: 1.0)
let sparkRadiusScale: Float = 2.75
var scaleTime: Double = 0.0
init(opacity: Float, settings: TorusMeshSettings, shouldAddEndpoint: Bool = false) async {
self.opacity = opacity
self.meshData = .init(settings: settings)
print("Building Torus with pointsPerRings: \(settings.pointsPerRing) and radius: \(settings.minorRadius)")
super.init()
await setup()
if shouldAddEndpoint {
await addStartpointEntity(radius: settings.minorRadius)
await addEndpointEntity(radius: settings.minorRadius)
}
}
@MainActor @preconcurrency required init() {
fatalError("init() has not been implemented")
}
func setup() async {
let material = await generateAddMaterial(color: baseColor, opacity: opacity)
self.mesh = nil
let mesh = try! VertexData.initializeMesh(vertexCapacity: meshData.maxVertexCount,
indexCapacity: meshData.maxIndexCount)
let meshResource = try! await MeshResource(from: mesh)
components.set(ModelComponent(mesh: meshResource, materials: [material]))
components.set(AnimatingTorusEntityComponent())
self.material = material
self.mesh = mesh
}
}
// MARK: Mesh
extension LoopingTorusEntity {
func resetMesh() {
guard let mesh else { return }
mesh.parts.replaceAll([
LowLevelMesh.Part(
indexCount: 0,
topology: .triangle,
bounds: meshData.boundingBox
)
])
meshData.reset()
meshData.isComplete = true
}
func update(deltaTime: TimeInterval) {
guard meshData.isComplete else {
resetMesh()
return
}
meshData.updateGrowth(deltaTime: deltaTime)
updateGeometry()
breathingScaleEffect(deltaTime: deltaTime)
}
func updateGeometry() {
guard meshData.isComplete else { return }
if endpointEntity != nil {
updateEndpointEntityPosition()
}
if !meshData.isRemoving {
updateVerticesAdding()
updateIndicesAdding()
} else {
updateVerticesRemoving()
updateIndicesRemoving()
}
}
func updateVerticesAdding() {
guard meshData.isComplete else { return }
let drawState = meshData.drawState
let lastSegmentVerticesCompleted = drawState.lastSegmentVerticesCompleted
guard let mesh else { return }
mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in
let vertices = rawBytes.bindMemory(to: VertexData.self)
let nextVertices = lastSegmentVerticesCompleted+1
if lastSegmentVerticesCompleted < meshData.completedRings {
for j in nextVertices...meshData.completedRings {
generateVerticesForRing(vertices: vertices, ringIndex: j, growthProgress: 0)
}
}
if meshData.isComplete && meshData.growingRingProgress != 0 && lastSegmentVerticesCompleted != -1 {
generateVerticesForRing(vertices: vertices, ringIndex: lastSegmentVerticesCompleted, growthProgress: meshData.growingRingProgress)
}
}
meshData.drawState.lastSegmentVerticesCompleted = meshData.completedRings
}
func updateVerticesRemoving() {
guard meshData.isComplete else { return }
guard let mesh else { return }
// we are simply adjusting the positions of the starting ring
let ringToUpdate = meshData.completedRings
mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in
let vertices = rawBytes.bindMemory(to: VertexData.self)
generateVerticesForRing(vertices: vertices, ringIndex: ringToUpdate, growthProgress: meshData.growingRingProgress, isRemoving: true)
}
}
func generateVerticesForRing(
vertices: UnsafeMutableBufferPointer<VertexData>,
ringIndex: Int,
growthProgress: Float,
isRemoving: Bool = false
) {
let pointsPerRing = meshData.pointsPerRing
let numberOfRings = meshData.numberOfRings
let torusMajorRadius = meshData.majorRadius
let torusMinorRadius = meshData.minorRadius
let angleForArcLength = meshData.arcLengthPerSegment * (Float(ringIndex) + growthProgress)
let cosineForArchLength = cos(angleForArcLength)
let sineForArchLength = sin(angleForArcLength)
for pointIndex in 0...pointsPerRing {
let ringPointProgress = Float(pointIndex) / Float(pointsPerRing)
let angleAroundRing = ringPointProgress * 2 * Float.pi
let cosineAroundRing = cos(angleAroundRing)
let sineAroundRing = sin(angleAroundRing)
let xPosition = (torusMajorRadius + torusMinorRadius * cosineAroundRing) * cosineForArchLength
let yPosition = (torusMajorRadius + torusMinorRadius * cosineAroundRing) * sineForArchLength
let zPosition = torusMinorRadius * sineAroundRing
let vertexPosition = SIMD3<Float>(xPosition, yPosition, zPosition)
let vertexNormal = normalize(SIMD3<Float>(
cosineAroundRing * cosineForArchLength,
cosineAroundRing * sineForArchLength,
sineAroundRing
))
// UV coordinates for texture mapping
let arcLengthProgress = Float(ringIndex) / Float(numberOfRings)
let textureCoordinates = SIMD2<Float>(ringPointProgress, arcLengthProgress)
// Calculate final vertex index in the buffer
var effectiveRingIndex: Int = ringIndex
if growthProgress != 0 && !isRemoving {
effectiveRingIndex += 1
}
let vertexIndex = effectiveRingIndex * (pointsPerRing + 1) + pointIndex
vertices[vertexIndex] = VertexData(
position: vertexPosition,
normal: vertexNormal,
uv: textureCoordinates
)
}
}
func updateIndicesAdding() {
guard meshData.isComplete else { return }
guard let mesh else { return }
let drawState = meshData.drawState
let lastSegmentIndicesCompleted = drawState.lastSegmentIndicesCompleted
var currentIndicesToDraw: Int = meshData.completedRings
if meshData.growingRingProgress != 0 {
currentIndicesToDraw += 1
}
guard lastSegmentIndicesCompleted < currentIndicesToDraw else { return }
let pointsPerRing = meshData.pointsPerRing
mesh.withUnsafeMutableIndices { rawIndices in
let indices = rawIndices.bindMemory(to: UInt32.self)
var index = lastSegmentIndicesCompleted * pointsPerRing * 6
// Generate indices for the torus
for ringIndex in lastSegmentIndicesCompleted..<currentIndicesToDraw {
for pointIndex in 0..<pointsPerRing {
// Current ring vertices
let currentRing = UInt32(ringIndex)
let nextRing = UInt32(ringIndex+1)
let currentPoint = UInt32(pointIndex)
let nextPoint = UInt32((pointIndex+1) % pointsPerRing)
// Calculate vertex indices
let v0 = currentRing * UInt32(pointsPerRing+1) + currentPoint
let v1 = nextRing * UInt32(pointsPerRing+1) + currentPoint
let v2 = currentRing * UInt32(pointsPerRing+1) + nextPoint
let v3 = nextRing * UInt32(pointsPerRing+1) + nextPoint
// First triangle
indices[index] = v0
indices[index + 1] = v1
indices[index + 2] = v2
indices[index + 3] = v1
indices[index + 4] = v3
indices[index + 5] = v2
index += 6
}
}
}
mesh.parts.replaceAll([
LowLevelMesh.Part(
indexCount: meshData.indexCount,
topology: .triangle,
bounds: meshData.boundingBox
)
])
meshData.drawState.lastSegmentIndicesCompleted = currentIndicesToDraw
}
func updateIndicesRemoving() {
guard meshData.isComplete else { return }
let ringIndex = meshData.completedRings
guard ringIndex > meshData.drawState.currentRemovalIndicesBookmarked else { return }
// we are slowly stepping through the rings, moving the offset forward to remove ring by ring from the start
let indexOffsetCount = (ringIndex) * (meshData.pointsPerRing) * 6
let indexCount = meshData.maxIndexCount - indexOffsetCount
let indexOffsetInBytes = indexOffsetInBytes(fromIndexCount: indexOffsetCount)
guard let mesh else { return }
mesh.parts.replaceAll([
LowLevelMesh.Part(indexOffset: indexOffsetInBytes, indexCount: indexCount, topology: .triangle, bounds: meshData.boundingBox)
])
meshData.drawState.currentRemovalIndicesBookmarked = ringIndex
}
func indexOffsetInBytes(fromIndexCount indexCount: Int) -> Int {
return indexCount * MemoryLayout<UInt32>.stride
}
}
// MARK: Materials
extension LoopingTorusEntity {
func generateAddMaterial(color: UIColor, opacity: Float) 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: .init(floatLiteral: opacity))
material.writesDepth = false
return material
}
}
// MARK: Endpoint Entity
extension LoopingTorusEntity {
func addStartpointEntity(radius: Float) async {
let startpointEntity = await getSparkEntity(radius: radius)
addChild(startpointEntity)
self.startpointEntity = startpointEntity
updateStartPointEntityPosition()
}
func addEndpointEntity(radius: Float) async {
let endpointEntity = await getSparkEntity(radius: radius)
addChild(endpointEntity)
self.endpointEntity = endpointEntity
updateEndpointEntityPosition()
}
func getSparkEntity(radius: Float) async -> Entity {
let entity = Entity()
let radius = radius * sparkRadiusScale
// Create all sphere entities in parallel using TaskGroup
let numSpheres = 35
await withTaskGroup(of: (Int, Entity).self) { group in
// Add tasks for each sphere entity
for i in 0..<numSpheres {
group.addTask {
let fraction = Float(i) / Float(numSpheres)
let sineReduction = sin(fraction * .pi * 0.5) * 0.7
let sphereRadius = radius * (1.0 - sineReduction)
let opacity = pow(fraction, 4) // Quadratic exaggerates effect
let sphere = await Entity()
let modelComponent = await self.getSparkModelComponent(radius: sphereRadius, opacity: opacity)
await sphere.components.set(modelComponent)
return (i, sphere)
}
}
// Collect results and add children in order
var spheres: [(Int, Entity)] = []
for await result in group {
spheres.append(result)
}
// Sort by index to maintain layer order and add to parent
spheres.sort { $0.0 < $1.0 }
for (_, sphere) in spheres {
entity.addChild(sphere)
}
}
return entity
}
private func breathingScaleEffect(deltaTime: TimeInterval) {
guard let startpointEntity else { return }
guard let endpointEntity else { return }
// scale/breathing logic
self.scaleTime += deltaTime
let scale = 0.025 * sin(Float(self.scaleTime * 4 * .pi / 2.0)) + 0.95
startpointEntity.scale = SIMD3<Float>.init(x: scale, y: scale, z: scale)
endpointEntity.scale = SIMD3<Float>.init(x: scale, y: scale, z: scale)
}
func removeStartAndEndpointEntities() {
if let startpointEntity {
removeChild(startpointEntity)
}
if let endpointEntity {
removeChild(endpointEntity)
}
}
// Only needs to be set once
func updateStartPointEntityPosition() {
guard meshData.isComplete else { return }
startpointEntity?.position = meshData.startpointLocation
}
func updateEndpointEntityPosition() {
guard meshData.isComplete else { return }
endpointEntity?.position = meshData.endpointLocation
}
func getSparkModelComponent(radius: Float, opacity: Float) async -> ModelComponent {
var material = await generateAddMaterial(color: baseColor, opacity: opacity)
material.faceCulling = .back
let sphereMesh = try! MeshResource.generateSpecificSphere(radius: radius, latitudeBands: 8, longitudeBands: 8)
return ModelComponent(mesh: sphereMesh, materials: [material])
}
}
import Foundation
import RealityKit
struct LoopingTorusMeshData: Codable {
var settings: TorusMeshSettings
init(settings: TorusMeshSettings = .init()) {
self.settings = settings
}
// Convenience getters
var majorRadius: Float { return settings.majorRadius }
var minorRadius: Float { return settings.minorRadius }
var numberOfRings: Int { return settings.numberOfRings }
var pointsPerRing: Int { return settings.pointsPerRing }
var growthSpeed: Float = 25
// Magic number to get the final "connection" of the ring to look right
static let overlapAngle: Float = 0.011
var totalCompletedArcLength: Float = Float.pi * 2 + overlapAngle
var isComplete: Bool = true
var isRemoving: Bool = false
var drawState: TorusGrowthState = .init()
struct TorusGrowthState: Codable {
var lastSegmentIndicesCompleted: Int = 0
var lastSegmentVerticesCompleted: Int = -1 // starting on -1 so we create the zero ring
var currentRemovalIndicesBookmarked: Int = 0
}
var boundingBox: BoundingBox {
let maxRadius = majorRadius + minorRadius
let minRadius = majorRadius - minorRadius
return BoundingBox(
min: [-maxRadius, -minRadius, -maxRadius],
max: [maxRadius, minRadius, maxRadius]
)
}
var completedRings: Int = 0
var growingRingProgress: Float = 0
var startpointLocation: SIMD3<Float> {
let arcLengthAlongRing: Float = 0
let x = majorRadius * cos(arcLengthAlongRing)
let y = majorRadius * sin(arcLengthAlongRing)
let z: Float = 0 // Endpoint is at the center of the tube cross-section
return SIMD3<Float>(x: x, y: y, z: z)
}
var endpointLocation: SIMD3<Float> {
var completedRings: Float = Float(self.completedRings)
completedRings += growingRingProgress
let arcLengthAlongRing: Float = arcLengthPerSegment * completedRings
let x = majorRadius * cos(arcLengthAlongRing)
let y = majorRadius * sin(arcLengthAlongRing)
let z: Float = 0 // Endpoint is at the center of the tube cross-section
return SIMD3<Float>(x: x, y: y, z: z)
}
var arcLengthPerSegment: Float {
totalCompletedArcLength / Float(numberOfRings)
}
var indexCount: Int {
var ringCount = completedRings
if growingRingProgress != 0 {
ringCount += 1
}
return pointsPerRing * ringCount * 6
}
// Allocation capacities for mesh initialization
var maxVertexCount: Int {
(numberOfRings + 1) * (pointsPerRing + 1)
}
var maxIndexCount: Int {
(pointsPerRing) * (numberOfRings+1) * 6
}
// Growth state management
mutating func updateGrowth(deltaTime: TimeInterval) {
guard isComplete else { return }
growingRingProgress += growthSpeed * Float(deltaTime)
if growingRingProgress >= 1.0 {
if completedRings == numberOfRings-1 {
growingRingProgress = 0
completedRings = 0
if !isRemoving {
isRemoving = true
} else {
isComplete = false
}
} else {
growingRingProgress = 0
completedRings += 1
}
}
}
mutating func reset() {
completedRings = 0
growingRingProgress = 0
drawState = .init()
isRemoving = false
}
}
import RealityKit
import SwiftUI
struct LoopingTorusView: View {
@State var rootEntity = Entity()
var body: some View {
RealityView { content in
await rootEntity.addChild(StackedLoopingTorusEntity(baseMinorRadius: 0.02))
content.add(rootEntity)
}
}
}
#Preview {
LoopingTorusView()
}
import Foundation
import RealityKit
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
}
}
extension SIMD3 where Scalar: FloatingPoint {
func normalized() -> Self {
let length = sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
return self / length
}
}
import Foundation
import RealityKit
class StackedLoopingTorusEntity: Entity {
init(baseMinorRadius: Float) async {
super.init()
let glowLayers: Int = 8
let baseMinorRadius: Float = 0.02
// How aggressively to reduce quality
let qualityReductionFactor: Float = 0.5
// Minimum quality as ratio of base
let minQualityRatio: Float = 0.125
// Base quality settings
let baseSettings = TorusMeshSettings()
let basePointsPerRing = baseSettings.pointsPerRing
transform.rotation = .init(angle: -.pi*0.5, axis: .init(x: 0, y: 0, z: 1))
// Create all torus entities in parallel using TaskGroup
await withTaskGroup(of: (Int, LoopingTorusEntity).self) { group in
// Add tasks for each torus entity
for i in 0...glowLayers {
group.addTask {
let fraction = Float(i) / Float(glowLayers)
// Sine wave radius distribution
let sineReduction = sin(fraction * .pi * 0.5) * 0.4
let minorRadius = baseMinorRadius * (1.0 - sineReduction)
// Exponential opacity distribution
let opacity = pow(fraction*0.75 + 0.25, 2.0)
// Calculate quality reduction based on layer index
let qualityMultiplier = await self.calculateQualityMultiplier(
layerIndex: i,
totalLayers: glowLayers,
reductionFactor: qualityReductionFactor,
minQualityRatio: minQualityRatio
)
// Apply quality reduction to mesh settings
let adjustedPointsPerRing = max(4, Int(Float(basePointsPerRing) * qualityMultiplier))
let settings = TorusMeshSettings(
minorRadius: minorRadius,
pointsPerRing: adjustedPointsPerRing
)
let entity = await LoopingTorusEntity(opacity: opacity,
settings: settings,
shouldAddEndpoint: i == 0)
return (i, entity)
}
}
// Collect results and add children in order
var entities: [(Int, LoopingTorusEntity)] = []
for await result in group {
entities.append(result)
}
// Sort by index to maintain layer order and add to parent
entities.sort { $0.0 < $1.0 }
for (_, entity) in entities {
addChild(entity)
}
}
}
private func calculateQualityMultiplier(
layerIndex: Int,
totalLayers: Int,
reductionFactor: Float,
minQualityRatio: Float
) -> Float {
guard totalLayers > 0 else { return 1.0 }
if layerIndex == 0 { return 1.0 }
// Calculate normalized position (0.0 to 1.0) where 1.0 is outermost layer
let normalizedPosition = Float(layerIndex) / Float(totalLayers)
// Use power function for smooth quality reduction
let qualityReduction = pow(reductionFactor, normalizedPosition * 3.0)
// Ensure we don't go below minimum quality ratio
return max(minQualityRatio, qualityReduction)
}
@MainActor @preconcurrency required init() {
fatalError("init() has not been implemented")
}
}
import Foundation
struct TorusMeshSettings: Codable {
var majorRadius: Float = 0.15
var minorRadius: Float = 0.01
var numberOfRings: Int = 64
var pointsPerRing: Int = 32
}
import Foundation
import RealityKit
extension VertexData {
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)!),
.init(semantic: .uv0, format: .float2, offset: MemoryLayout<Self>.offset(of: \.uv)!)
]
static var vertexLayouts: [LowLevelMesh.Layout] = [
.init(bufferIndex: 0, bufferStride: MemoryLayout<Self>.stride)
]
static var descriptor: LowLevelMesh.Descriptor {
var desc = LowLevelMesh.Descriptor()
desc.vertexAttributes = VertexData.vertexAttributes
desc.vertexLayouts = VertexData.vertexLayouts
desc.indexType = .uint32
return desc
}
@MainActor static func initializeMesh(vertexCapacity: Int,
indexCapacity: Int) throws -> LowLevelMesh {
var desc = VertexData.descriptor
desc.vertexCapacity = vertexCapacity
desc.indexCapacity = indexCapacity
return try LowLevelMesh(descriptor: desc)
}
}
#include <simd/simd.h>
#ifndef VertexData_h
#define VertexData_h
struct VertexData {
simd_float3 position;
simd_float3 normal;
simd_float2 uv;
};
#endif /* VertexData_h */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment