One of the most confusing moments when learning shaders happens when you encounter something like this:
gl_FragColor = vec4(vec3(uv.y), 1.0);
At first glance, it does not seem like it should create a gradient at all.
After all, if we write:
vec3(uv.y)
GLSL expands it to:
vec3(uv.y, uv.y, uv.y)
Every component contains the same value.
So why do we see a smooth transition from black to white across the screen?
The answer lies in understanding what changes and what stays the same.
Earlier in the shader we usually create normalized coordinates:
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
This converts pixel coordinates into values between 0.0 and 1.0.
For the vertical coordinate:
uv.y
the bottom of the screen is:
0.0
the middle is:
0.5
and the top is:
1.0
Every pixel receives its own value.
Imagine a screen that is only five pixels tall:
| Position | uv.y |
|---|---|
| Top | 1.0 |
| 75% | 0.75 |
| Middle | 0.5 |
| 25% | 0.25 |
| Bottom | 0.0 |
The value is not random. It is determined entirely by where the pixel is located.
Suppose a pixel has:
uv.y = 0.5;
Then:
vec3(uv.y)
becomes:
vec3(0.5, 0.5, 0.5)
This means:
Red = 0.5
Green = 0.5
Blue = 0.5
Since all three color channels are equal, the result is gray.
Now look at another pixel:
uv.y = 0.8;
which becomes:
vec3(0.8, 0.8, 0.8)
This is still gray, but a much brighter gray.
A common misunderstanding is thinking that the gradient comes from the three channels being different.
It does not.
In fact, the channels are identical.
The gradient comes from neighboring pixels receiving different values.
For one pixel:
vec3(0.2, 0.2, 0.2)
For the next pixel above:
vec3(0.21, 0.21, 0.21)
For the next:
vec3(0.22, 0.22, 0.22)
And so on.
Each pixel is slightly brighter than the one below it.
That gradual change creates the gradient.
You can think of the screen like this:
uv.y = 1.0 ██████████████
uv.y = 0.8 ▓▓▓▓▓▓▓▓▓▓▓▓▓
uv.y = 0.6 ▒▒▒▒▒▒▒▒▒▒▒▒▒
uv.y = 0.4 ░░░░░░░░░░░░░
uv.y = 0.2 ..............
uv.y = 0.0
The higher the pixel, the larger the value.
The larger the value, the brighter the gray.
This simple example introduces one of the most important ideas in shader programming.
Every pixel runs the shader independently.
When you write:
vec3(uv.y)
you are not creating one color.
You are creating a different color for every pixel based on its position.
That idea is the foundation of gradients, patterns, shapes, noise, and nearly every visual effect you will build later.
Create a horizontal gradient:
gl_FragColor = vec4(vec3(uv.x), 1.0);
Use both coordinates:
gl_FragColor = vec4(uv.x, uv.y, 0.0, 1.0);
Invert the gradient:
gl_FragColor = vec4(vec3(1.0 - uv.y), 1.0);
The more you experiment with uv.x and uv.y, the more you will realize that shaders are often just ways of turning positions into colors.