Now that we have successfully drawn our field lines, and wrapped our heads around doing robust computing in the vertex shader, we immediately come across another application. We need directional arrows for our field lines.
Not only do we need the field lines themselves, we also need to indicate the direction of the field. We indicate this direction with small arrows along the field line. Like the field lines, we draw the arrows in screen space, so they always appear in silhouette. I guess we can call these fake volumetric arrows.
This section walks through the geometry that we want for our arrows. Just keep in mind that the shader does all this in screen space.
Forming the arrow starts by setting the tip at the end of a line segment. The base of the arrow is then stepped back along the field by the arrow length, l. Once we compute them, we will use these vertices to draw the triangles △012 and △213.
We push the base and the tip onto the field line object, that knows how to construct the full arrow. This time we build three vertices at the base and only one at the tip. The barbs of the arrow are displaced perpendicular to the line connecting the base and the tip, and a bit along the line as well.
// Vertex 0, a barb, displaced up and back
makeArrowVertex(base, tip, +1.0);
// Vertex 1, the base of the arrow, not displaced
makeArrowVertex(base, tip, 0.0);
// Vertex 2, the tip of the arrow, not displaced
makeArrowVertex(tip, tip, 0.0);
// Vertex 3, the other barb, displaced down and back.
makeArrowVertex(base, tip, -1.0);
The first vertex is displaced upward and back from the base of the arrow to form one of the barbs. The next two vertices are the base and the tip, which have no displacement. The final vertex is displaced down and back to form the other barb.
This time we introduce index buffers to avoid duplicating vertices.
// The upper half of the arrow
arrowIndices[arrowIndexPtr++] = baseIndex;
arrowIndices[arrowIndexPtr++] = baseIndex + 1;
arrowIndices[arrowIndexPtr++] = baseIndex + 2;
// Lower half of the arrow
arrowIndices[arrowIndexPtr++] = baseIndex + 2;
arrowIndices[arrowIndexPtr++] = baseIndex + 1;
arrowIndices[arrowIndexPtr++] = baseIndex + 3;
The data for each vertex is packed in a similar format to what we used for lines. We include the immediate point we are working with, another point we use in the calculations, and a control variable, the direction for the displacement.
This layout is reflected in the vertex shader attributes. Just as before, we also pass in the drawing surface aspect ratio and the arrow halfwidth as uniforms.
attribute vec3 current;
attribute vec3 tip;
// Displacement direction for the arrow barbs, zero for tip and base
attribute float direction;
uniform float aspect;
uniform float halfWidth;
The process of mapping the current point and the tip to screen space exactly parallels that we used for the line end points. We use the model view projection matrix to transform both the current point and the tip into clip space
vec4 currentProjected = projModelView * vec4(current, 1.0);
vec4 tipProjected = projModelView * vec4(tip, 1.0);
Once we are in clip space, we transform into screen space through perspective division, and adjusting for the drawing surface aspect ratio.
vec2 currentScreen = currentProjected.xy / currentProjected.w;
currentScreen.x *= aspect;
vec2 tipScreen = tipProjected.xy / tipProjected.w;
tipScreen.x *= aspect;
In screen space, we diverge a little from the line drawing code to harden the shader
against the case where the current point is the tip, which yields a zero dir vector. Attempting to
normalize this vector yields NaN
. Any of the following operations involving a
NaN
would similarly yield NaN
, so we avoid this.
vec2 dir = currentScreen - tipScreen;
vec2 normal = (dir == vec2(0.0, 0.0) ? vec2(0.0, 0.0) : normalize(vec2(-dir.y, dir.x)));
normal *= halfWidth;
normal.x /= aspect;"
We have the line from the current point to the tip of the arrow along with its normal in screen space. For the barbs we compute the vertex by displacing the current point in a positive or negative direction along the normal, and backward by the distance between the base and the tip. For the base and the tip, we give a zero offset so we directly use the current point.
vec4 offset = (direction == 0.0 ?
// No direction => no displacement
vec4(0.0, 0.0, 0.0, 0.0) :
// Displace the arrow barbs away from the line and backwards
vec4((normal * direction / currentProjected.w) + dir/3.0, 0.0, 0.0));
gl_Position = currentProjected + offset;