对物理环境应用虚拟雾。
概述
第二代 11 英寸 iPad Pro 和第四代 12.9 英寸 iPad Pro 等设备可以使用激光雷达扫描仪计算现实世界中物体与用户的距离。 在 iOS 14 的世界追踪体验中,ARKit 提供了一个缓冲区,用于描述对象与设备的距离(以米为单位)。
此示例应用程序使用深度缓冲区实时创建虚拟雾效果。 为了绘制其图形,示例应用程序使用了一个小型 Metal 渲染器。 ARKit 为摄像机源中的对象提供精确的深度值,因此示例应用程序使用金属性能着色器 (MPS) 应用高斯模糊来柔化雾效果。 在将相机图像绘制到屏幕上时,渲染器会检查每个像素的深度纹理,并根据该像素与设备的距离覆盖雾色。 有关使用金属采样纹理和绘图的更多信息,请参阅创建和采样纹理。
启用场景深度并运行会话
为了避免运行不支持的配置,示例应用首先检查设备是否支持场景深度。
if #available(iOS 14.0, *) {
if (!ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) || !ARWorldTrackingConfiguration.supportsFrameSemantics(.smoothedSceneDepth)) {
print("不支持")
} else {
print("支持")
loadWorld()
}
} else {
// Fallback on earlier versions
}
如果运行应用程序的设备不支持场景深度,示例项目将停止。 或者,应用程序可以向用户显示错误消息并在没有场景深度的情况下继续体验。
如果设备支持场景深度,示例应用程序会创建一个世界跟踪配置并启用 ARFrameSemantics 属性上的 ARFrameSemanticSmoothedSceneDepth 选项。
configuration.frameSemantics = .smoothedSceneDepth
然后,示例项目通过运行会话开始 AR 体验。
session.run(configuration)
访问场景的深度
ARKit 在当前帧的 ARFrameSemanticSceneDepth 或 ARFrameSemanticSmoothedSceneDepth 属性上提供深度缓冲区 (depthMap) 作为 CVPixelBuffer,具体取决于启用的帧语义。 此示例应用默认可视化 ARFrameSemanticSmoothedSceneDepth。 ARFrameSemanticSceneDepth 中的原始深度值可以创建闪烁效果的印象,但是平均帧间深度差异的过程可以将视觉平滑为更逼真的雾效果。 出于调试目的,该示例允许通过屏幕切换在 ARFrameSemanticSmoothedSceneDepth 和 ARFrameSemanticSceneDepth 之间切换。
guard let sceneDepth = frame.smoothedSceneDepth ?? frame.sceneDepth else {
print("Failed to acquire scene depth.")
return
}
var pixelBuffer: CVPixelBuffer!
pixelBuffer = sceneDepth.depthMap
深度缓冲区中的每个像素都映射到可见场景的一个区域,该区域定义了该区域与设备的距离(以米为单位)。 因为示例项目使用 Metal 绘制到屏幕上,所以它将像素缓冲区转换为 Metal 纹理以将深度数据传输到 GPU 进行渲染。
var texturePixelFormat: MTLPixelFormat!
setMTLPixelFormat(&texturePixelFormat, basedOn: pixelBuffer)
depthTexture = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat: texturePixelFormat, planeIndex: 0)
为了设置深度纹理的金属像素格式,示例项目使用 depthMap 调用 CVPixelBufferGetPixelFormatType 并根据结果选择适当的映射。
fileprivate func setMTLPixelFormat(_ texturePixelFormat: inout MTLPixelFormat?, basedOn pixelBuffer: CVPixelBuffer!) {
if CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_DepthFloat32 {
texturePixelFormat = .r32Float
} else if CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_OneComponent8 {
texturePixelFormat = .r8Uint
} else {
fatalError("Unsupported ARDepthData pixel-buffer format.")
}
}
对深度缓冲区应用模糊
作为使用 Metal 渲染图形的一个好处,这个应用程序可以使用 MPS 的显示便利。 示例项目使用 MPS 高斯模糊滤镜制作逼真的雾。 实例化过滤器时,示例项目通过 sigma 5 来指定 5 像素半径模糊。
blurFilter = MPSImageGaussianBlur(device: device, sigma: 5)
为了以精度为代价获得性能,应用程序可以将 MPSKernelOptionsAllowReducedPrecision 添加到模糊过滤器的选项中,从而通过使用 half 而不是 float 来减少计算时间。
MPS 需要输入和输出图像来定义过滤操作的源和目标像素数据。
let inputImage = MPSImage(texture: depthTexture, featureChannels: 1)
let outputImage = MPSImage(texture: filteredDepthTexture, featureChannels: 1)
示例应用程序将输入和输出图像传递给模糊的编码函数,该函数将模糊安排在 GPU 上发生。
blur.encode(commandBuffer: commandBuffer, sourceImage: inputImage, destinationImage: outputImage)
In-place MPS 操作可以节省时间、内存和功耗。 由于 In-place MPS 需要为不支持它的设备提供后备代码,因此此示例项目不使用它。 有关就地操作的更多信息,请参阅图像过滤器。
可视化模糊深度以创建雾
Metal 通过向 GPU 提供一个绘制应用程序图形的片段着色器来进行渲染。 由于示例项目渲染相机图像,它通过调用 setFragmentTexture 为片段着色器打包相机图像。
enderEncoder.setFragmentTexture(CVMetalTextureGetTexture(cameraImageY), index: 0)
renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(cameraImageCbCr), index: 1)
接下来,示例应用程序打包过滤后的深度纹理。
renderEncoder.setFragmentTexture(filteredDepthTexture, index: 2)
示例项目的 GPU 端代码按索引参数的顺序字段纹理参数。 例如,片段着色器将上面索引为 0 的纹理字段作为包含后缀纹理(0)的参数,如下例所示。
fragment half4 fogFragmentShader(FogColorInOut in [[ stage_in ]],
texture2d<float, access::sample> cameraImageTextureY [[ texture(0) ]],
texture2d<float, access::sample> cameraImageTextureCbCr [[ texture(1) ]],
depth2d<float, access::sample> arDepthTexture [[ texture(2) ]],
为了输出渲染,Metal 为它绘制到目标的每个像素调用一次片段着色器。 示例项目的片段着色器首先读取相机图像中当前像素的 RGB 值。 对象“s”是一个采样器,它使着色器能够检查特定位置的纹理。 in.texCoordCamera 中的值是指该像素在相机图像中的相对位置。
constexpr sampler s(address::clamp_to_edge, filter::linear);
// 采样此像素的相机图像颜色。
float4 rgb = ycbcrToRGBTransform(
cameraImageTextureY.sample(s, in.texCoordCamera),
cameraImageTextureCbCr.sample(s, in.texCoordCamera)
);
half4 cameraColor = half4(rgb);
通过在 in.texCoordCamera 处对深度纹理进行采样,着色器在与相机图像相同的相对位置查询深度,并获取当前像素与设备的距离(以米为单位)。
float depth = arDepthTexture.sample(s, in.texCoordCamera);
为了确定覆盖该像素的雾量,示例应用程序使用当前像素的距离除以雾效果使场景完全饱和的距离来计算分数。
// 确定此片段的雾百分比。
float fogPercentage = depth / fogMax;
混合功能根据百分比混合两种颜色。 示例项目传入 RGB 值、雾颜色和雾百分比来为当前像素创建适量的雾。
half4 foggedColor = mix(cameraColor, fogColor, fogPercentage);
在 Metal 为每个像素调用片段着色器之后,视图将物理环境的最终雾化图像呈现到屏幕上。
可视化置信数据
ARKit 在 ARDepthData 中提供了 confidenceMap 属性来测量相应深度数据(depthMap)的准确度。 尽管此示例项目没有将深度置信度纳入其雾效果,但如果应用程序的算法需要,置信度数据可以过滤掉精度较低的深度值。
为了提供深度置信度,此示例应用程序使用 Shaders.metal 文件中的 confidenceDebugVisualizationEnabled 在运行时可视化置信度数据。
// 设置为 `true` 以可视化置信度。
bool confidenceDebugVisualizationEnabled = false;
当渲染器访问当前帧的场景深度时,示例项目会创建 confidenceMap 的 Metal 纹理以将其绘制到 GPU 上。
pixelBuffer = sceneDepth.confidenceMap
setMTLPixelFormat(&texturePixelFormat, basedOn: pixelBuffer)
confidenceTexture = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat: texturePixelFormat, planeIndex: 0)
在渲染器安排其绘制时,示例项目通过调用 setFragmentTexture 为 GPU 打包置信度纹理。
renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(confidenceTexture), index: 3)
GPU 端代码将置信度数据字段作为片段着色器的第三个纹理参数。
texture2d<uint> arDepthConfidence [[ texture(3) ]])
为了访问当前像素深度的置信度值,片段着色器在 in.texCoordCamera 处对置信度纹理进行采样。 此纹理中的每个置信度值都是其在 ARConfidenceLevel 枚举中的对应情况的 uint 等价物。
uint confidence = arDepthConfidence.sample(s, in.texCoordCamera).x;
基于当前像素的置信度值,片段着色器创建一个归一化的置信度百分比来覆盖。
float confidencePercentage = (float)confidence / (float)maxConfidence;
示例项目调用 mix 函数,根据置信百分比将置信颜色混合到处理后的像素中。
return mix(confidenceColor, foggedColor, confidencePercentage);
在 Metal 为每个像素调用片段着色器之后,视图会显示增强了置信度可视化的相机图像。
此示例使用红色来识别深度置信度低于 ARConfidenceLevelHigh 的场景部分。 在归一化百分比为 0 的低置信深度值下,可视化呈现纯红色 (confidenceColor)。 对于值为 1 的高置信深度值,mix 调用返回未经过滤的雾化相机图像颜色 (foggedColor)。 在场景的中等置信度区域,mix 调用返回两种颜色的混合,将微红色调应用到雾化的相机图像。
代码:
Shaders.metal
// 示例应用程序的着色器。
#include <metal_stdlib>
#include <simd/simd.h>
// 包括在此 Metal 着色器代码和执行 Metal API 命令的 C 代码之间共享的标头。
#import "ShaderTypes.h"
using namespace metal;
typedef struct {
float2 position [[attribute(kVertexAttributePosition)]];
float2 texCoord [[attribute(kVertexAttributeTexcoord)]];
} ImageVertex;
typedef struct {
float4 position [[position]];
float2 texCoord;
} ImageColorInOut;
// 从 YCbCr 转换为 rgb。
float4 ycbcrToRGBTransform(float4 y, float4 CbCr) {
const float4x4 ycbcrToRGBTransform = float4x4(
float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
);
float4 ycbcr = float4(y.r, CbCr.rg, 1.0);
return ycbcrToRGBTransform * ycbcr;
}
typedef struct {
float2 position;
float2 texCoord;
} FogVertex;
typedef struct {
float4 position [[position]];
float2 texCoordCamera;
float2 texCoordScene;
} FogColorInOut;
// 雾化图像顶点函数。
vertex FogColorInOut fogVertexTransform(const device FogVertex* cameraVertices [[ buffer(0) ]],
const device FogVertex* sceneVertices [[ buffer(1) ]],
unsigned int vid [[ vertex_id ]]) {
FogColorInOut out;
const device FogVertex& cv = cameraVertices[vid];
const device FogVertex& sv = sceneVertices[vid];
out.position = float4(cv.position, 0.0, 1.0);
out.texCoordCamera = cv.texCoord;
out.texCoordScene = sv.texCoord;
return out;
}
// 雾片段功能。
fragment half4 fogFragmentShader(FogColorInOut in [[ stage_in ]],
texture2d<float, access::sample> cameraImageTextureY [[ texture(0) ]],
texture2d<float, access::sample> cameraImageTextureCbCr [[ texture(1) ]],
depth2d<float, access::sample> arDepthTexture [[ texture(2) ]],
texture2d<uint> arDepthConfidence [[ texture(3) ]])
{
// 是否显示置信度调试可视化。
// -标签: ConfidenceVisualization-
// 设置为“true”以可视化信度(Confidence)。
bool confidenceDebugVisualizationEnabled = false;
// 将最大雾饱和度设置为 4.0 米。 设备最大为 5.0 米。
const float fogMax = 4.0;
// 雾完全不透明,中灰色
const half4 fogColor = half4(0.5, 0.5, 0.5, 1.0);
// 信度调试可视化是红色的。
const half4 confidenceColor = half4(1.0, 0.0, 0.0, 1.0);
// 最大置信度为 `ARConfidenceLevelHigh` = 2。
const uint maxConfidence = 2;
// 创建一个对象来采样纹理。
constexpr sampler s(address::clamp_to_edge, filter::linear);
// 采样此像素的相机图像颜色。
float4 rgb = ycbcrToRGBTransform(
cameraImageTextureY.sample(s, in.texCoordCamera),
cameraImageTextureCbCr.sample(s, in.texCoordCamera)
);
half4 cameraColor = half4(rgb);
// 采样此像素的深度值。
float depth = arDepthTexture.sample(s, in.texCoordCamera);
// 忽略大于最大雾距离的深度值。
depth = clamp(depth, 0.0, fogMax);
// 确定此片段的雾百分比。
float fogPercentage = depth / fogMax;
// 根据雾的百分比混合相机和雾的颜色。
half4 foggedColor = mix(cameraColor, fogColor, fogPercentage);
// 如果禁用置信度可视化,只需返回雾化颜色。
if(!confidenceDebugVisualizationEnabled) {
return foggedColor;
} else {
// 采样深度置信度。
uint confidence = arDepthConfidence.sample(s, in.texCoordCamera).x;
// 根据置信度分配颜色百分比。
float confidencePercentage = (float)confidence / (float)maxConfidence;
// 回混合置信度和foggedColor。
return mix(confidenceColor, foggedColor, confidencePercentage);
}
}
// 在着色器和宿主应用程序代码之间共享的类型和枚举。
#ifndef ShaderTypes_h
#define ShaderTypes_h
#include <simd/simd.h>
// 着色器和 C 代码之间共享的缓冲区索引值,
// 以确保 Metal 着色器缓冲区输入匹配 Metal API 缓冲区集调用
typedef enum BufferIndices {
kBufferIndexMeshPositions = 0,
kBufferIndexMeshGenerics = 1,
kBufferIndexInstanceUniforms = 2,
kBufferIndexSharedUniforms = 3
} BufferIndices;
// 着色器和 C 代码之间共享的属性索引值,
// 以确保 Metal 着色器顶点属性索引与 Metal API 顶点描述符属性索引匹配
typedef enum VertexAttributes {
kVertexAttributePosition = 0,
kVertexAttributeTexcoord = 1,
kVertexAttributeNormal = 2
} VertexAttributes;
// 着色器和 C 代码之间共享的纹理索引值,
// 以确保 Metal 着色器纹理索引匹配 Metal API 纹理集调用的索引
typedef enum TextureIndices {
kTextureIndexColor = 0,
kTextureIndexY = 1,
kTextureIndexCbCr = 2,
} TextureIndices;
// 着色器和 C 代码之间共享的结构,
// 以确保在 Metal 着色器中访问的共享统一数据的布局与 C 代码中统一数据集的布局相匹配
typedef struct {
// 相机
matrix_float4x4 projectionMatrix;
matrix_float4x4 viewMatrix;
// 照明属性
vector_float3 ambientLightColor;
vector_float3 directionalLightDirection;
vector_float3 directionalLightColor;
float materialShininess;
// 消光
int useDepth;
} SharedUniforms;
// 着色器和 C 代码之间共享的结构,
// 以确保在 Metal 着色器中访问的实例统一数据的布局与 C 代码中统一数据集的布局相匹配
typedef struct {
matrix_float4x4 modelMatrix;
} InstanceUniforms;
#endif /* ShaderTypes_h */
// 主应用程序渲染器
import Foundation
import Metal
import MetalKit
import ARKit
import MetalPerformanceShaders
protocol RenderDestinationProvider {
var currentRenderPassDescriptor: MTLRenderPassDescriptor? { get }
var currentDrawable: CAMetalDrawable? { get }
var colorPixelFormat: MTLPixelFormat { get set }
var sampleCount: Int { get set }
}
// 飞行中的命令缓冲区的最大数量。
let kMaxBuffersInFlight: Int = 3
// 图像平面的顶点数据。
let kImagePlaneVertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 1.0, 1.0,
-1.0, 1.0, 0.0, 0.0,
1.0, 1.0, 1.0, 0.0
]
class Renderer {
let session: ARSession
let device: MTLDevice
let inFlightSemaphore = DispatchSemaphore(value: kMaxBuffersInFlight)
var renderDestination: RenderDestinationProvider
// Metal 物体。
var commandQueue: MTLCommandQueue!
// 保存用于源和目标渲染的顶点信息的对象。
var imagePlaneVertexBuffer: MTLBuffer!
// 定义渲染相机图像和雾的 Metal 着色器的对象。
var fogPipelineState: MTLRenderPipelineState!
// 用于将当前相机图像传输到 GPU 进行渲染的纹理。
var cameraImageTextureY: CVMetalTexture?
var cameraImageTextureCbCr: CVMetalTexture?
// 用于存储当前帧深度信息的纹理。
var depthTexture: CVMetalTexture?
// 用于将置信度信息传递给 GPU 以进行雾渲染的纹理。
var confidenceTexture: CVMetalTexture?
// 将模糊深度数据的纹理传递给 GPU 以进行雾渲染。
var filteredDepthTexture: MTLTexture!
// 用于模糊渲染雾的深度数据的过滤器。
var blurFilter: MPSImageGaussianBlur?
// 捕获的图像纹理缓存。
var cameraImageTextureCache: CVMetalTextureCache!
// 当前视口大小。
var viewportSize: CGSize = CGSize()
// 视口大小更改的标志。
var viewportSizeDidChange: Bool = false
// 通过设置 AR 会话、GPU 和屏幕后备存储来初始化渲染器。
init(session: ARSession, metalDevice device: MTLDevice, renderDestination: RenderDestinationProvider) {
self.session = session
self.device = device
self.renderDestination = renderDestination
// 执行 Metal 对象的一次性设置。
loadMetal()
}
// 重新绘制尺寸
func drawRectResized(size: CGSize) {
viewportSize = size
viewportSizeDidChange = true
}
func update() {
// 等待以确保只有 kMaxBuffersInFlight 被 Metal 管道中的任何阶段(应用程序、Metal、驱动程序、GPU 等)处理。
_ = inFlightSemaphore.wait(timeout: DispatchTime.distantFuture)
// 为当前可绘制对象的每个渲染通道创建一个新的命令缓冲区。
if let commandBuffer = commandQueue.makeCommandBuffer() {
commandBuffer.label = "MyCommand"
// 添加完成处理程序,它在 Metal 和 GPU 完全完成处理
// 我们正在编码此帧的命令时发出 _inFlightSemaphore 信号。
// 这表示 Metal 和 GPU 何时不再需要我们正在写入此帧的动态缓冲区。
commandBuffer.addCompletedHandler { [weak self] commandBuffer in
if let strongSelf = self {
strongSelf.inFlightSemaphore.signal()
}
}
updateAppState()
applyGaussianBlur(commandBuffer: commandBuffer)
// 将深度和置信度像素缓冲区传递给 GPU 以在雾中着色。
if let renderPassDescriptor = renderDestination.currentRenderPassDescriptor, let currentDrawable = renderDestination.currentDrawable {
if let fogRenderEncoding = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) {
// 设置标签以在捕获的 Metal 帧中标识此渲染通道。
fogRenderEncoding.label = "MyFogRenderEncoder"
// 安排将相机图像和雾绘制到屏幕上。
doFogRenderPass(renderEncoder: fogRenderEncoding)
// 完成编码命令。
fogRenderEncoding.endEncoding()
}
// 使用当前可绘制对象在帧缓冲区完成后安排呈现。
commandBuffer.present(currentDrawable)
}
// 在此处完成渲染并将命令缓冲区推送到 GPU。
commandBuffer.commit()
}
}
// MARK: - Private
// 创建并加载我们的基本 Metal 状态对象。
func loadMetal() {
// 设置渲染所需的默认格式。
renderDestination.colorPixelFormat = .bgra8Unorm
renderDestination.sampleCount = 1
// 使用我们的图像平面顶点数据创建一个顶点缓冲区。
let imagePlaneVertexDataCount = kImagePlaneVertexData.count * MemoryLayout<Float>.size
imagePlaneVertexBuffer = device.makeBuffer(bytes: kImagePlaneVertexData, length: imagePlaneVertexDataCount, options: [])
imagePlaneVertexBuffer.label = "ImagePlaneVertexBuffer"
// 在项目中加载所有带有 Metal 文件扩展名的着色器文件。
let defaultLibrary = device.makeDefaultLibrary()!
// 为我们的图像平面顶点缓冲区创建一个顶点描述符。
let imagePlaneVertexDescriptor = MTLVertexDescriptor()
// 位置
imagePlaneVertexDescriptor.attributes[0].format = .float2
imagePlaneVertexDescriptor.attributes[0].offset = 0
imagePlaneVertexDescriptor.attributes[0].bufferIndex = Int(kBufferIndexMeshPositions.rawValue)
// 纹理坐标
imagePlaneVertexDescriptor.attributes[1].format = .float2
imagePlaneVertexDescriptor.attributes[1].offset = 8
imagePlaneVertexDescriptor.attributes[1].bufferIndex = Int(kBufferIndexMeshPositions.rawValue)
// 缓冲区布局。
imagePlaneVertexDescriptor.layouts[0].stride = 16
imagePlaneVertexDescriptor.layouts[0].stepRate = 1
imagePlaneVertexDescriptor.layouts[0].stepFunction = .perVertex
// 创建相机图像纹理缓存。
var textureCache: CVMetalTextureCache?
CVMetalTextureCacheCreate(nil, nil, device, nil, &textureCache)
cameraImageTextureCache = textureCache
// 定义将在 GPU 上渲染相机图像和雾的着色器。
let fogVertexFunction = defaultLibrary.makeFunction(name: "fogVertexTransform")!
let fogFragmentFunction = defaultLibrary.makeFunction(name: "fogFragmentShader")!
let fogPipelineStateDescriptor = MTLRenderPipelineDescriptor()
fogPipelineStateDescriptor.label = "MyFogPipeline"
fogPipelineStateDescriptor.sampleCount = renderDestination.sampleCount
fogPipelineStateDescriptor.vertexFunction = fogVertexFunction
fogPipelineStateDescriptor.fragmentFunction = fogFragmentFunction
fogPipelineStateDescriptor.vertexDescriptor = imagePlaneVertexDescriptor
fogPipelineStateDescriptor.colorAttachments[0].pixelFormat = renderDestination.colorPixelFormat
// 初始化管道。
do {
try fogPipelineState = device.makeRenderPipelineState(descriptor: fogPipelineStateDescriptor)
} catch let error {
print("未能创建雾管道状态, error \(error)")
}
// 一帧渲染工作创建命令队列。
commandQueue = device.makeCommandQueue()
}
// 更新任何应用程序状态。
func updateAppState() {
// 获取 AR 会话的当前帧。
guard let currentFrame = session.currentFrame else {
return
}
// 准备当前帧的相机图像以传输到 GPU。
updateCameraImageTextures(frame: currentFrame)
// 准备当前帧的深度和置信度图像以传输到 GPU。
updateARDepthTexures(frame: currentFrame)
// 如果屏幕大小发生变化,则更新目标渲染顶点信息。
if viewportSizeDidChange {
viewportSizeDidChange = false
updateImagePlane(frame: currentFrame)
}
}
// 创建两个纹理(Y 和 CbCr)以将当前帧的相机图像传输到 GPU 进行渲染。
func updateCameraImageTextures(frame: ARFrame) {
if CVPixelBufferGetPlaneCount(frame.capturedImage) < 2 {
return
}
cameraImageTextureY = createTexture(fromPixelBuffer: frame.capturedImage, pixelFormat: .r8Unorm, planeIndex: 0)
cameraImageTextureCbCr = createTexture(fromPixelBuffer: frame.capturedImage, pixelFormat: .rg8Unorm, planeIndex: 1)
}
// 给定参数像素缓冲区的格式,分配适当的 MTL 像素格式。
fileprivate func setMTLPixelFormat(_ texturePixelFormat: inout MTLPixelFormat?, basedOn pixelBuffer: CVPixelBuffer!) {
if CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_DepthFloat32 {
texturePixelFormat = .r32Float
} else if CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_OneComponent8 {
texturePixelFormat = .r8Uint
} else {
fatalError("Unsupported ARDepthData pixel-buffer format.")
}
}
// 准备场景深度信息以传输到 GPU 进行渲染。
func updateARDepthTexures(frame: ARFrame) {
if #available(iOS 14.0, *) {
// 当前帧获取场景深度或平滑场景深度。
guard let sceneDepth = frame.smoothedSceneDepth ?? frame.sceneDepth else {
print("获取场景深度失败。")
return
}
var pixelBuffer: CVPixelBuffer!
pixelBuffer = sceneDepth.depthMap
// 为深度信息设置目标像素格式,
// 并根据 ARKit 提供的深度图像创建金属纹理。
var texturePixelFormat: MTLPixelFormat!
setMTLPixelFormat(&texturePixelFormat, basedOn: pixelBuffer)
depthTexture = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat: texturePixelFormat, planeIndex: 0)
// 从当前帧获取当前深度置信度值。
// 设置置信度信息的目标像素格式,
// 并根据 ARKit 提供的置信度图像创建 Metal 纹理。
pixelBuffer = sceneDepth.confidenceMap
setMTLPixelFormat(&texturePixelFormat, basedOn: pixelBuffer)
confidenceTexture = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat: texturePixelFormat, planeIndex: 0)
}
}
// 从参数平面索引处的 CVPixelBuffer 创建具有参数像素格式的 Metal 纹理。
func createTexture(fromPixelBuffer pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> CVMetalTexture? {
let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
var texture: CVMetalTexture? = nil
let status = CVMetalTextureCacheCreateTextureFromImage(nil, cameraImageTextureCache, pixelBuffer, nil, pixelFormat,
width, height, planeIndex, &texture)
if status != kCVReturnSuccess {
texture = nil
}
return texture
}
// 置顶点数据(源和目标矩形)渲染。
func updateImagePlane(frame: ARFrame) {
// 更新图像平面的纹理坐标以填充视口。
let displayToCameraTransform = frame.displayTransform(for: .landscapeRight, viewportSize: viewportSize).inverted()
let vertexData = imagePlaneVertexBuffer.contents().assumingMemoryBound(to: Float.self)
let fogVertexData = imagePlaneVertexBuffer.contents().assumingMemoryBound(to: Float.self)
for index in 0...3 {
let textureCoordIndex = 4 * index + 2
let textureCoord = CGPoint(x: CGFloat(kImagePlaneVertexData[textureCoordIndex]), y: CGFloat(kImagePlaneVertexData[textureCoordIndex + 1]))
let transformedCoord = textureCoord.applying(displayToCameraTransform)
vertexData[textureCoordIndex] = Float(transformedCoord.x)
vertexData[textureCoordIndex + 1] = Float(transformedCoord.y)
fogVertexData[textureCoordIndex] = Float(transformedCoord.x)
fogVertexData[textureCoordIndex + 1] = Float(transformedCoord.y)
}
}
// 安排要在 GPU 上渲染的相机图像和雾。
func doFogRenderPass(renderEncoder: MTLRenderCommandEncoder) {
guard let cameraImageY = cameraImageTextureY, let cameraImageCbCr = cameraImageTextureCbCr,
let confidenceTexture = confidenceTexture else {
return
}
// 推送一个调试组,使您能够在 Metal 帧捕获中识别此渲染通道。
renderEncoder.pushDebugGroup("FogPass")
// 设置渲染命令编码器状态。
renderEncoder.setCullMode(.none)
renderEncoder.setRenderPipelineState(fogPipelineState)
// 设置平面顶点缓冲区。
renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 0)
renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 1)
// 为雾片段着色器设置纹理。
renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(cameraImageY), index: 0)
renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(cameraImageCbCr), index: 1)
renderEncoder.setFragmentTexture(filteredDepthTexture, index: 2)
renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(confidenceTexture), index: 3)
// 绘制最终四边形以显示
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
renderEncoder.popDebugGroup()
}
// MARK: - MPS Filter
// 设置过滤器来处理深度纹理。
func setupFilter(width: Int, height: Int) {
// 创建一个目标后备存储来保存模糊结果。
let filteredDepthDescriptor = MTLTextureDescriptor()
filteredDepthDescriptor.pixelFormat = .r32Float
filteredDepthDescriptor.width = width
filteredDepthDescriptor.height = height
filteredDepthDescriptor.usage = [.shaderRead, .shaderWrite]
filteredDepthTexture = device.makeTexture(descriptor: filteredDepthDescriptor)
blurFilter = MPSImageGaussianBlur(device: device, sigma: 5)
}
// 使用 `blurFilter` 在 GPU 上安排要模糊的深度纹理。
func applyGaussianBlur(commandBuffer: MTLCommandBuffer) {
guard let arDepthTexture = depthTexture, let depthTexture = CVMetalTextureGetTexture(arDepthTexture) else {
print("错误:无法应用 MPS 过滤器。")
return
}
guard let blur = blurFilter else {
setupFilter(width: depthTexture.width, height: depthTexture.height)
return
}
let inputImage = MPSImage(texture: depthTexture, featureChannels: 1)
let outputImage = MPSImage(texture: filteredDepthTexture, featureChannels: 1)
blur.encode(commandBuffer: commandBuffer, sourceImage: inputImage, destinationImage: outputImage)
}
}
import UIKit
import ARKit
class Demo01_SelectARViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 14.0, *) {
if (!ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) || !ARWorldTrackingConfiguration.supportsFrameSemantics(.smoothedSceneDepth)) {
print("不支持")
} else {
print("支持")
let vc = Demo01_ARViewController()
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
}
} else {
// Fallback on earlier versions
}
}
}
import UIKit
import ARKit
import Metal
import MetalKit
extension MTKView: RenderDestinationProvider {}
class Demo01_ARViewController: UIViewController, ARSessionDelegate, MTKViewDelegate {
let mtkView = MTKView()
var session: ARSession!
var configuration = ARWorldTrackingConfiguration()
var renderer: Renderer!
override func viewDidLoad() {
super.viewDidLoad()
// 将此视图控制器设置为会话的委托。
session = ARSession()
session.delegate = self
// 将视图设置为使用默认设备。
mtkView.frame = self.view.bounds
self.view.addSubview(mtkView)
mtkView.device = MTLCreateSystemDefaultDevice()
mtkView.backgroundColor = .clear
mtkView.delegate = self
guard mtkView.device != nil else {
print("Metal 不支持这设备")
return
}
// 配置渲染器以绘制到视图。
renderer = Renderer(session: session, metalDevice: mtkView.device!, renderDestination: mtkView)
// 计划第一次绘制的屏幕大小。
renderer.drawRectResized(size: view.bounds.size)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 启用平滑的场景深度帧语义。
if #available(iOS 14.0, *) {
configuration.frameSemantics = .smoothedSceneDepth
configuration.frameSemantics = .sceneDepth
}
// 运行视图的会话。
session.run(configuration)
// 在 AR 体验期间屏幕不应变暗。
UIApplication.shared.isIdleTimerDisabled = true
}
// 自动隐藏主页指示器以最大限度地沉浸在 AR 体验中。
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
// 隐藏状态栏以最大限度地沉浸在 AR 体验中。
override var prefersStatusBarHidden: Bool {
return true
}
// MARK: - MTKViewDelegate
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
renderer.drawRectResized(size: size)
}
func draw(in view: MTKView) {
renderer.update()
}
// MARK: - ARSessionDelegate
func session(_ session: ARSession, didFailWithError error: Error) {
// 向用户显示错误消息。
guard error is ARError else { return }
let errorWithInfo = error as NSError
let messages = [
errorWithInfo.localizedDescription,
errorWithInfo.localizedFailureReason,
errorWithInfo.localizedRecoverySuggestion
]
let errorMessage = messages.compactMap({ $0 }).joined(separator: "\n")
DispatchQueue.main.async {
// 显示警报,告知已发生的错误。
let alertController = UIAlertController(title: "AR 会话失败。", message: errorMessage, preferredStyle: .alert)
let restartAction = UIAlertAction(title: "重新启动会话", style: .default) { _ in
alertController.dismiss(animated: true, completion: nil)
self.session.run(self.configuration, options: .resetSceneReconstruction)
}
alertController.addAction(restartAction)
self.present(alertController, animated: true, completion: nil)
}
}
}