Last active
August 9, 2024 00:26
-
-
Save Matt54/e56254b73b61356b0cac869a1b7f2a9b to your computer and use it in GitHub Desktop.
RealityView LowLevelMesh Cylinder with a wiggling animation
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 SwiftUI | |
import RealityKit | |
struct CylinderWiggleMeshView: View { | |
let radialSegments = 16 | |
let heightSegments = 5 | |
let animationStepAmount: Float = 0.05 | |
let radius: Float = 0.005 | |
let height: Float = 0.15 | |
let displacementRange: Float = 0.005 | |
@State var mesh: LowLevelMesh? | |
@State var currentPositions: [SIMD3<Float>] = [] | |
@State var targetPositions: [SIMD3<Float>] = [] | |
@State var animationProgress: Float = 0 | |
@State var timer: Timer? | |
var body: some View { | |
RealityView { content in | |
let mesh = try! getMesh() | |
let meshResource = try! MeshResource(from: mesh) | |
var material = PhysicallyBasedMaterial() | |
material.faceCulling = .front | |
material.baseColor.tint = .init(red: 0.5, green: 0.5, blue: 1.0, alpha: 1.0) | |
material.metallic = 1.0 | |
material.roughness = 0.0 | |
let entity = ModelEntity(mesh: meshResource, materials: [material]) | |
content.add(entity) | |
currentPositions = generatePositions() | |
targetPositions = generatePositions() | |
updateMesh(mesh) | |
self.mesh = mesh | |
} | |
.onAppear { startTimer() } | |
.onDisappear { stopTimer() } | |
} | |
private func startTimer() { | |
timer = Timer.scheduledTimer(withTimeInterval: 1/120.0, repeats: true) { _ in | |
animationProgress += animationStepAmount | |
if animationProgress >= 1 { | |
currentPositions = targetPositions | |
targetPositions = generatePositions() | |
animationProgress = 0 | |
} | |
if let mesh = mesh { | |
updateMesh(mesh) | |
} | |
} | |
} | |
func stopTimer() { | |
timer?.invalidate() | |
timer = nil | |
} | |
func getMesh() throws -> LowLevelMesh { | |
let vertexCount = (radialSegments + 1) * (heightSegments + 1) | |
let indexCount = radialSegments * heightSegments * 6 | |
var desc = VertexData.descriptor | |
desc.vertexCapacity = vertexCount | |
desc.indexCapacity = indexCount | |
return try LowLevelMesh(descriptor: desc) | |
} | |
func updateMesh(_ mesh: LowLevelMesh) { | |
mesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in | |
let vertices = rawBytes.bindMemory(to: VertexData.self) | |
for y in 0...heightSegments { | |
let currentPosition = currentPositions[y] | |
let targetPosition = targetPositions[y] | |
let interpolatedPosition = mix(currentPosition, targetPosition, t: animationProgress) | |
for x in 0...radialSegments { | |
let angle = Float(x) / Float(radialSegments) * 2 * Float.pi | |
let xOffset = cos(angle) * radius | |
let zOffset = sin(angle) * radius | |
let position = SIMD3<Float>(interpolatedPosition.x + xOffset, interpolatedPosition.y, interpolatedPosition.z + zOffset) | |
let normal = normalize(SIMD3<Float>(-xOffset, 0, -zOffset)) | |
let index = y * (radialSegments + 1) + x | |
vertices[index] = VertexData(position: position, normal: normal) | |
} | |
} | |
} | |
mesh.withUnsafeMutableIndices { rawIndices in | |
let indices = rawIndices.bindMemory(to: UInt32.self) | |
var index = 0 | |
for y in 0..<heightSegments { | |
for x in 0..<radialSegments { | |
let a = y * (radialSegments + 1) + x | |
let b = a + 1 | |
let c = a + (radialSegments + 1) | |
let d = c + 1 | |
indices[index] = UInt32(a) | |
indices[index + 1] = UInt32(b) | |
indices[index + 2] = UInt32(d) | |
indices[index + 3] = UInt32(a) | |
indices[index + 4] = UInt32(d) | |
indices[index + 5] = UInt32(c) | |
index += 6 | |
} | |
} | |
} | |
let meshBounds = BoundingBox(min: [-radius*2, -height/2, -radius*2], max: [radius*2, height/2, radius*2]) | |
mesh.parts.replaceAll([ | |
LowLevelMesh.Part( | |
indexCount: radialSegments * heightSegments * 6, | |
topology: .triangle, | |
bounds: meshBounds | |
) | |
]) | |
} | |
func generatePositions() -> [SIMD3<Float>] { | |
var positions: [SIMD3<Float>] = [] | |
let startPosition = SIMD3<Float>(0, -height/2, 0) | |
let endPosition = SIMD3<Float>(0, height/2, 0) | |
for y in 0...heightSegments { | |
let progress = Float(y) / Float(heightSegments) | |
var centerPosition = mix(startPosition, endPosition, t: progress) | |
if y != 0 && y != heightSegments { | |
centerPosition.x += Float.random(in: -displacementRange...displacementRange) | |
centerPosition.z += Float.random(in: -displacementRange...displacementRange) | |
} | |
positions.append(centerPosition) | |
} | |
return positions | |
} | |
struct VertexData { | |
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 = VertexData.vertexAttributes | |
desc.vertexLayouts = VertexData.vertexLayouts | |
desc.indexType = .uint32 | |
return desc | |
} | |
} | |
} | |
#Preview { | |
CylinderWiggleMeshView() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment