Your First Shader
Writing code that paints every pixel on screen
Now we write code. We will start with the simplest possible shader: painting every pixel the same color. Then we will add a single ingredient, the pixel's position, and watch solid color transform into gradients. By the end of this chapter, you will understand the fundamental anatomy of every fragment shader.
The Anatomy of a Fragment Shader
Every fragment shader contains a function called main. This function runs once for each pixel being rendered. Its job is to set a special output variable called gl_FragColor, which holds the final color of that pixel.
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}This shader paints every pixel red. Let us break down what is happening.
gl_FragColor expects a vec4: four numbers representing red, green, blue, and alpha (transparency). Each component ranges from 0.0 to 1.0, where 0.0 means none of that color and 1.0 means full intensity.
So vec4(1.0, 0.0, 0.0, 1.0) means:
- Red: 1.0 (full)
- Green: 0.0 (none)
- Blue: 0.0 (none)
- Alpha: 1.0 (fully opaque)
A Solid Red Shader
That is it. Every pixel asks "what color should I be?" and the shader answers "red." Every single pixel gets the same answer because we have not given them any way to differentiate themselves.
Enter gl_FragCoord
To create anything more interesting than a solid color, each pixel needs to know where it is. The built-in variable gl_FragCoord provides exactly this. It contains the pixel's position in screen coordinates, where (0, 0) is the bottom-left corner.
gl_FragCoord is a vec4, but we usually only care about .x and .y:
gl_FragCoord.xis the pixel's horizontal positiongl_FragCoord.yis the pixel's vertical position
If we use these raw coordinates as colors, we will get numbers like 0, 1, 2... up to the screen width or height. Since colors need to be between 0 and 1, we need to normalize these coordinates.
Normalizing Coordinates
To convert pixel coordinates to a 0-1 range, we divide by the resolution. This is such a common operation that shader environments typically provide the screen dimensions as a uniform variable. In our setup, this is u_resolution.
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = vec4(uv.x, 0.0, 0.0, 1.0);
}Now uv.x goes from 0.0 at the left edge to 1.0 at the right edge. We use this value as the red channel, creating a horizontal gradient.
Horizontal Gradient: Red increases from left to right
Toggle between horizontal and vertical to see how uv.x and uv.y create different gradients
The left pixels have uv.x close to 0, so they appear black. The right pixels have uv.x close to 1, so they appear bright red. Every pixel in between gets a proportional shade.
This is the core insight of shader programming: position becomes color. The pixel's coordinates determine its color through whatever mathematical relationship we define.
Two-Axis Gradients
Let us use both x and y. By mapping x to the red channel and y to the green channel, we create a gradient that varies in two dimensions:
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = vec4(uv.x, uv.y, 0.0, 1.0);
}Two-Axis Gradient: Red and green vary with position
Study this image. The bottom-left corner is black (x=0, y=0). The bottom-right is red (x=1, y=0). The top-left is green (x=0, y=1). The top-right is yellow (x=1, y=1), because mixing full red and full green produces yellow.
This pattern, mapping UV coordinates to the red and green channels, is so fundamental that it has become a debugging tool. When something goes wrong in shader code, experienced developers often output the UV coordinates to visually verify that the coordinate system is correct.
Your Turn
Now it is your turn. The editor below contains a shader you can modify. Try these experiments:
- Paint the screen solid blue
- Create a vertical gradient (dark at bottom, bright at top)
- Make the blue channel vary with position while keeping red and green at 0.5
- Create a gradient that goes from cyan (0, 1, 1) to magenta (1, 0, 1)
Interactive: Your First Shader
Available Uniforms
vec2 u_resolutionfloat u_timevec2 u_mouseDo not worry about making mistakes. The worst that can happen is you see an error message or a strange color. Play with the numbers, change expressions, and observe what happens.
The Pattern
Every shader we write will follow this same structure:
- Calculate normalized UV coordinates
- Transform or use those coordinates somehow
- Convert the result to a color
- Assign to
gl_FragColor
What changes is step 2. The transformations get more sophisticated, the math gets more interesting, but the fundamental flow remains: position in, color out.
Key Takeaways
main()runs once per pixel and must setgl_FragColorgl_FragColoris avec4with RGBA values from 0.0 to 1.0gl_FragCoord.xygives the pixel's screen position in pixels- Dividing by
u_resolutionnormalizes coordinates to the 0-1 range - The relationship between position and color is the heart of shader programming