summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/shadertoy/README.md204
-rwxr-xr-xtools/shadertoy/convert_shadertoy.py399
-rw-r--r--tools/shadertoy/example.txt25
-rw-r--r--tools/shadertoy/template.cc120
-rw-r--r--tools/shadertoy/template.h41
-rw-r--r--tools/shadertoy/template.wgsl90
6 files changed, 879 insertions, 0 deletions
diff --git a/tools/shadertoy/README.md b/tools/shadertoy/README.md
new file mode 100644
index 0000000..283a65f
--- /dev/null
+++ b/tools/shadertoy/README.md
@@ -0,0 +1,204 @@
+# ShaderToy Conversion Guide
+
+Quick guide to convert ShaderToy shaders to demo effects.
+
+**For complete workflow:** See `doc/EFFECT_WORKFLOW.md` for full integration checklist.
+
+## Quick Start (Automated)
+
+```bash
+# Save ShaderToy code to a file
+cat > tunnel.txt << 'EOF'
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 uv = fragCoord / iResolution.xy;
+ vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4));
+ fragColor = vec4(col, 1.0);
+}
+EOF
+
+# Generate effect files
+./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel
+
+# Regenerate only shader (if .h/.cc already exist)
+./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel --shader-only
+
+# Follow printed instructions to integrate
+```
+
+## Files
+
+**Automated Script:**
+- `convert_shadertoy.py` - Generates all files from ShaderToy code
+- `example.txt` - Example ShaderToy shader for testing
+
+**Manual Templates:**
+- `template.h` - Header boilerplate
+- `template.cc` - Implementation boilerplate
+- `template.wgsl` - Shader boilerplate with conversion notes
+
+## Manual Steps
+
+### 1. Copy Templates
+
+```bash
+# Choose effect name (e.g., "tunnel", "plasma", "warp")
+EFFECT_NAME="myeffect"
+
+cp tools/shadertoy/template.h src/gpu/effects/${EFFECT_NAME}_effect.h
+cp tools/shadertoy/template.cc src/gpu/effects/${EFFECT_NAME}_effect.cc
+cp tools/shadertoy/template.wgsl workspaces/main/shaders/${EFFECT_NAME}.wgsl
+```
+
+### 2. Rename Class
+
+In both `.h` and `.cc`:
+- `ShaderToyEffect` → `MyEffectEffect`
+- `SHADERTOY_EFFECT_H_` → `MYEFFECT_EFFECT_H_`
+- `shadertoy_effect.h` → `myeffect_effect.h`
+
+### 3. Convert Shader
+
+In `.wgsl`, paste ShaderToy `mainImage()` into `fs_main()`:
+
+**ShaderToy:**
+```glsl
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 uv = fragCoord / iResolution.xy;
+ fragColor = vec4(uv, 0.5, 1.0);
+}
+```
+
+**WGSL:**
+```wgsl
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ let uv = p.xy / uniforms.resolution;
+ return vec4<f32>(uv, 0.5, 1.0);
+}
+```
+
+### 4. Update Asset Name
+
+In `.cc`, update `AssetId::ASSET_SHADERTOY_SHADER` to match your shader filename:
+```cpp
+AssetId::ASSET_MYEFFECT_SHADER
+```
+
+### 5. Add to Assets
+
+In `workspaces/main/assets.txt`:
+```
+shaders/myeffect.wgsl
+```
+
+### 6. Register Effect
+
+In `src/gpu/demo_effects.h`:
+```cpp
+#include "gpu/effects/myeffect_effect.h"
+```
+
+In `workspaces/main/timeline.seq`:
+```
+SEQUENCE 0.0 0
+ EFFECT + MyEffectEffect 0.0 10.0
+```
+
+### 7. Update CMakeLists.txt
+
+Add effect source to `CMakeLists.txt` GPU_SOURCES (both headless and normal mode sections):
+```cmake
+src/gpu/effects/myeffect_effect.cc
+```
+
+### 8. Update Tests
+
+In `src/tests/gpu/test_demo_effects.cc`:
+- Add to `post_process_effects` list (lines 80-93) if it's a post-process effect
+- OR add to `scene_effects` list (lines 125-137) if it's a scene effect
+- Example: `{"MyEffectEffect", std::make_shared<MyEffectEffect>(fixture.ctx())},`
+
+### 9. Build & Test
+
+```bash
+cmake --build build -j4
+./build/demo64k
+
+# Run tests
+cmake -S . -B build -DDEMO_BUILD_TESTS=ON
+cmake --build build -j4
+cd build && ctest
+```
+
+## Example Conversion
+
+**Input ShaderToy:**
+```glsl
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 uv = fragCoord / iResolution.xy;
+ vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4));
+ fragColor = vec4(col, 1.0);
+}
+```
+
+**Generated WGSL (after script + manual fixes):**
+```wgsl
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ let uv = p.xy / uniforms.resolution;
+ let col = vec3<f32>(0.5) + 0.5 * cos(uniforms.time + uv.xyx + vec3<f32>(0.0, 2.0, 4.0));
+ return vec4<f32>(col, 1.0);
+}
+```
+
+## Common Conversions
+
+| ShaderToy | WGSL |
+|-----------|------|
+| `iResolution.xy` | `uniforms.resolution` |
+| `iTime` | `uniforms.time` |
+| `fragCoord` | `p.xy` |
+| `float` | `f32` |
+| `vec2` | `vec2<f32>` |
+| `mod(x, y)` | `x % y` |
+| `texture(iChannel0, uv)` | `textureSample(txt, smplr, uv)` |
+| `fragColor = ...` | `return ...` |
+| `vec2 p = ...` | `let p = vec2<f32>(...)` or `var p: vec2<f32> = ...` |
+
+## Custom Parameters
+
+For tunable values:
+
+**C++ (`.h`):**
+```cpp
+struct MyEffectParams {
+ float speed;
+ float scale;
+ float _pad[2];
+};
+static_assert(sizeof(MyEffectParams) == 16, "...");
+```
+
+**WGSL:**
+```wgsl
+struct MyEffectParams {
+ speed: f32,
+ scale: f32,
+ _pad0: f32,
+ _pad1: f32,
+}
+@group(0) @binding(3) var<uniform> params: MyEffectParams;
+```
+
+## Available Uniforms
+
+Always available in `uniforms: CommonUniforms`:
+- `resolution: vec2<f32>` - Screen resolution
+- `aspect_ratio: f32` - Width/height
+- `time: f32` - Demo time (seconds)
+- `beat: f32` - Music beat sync (0-1)
+- `audio_intensity: f32` - Audio reactivity
+
+## Next Steps
+
+- See `doc/CONTRIBUTING.md` for commit policy
+- See `doc/SEQUENCE.md` for timeline syntax
+- See existing effects in `src/gpu/effects/` for examples
diff --git a/tools/shadertoy/convert_shadertoy.py b/tools/shadertoy/convert_shadertoy.py
new file mode 100755
index 0000000..e85f384
--- /dev/null
+++ b/tools/shadertoy/convert_shadertoy.py
@@ -0,0 +1,399 @@
+#!/usr/bin/env python3
+# This file is part of the 64k demo project.
+# Converts ShaderToy shader to demo effect boilerplate.
+#
+# Usage:
+# ./tools/shadertoy/convert_shadertoy.py <shader.txt> <EffectName>
+#
+# Example:
+# ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel
+# ./tools/shadertoy/convert_shadertoy.py tools/shadertoy/example.txt Rainbow
+#
+# Generates:
+# - src/gpu/effects/<effect>_effect.h
+# - src/gpu/effects/<effect>_effect.cc
+# - workspaces/main/shaders/<effect>.wgsl
+#
+# The script performs basic ShaderToy→WGSL conversion:
+# - Converts types (float→f32, vec2→vec2<f32>, etc.)
+# - Converts uniforms (iTime→uniforms.time, etc.)
+# - Extracts mainImage() body into fs_main()
+# - Generates boilerplate C++ effect class
+#
+# Manual fixes usually needed:
+# - fragColor assignments → return statements
+# - Variable name conflicts (e.g., shadowing 'p')
+# - Complex type inference
+# - Texture channel mappings
+# - Helper function signatures
+
+import sys
+import os
+import re
+from pathlib import Path
+
+def to_snake_case(name):
+ """Convert CamelCase to snake_case."""
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
+
+def to_upper_snake_case(name):
+ """Convert CamelCase to UPPER_SNAKE_CASE."""
+ return to_snake_case(name).upper()
+
+def to_camel_case(name):
+ """Convert snake_case to CamelCase."""
+ return ''.join(word.capitalize() for word in name.split('_'))
+
+def convert_shadertoy_to_wgsl(shader_code):
+ """Basic ShaderToy to WGSL conversion."""
+ # Extract mainImage first
+ main_match = re.search(r'void\s+mainImage\s*\([^)]+\)\s*\{(.*)\}', shader_code, re.DOTALL)
+ if main_match:
+ main_body = main_match.group(1).strip()
+ helpers = shader_code[:main_match.start()]
+ else:
+ main_body = ""
+ helpers = shader_code
+
+ # Replace common ShaderToy defines
+ conversions = [
+ (r'#define\s+TIME\s+iTime', ''),
+ (r'#define\s+RESOLUTION\s+iResolution', ''),
+ (r'#define\s+PI\s+[\d.]+', 'const PI: f32 = 3.141592654;'),
+ (r'#define\s+TAU\s+\([^)]+\)', 'const TAU: f32 = 6.283185307;'),
+ (r'#define\s+ROT\(a\)\s+mat2\([^)]+\)', ''), # Will be converted to function
+
+ # Common ShaderToy uniforms
+ (r'\bTIME\b', 'uniforms.time'),
+ (r'\biTime\b', 'uniforms.time'),
+ (r'\bRESOLUTION\b', 'uniforms.resolution'),
+ (r'\biResolution\b', 'uniforms.resolution'),
+ (r'\bfragCoord\b', 'p.xy'),
+
+ # Type conversions
+ (r'\bfloat\b', 'f32'),
+ (r'\bvec2\b', 'vec2<f32>'),
+ (r'\bvec3\b', 'vec3<f32>'),
+ (r'\bvec4\b', 'vec4<f32>'),
+ (r'\bmat2\b', 'mat2x2<f32>'),
+ (r'\bmat3\b', 'mat3x3<f32>'),
+ (r'\bmat4\b', 'mat4x4<f32>'),
+
+ # Function declarations (preserve return type context)
+ (r'\bf32\s+(\w+)\s*\(', r'fn \1('),
+ (r'\bvec2<f32>\s+(\w+)\s*\(', r'fn \1('),
+ (r'\bvec3<f32>\s+(\w+)\s*\(', r'fn \1('),
+ (r'\bvec4<f32>\s+(\w+)\s*\(', r'fn \1('),
+ (r'\bvoid\s+(\w+)\s*\(', r'fn \1('),
+
+ # Const declarations
+ (r'\bconst\s+f32\s+(\w+)\s*=', r'const \1: f32 ='),
+ (r'\bconst\s+vec2<f32>\s+(\w+)\s*=', r'const \1 ='),
+ (r'\bconst\s+vec3<f32>\s+(\w+)\s*=', r'const \1 ='),
+ (r'\bconst\s+vec4<f32>\s+(\w+)\s*=', r'const \1 ='),
+
+ # Function calls that need fixing
+ (r'\bfract\s*\(', 'fract('),
+ (r'\bmod\s*\(([^,]+),\s*([^)]+)\)', r'(\1 % \2)'),
+ ]
+
+ converted_helpers = helpers
+ for pattern, replacement in conversions:
+ converted_helpers = re.sub(pattern, replacement, converted_helpers)
+
+ # Convert mainImage body
+ converted_main = main_body
+ for pattern, replacement in conversions:
+ converted_main = re.sub(pattern, replacement, converted_main)
+
+ # Fix fragColor assignments -> returns
+ converted_main = re.sub(r'\bfragColor\s*=\s*([^;]+);', r'return \1;', converted_main)
+
+ # Indent main body
+ indented_main = '\n'.join(' ' + line if line.strip() else '' for line in converted_main.split('\n'))
+
+ # Build fragment function with Y-flip for ShaderToy convention
+ fragment = f"""@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {{
+ // Flip Y to match ShaderToy convention (origin at bottom-left)
+ let flipped = vec2<f32>(p.x, uniforms.resolution.y - p.y);
+ let q = flipped / uniforms.resolution;
+ var coord = -1.0 + 2.0 * q;
+ coord.x *= uniforms.resolution.x / uniforms.resolution.y;
+
+{indented_main}
+}}"""
+
+ return converted_helpers + '\n\n' + fragment
+
+def extract_main_image(shader_code):
+ """Extract mainImage function body from ShaderToy code."""
+ # Try to find mainImage function
+ match = re.search(r'void\s+mainImage\s*\([^)]+\)\s*\{(.*)\}', shader_code, re.DOTALL)
+ if match:
+ return match.group(1).strip()
+
+ # If no mainImage found, return whole shader
+ return shader_code
+
+def generate_header(effect_name, is_post_process=False):
+ """Generate .h file content."""
+ class_name = f"{effect_name}Effect"
+ upper_name = to_upper_snake_case(effect_name)
+
+ if is_post_process:
+ return f"""// This file is part of the 64k demo project.
+// {effect_name} effect - ShaderToy conversion (post-process)
+// Generated by convert_shadertoy.py
+
+#ifndef {upper_name}_EFFECT_H_
+#define {upper_name}_EFFECT_H_
+
+#include "gpu/effect.h"
+#include "gpu/effects/post_process_helper.h"
+
+class {class_name} : public PostProcessEffect {{
+ public:
+ {class_name}(const GpuContext& ctx);
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+ void update_bind_group(WGPUTextureView input_view) override;
+}};
+
+#endif /* {upper_name}_EFFECT_H_ */
+"""
+ else:
+ # Scene effect (simpler, like HeptagonEffect)
+ return f"""// This file is part of the 64k demo project.
+// {effect_name} effect - ShaderToy conversion (scene)
+// Generated by convert_shadertoy.py
+
+#ifndef {upper_name}_EFFECT_H_
+#define {upper_name}_EFFECT_H_
+
+#include "gpu/effect.h"
+
+class {class_name} : public Effect {{
+ public:
+ {class_name}(const GpuContext& ctx);
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+ private:
+ RenderPass pass_;
+}};
+
+#endif /* {upper_name}_EFFECT_H_ */
+"""
+
+def generate_implementation(effect_name, is_post_process=False):
+ """Generate .cc file content."""
+ class_name = f"{effect_name}Effect"
+ snake_name = to_snake_case(effect_name)
+
+ if is_post_process:
+ return f"""// This file is part of the 64k demo project.
+// {effect_name} effect - ShaderToy conversion (post-process)
+// Generated by convert_shadertoy.py
+
+#include "gpu/demo_effects.h"
+#include "gpu/effects/post_process_helper.h"
+#include "gpu/gpu.h"
+
+{class_name}::{class_name}(const GpuContext& ctx) : PostProcessEffect(ctx) {{
+ pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, {snake_name}_shader_wgsl);
+}}
+
+void {class_name}::render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) {{
+ const CommonPostProcessUniforms u = {{
+ .resolution = {{(float)width_, (float)height_}},
+ ._pad = {{0.0f, 0.0f}},
+ .aspect_ratio = aspect_ratio,
+ .time = time,
+ .beat = beat,
+ .audio_intensity = intensity,
+ }};
+ uniforms_.update(ctx_.queue, u);
+
+ wgpuRenderPassEncoderSetPipeline(pass, pipeline_);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
+}}
+
+void {class_name}::update_bind_group(WGPUTextureView input_view) {{
+ pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, uniforms_.get());
+}}
+"""
+ else:
+ # Scene effect (simpler pattern like HeptagonEffect)
+ return f"""// This file is part of the 64k demo project.
+// {effect_name} effect - ShaderToy conversion (scene)
+// Generated by convert_shadertoy.py
+
+#include "gpu/demo_effects.h"
+#include "gpu/gpu.h"
+
+{class_name}::{class_name}(const GpuContext& ctx) : Effect(ctx) {{
+ ResourceBinding bindings[] = {{{{uniforms_.get(), WGPUBufferBindingType_Uniform}}}};
+ pass_ = gpu_create_render_pass(ctx_.device, ctx_.format, {snake_name}_shader_wgsl,
+ bindings, 1);
+ pass_.vertex_count = 3;
+}}
+
+void {class_name}::render(WGPURenderPassEncoder pass, float t, float b,
+ float i, float a) {{
+ CommonPostProcessUniforms u = {{
+ .resolution = {{(float)width_, (float)height_}},
+ ._pad = {{0.0f, 0.0f}},
+ .aspect_ratio = a,
+ .time = t,
+ .beat = b,
+ .audio_intensity = i,
+ }};
+ uniforms_.update(ctx_.queue, u);
+ wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, pass_.vertex_count, 1, 0, 0);
+}}
+"""
+
+def generate_shader(effect_name, shadertoy_code, is_post_process=False):
+ """Generate .wgsl file content."""
+ # Convert to WGSL (full shader, not just mainImage)
+ converted = convert_shadertoy_to_wgsl(shadertoy_code)
+
+ if is_post_process:
+ bindings = """@group(0) @binding(0) var smplr: sampler;
+@group(0) @binding(1) var txt: texture_2d<f32>;
+
+#include "common_uniforms"
+
+@group(0) @binding(2) var<uniform> uniforms: CommonUniforms;"""
+ else:
+ # Scene effect - only uniforms, no texture input
+ bindings = """#include "common_uniforms"
+
+@group(0) @binding(0) var<uniform> uniforms: CommonUniforms;"""
+
+ return f"""// {effect_name} effect shader - ShaderToy conversion
+// Generated by convert_shadertoy.py
+// NOTE: Manual review recommended - conversion is basic
+
+{bindings}
+
+@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {{
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(3.0, -1.0),
+ vec2<f32>(-1.0, 3.0)
+ );
+ return vec4<f32>(pos[i], 0.0, 1.0);
+}}
+
+{converted}
+"""
+
+def main():
+ if len(sys.argv) < 3:
+ print("Usage: convert_shadertoy.py <shader.txt> <EffectName> [--post-process] [--shader-only]")
+ print()
+ print("Examples:")
+ print(" ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel")
+ print(" ./tools/shadertoy/convert_shadertoy.py blur.txt Blur --post-process")
+ print(" ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel --shader-only")
+ print()
+ print("Options:")
+ print(" --post-process Generate post-process effect (operates on previous frame)")
+ print(" Default: scene effect (renders geometry)")
+ print(" --shader-only Only regenerate .wgsl shader (skip .h/.cc files)")
+ print()
+ print("This will generate:")
+ print(" src/gpu/effects/<effect>_effect.h")
+ print(" src/gpu/effects/<effect>_effect.cc")
+ print(" workspaces/main/shaders/<effect>.wgsl")
+ sys.exit(1)
+
+ shader_file = sys.argv[1]
+ effect_name = sys.argv[2]
+ is_post_process = '--post-process' in sys.argv
+ shader_only = '--shader-only' in sys.argv
+
+ # Ensure effect name is CamelCase
+ if '_' in effect_name:
+ effect_name = to_camel_case(effect_name)
+
+ # Read shader code
+ if not os.path.exists(shader_file):
+ print(f"Error: {shader_file} not found")
+ sys.exit(1)
+
+ with open(shader_file, 'r') as f:
+ shadertoy_code = f.read()
+
+ # Generate file names
+ snake_name = to_snake_case(effect_name)
+ upper_name = to_upper_snake_case(effect_name)
+
+ # Script is in tools/shadertoy/, so go up two levels to repo root
+ repo_root = Path(__file__).parent.parent.parent
+ header_path = repo_root / "src" / "gpu" / "effects" / f"{snake_name}_effect.h"
+ impl_path = repo_root / "src" / "gpu" / "effects" / f"{snake_name}_effect.cc"
+ shader_path = repo_root / "workspaces" / "main" / "shaders" / f"{snake_name}.wgsl"
+
+ # Generate files
+ if shader_only:
+ print(f"Regenerating shader only: {effect_name}")
+ print(f" Shader: {shader_path}")
+ print()
+ shader_path.write_text(generate_shader(effect_name, shadertoy_code, is_post_process))
+ print(f"✓ Shader regenerated")
+ return
+
+ print(f"Generating effect: {effect_name}")
+ print(f" Header: {header_path}")
+ print(f" Impl: {impl_path}")
+ print(f" Shader: {shader_path}")
+ print()
+
+ # Write files
+ header_path.write_text(generate_header(effect_name, is_post_process))
+ impl_path.write_text(generate_implementation(effect_name, is_post_process))
+ shader_path.write_text(generate_shader(effect_name, shadertoy_code, is_post_process))
+
+ effect_type = "post-process" if is_post_process else "scene"
+ print(f"✓ Files generated ({effect_type} effect)")
+ print()
+ print("Next steps (see doc/EFFECT_WORKFLOW.md for details):")
+ print()
+ print("1. Add shader to workspaces/main/assets.txt:")
+ print(f" SHADER_{upper_name}, NONE, shaders/{snake_name}.wgsl, \"{effect_name} effect\"")
+ print()
+ print()
+ print("2. Add shader declaration to src/gpu/effects/shaders.h:")
+ print(f" extern const char* {snake_name}_shader_wgsl;")
+ print()
+ print("3. Add shader definition to src/gpu/effects/shaders.cc:")
+ print(f" const char* {snake_name}_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_{upper_name});")
+ print()
+ print("4. Include header in src/gpu/demo_effects.h:")
+ print(f' #include "gpu/effects/{snake_name}_effect.h"')
+ print()
+ print("5. Add to timeline in workspaces/main/timeline.seq:")
+ print(f" EFFECT + {effect_name}Effect 0.0 10.0")
+ print()
+ print("6. Add to CMakeLists.txt GPU_SOURCES (both headless and normal mode):")
+ print(f" src/gpu/effects/{snake_name}_effect.cc")
+ print()
+ print("7. Update src/tests/gpu/test_demo_effects.cc:")
+ test_list = "post_process_effects" if is_post_process else "scene_effects"
+ print(f' - Add "{{{effect_name}Effect", std::make_shared<{effect_name}Effect>(fixture.ctx())}}" to {test_list} list')
+ print()
+ print("8. Build and test:")
+ print(" cmake --build build -j4")
+ print(" ./build/demo64k")
+ print()
+ print("Note: Review generated shader for const expression issues (normalize, etc)")
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/shadertoy/example.txt b/tools/shadertoy/example.txt
new file mode 100644
index 0000000..e0287de
--- /dev/null
+++ b/tools/shadertoy/example.txt
@@ -0,0 +1,25 @@
+// Example ShaderToy shader for testing convert_shadertoy.py
+// Simple animated gradient effect
+//
+// Test with:
+// ./tools/shadertoy/convert_shadertoy.py tools/shadertoy/example.txt Rainbow
+
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ // Normalized pixel coordinates (from 0 to 1)
+ vec2 uv = fragCoord / iResolution.xy;
+
+ // Center coordinates
+ vec2 center = uv - 0.5;
+
+ // Distance from center
+ float dist = length(center);
+
+ // Animated rainbow colors
+ vec3 col = 0.5 + 0.5 * cos(iTime + dist * 10.0 + vec3(0.0, 2.0, 4.0));
+
+ // Pulsing effect
+ col *= 1.0 + 0.2 * sin(iTime * 2.0);
+
+ // Output to screen
+ fragColor = vec4(col, 1.0);
+}
diff --git a/tools/shadertoy/template.cc b/tools/shadertoy/template.cc
new file mode 100644
index 0000000..288283d
--- /dev/null
+++ b/tools/shadertoy/template.cc
@@ -0,0 +1,120 @@
+// This file is part of the 64k demo project.
+// ShaderToy effect implementation - REPLACE THIS LINE
+// TODO: Update description, rename class
+
+#include "gpu/effects/shadertoy_effect.h"
+#include "gpu/effects/shader_composer.h"
+#include "generated/assets.h"
+
+// TODO: Rename class and adjust constructor parameters
+ShaderToyEffect::ShaderToyEffect(const GpuContext& ctx) : Effect(ctx) {
+}
+
+ShaderToyEffect::~ShaderToyEffect() {
+ if (sampler_)
+ wgpuSamplerRelease(sampler_);
+ if (bind_group_)
+ wgpuBindGroupRelease(bind_group_);
+ if (pipeline_)
+ wgpuRenderPipelineRelease(pipeline_);
+}
+
+void ShaderToyEffect::init(MainSequence* demo) {
+ demo_ = demo;
+ params_.init(ctx_.device);
+
+ WGPUSamplerDescriptor sampler_desc = {};
+ sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge;
+ sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge;
+ sampler_desc.magFilter = WGPUFilterMode_Linear;
+ sampler_desc.minFilter = WGPUFilterMode_Linear;
+ sampler_desc.mipmapFilter = WGPUMipmapFilterMode_Linear;
+ sampler_desc.maxAnisotropy = 1;
+ sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc);
+
+ // TODO: Update asset name to match your shader file
+ size_t shader_size;
+ const char* shader_code = (const char*)GetAsset(
+ AssetId::ASSET_SHADERTOY_SHADER, &shader_size);
+
+ std::string composed = ShaderComposer::Get().Compose({}, shader_code);
+
+ WGPUShaderSourceWGSL wgsl = {};
+ wgsl.chain.sType = WGPUSType_ShaderSourceWGSL;
+ wgsl.code = str_view(composed.c_str());
+
+ WGPUShaderModuleDescriptor desc = {};
+ desc.nextInChain = &wgsl.chain;
+ WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx_.device, &desc);
+
+ const WGPUColorTargetState target = {
+ .format = ctx_.format,
+ .writeMask = WGPUColorWriteMask_All,
+ };
+ WGPUFragmentState frag = {};
+ frag.module = module;
+ frag.entryPoint = str_view("fs_main");
+ frag.targetCount = 1;
+ frag.targets = &target;
+
+ const WGPUDepthStencilState depth_stencil = {
+ .format = WGPUTextureFormat_Depth24Plus,
+ .depthWriteEnabled = WGPUOptionalBool_False,
+ .depthCompare = WGPUCompareFunction_Always,
+ };
+
+ WGPURenderPipelineDescriptor pipeline_desc = {};
+ pipeline_desc.label = label_view("ShaderToyEffect");
+ pipeline_desc.vertex.module = module;
+ pipeline_desc.vertex.entryPoint = str_view("vs_main");
+ pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
+ pipeline_desc.primitive.cullMode = WGPUCullMode_None;
+ pipeline_desc.depthStencil = &depth_stencil;
+ pipeline_desc.multisample.count = 1;
+ pipeline_desc.multisample.mask = 0xFFFFFFFF;
+ pipeline_desc.fragment = &frag;
+
+ pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &pipeline_desc);
+ wgpuShaderModuleRelease(module);
+
+ WGPUTextureView prev_view = demo_->get_prev_texture_view();
+ const WGPUBindGroupEntry entries[] = {
+ {.binding = 0, .sampler = sampler_},
+ {.binding = 1, .textureView = prev_view},
+ {.binding = 2,
+ .buffer = uniforms_.get().buffer,
+ .size = sizeof(CommonPostProcessUniforms)},
+ {.binding = 3,
+ .buffer = params_.get().buffer,
+ .size = sizeof(ShaderToyParams)},
+ };
+ const WGPUBindGroupDescriptor bg_desc = {
+ .layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0),
+ .entryCount = 4,
+ .entries = entries,
+ };
+ bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc);
+}
+
+void ShaderToyEffect::render(WGPURenderPassEncoder pass, float time,
+ float beat, float intensity, float aspect_ratio) {
+ const CommonPostProcessUniforms uniforms = {
+ .resolution = {static_cast<float>(width_), static_cast<float>(height_)},
+ .aspect_ratio = aspect_ratio,
+ .time = time,
+ .beat = beat,
+ .audio_intensity = intensity,
+ };
+ uniforms_.update(ctx_.queue, uniforms);
+
+ // TODO: Update parameters based on your effect
+ const ShaderToyParams params = {
+ .param1 = 1.0f,
+ .param2 = beat,
+ };
+ params_.update(ctx_.queue, params);
+
+ wgpuRenderPassEncoderSetPipeline(pass, pipeline_);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
+}
diff --git a/tools/shadertoy/template.h b/tools/shadertoy/template.h
new file mode 100644
index 0000000..2e4af5f
--- /dev/null
+++ b/tools/shadertoy/template.h
@@ -0,0 +1,41 @@
+// This file is part of the 64k demo project.
+// ShaderToy effect boilerplate - REPLACE THIS LINE WITH DESCRIPTION
+// TODO: Update description, rename class, adjust parameters
+
+#ifndef SHADERTOY_EFFECT_H_
+#define SHADERTOY_EFFECT_H_
+
+#include "gpu/effect.h"
+#include "gpu/effects/post_process_helper.h"
+#include "gpu/uniform_helper.h"
+
+// TODO: Rename class to match your effect (e.g., TunnelEffect, PlasmaEffect)
+class ShaderToyEffect : public Effect {
+ public:
+ // TODO: Add constructor parameters for tunable values
+ ShaderToyEffect(const GpuContext& ctx);
+ ~ShaderToyEffect() override;
+
+ void init(MainSequence* demo) override;
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+ private:
+ // TODO: Add effect-specific parameters here
+ // Must match WGSL struct exactly - use padding for 16-byte alignment
+ struct ShaderToyParams {
+ float param1;
+ float param2;
+ float _pad[2]; // Padding to 16 bytes
+ };
+ static_assert(sizeof(ShaderToyParams) == 16,
+ "ShaderToyParams must be 16 bytes for WGSL alignment");
+
+ MainSequence* demo_ = nullptr;
+ WGPURenderPipeline pipeline_ = nullptr;
+ WGPUBindGroup bind_group_ = nullptr;
+ WGPUSampler sampler_ = nullptr;
+ UniformBuffer<ShaderToyParams> params_;
+};
+
+#endif /* SHADERTOY_EFFECT_H_ */
diff --git a/tools/shadertoy/template.wgsl b/tools/shadertoy/template.wgsl
new file mode 100644
index 0000000..37e7def
--- /dev/null
+++ b/tools/shadertoy/template.wgsl
@@ -0,0 +1,90 @@
+// ShaderToy conversion template for 64k demo project
+// TODO: Paste ShaderToy mainImage() function below and adapt
+
+@group(0) @binding(0) var smplr: sampler;
+@group(0) @binding(1) var txt: texture_2d<f32>;
+
+#include "common_uniforms"
+
+@group(0) @binding(2) var<uniform> uniforms: CommonUniforms;
+
+// TODO: Define your effect parameters (must match C++ struct)
+struct ShaderToyParams {
+ param1: f32,
+ param2: f32,
+ _pad0: f32,
+ _pad1: f32,
+}
+
+@group(0) @binding(3) var<uniform> params: ShaderToyParams;
+
+// Standard fullscreen triangle vertex shader
+@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(3.0, -1.0),
+ vec2<f32>(-1.0, 3.0)
+ );
+ return vec4<f32>(pos[i], 0.0, 1.0);
+}
+
+// ============================================================================
+// PASTE SHADERTOY CODE HERE
+// ============================================================================
+// ShaderToy → WGSL conversion notes:
+//
+// 1. Replace ShaderToy uniforms:
+// iResolution.xy → uniforms.resolution
+// iTime → uniforms.time
+// fragCoord → p.xy (from @builtin(position))
+// fragColor → return value
+//
+// 2. Coordinate conversion:
+// vec2 uv = fragCoord / iResolution.xy;
+// becomes:
+// let uv = p.xy / uniforms.resolution;
+//
+// 3. Type syntax changes:
+// float → f32
+// vec2/vec3/vec4 → vec2<f32>, vec3<f32>, vec4<f32>
+// mat2/mat3/mat4 → mat2x2<f32>, mat3x3<f32>, mat4x4<f32>
+//
+// 4. Function syntax:
+// float foo(vec2 p) → fn foo(p: vec2<f32>) -> f32
+//
+// 5. Common functions (mostly same):
+// mix, sin, cos, length, normalize, dot, cross, etc.
+// fract() → fract()
+// mod(x, y) → x % y OR x - y * floor(x / y)
+//
+// 6. Texture sampling:
+// texture(iChannel0, uv) → textureSample(txt, smplr, uv)
+//
+// 7. Variable declarations:
+// float x = 1.0; → var x: f32 = 1.0; OR let x = 1.0;
+// const float x = 1.0; → const x: f32 = 1.0;
+//
+// 8. Swizzling is the same: col.rgb, uv.xy, etc.
+//
+// ============================================================================
+
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ // TODO: Paste and adapt ShaderToy mainImage() body here
+
+ // Example coordinate setup (typical ShaderToy pattern):
+ let uv = p.xy / uniforms.resolution;
+
+ // TODO: Your effect code here
+ var col = vec3<f32>(uv.x, uv.y, 0.5);
+
+ // Optional: Sample previous frame
+ // var prev_col = textureSample(txt, smplr, uv);
+
+ // Optional: Audio reactivity
+ // col *= 1.0 + uniforms.audio_intensity * 0.2;
+
+ // Optional: Beat sync
+ // col *= 1.0 + uniforms.beat * 0.1;
+
+ return vec4<f32>(col, 1.0);
+}