使用場景的深度數(shù)據(jù)在現(xiàn)實世界中布點,以呈現(xiàn)物理環(huán)境的形狀。
概述
Depth Cloud 是一款應(yīng)用程序,它使用 Metal 根據(jù)來自設(shè)備的 LiDAR 掃描儀的深度信息,通過在物理環(huán)境中放置一組點來顯示攝像頭饋送。 對于會話的周期性深度讀數(shù) (depthMap) 中的每個距離樣本,應(yīng)用程序會在物理環(huán)境中的該位置放置一個虛擬點,最終結(jié)果類似于點云。 Depth Cloud 根據(jù) ARKit 的相機圖像 (capturedImage) 為云著色。
對于深度圖中的每個條目——因此,對于云中的每個點——示例應(yīng)用程序檢查相機圖像中的相應(yīng)像素并將像素的顏色分配給點。 當(dāng)用戶直接查看點云時,應(yīng)用程序的顯示看起來幾乎與相機饋送相同。 為了演示云的 3D 形狀,示例應(yīng)用程序不斷旋轉(zhuǎn)云以改變其相對于用戶的視角。
下圖說明了來自單幀數(shù)據(jù)的點云,在 y 軸上旋轉(zhuǎn)以顯示蘋果背后的黑暗區(qū)域,當(dāng)前傳感器讀數(shù)缺少顏色和深度信息。

