In chapter 05, one parameter blended two colors. Now each pixel needs its own parameter. Reading p.x gives a horizontal change, and reading p.y gives a vertical one. How do we read a coordinate along a diagonal?
We will combine the two components using a direction. Then turning that direction will turn the gradient, without changing the rest of the shader.
Multiply, then add
Start with two vectors. Multiply the matching components, then add the results:
vec2 d = vec2(0.5, 0.0);
vec2 u = vec2(0.6, 0.8);
float along = d.x * u.x + d.y * u.y;
// 0.5 * 0.6 + 0.0 * 0.8 = 0.3
This operation is the dot product. Its result is one number, a scalar. GLSL writes it as:
float along = dot(d, u);
Do not confuse it with d * u. Multiplying two vec2 values with * gives the component-wise pair (0.3, 0.0). The dot product adds that pair to get 0.3. In 3D, it adds the Z product as well.
Measure one direction
Let d be a displacement and u a unit direction. Then dot(d, u) measures how much of that displacement lies along u, including its sign.
For u = (1, 0), the formula returns d.x. For u = (0, 1), it returns d.y. A diagonal unit vector combines both components into a coordinate along that diagonal.
Why must the measuring direction have length one? Move exactly k units along it. The displacement is k * u:
dot(k * u, u)
= k * (u.x * u.x + u.y * u.y)
= k * 1.0
= k
The middle sum is one because u has length one. The measurement therefore returns the distance we traveled, with a negative value when we travel backward. This signed number is also called a scalar projection. It is not a position or a vector.
For our gradient, the displacement starts at a chosen center:
vec2 d = p - center;
float along = dot(d, unitDirection);
p and center use the equal-unit space from chapter 01. Their difference is in coordinate units. The unit direction is dimensionless, so along is also in coordinate units.
Read the sign
For two unit vectors, the result stays between -1 and 1. Compare these directions with u = (0.6, 0.8):
| Other unit vector | Products and sum | Meaning |
|---|---|---|
| (0.6, 0.8) | 0.36 + 0.64 = 1 | Same direction |
| (-0.8, 0.6) | -0.48 + 0.48 = 0 | Perpendicular |
| (-0.6, -0.8) | -0.36 - 0.64 = -1 | Opposite direction |
A positive result means the vectors point partly toward the same side. A negative result means they point partly against each other. Zero means perpendicular when both vectors are nonzero. The zero vector also has dot product zero with everything, but it has no direction to compare.
For arbitrary vectors, the result also depends on their lengths. Doubling either input doubles the dot product; doubling both multiplies it by four. For example, dot((0.3, 0.4), (0.3, 0.4)) is 0.25, not one, even though the directions match perfectly.
In GLSL, dot(v, v) is the squared length of v. Normalize both inputs when you want a pure comparison of direction. In our gradient, normalize only the measuring arrow. Keep the pixel displacement’s length: that is the distance we want to measure.
Turn the number into color
At the center, along is zero. On the forward side it is positive; on the backward side it is negative. Map a band around zero into the 0–1 range needed by mix:
float gradient = clamp(0.5 + along / width, 0.0, 1.0);
vec3 color = mix(colorA, colorB, gradient);
width is the full transition width in coordinate units, measured along the unit direction. At along = -width / 2, the blend reaches zero. At the center it is 0.5. At along = width / 2, it reaches one. Beyond those boundaries, clamping keeps an endpoint color.
With width 1 and our probe at (0.5, 0), along = 0.3, so the blend is 0.8. Making the width smaller creates a sharper wipe. Moving the center slides the transition through the image.
Width must stay positive. This demo limits it to 0.02–2. A width of zero would introduce division by zero; use an explicit step for a truly hard boundary instead.