In the previous tutorial, we introduced time into our shaders. By using a time uniform, we were able to move gradients and animate repeating patterns.
That was our first look at animation, but the movement was very simple. Everything moved in a straight line at a constant speed.
Many natural movements are different. Ocean waves rise and fall. Light pulses gently. Sound travels in waves. Even the motion of a swinging pendulum follows a repeating pattern.
To create these kinds of animations, GLSL provides a function called sin().
It is one of the most important functions in shader programming. Once you understand it, you can create flowing water, breathing lights, waving flags, audio visualizers, and countless procedural effects.
This tutorial builds on the previous lessons.
Part 1: Your First Shader: Painting the Entire Screen One Color
Part 2: Understanding UV Coordinates: The Secret Behind Every Shader
Part 3: Your First Gradient: Using UV Coordinates to Paint Space
Part 4: Mixing Colors with mix(): Creating Smooth Color Transitions
Part 5: Keeping Values Under Control with clamp()
Part 6: Adding Contrast with pow(): Making Gradients More Interesting
Part 7: Repeating Patterns with fract(): Creating Infinite Tiles
Part 8: Drawing Sharp and Smooth Edges with step() and smoothstep()
Part 9: Bringing Shaders to Life with Time
Now we are going to make our animations feel much more natural.
sin()?The sin() function creates a value that rises and falls continuously.
Unlike a gradient, which keeps increasing, sin() repeats forever.
Its structure is simple.
sin(value)
The input can keep increasing forever, but the output will always stay between
-1.0
and
1.0
That repeating motion is why it appears in so many shaders.
Imagine watching a point move up and down forever.
●
● ●
● ●
● ●
●-----------●-----------
●
●
●
It never suddenly jumps.
It moves smoothly, reaches the top, comes back down, reaches the bottom, and repeats.
This smooth motion makes animations feel natural.
sin() ExampleLet's start with a simple number.
float value = sin(0.0);
The result is
0.0
Now change the input.
float value = sin(1.0);
The value changes.
Increase it again.
float value = sin(2.0);
It changes again.
As the input keeps increasing, the output continuously moves between -1.0 and 1.0.
Now combine sin() with the time uniform.
float wave = sin(uTime);
Since uTime increases every frame, the sine value keeps changing.
Instead of increasing forever, it rises and falls repeatedly.
This is the beginning of procedural animation.
Let's use that value as brightness.
#ifdef GL_ES
precision mediump float;
#endif
uniform float uTime;
void main() {
float wave = sin(uTime);
gl_FragColor = vec4(vec3(wave * 0.5 + 0.5), 1.0);
}
You might notice something new.
We wrote
wave * 0.5 + 0.5
Why?
Because sin() produces values between -1.0 and 1.0.
Colors usually work between 0.0 and 1.0.
Multiplying by 0.5 makes the range smaller.
Adding 0.5 moves it upward.
The new range becomes
0.0
to
1.0
Now the brightness smoothly pulses instead of becoming negative.
Instead of changing brightness, let's move a gradient.
float gradient = vUv.x + sin(uTime);
The entire gradient shifts left and right as the sine wave changes.
The movement slows naturally at each end before changing direction.
This feels much more organic than constant motion.
Sometimes the movement is too large.
You can control it by multiplying the sine wave.
sin(uTime) * 0.2
Now the animation only moves a short distance.
Increasing the value makes the movement larger.
sin(uTime) * 0.8
This multiplier is called the amplitude.
It controls how big the wave becomes.
Think of it like the height of ocean waves.
Small amplitude creates gentle ripples.
Large amplitude creates dramatic waves.
The animation speed is controlled by the input.
sin(uTime * 0.5)
This moves slowly.
Now try
sin(uTime * 3.0)
The wave moves much faster.
The number multiplying uTime controls how quickly the wave repeats.
Let's combine sin() with mix().
float t = sin(uTime) * 0.5 + 0.5;
vec3 color = mix(
vec3(0.0, 0.3, 1.0),
vec3(1.0, 0.2, 0.6),
t
);
gl_FragColor = vec4(color, 1.0);
Instead of staying one color, the screen slowly fades between blue and pink.
The transition never suddenly changes.
It keeps flowing forever.
Now combine UV coordinates with time.
float wave = sin(vUv.x * 10.0 + uTime);
Instead of the entire screen changing together, each position has its own wave.
The result looks like ripples travelling across the screen.
This is one of the most common techniques used in shader animation.
Slow wave.
sin(uTime * 0.5)
Fast wave.
sin(uTime * 4.0)
Small movement.
sin(uTime) * 0.2
Large movement.
sin(uTime) * 1.0
Moving horizontal waves.
sin(vUv.x * 8.0 + uTime)
Try changing each number and observe how the animation changes.
Can you create these effects?
Spend a few minutes experimenting before moving on.
The best way to understand sin() is by changing the numbers yourself.
The sin() function creates a repeating wave that smoothly moves between -1.0 and 1.0.
By combining it with uTime, we can create natural animation instead of simple linear movement.
Changing the amplitude controls how large the motion becomes.
Changing the speed controls how quickly the animation repeats.
These simple ideas are used in thousands of shaders, from animated backgrounds to realistic water and procedural effects.