Created
August 24, 2024 21:00
-
-
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
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 | |
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() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment