Files
shadertoy/CLAUDE.md
Sergio Valor 44de2c7013 Add FPS counter, VSync toggle, shader metadata system, and multi-pass infrastructure
- FPS counter in window title (updates every 500ms)
- F4 key toggles VSync on/off
- Shader metadata: Name and Author from comments
- iChannel metadata parsing for multi-pass support
- Base structures: ShaderBuffer, ShaderPass
- FBO/texture management functions
- Updated all 11 shaders with Name/Author metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 15:22:06 +01:00

9.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This is a cross-platform Shadertoy-like fragment shader viewer built with SDL3 + OpenGL 3.3. The application loads and displays GLSL shaders from the shaders/ directory with runtime switching capabilities.

Build and Development Commands

Building (CMake - Development)

mkdir build && cd build
cmake ..
cmake --build . --config Release

Building (Makefile - Release Packages)

make windows_release   # Creates .zip with DLLs
make macos_release     # Creates .dmg with app bundle
make linux_release     # Creates .tar.gz
make show_version      # Display build version (YYYY-MM-DD format)

Platform-specific debug builds:

make windows_debug
make macos_debug
make linux_debug

Running the Application

./shadertoy [SHADER_PATH] [-F|--fullscreen]

# Examples:
./shadertoy shaders/test.frag.glsl
./shadertoy -F shaders/fractal_pyramid.frag.glsl

Runtime Controls:

  • ESC - Exit
  • F3 - Toggle fullscreen
  • LEFT/RIGHT ARROW - Cycle through shaders in directory

Architecture

Core Design

All application logic resides in src/main.cpp (~469 lines) - a monolithic design that's straightforward to understand. Key components:

  1. Shader Loading System - Automatic directory scanning of .glsl files, sorted alphabetically
  2. OpenGL Rendering - Single fullscreen quad with fragment shader using GL_TRIANGLE_STRIP
  3. Event Loop - SDL3-based with vsync (SDL_GL_SwapWindow + 1ms delay)
  4. Resource Path Resolution - Multi-path fallback system for executable, relative, and macOS bundle paths

Global State (main.cpp)

shader_list_           // Vector of discovered .glsl shader paths
current_shader_index_  // Active shader in rotation
current_program_       // OpenGL shader program handle
shader_start_ticks_    // Base time for iTime uniform calculation
window_                // SDL3 window pointer
shaders_directory_     // Shader directory path (resolved at startup)

Dependencies

  • SDL3 - Window/input management, OpenGL context
  • GLAD - OpenGL 3.3 loader (statically linked via third_party/glad/)
  • C++17 stdlib - filesystem, fstream, vector, algorithms

Platform-Specific Code

Uses preprocessor defines (WINDOWS_BUILD, MACOS_BUILD, LINUX_BUILD) for:

  • getExecutableDirectory() - Windows API, mach-o dyld, or /proc/self/exe
  • getResourcesDirectory() - Special macOS app bundle handling (Contents/Resources)
  • Linking flags - Windows uses static linking, macOS links OpenGL framework

Shader System

Shader Format (GLSL 3.3 Core)

All shaders must follow this structure:

#version 330 core
precision highp float;

out vec4 FragColor;
in vec2 vUV;              // Normalized [0,1] coordinates from vertex shader
uniform vec2 iResolution; // Window resolution in pixels
uniform float iTime;      // Time since shader loaded (seconds)

// Shadertoy-style entry point
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // fragCoord is in pixel coordinates
    // Your shader code here
}

// Wrapper converts vUV to pixel coordinates
void main() {
    vec2 fragCoordPixels = vUV * iResolution;
    vec4 outColor;
    mainImage(outColor, fragCoordPixels);
    FragColor = outColor;
}

Adding New Shaders

  1. Create .glsl file in shaders/ directory (use .frag.glsl convention)
  2. Follow required format above
  3. File automatically appears in runtime shader rotation (arrow keys to navigate)
  4. No code changes required - directory is scanned at startup

Shader Loading Pipeline

  1. Directory scan on startup (scanShaderDirectory())
  2. Sort alphabetically
  3. Load fragment shader source from disk
  4. Compile vertex shader (fixed, embedded in main.cpp)
  5. Compile fragment shader with error logging
  6. Link program with error handling
  7. Update uniforms each frame (iResolution, iTime)

Supported Shadertoy Features

  • iTime - Time uniform
  • iResolution - Window resolution (vec2, not vec3)
  • mainImage() function signature
  • iMouse - Not implemented
  • iChannel0-3 - No texture channels (multi-pass not supported)
  • iFrame, iTimeDelta, iDate - Not implemented

Converting from Shadertoy

When porting Shadertoy shaders:

  1. Copy the mainImage() function as-is
  2. Add standard header (see format above)
  3. Add wrapper main() function
  4. Remove texture channel references (iChannel0-3)
  5. Change iResolution.xy to iResolution (this project uses vec2)

Common Conversion Issues and Solutions

