Basic Shader Syntax

Learning Objectives: By the end of this lesson, you will be able to:

  • Write your first functional 2D shader from scratch
  • Understand the fundamental structure and syntax of shader languages
  • Navigate the shader pipeline for 2D graphics effectively
  • Implement simple color manipulation techniques

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about learning a new spoken language - you start with basic vocabulary and grammar before forming complex sentences. Shader programming is similar! Today we’ll learn the “vocabulary” and “grammar” of shader languages. Have you ever tried to read shader code online and felt overwhelmed by the syntax? By the end of today, those mysterious symbols and keywords will start making perfect sense!

Lesson Overview

  • Duration: 90 minutes
  • Prerequisites: Completion of “Introduction to 2D Shaders” lesson, basic programming concepts
  • Materials Needed: Shader development environment (Unity, Godot, or web-based editor), text editor

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Shader Language Fundamentals - GLSL syntax, data types, and structure
  2. Writing Your First Shader - Step-by-step shader creation process
  3. Understanding the Shader Pipeline - How vertex and fragment shaders work together
  4. Color Manipulation Basics - Mathematical operations on colors
  5. Variables and Functions - Building blocks for shader logic

Success Criteria:

By the end of this session, you should be able to write a complete shader program, understand its structure, and modify colors using mathematical operations.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Shader Language Structure (GLSL)

GLSL (OpenGL Shading Language) is the most common shader language for 2D graphics. It’s similar to C in syntax but designed specifically for parallel GPU execution.

Real-World Analogy: Think of GLSL as a specialized language for giving instructions to an army of artists (GPU cores). Unlike giving instructions to one person (CPU), you’re giving the same instructions to thousands of workers simultaneously.

Why This Matters: Understanding GLSL syntax is crucial because:

  • It’s the foundation for all shader programming
  • The syntax is consistent across different platforms
  • Mastering the basics makes advanced techniques accessible
  • It enables you to read and modify existing shaders

Data Types in Shaders

Shaders work with specific data types optimized for graphics operations:

Scalar Types:

  • float: Single floating-point number (e.g., 1.5, 0.0, -2.3)
  • int: Integer number (e.g., 1, 42, -5)
  • bool: Boolean value (true or false)

Vector Types:

  • vec2: Two-component vector (x, y) - often used for UV coordinates
  • vec3: Three-component vector (x, y, z) - often used for RGB colors
  • vec4: Four-component vector (x, y, z, w) - often used for RGBA colors

Key Terminology

  • Precision Qualifier: Specifies the numerical precision for variables (highp, mediump, lowp)
  • Uniform: A variable that remains constant for all pixels in a single draw call
  • Varying: A variable that interpolates smoothly across a surface
  • Attribute: Input data from the vertex buffer (vertices, UV coordinates, etc.)
  • Built-in Variables: Pre-defined variables like gl_FragColor, gl_Position
  • Swizzling: Accessing vector components in different orders (e.g., color.rgb, position.xy)

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Shader Structure

Every shader program consists of specific sections that must appear in order:

// 1. Version declaration (optional but recommended)
#version 330 core

// 2. Precision qualifiers
precision mediump float;

// 3. Input/Output declarations
varying vec2 v_uv;
uniform float u_time;
uniform vec2 u_resolution;

