禁止轉(zhuǎn)載
AR應用, 用RealityKit呈現(xiàn), 要實現(xiàn)拍照與錄像的功能, github上搜到有個庫, 但是只支持ARSCNView, 而且感覺這個功能算是比較正常的功能吧, 竟然沒搜到其他實現(xiàn)的, 踩了不少的坑只實現(xiàn)了拍照, 不過效果還算不錯.
此方法只適用于iOS15及之后.
開始的思路是獲取到ARFrame的一幀, 然后給存到相冊. 正好ARSessionDelegate的session(_ session: ARSession, didUpdate frame: ARFrame)方法會返回ARFrame, ARFrame包含capturedImage. 結(jié)果這個capturedImage是只有相機的內(nèi)容, 看網(wǎng)上有人說他是拿到相機內(nèi)容后再用metal把虛擬內(nèi)容渲上去, 感覺直接用metal渲的可能還是少數(shù)吧.
然后我想到用ReplayKit去錄屏, 但是用這種方法每次錄屏都會彈出一個系統(tǒng)提示, 就也放棄這個方法了.
一籌莫展之際, 我想起了蘋果官方postProcess的代碼, 后期處理的話應該是相機與虛擬內(nèi)容都渲好的圖像. 試了一下果然是ok的. 流程大概是arView有renderCallbacks, 其中prepareWithDevice的回調(diào)獲取ciContext, 然后postProcess的回調(diào)獲取到CIImage, 再轉(zhuǎn)成UIImage保存到相冊.
在此之前, 先檢查適合當前設備的輸出材質(zhì)的像素格式
@available(iOS 15.0, *)
extension RealityKit.ARView.PostProcessContext {
// Returns the output texture, ensuring that the pixel format is
// appropriate for the current device's GPU.
var compatibleTargetTexture: MTLTexture! {
if self.device.supportsFamily(.apple2) {
return targetColorTexture
} else {
return targetColorTexture.makeTextureView(pixelFormat: .bgra8Unorm)!
}
}
}
注冊prepareWithDevice的回調(diào), 并保存CIContext
arView.renderCallbacks.prepareWithDevice = setupCoreImage
func setupCoreImage(device: MTLDevice) {
// Create a CIContext and store it in a property.
self.ciContext = CIContext(mtlDevice: device)
// Do other expensive tasks, like loading images, here.
}
注冊postProcess的回調(diào), 獲取需要的UIImage, 并渲染. 這里是與官方后期處理的代碼不同的地方, 畢竟我只是想要一幀而不是想要后期處理. 所以如果對比的話會發(fā)現(xiàn)我刪除了濾鏡的部分.
arView.renderCallbacks.postProcess = postProcessWithCoreImage
func postProcessWithCoreImage(context: ARView.PostProcessContext) {
// Convert the frame buffer from a Metal texture to a CIImage, and
// set the CIImage as the filter's input image.
guard let input = CIImage(mtlTexture: context.sourceColorTexture) else {
fatalError("Unable to create a CIImage from sourceColorTexture.")
}
// 確保只拿一幀
if cameraState == .needCapture {
cameraState = .captured
DispatchQueue.main.async { [weak self] in
// 因為渲染時的坐標系(左下0,0)與UIKit的坐標系(左上0,0)不同, 所以這里需要縱向鏡像一下
let upsideDown = input.oriented(.downMirrored)
let image = UIImage(ciImage: upsideDown)
// 獲取到我們需要的UIImage
self?.tempImage = image
}
}
// 下面這部分代碼的作用是把內(nèi)容渲染到arView上
// Create a render destination and render the filter to the context's command buffer.
let destination = CIRenderDestination(mtlTexture: context.compatibleTargetTexture,
commandBuffer: context.commandBuffer)
destination.isFlipped = false
_ = try? self.ciContext.startTask(toRender: input, to: destination)
}
最后是保存到相冊
func saveImage() {
DispatchQueue.main.async { [weak self] in
self?.saveJpg((self?.tempImage)!)
let path = self?.documentDirectoryPath()?.appendingPathComponent("temp.jpg")
let image = UIImage.init(contentsOfFile: path!.path)!
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self?.saveImageComplete), nil)
}
}
func saveJpg(_ image: UIImage) {
if let jpgData = image.jpegData(compressionQuality: 0.5),
let path = documentDirectoryPath()?.appendingPathComponent("temp.jpg") {
try? jpgData.write(to: path)
}
}
func documentDirectoryPath() -> URL? {
let path = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)
return path.first
}
@objc func saveImageComplete(image:UIImage, didFinishSavingWithError error:NSError?, contextInfo:AnyObject) {
if error == nil {
showAlert(title: "圖片已存至相冊")
} else {
showAlert(title: "保存到相冊失敗")
}
}
保存相冊這步看網(wǎng)上都是只有UIImageWriteToSavedPhotosAlbum這一步, 看方法描述的話確實感覺這個方法傳一個UIImage就ok了, 結(jié)果是如果不把UIImage先給存下來的話, 會遇到下面未知錯誤, 賊坑
Error Domain=ALAssetsLibraryErrorDomain Code=-1 "未知錯誤" UserInfo={NSLocalizedDescription=未知錯誤, NSUnderlyingError=0x2832cefd0 {Error Domain=PHPhotosErrorDomain Code=3303 "(null)"}}
理論上postProcess那里可以拿到每秒60幀所以實現(xiàn)錄像的話應該也可以, 但是我還沒研究出來具體怎么實現(xiàn)所以有大佬的話可以指點一下.
我現(xiàn)在是全職做iOS AR開發(fā), 感覺坑是真的多, 而且網(wǎng)上能搜到的內(nèi)容比較少, 如果有跟我一樣感受的不妨加個好友我們可以互相避坑.