290 lines
13 KiB
Markdown
290 lines
13 KiB
Markdown
# 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:
|
|
```bash
|
|
./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)
|
|
```bash
|
|
mkdir build && cd build
|
|
cmake ..
|
|
cmake --build . --config Release
|
|
```
|
|
|
|
### Building (Makefile - Release Packages)
|
|
```bash
|
|
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:
|
|
```bash
|
|
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:
|
|
```bash
|
|
cmake --build build --target compile_shaders
|
|
```
|
|
Requires `glslc` (Debian/Ubuntu: `apt install glslang-tools`; macOS: `brew install glslang`).
|
|
|
|
### Running the Application
|
|
```bash
|
|
./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)
|
|
```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:
|
|
|
|
```glsl
|
|
#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:**
|
|
```glsl
|
|
// 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. |