[譯]基於 Metal 的 ARKit 使用指南(下)

Swants發表於2017-09-15

基於 Metal 的 ARKit 使用指南(下)

我們們上篇提到過 , ARKit 應用通常包括三個圖層 : 渲染層 , 追蹤層場景解析層 。上一篇我們通過一個自定義檢視已經非常詳細地分析了渲染層在 Metal 中是如何工作的了。 ARKit 使用 視覺慣性測程法 準確地追蹤它周圍的環境,並將相機感測器資料和 CoreMotion 資料相結合。這樣當相機隨我們運動時,不需要額外的校準就可以保證影象的穩定性。這篇文章我們將研究 場景解析 —— 通過平面檢測,碰撞測試和光線測定來描述場景特徵的方法。 ARKit 可以分析相機呈現出來的場景並在場景中找到類似地板這樣的水平面。前提是,我們需要在執行 session configuration 之前,簡單地新增額外的一行程式碼來開啟水平面檢測的新特性(預設是關閉的):

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
session.run(configuration)
}複製程式碼

注意,在當前的 API 版本中只能新增水平的平面檢測。

使用 ARSessionObserver 協議方法來處理會話錯誤,追蹤變化和打斷:

func session(_ session: ARSession, didFailWithError error: Error) {}
func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {}
func session(_ session: ARSession, didOutputAudioSampleBuffer audioSampleBuffer: CMSampleBuffer) {}
func sessionWasInterrupted(_ session: ARSession) {}
func sessionInterruptionEnded(_ session: ARSession) {}複製程式碼

與此同時, ARSessionDelegate 協議還有其他的代理方法(繼承於 ARSessionObserver )來讓我們處理錨點:我們在第一個方法中呼叫 print()

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
print(anchors)
}
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {}
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {}
func session(_ session: ARSession, didUpdate frame: ARFrame) {}複製程式碼

讓我們現在開啟 Renderer.swift 檔案。首先,建立一些我們需要的類屬性。這些變數將會幫助我們在螢幕上建立和展示一個除錯介面:

var debugUniformBuffer: MTLBuffer!
var debugPipelineState: MTLRenderPipelineState!
var debugDepthState: MTLDepthStencilState!var debugMesh: MTKMesh!
var debugUniformBufferOffset: Int = 0
var debugUniformBufferAddress: UnsafeMutableRawPointer!
var debugInstanceCount: Int = 0複製程式碼

其次,我們在 setupPipeline() 中建立快取區:

debugUniformBuffer = device.makeBuffer(length: anchorUniformBufferSize, options: .storageModeShared)複製程式碼

我們需要為我們的平面建立新的頂點和分段函式,以及新的渲染管道和深度模板狀態。在剛才建立命令列佇列的程式碼上面,新增以下程式碼:

let debugGeometryVertexFunction = defaultLibrary.makeFunction(name: "vertexDebugPlane")!
let debugGeometryFragmentFunction = defaultLibrary.makeFunction(name: "fragmentDebugPlane")!
anchorPipelineStateDescriptor.vertexFunction =  debugGeometryVertexFunction
anchorPipelineStateDescriptor.fragmentFunction = debugGeometryFragmentFunction
do { try debugPipelineState = device.makeRenderPipelineState(descriptor: anchorPipelineStateDescriptor)
} catch let error { print(error) }
debugDepthState = device.makeDepthStencilState(descriptor: anchorDepthStateDescriptor)複製程式碼

再次,在 setupAssets() 方法中,我們需要建立一個新的 Model I/O 平面網格,然後在通過它建立一個 Metal 網格。在這個方法的末尾新增下面的程式碼:

mdlMesh = MDLMesh(planeWithExtent: vector3(0.1, 0.1, 0.1), segments: vector2(1, 1), geometryType: .triangles, allocator: metalAllocator)
mdlMesh.vertexDescriptor = vertexDescriptor
do { try debugMesh = MTKMesh(mesh: mdlMesh, device: device)
} catch let error { print(error) }複製程式碼

下一步,在 updateBufferStates() 方法中,我們需要更新平面所在快取區的地址。新增下面的程式碼:

debugUniformBufferOffset = alignedInstanceUniformSize * uniformBufferIndex
debugUniformBufferAddress = debugUniformBuffer.contents().advanced(by: debugUniformBufferOffset)複製程式碼

接下來,在 updateAnchors() 方法中,我們需要更新轉換矩陣和錨點的數量。在迴圈之前新增下面的程式碼:

let count = frame.anchors.filter{ $0.isKind(of: ARPlaneAnchor.self) }.count
debugInstanceCount = min(count, maxAnchorInstanceCount - (anchorInstanceCount - count))複製程式碼

然後,在迴圈中用下面程式碼替換最後的三行程式碼:

if anchor.isKind(of: ARPlaneAnchor.self) {
let transform = anchor.transform * rotationMatrix(rotation: float3(0, 0, Float.pi/2))
let modelMatrix = simd_mul(transform, coordinateSpaceTransform)
let debugUniforms = debugUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)
debugUniforms.pointee.modelMatrix = modelMatrix
} else {
let modelMatrix = simd_mul(anchor.transform, coordinateSpaceTransform)
let anchorUniforms = anchorUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)
anchorUniforms.pointee.modelMatrix = modelMatrix
}複製程式碼

