请求指导:基于SwiftUI与Metal Shader实现图片自由拉伸变形
实现SwiftUI+Metal的图片自由拉伸变形功能
核心思路
放弃SwiftUI内置的resizable()等布局约束,通过以下三步实现需求:
- 用SwiftUI手势捕获拖拽位置,维护图片四个边角的坐标状态
- 将坐标状态传递给封装在
UIViewRepresentable中的MTKView - 在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); }
关键优化点
- 性能保障:Metal命令缓冲区异步提交,避免主线程阻塞;使用双线性过滤减少变形后的锯齿
- 坐标适配:统一SwiftUI与Metal的坐标系差异,确保拖拽位置与渲染结果一致
- 扩展性:可快速添加边缘拖拽逻辑,只需同步更新对应两个边角的坐标
内容的提问来源于stack exchange,提问作者HeWhoRemains
相关产品推荐
相关产品推荐

