文章

通过 Metal 呈现增强现实体验

通过渲染摄像头图像和使用位置追踪信息来显示重叠内容,构建自定增强现实视图。

概览

ARKit 包含一些视图类,能够轻松通过 SceneKit 或 SpriteKit 呈现增强现实体验。如果你想要构建自己的渲染引擎 (或与第三方引擎整合),ARKit 另有提供所有必要的支持,让你能够通过自定视图呈现增强现实体验。

在任何增强现实体验中,第一步都是配置 ARSession (英文) 对象来管理摄像头捕捉和运动处理。会话用于定义和维护设备所在的现实世界和增强现实内容建模的虚拟空间之间的对应关系。要在自动视图中呈现增强现实体验,需要做到以下几点:

  1. 从会话中检索视频帧和追踪信息。

  2. 渲染这些帧图像作为视图背景。

  3. 使用追踪信息确定增强现实内容在摄像头图像上的位置并绘制这些内容。

从会话中获取视频帧和追踪数据

创建和维护你自己的 ARSession (英文) 实例,并根据你想要支持的增强现实体验类型,使用合适的会话配置来运行这个实例。会话会从摄像头捕捉视频,追踪设备在 3D 模型空间中的位置和方向,并提供 ARFrame (英文) 对象。这类对象每个都包含对应帧捕获时刻的单一视频帧图像和位置追踪信息。

你可以通过两种方法访问增强现实会话产生的 ARFrame (英文) 对象,具体使用哪种方法取决于你的 App 倾向于使用拉取还是推送设计模式。

如果你想要控制帧定时 (拉取设计模式),请使用会话的 currentFrame (英文) 属性,在每次重新绘制视图内容时获取当前帧图像和追踪信息。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)
    }
}

或者,如果你的 App 设计倾向于使用推送模式,请实施 session(_:didUpdate:) (英文) 委托方法,会话会针对捕捉的每个视频帧调用一次这个方法 (默认为 60 fps)。

在获取帧时,你需要绘制摄像头图像,以及更新并渲染增强现实体验包含的任何重叠内容。

绘制摄像头图像

每个 ARFrame (英文) 对象的 capturedImage (英文) 属性都包含从设备摄像头捕捉的像素缓冲。要绘制这个图像作为自定视图的背景,你需要从图像内容创建纹理,并提交使用这些纹理的 GPU 渲染命令。

像素缓冲的内容以双平面 YCbCr (亦称 YUV) 数据格式编码;要渲染图像,你需要将这个像素数据转换为可绘制 RGB 格式。使用 Metal 进行渲染时,完成这个转换的最高效方法是使用 GPU 着色器代码。先使用 CVMetalTextureCache (英文) API 从像素缓冲创建两个 Metal 纹理——缓冲的亮度 (Y) 和色度 (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
}

接着,对渲染命令编码,使用分段函数通过颜色变换矩阵完成从 YCbCr 到 RGB 的转换,从而绘制这两个纹理:

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;
}

追踪和渲染重叠内容

增强现实体验通常侧重于渲染 3D 重叠内容,让这些内容看似真实存在于摄像头图像里的现实世界中一样。要实现这一视觉效果,可使用 ARAnchor (英文) 类,相对现实世界为自身 3D 内容的位置和方向建模。锚点提供变换属性,在渲染的时候可供参考。

例如,每当用户轻点屏幕时,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 方法添加到会话中的每个锚点来放置简单的立方体网格:

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
    }
}

使用逼真灯光进行渲染

配置着色器在场景中绘制 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

另请参阅

显示

class ARSCNView (英文)

一个用于显示增强现实体验的视图,其中会以 3D SceneKit 内容增强摄像头视图。

class ARSKView (英文)

一个用于显示增强现实体验的视图,其中会以 2D SpriteKit 内容增强摄像头视图。