Based on experience converting complex shaders like ddla_light_tunnel:

iResolution vec3 vs vec2

  • Shadertoy: iResolution is vec3(width, height, width/height)
  • This project: iResolution is vec2(width, height)
  • Solution: Create vec3 manually: vec3 r = vec3(iResolution.xy, iResolution.x/iResolution.y);
  • Then use: r.xy for resolution, r.y for height (as in original)

Uninitialized Variables

  • Problem: Shadertoy code may have vec3 rgb; without initialization
  • Shadertoy behavior: Likely initializes to vec3(0.0) (black)
  • This project: Uninitialized variables contain undefined values (often causes black screen or wrong colors)
  • Solution: Always initialize: vec3 rgb = vec3(0.0);
  • Wrong approach: Don't use random noise unless shader explicitly uses iChannel texture for noise

Low mix() Factors Are Intentional

  • Example: rgb = mix(rgb, calculated_color, 0.01); means 1% new color, 99% existing
  • Don't change these factors - they create subtle effects intentionally
  • If output is black: Problem is likely the base value (rgb), not the mix factor

iChannel Textures

  • Shadertoy shows iChannel0-3 in UI, but shader may not use them
  • If iChannel3 has RGB noise but code doesn't reference it: The noise is not actually used
  • Check code for texture(iChannelN, ...) calls - if none exist, ignore the iChannel setup
  • Procedural noise replacement: Only if shader explicitly samples the texture

Color Swapping Issues

  • If colors are completely wrong: Don't randomly swap color variables
  • First verify: Code matches original Shadertoy exactly (except required GLSL changes)
  • Common mistake: Changing color applications that were correct in original
  • Debug approach: Revert to exact original code, only modify for GLSL 3.3 compatibility

Division by Small Values - Color Overflow Artifacts

  • Problem: Division by values near zero causes extreme values → color overflow artifacts (green points, strange colors)
  • Example: .01*vec4(6,2,1,0)/length(u*sin(iTime)) can divide by ~0.0 when near center and sin≈0
  • Symptoms: Bright white center that pulses to green, or random color artifacts in specific areas
  • Root cause: Even with tonemap (tanh/clamp), extreme intermediate values cause precision issues or saturation
  • Solution - Double Protection:
    1. Add epsilon to denominator: max(denominator, 0.001) prevents division by exact zero
    2. Clamp the result: min(result, vec4(50.0)) prevents extreme accumulation
    3. Only clamp problematic terms: Don't clamp everything or scene becomes dim
  • Example fix:
    // Original (causes overflow):
    o += .01*vec4(6,2,1,0)/length(u*sin(t+t+t)) + 1./s * length(u);
    
    // Fixed (prevents overflow while maintaining brightness):
    vec4 brightTerm = min(.01*vec4(6,2,1,0)/max(length(u*sin(t+t+t)), 0.001), vec4(50.0));
    o += brightTerm + 1./s * length(u);
    
  • Key values: epsilon ~0.001 (not too small, not too large), clamp ~50.0 (allows brightness without explosion)
  • Why this works in Shadertoy: WebGL/browsers may handle float overflow differently than native OpenGL drivers

Debugging Black/Wrong Output

  1. Check compilation errors first - shader must compile without errors
  2. Initialize all variables - especially vec3 colors
  3. Verify vec3 iResolution handling - create vec3 from vec2 if needed
  4. Don't modify mix factors - keep original values
  5. Compare with original code - ensure logic is identical
  6. Test progressive changes - add header, test; add wrapper, test; etc.
  7. Check for division by small values - if you see color artifacts (especially green), look for divisions that can approach zero

Build System Details

Version Numbering

Releases use build date format: shadertoy-YYYY-MM-DD-{platform} (auto-generated by Makefile)

Release Artifacts

  • Windows: .zip with shadertoy.exe + SDL3.dll + shaders
  • macOS: .dmg with app bundle containing embedded SDL3.framework (arm64 only)
  • Linux: .tar.gz with binary + shaders

Resource Bundling

  • Shaders copied to release packages
  • LICENSE and README.md included
  • Platform-specific dependencies bundled (DLLs on Windows, frameworks on macOS)

Important Notes

Vertex Shader

The vertex shader is hardcoded in main.cpp and creates a fullscreen quad. It:

  • Takes vec2 aPos (location 0) in NDC space [-1, 1]
  • Outputs vec2 vUV normalized to [0, 1]
  • No transformation matrices needed

Shader Hot-Reloading

Currently not implemented. Shader changes require application restart. The architecture would support adding this via file watching.

Multi-Pass Rendering

Single-pass only. To add multi-pass (BufferA/B/C like Shadertoy):

  • Create FBOs and textures per buffer
  • Render buffers in dependency order
  • Pass textures as iChannel0-3 uniforms
  • Use ping-pong for feedback loops

OpenGL Context

Created via SDL3 with core profile (no deprecated functions). Context version: 3.3 core.