Skip to content

Instantly share code, notes, and snippets.

@Matt54
Created August 24, 2024 21:00
Show Gist options
  • Save Matt54/7254c9f55ac5ac769266d66eb5baed32 to your computer and use it in GitHub Desktop.
Save Matt54/7254c9f55ac5ac769266d66eb5baed32 to your computer and use it in GitHub Desktop.
RealityView loading USDZ file and copying it's TextureResource into a LowLevelTexture for animation using metal
import SwiftUI
import RealityKit
import MetalKit
struct AdjustModelTextureRealityView: View {
@State var entity: ModelEntity?
@State var originalTexture: LowLevelTexture?
@State var fadedTexture: LowLevelTexture?
@State var fadeValue: Float = 0.0
@State var timer: Timer?
@State var isForward: Bool = true
let timerUpdateDuration: TimeInterval = 1/120.0
let fadeInDuration: TimeInterval = 2.5
let device: MTLDevice
let commandQueue: MTLCommandQueue
let textureComputePipeline: MTLComputePipelineState
init() {
self.device = MTLCreateSystemDefaultDevice()!
self.commandQueue = device.makeCommandQueue()!
let library = device.makeDefaultLibrary()!
let updateFunction = library.makeFunction(name: "fadeInKernel")!
self.textureComputePipeline = try! device.makeComputePipelineState(function: updateFunction)
}
var body: some View {
RealityView { content in
let model = try! await loadModelEntity()
self.entity = model
var material = model.model?.materials.first as! PhysicallyBasedMaterial
let baseColorTexture = material.baseColor.texture!.resource
let originalTexture = try! copyTextureResourceToLowLevelTexture(from: baseColorTexture)
let fadedTexture = try! copyTextureResourceToLowLevelTexture(from: baseColorTexture)
let newTextureResource = try! await TextureResource(from: fadedTexture)
material.baseColor.texture = .init(newTextureResource)
material.blending = .transparent(opacity: 1.0)
updateFadedTexture(original: originalTexture, faded: fadedTexture, fadeValue: fadeValue)
model.model?.materials = [material]
content.add(model)
self.originalTexture = originalTexture
self.fadedTexture = fadedTexture
}
.onAppear { startTimer() }
.onDisappear { stopTimer() }
}
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: timerUpdateDuration, repeats: true) { timer in
let stepValue = Float(timerUpdateDuration / fadeInDuration)
if isForward {
fadeValue += stepValue
} else {
fadeValue -= stepValue
}
if fadeValue >= 1.0 {
fadeValue = 1.0
isForward = false
} else if fadeValue < 0 {
fadeValue = 0.0
isForward = true
}
if let original = originalTexture, let faded = fadedTexture {
updateFadedTexture(original: original, faded: faded, fadeValue: fadeValue)
}
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
func copyTextureResourceToLowLevelTexture(from textureResource: TextureResource) throws -> LowLevelTexture {
var descriptor = LowLevelTexture.Descriptor()
descriptor.textureType = .type2D
descriptor.pixelFormat = .rgba16Float // Use a high precision format
descriptor.width = textureResource.width
descriptor.height = textureResource.height
descriptor.mipmapLevelCount = 1
descriptor.textureUsage = [.shaderRead, .shaderWrite]
let texture = try LowLevelTexture(descriptor: descriptor)
try textureResource.copy(to: texture.read())
return texture
}
func updateFadedTexture(original: LowLevelTexture, faded: LowLevelTexture, fadeValue: Float) {
let commandBuffer = commandQueue.makeCommandBuffer()!
let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
computeEncoder.setComputePipelineState(textureComputePipeline)
computeEncoder.setTexture(original.read(), index: 0)
computeEncoder.setTexture(faded.replace(using: commandBuffer), index: 1)
var fade = fadeValue
computeEncoder.setBytes(&fade, length: MemoryLayout<Float>.size, index: 0)
let threadGroupSize = MTLSizeMake(8, 8, 1)
let threadGroups = MTLSizeMake(
(original.descriptor.width + threadGroupSize.width - 1) / threadGroupSize.width,
(original.descriptor.height + threadGroupSize.height - 1) / threadGroupSize.height,
1
)
computeEncoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupSize)
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
}
}
#Preview {
AdjustModelTextureRealityView()
}
#include <metal_stdlib>
using namespace metal;
kernel void fadeInKernel(texture2d<float, access::read> inTexture [[texture(0)]],
texture2d<float, access::write> outTexture [[texture(1)]],
constant float &revealProgress [[buffer(0)]],
uint2 gid [[thread_position_in_grid]])
{
float4 color = inTexture.read(gid);
float2 texCoord = float2(gid) / float2(outTexture.get_width(), outTexture.get_height());
float2 centerPoint = float2(0.5, 0.5);
float2 distanceVector = texCoord - centerPoint;
float distance = length(distanceVector);
if (distance > revealProgress) { color.a = 0.0; }
outTexture.write(color, gid);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment