Created
July 20, 2025 12:40
-
-
Save Matt54/205950b77ffdfae93d114927c76636ec to your computer and use it in GitHub Desktop.
RealityKit LowLevelMesh Glowing Torus Spinner
This file contains hidden or 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 | |
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() {} | |
} |
This file contains hidden or 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 | |
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]) | |
} | |
} |
This file contains hidden or 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 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 | |
} | |
} |
This file contains hidden or 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 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() | |
} |
This file contains hidden or 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 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 | |
} | |
} |
This file contains hidden or 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 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") | |
} | |
} |
This file contains hidden or 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 Foundation | |
struct TorusMeshSettings: Codable { | |
var majorRadius: Float = 0.15 | |
var minorRadius: Float = 0.01 | |
var numberOfRings: Int = 64 | |
var pointsPerRing: Int = 32 | |
} |
This file contains hidden or 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 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) | |
} | |
} |
This file contains hidden or 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
#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