通過Metal展示AR體驗

在相機信息流上控制你的app虛擬內容渲染。

一、概述

ARKit包含視圖類,可輕鬆顯示SceneKitSpriteKitAR體驗。 但是,如果改爲使用Metal來構建自己的渲染引擎,則ARKit也將提供所有必要的支持,以使用自定義視圖顯示AR體驗。

在任何AR體驗中,第一步都是配置一個ARSession對象以管理相機捕獲和運動處理。 session定義並維持設備所居住的真實空間與您爲AR內容建模的虛擬空間之間的對應關係。 要在自定義視圖中顯示您的AR體驗,您需要:

  1. session中檢索視頻幀和跟蹤信息。
  2. 渲染這些幀圖像作爲您的視圖背景。
  3. 使用跟蹤信息將AR內容定位並繪製在相機圖像上。

注意
本文介紹了在Xcode項目模板中找到的代碼。 有關完整的示例代碼,請使用Augmented Reality模板創建一個新的iOS應用程序,然後從Content Technology彈出菜單中選擇Metal

二、從Session中獲取視頻幀和跟蹤數據

創建並維護您自己的ARSession實例,並使用適合您要支持的AR體驗的session配置來運行它。 session從相機捕獲視頻,在建模的3D空間中跟蹤設備的位置和方向,並提供ARFrame對象。 從捕獲幀的那一刻起,每個這樣的對象都包含一個單獨的視頻幀圖像和位置跟蹤信息。

有兩種方法可以訪問AR session生成的ARFrame對象,具體取決於您的應用是支持拉還是推設計模式。

如果您希望控制幀計時(拉動設計模式),則每次重繪視圖內容時,都可以使用sessioncurrentFrame屬性獲取當前幀圖像和跟蹤信息。 ARKit Xcode模板使用以下方法:

// in Renderer class, called from MTKViewDelegate.draw(in:) via Renderer.update()
func updateGameState() {        
    guard let currentFrame = session.currentFrame else {
        return
    }
    
    updateSharedUniforms(frame: currentFrame)
    updateAnchors(frame: currentFrame)
    updateCapturedImageTextures(frame: currentFrame)
    
    if viewportSizeDidChange {
        viewportSizeDidChange = false
        
        updateImagePlane(frame: currentFrame)
    }
}

或者,如果您的應用程序設計偏愛推送模式,請實現 session(_:didUpdate :)委託方法,session將針對其捕獲的每個視頻幀對其調用一次(默認爲每秒60幀)。

獲取一幀後,您需要繪製相機圖像,並更新和渲染您的AR體驗中包括的任何疊加內容。

三、繪製相機圖像

每個ARFrame對象的captureImage屬性都包含一個從設備相機捕獲的像素緩衝區。 要將圖像繪製爲自定義視圖的背景,您需要根據圖像內容創建紋理,並提交使用這些紋理的GPU渲染命令。

像素緩衝區的內容以雙平面YCbCr(也稱爲Y'UV)數據格式進行編碼; 要渲染圖像,您需要將該像素數據轉換爲可繪製的RGB格式。 對於使用Metal渲染,您可以在GPU着色器代碼中最高效地執行此轉換。 使用CVMetalTextureCache API從像素緩衝區創建兩個Metal紋理———緩衝區的亮度luma(Y)和色度chroma(CbCr)平面各一個:

func updateCapturedImageTextures(frame: ARFrame) {
    // Create two textures (Y and CbCr) from the provided frame's captured image
    let pixelBuffer = frame.capturedImage
    if (CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
        return
    }
    capturedImageTextureY = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.r8Unorm, planeIndex:0)!
    capturedImageTextureCbCr = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.rg8Unorm, planeIndex:1)!
}

