如何将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
相关产品推荐
相关产品推荐