// 4. Function definitions (optional)
float random(vec2 st) {
    return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

// 5. Main function (required)
void main() {
    // Shader logic goes here
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Variable Declarations and Scope

Global Variables:

// These can be accessed from any function
uniform float u_time;        // Passed from application
varying vec2 v_uv;          // Interpolated from vertex shader
const float PI = 3.14159;   // Constant value

Local Variables:

void main() {
    // These only exist within the main function
    float intensity = 0.5;
    vec3 color = vec3(1.0, 0.0, 0.0);
    vec2 position = v_uv * 2.0 - 1.0;
}

Mathematical Operations

Shaders excel at mathematical operations, which are essential for color manipulation:

// Basic arithmetic
float a = 1.0 + 2.0;        // Addition
float b = 3.0 * 4.0;        // Multiplication
float c = 10.0 / 2.0;       // Division
float d = 7.0 - 3.0;        // Subtraction

// Vector operations
vec3 color1 = vec3(1.0, 0.5, 0.0);
vec3 color2 = vec3(0.0, 0.5, 1.0);
vec3 mixed = color1 + color2;        // Component-wise addition
vec3 scaled = color1 * 2.0;          // Scalar multiplication

Practical Applications

Color Manipulation:

  • Brightness adjustment: color * brightness_factor
  • Contrast enhancement: (color - 0.5) * contrast + 0.5
  • Color mixing: mix(color1, color2, blend_factor)
  • Saturation control: mix(grayscale, original_color, saturation)

Coordinate Transformations:

  • Centering UV coordinates: uv * 2.0 - 1.0
  • Scaling: uv * scale_factor
  • Rotation: Using rotation matrices with trigonometric functions

Common Misconceptions

  1. Misconception: “Shader variables work like regular programming variables” Reality: Shader variables represent values across many pixels simultaneously; operations happen in parallel

  2. Misconception: “You can use loops and conditions freely in shaders” Reality: Branches and loops can severely impact performance; use them carefully

  3. Misconception: “Precision qualifiers don’t matter” Reality: Precision affects both performance and accuracy, especially on mobile devices

5. Live Coding Demonstration (20-25 minutes)

Example 1: Basic Shader Structure

// Complete basic fragment shader
precision mediump float;

// Input from vertex shader
varying vec2 v_uv;

// Uniform inputs from application
uniform float u_time;

void main() {
    // Start with UV coordinates (0.0 to 1.0)
    vec2 uv = v_uv;
    
    // Create a simple color based on position
    vec3 color = vec3(uv.x, uv.y, 0.5);
    
    // Output the final color
    gl_FragColor = vec4(color, 1.0);
}

Explanation:

  • Line 4: varying vec2 v_uv receives interpolated UV coordinates from the vertex shader
  • Line 7: uniform float u_time receives time value from the application for animations
  • Line 12: vec3 color = vec3(uv.x, uv.y, 0.5) creates RGB color where red increases left-to-right, green increases bottom-to-top
  • Line 15: gl_FragColor = vec4(color, 1.0) outputs the final RGBA color

Example 2: Color Manipulation Functions

precision mediump float;

varying vec2 v_uv;
uniform float u_brightness;
uniform float u_contrast;

// Function to adjust brightness
vec3 adjustBrightness(vec3 color, float brightness) {
    return color * brightness;
}

// Function to adjust contrast
vec3 adjustContrast(vec3 color, float contrast) {
    return (color - 0.5) * contrast + 0.5;
}

void main() {
    // Base color gradient
    vec3 color = vec3(v_uv.x, v_uv.y, 0.5);
    
    // Apply brightness adjustment
    color = adjustBrightness(color, u_brightness);
    
    // Apply contrast adjustment
    color = adjustContrast(color, u_contrast);
    
    gl_FragColor = vec4(color, 1.0);
}

Example 3: Vector Swizzling and Built-in Functions

precision mediump float;

varying vec2 v_uv;
uniform float u_time;

void main() {
    vec2 uv = v_uv;
    
    // Vector swizzling examples
    vec3 color1 = vec3(uv.x, uv.y, 0.0);      // Explicit component access
    vec3 color2 = vec3(uv.xy, 0.0);           // Swizzling xy components
    vec3 color3 = vec3(uv.yx, 0.0);           // Swapped x and y
    
    // Built-in function examples
    float dist = distance(uv, vec2(0.5, 0.5)); // Distance from center
    float pulse = sin(u_time * 2.0) * 0.5 + 0.5; // Sine wave animation
    
    // Combine effects
    vec3 finalColor = mix(color1, color2, pulse);
    finalColor *= smoothstep(0.3, 0.1, dist); // Circular vignette
    
    gl_FragColor = vec4(finalColor, 1.0);
}

Debugging Demonstration

// Common mistake: Uninitialized variables
precision mediump float;
varying vec2 v_uv;

void main() {
    vec3 color; // Uninitialized! Will contain garbage values
    gl_FragColor = vec4(color, 1.0);
}

// Corrected version
precision mediump float;
varying vec2 v_uv;

void main() {
    vec3 color = vec3(0.0); // Initialize to black
    color = vec3(v_uv.x, v_uv.y, 0.5);
    gl_FragColor = vec4(color, 1.0);
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Your First Complete Shader (15 minutes)

Objective: Write a shader that creates a diagonal gradient with animated colors

Instructions:

  1. Create a new fragment shader file
  2. Set up the basic structure with precision and main function
  3. Create a diagonal gradient using UV coordinates
  4. Add time-based animation to shift colors

Starter Code:

precision mediump float;

varying vec2 v_uv;
uniform float u_time;

void main() {
    // Your code here
    vec2 uv = v_uv;
    
    // Create diagonal gradient (hint: use uv.x + uv.y)
    
    // Add time-based animation
    
    gl_FragColor = vec4(/* your color here */, 1.0);
}

Solution:

precision mediump float;

varying vec2 v_uv;
uniform float u_time;

void main() {
    vec2 uv = v_uv;
    
    // Diagonal gradient
    float gradient = (uv.x + uv.y) * 0.5;
    
    // Animated color shift
    float timeShift = sin(u_time) * 0.5 + 0.5;
    
    vec3 color = vec3(gradient + timeShift, gradient, 1.0 - gradient);
    gl_FragColor = vec4(color, 1.0);
}

Exercise 2: Color Function Library (15 minutes)

Objective: Create reusable functions for color manipulation

Requirements:

  • Write a function to convert RGB to grayscale
  • Write a function to invert colors
  • Write a function to adjust saturation
  • Apply these functions to a test pattern

Hints:

  • Grayscale: 0.299*r + 0.587*g + 0.114*b (luminance formula)
  • Invert: 1.0 - original_color
  • Saturation: Mix between grayscale and original color

Extension Ideas:

  • Add a hue shift function
  • Create a sepia tone effect
  • Implement color temperature adjustment

Exercise 3: Interactive Shader Parameters (10 minutes)

Objective: Create a shader that responds to multiple uniform inputs

Challenge: Build a shader that accepts the following uniforms:

  • u_brightness (float): Controls overall brightness
  • u_contrast (float): Controls contrast
  • u_hue_shift (float): Shifts colors around the color wheel
  • u_mouse_pos (vec2): Mouse position for interactive effects

Advanced Challenge: Make the mouse position affect the color intensity in a circular area around the cursor.

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Compilation errors with unclear error messages Cause: Missing semicolons, incorrect function signatures, or undeclared variables Solution: Check syntax carefully; GLSL is strict about punctuation and declarations

  2. Problem: Colors appear clamped or unexpected Cause: Values outside the 0.0-1.0 range Solution: Use clamp(value, 0.0, 1.0) or ensure calculations stay within bounds

  3. Problem: Performance issues on mobile devices Cause: Using high precision when medium precision would suffice Solution: Use appropriate precision qualifiers for each variable

Debugging Tips

  • Visualize intermediate values: Use color channels to debug calculations: gl_FragColor = vec4(debug_value, 0.0, 0.0, 1.0);
  • Start simple: Begin with basic colors and gradually add complexity
  • Use step-by-step approach: Comment out complex parts and add them back one at a time
  • Check uniform values: Ensure uniforms are being passed correctly from your application

Shader Compilation Process

Understanding how shaders are compiled helps with debugging:

  1. Syntax checking: GLSL compiler checks for proper syntax
  2. Type checking: Ensures variable types match function signatures
  3. Optimization: Compiler optimizes the code for GPU execution
  4. Linking: Vertex and fragment shaders are linked together

8. Best Practices and Professional Tips (5 minutes)

Code Organization

// Good organization example
precision mediump float;

// === UNIFORMS ===
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;

// === VARYINGS ===
varying vec2 v_uv;

// === CONSTANTS ===
const float PI = 3.14159;
const float TAU = 6.28318;

// === UTILITY FUNCTIONS ===
float random(vec2 st) {
    return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

// === MAIN FUNCTION ===
void main() {
    // Main shader logic
}

Performance Optimization

  • Use appropriate precision: lowp for simple operations, mediump for most cases, highp only when necessary
  • Minimize texture lookups: Cache texture samples in variables
  • Avoid conditional branches: Use mathematical functions instead of if/else when possible
  • Use built-in functions: Functions like mix(), step(), smoothstep() are highly optimized

Version Control and Documentation

/*
 * Gradient Shader v1.2
 * Author: Your Name
 * Created: 2025-06-02
 * 
 * Description: Creates an animated diagonal gradient with color shifting
 * 
 * Uniforms:
 * - u_time: Time value for animation
 * - u_brightness: Overall brightness multiplier (0.0 - 2.0)
 */

9. Summary and Review (5 minutes)

Key Takeaways

  1. GLSL structure is predictable: Precision declarations, variable declarations, functions, then main()
  2. Vector operations are powerful: Swizzling and component-wise operations make color manipulation elegant
  3. Functions improve organization: Reusable functions make shaders more maintainable and readable
  4. Precision matters: Choose appropriate precision for performance and accuracy balance

Quick Review Questions

  1. What is the purpose of the precision mediump float; declaration?
  2. How do you access the red and green components of a vec3 color variable named rgb?
  3. What’s the difference between uniform and varying variables?
  4. Why should you be careful with conditional statements in shaders?

Answers:

  1. Specifies the floating-point precision for better performance and mobile compatibility
  2. rgb.r and rgb.g or rgb.x and rgb.y
  3. uniform values are constant across all pixels in a draw call; varying values interpolate across the surface
  4. Conditional statements can cause performance issues due to GPU architecture (branching)

For Instructors

Teaching Tips

  • Live code everything: Students learn better by seeing the development process
  • Encourage experimentation: Let students modify values and see immediate results
  • Use visual debugging: Show how to use color channels to visualize data
  • Connect to math concepts: Relate shader operations to familiar mathematical concepts

Common Student Questions

  • “Why do some tutorials use different syntax?” Different GLSL versions and platforms may have slight variations
  • “How do I know what precision to use?” Start with mediump and adjust based on visual quality and performance needs
  • “Can I use regular programming constructs?” Most are available, but with performance considerations

Assessment Ideas

  • Code review sessions: Students present their shaders and explain the logic
  • Debugging exercises: Provide broken shaders for students to fix
  • Creative challenges: “Create a shader that represents your mood today”

Adaptation Notes

  • For visual learners: Use lots of color examples and visual effects
  • For mathematical minds: Emphasize the mathematical beauty of vector operations
  • Online delivery: Ensure students can share their screens to show shader results
  • Time management: Theory (30 min), live coding (25 min), practice (25 min), review (10 min)