我正在尝试使用 SceneKit 编写增强现实应用程序,并且使用 SCNSceneRenderer 的 unprojectPoint 方法给定 2D 像素和深度,我需要当前渲染帧中的准确 3D 点。这需要 x、y 和 z,其中 x 和 y 是像素坐标,通常 z 是从该帧的深度缓冲区读取的值。
SCNView 的委托有这个方法来渲染深度帧:
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
renderDepthFrame()
}
func renderDepthFrame(){
// setup our viewport
let viewport: CGRect = CGRect(x: 0, y: 0, width: Double(SettingsModel.model.width), height: Double(SettingsModel.model.height))
// depth pass descriptor
let renderPassDescriptor = MTLRenderPassDescriptor()
let depthDescriptor: MTLTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: MTLPixelFormat.depth32Float, width: Int(SettingsModel.model.width), height: Int(SettingsModel.model.height), mipmapped: false)
let depthTex = scnView!.device!.makeTexture(descriptor: depthDescriptor)
depthTex.label = "Depth Texture"
renderPassDescriptor.depthAttachment.texture = depthTex
renderPassDescriptor.depthAttachment.loadAction = .clear
renderPassDescriptor.depthAttachment.clearDepth = 1.0
renderPassDescriptor.depthAttachment.storeAction = .store
let commandBuffer = commandQueue.makeCommandBuffer()
scnRenderer.scene = scene
scnRenderer.pointOfView = scnView.pointOfView!
scnRenderer!.render(atTime: 0, viewport: viewport, commandBuffer: commandBuffer, passDescriptor: renderPassDescriptor)
// setup our depth buffer so the cpu can access it
let depthImageBuffer: MTLBuffer = scnView!.device!.makeBuffer(length: depthTex.width * depthTex.height*4, options: .storageModeShared)
depthImageBuffer.label = "Depth Buffer"
let blitCommandEncoder: MTLBlitCommandEncoder = commandBuffer.makeBlitCommandEncoder()
blitCommandEncoder.copy(from: renderPassDescriptor.depthAttachment.texture!, sourceSlice: 0, sourceLevel: 0, sourceOrigin: MTLOriginMake(0, 0, 0), sourceSize: MTLSizeMake(Int(SettingsModel.model.width), Int(SettingsModel.model.height), 1), to: depthImageBuffer, destinationOffset: 0, destinationBytesPerRow: 4*Int(SettingsModel.model.width), destinationBytesPerImage: 4*Int(SettingsModel.model.width)*Int(SettingsModel.model.height))
blitCommandEncoder.endEncoding()
commandBuffer.addCompletedHandler({(buffer) -> Void in
let rawPointer: UnsafeMutableRawPointer = UnsafeMutableRawPointer(mutating: depthImageBuffer.contents())
let typedPointer: UnsafeMutablePointer<Float> = rawPointer.assumingMemoryBound(to: Float.self)
self.currentMap = Array(UnsafeBufferPointer(start: typedPointer, count: Int(SettingsModel.model.width)*Int(SettingsModel.model.height)))
})
commandBuffer.commit()
}
这有效。我得到的深度值在 0 到 1 之间。问题是我无法在 unprojectPoint 中使用它们,因为尽管使用相同的 SCNScene 和 SCNCamera,但它们的缩放比例似乎与初始通道不同。
我的问题:
有没有办法直接从 SceneKit SCNView 的主通道获取深度值,而不必使用单独的 SCNRenderer 进行额外的通道?
为什么我的通道中的深度值与我通过命中测试然后取消投影获得的值不匹配?我的通道的深度值从 0.78 到 0.94。命中测试中的深度值范围从 0.89 到 0.97,奇怪的是,当我在 Python 中渲染场景时,它与场景的 OpenGL 深度值相匹配。
我的预感是这是视口中的差异,SceneKit 正在做一些事情将深度值从 -1 缩放到 1,就像 OpenGL 一样。
编辑:如果您想知道,我不能直接使用 hitTest 方法。对于我想要实现的目标来说,这太慢了。
SceneKit 默认使用对数刻度反向 Z 缓冲区。您可以很容易地禁用反向 Z 缓冲区 (
scnView.usesReverseZ = false
),但是将日志深度设置为线性分布的 [0, 1] 范围需要访问深度缓冲区、远剪裁范围的值和近剪裁范围的值剪裁范围。这是将非反向 z-log-深度取为 [0, 1] 范围内的线性分布深度的过程:
float delogDepth(float depth, float nearClip, float farClip) {
// The depth buffer is in Log Format. Probably a 24bit float depth with 8 for stencil.
// https://outerra.blogspot.com/2012/11/maximizing-depth-buffer-range-and.html
// We need to undo the log format.
// https://stackoverflow.com/questions/18182139/logarithmic-depth-buffer-linearization
float logTuneConstant = nearClip / farClip;
float deloggedDepth = ((pow(logTuneConstant * farClip + 1.0, depth) - 1.0) / logTuneConstant) / farClip;
// The values are going to hover around a particular range. Linearize that distribution.
// This part may not be necessary, depending on how you will use the depth.
// http://glampert.com/2014/01-26/visualizing-the-depth-buffer/
float negativeOneOneDepth = deloggedDepth * 2.0 - 1.0;
float zeroOneDepth = ((2.0 * nearClip) / (farClip + nearClip - negativeOneOneDepth * (farClip - nearClip)));
return zeroOneDepth;
}
作为解决方法,我切换到 OpenGL ES 并通过添加片段着色器来读取深度缓冲区,该片段着色器将深度值打包到 RGBA 渲染缓冲区 SCNShadable 中。
请参阅此处了解更多信息:http://concord-consortium.github.io/lab/experiments/webgl-gpgpu/webgl.html
我知道这是一种有效的方法,因为它经常在 OpenGL ES 设备和 WebGL 上的阴影贴图中使用,但这对我来说感觉很奇怪,我不应该这样做。如果有人能弄清楚 Metal 的视口转换,我仍然对另一个答案感兴趣。
这里有一些 Metal 代码,用于从 SceneKit 的默认 z 缓冲区中提取视图空间深度,基于 pprovins 答案和 对数深度缓冲区线性化:
float C = zNear / zFar;
float deloggedDepth = ((pow(C * zFar + 1.0, depth) - 1.0) / C);
float viewSpaceDepth = 1 / deloggedDepth;