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

请求指导:基于SwiftUI与Metal Shader实现图片自由拉伸变形

实现SwiftUI+Metal的图片自由拉伸变形功能

核心思路

放弃SwiftUI内置的resizable()等布局约束,通过以下三步实现需求:

  1. 用SwiftUI手势捕获拖拽位置,维护图片四个边角的坐标状态
  2. 将坐标状态传递给封装在UIViewRepresentable中的MTKView
  3. 在Metal Shader中基于边角坐标实现逐像素的纹理映射,完成自由变形渲染

分步实现

1. 状态管理:定义边角坐标结构体

用于存储拖拽过程中四个边角的实时位置:

struct QuadCorners {
    var topLeft: CGPoint
    var topRight: CGPoint
    var bottomLeft: CGPoint
    var bottomRight: CGPoint
    
    static func initial(for size: CGSize) -> Self {
        .init(
            topLeft: .zero,
            topRight: CGPoint(x: size.width, y: 0),
            bottomLeft: CGPoint(x: 0, y: size.height),
            bottomRight: CGPoint(x: size.width, y: size.height)
        )
    }
}

2. SwiftUI视图层:手势与渲染容器

实现拖拽控制点和Metal渲染视图的布局:

struct ImageStretchView: View {
    @State private var corners: QuadCorners
    private let image: UIImage
    private let renderSize: CGSize
    
    init(image: UIImage, renderSize: CGSize = CGSize(width: 600, height: 400)) {
        self.image = image
        self.renderSize = renderSize
        self._corners = State(initialValue: .initial(for: renderSize))
    }
    
    var body: some View {
        ZStack {
            // Metal渲染视图
            MetalImageRenderer(image: image, corners: corners)
                .frame(width: renderSize.width, height: renderSize.height)
            
            // 四个边角拖拽控制点
            DragControl(point: $corners.topLeft)
                .position(corners.topLeft)
            DragControl(point: $corners.topRight)
                .position(corners.topRight)
            DragControl(point: $corners.bottomLeft)
                .position(corners.bottomLeft)
            DragControl(point: $corners.bottomRight)
                .position(corners.bottomRight)
            
            // 可选:添加边缘拖拽控制点(逻辑同边角,拖拽时同步更新对应两个边角坐标)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.black)
    }
}

// 拖拽控制点组件
struct DragControl: View {
    @Binding var point: CGPoint
    
    var body: some View {
        Circle()
            .fill(Color.blue)
            .frame(width: 20, height: 20)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        point = value.location
                    }
            )
    }
}

3. Metal集成:SwiftUI与MTKView绑定

将MTKView封装为SwiftUI可识别的视图,传递边角状态并处理渲染逻辑:

struct MetalImageRenderer: UIViewRepresentable {
    let image: UIImage
    let corners: QuadCorners
    
    func makeUIView(context: Context) -> MTKView {
        let mtkView = MTKView()
        guard let device = MTLCreateSystemDefaultDevice() else {
            fatalError("Metal is not supported on this device")
        }
        mtkView.device = device
        
        // 加载Metal管道
        let library = try! device.makeDefaultLibrary()
        let vertexShader = library.makeFunction(name: "vertexShader")!
        let fragmentShader = library.makeFunction(name: "fragmentShader")!
        
        let pipelineDescriptor = MTLRenderPipelineDescriptor()
        pipelineDescriptor.vertexFunction = vertexShader
        pipelineDescriptor.fragmentFunction = fragmentShader
        pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        
        let pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)
        context.coordinator.pipelineState = pipelineState
        
        // 转换UIImage为Metal纹理
        context.coordinator.texture = try! image.makeMetalTexture(device: device)
        
