Files
shadertoy/CLAUDE.md

13 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. It supports two rendering backends:

  • SDL3 GPU (default): renders via Vulkan on Linux/Windows and Metal on macOS.
  • OpenGL 3.3 (fallback): the original backend, used automatically if SDL3 GPU init fails.

Selection at launch:

./shadertoy --backend=gpu     # force SDL3 GPU
./shadertoy --backend=opengl  # force OpenGL
./shadertoy --backend=auto    # default: GPU first, fall back to OpenGL on failure

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

Compiling Shaders to SPIR-V

The SDL3 GPU backend reads compiled SPIR-V (.frag.spv) at runtime. Run this after touching any .vk.glsl file:

cmake --build build --target compile_shaders

Requires glslc (Debian/Ubuntu: apt install glslang-tools; macOS: brew install glslang).

Running the Application

./shadertoy [SHADER_NAME_OR_PATH] [-F|--fullscreen] [--backend=auto|gpu|opengl]

# Examples:
./shadertoy test                            # auto backend, shaders/test/
./shadertoy --backend=gpu seascape          # force SDL3 GPU (Vulkan/Metal)
./shadertoy -F shaders/fractal_pyramid      # explicit folder path, fullscreen

Runtime Controls:

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

Architecture

Layered design

  • src/main.cpp — SDL3 window, event loop, shader-list scanning, dispatch to a backend.
  • src/rendering/shader_backend.hpp — abstract IShaderBackend interface + ShaderMetadata / ShaderUniforms / ShaderProgramSpec shared types. Two factories: makeOpenGLBackend(), makeSdl3GpuBackend().
  • src/rendering/opengl_shader_backend.{hpp,cpp} — OpenGL 3.3 implementation. Loads <folder>/<name>.gl.glsl at runtime.
  • src/rendering/sdl3gpu/sdl3gpu_shader_backend.{hpp,cpp} — SDL3 GPU implementation. Loads <folder>/<name>.frag.spv (Linux/Windows) or <folder>/<name>.frag.msl (macOS); shares one passthrough vertex shader from shaders/_common/passthrough.vert.{spv,msl}.
  • src/rendering/sdl3gpu/shader_factory.hpp — small helper that loads a shader binary/source from disk and creates an SDL_GPUShader.

Shader folder layout

One folder per shader, under shaders/:

shaders/<name>/
    <name>.gl.glsl    # OpenGL GLSL 3.30 (handwritten)
    <name>.vk.glsl    # Vulkan GLSL 4.50 (handwritten)
    <name>.frag.msl   # Metal Shading Language (handwritten)
    <name>.frag.spv   # generated by `make compile_shaders` from .vk.glsl
    meta.txt          # Name / Author / iChannelN
shaders/_common/
    passthrough.vk.glsl    # shared fullscreen-triangle vertex (Vulkan)
    passthrough.vert.spv   # generated
    passthrough.vert.msl   # handwritten Metal

Folders starting with _ or . are skipped by the scanner. The meta.txt parser is in Rendering::parseMetaFile.

Backend selection logic (main.cpp)

  1. Parse --backend=... flag (default auto).
  2. If not OpenGL: create a window with no flags, instantiate Sdl3GpuShaderBackend. If init() fails AND choice was auto, destroy the window and continue. If choice was gpu, exit with error.
  3. Otherwise: create a window with SDL_WINDOW_OPENGL and instantiate OpenGLShaderBackend.

Global State (main.cpp)

shader_list_           // Vector<ShaderEntry { folder, base_name }>
current_shader_index_  // Active shader in rotation
shader_start_ticks_    // Base time for iTime uniform calculation
window_                // SDL3 window pointer
backend_               // unique_ptr<IShaderBackend>
shaders_directory_     // Shaders root path (resolved at startup)

Dependencies

  • SDL3 ≥ 3.2 — windowing, events, GPU API (SDL_gpu.h)
  • GLAD — OpenGL 3.3 loader (used only by the OpenGL backend, statically linked via third_party/glad/)
  • C++17 stdlib — filesystem, fstream, vector, algorithms
  • Build-time tool: glslc (only required to regenerate .frag.spv)

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

Authoring a shader (manual steps)

For each new shader, three handwritten files live in shaders/<name>/:

  1. <name>.gl.glsl — OpenGL GLSL 3.30 (the original Shadertoy-style format described below).
  2. <name>.vk.glsl — Vulkan GLSL 4.50 with explicit layout(set=3, binding=0) uniform ShadertoyUBO { float iTime; vec2 iResolution; } u; and layout(set=2, binding=N) uniform sampler2D for any texture channel.
  3. <name>.frag.msl — Metal Shading Language. Entry point must be named <name>_fs. Uniforms come in via [[buffer(0)]]; samplers via [[texture(N)]] [[sampler(N)]].

After editing any .vk.glsl, rebuild SPIR-V: cmake --build build --target compile_shaders.

The MSL is not auto-generated — write it by hand following the same convention as aee_2026/source/core/rendering/sdl3gpu/msl/*.msl.h.

Shadertoy → Vulkan/Metal porting cheatsheet

  • iResolution is vec2 here (no .xy). Reconstruct vec3 manually if needed.
  • Replace texture(iChannel0, ...) calls — no texture channels currently wired through the GPU backend (planned).
  • The shared vertex stage emits vUV already in Shadertoy convention (0,0) bottom-left → (1,1) top-right; multiply by iResolution to get fragCoord.
  • The Y axis is flipped in NDC inside the shared vertex shader so Vulkan/Metal render right-side-up like OpenGL.

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.