06_ARKit 相机:使用场景深度打造雾效

对物理环境应用虚拟雾。

概述

第二代 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)
        }
        
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容