        mtkView.delegate = context.coordinator
        return mtkView
    }
    
    func updateUIView(_ uiView: MTKView, context: Context) {
        context.coordinator.corners = corners
        uiView.setNeedsDisplay()
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, MTKViewDelegate {
        var pipelineState: MTLRenderPipelineState!
        var texture: MTLTexture!
        var corners: QuadCorners = .initial(for: .zero)
        
        func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
        
        func draw(in view: MTKView) {
            guard let commandQueue = view.device?.makeCommandQueue(),
                  let renderPassDescriptor = view.currentRenderPassDescriptor,
                  let drawable = view.currentDrawable else { return }
            
            // 准备顶点数据(位置+原始纹理坐标)
            struct Vertex {
                var position: [Float]
                var texCoord: [Float]
            }
            let vertices = [
                Vertex(position: [corners.topLeft.x, corners.topLeft.y], texCoord: [0, 1]),
                Vertex(position: [corners.topRight.x, corners.topRight.y], texCoord: [1, 1]),
                Vertex(position: [corners.bottomLeft.x, corners.bottomLeft.y], texCoord: [0, 0]),
                Vertex(position: [corners.bottomRight.x, corners.bottomRight.y], texCoord: [1, 0])
            ]
            let vertexBuffer = view.device?.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Vertex>.stride)
            
            // 传递边角数据到片段着色器
            struct CornerData {
                var topLeft: SIMD2<Float>
                var topRight: SIMD2<Float>
                var bottomLeft: SIMD2<Float>
                var bottomRight: SIMD2<Float>
            }
            let cornerData = CornerData(
                topLeft: SIMD2<Float>(Float(corners.topLeft.x), Float(corners.topLeft.y)),
                topRight: SIMD2<Float>(Float(corners.topRight.x), Float(corners.topRight.y)),
                bottomLeft: SIMD2<Float>(Float(corners.bottomLeft.x), Float(corners.bottomLeft.y)),
                bottomRight: SIMD2<Float>(Float(corners.bottomRight.x), Float(corners.bottomRight.y))
            )
            let cornerBuffer = view.device?.makeBuffer(bytes: &cornerData, length: MemoryLayout<CornerData>.stride)
            
            // 传递渲染尺寸
            let renderSize = SIMD2<Float>(Float(view.drawableSize.width), Float(view.drawableSize.height))
            let sizeBuffer = view.device?.makeBuffer(bytes: &renderSize, length: MemoryLayout<SIMD2<Float>>.stride)
            
            // 编码渲染命令
            let commandBuffer = commandQueue.makeCommandBuffer()!
            let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
            
            renderEncoder.setRenderPipelineState(pipelineState)
            renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
            renderEncoder.setFragmentBuffer(cornerBuffer, offset: 0, index: 0)
            renderEncoder.setFragmentBuffer(sizeBuffer, offset: 0, index: 1)
            renderEncoder.setFragmentTexture(texture, index: 0)
            
            // 绘制四边形(三角带模式)
            renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
            
            renderEncoder.endEncoding()
            commandBuffer.present(drawable)
            commandBuffer.commit()
        }
    }
}

// UIImage转Metal纹理扩展
extension UIImage {
    func makeMetalTexture(device: MTLDevice) throws -> MTLTexture {
        guard let cgImage = cgImage else { throw NSError(domain: "ImageError", code: 0, userInfo: nil) }
        
        let descriptor = MTLTextureDescriptor.texture2DDescriptor(
            pixelFormat: .rgba8Unorm,
            width: cgImage.width,
            height: cgImage.height,
            mipmapped: false
        )
        let texture = device.makeTexture(descriptor: descriptor)!
        
        let bytesPerRow = cgImage.width * 4
        let context = CGContext(
            data: texture.contents,
            width: cgImage.width,
            height: cgImage.height,
            bitsPerComponent: 8,
            bytesPerRow: bytesPerRow,
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
        )!
        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
        
        return texture
    }
}

4. Metal Shader:实现自由变形渲染

创建ImageStretchShader.metal文件,编写顶点和片段着色器:

#include <metal_stdlib>
using namespace metal;

struct VertexIn {
    float2 position [[attribute(0)]];
    float2 texCoord [[attribute(1)]];
};

