如何在 SwiftUI 中渲染场强

How to render a field strength in SwiftUI

提问人:Reinhard Männer 提问时间:11/12/2023 更新时间:11/22/2023 访问量:77

问:

我的应用程序需要呈现场强度,即矩形视图中函数 f(x,y) 的值。
我知道我可以渲染线性或径向渐变,见这里,但我没有找到一种方法来根据函数 f(x,y) 设置视图像素的亮度或不透明度。
我可能会组装很多子视图的视图,其中每个子视图的亮度或不透明度都是根据这样的函数设置的,但这很丑陋,应该有更好的方法。
有什么建议吗?

SwiftUI 渐变 不透明度

评论

0赞 lorem ipsum 11/12/2023
使用 UIImage,您可以在那里更改像素

答:

1赞 Reinhard Männer 11/13/2023 #1

这是我的解决方案,基于上面 lorem ipsum 的建议。
此建议需要计算 an 并将其覆盖在 .
ImageView

因此,我写了以下扩展:Image

extension Image {
    
    /// Creates an Image of a given size with a given color. 
    /// The color, without the alpha value, is here hard coded, but could of course be given by a parameter.
    /// The alpha value represents the field function, depending on the (x,y) pixel value.
    /// - Parameters:
    ///   - size: The size of the resulting image
    ///   - alpha: The field function
    init?(size: CGSize, alpha: (_ x: Int, _ y: Int) -> Double) {
        // Set color components as 8 bits in hex. Alpha will be taken from the function parameter.
        // Pixel values are 32 bits: ARGB
        let r: UInt32 = 0x00 << 16
        let g: UInt32 = 0x00 << 8
        let b: UInt32 = 0xFF 
        
        let pixelWidth  = Int(size.width)
        let pixelHeight = Int(size.height)
        var srgbArray: [UInt32] = [] // The pixel array

        // Compute field values
        for y in 0 ..< pixelHeight {
            for x in 0 ..< pixelWidth {
                let fieldStrength = alpha(x, y)
                if fieldStrength < 0 || fieldStrength > 1.0 {
                    return nil // The alpha function did return an illegal value (< 0 or > 1).
                }
                let a = UInt32(fieldStrength * 255.0) << 24 // alpha
                srgbArray.append(a | r | g | b)
            }
        }
        
        // https://forums.swift.org/t/creating-a-cgimage-from-color-array/18634
        let cgImg = srgbArray.withUnsafeMutableBytes { ptr -> CGImage in
            let ctx = CGContext(
                data: ptr.baseAddress,
                width: pixelWidth,
                height: pixelHeight,
                bitsPerComponent: 8,
                bytesPerRow: 4 * pixelWidth,
                space: CGColorSpace(name: CGColorSpace.sRGB)!,
                bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue
            )!
            return ctx.makeImage()!
        }
        
        self.init(
            cgImg,
            scale: 1.0,
            orientation: .up,
            label: Text("Field")
        )
    }
}

初始化为Image

let image = Image(size: fieldSize, alpha: alpha(x:y:))  

其中 function 可以是任何函数。在这里,出于演示目的,我使用以下函数:alpha

func alpha(x: Int, y: Int) -> Double {
    Field.borderfield(size: fieldSize, x: x, y: y)
}

struct Field {
    
    static func borderfield(size: CGSize, x: Int, y: Int) -> Double {
        let a = Int(size.width)
        let b = Int(size.height)
        assert(x >= 0 && x <= a)
        assert(y >= 0 && y <= b)
        
        // Compute distance to border
        let dt = b - y  // Distance to top
        let db = y      // Distance to bottom
        let dl = x      // Distance to left
        let dr = a - x  // Distance to right
        let minDistance = min(dt, db, dl, dr)
        
        let d = Double(minDistance)
        let r = d + 1.0 // min r is now 1
        let signalStrength = 1.0/sqrt(r)
        print(signalStrength)
        return signalStrength
    }
    
}  

函数是显示矩形的带电边框将产生的场的非常粗略的近似值。
这是图像:
borderfield
enter image description here

评论

0赞 jrturton 11/14/2023
我倾向于研究这样做,以避免很多丑陋的投射和指针的东西Canvas
0赞 Reinhard Männer 11/14/2023
@jrturton我没有计算机图形学方面的经验,我通过从网络上收集建议来组装我的解决方案。也许你可以提出一个想法,如何做到这一点?我很乐意有一个更像 SwiftUI 的解决方案。Canvas
1赞 jrturton 11/14/2023 #2

