Introduction to Functions

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.

Interference Pattern

Two sine waves multiplied together create interference patterns - bright where they agree, dark where they disagree

[[ 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);
}
SwiftUI Metal Shader

Building Complex Effects

Understanding Phase

Traveling Wave

A sine wave that moves across the screen using phase shift - demonstrates how time creates motion

[[ 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);
}
SwiftUI Metal Shader

The -phase shifts where the wave starts. As time increases, the wave appears to move right.

Radial Patterns: From Linear to Polar

Radial Pulse

Concentric circular waves radiating from the center, with smooth edge masking for a focused effect

[[ 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);
}
SwiftUI Metal Shader

The Power of Power

Falloff Comparison

Three different power curves displayed as RGB channels - linear (red), quadratic (green), and square root (blue)

[[ 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);
}
SwiftUI Metal Shader

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)

Complex Wave (Multi-Octave)

Multiple sine waves combined with increasing frequency and decreasing amplitude - a preview of noise generation techniques

[[ 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);
}
SwiftUI Metal Shader

Challenges

Challenge 1: Wave Mastery

Wave Mastery Example

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

Fract Exploration Example

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

Smoothstep Animation Example

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

Function Composition Example

Create moire that:

  • Combines rotated coordinate systems
  • Uses sin() on both
  • Creates interference patterns
  • Should look like silk fabric patterns

Challenge 5: Distortion Introduction

Distortion Example

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;
}

🔥 Black Friday — 50% OFF

Want the Challenges Solutions?

Get the full Xcode project with solutions to all challenges, bonus examples, and clean, runnable code.

Get 50% Off Now →

Validation Questions

Before proceeding to Chapter 5:

  1. 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.

  1. 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.

  1. 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.

  1. Why use smoothstep(0.4, 0.6, x) instead of step(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.

  1. 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).

  1. 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.