我們必須以 Z 軸為軸心扭轉平面 90°,這樣我們就可以使平面保持水平。注意我們使用了一個叫做 rotationMatrix() 的自定義方法,現在來讓我們定義這個方法。在我早先的文章第一次介紹 3D 轉換時提到過這個矩陣:

func rotationMatrix(rotation: float3) -> float4x4 {
var matrix: float4x4 = matrix_identity_float4x4
let x = rotation.x
let y = rotation.y
let z = rotation.z
matrix.columns.0.x = cos(y) * cos(z)
matrix.columns.0.y = cos(z) * sin(x) * sin(y) - cos(x) * sin(z)
matrix.columns.0.z = cos(x) * cos(z) * sin(y) + sin(x) * sin(z)
matrix.columns.1.x = cos(y) * sin(z)
matrix.columns.1.y = cos(x) * cos(z) + sin(x) * sin(y) * sin(z)
matrix.columns.1.z = -cos(z) * sin(x) + cos(x) * sin(y) * sin(z)
matrix.columns.2.x = -sin(y)
matrix.columns.2.y = cos(y) * sin(x)
matrix.columns.2.z = cos(x) * cos(y)
matrix.columns.3.w = 1.0
return matrix
}複製程式碼

接著,在 drawAnchorGeometry() 方法中,我們需要確保我們在渲染的時候至少擁有一個錨點,用下面的程式碼替換方法第一行:

guard anchorInstanceCount - debugInstanceCount > 0 else { return }複製程式碼

再然後,讓我們最後建立 drawDebugGeometry() 方法來繪製我們的平面。它和錨點渲染方法是非常相似的:

func drawDebugGeometry(renderEncoder: MTLRenderCommandEncoder) {
guard debugInstanceCount > 0 else { return }
renderEncoder.pushDebugGroup("DrawDebugPlanes")
renderEncoder.setCullMode(.back)
renderEncoder.setRenderPipelineState(debugPipelineState)
renderEncoder.setDepthStencilState(debugDepthState)
renderEncoder.setVertexBuffer(debugUniformBuffer, offset: debugUniformBufferOffset, index: 2)
renderEncoder.setVertexBuffer(sharedUniformBuffer, offset: sharedUniformBufferOffset, index: 3)
renderEncoder.setFragmentBuffer(sharedUniformBuffer, offset: sharedUniformBufferOffset, index: 3)
for bufferIndex in 0..<debugMesh.vertexBuffers.count {
let vertexBuffer = debugMesh.vertexBuffers[bufferIndex]
renderEncoder.setVertexBuffer(vertexBuffer.buffer, offset: vertexBuffer.offset, index:bufferIndex)
}
for submesh in debugMesh.submeshes {
renderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset, instanceCount: debugInstanceCount)
}
renderEncoder.popDebugGroup()
}複製程式碼

在渲染層還有最後件事要做 —— 就是在 update() 裡我們剛才結束編碼的那一行上面呼叫這個方法:

drawDebugGeometry(renderEncoder: renderEncoder)複製程式碼

接著,我們開啟 Shaders.metal 檔案,我們需要一個新的結構體,只需通過一個頂點描述符傳遞頂點的位置

typedef struct {
float3 position [[attribute(0)]];
} DebugVertex;複製程式碼

在頂點著色器中,我們使用模型檢視矩陣來更新頂點的位置:

vertex float4 vertexDebugPlane(DebugVertex in [[ stage_in]],
constant SharedUniforms &sharedUniforms [[ buffer(3) ]],
constant InstanceUniforms *instanceUniforms [[ buffer(2) ]],
ushort vid [[vertex_id]],
ushort iid [[instance_id]]) {
float4 position = float4(in.position, 1.0);
float4x4 modelMatrix = instanceUniforms[iid].modelMatrix;
float4x4 modelViewMatrix = sharedUniforms.viewMatrix * modelMatrix;
float4 outPosition = sharedUniforms.projectionMatrix * modelViewMatrix * position;
return outPosition;
}複製程式碼

最後,在片段著色器中,我們給平面一個鮮豔的顏色,使我們在檢視中可以一眼看到它:

fragment float4 fragmentDebugPlane() {
return float4(0.99, 0.42, 0.62, 1.0);
}複製程式碼

如果你執行這個 app , 當 APP 檢測到一個平面時,你應該能夠看到一個矩形,就像這樣:

接下來,我們可以通過檢測其他目標,或將視角從之前的檢測目標上移開來更新或者移除平面。其他的代理方法可以幫助我們實現這一點。接著我們可以研究碰撞和其他物理效果。當然,這只是一個未來的想象。

我想要感謝 Caroline 為本篇文章指定檢測目標(平面)! 按照慣例,原始碼 都發表在 Github 上。

期待下次相見!


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章