提问人:Reinhard Männer 提问时间:11/12/2023 更新时间:11/22/2023 访问量:77
如何在 SwiftUI 中渲染场强
How to render a field strength in SwiftUI
问:
我的应用程序需要呈现场强度,即矩形视图中函数 f(x,y) 的值。
我知道我可以渲染线性或径向渐变,见这里,但我没有找到一种方法来根据函数 f(x,y) 设置视图像素的亮度或不透明度。
我可能会组装很多子视图的视图,其中每个子视图的亮度或不透明度都是根据这样的函数设置的,但这很丑陋,应该有更好的方法。
有什么建议吗?
答:
这是我的解决方案,基于上面 lorem ipsum 的建议。
此建议需要计算 an 并将其覆盖在 .Image
View
因此,我写了以下扩展: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
评论
Canvas
Canvas
这是一个类似的(但要短得多!)的实现,使用 .我没有看过性能,但我对画布的体验是它非常好,特别是如果你像我一样缓存解析的着色值。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)
}
}
给:
评论
这是使用 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)))
请注意,我们在这里传入的是大小和基色。
这给出了:
我很想知道这些实现之间遇到的性能差异。
——
来自 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
评论