My app requires to render a SwiftUI View
with an opacity defined by a field function.
jrturton developed a Canvas version, and a much faster Metal version (thanks again!).
I expected that both versions yield the same result, but this is not the case and I do not understand why, because both versions use float 32
, as far as I can see.
Here is my result for both versions:
And here is my code:
struct ContentView: View {
let boardSize = CGSize(width: 100, height: 100)
var body: some View {
HStack {
VStack {
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 path = Path(CGRect(x: x, y: y, width: 1, height: 1))
let strength = BorderField.borderField(size: boardSize, at: CGPoint(x: x, y: y))
if let shading = shadings[strength] {
context.fill(path, with: shading)
} else {
let shading = GraphicsContext.Shading.color(.red.opacity(strength))
shadings[strength] = shading
context.fill(path, with: shading)
}
}
}
}
.frame(width: boardSize.width, height: boardSize.height)
Text("Canvas")
}
VStack {
Rectangle()
.frame(width: boardSize.width, height: boardSize.height)
.colorEffect(ShaderLibrary.borderField(.float2(boardSize.width, boardSize.height), .color(.red)))
Text("Metal")
}
}
}
class BorderField {
static func borderField(size: CGSize, at point: CGPoint) -> Double {
let a = size.width
let b = size.height
let x = point.x
let y = point.y
// 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 strength = 1.0/sqrt(r)
return strength
}
}
#include <metal_stdlib>
using namespace metal;
[[ stitchable ]] half4 borderField(float2 position, half4 currentColor, float2 size, half4 newColor) {
// 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);
float str = strength < 0.2 ? 0.0 : strength; // If not set to exactly 0, zstacked views are covered
return half4(newColor.rgb, str);
}
I don't have any experience in Metal rendering, and would be happy for an explanation.
EDIT:
As Hamid Yusifli pointed out in his answer, my code is a slightly modified version of the originally suggested code. I used the additional line
float str = strength < 0.2 ? 0.0 : strength;
because my app requires to overlay several fields using a ZStack
, and these overlayed Views behave differently for Canvas and Metal rendering.
One can see this even without a ZStack
:
Here, the Metal view was created by the original code.
So the question remains: Why does Metal render differently?
I think the difference comes from the processing of the Color
that is passed to the function. Things like Color.blue
are
A representation of a color that adapts to a given context
And it seems like the precise RGB values being used in the canvas and the metal shader are different. We don't know what "context" the color is adapting to in the shader. If you force a uniform alpha of 0.5 for the canvas and the metal shader, and set Color.blue
, you get this:
(Canvas on left, Metal on the right).
However, if you instead use a non-contextual input colour with defined RGB values, such as Color(red: 0, green: 0, blue: 1)
, you get this with a fixed 0.5 alpha:
And returning to the border field function:
To get consistent results, you have to be explicit about the RGB values of the input colours.