Another way of representing vector fields is to draw individual vectors showing the magnitude and direction of the field rather than drawing field lines. We follow common practice in that each vector represents the value of the field at the base of the vector. A little thought will show that these vectors are tangent to the field lines.
To draw these vectors we extend what we learned in drawing the field lines and directional arrows. Each vector will be generated in the vertex shader from its base and tip, along with parallel and perpendicular displacements.
Interestingly, the geometry for the vector is a combination of what we used for the field line and the directional arrows.
The JavaScript to setup these vertices is a bit more complicated than previous examples, but it remains highly performant.
We start with the base of the vector (x0, y0, z0
).
base.x = x0;
base.y = y0;
base.z = z0;
The tip is then the base translated in the direction of the field. This generates a vector that reflects the magnitude and direction of the field.
tip.x = x0 + field[0]*arrowSize;
tip.y = y0 + field[1]*arrowSize;
tip.z = z0 + field[2]*arrowSize;
We push
the data for each vertex onto the indexedVertices
array.
// Vertex 0
this.pushVertex(indexedVertices, base, tip, lineWidth, 0.0);
This sample sets up the data for vertex 0, the first vertex that in the line. It has a displacement
of lineWidth
perpendicular to the base
to tip
line
(), and zero displacement along that line
().
// Vertex 4 The first barb
this.pushVertex(indexedVertices, tip, base, arrowHeadWidth, arrowHeadSize*1.33);
Vertex 4 is the first vertex in the arrow head. It has a
of arrowHeadWidth
, and a of
1.33*arrowHeadSize
.
The vertex shader is a bit simpler. It follows a slightly simpler path than before:
We can visualize how the data for each vertex is organized.
This time the attributes include normal and parallel displacements.
attribute vec3 current;
attribute vec3 other;
attribute float normalDisplacement;
attribute float parallelDisplacement;
As well as the x and y resolutions.
uniform vec2 resolution;
Transforming the current and other points to screen space, then finding the connecting line follows a now familiar pattern. We multiple by the model view projection, to transform into clip space. Then perform the perspective division to get screen space points.
mat4 projModelView = projectionMatrix * modelViewMatrix;
vec4 currentProjected = projModelView * vec4(current, 1.0);
vec4 otherProjected = projModelView * vec4(other, 1.0);
vec2 currentScreen = currentProjected.xy / currentProjected.w;
vec2 otherScreen = otherProjected.xy / otherProjected.w;
We will need unit (normalized) vectors along the line connecting our points and the normal to that line. We tuned the normalization a bit to our new method where we directly specify the arrow head size through the displacements.
vec2 dir = otherScreen - currentScreen;
dir = (dir == vec2(0.0, 0.0) ? dir : normalize(dir));
vec2 normal = vec2(-dir.y, dir.x);
Now that we have these unit vectors, multiply them by the displacements and add the results to the current point to get the actual vertex.
// Adjustments perpendicular to the arrow
vec4 offset = vec4(normal*normalDisplacement, 0.0, 0.0);
// Adjustments along the arrow
offset += vec4(dir * parallelDisplacement, 0.0, 0.0);
offset.xy /= resolution * currentProjected.w;
gl_Position = currentProjected + offset;
We have continued the practice of drawing discrete objects by generating vertices with some data and a little simple computation. This is quite common, indeed, it is now built into OpenGL in the form of Geometry Shaders. However, Geometry Shaders are not available through the current version of WebGL, so we do this work directly.