diff options
Diffstat (limited to 'tools/shadertoy')
| -rw-r--r-- | tools/shadertoy/README.md | 204 | ||||
| -rwxr-xr-x | tools/shadertoy/convert_shadertoy.py | 399 | ||||
| -rw-r--r-- | tools/shadertoy/example.txt | 25 | ||||
| -rw-r--r-- | tools/shadertoy/template.cc | 120 | ||||
| -rw-r--r-- | tools/shadertoy/template.h | 41 | ||||
| -rw-r--r-- | tools/shadertoy/template.wgsl | 90 |
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 = ⌖ + + 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); +} |
