My app requires to render a field strength in a SwiftUI View
, i.e. to assign a color with a coordinate dependent opacity to each point of the View
.
This can be done using Canvas, as described here, but this is very slow.
Much faster is to use a Metal shader, as described here.
To render a point field, I am using this Metal function:
[[ stitchable ]] half4 pointField(float2 position, half4 currentColor, float2 center, half4 newColor) {
// Compute distance center - position
float x = position.x;
float y = position.y;
float xDistance = center.x - x;
float yDistance = center.y - y;
float d = sqrt(xDistance * xDistance + yDistance * yDistance);
float r = d + 1.0; // min r is now 1
float strength = 1.0 / sqrt(r);
return half4(newColor.rgb, strength);
}
This function can be called from SwiftUI using e.g.
Rectangle()
.frame(width: boardSize.width, height: boardSize.height)
.colorEffect(ShaderLibrary.pointField(.float2(center.x, center.y), .color(Color(red: 0, green: 1, blue: 0))))
where center
is the center of the point field.
According to Apple's docu, a Metal shader function has to have to following signature:
[[ stitchable ]] half4 name(float2 position, args...)
I want now to display a superposition of a number of point fields. Since the number can be changed at run time, my idea is to use a Metal shader comparable to the code shown above, but in a loop over the point sources given by the variadic arguments.
This blog shows how to call a Metal shader function with variadic arguments, using
let shader = Shader(function: function, arguments: [])
My question is:
How can I determine, how many arguments have been passed from SwiftUI to Metal? This would be required to loop over the code above, and to sum up the field strengths of the various point fields.
I could imagine to pass as the first required argument the number of point field centers that follow. Is this the right way?
Jeshua Lacock's answer motivated me to understand how to pass arguments to Metal shader functions, using MTLBufer
. Unfortunately, I failed, since I am completely new to Metal. But I found a (for me) simpler way:
A SwiftUI Shader has some arguments. Among them is the Type Method floatArray(_:)
.
The docu says:
Returns an argument value defined by the provided array of floating point numbers. When passed to an MSL function it will convert to a device const float *ptr, int count pair of parameters.
I thus can now call the Metal shader from SwiftUI with (test):
let center1 = CGPoint(x: 100, y: 50)
let center2 = CGPoint(x: 200, y: 150)
let pointCenters: [Float] = [Float(center1.x), Float(center1.y), Float(center2.x), Float(center2.y)]
Rectangle()
.frame(width: boardSize.width, height: boardSize.height)
.colorEffect(ShaderLibrary.pointFields(.floatArray(pointCenters)))
and the corresponding Metal function is:
[[ stitchable ]] half4 pointFields(float2 position, half4 currentColor, device const float *centers, int count) {
float totalStrength = 0.0;
for (int i = 0; i < count; i += 2) {
float nextCenterX = centers[i];
float nextCenterY = centers[i+1];
// Compute distance center - position
float x = position.x;
float y = position.y;
float xDistance = nextCenterX - x;
float yDistance = nextCenterY - y;
float d = sqrt(xDistance * xDistance + yDistance * yDistance);
float r = d + 1.0; // min r is now 1
totalStrength += 1.0 / sqrt(r);
}
return half4(0.0, 1.0, 0.0, totalStrength);
}