如何在SwiftUI中绘制类似示例图的Mesh Gradient?
在SwiftUI中实现Mesh Gradient(网格渐变)
SwiftUI自带的LinearGradient、RadialGradient、AngularGradient都是规则渐变,无法直接生成Mesh Gradient这种不规则、色彩交错过渡的效果。下面提供几种自定义实现方案,你可以根据需求选择:
方案1:Canvas + Perlin噪声(快速实现)
通过Perlin噪声生成随机色彩分布,绘制小网格后添加模糊过渡,模拟自然的Mesh Gradient效果:
import SwiftUI struct MeshGradientView: View { let colors: [Color] = [.systemRed, .systemBlue, .systemGray, .systemPurple, .systemYellow] var body: some View { Canvas { context, size in let noiseGenerator = PerlinNoiseGenerator(seed: 42) // 绘制网格色块 for x in stride(from: 0, to: size.width, by: 8) { for y in stride(from: 0, to: size.height, by: 8) { let noiseValue = noiseGenerator.noise(x: x / size.width * 5, y: y / size.height * 5) let normalizedValue = (noiseValue + 1) / 2 // 归一化到0-1区间 let colorIndex = Int(normalizedValue * Double(colors.count - 1)) let color = colors[colorIndex] context.fill( CGRect(x: x, y: y, width: 8, height: 8), with: .color(color) ) } } // 模糊过渡让色彩更自然 context.filter = .blur(radius: 4) } .frame(width: 300, height: 300) } } // 简易Perlin噪声生成器 class PerlinNoiseGenerator { private let permutation: [Int] init(seed: Int) { var base = Array(0..<256) var rng = RandomNumberGenerator(seed: seed) base.shuffle(using: &rng) permutation = base + base } func noise(x: Double, y: Double) -> Double { let xi = Int(x) & 255 let yi = Int(y) & 255 let xf = x - Double(Int(x)) let yf = y - Double(Int(y)) let u = fade(t: xf) let v = fade(t: yf) let aa = permutation[permutation[xi] + yi] let ab = permutation[permutation[xi] + yi + 1] let ba = permutation[permutation[xi + 1] + yi] let bb = permutation[permutation[xi + 1] + yi + 1] let x1 = lerp(t: u, a: grad(hash: aa, x: xf, y: yf), b: grad(hash: ba, x: xf - 1, y: yf)) let x2 = lerp(t: u, a: grad(hash: ab, x: xf, y: yf - 1), b: grad(hash: bb, x: xf - 1, y: yf - 1)) return lerp(t: v, a: x1, b: x2) } private func fade(t: Double) -> Double { t * t * t * (t * (t * 6 - 15) + 10) } private func lerp(t: Double, a: Double, b: Double) -> Double { a + t * (b - a) } private func grad(hash: Int, x: Double, y: Double) -> Double { let h = hash & 15 let u = h < 8 ? x : y let v = h < 4 ? y : (h == 12 || h == 14 ? x : 0) return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v) } } // 自定义随机数生成器 struct RandomNumberGenerator: RandomNumberGenerator { var seed: UInt64 mutating func next() -> UInt64 { seed = seed &* 0x5DEECE66D &+ 0xB return seed >> 16 } } // 使用示例 struct ContentView: View { var body: some View { MeshGradientView() } }
方案2:Metal Shader(高性能实现)
如果需要更大尺寸或更流畅的效果,用Metal Shader在GPU上计算色彩分布,性能更优:
- 创建Metal Shader文件(命名为
MeshGradientShader.metal):
#include <metal_stdlib> using namespace metal; struct VertexIn { float4 position [[attribute(0)]]; }; struct VertexOut { float4 position [[position]]; float2 uv; }; vertex VertexOut vertexShader(VertexIn in [[stage_in]], constant float4x4& projectionMatrix [[buffer(0)]]) { VertexOut out; out.position = projectionMatrix * in.position; out.uv = in.position.xy * 0.5 + 0.5; return out; } float noise(float2 p) { float2 i = floor(p); float2 f = fract(p); float2 u = f * f * (3.0 - 2.0 * f); float a = dot(i, float2(127.1, 311.7)); float b = dot(i + float2(1.0, 0.0), float2(127.1, 311.7)); float c = dot(i + float2(0.0, 1.0), float2(127.1, 311.7)); float d = dot(i + float2(1.0, 1.0), float2(127.1, 311.7)); return lerp(lerp(fract(sin(a) * 43758.5453), fract(sin(b) * 43758.5453), u.x), lerp(fract(sin(c) * 43758.5453), fract(sin(d) * 43758.5453), u.x), u.y); } fragment half4 fragmentShader(VertexOut in [[stage_in]], constant half4* colors [[buffer(1)]], constant int& colorCount [[buffer(2)]]) { float noiseValue = noise(in.uv * 5.0); float normalized = (noiseValue + 1.0) * 0.5; int index = int(normalized * float(colorCount - 1)); return colors[index]; }
- SwiftUI中调用Shader:
import SwiftUI import MetalKit struct MeshGradientShaderView: View { let colors: [Color] = [.systemRed, .systemBlue, .systemGray, .systemPurple, .systemYellow] var body: some View { MTKViewRepresentable(colors: colors) .frame(width: 300, height: 300) } } struct MTKViewRepresentable: UIViewRepresentable { let colors: [Color] func makeUIView(context: Context) -> MTKView { let view = MTKView() view.device = MTLCreateSystemDefaultDevice() view.delegate = context.coordinator view.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) return view } func updateUIView(_ uiView: MTKView, context: Context) { context.coordinator.updateColors(colors) uiView.draw() } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, MTKViewDelegate { var parent: MTKViewRepresentable var pipelineState: MTLRenderPipelineState? var colorsBuffer: MTLBuffer? var colorCountBuffer: MTLBuffer? init(_ parent: MTKViewRepresentable) { self.parent = parent super.init() setupPipeline() updateColors(parent.colors) } private func setupPipeline() { guard let device = MTLCreateSystemDefaultDevice() else { return } 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 { pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) } catch { print("Pipeline state error: \(error)") } } func updateColors(_ colors: [Color]) { guard let device = MTLCreateSystemDefaultDevice() else { return } let metalColors = colors.map { color in let uiColor = UIColor(color) var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) return half4(half(red), half(green), half(blue), half(alpha)) } colorsBuffer = device.makeBuffer(bytes: metalColors, length: metalColors.count * MemoryLayout<half4>.stride, options: []) colorCountBuffer = device.makeBuffer(bytes: [metalColors.count], length: MemoryLayout<Int>.stride, options: []) } func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} func draw(in view: MTKView) { guard let device = view.device, let pipelineState = pipelineState, let drawable = view.currentDrawable, let renderPassDescriptor = view.currentRenderPassDescriptor else { return } let commandQueue = device.makeCommandQueue() let commandBuffer = commandQueue?.makeCommandBuffer() let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) renderEncoder?.setRenderPipelineState(pipelineState) // 全屏顶点数据 let vertices: [Float] = [ -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 0.0 ] let vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float>.stride, options: []) renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0) // 正交投影矩阵 let projectionMatrix = matrix_float4x4.identity let matrixBuffer = device.makeBuffer(bytes: &projectionMatrix, length: MemoryLayout<matrix_float4x4>.stride, options: []) renderEncoder?.setVertexBuffer(matrixBuffer, offset: 0, index: 1) // 传递颜色数据 renderEncoder?.setFragmentBuffer(colorsBuffer, offset: 0, index: 1) renderEncoder?.setFragmentBuffer(colorCountBuffer, offset: 0, index: 2) renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) renderEncoder?.endEncoding() commandBuffer?.present(drawable) commandBuffer?.commit() } } } // 使用示例 struct ContentView: View { var body: some View { MeshGradientShaderView() } }
方案3:控制点加权混合(自定义色彩分布)
如果需要指定多个控制点来定义渐变范围,可通过计算每个网格点到控制点的距离,加权混合颜色:
struct ControlPointMeshGradient: View { // 自定义控制点:位置+颜色 let controlPoints: [(position: CGPoint, color: Color)] = [ (CGPoint(x: 50, y: 50), .systemRed), (CGPoint(x: 250, y: 50), .systemBlue), (CGPoint(x: 50, y: 250), .systemYellow), (CGPoint(x: 250, y: 250), .systemPurple), (CGPoint(x: 150, y: 150), .systemGray) ] var body: some View { Canvas { context, size in // 绘制网格色块 for x in stride(from: 0, to: size.width, by: 4) { for y in stride(from: 0, to: size.height, by: 4) { let point = CGPoint(x: x, y: y) var totalWeight: CGFloat = 0 var mixedColor = Color.clear // 加权混合所有控制点颜色 for control in controlPoints { let distance = hypot(point.x - control.position.x, point.y - control.position.y) let weight = max(0, 1 / (distance * distance + 1)) // 距离越近权重越高 totalWeight += weight mixedColor = mixedColor.opacity(weight) + control.color.opacity(weight) } if totalWeight > 0 { mixedColor = mixedColor.opacity(1 / totalWeight) } context.fill( CGRect(x: x, y: y, width: 4, height: 4), with: .color(mixedColor) ) } } // 模糊过渡 context.filter = .blur(radius: 2) } .frame(width: 300, height: 300) } }
内容的提问来源于stack exchange,提问作者abs8090
相关产品推荐
相关产品推荐