應(yīng)用程序循環(huán)遍歷深度置信度值(參見 confidenceMap),擴大深度緩沖區(qū),并切換 ARKit 的平滑深度選項(ARFrameSemanticSmoothedSceneDepth)。 通過將用戶的選擇實時應(yīng)用于點云,用戶可以看到設(shè)置在整個體驗中產(chǎn)生的差異。
WWDC20 會議 10611:探索 ARKit 4 引用了此示例應(yīng)用程序的先前版本,它在云中累積點。 對于會話中顯示的應(yīng)用程序的原始版本,從下載根文件夾中的 Git 存儲庫克隆初始提交。
有關(guān) ARKit 深度數(shù)據(jù)的實際應(yīng)用,請參閱使用場景深度創(chuàng)建霧效果。
設(shè)置相機源
為了顯示相機源,示例項目定義了一個 SwiftUI 場景,其主體包含一個窗口。 為了從窗口代碼中抽象出視圖代碼,示例項目將其所有顯示包裝在一個名為 MetalDepthView 的視圖中。
@main
struct PointCloudDepthSample: App {
var body: some Scene {
WindowGroup {
MetalDepthView()
因為 Depth Cloud 使用 Metal 繪制圖形,所以示例項目通過定義自定義 GPU 代碼來顯示相機源。 示例項目在其 ARReceiver.swift 文件中訪問 ARKit 的相機源,并將其包裝在自定義 ARData 對象中,以便最終傳輸?shù)?GPU。
arData.colorImage = frame.capturedImage
確保設(shè)備支持并開始會話
設(shè)備需要激光雷達掃描儀才能訪問場景的深度。 在深度可視化視圖的主體定義中,應(yīng)用程序通過檢查設(shè)備是否支持場景深度來防止運行不受支持的配置。
if !ARWorldTrackingConfiguration.supportsFrameSemantics([.sceneDepth, .smoothedSceneDepth]) {
Text("Unsupported Device: This app requires the LiDAR Scanner to access the scene's depth.")
為了將數(shù)據(jù)采集與顯示分開,示例應(yīng)用程序?qū)?ARKit 調(diào)用包裝在其 ARProvider 類中。
var arProvider: ARProvider = ARProvider()
AR 提供程序運行世界跟蹤配置,并通過配置場景深度幀語義來請求有關(guān)場景深度的信息(請參閱 ARFrameSemanticSceneDepth 和 ARFrameSemanticSmoothedSceneDepth)。
let config = ARWorldTrackingConfiguration()
config.frameSemantics = [.sceneDepth, .smoothedSceneDepth]
arSession.run(config)
訪問場景的深度
為了響應(yīng)配置的場景深度幀語義,框架在會話的 currentFrame 上定義了 sceneDepth 和 smoothedSceneDepth 的幀的 depthMap 屬性。
func session(_ session: ARSession, didUpdate frame: ARFrame) {
if(frame.sceneDepth != nil) && (frame.smoothedSceneDepth != nil) {
arData.depthImage = frame.sceneDepth?.depthMap
arData.depthSmoothImage = frame.smoothedSceneDepth?.depthMap
由于示例項目使用 Metal 繪制圖形,因此應(yīng)用程序的 CPU 代碼捆綁了其 GPU 代碼顯示體驗所需的數(shù)據(jù)。 要使用點云對物理環(huán)境進行建模,該應(yīng)用程序需要相機捕獲數(shù)據(jù)來為每個點著色,并使用深度數(shù)據(jù)來定位它們。
示例項目在 GPU 代碼中定位每個點,因此 CPU 端將深度數(shù)據(jù)打包在 Metal 紋理中以供 GPU 使用。
depthContent.texture = lastArData?.depthImage?.texture(withFormat: .r32Float, planeIndex: 0, addToCache: textureCache!)!
示例項目為 GPU 代碼中的每個點著色,因此 CPU 端將相機數(shù)據(jù)打包以在 GPU 上使用。
colorYContent.texture = lastArData?.colorImage?.texture(withFormat: .r8Unorm, planeIndex: 0, addToCache: textureCache!)!
colorCbCrContent.texture = lastArData?.colorImage?.texture(withFormat: .rg8Unorm, planeIndex: 1, addToCache: textureCache!)!
轉(zhuǎn)換相機數(shù)據(jù)
在 pointCloudVertexShader 函數(shù)(參見示例項目的 shaders.metal 文件)中,示例項目為深度紋理中的每個值創(chuàng)建一個點,并通過對該深度紋理值在相機圖像中的位置進行采樣來確定該點的顏色。 每個頂點通過將其在一維頂點數(shù)組中的位置轉(zhuǎn)換為深度紋理中的二維位置來計算其在相機圖像中的 x 和 y 位置。
uint2 pos;
// 計算深度-紋理-寬度寬的行以確定 y 值。
pos.y = vertexID / depthTexture.get_width();
// x 位置是 y 值除法的余數(shù)。
pos.x = vertexID % depthTexture.get_width();
系統(tǒng)的相機捕獲管道以 YUV 格式表示數(shù)據(jù),示例項目使用亮度圖 (colorYtexture) 和藍色與紅色色度圖 (colorCbCrtexture) 對其進行建模。 GPU顏色格式為RGBA,需要樣例工程轉(zhuǎn)換相機數(shù)據(jù)才能顯示。 著色器在頂點的 x、y 位置對亮度和色度紋理進行采樣,并應(yīng)用靜態(tài)轉(zhuǎn)換因子。
constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear);
out.coor = { pos.x / (depthTexture.get_width() - 1.0f), pos.y / (depthTexture.get_height() - 1.0f) };
half y = colorYtexture.sample(textureSampler, out.coor).r;
half2 uv = colorCbCrtexture.sample(textureSampler, out.coor).rg - half2(0.5h, 0.5h);
// 內(nèi)聯(lián)將 YUV 轉(zhuǎn)換為 RGB。
half4 rgbaResult = half4(y + 1.402h * uv.y, y - 0.7141h * uv.y - 0.3441h * uv.x, y + 1.772h * uv.x, 1.0h);
為簡潔起見,示例項目演示了 YUV 到 RGB 的內(nèi)聯(lián)轉(zhuǎn)換。 要查看將靜態(tài)轉(zhuǎn)換因子提取到 4 x 4 矩陣的示例,請參閱使用 Metal 顯示 AR 體驗。
設(shè)置點云視圖
為了使用點云顯示相機源,該項目定義了一個 UIViewRepresentable 對象 MetalPointCloud,其中包含一個顯示 Metal 內(nèi)容的 MTKView。
struct MetalPointCloud: UIViewRepresentable {
該項目通過將點云視圖嵌入到 MetalDepthView 布局中,將其插入到視圖層次結(jié)構(gòu)中。
HStack() {
Spacer()
MetalPointCloud(arData: arProvider,
confSelection: $selectedConfidence,
scaleMovement: $scaleMovement).zoomOnTapModifier(
height: geometry.size.width / 2 / sizeW * sizeH,
width: geometry.size.width / 2, title: "")
作為 UIView 的代表,Metal 紋理視圖定義了一個協(xié)調(diào)器,CoordinatorPointCloud。
func makeCoordinator() -> CoordinatorPointCloud {
return CoordinatorPointCloud(arData: arData, confSelection: $confSelection, scaleMovement: $scaleMovement)
}
點云協(xié)調(diào)器擴展了 MTKCoordinator,該示例在顯示金屬內(nèi)容的其他視圖中共享它。
final class CoordinatorPointCloud: MTKCoordinator {
作為 MTKViewDelegate,MTKCoordinator 處理在整個 Metal 視圖生命周期中發(fā)生的相關(guān)事件。
class MTKCoordinator: NSObject, MTKViewDelegate {
在 UIView 可表示的 makeUIView 實現(xiàn)中,示例項目將協(xié)調(diào)器分配為視圖的委托。
func makeUIView(context: UIViewRepresentableContext<MetalPointCloud>) -> MTKView {
let mtkView = MTKView()
mtkView.delegate = context.coordinator
在運行時,顯示鏈接然后調(diào)用 Metal 協(xié)調(diào)器的 drawInMTKView: 實現(xiàn)來發(fā)出 CPU 端渲染命令。
override func draw(in view: MTKView) {
content = arData.depthContent
let confidence = (arData.isToUpsampleDepth) ? arData.upscaledConfidence:arData.confidenceContent
guard arData.lastArData != nil else {
使用 GPU 代碼顯示點云
點云(英語:point cloud)是空間中的數(shù)據(jù)集,可以表示三維形狀或?qū)ο?,通常由三維掃描儀獲取。
示例項目在 GPU 上繪制點云。 點云視圖將其相應(yīng)的 GPU 代碼作為輸入所需的幾個紋理打包在一起。
encoder.setVertexTexture(content.texture, index: 0)
encoder.setVertexTexture(confidence.texture, index: 1)
encoder.setVertexTexture(arData.colorYContent.texture, index: 2)
encoder.setVertexTexture(arData.colorCbCrContent.texture, index: 3)
類似地,點云視圖將其相應(yīng)的 GPU 代碼作為輸入所需的幾個計算屬性打包。
encoder.setVertexBytes(&pmv, length: MemoryLayout<matrix_float4x4>.stride, index: 0)
encoder.setVertexBytes(&cameraIntrinsics, length: MemoryLayout<matrix_float3x3>.stride, index: 1)
encoder.setVertexBytes(&confSelection, length: MemoryLayout<Int>.stride, index: 2)
為了調(diào)用繪制點云的 GPU 函數(shù),該示例定義了一個管道狀態(tài),該狀態(tài)將其 pointCloudVertexShader 和 pointCloudFragmentShader 金屬函數(shù)排隊(請參閱項目的 shaders.metal 文件)。
pipelineDescriptor.vertexFunction = library.makeFunction(name: "pointCloudVertexShader")
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "pointCloudFragmentShader")
在 GPU 上,點云頂點著色器決定了每個點在屏幕上的顏色和位置。 在函數(shù)簽名中,頂點著色器接收 CPU 代碼發(fā)送的輸入紋理和屬性。
vertex ParticleVertexInOut pointCloudVertexShader(
uint vertexID [[ vertex_id ]],
texture2d<float, access::read> depthTexture [[ texture(0) ]],
texture2d<float, access::read> confTexture [[ texture(1) ]],
constant float4x4& viewMatrix [[ buffer(0) ]],
constant float3x3& cameraIntrinsics [[ buffer(1) ]],
constant int &confFilterMode [[ buffer(2) ]],
texture2d<half> colorYtexture [[ texture(2) ]],
texture2d<half> colorCbCrtexture [[ texture(3) ]]
)
{ // ...
該代碼將點的世界位置基于其在相機饋送中的位置和深度。
float xrw = ((int)pos.x - cameraIntrinsics[2][0]) * depth / cameraIntrinsics[0][0];
float yrw = ((int)pos.y - cameraIntrinsics[2][1]) * depth / cameraIntrinsics[1][1];
float4 xyzw = { xrw, yrw, depth, 1.f };
該點的屏幕位置是其世界位置和參數(shù)投影矩陣的乘積。
float4 vecout = viewMatrix * xyzw;
vertex 函數(shù)將點的屏幕位置以及點的顏色作為轉(zhuǎn)換后的 RGB 結(jié)果輸出。
out.color = rgbaResult;
out.clipSpacePosition = vecout;
片段著色器在其函數(shù)簽名中接收頂點函數(shù)輸出。
fragment half4 pointCloudFragmentShader(
ParticleVertexInOut in [[stage_in]])
在過濾任何離設(shè)備相機太近的點后,片段著色器通過返回每個頂點的顏色將剩余的點排隊以供顯示。
if (in.depth < 1.0f)
discard_fragment();
else
{
return in.color;
改變云的方向以傳達深度
當(dāng)用戶直接查看點云時,它在視覺上等同于 2D 相機圖像。 但是,當(dāng)示例應(yīng)用程序稍微旋轉(zhuǎn)點云時,點云的 3D 形狀對用戶來說變得很明顯。

點云的屏幕位置是其投影矩陣的一個因素。 在示例項目的 calcCurrentPMVMatrix 函數(shù)(參見 MetalPointCloud.swift)中,該函數(shù)設(shè)置了一個基本矩陣。
func calcCurrentPMVMatrix(viewSize: CGSize) -> matrix_float4x4 {
let projection: matrix_float4x4 = makePerspectiveMatrixProjection(fovyRadians: Float.pi / 2.0,
aspect: Float(viewSize.width) / Float(viewSize.height),
nearZ: 10.0, farZ: 8000.0)
為了調(diào)整點云相對于用戶的方向,示例應(yīng)用程序相反地為相機的姿勢設(shè)置了平移和旋轉(zhuǎn)偏移。
// 隨機化相機比例。
translationCamera.columns.3 = [150 * sinf, -150 * cossqr, -150 * scaleMovement * sinsqr, 1]
// 隨機化相機移動。
cameraRotation = simd_quatf(angle: staticAngle, axis: SIMD3(x: -sinsqr / 3, y: -cossqr / 3, z: 0))
示例項目在返回調(diào)整后的結(jié)果之前將相機位姿偏移應(yīng)用于原始投影矩陣。
let rotationMatrix: matrix_float4x4 = matrix_float4x4(cameraRotation)
let pmv = projection * rotationMatrix * translationCamera * translationOrig * orientationOrig
return pmv
擴大深度緩沖區(qū)
ARKit 的深度圖包含相機饋送中對象的精確、低分辨率深度值。 為了創(chuàng)建高分辨率深度圖的錯覺,示例應(yīng)用程序提供了使用 Metal Performance Shaders (MPS) 放大深度圖的 UI。 通過填充框架深度信息中的空白,擴大的深度緩沖區(qū)會在場景中產(chǎn)生更多深度信息的錯覺。

示例項目使用 MPS 擴大深度緩沖區(qū); 查看 ARDataProvider.swift 文件。 ARProvider 類初始化器創(chuàng)建一個引導(dǎo)過濾器來擴大深度緩沖區(qū)。
guidedFilter = MPSImageGuidedFilter(device: metalDevice, kernelDiameter: guidedFilterKernelDiameter)
為了對齊相關(guān)視覺效果(相機圖像和置信度紋理)的大小,AR 提供者使用 MPS 雙線性比例過濾器。
mpsScaleFilter = MPSImageBilinearScale(device: metalDevice)
在 processLastARData 例程中,AR 提供程序為擴大深度緩沖區(qū)的計算通道創(chuàng)建額外的 Metal 命令緩沖區(qū)。
if isToUpsampleDepth {
AR 提供程序?qū)⑤斎肷疃葦?shù)據(jù)轉(zhuǎn)換為 RGB 格式,根據(jù)引導(dǎo)過濾器的要求。
let convertYUV2RGBFunc = lib.makeFunction(name: "convertYCbCrToRGBA")
pipelineStateCompute = try metalDevice.makeComputePipelineState(function: convertYUV2RGBFunc!)
在對雙線性比例和引導(dǎo)過濾器進行編碼后,AR 提供者設(shè)置放大的深度緩沖區(qū)。
depthContent.texture = destDepthTexture
顯示深度置信度
除了點云可視化之外,示例應(yīng)用程序還同時添加了深度距離和置信度可視化。 用戶在體驗過程中隨時參考任一可視化,以更好地掌握激光雷達掃描儀讀取物理環(huán)境的準確性。

為了顯示深度和置信度可視化,Depth Cloud 定義了一個 UIViewRepresentable 對象 MetalTextureView,它包含一個顯示 Metal 內(nèi)容的 MTKView(參見項目的 MetalTextureView.swift 文件)。 此設(shè)置類似于 MetalDepthView,只是示例應(yīng)用程序?qū)⒁晥D的可顯示內(nèi)容存儲在單個紋理中。
encoder.setFragmentTexture(content.texture, index: 0)
該項目通過將深度可視化視圖嵌入到項目的 MetalViewSample.swift 文件中的 MetalDepthView 布局中,將其插入到視圖層次結(jié)構(gòu)中。
ScrollView(.horizontal) {
HStack() {
MetalTextureViewDepth(content: arProvider.depthContent, confSelection: $selectedConfidence)
.zoomOnTapModifier(height: sizeH, width: sizeW, title: isToUpsampleDepth ? "Upscaled Depth" : "Depth")
深度可視化視圖的內(nèi)容由包含來自 AR 會話當(dāng)前幀的深度數(shù)據(jù)的紋理組成。
depthContent.texture = lastArData?.depthImage?.texture(withFormat: .r32Float, planeIndex: 0, addToCache: textureCache!)!
深度紋理視圖的協(xié)調(diào)器 CoordinatorDepth 分配一個填充紋理的著色器。
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "planeFragmentShaderDepth")
planeFragmentShaderDepth 著色器(請參閱 shaders.metal)根據(jù)需要將深度值轉(zhuǎn)換為 RGB 以顯示它們。
fragment half4 planeFragmentShaderDepth(ColorInOut in [[stage_in]], texture2d<float, access::sample> textureDepth [[ texture(0) ]])
{
constexpr sampler colorSampler(address::clamp_to_edge, filter::nearest);
float4 s = textureDepth.sample(colorSampler, in.texCoord);
// Size the color gradient to a maximum distance of 2.5 meters.
// The LiDAR Scanner supports a value no larger than 5.0; the
// sample app uses a value of 2.5 to better distinguish depth
// in smaller environments.
half val = s.r / 2.5h;
half4 res = getJetColorsFromNormalizedVal(val);
return res;
同樣,該項目通過將置信度可視化視圖嵌入到項目的 MetalViewSample.swift 文件中的 MetalDepthView 布局中,將其插入到視圖層次結(jié)構(gòu)中。
MetalTextureViewConfidence(content: arProvider.confidenceContent)
.zoomOnTapModifier(height: sizeH, width: sizeW, title: "Confidence")
置信度可視化視圖的內(nèi)容由包含來自 AR 會話當(dāng)前幀的置信度數(shù)據(jù)的紋理組成。
confidenceContent.texture = lastArData?.confidenceImage?.texture(withFormat: .r8Unorm, planeIndex: 0, addToCache: textureCache!)!
置信度紋理視圖的協(xié)調(diào)器 CoordinatorConfidence 分配一個填充紋理的著色器。
pipelineDescriptor.fragmentFunction = library.makeFunction(name: "planeFragmentShaderConfidence")
planeFragmentShaderConfidence 著色器(請參閱 shaders.metal)根據(jù)需要將深度值轉(zhuǎn)換為 RGB 以顯示它們。
fragment half4 planeFragmentShaderConfidence(ColorInOut in [[stage_in]], texture2d<float, access::sample> textureIn [[ texture(0) ]])
{
constexpr sampler colorSampler(address::clamp_to_edge, filter::nearest);
float4 s = textureIn.sample(colorSampler, in.texCoord);
float res = round( 255.0f*(s.r) ) ;
int resI = int(res);
half4 color = half4(0.0h, 0.0h, 0.0h, 0.0h);
if (resI == 0)
color = half4(1.0h, 0.0h, 0.0h, 1.0h);
else if (resI == 1)
color = half4(0.0h, 1.0h, 0.0h, 1.0h);
else if (resI == 2)
color = half4(0.0h, 0.0h, 1.0h, 1.0h);
return color;