Created
June 20, 2024 23:49
-
-
Save Matt54/7086d9e061031ec45d2ee693442e355a to your computer and use it in GitHub Desktop.
A RealityView using a LowLevelMesh to produce a morphing sphere
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 | |
| import GameplayKit | |
| struct MorphingSphereRealityView: View { | |
| @State private var currentEntity: Entity? | |
| @State private var morphFactor: Float = 0.0 | |
| @State private var frameDuration: TimeInterval = 0.0 | |
| @State private var lastUpdateTime = CACurrentMediaTime() | |
| static let animationFrameDuration: TimeInterval = 1.0 / 120.0 | |
| private let timer = Timer.publish(every: animationFrameDuration, on: .main, in: .common).autoconnect() | |
| var body: some View { | |
| RealityView { content in | |
| currentEntity = try! createSphereEntity(morphFactor: 0) | |
| if let entity = currentEntity { | |
| content.add(entity) | |
| } | |
| } update: { content in | |
| if let modelComponent = try? getModelComponent(morphFactor: morphFactor) { | |
| currentEntity?.components.set(modelComponent) | |
| } | |
| } | |
| .onReceive(timer) { input in | |
| let currentTime = CACurrentMediaTime() | |
| frameDuration = currentTime - lastUpdateTime | |
| lastUpdateTime = currentTime | |
| let morphAmount = Float(frameDuration * 0.8) | |
| morphFactor = morphFactor + morphAmount | |
| } | |
| } | |
| func createSphereEntity(morphFactor: Float) throws -> Entity { | |
| let modelComponent = try getModelComponent(morphFactor: morphFactor) | |
| let entity = Entity() | |
| entity.name = "Sphere" | |
| entity.components.set(modelComponent) | |
| entity.scale *= 0.1 | |
| return entity | |
| } | |
| func getModelComponent(morphFactor: Float) throws -> ModelComponent { | |
| let lowLevelMesh = try sphereMesh(morphFactor: morphFactor) | |
| let resource = try MeshResource(from: lowLevelMesh) | |
| var material = PhysicallyBasedMaterial() | |
| material.baseColor.tint = .init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) | |
| material.roughness.scale = 0.0 | |
| material.metallic.scale = 1.0 | |
| material.faceCulling = .none | |
| return ModelComponent(mesh: resource, materials: [material]) | |
| } | |
| func sphereMesh(morphFactor: Float) throws -> LowLevelMesh { | |
| let latitudeBands = 40 | |
| let longitudeBands = 40 | |
| let radius: Float = 0.5 | |
| 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) | |
| var x = cosPhi * sinTheta | |
| var y = cosTheta | |
| var z = sinPhi * sinTheta | |
| let noiseScale: Float = 0.05 | |
| let noiseValueX = customNoise(x: morphFactor + theta, y: phi) * noiseScale | |
| let noiseValueY = customNoise(x: morphFactor + phi, y: theta) * noiseScale | |
| let noiseValueZ = customNoise(x: morphFactor + theta + phi, y: theta) * noiseScale | |
| let noiseScale2: Float = 0.0125 | |
| let noiseValueX2 = perlinNoise(x: morphFactor + theta, y: phi) * noiseScale2 | |
| let noiseValueY2 = perlinNoise(x: morphFactor + phi, y: theta) * noiseScale2 | |
| let noiseValueZ2 = perlinNoise(x: morphFactor + theta + phi, y: theta) * noiseScale2 | |
| x += noiseValueX+noiseValueX2 | |
| y += noiseValueY+noiseValueY2 | |
| z += noiseValueZ+noiseValueZ2 | |
| let position = SIMD3<Float>(x, y, z) * radius | |
| let color = 0xFFFFFFFF | |
| 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 { | |
| // Each quad (rectangle) on the sphere’s surface is made up of two triangles. | |
| /* | |
| first first+1 | |
| * -------- * | |
| | / | | |
| | / | | |
| | / | | |
| | / | | |
| * -------- * | |
| second second+1 | |
| */ | |
| // index of the first vertex of the quad. | |
| let first = (latNumber * (longitudeBands + 1)) + longNumber | |
| // index of the vertex directly below the first vertex. | |
| let second = first + longitudeBands + 1 | |
| // first vertex of the first triangle. | |
| indices[index] = UInt32(first) | |
| // second vertex of the first triangle | |
| indices[index + 1] = UInt32(second) | |
| // third vertex of the first triangle | |
| indices[index + 2] = UInt32(first + 1) | |
| // first vertex of the second triangle | |
| indices[index + 3] = UInt32(second) | |
| // second vertex of the second triangle | |
| indices[index + 4] = UInt32(second + 1) | |
| // third vertex of the second triangle | |
| 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 mesh | |
| } | |
| } | |
| func customNoise(x: Float, y: Float) -> Float { | |
| let value1 = (sin(x * 10.0) + cos(y * 10.0)) * 0.5 | |
| let value2 = (-sin(x * 10.0) + cos(y * 20.0)) * 0.1 | |
| let value3 = (sin(x * 20.0) - cos(y * 10.0)) * 0.05 | |
| return value1 + value2 + value3 | |
| } | |
| func perlinNoise(x: Float, y: Float, seed: Int = 42) -> Float { | |
| let noiseSource = GKPerlinNoiseSource(frequency: 1.0, octaveCount: 6, persistence: 0.5, lacunarity: 2.0, seed: Int32(seed)) | |
| let noise = GKNoise(noiseSource) | |
| let noiseMap = GKNoiseMap(noise, size: vector_double2(1.0, 1.0), origin: vector_double2(Double(x), Double(y)), sampleCount: vector_int2(1, 1), seamless: false) | |
| return Float(noiseMap.value(at: vector_int2(0, 0))) | |
| } | |
| struct MyVertex { | |
| var position: SIMD3<Float> = .zero | |
| var color: UInt32 = .zero | |
| } | |
| extension MyVertex { | |
| 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 | |
| } | |
| } | |
| #Preview { | |
| MorphingSphereRealityView() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment