通過(guò)渲染相機(jī)圖像和使用位置跟蹤信息來(lái)顯示復(fù)雜內(nèi)容并構(gòu)建自定義AR視圖。
概觀
ARKit庫(kù)包含了使用SceneKit或SpriteKit顯示簡(jiǎn)單AR體驗(yàn)的視圖類。但是,如果你建立自己的渲染引擎(或與第三方引擎集成)蓖柔,則ARKit還提供了AR體驗(yàn)自定義視圖所需的全部支持類。
在任何AR場(chǎng)景中风纠,第一步是配置一個(gè)ARSession對(duì)象來(lái)管理攝像頭拍攝和運(yùn)動(dòng)處理况鸣。該會(huì)話定義和維護(hù)設(shè)備所在真實(shí)世界空間與AR內(nèi)容建模的虛擬空間之間的對(duì)應(yīng)關(guān)系。要在自定義視圖中顯示你的AR場(chǎng)景竹观,需要:
- 從會(huì)話中檢索視頻幀和跟蹤信息镐捧。
- 將這些框架圖像作為視圖的背景渲染。
- 使用跟蹤信息在攝像機(jī)圖像上方定位和繪制AR內(nèi)容臭增。
Tips
本文包含了Xcode項(xiàng)目模板中的代碼懂酱。有關(guān)完整示例代碼,請(qǐng)使用AR模板創(chuàng)建新的iOS應(yīng)用程序誊抛,然后在彈出菜單中選擇“Metal”列牺。
從會(huì)話獲取視頻幀和跟蹤數(shù)據(jù)
創(chuàng)建和持有自己的ARSession實(shí)例,并使用適合你想要支持的AR場(chǎng)景類型的會(huì)話配置來(lái)運(yùn)行它拗窃。(請(qǐng)參閱Building a Basic AR Experience瞎领。)會(huì)話從相機(jī)捕獲影像,在已經(jīng)建模的3D空間中跟蹤設(shè)備的位置和方向随夸,并提供ARFrame對(duì)象九默。每個(gè)這樣的對(duì)象都可以從當(dāng)前捕獲的幀中獲取單獨(dú)的視頻幀圖像和位置跟蹤信息。
AR會(huì)話有兩種訪問(wèn)ARFrame的方式逃魄,這取決于你的應(yīng)用程序是傾向于pull或push設(shè)計(jì)模式荤西。
如果您喜歡控制幀的時(shí)序(pull設(shè)計(jì)模式)澜搅,請(qǐng)使用會(huì)話的currentFrame屬性伍俘,在每次重新繪制視圖的內(nèi)容時(shí)來(lái)獲取當(dāng)前的幀圖像和跟蹤信息邪锌。 ARKit Xcode示例使用這種方法:
//在Renderer類中,從MTKViewDelegate.draw(in:)通過(guò)Renderer.update()中調(diào)用
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)
}
}
或者癌瘾,如果您的應(yīng)用程序傾向于push模式觅丰,可以實(shí)現(xiàn)session(_:didUpdate:)
代理方法,并且會(huì)話將會(huì)在每次捕獲視頻幀時(shí)調(diào)用(默認(rèn)情況下為每秒60幀)妨退。
獲得一個(gè)視頻幀后妇萄,你將需要繪制相機(jī)圖像,并更新和呈現(xiàn)AR場(chǎng)景內(nèi)任何重疊內(nèi)容咬荷。
繪制相機(jī)圖像
每個(gè)ARFrame對(duì)象的capturedImage屬性包含了一個(gè)從設(shè)備攝像頭捕獲的像素緩沖區(qū)冠句。要將此圖像繪制為自定義視圖的背景,你需要從圖像內(nèi)容創(chuàng)建紋理幸乒,并提交使用這些紋理的GPU渲染命令懦底。
像素緩沖器的內(nèi)容被編碼為雙平面YCbCr(也稱為YUV)的數(shù)據(jù)格式;要渲染圖像,你需要將此像素?cái)?shù)據(jù)轉(zhuǎn)換為可繪制的RGB格式罕扎。對(duì)于使用Metal渲染聚唐,您可以在GPU著色器代碼中最有效地執(zhí)行此轉(zhuǎn)換。使用CVMetalTextureCache API從像素緩沖區(qū)創(chuàng)建兩個(gè)Metal紋理腔召,用于緩沖區(qū)的亮度(Y)和色度(CbCr)平面:
func updateCapturedImageTextures(frame: ARFrame) {
// 從提供的幀的捕獲圖像創(chuàng)建兩個(gè)紋理(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
}
接下來(lái)杆查,使用將顏色變換矩陣執(zhí)行YCbCr到RGB轉(zhuǎn)換的方法來(lái)編碼繪制這兩個(gè)紋理的渲染命令:
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:)方法來(lái)確保攝像機(jī)圖像覆蓋整個(gè)視圖。 使用該方法臀蛛,以及完整的Metal管道設(shè)置代碼的實(shí)例代碼亲桦,請(qǐng)參閱完整的Xcode模板。(使用AR模板創(chuàng)建新的iOS應(yīng)用程序浊仆,然后從彈出菜單中選擇Metal烙肺。)
跟蹤和渲染覆蓋內(nèi)容
AR經(jīng)驗(yàn)通常側(cè)重于渲染3D重疊內(nèi)容,使內(nèi)容似乎是在相機(jī)圖像中看到的真實(shí)世界的一部分氧卧。 為了達(dá)到這個(gè)幻想桃笙,使用ARAnchor課程來(lái)模擬您自己的3D內(nèi)容相對(duì)于現(xiàn)實(shí)世界空間的位置和方向。 錨點(diǎn)提供可在渲染過(guò)程中引用的變換沙绝。
例如搏明,當(dāng)用戶點(diǎn)擊屏幕時(shí),Xcode模板創(chuàng)建位于設(shè)備前面大約20厘米處的錨點(diǎn):
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)
}
}
在您的渲染引擎中闪檬,使用每個(gè)ARAnchor的transform屬性來(lái)放置可視內(nèi)容星著。 Xcode模板使用其handleTap方法中添加到會(huì)話中的每個(gè)錨來(lái)放置簡(jiǎn)單的立方體:
func updateAnchors(frame: ARFrame) {
//使用當(dāng)前幀的錨點(diǎn)矩陣更新錨的緩沖
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]
//翻轉(zhuǎn)Z軸將坐標(biāo)系從右手轉(zhuǎn)換為左手
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
在復(fù)雜的AR場(chǎng)景中,您可以使用命中測(cè)試或平面檢測(cè)來(lái)尋找真實(shí)世界平面的位置粗悯。 更多的詳細(xì)信息虚循,請(qǐng)參閱planeDetection 屬性和hitTest(_:types:)方法。 在這兩種情況下,ARKit都提供了ARAnchor的結(jié)果對(duì)象横缔,所以你仍然可以使用錨矩陣來(lái)放置視覺(jué)內(nèi)容铺遂。
渲染真實(shí)光照
當(dāng)你在場(chǎng)景中配置用于繪制3D內(nèi)容的陰影時(shí),請(qǐng)使用每個(gè)ARFrame中的預(yù)估照明信息來(lái)產(chǎn)生更逼真的陰影:
// 在Renderer.updateSharedUniforms(frame:):中
// 使用環(huán)境強(qiáng)度(如果提供)為場(chǎng)景設(shè)置光照
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
有關(guān)本示例的完整Metal設(shè)置和渲染命令集茎刚,請(qǐng)參閱完整的Xcode模板襟锐。 (使用AR模板創(chuàng)建新的iOS應(yīng)用程序,然后從彈出菜單中選擇Metal膛锭。)
參考:
Apple官方文檔 :Displaying an AR Experience with Metal