iosswiftmetalmetalkit

Metal lineStrip: how to draw thicker lines?


I am developing an iOS application using Xcode 14.2, and my deployment target is iOS 16.2. I have a list of X,Y, and Z values that I want to draw with Metal using lineStrip. This works. However, the line that is drawn is too thin for my purposes. I've read about various strategies on how to thicken the line and I've decided to attempt to draw the same line many iterations with a small amount of noise each time to give the appearance of a thicker line.

I generate 3 random floats each time through the render loop and send them to my vertex shader with a uniform. The issue is that the resulting line seems to be more periodic than random and more iterations does not seem to give the appearance of a thicker line.

How can I draw thicker lines using this strategy?

enter image description here enter image description here

Draw many iterations:

// Draw many iterations
for iteration in 1...1024 {
    scene.track?.draw(encoder: commandEncoder,
                      modelMatrix: accumulatedRotationMatrix,
                      projectionMatrix: projectionMatrix * viewMatrix,
                      secondsInEpoch: Float(self.epochTime))
}

Random floats:

var jitter = 1.0 / Float(self.screenSizeX) - 1 / Float(self.screenSizeY)
var jitterX = Float.random(in: -jitter...jitter)
var jitterY = Float.random(in: -jitter...jitter)
var jitterZ = Float.random(in: -jitter...jitter)

Vertex Uniform:

struct VertexUniforms {
    
    var viewProjectionMatrix: float4x4
    var modelMatrix: float4x4
    var normalMatrix: float3x3
    var jitterX: Float
    var jitterY: Float
    var jitterZ: Float
    var iteration: Float
}

Draw primitives call:

encoder.drawPrimitives(type: .lineStrip , vertexStart: 0, vertexCount: vertices.count / 3)

Vertex shader:

// Calculate the jitter for X/Y/Z
//float subFactor = 0.0099;
float subFactor = 0.0105;
float smallFactorX = (subFactor * uniforms.jitterX);
float smallFactorY = (subFactor * uniforms.jitterY);
float smallFactorZ = (subFactor * uniforms.jitterZ);
if (vertexId % 2 == 0) {
    vertexOut.position.x += (vertexOut.position.x * smallFactorX);
    vertexOut.position.y += (vertexOut.position.y * smallFactorY);
    vertexOut.position.z += (vertexOut.position.z * smallFactorZ);
} else {
    vertexOut.position.x -= (vertexOut.position.x * smallFactorX);
    vertexOut.position.y -= (vertexOut.position.y * smallFactorY);
    vertexOut.position.z -= (vertexOut.position.z * smallFactorZ);
}

return vertexOut;

Solution

  • To generate a thicker line you can either:

    You need to keep in mind that Metal lines are one pixel thick. As such, for every extra pixel of line thickness, you would need a new, immediately adjacent line. Instead of relying on randomness, I would simply add/subtract one pixel to the line every iteration, in a way that is perpendicular to the direction of the line. I would also make the number of iterations contingent on the screen size.

    You could set the number of iterations to the screen width or height (depending on whether the line is vertical or horizontal) divided by the desired thickness of the line. You could also pass the pixel size (1 over the screen size) to the vertex shader, where you would offset the line by the pixel size times the iteration number of the line in question.

    Alternatively, you could abandon the idea of drawing a lineStrip and draw a triangleStrip instead. This solution is likely much more efficient than drawing the line a large number of times at slightly different offsets.

    To implement this, you would duplicate every vertex in the line (1 extra line instead of a bazillion), and offset it by the desired thickness in the direction perpendicular to the line itself. And then you would call drawPrimitives() with type: .triangleStrip instead of type: .lineStrip. The new vertices would be interleaved with the old ones instead of comming after them.