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

如何在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上计算色彩分布,性能更优:

  1. 创建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];
}
  1. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 11:20:28