这是一个类似的(但要短得多!)的实现,使用 .我没有看过性能,但我对画布的体验是它非常好,特别是如果你像我一样缓存解析的着色值。Canvas

struct FieldStrengthView: View {

    let color: Color
    let alpha: (_ x: Int, _ y: Int) -> Double

    var body: some View {
        Canvas { context, size in
            let pixelWidth  = Int(size.width)
            let pixelHeight = Int(size.height)
            var shadings = [Double: GraphicsContext.Shading]()
            for y in 0 ..< pixelHeight {
                for x in 0 ..< pixelWidth {
                    let strength = alpha(x, y)
                    let path = Path(CGRect(x: x, y: y, width: 1, height: 1))
                    if let shading = shadings[strength] {
                        context.fill(path, with: shading)
                    } else {
                        let shading = GraphicsContext.Shading.color(color.opacity(strength))
                        shadings[strength] = shading
                        context.fill(path, with: shading)
                    }
                }
            }
        }
    }
}

它使用现有函数的场强一次填充一个像素:

#Preview {
    VStack {
        FieldStrengthView(color: .blue, alpha: { x, y in
            Field.borderfield(size: CGSize(width: 300, height: 300), x: x, y: y)
        })
        .frame(width: 300, height: 300)
        FieldStrengthView(color: .green, alpha: { x, y in
            Field.borderfield(size: CGSize(width: 200, height: 200), x: x, y: y)
        })
        .frame(width: 200, height: 200)
    }
}

给:

enter image description here

评论

0赞 Reinhard Männer 11/14/2023
很棒的解决方案!
0赞 jrturton 11/14/2023
可能总体上最好的方法是使用金属着色器,就像这里描述的那样,但我没有知识来写这个答案:)
0赞 Reinhard Männer 11/18/2023
我只是安排了你和我的解决方案的时间。对于 900 x 357 像素的矩形大小,您的解决方案仅比我的慢 1%。在 M1 MacBook 上,两者都需要大约 0.15 秒。
0赞 jrturton 11/20/2023
我敢打赌,金属着色器的速度会快一个数量级。也许我会想办法写一个......
1赞 jrturton 11/20/2023 #3

这是使用 Metal 着色器的替代答案。您需要为场强算法的每个实现创建一个着色器,这是我对边界场的尝试:

#include <metal_stdlib>
using namespace metal;

[[ stitchable ]] half4 borderField(float2 position, half4 currentColor, float2 size, half4 newColor) {
    assert(size.x >= 0 && position.x <= size.x);
    assert(size.y >= 0 && position.y <= size.y);

    // Compute distance to border
    float dt = size.y - position.y;  // Distance to top
    float db = position.y;     // Distance to bottom
    float dl = position.x;      // Distance to left
    float dr = size.x - position.x;  // Distance to right
    float minDistance = min(min(dt, db), min(dl, dr));
    float r = minDistance + 1.0;
    float strength = 1.0 / sqrt(r);
    return half4(newColor.rgb, strength);

}

将其添加到新文件中的项目。.metal

默认情况下,前两个参数是传递的,即当前像素的位置及其当前颜色。接下来的由你决定。

若要在 SwiftUI 中使用,请使用修饰符:.colorEffect

Rectangle()
    .frame(width: 300, height: 300)
    .colorEffect(ShaderLibrary.borderField(.float2(300, 300), .color(.blue)))

请注意,我们在这里传入的是大小和基色。

这给出了:

enter image description here

我很想知道这些实现之间遇到的性能差异。

——

来自 Reinhard Männer 的更新,看起来 Metal 真的快了很多!

您的第一个解决方案已经很棒了,但这个解决方案绝对很棒
我在我的应用程序中实现了它。
我不确定如何正确计时,但我做了以下工作:

var body: some View {
    let start = Date.now
    Rectangle()
        .frame(width: boardSize.width, height: boardSize.height)
        .colorEffect(ShaderLibrary.borderField(.float2(boardSize.width, boardSize.height), .color(.green)))
    let _ = print(-start.timeIntervalSinceNow)
}

其中的设置与我之前的时间相同 (900,357)。
早些时候,渲染大约需要 0.15 秒。使用您的金属解决方案和我上面的时间,结果是
3.0040740966796875e-05
如果我的时机正确,这将是令人难以置信的 5.000 加速。这激励着我,也去研究金属。多谢!
boardSize