# Shader basics: Coordinates and colors

Table of Contents

Shaders turn math into visuals by calculating a color for every pixel on the screen. Before we can start drawing circles, animating patterns, or building trippy effects, we need the basics.

In this post, we’ll look at coordinate systems and write a couple of simple shaders. We’ll use Shadertoy so you don’t need to set up anything. Just type code and see pixels change. The ideas here carry over to Unity, Three.js, or plain OpenGL.

The fundamentals

Every fragment shader has to decide one thing: what color should this pixel be?
That color is written into a variable called fragColor. It’s a vec4, a vector made of 4 floating-point numbers representing red, green, blue, and alpha (transparency). Each component is between 0.0 and 1.0.

  • vec4(1.0, 0.0, 0.0, 1.0) → solid red
  • vec4(0.0, 1.0, 0.0, 1.0) → solid green
  • vec4(0.0, 0.0, 1.0, 1.0) → solid blue

Here’s the smallest possible shader:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
fragColor = vec4(1.0, 0.0, 0.0, 1.0); // paint everything red
}

Create a new shader in Shadertoy, paste the code above into the textbox and hit compile (alt + enter). You should see something like the following:

Static shader 0 × 0
Red

You might have noticed that the mainImage function has a second parameter, fragCoord, a vec2 that consists of the x and y position of the pixel we are shading. Right now we’re ignoring it, but in the next section we’ll use it to make the color change depending on the pixel’s location.

From coordinates to color

So far, every pixel is the same color. To make shaders more interesting, we need to know which pixel we’re shading. That’s what fragCoord gives us: the x and y position of the pixel on the screen.

On Shadertoy, the origin (0, 0) is the bottom-left corner, and the values go up to the screen resolution (e.g., 1920×1080). If we use these raw values directly, the numbers are huge and hard to work with. Instead, we usually normalize the coordinates so they go from 0.0 to 1.0:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy; // normalize pixel coords (0–1)
fragColor = vec4(uv.x, uv.y, 0.0, 1.0); // red = x, green = y
}

iResolution is a vec3 where x and y are the resolution and z is the pixel aspect ratio (this is rarely used). Now the left edge is red = 0.0, the right edge is red = 1.0. The bottom edge is green = 0.0, and the top is green = 1.0. The result is a smooth gradient across the screen.

Static shader 0 × 0
UV gradient

Mathematical functions

GLSL provides functions like sin, which lets us turn coordinates into smooth repeating patterns. Sine waves are useful because they oscillate smoothly between -1 and 1. By scaling and shifting their output, we can turn them into color values in the [0,1] range.

In the following example, we multiply the x position by 50.0 to achieve a more frequent wave. Experiment by using the y axis instead and multiply it by different values.

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float color = 0.5 + 0.5 * sin(uv.x * 50.0);
fragColor = vec4(color, color, color, 1.0);
}
Static shader 0 × 0
Sine wave pattern

Animating shaders

Static patterns are interesting, but animation makes them dynamic. iTime gives us the elapsed time in seconds, which we can feed into math functions to animate them. The following produces a simple pulsing effect.

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float color = 0.5 + 0.5 * sin(iTime);
fragColor = vec4(color, color, color, 1.0);
}
0.00 0.0 fps 0 × 0
Pulsing grayscale

Combining time and position

Combining pixel position and iTime allows waves and patterns to move across the screen. This introduces motion, phase shifts, and directional effects. Experimenting with frequency, amplitude, and phase offsets produces a wide range of effects. Small changes can create strikingly different visuals, which is why shaders are so versatile. You can multiply iTime with larger values to make the animation faster.

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float color = 0.5 + 0.5 * sin(iTime * 5.0 + uv.x * 10.0);
fragColor = vec4(color, color, color, 1.0);
}
0.00 0.0 fps 0 × 0
Animated waves combining position and time

Conclusion

In this post, we covered the fundamentals of fragment shaders:

  • Every pixel’s color is determined by writing to fragColor, a vec4 representing RGBA.

  • fragCoord provides the position of each pixel, which can be normalized to [0,1] for easier calculations.

  • Mathematical functions like sin allow creation of repeating patterns and gradients.

  • iTime lets you animate shaders by feeding elapsed time into these functions.

  • Combining position and time enables dynamic, moving patterns.

Understanding these basics provides a foundation for building more complex shaders, whether in Shadertoy, WebGL, Unity, or other graphics frameworks. Once you’re comfortable with coordinates, colors, and simple animations, you can start experimenting with shapes, textures, and interactive effects.

See you in the next post in the series!

My avatar

Thanks for reading! I hope you found this helpful. Feel free to leave a comment or reach out if you have questions or want to have chat. You can find me on the social links below.


More Posts

Reactions & Comments