Skip to content

Instantly share code, notes, and snippets.

@Matt54
Created August 26, 2024 00:10
Show Gist options
  • Save Matt54/c24e9163d70905fe3b14cb5fc209c9f8 to your computer and use it in GitHub Desktop.
Save Matt54/c24e9163d70905fe3b14cb5fc209c9f8 to your computer and use it in GitHub Desktop.
RealityView loading a USDZ model, extracting a LowLevelMesh from it, and then animating a warp reveal of it's vertices
#include <metal_stdlib>
using namespace metal;
struct VertexData {
float3 position;
float3 normal;
float2 uv;
};
kernel void displacementRevealKernel(device VertexData* vertices [[buffer(0)]],
constant float& revealProgress [[buffer(1)]],
uint vid [[thread_position_in_grid]])
{
if (vertices[vid].position.y > revealProgress) {
vertices[vid].position = float3(0, 100000, 0);
}
}
import SwiftUI
import RealityKit
import Metal
struct LowLevelMeshRevealView: View {
@State var entity: ModelEntity?
@State var lowLevelMesh: LowLevelMesh?
@State var originalVertices: [VertexData] = []
@State var timer: Timer?
@State var isForward: Bool = true
@State var revealProgress: Float = -0.55 // Start slightly below the model
let timerUpdateDuration: TimeInterval = 1/120.0
let bottomValue: Float = -0.375
let topValue: Float = 0.55
let device: MTLDevice
let commandQueue: MTLCommandQueue
let computePipelineState: MTLComputePipelineState
init() {
self.device = MTLCreateSystemDefaultDevice()!
self.commandQueue = device.makeCommandQueue()!
let library = device.makeDefaultLibrary()!
let kernelFunction = library.makeFunction(name: "displacementRevealKernel")!
self.computePipelineState = try! device.makeComputePipelineState(function: kernelFunction)
}
var body: some View {
RealityView { content in
let model = try! await loadModelEntity()
content.add(model)
let lowLevelMesh = createMesh(from: model)!
// Store original vertex data
lowLevelMesh.withUnsafeBytes(bufferIndex: 0) { buffer in
let vertices = buffer.bindMemory(to: VertexData.self)
self.originalVertices = Array(vertices)
}
model.model?.mesh = try! await MeshResource(from: lowLevelMesh)
self.entity = model
self.lowLevelMesh = lowLevelMesh
}
.onAppear { startTimer() }
.onDisappear { stopTimer() }
}
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: timerUpdateDuration, repeats: true) { timer in
let stepValue: Float = 0.0025
if isForward {
revealProgress += stepValue
} else {
revealProgress -= stepValue
}
if revealProgress >= topValue {
revealProgress = topValue
isForward = false
} else if revealProgress < bottomValue {
revealProgress = bottomValue
isForward = true
}
print(revealProgress)
updateMesh()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
func createMesh(from modelEntity: ModelEntity) -> LowLevelMesh? {
guard let meshResource = modelEntity.model?.mesh,
let meshPart = meshResource.contents.models.first?.parts.first else {
return nil
}
let positions = meshPart[MeshBuffers.positions]?.elements ?? []
let normals = meshPart[MeshBuffers.normals]?.elements ?? []
let textureCoordinates = meshPart[MeshBuffers.textureCoordinates]?.elements ?? []
let triangleIndices = meshPart.triangleIndices?.elements ?? []
print("positions.count \(positions.count)")
print("normals.count \(normals.count)")
print("textureCoordinates.count \(textureCoordinates.count)")
print("triangleIndices.count \(triangleIndices.count)")
var descriptor = VertexData.descriptor
descriptor.vertexCapacity = positions.count
descriptor.indexCapacity = triangleIndices.count
guard let lowLevelMesh = try? LowLevelMesh(descriptor: descriptor) else {
return nil
}
// Copy vertex data
lowLevelMesh.withUnsafeMutableBytes(bufferIndex: 0) { buffer in
let vertices = buffer.bindMemory(to: (SIMD3<Float>, SIMD3<Float>, SIMD2<Float>).self)
for i in 0..<positions.count {
vertices[i] = (positions[i], normals[i], textureCoordinates[i])
}
}
// Copy index data
lowLevelMesh.withUnsafeMutableIndices { buffer in
let indices = buffer.bindMemory(to: UInt32.self)
for (index, triangleIndex) in triangleIndices.enumerated() {
indices[index] = UInt32(triangleIndex)
}
}
// Set up parts
let bounds = meshResource.bounds
lowLevelMesh.parts.replaceAll([
LowLevelMesh.Part(
indexCount: triangleIndices.count,
topology: .triangle,
bounds: bounds
)
])
return lowLevelMesh
}
func updateMesh() {
guard let mesh = lowLevelMesh,
let commandBuffer = commandQueue.makeCommandBuffer(),
let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return }
// Reset mesh to original state
mesh.withUnsafeMutableBytes(bufferIndex: 0) { buffer in
let vertices = buffer.bindMemory(to: VertexData.self)
for i in 0..<originalVertices.count {
vertices[i] = originalVertices[i]
}
}
let vertexBuffer = mesh.replace(bufferIndex: 0, using: commandBuffer)
computeEncoder.setComputePipelineState(computePipelineState)
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 0)
computeEncoder.setBytes(&revealProgress, length: MemoryLayout<Float>.size, index: 1)
let threadsPerGrid = MTLSize(width: mesh.vertexCapacity, height: 1, depth: 1)
let threadsPerThreadgroup = MTLSize(width: 64, height: 1, depth: 1)
computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
computeEncoder.endEncoding()
commandBuffer.commit()
}
func loadModelEntity(url: URL = URL(string: "https://matt54.github.io/Resources/Anubis_Statue_1.usdz")!) async throws -> ModelEntity {
let (downloadedURL, _) = try await URLSession.shared.download(from: url)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsDirectory.appendingPathComponent("downloadedModel.usdz")
if FileManager.default.fileExists(atPath: destinationURL.path) {
try FileManager.default.removeItem(at: destinationURL)
}
try FileManager.default.moveItem(at: downloadedURL, to: destinationURL)
let entity = try await ModelEntity.init(contentsOf: destinationURL)
try FileManager.default.removeItem(at: destinationURL)
return entity
}
struct VertexData {
var position: SIMD3<Float> = .zero
var normal: SIMD3<Float> = .zero
var uv: SIMD2<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)!),
.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
}
}
}
#Preview {
LowLevelMeshRevealView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment