Linear Algebra for GLSL Shaders
04 / Meet the vectors

Keep the direction.
Change the length.

Make an arrow point at a target without growing as the target moves away. Choose its direction and its length separately.

20–30 min with experimentsGLSL ES 3.00 / WebGL 2After chapter 03
What we want to make

Aim without stretching.

Move the target ↓

The target gives us a direction. A separate length controls how far the arrow extends. Moving the target farther along the same line should leave the arrow unchanged.

From origin (0,0), a target at (0.3,0.4) defines direction (0.6,0.8). An arrow of length 0.6 ends at (0.36,0.48), beyond the target.
Displacement → unit direction → chosen length(0.3, 0.4) → (0.6, 0.8) → (0.36, 0.48)

A pointer, a small light indicator, or an aiming guide can point toward a target without reaching it. We want the arrow to turn as the target moves, while keeping its own size.

We already have the two ingredients. Chapter 02 gave us the displacement between points. Chapter 03 gave us its length. Now we will use that length to remove the displacement’s size.

Reach the target or aim at it

Start with an origin and a target in the same equal-unit coordinate space:

vec2 origin = vec2(0.0);
vec2 target = vec2(0.3, 0.4);
vec2 v = target - origin;
vec2 tip = origin + v; // Exactly the target.

This arrow reaches the target. Its length is 0.5. Move the target to (0.6, 0.8) and the displacement doubles. The arrow grows to length 1.0.

Multiplying by a constant does not solve the problem. v * 0.6 still doubles when v doubles. We need to remove the original length before applying the one we want.

Make the length one

Divide both components by the same length:

float len = length(v); // 0.5
vec2 direction = v / len;
// (0.3 / 0.5, 0.4 / 0.5) = (0.6, 0.8)

Division by a positive number scales the whole vector without turning it. The components keep the same proportion and signs. Because we divided by its own length, the resulting vector has length one:

// sqrt(0.6 * 0.6 + 0.8 * 0.8)
// = sqrt(0.36 + 0.64) = 1.0

This is a unit vector. Making a nonzero vector’s length one is called normalization. In exact arithmetic, length(v / length(v)) is one. GPU calculations have finite precision, so measured results can differ slightly from one.

Notice that neither component must equal one. The pair together has length one. A diagonal direction such as (0.6, 0.8) is just as valid as (1, 0) or (0, -1).

For a nonzero vector, GLSL provides the shorter form:

vec2 direction = normalize(v);

We will use explicit division in the demo so we can inspect the length and check it before dividing. Both forms express the same operation for an ordinary nonzero input.

Choose a new length

Multiply the unit direction by the desired arrow length, then add the origin:

float arrowLength = 0.6;
vec2 displacement = direction * arrowLength; // (0.36, 0.48)
vec2 tip = origin + displacement;

arrowLength uses our coordinate units. Its value no longer depends on the target distance. With the origin at zero, the tip is (0.36, 0.48). Its distance from the origin is 0.6.

The target is only 0.5 units away, so this arrow ends beyond it. That is expected: we are aiming at the target, not trying to stop there.

Origin (0, 0), arrow length 0.6
TargetTarget distanceUnit directionArrow tip
(0.15, 0.20)0.25(0.6, 0.8)(0.36, 0.48)
(0.30, 0.40)0.50(0.6, 0.8)(0.36, 0.48)
(0.60, 0.80)1.00(0.6, 0.8)(0.36, 0.48)

All three targets lie along the same direction from the origin. Their distances differ, but normalization produces the same direction. Move the target around the origin instead, and the arrow turns.

At arrowLength = 0, the tip coincides with the origin. The chosen direction can remain valid even though the displayed arrow has no extent. A negative multiplier would reverse it, as in chapter 02; this playground limits length to zero or greater.

When the points coincide

Now put the target exactly at the origin. The displacement and its length are both zero. Which direction should the arrow use?

There is no direction in that input. Dividing by zero cannot recover one, and normalize(vec2(0.0)) cannot supply a meaningful unit direction. We must choose the behavior ourselves.

vec2 v = target - origin;
float len = length(v);
vec2 direction = vec2(1.0, 0.0); // Our chosen fallback: right.
if (len > 0.0001) {
    direction = v / len;
}
vec2 tip = origin + direction * arrowLength;

The check comes before the division. When the length is too small, we use a known unit vector. This keeps the result finite and gives the image a defined state.

The threshold 0.0001 is in coordinate units. It is a chosen tolerance for this small scene, whose visible vertical range is -1 to 1. It also avoids letting tiny changes very close to the origin determine the direction. It is not a universal constant to copy into every application. Very close to the threshold, floating-point rounding can affect which side a value falls on; the Near zero preset uses a length safely below it.

A fixed fallback may cause a visible turn to the right when the target reaches the origin. That is our policy, not a direction hidden inside the zero vector. Another useful policy is to keep the last valid direction. We will try both below.

Try it

Move the target. Keep the length.

LIVE GLSL / WEBGL 2

Drag the target, use the coordinate controls, or try Closer and Farther. The faint reference arrow reaches the target; the bright arrow uses the chosen length. Watch length(direction) and the tip.

The view uses equal units with no extra zoom: Y runs from -1 to 1. The preset buttons set the origin to zero except Coincide, which places the target at the current origin. Readouts follow the built-in formulas, even when you edit the shader.

Static default example: origin (0,0), target (0.3,0.4). The displacement has length 0.5. Its unit direction is (0.6,0.8), so an arrow of length 0.6 ends at (0.36,0.48).
White: originCyan: target / original displacementAmber: fixed-length arrow / tip

Equal units; no extra zoom. Vertical range: −1 to 1. Grid: 0.5 units. One unit occupies half the canvas height on either axis.

The target chooses a direction. L chooses the amber arrow's length. When the displacement length is at most 0.0001 coordinate units, the selected fallback supplies a direction.

Origin
Target

Closer, Farther, and Near zero set the origin to (0,0) and place the target along (0.6,0.8). They keep L and the fallback choice. Coincide moves the target to the current origin.

Preparing the live shader.

Displacement · v = target − origin
(0.30000, 0.40000)
Displacement length · length(v)
0.50000
Chosen direction
(0.60000, 0.80000)
Direction length · length(direction)
1.00000
Tip · origin + direction × L
(0.36000, 0.48000)
Saved direction · uLastDirection
(0.60000, 0.80000)

The displacement is longer than the threshold. Both components are divided by its length; the direction has length 1.

Readouts evaluate the built-in formulas and selected fallback. They do not execute your shader edits. After changing the fallback in code, select “Last valid direction” to make the reference readouts follow that same rule.

Click or drag in the diagram to move the target. Focus the diagram and use arrow keys for steps of 0.05; hold Shift for 0.25. On a touch screen, tap a location or use the controls; a swipe scrolls the page. Coordinates range from −1 to 1 and retain five decimal places. An arrow may extend outside the view.

Open the complete shader edit · run · reset

The page supplies uOrigin, uTarget, uArrowLength, uLastDirection, uFallbackMode, and uResolution. uLastDirection is updated only when the displacement is longer than 0.0001. It starts at (0.6,0.8). Fallback mode 0 selects +X; mode 1 selects this saved direction. The arrow and grid helpers below are supplied drawing geometry.

⌘ / Ctrl + Enter to run

The complete editor includes supplied arrow, grid, and marker helpers. Their geometry is only for drawing the measurements; the lesson is how direction and tip are calculated. The page renders when something changes.

A complete endpoint shader

This smaller shader places a green dot at the computed tip. Paste it into the editor; origin, target, length, and fallback controls still work. The grid and arrows are omitted so the endpoint is easy to inspect.

#version 300 es
precision highp float;
uniform vec2 uResolution;
uniform vec2 uOrigin;
uniform vec2 uTarget;
uniform float uArrowLength;
uniform vec2 uLastDirection; // A unit vector maintained by the host.
uniform int uFallbackMode;   // 0 = right, 1 = last valid direction.
out vec4 fragColor;

void main() {
    vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
    vec2 v = uTarget - uOrigin;
    float len = length(v);
    vec2 fallback = uFallbackMode == 1 ? uLastDirection : vec2(1.0, 0.0);
    vec2 direction = fallback;
    if (len > 0.0001) {
        direction = v / len;
    }
    vec2 tip = uOrigin + direction * uArrowLength;
    float marker = 1.0 - smoothstep(0.035, 0.045, length(p - tip));
    fragColor = vec4(mix(vec3(0.04, 0.06, 0.09),
                         vec3(0.29, 0.88, 0.56), marker), 1.0);
}

A mistake worth making: normalize after scaling

For a nonzero v and a positive arrowLength, compare:

vec2 a = normalize(v) * arrowLength;
vec2 b = normalize(v * arrowLength);

The first displacement has the chosen length. The second has length one: normalization removes the scale we just applied. In the editor, keep the arrow length above zero and replace the line that computes tip with this exact line:

vec2 tip = uOrigin + normalize(direction * uArrowLength);

The arrow now has length one and stops responding to positive length changes. Restore vec2 tip = uOrigin + direction * uArrowLength; afterward. Normalize first, scale second.

Do not fix a zero denominator by adding the same small number to both components of v. That changes its direction and turns the zero vector into an arbitrary diagonal. Choose and label the fallback instead.

Remember the last valid direction

A fragment shader invocation does not remember the last pointer event. The host application can keep that information and send it through a uniform.

Here is the relevant JavaScript idea. The host calls this function after origin or target changes. It uses the same tolerance as the shader:

let lastDirection = [1, 0]; // Valid even before the first movement.
function rememberDirection(origin, target) {
    const dx = target[0] - origin[0];
    const dy = target[1] - origin[1];
    const len = Math.hypot(dx, dy);
    if (len > 0.0001) {
        lastDirection = [dx / len, dy / len];
    }
    return lastDirection;
}

The host sends the returned pair as uLastDirection. At zero or below the threshold, it leaves the saved pair unchanged. It must never replace that pair with a zero vector. The complete host code implements this behavior; it also rounds the reference calculations to 32-bit floats to match the shader inputs. The short snippet above shows the memory rule. The shader editor edits only the fragment shader.

Selecting the remembered fallback therefore preserves the arrow’s last orientation at coincidence. It still cannot find a direction from the coincident points. It uses information from an earlier valid state.

Your turn

Use Reset lab. Keep the origin at zero and arrow length at 0.6.

  1. Compare Closer and Farther. Predict the two target distances and the common arrow tip. Check that the direction length stays one.
  2. Choose Coincide with the fixed-right fallback. Predict the tip and explain why the arrow turns.
  3. Reset. In the complete shader, replace the assignment to fallback with vec2 fallback = uLastDirection;. Run it, then choose Coincide. Explain why the diagonal direction is now preserved. The host already supplies the memory.
  4. Use Reset code and choose the remembered fallback in the controls to get the same behavior without editing. Try Farther, then Near zero. Its displacement is (0.00003, 0.00004); explain why the fallback is used even though the points are not exactly identical.
  5. Set arrow length to zero. Explain why the direction readout stays valid while the visible arrow becomes a point.
A hint

Normalize the target displacement before applying 0.6. For the zero case, inspect the selected fallback rather than dividing. For the near-zero case, compare its length with 0.0001.

Check your result

Closer has distance 0.25, Farther has distance 1.0. Both give direction (0.6, 0.8) and tip (0.36, 0.48).

At coincidence, fixed-right chooses (1, 0), so the tip becomes (0.6, 0). After Reset, the last valid direction is the default diagonal. Using uLastDirection keeps the tip at (0.36, 0.48). The formula readouts still follow the selected control policy when custom code changes it.

The Near zero displacement has length 0.00005, below our threshold. After Farther, the remembered direction remains (0.6, 0.8). At zero arrow length, the displacement to the tip is zero even though the stored direction still has length one.

What you can do now

You can separate direction from distance. Move a target farther along the same direction: the arrow keeps its length. Make the points coincide: the chosen fallback keeps every result defined.

We have changed one vector’s size without turning it. Next, we will combine values to place a point between two positions or blend two colors.