The Hidden Pattern: Everything is a Wave
You've mastered spatial patterns with coordinates (Chapter 2) and color manipulation (Chapter 3). But here's what no tutorial tells you: every visual effect in computer graphics ultimately comes from just a handful of mathematical functions. Master these, and you'll see them everywhere - in water simulations, audio visualizers, organic textures, even UI animations.
But here's the deeper truth: these aren't arbitrary functions. They represent fundamental patterns in nature and mathematics.
Concept Introduction: The Universe Runs on Waves
Look at these phenomena:
- Ocean waves
- Sound vibrations
- Light oscillations
- Heartbeats
- Planetary orbits
What do they share? Periodic motion. Something that repeats, oscillates, cycles. The mathematics that describes a pendulum also describes how colors pulse in your shader.
The functions we'll learn aren't just "useful for graphics" - they're the building blocks of pattern itself.
Mathematical Foundation: From Circles to Waves
Why Sine Creates Waves
The sine function seems magical until you understand its origin:
Imagine a point rotating around a circle.
Track only its vertical position over time.
That trace creates a sine wave.
This is why:
sin(0) = 0
(starting at the right, vertical position is 0)sin(π/2) = 1
(top of circle, maximum vertical position)sin(π) = 0
(left side, back to center height)sin(3π/2) = -1
(bottom, minimum vertical position)sin(2π) = 0
(full rotation, back to start)
In shaders:
float wave = sin(angle); // -1 to 1
But angle comes from position:
float angle = uv.x * frequency; // Convert position to angle
float wave = sin(angle);
The Deep Meaning of Frequency
When you write sin(x * 5.0)
, that 5.0
isn't just "making more waves." You're saying: "In the space where x goes from 0 to 1, complete 5 full rotations around the circle."
Higher frequency = more rotations = more waves.
Fract: The Space Duplicator
The fract()
function is misunderstood. It doesn't just "repeat patterns" - it creates infinite copies of coordinate space.
fract(3.7) = 0.7
But conceptually:
Original space: 0 -------- 1 -------- 2 -------- 3 -------- 4
After fract: 0 -------- 1, 0 ----- 1, 0 ----- 1, 0 ----- 1
[Copy 1] [Copy 2] [Copy 3] [Copy 4]
When you do:
float2 cell = fract(uv * 10.0);
You're creating 10×10 copies of your UV space. Each pixel thinks it's in a 0-1 box, unaware of the other 99 copies.
Step Functions: Digital Reality
step(edge, x)
represents a fundamental question: "Have we crossed the threshold?"
In nature:
- Water freezes at 0°C (step function)
- Neurons fire when voltage exceeds threshold (step function)
- Light switches toggle (step function)
In shaders:
float mask = step(0.5, uv.x); // Left half: 0, Right half: 1
Smoothstep: Nature Abhors Sharp Edges
Real world transitions are rarely instant. smoothstep
adds a transition zone:
// Hermite interpolation - smooth acceleration and deceleration
float t = clamp((x - a) / (b - a), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
This specific curve (3t² - 2t³) has special properties:
- Starts slowly (derivative = 0 at t=0)
- Ends slowly (derivative = 0 at t=1)
- Smooth throughout (continuous second derivative)
Visual Intuition
Let's see these functions as transformations of space:
Linear Space
Input: 0.0 ---- 0.5 ---- 1.0
Output: 0.0 ---- 0.5 ---- 1.0
After sin()
Input: 0.0 ---- 0.5π ---- π ---- 1.5π ---- 2π
Output: 0.0 ---- 1.0 ----- 0.0 --- -1.0 ---- 0.0
After fract()
Input: 0.0 ---- 1.0 ---- 2.0 ---- 3.0
Output: 0.0 ---- 1.0, 0.0 - 1.0, 0.0 - 1.0
After pow(x, 2)
Input: 0.0 -- 0.5 -- 1.0
Output: 0.0 - 0.25 - 1.0
Your First Deep Pattern
Let's build understanding step by step:
[[ stitchable ]] half4 interferencePattern(float2 position, half4 color, float2 size) {
float2 uv = position / size;
// Two waves at different angles
float wave1 = sin(uv.x * 10.0);
float wave2 = sin(uv.y * 10.0);
// Multiply: where both are positive or both negative = bright
// Where they disagree = dark
float interference = wave1 * wave2;
// This creates a checkerboard of positive/negative values
// Map to 0-1 for visibility
float brightness = interference * 0.5 + 0.5;
return half4(brightness, brightness, brightness, 1.0);
}
Building Complex Effects
Understanding Phase
[[ stitchable ]] half4 travelingWave(float2 position, half4 color, float2 size, float time) {
float2 uv = position / size;
// Phase shift moves the wave
float phase = time * 2.0;
float wave = sin(uv.x * 10.0 - phase);
// Visualize
float brightness = wave * 0.5 + 0.5;
return half4(brightness, brightness, brightness, 1.0);
}
The -phase
shifts where the wave starts. As time increases, the wave appears to move right.
Radial Patterns: From Linear to Polar
[[ stitchable ]] half4 radialPulse(float2 position, half4 color, float2 size, float time) {
float2 uv = position / size;
float2 center = uv - 0.5;
// Distance creates circular coordinate
float radius = length(center);
// Apply sine to radius instead of x/y
float wave = sin(radius * 30.0 - time * 3.0);
// Smooth edges
float mask = 1.0 - smoothstep(0.4, 0.5, radius);
float brightness = wave * 0.5 + 0.5;
return half4(brightness * mask, brightness * mask, brightness * mask, 1.0);
}
The Power of Power
[[ stitchable ]] half4 falloffComparison(float2 position, half4 color, float2 size) {
float2 uv = position / size;
// Three different falloff curves
float linear = uv.x;
float quadratic = pow(uv.x, 2.0);
float sqrt = pow(uv.x, 0.5);
// Show each in R, G, B channels
return half4(linear, quadratic, sqrt, 1.0);
}
Common Pitfalls
Pitfall 1: Fighting the Coordinate System
// WRONG - Trying to create vertical waves with horizontal coordinate
float wave = sin(uv.x * 10.0); // Always horizontal!
// CORRECT - Match coordinate to desired direction
float wave = sin(uv.y * 10.0); // Vertical waves
Pitfall 2: Misunderstanding Frequency Limits
The Nyquist theorem applies to shaders too. If your frequency is too high:
// TOO HIGH - Creates aliasing
float wave = sin(uv.x * 1000.0);
// CORRECT - Respect pixel density
float wave = sin(uv.x * 10.0);
Pitfall 3: Phase vs Frequency Confusion
// Frequency: How many waves fit in the space
sin(uv.x * frequency)
// Wavelength: How wide each wave is (1/frequency)
// Phase: Where the wave starts
sin(uv.x * frequency + phase)
// Amplitude: How tall the wave is
sin(uv.x * frequency) * amplitude
Advanced Pattern Building
Combining Octaves (Preview of Noise)
[[ stitchable ]] half4 complexWave(float2 position, half4 color, float2 size) {
float2 uv = position / size;
float wave = 0.0;
float amplitude = 1.0;
float frequency = 1.0;
// Add multiple octaves
for(int i = 0; i < 4; i++) {
wave += sin(uv.x * frequency * 10.0) * amplitude;
frequency *= 2.0; // Double frequency
amplitude *= 0.5; // Halve amplitude
}
// Normalize
wave = wave * 0.5 + 0.5;
return half4(wave, wave, wave, 1.0);
}
Challenges
Challenge 1: Wave Mastery
Create waveVisualizer
that shows:
- A sine wave
- The same wave with double frequency
- The same wave with half amplitude
- All three visible simultaneously (use R, G, B channels)
Challenge 2: Fract Exploration
Create gridExplorer
that:
- Uses
fract()
to create a 5×5 grid - Colors each cell differently based on its grid position
- Hint:
floor(uv * 5.0)
gives you cell coordinates
Challenge 3: Smoothstep Animation
Create breathingCircle
that:
- Shows a circle that grows and shrinks
- Uses
smoothstep
for soft edges - Uses
sin(time)
to control size - Bonus: Make it pulse with a heartbeat rhythm
Challenge 4: Function Composition
Create moire
that:
- Combines rotated coordinate systems
- Uses sin() on both
- Creates interference patterns
- Should look like silk fabric patterns
Challenge 5: Distortion Introduction
Create ripple
using distortionEffect
:
[[ stitchable ]] float2 ripple(float2 position, float2 size, float time) {
float2 uv = position / size;
float2 center = uv - 0.5;
float dist = length(center);
float wave = sin(dist * 20.0 - time * 3.0) * 0.02;
// Return new position (this is distortionEffect!)
return position + normalize(center) * wave * size.x;
}
Want the Challenges Solutions?
Get the full Xcode project with solutions to all challenges, bonus examples, and clean, runnable code.
Get the Full Project →Validation Questions
Before proceeding to Chapter 5:
- Why does
sin()
create smooth waves instead of sharp triangles?
Because sine comes from circular motion - tracking the vertical position of a point rotating around a circle. This creates smooth acceleration and deceleration, not linear motion.
- What happens when you multiply two sine waves together?
You get interference patterns. Where both waves are positive or both negative, you get bright areas. Where they have opposite signs, you get dark areas, creating a checkerboard-like pattern.
- How does
fract(uv * 5)
create 5 copies of space?
fract()
returns only the fractional part (0.0-1.0), so multiplying by 5 creates coordinates from 0-5, but fract()
wraps each integer interval back to 0-1, creating 5 copies of the original 0-1 coordinate space.
- Why use
smoothstep(0.4, 0.6, x)
instead ofstep(0.5, x)
?
step()
creates a hard binary transition (0 or 1) which causes aliasing and jagged edges. smoothstep()
creates a smooth transition zone between 0.4 and 0.6, eliminating visual artifacts.
- What's the relationship between frequency and wavelength?
They're inversely related: wavelength = 1/frequency. Higher frequency means more waves in the same space, so each individual wave is shorter (smaller wavelength).
- How would you create a wave that speeds up over time?
Use accelerating phase: sin(uv.x * frequency - time * time * acceleration)
. The phase advances faster and faster over time, making the wave move at increasing speed. Alternative: increase frequency over time with sin(uv.x * (baseFrequency + time * rate))
for more waves over time.
Debugging Deep Patterns
// Visualize function ranges
return half4(
sin(uv.x * 5.0) * 0.5 + 0.5, // Red: sine
fract(uv.x * 5.0), // Green: fract
smoothstep(0.4, 0.6, uv.x), // Blue: smoothstep
1.0
);
Further Exploration
- Fourier Analysis: How any pattern can be built from sines
- Signal Processing: Why these functions matter beyond graphics
- Natural Patterns: Reaction-diffusion, fluid dynamics, crystal growth
- Distortion Effects: The path to truly dynamic shaders
Next Chapter Preview: You've learned that functions create patterns. Chapter 5 will show you how to use mathematical tricks to generate complex, infinitely detailed designs from simple rules. We'll explore how fract()
, mod()
, and coordinate transformation can create anything from celtic knots to fractal landscapes.