You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Metal着色器处理的相机采集画面缩放到全屏?

实现Metal纹理全屏缩放(模拟resizeAspectFill效果)

要让Metal渲染的相机画面实现全屏填充(和AVCaptureVideoPreviewLayer的.resizeAspectFill效果一致),需要从捕获足够大的视频帧和改用Render Pipeline处理缩放两个核心点调整:


1. 确保AVCaptureSession输出高分辨率帧

你当前拿到的1170x1560是AVCaptureSession默认预设的小尺寸输出,先把session的预设改成高分辨率选项:

let captureSession = AVCaptureSession()
// 优先用.high,也可以根据设备支持选择.hevc3840x2160这类4K预设
captureSession.sessionPreset = .high

如果需要更精准匹配屏幕分辨率,还可以手动配置AVCaptureVideoDataOutput的输出尺寸:

let videoOutput = AVCaptureVideoDataOutput()
let screenScale = UIScreen.main.scale
videoOutput.videoSettings = [
    kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
    kCVPixelBufferWidthKey as String: UIScreen.main.bounds.width * screenScale,
    kCVPixelBufferHeightKey as String: UIScreen.main.bounds.height * screenScale
]

2. 改用Render Pipeline实现缩放填充

Compute Pipeline更适合并行计算任务,而画面缩放、拉伸这类渲染需求,用Render Pipeline更高效且容易实现resizeAspectFill逻辑。

步骤一:准备全屏顶点数据

创建覆盖整个Metal视图的矩形顶点,后续通过矩阵调整纹理坐标实现填充:

struct Vertex {
    var position: SIMD4<Float>
    var textureCoordinate: SIMD2<Float>
}

// 全屏顶点(覆盖屏幕),对应纹理原始坐标
let vertices: [Vertex] = [
    Vertex(position: SIMD4<Float>(1.0, 1.0, 0.0, 1.0), textureCoordinate: SIMD2<Float>(1.0, 0.0)),
    Vertex(position: SIMD4<Float>(1.0, -1.0, 0.0, 1.0), textureCoordinate: SIMD2<Float>(1.0, 1.0)),
    Vertex(position: SIMD4<Float>(-1.0, -1.0, 0.0, 1.0), textureCoordinate: SIMD2<Float>(0.0, 1.0)),
    Vertex(position: SIMD4<Float>(-1.0, 1.0, 0.0, 1.0), textureCoordinate: SIMD2<Float>(0.0, 0.0))
]

// 提前创建顶点缓冲区
let vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Vertex>.stride, options: [])

步骤二:修改Render函数(替换Compute逻辑)

private func render() {
    guard let pixelBuffer = self.pixelBuffer else { return }
    let inputWidth = CVPixelBufferGetWidth(pixelBuffer)
    let inputHeight = CVPixelBufferGetHeight(pixelBuffer)
    
    // 创建输入纹理
    var cvTextureOut: CVMetalTexture?
    CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache!, pixelBuffer, nil, .bgra8Unorm, inputWidth, inputHeight, 0, &cvTextureOut)
    guard let cvTexture = cvTextureOut, let inputTexture = CVMetalTextureGetTexture(cvTexture) else {
        fatalError("Failed to create metal textures")
    }
    
    guard let drawable = currentDrawable, let commandQueue = commandQueue else { return }
    
    // 计算AspectFill的纹理坐标变换矩阵
    let screenAspect = Float(drawable.texture.width) / Float(drawable.texture.height)
    let inputAspect = Float(inputWidth) / Float(inputHeight)
    var scale: Float = 1.0
    var offsetX: Float = 0
    var offsetY: Float = 0
    
    if screenAspect > inputAspect {
        // 屏幕更宽:纵向填满,横向裁剪
        scale = Float(drawable.texture.height) / Float(inputHeight)
        let scaledWidth = Float(inputWidth) * scale
        offsetX = (scaledWidth - Float(drawable.texture.width)) / 2 / scaledWidth
    } else {
        // 屏幕更高:横向填满,纵向裁剪
        scale = Float(drawable.texture.width) / Float(inputWidth)
        let scaledHeight = Float(inputHeight) * scale
        offsetY = (scaledHeight - Float(drawable.texture.height)) / 2 / scaledHeight
    }
    
    let textureTransform = float3x3(
        SIMD3<Float>(scale, 0, offsetX),
        SIMD3<Float>(0, scale, offsetY),
        SIMD3<Float>(0, 0, 1)
    )
    
    guard let commandBuffer = commandQueue.makeCommandBuffer(),
          let renderPassDescriptor = currentRenderPassDescriptor,
          let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
        return
    }
    
    // 设置渲染管线、顶点缓冲区和纹理
    renderEncoder.setRenderPipelineState(renderPipelineState)
    renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    renderEncoder.setVertexBytes(&textureTransform, length: MemoryLayout<float3x3>.stride, index: 1)
    renderEncoder.setFragmentTexture(inputTexture, index: 0)
    
    // 绘制全屏矩形
    renderEncoder.drawPrimitives(type: .triangleFan, vertexStart: 0, vertexCount: 4)
    
    renderEncoder.endEncoding()
    commandBuffer.present(drawable)
    commandBuffer.commit()
}

步骤三:编写Metal着色器

创建.metal文件,实现顶点和片元着色器:

#include <metal_stdlib>
using namespace metal;

struct VertexIn {
    float4 position;
    float2 textureCoordinate;
};

struct VertexOut {
    float4 position [[position]];
    float2 textureCoordinate;
};

vertex VertexOut vertexShader(const VertexIn in [[stage_in]],
                           constant float3x3& textureTransform [[buffer(1)]]) {
    VertexOut out;
    out.position = in.position;
    // 应用纹理坐标变换,实现aspectFill
    float3 transformed = textureTransform * float3(in.textureCoordinate, 1.0);
    out.textureCoordinate = transformed.xy;
    return out;
}

fragment half4 fragmentShader(VertexOut in [[stage_in]],
                              texture2d<half, access::sample> inputTexture [[texture(0)]]) {
    constexpr sampler textureSampler(mip_filter::linear, mag_filter::linear, min_filter::linear);
    return inputTexture.sample(textureSampler, in.textureCoordinate);
}

步骤四:提前创建Render Pipeline State

在Metal初始化逻辑中添加:

private func setupRenderPipeline() {
    guard let device = MTLCreateSystemDefaultDevice() else { fatalError("Metal not supported") }
    let library = device.makeDefaultLibrary()!
    let vertexFunction = library.makeFunction(name: "vertexShader")!
    let fragmentFunction = library.makeFunction(name: "fragmentShader")!
    
    let pipelineDescriptor = MTLRenderPipelineDescriptor()
    pipelineDescriptor.vertexFunction = vertexFunction
    pipelineDescriptor.fragmentFunction = fragmentFunction
    pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
    
    do {
        renderPipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
    } catch {
        fatalError("Failed to create render pipeline state: \(error)")
    }
}

为什么之前的Compute方式不行?

你当前用Compute Encoder时,是按照输入纹理的尺寸调度线程组,这意味着只有输入纹理大小的区域会被渲染,而屏幕尺寸的drawable其余区域没有被写入,所以画面只能显示1170x1560的大小。Render Pipeline则会直接覆盖整个屏幕视口,通过顶点和纹理坐标的计算,把输入纹理缩放填充到全屏,完全匹配resizeAspectFill的效果。

内容的提问来源于stack exchange,提问作者Victor Pobedrya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 03:54:10