This creates the arc “object”: a mesh with a shader attached. The arc
function calls this with different parameters passed to the shader. The Arc
class uses this function to create a new mesh for every instance.
So using the arc
function means one mesh, but every invocation requires fresh data passed to the GPU. Using the Arc
class means many meshes, but each is hard-coded with its data (though it can be changed).
local __makeArc = function(nsteps)
-- nsteps doesn't make a huge difference in the range 50,300
nsteps = nsteps or 50
local m = mesh()
m.shader = shader([[
Now we examine the shader code, starting with the vertex shader.
//
// A basic vertex shader
//
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
The uniforms scolour
and ecolour
hold the start and end colours of the arc. From these, we compute the difference “colour” (it may not be a valid colour, but that’s not important).
uniform lowp vec4 scolour;
uniform lowp vec4 ecolour;
lowp vec4 mcolour = ecolour - scolour;
Standard mesh attributes and varyings.
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying highp vec2 vTexCoord;
varying lowp vec4 vColour;
varying highp float vWidth;
The varying vCore
holds the parameter along the curve clamped to [0,1]
which will be used in the fragment shader to deal with the ends. The body of the curve corresponds to y
positions in [0,1]
and the caps extend beyond, so if y == vCore
then we’re in the body of the curve and if y ~= vCore
then we’re in one of the caps.
varying highp float vCore;
These constants control the appearance of the curve. width
is hopefully obvious, taper
is how much the width varies, blur
is how much the edges are blurred. So the actual starting width, swidth
, is width + blur
and the actual ending width, ewidth
, is taper * width - width
(hmm, now that I look at it maybe there should be a + blur
here!). We can set a global cap
type for both ends and scap
and ecap
are used to turn it on or off for start and finish separately. The variables ecapsw
and scapsw
contain how far the caps extend from the curve.
uniform float width;
uniform float taper;
uniform float blur;
uniform float cap;
uniform float scap;
uniform float ecap;
float swidth = width + blur;
float ewidth = taper*width - width;
float ecapsw = clamp(cap,0.,1.)*ecap;
float scapsw = clamp(cap,0.,1.)*scap;
This set of parameters control the actual arc parameters. The angles are in radians. The axes are vectors as they needn’t be the x-axis and y-axis.
uniform vec2 centre;
uniform vec2 xaxis;
uniform vec2 yaxis;
uniform float startAngle;
uniform float deltaAngle;
Now we enter the main function. The idea of the shader is that the mesh is a series of rectangles running from (-.5,0)
to (.5,1)
(well, it extends slightly above and below for the line caps). The y
-coordinate is used to parametrise the arc and the x
-coordinate goes across the curve. So the vertex shader needs to move these vertices into the right places and scale them accordingly.
void main()
{
As said above, the y
value is in the interval [0,1]
except for the caps which extend slightly out, so t
is the actual parameter along the curve.
highp float t = clamp(position.y,0.,1.);
vCore = t;
To compute the width, we need to interpolate between the starting and ending widths. Rather than a linear interpolation, we use smoothstep
so that the width doesn’t change much at the start and end of the arc. This means that if we put two arcs next to each other with matching widths then there won’t be a noticeable change in how much the widths change.
highp float w = smoothstep(0.,1.,t);
vWidth = w*ewidth + swidth;
This contains the position along the curve.
highp vec2 bpos = centre + cos(t*deltaAngle + startAngle) * xaxis + sin(t*deltaAngle + startAngle) * yaxis;
To work out the thickness, we need to know the normal vector at this point on the curve. We start with the tangent vector.
highp vec2 bdir = -sin(t*deltaAngle + startAngle) * xaxis + cos(t*deltaAngle + startAngle) * yaxis;
Rotate it to get the normal vector.
bdir = vec2(bdir.y,-bdir.x);
Normalise, and multiply by the width and the x
-coordinate of the vertex.
bdir = vWidth*normalize(bdir);
bpos = bpos + position.x*bdir;
Convert this to a vec4
since that’s what the shader needs to produce at the end.
highp vec4 bzpos = vec4(bpos.x,bpos.y,0.,1.);
The caps need special handling. This ensures that they poke out a bit from the ends of the arcs in the right directions.
bzpos.xy += (ecapsw*max(position.y-1.,0.)
+scapsw*min(position.y,0.))*vec2(-bdir.y,bdir.x);
highp float s = clamp(position.y,
scapsw*position.y,1.+ecapsw*(position.y-1.));
vTexCoord = vec2(texCoord.x,s);
This blends the colours.
vColour = t*mcolour + scolour;
//Multiply the vertex position by our combined transform
gl_Position = modelViewProjection * bzpos;
}
]],[[
//
// A basic fragment shader
//
Now the fragment shader, we need to know the blur
and cap
type.
uniform highp float blur;
uniform highp float cap;
All the varyings we want from the vertex fragment.
varying highp vec2 vTexCoord;
varying highp float vWidth;
varying lowp vec4 vColour;
varying highp float vCore;
void main()
{
Start with the main colour (the blending from start to finish is already taken care of).
lowp vec4 col = vColour;
This tells us where the edge should be.
highp float edge = blur/(vWidth+blur);
The alpha is modified to take into account the edge and the cap type.
col.a = mix( 0., col.a,
(2.-cap)*smoothstep( 0., edge,
min(vTexCoord.x,1. - vTexCoord.x) )
* smoothstep( 0., edge,
min(1.5-vTexCoord.y, .5+vTexCoord.y) )
+ (cap - 1.)*smoothstep( 0., edge,
.5-length(vTexCoord - vec2(.5,vCore)))
);
gl_FragColor = col;
}
]])
Back to the lua code, as said in the shader the arc is a mesh filling in the rectangle from (-.5,0)
to (.5,1)
. We use lots of rectangles to make the arc smooth.
for n=1,nsteps do
m:addRect(0,(n-.5)/nsteps,1,1/nsteps)
end
m:addRect(0,1.25,1,.5)
m:addRect(0,-.25,1,.5)
return m
end
There’s a lot of flexibility in there which could be removed to speed it up a bit. One thing it doesn’t do is fill in the arc: it is definitely an arc and not a segment of a circle.