通过渲染相机图像和使用位置跟踪信息来显示复杂内容并构建自定义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)
}
}
在您的渲染引擎中,使用每个ARAnchor的transform属性来放置可视内容。 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