The Coordinate System

Core Concept: Position as the foundation of all shader effects. Explore UV vs pixel coordinates, gesture coordinate conversion, device rotation/orientation, and coordinate transformations. Challenge: Spotlight effect that follows touch input.

Your Current Understanding Is Incomplete

In Chapter 1, you learned that shaders receive a position parameter. You might think: "Position tells me where the pixel is, so I can use it to create patterns." This captures only part of the truth.

The real insight: Position is not just location - it's the fundamental input that drives everything in shader programming. Every effect, every pattern, every animation ultimately derives from transforming position into color.

But here's what no tutorial tells you: the position parameter you receive is almost never what you actually want to use. Raw pixel coordinates are like getting GPS coordinates when you need driving directions. Technically correct, but practically useless until transformed.

Concept Introduction: The Coordinate Transformation

The first line of almost every shader you'll ever write:

float2 uv = position / size;

This transforms pixel coordinates (0 to width, 0 to height) into UV coordinates (0 to 1, 0 to 1). UV coordinates are percentages - they tell you "how far across" and "how far down" you are, regardless of screen size.

Why this matters: A circle drawn with UV coordinates looks the same on iPhone and iPad. A circle drawn with pixel coordinates gets stretched and distorted.

Mathematical Foundation: UV Space

The Transformation

float2 uv = position / size;
// position: (400, 300) on 800x600 screen
// size: (800, 600)
// uv: (0.5, 0.5) - exactly center, always

UV Coordinate Properties

  • (0, 0): Top-left corner
  • (1, 1): Bottom-right corner
  • (0.5, 0.5): Exact center
  • Range: Always 0.0 to 1.0, regardless of screen size

Centered Coordinates

Often you want (0,0) at the center:

float2 uv = position / size;
float2 centered = uv - 0.5;  // Range: -0.5 to 0.5
// or
float2 centered = (uv - 0.5) * 2.0;  // Range: -1.0 to 1.0

Your First Pattern: Horizontal Gradient

[[ stitchable ]] half4 horizontalGradient(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    return half4(uv.x, uv.x, uv.x, 1.0);
}

What happens:

  • Left edge: uv.x = 0.0 → Black
  • Right edge: uv.x = 1.0 → White
  • Center: uv.x = 0.5 → Gray

Drawing Shapes with Distance

Circle Using Distance

[[ stitchable ]] half4 circle(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    float2 center = uv - 0.5;  // Center at (0,0)
    
    float dist = length(center);  // Distance from center
    
    if (dist < 0.3) {
        return half4(1.0, 1.0, 1.0, 1.0);  // White inside
    } else {
        return half4(0.0, 0.0, 0.0, 1.0);  // Black outside
    }
}

Smooth Circle with smoothstep

[[ stitchable ]] half4 smoothCircle(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    float2 center = uv - 0.5;
    
    float dist = length(center);
    float circle = 1.0 - smoothstep(0.29, 0.31, dist);
    
    return half4(circle, circle, circle, 1.0);
}

Key insight: smoothstep(0.29, 0.31, dist) creates a smooth transition between 0.29 and 0.31, eliminating jagged edges.

Pattern Creation

Grid Pattern

[[ stitchable ]] half4 gridPattern(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    
    // Create 8x8 grid
    float2 grid = fract(uv * 8.0);
    
    // Draw lines at grid boundaries
    float lineWidth = 0.1;
    float lines = 0.0;
    
    if (grid.x < lineWidth || grid.y < lineWidth) {
        lines = 1.0;
    }
    
    return half4(lines, lines, lines, 1.0);
}

Radial Pattern

[[ stitchable ]] half4 radialPattern(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    float2 center = uv - 0.5;
    
    float dist = length(center);
    float pattern = sin(dist * 20.0) * 0.5 + 0.5;
    
    return half4(pattern, pattern, pattern, 1.0);
}

Common Pitfalls

Pitfall 1: Forgetting the UV Transform

// WRONG - Uses raw pixel coordinates
float2 center = position - size * 0.5;

// CORRECT - Transform to UV first
float2 uv = position / size;
float2 center = uv - 0.5;

Pitfall 2: Aspect Ratio Issues

// WRONG - Circles become ellipses on non-square screens
float dist = length(uv - 0.5);

// CORRECT - Account for aspect ratio
float2 uv = position / size;
float aspectRatio = size.x / size.y;
float2 centered = (uv - 0.5) * float2(aspectRatio, 1.0);
float dist = length(centered);

Pitfall 3: Hard Edges

// WRONG - Jagged edges
float circle = step(0.3, dist);

// CORRECT - Smooth edges
float circle = smoothstep(0.29, 0.31, dist);

SwiftUI Integration

For testing your shaders, use this SwiftUI template:

struct ShaderTestView: View {
    var body: some View {
        Rectangle()
            .frame(width: 300, height: 300)
            .visualEffect { content, proxy in
                content.colorEffect(
                    ShaderLibrary.yourShaderName(.float2(proxy.size))
                )
            }
    }
}

Key point: Use visualEffect with proxy.size to pass the actual rendered size to your shader.

Challenges

Challenge 1: Basic Gradients

Create three gradient shaders:

  1. verticalGradient: Black at top, white at bottom
  2. diagonalGradient: Gradient from top-left to bottom-right
  3. radialGradient: White center fading to black edges

SwiftUI Test Code:

VStack {
    Rectangle()
        .visualEffect { content, proxy in
            content.colorEffect(ShaderLibrary.verticalGradient(.float2(proxy.size)))
        }
    Rectangle()
        .visualEffect { content, proxy in
            content.colorEffect(ShaderLibrary.diagonalGradient(.float2(proxy.size)))
        }
    Rectangle()
        .visualEffect { content, proxy in
            content.colorEffect(ShaderLibrary.radialGradient(.float2(proxy.size)))
        }
}
.frame(width: 200, height: 200)

Challenge 2: Shape Drawing

Create these shape shaders:

  1. smoothCircle: Perfect circle with smooth edges
  2. ring: Ring shape (circle with hole)
  3. square: Square using distance functions

Hint for ring: Use two distance checks - outer and inner radius.

Challenge 3: Pattern Creation

Create these pattern shaders:

  1. checkerboard: 8x8 checkerboard pattern
  2. stripes: Horizontal stripes
  3. concentricCircles: Multiple circles from center

Hint for checkerboard: Use fract() and step() with grid coordinates.

Challenge 4: Interactive Spotlight

Create spotlight shader that takes a center position parameter:

[[ stitchable ]] half4 spotlight(
    float2 position, 
    half4 color, 
    float2 size,
    float2 lightCenter  // UV coordinates (0-1)
) {
    // Your code here
}

SwiftUI Integration:

struct SpotlightView: View {
    @State private var lightPos = CGPoint(x: 0.5, y: 0.5)
    
    var body: some View {
        GeometryReader { geo in
            Rectangle()
                .visualEffect { content, proxy in
                    content.colorEffect(ShaderLibrary.spotlight(
                        .float2(proxy.size),
                        .float2(Float(lightPos.x), Float(lightPos.y))
                    ))
                }
                .gesture(
                    DragGesture().onChanged { value in
                        lightPos = CGPoint(
                            x: value.location.x / geo.size.width,
                            y: value.location.y / geo.size.height
                        )
                    }
                )
        }
        .frame(width: 300, height: 300)
    }
}

Challenge 5: Coordinate Transformations

Create rotatingPattern that rotates a pattern over time:

[[ stitchable ]] half4 rotatingPattern(
    float2 position, 
    half4 color, 
    float2 size,
    float time
) {
    // Rotate coordinates, then create pattern
}

SwiftUI Animation:

struct RotatingPatternView: View {
    @State private var time: Float = 0
    
    var body: some View {
        Rectangle()
            .visualEffect { content, proxy in
                content.colorEffect(ShaderLibrary.rotatingPattern(
                    .float2(proxy.size),
                    .float(time)
                ))
            }
            .onAppear {
                Timer.scheduledTimer(withTimeInterval: 1/60.0, repeats: true) { _ in
                    time += 1/60.0
                }
            }
    }
}

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 3:

  1. What does position / size accomplish?

Transforms pixel coordinates (0 to width, 0 to height) into UV coordinates (0 to 1, 0 to 1). This creates normalized coordinates that work consistently across different screen sizes, so patterns look the same on iPhone and iPad.

  1. What are the UV coordinates of the center of any screen?

(0.5, 0.5) - always, regardless of screen size or aspect ratio.

  1. How do you create a smooth circle edge?

Use smoothstep() instead of hard comparison: smoothstep(0.29, 0.31, dist) creates a smooth transition between the two values, eliminating jagged edges.

  1. Why use length(uv - 0.5) for distance from center?

uv - 0.5 centers the coordinate system at (0,0), then length() calculates the Euclidean distance from that centered origin to the current pixel position.

  1. What's the difference between step() and smoothstep()?

step() creates a hard binary transition (0 or 1), while smoothstep() creates a smooth interpolated transition between two threshold values, which prevents aliasing and jagged edges.

Essential Functions to Remember

// Coordinate transformation
float2 uv = position / size;

// Distance from center
float dist = length(uv - 0.5);

// Smooth transitions
float smooth = smoothstep(edge1, edge2, value);

// Grid creation
float2 grid = fract(uv * gridSize);

// Rotation
float2 rotated = float2(
    uv.x * cos(angle) - uv.y * sin(angle),
    uv.x * sin(angle) + uv.y * cos(angle)
);

Next Chapter Preview: You can create patterns and shapes, but they're all grayscale. Chapter 3 reveals how to think about color mathematically - not as "red, green, blue" but as vectors in 3D space that you can rotate, scale, and transform. You'll learn why Instagram filters work and how to create professional color effects.