用RealityKit拍照, 并存到相冊

禁止轉(zhuǎn)載

AR應用, 用RealityKit呈現(xiàn), 要實現(xiàn)拍照與錄像的功能, github上搜到有個庫, 但是只支持ARSCNView, 而且感覺這個功能算是比較正常的功能吧, 竟然沒搜到其他實現(xiàn)的, 踩了不少的坑只實現(xiàn)了拍照, 不過效果還算不錯.

此方法只適用于iOS15及之后.


示例.png

開始的思路是獲取到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保存到相冊.

在此之前, 先檢查適合當前設(shè)備的輸出材質(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)容比較少, 如果有跟我一樣感受的不妨加個好友我們可以互相避坑.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容