ARKit Apple官翻 04 使用Metal开发AR

通过渲染相机图像和使用位置跟踪信息来显示复杂内容并构建自定义AR视图。

概观

ARKit库包含了使用SceneKit或SpriteKit显示简单AR体验的视图类。但是,如果你建立自己的渲染引擎(或与第三方引擎集成),则ARKit还提供了AR体验自定义视图所需的全部支持类。



在任何AR场景中,第一步是配置一个ARSession对象来管理摄像头拍摄和运动处理。该会话定义和维护设备所在真实世界空间与AR内容建模的虚拟空间之间的对应关系。要在自定义视图中显示你的AR场景,需要:

  • 从会话中检索视频帧和跟踪信息。
  • 将这些框架图像作为视图的背景渲染。
  • 使用跟踪信息在摄像机图像上方定位和绘制AR内容。
Tips

本文包含了Xcode项目模板中的代码。有关完整示例代码,请使用AR模板创建新的iOS应用程序,然后在弹出菜单中选择“Metal”。

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

创建和持有自己的ARSession实例,并使用适合你想要支持的AR场景类型的会话配置来运行它。(请参阅Building a Basic AR Experience。)会话从相机捕获影像,在已经建模的3D空间中跟踪设备的位置和方向,并提供ARFrame对象。每个这样的对象都可以从当前捕获的帧中获取单独的视频帧图像和位置跟踪信息。
AR会话有两种访问ARFrame的方式,这取决于你的应用程序是倾向于pull或push设计模式。
如果您喜欢控制帧的时序(pull设计模式),请使用会话的currentFrame属性,在每次重新绘制视图的内容时来获取当前的帧图像和跟踪信息。 ARKit Xcode示例使用这种方法:

//在Renderer类中,从MTKViewDelegate.draw(in:)通过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)    
    }
}

或者,如果您的应用程序倾向于push模式,可以实现session(_:didUpdate:)
代理方法,并且会话将会在每次捕获视频帧时调用(默认情况下为每秒60帧)。
获得一个视频帧后,你将需要绘制相机图像,并更新和呈现AR场景内任何重叠内容。

绘制相机图像

每个ARFrame对象的capturedImage属性包含了一个从设备摄像头捕获的像素缓冲区。要将此图像绘制为自定义视图的背景,你需要从图像内容创建纹理,并提交使用这些纹理的GPU渲染命令。
像素缓冲器的内容被编码为双平面YCbCr(也称为YUV)的数据格式;要渲染图像,你需要将此像素数据转换为可绘制的RGB格式。对于使用Metal渲染,您可以在GPU着色器代码中最有效地执行此转换。使用CVMetalTextureCache API从像素缓冲区创建两个Metal纹理,用于缓冲区的亮度(Y)和色度(CbCr)平面:

func updateCapturedImageTextures(frame: ARFrame) {
    // 从提供的帧的捕获图像创建两个纹理(Y和CbCr)
    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.164380f, +1.164380f, +1.164380f, +0.000000f),       
        float4(+0.000000f, -0.391762f, +2.017230f, +0.000000f),       
        float4(+1.596030f, -0.812968f, +0.000000f, +0.000000f),       
        float4(-0.874202f, +0.531668f, -1.085630f, +1.000000f)   
    );       
    // 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;
}
Tips

使用 displayTransform(withViewportSize:orientation:)方法来确保摄像机图像覆盖整个视图。 使用该方法,以及完整的Metal管道设置代码的实例代码,请参阅完整的Xcode模板。(使用AR模板创建新的iOS应用程序,然后从弹出菜单中选择Metal。)

跟踪和渲染覆盖内容

AR经验通常侧重于渲染3D重叠内容,使内容似乎是在相机图像中看到的真实世界的一部分。 为了达到这个幻想,使用ARAnchor课程来模拟您自己的3D内容相对于现实世界空间的位置和方向。 锚点提供可在渲染过程中引用的变换。
例如,当用户点击屏幕时,Xcode模板创建位于设备前面大约20厘米处的锚点:

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

在您的渲染引擎中,使用每个ARAnchortransform属性来放置可视内容。 Xcode模板使用其handleTap方法中添加到会话中的每个锚来放置简单的立方体:

func updateAnchors(frame: ARFrame) {
    //使用当前帧的锚点矩阵更新锚的缓冲
    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]
       
        //翻转Z轴将坐标系从右手转换为左手
        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
    }
}
Tips

在复杂的AR场景中,您可以使用命中测试或平面检测来寻找真实世界平面的位置。 更多的详细信息,请参阅planeDetection 属性和hitTest(_:types:)方法。 在这两种情况下,ARKit都提供了ARAnchor的结果对象,所以你仍然可以使用锚矩阵来放置视觉内容。

渲染真实光照

当你在场景中配置用于绘制3D内容的阴影时,请使用每个ARFrame中的预估照明信息来产生更逼真的阴影:

// 在Renderer.updateSharedUniforms(frame:):中
// 使用环境强度(如果提供)为场景设置光照

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
Tips

有关本示例的完整Metal设置和渲染命令集,请参阅完整的Xcode模板。 (使用AR模板创建新的iOS应用程序,然后从弹出菜单中选择Metal。)

参考:

Apple官方文档 :Displaying an AR Experience with Metal

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容