struct VertexOut {
    float4 position [[position]];
    float2 texCoord [[user(0)]];
};

struct CornerData {
    float2 topLeft;
    float2 topRight;
    float2 bottomLeft;
    float2 bottomRight;
};

// 顶点着色器:将SwiftUI坐标转换为Metal归一化设备坐标
vertex VertexOut vertexShader(VertexIn in [[stage_in]],
                              constant float2& renderSize [[buffer(1)]]) {
    VertexOut out;
    float2 normalizedPos = (in.position / renderSize) * 2.0 - 1.0;
    out.position = float4(normalizedPos.x, -normalizedPos.y, 0.0, 1.0); // 翻转Y轴适配Metal坐标系
    out.texCoord = in.texCoord;
    return out;
}

// 计算点在四边形内的重心坐标,实现纹理映射
float2 getOriginalTexCoord(float2 pixelPos, CornerData corners, float2 textureSize) {
    // 将四边形拆分为两个三角形计算重心坐标
    float2 v0 = corners.topLeft;
    float2 v1 = corners.topRight;
    float2 v2 = corners.bottomLeft;
    float2 v3 = corners.bottomRight;
    
    // 第一个三角形:v0-v1-v2
    float denom = (v1.y - v2.y) * (v0.x - v2.x) + (v2.x - v1.x) * (v0.y - v2.y);
    float baryX = ((v1.y - v2.y) * (pixelPos.x - v2.x) + (v2.x - v1.x) * (pixelPos.y - v2.y)) / denom;
    float baryY = ((v2.y - v0.y) * (pixelPos.x - v2.x) + (v0.x - v2.x) * (pixelPos.y - v2.y)) / denom;
    float baryZ = 1.0 - baryX - baryY;
    
    if (baryX >= 0.0 && baryY >= 0.0 && baryZ >= 0.0) {
        float2 originalPos = baryX * v0 + baryY * v1 + baryZ * v2;
        return originalPos / textureSize;
    }
    
    // 第二个三角形:v1-v3-v2
    denom = (v3.y - v2.y) * (v1.x - v2.x) + (v2.x - v3.x) * (v1.y - v2.y);
    baryX = ((v3.y - v2.y) * (pixelPos.x - v2.x) + (v2.x - v3.x) * (pixelPos.y - v2.y)) / denom;
    baryY = ((v2.y - v1.y) * (pixelPos.x - v2.x) + (v1.x - v2.x) * (pixelPos.y - v2.y)) / denom;
    baryZ = 1.0 - baryX - baryY;
    
    float2 originalPos = baryX * v1 + baryY * v3 + baryZ * v2;
    return originalPos / textureSize;
}

// 片段着色器:实现自由变形纹理采样
fragment half4 fragmentShader(VertexOut in [[stage_in]],
                              texture2d<float> texture [[texture(0)]],
                              constant CornerData& corners [[buffer(0)]],
                              constant float2& renderSize [[buffer(1)]]) {
    // 获取当前像素在渲染区域的位置
    float2 pixelPos = (in.position.xy + 1.0) * 0.5 * renderSize;
    
    // 计算对应原始纹理坐标
    float2 textureSize = float2(texture.get_width(), texture.get_height());
    float2 texCoord = getOriginalTexCoord(pixelPos, corners, textureSize);
    
    // 双线性采样提升画质
    constexpr sampler s(address::clamp_to_edge, filter::linear);
    float4 color = texture.sample(s, texCoord);
    
    return half4(color);
}

关键优化点

  1. 性能保障:Metal命令缓冲区异步提交,避免主线程阻塞;使用双线性过滤减少变形后的锯齿
  2. 坐标适配:统一SwiftUI与Metal的坐标系差异,确保拖拽位置与渲染结果一致
  3. 扩展性:可快速添加边缘拖拽逻辑,只需同步更新对应两个边角的坐标

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 14:48:11