func createTexture(fromPixelBuffer pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> MTLTexture? {
    var mtlTexture: MTLTexture? = nil
    let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
    let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
    
    var texture: CVMetalTexture? = nil
    let status = CVMetalTextureCacheCreateTextureFromImage(nil, capturedImageTextureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &texture)
    if status == kCVReturnSuccess {
        mtlTexture = CVMetalTextureGetTexture(texture!)
    }
    
    return mtlTexture
}

接下來,使用片段函數對繪製這兩個紋理的渲染命令進行編碼,該片段函數通過顏色轉換矩陣執行從YCbCrRGB的轉換:

fragment float4 capturedImageFragmentShader(ImageColorInOut in [[stage_in]],
                                            texture2d<float, access::sample> capturedImageTextureY [[ texture(kTextureIndexY) ]],
                                            texture2d<float, access::sample> capturedImageTextureCbCr [[ texture(kTextureIndexCbCr) ]]) {
    
    constexpr sampler colorSampler(mip_filter::linear,
                                   mag_filter::linear,
                                   min_filter::linear);
    
    const float4x4 ycbcrToRGBTransform = float4x4(
        float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
        float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
        float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
        float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
    );
    
    // Sample Y and CbCr textures to get the YCbCr color at the given texture coordinate
    float4 ycbcr = float4(capturedImageTextureY.sample(colorSampler, in.texCoord).r,
                          capturedImageTextureCbCr.sample(colorSampler, in.texCoord).rg, 1.0);
    
    // Return converted RGB color
    return ycbcrToRGBTransform * ycbcr;
}

注意
使用displayTransform(for: viewportSize:)方法確保相機圖像覆蓋整個視圖。 例如,使用此方法以及完整的Metal管道設置代碼,請參閱完整的Xcode模板。 (使用增強現實模板創建一個新的iOS應用程序,然後從Content Technology彈出菜單中選擇Metal。)

四、跟蹤和渲染疊加內容

AR體驗通常專注於渲染3D疊加內容,因此該內容似乎是在相機圖像中看到的真實世界的一部分。 要實現這種錯覺,請使用ARAnchor類對您自己的3D內容相對於真實空間的位置和方向進行建模。 Anchors提供您可以在渲染期間參考的轉換。

例如,每當用戶點擊屏幕時,Xcode模板都會在設備前方約20 cm處創建一個錨點:

func handleTap(gestureRecognize: UITapGestureRecognizer) {
    // Create anchor using the camera's current position
    if let currentFrame = session.currentFrame {
        
        // Create a transform with a translation of 0.2 meters in front of the camera
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.2
        let transform = simd_mul(currentFrame.camera.transform, translation)
        
        // Add a new anchor to the session
        let anchor = ARAnchor(transform: transform)
        session.add(anchor: anchor)
    }
}

在渲染引擎中,使用每個ARAnchor對象的transform屬性放置可視化內容。 Xcode模板使用其handleTap方法中添加到session的每個錨點來定位簡單的多維數據集網格:

func updateAnchors(frame: ARFrame) {
    // Update the anchor uniform buffer with transforms of the current frame's anchors
    anchorInstanceCount = min(frame.anchors.count, kMaxAnchorInstanceCount)
    
    var anchorOffset: Int = 0
    if anchorInstanceCount == kMaxAnchorInstanceCount {
        anchorOffset = max(frame.anchors.count - kMaxAnchorInstanceCount, 0)
    }
    
    for index in 0..<anchorInstanceCount {
        let anchor = frame.anchors[index + anchorOffset]
        
        // Flip Z axis to convert geometry from right handed to left handed
        var coordinateSpaceTransform = matrix_identity_float4x4
        coordinateSpaceTransform.columns.2.z = -1.0
        
        let modelMatrix = simd_mul(anchor.transform, coordinateSpaceTransform)
        
        let anchorUniforms = anchorUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)
        anchorUniforms.pointee.modelMatrix = modelMatrix
    }
}

注意
在更復雜的AR體驗中,您可以使用命中測試或平面檢測來找到真實表面的位置。 有關詳細信息,請參見planeDetection屬性和hitTest(_: types: )方法。 在這兩種情況下,ARKit都將結果作爲ARAnchor對象提供,因此您仍然可以使用錨點轉換來放置可視化內容。

五、渲染逼真的燈光

當配置着色器以在場景中繪製3D內容時,請使用每個ARFrame對象中的估計照明信息來產生更逼真的陰影:

// in Renderer.updateSharedUniforms(frame:):
// Set up lighting for the scene using the ambient intensity if provided
var ambientIntensity: Float = 1.0
if let lightEstimate = frame.lightEstimate {
    ambientIntensity = Float(lightEstimate.ambientIntensity) / 1000.0
}
let ambientLightColor: vector_float3 = vector3(0.5, 0.5, 0.5)
uniforms.pointee.ambientLightColor = ambientLightColor * ambientIntensity

注意
有關此示例附帶的完整的Metal設置和渲染命令集,請參見完整的Xcode模板。(使用增強現實模板創建一個新的iOS應用程序,然後從Content Technology彈出菜單中選擇Metal。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章