diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-28 09:25:35 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-28 09:25:35 +0100 |
| commit | bc1beb58ba259263eb98d43d2aa742307764591c (patch) | |
| tree | eae97b1496811ed6f2fde60b1b914f2bcf2d825f /tools/shadertoy | |
| parent | 34bee8b09566a52cedced99b7bd13d29907512ce (diff) | |
fix(tools/shadertoy): sync templates and script to current codebase conventions
- template.h/cc: new Effect constructor/render signatures, RAII wrappers,
HEADLESS_RETURN_IF_NULL, #pragma once
- template.wgsl: sequence_uniforms + render/fullscreen_uv_vs includes,
UniformsSequenceParams at binding 2, VertexOutput in fs_main
- convert_shadertoy.py: paths src/effects/ + src/shaders/, new Effect
pattern (create_post_process_pipeline, pp_update_bind_group), correct
field names (beat_time/beat_phase), updated next-steps instructions
- README.md: streamlined to quick-ref; accurate GLSL→WGSL table and uniforms
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tools/shadertoy')
| -rw-r--r-- | tools/shadertoy/README.md | 211 | ||||
| -rwxr-xr-x | tools/shadertoy/convert_shadertoy.py | 255 | ||||
| -rw-r--r-- | tools/shadertoy/template.cc | 140 | ||||
| -rw-r--r-- | tools/shadertoy/template.h | 39 | ||||
| -rw-r--r-- | tools/shadertoy/template.wgsl | 75 |
5 files changed, 266 insertions, 454 deletions
diff --git a/tools/shadertoy/README.md b/tools/shadertoy/README.md index 1ce42e8..878b53c 100644 --- a/tools/shadertoy/README.md +++ b/tools/shadertoy/README.md @@ -1,137 +1,48 @@ # ShaderToy Conversion Guide -Quick guide to convert ShaderToy shaders to demo effects. - -**For complete workflow:** See `doc/EFFECT_WORKFLOW.md` for full integration checklist. +Convert ShaderToy shaders to demo effects. 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 +# Generate effect files from ShaderToy code ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel +./tools/shadertoy/convert_shadertoy.py blur.txt Blur --post-process +./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel --shader-only # regen shader only -# Regenerate only shader (if .h/.cc already exist) -./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel --shader-only - -# Follow printed instructions to integrate +# Follow printed instructions to integrate (assets.txt, shaders.h, CMakeLists.txt, timeline.seq) ``` -## Files - -**Automated Script:** -- `convert_shadertoy.py` - Generates all files from ShaderToy code -- `example.txt` - Example ShaderToy shader for testing +Generates: `src/effects/<name>_effect.{h,cc}` + `src/shaders/<name>.wgsl` -**Manual Templates:** -- `template.h` - Header boilerplate -- `template.cc` - Implementation boilerplate -- `template.wgsl` - Shader boilerplate with conversion notes - -## Manual Steps - -### 1. Copy Templates +## Manual Templates ```bash -# Choose effect name (e.g., "tunnel", "plasma", "warp") -EFFECT_NAME="myeffect" - -cp tools/shadertoy/template.h src/effects/${EFFECT_NAME}_effect.h -cp tools/shadertoy/template.cc src/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 +cp tools/shadertoy/template.h src/effects/myeffect_effect.h +cp tools/shadertoy/template.cc src/effects/myeffect_effect.cc +cp tools/shadertoy/template.wgsl src/shaders/myeffect.wgsl ``` -### 6. Register Effect +Then rename `ShaderToyEffect` → `MyeffectEffect` in `.h` and `.cc`, paste shader code into `fs_main()`, and follow `doc/EFFECT_WORKFLOW.md`. -In `src/gpu/demo_effects.h`: -```cpp -#include "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/effects/myeffect_effect.cc -``` +## GLSL → WGSL Conversions -### 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 -``` +| ShaderToy | WGSL | +|-----------|------| +| `iResolution.xy` | `uniforms.resolution` | +| `iTime` | `uniforms.time` | +| `fragCoord` | `in.position.xy` (pixel) or `in.uv * uniforms.resolution` | +| `float` | `f32` | +| `vec2/vec3/vec4` | `vec2f/vec3f/vec4f` | +| `mat2/mat3/mat4` | `mat2x2f/mat3x3f/mat4x4f` | +| `mod(x, y)` | `x % y` | +| `texture(iChannel0, uv)` | `textureSample(txt, smplr, uv)` | +| `fragColor = col;` | `return col;` | +| `float foo(vec2 p)` | `fn foo(p: vec2f) -> f32` | -## Example Conversion +## Example -**Input ShaderToy:** +**Input:** ```glsl void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; @@ -140,65 +51,39 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { } ``` -**Generated WGSL (after script + manual fixes):** +**Output WGSL:** ```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); +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let uv = in.uv; + let col = vec3f(0.5) + 0.5 * cos(uniforms.time + uv.xyx + vec3f(0.0, 2.0, 4.0)); + return vec4f(col, 1.0); } ``` -## Common Conversions +## Available Uniforms (`UniformsSequenceParams`, binding 2) -| 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 +| Field | Type | Description | +|-------|------|-------------| +| `resolution` | `vec2f` | Screen dimensions | +| `aspect_ratio` | `f32` | Width/height | +| `time` | `f32` | Physical time (seconds) | +| `beat_time` | `f32` | Musical beats (absolute) | +| `beat_phase` | `f32` | Fractional beat [0..1] | +| `audio_intensity` | `f32` | Audio peak | -For tunable values: +## Custom Parameters (optional, binding 3) -**C++ (`.h`):** +**C++** (must be 16-byte aligned): ```cpp -struct MyEffectParams { - float speed; - float scale; - float _pad[2]; -}; -static_assert(sizeof(MyEffectParams) == 16, "..."); +struct MyParams { float speed; float scale; float _pad[2]; }; +static_assert(sizeof(MyParams) == 16, "..."); +UniformBuffer<MyParams> params_; // add to .h ``` **WGSL:** ```wgsl -struct MyEffectParams { - speed: f32, - scale: f32, - _pad0: f32, - _pad1: f32, -} -@group(0) @binding(3) var<uniform> params: MyEffectParams; +struct MyParams { speed: f32, scale: f32, _pad0: f32, _pad1: f32, } +@group(0) @binding(3) var<uniform> params: MyParams; ``` -## 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/effects/` for examples +Also pass `params_.get()` instead of `{nullptr, 0}` in `pp_update_bind_group()`. diff --git a/tools/shadertoy/convert_shadertoy.py b/tools/shadertoy/convert_shadertoy.py index 362de6c..22b3276 100755 --- a/tools/shadertoy/convert_shadertoy.py +++ b/tools/shadertoy/convert_shadertoy.py @@ -12,10 +12,10 @@ # Generates: # - src/effects/<effect>_effect.h # - src/effects/<effect>_effect.cc -# - workspaces/main/shaders/<effect>.wgsl +# - src/shaders/<effect>.wgsl # # The script performs basic ShaderToy→WGSL conversion: -# - Converts types (float→f32, vec2→vec2<f32>, etc.) +# - Converts types (float→f32, vec2→vec2f, etc.) # - Converts uniforms (iTime→uniforms.time, etc.) # - Extracts mainImage() body into fs_main() # - Generates boilerplate C++ effect class @@ -69,29 +69,29 @@ def convert_shadertoy_to_wgsl(shader_code): (r'\biTime\b', 'uniforms.time'), (r'\bRESOLUTION\b', 'uniforms.resolution'), (r'\biResolution\b', 'uniforms.resolution'), - (r'\bfragCoord\b', 'p.xy'), + (r'\bfragCoord\b', 'in.position.xy'), - # Type conversions + # Type conversions (use short form vec2f etc.) (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>'), + (r'\bvec2\b', 'vec2f'), + (r'\bvec3\b', 'vec3f'), + (r'\bvec4\b', 'vec4f'), + (r'\bmat2\b', 'mat2x2f'), + (r'\bmat3\b', 'mat3x3f'), + (r'\bmat4\b', 'mat4x4f'), # 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'\bvec2f\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec3f\s+(\w+)\s*\(', r'fn \1('), + (r'\bvec4f\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 ='), + (r'\bconst\s+vec2f\s+(\w+)\s*=', r'const \1 ='), + (r'\bconst\s+vec3f\s+(\w+)\s*=', r'const \1 ='), + (r'\bconst\s+vec4f\s+(\w+)\s*=', r'const \1 ='), # Function calls that need fixing (r'\bfract\s*\(', 'fract('), @@ -114,9 +114,9 @@ def convert_shadertoy_to_wgsl(shader_code): 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> {{ + fragment = f"""@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {{ // Flip Y to match ShaderToy convention (origin at bottom-left) - let flipped = vec2<f32>(p.x, uniforms.resolution.y - p.y); + let flipped = vec2f(in.position.x, uniforms.resolution.y - in.position.y); let q = flipped / uniforms.resolution; var coord = -1.0 + 2.0 * q; coord.x *= uniforms.resolution.x / uniforms.resolution.y; @@ -126,16 +126,6 @@ def convert_shadertoy_to_wgsl(shader_code): 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" @@ -146,44 +136,52 @@ def generate_header(effect_name, is_post_process=False): // {effect_name} effect - ShaderToy conversion (post-process) // Generated by convert_shadertoy.py -#ifndef {upper_name}_EFFECT_H_ -#define {upper_name}_EFFECT_H_ +#pragma once #include "gpu/effect.h" -#include "gpu/post_process_helper.h" +#include "gpu/wgpu_resource.h" -class {class_name} : public PostProcessEffect {{ +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; - void update_bind_group(WGPUTextureView input_view) override; -}}; + {class_name}(const GpuContext& ctx, + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time); + + void render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) override; -#endif /* {upper_name}_EFFECT_H_ */ + private: + RenderPipeline pipeline_; + BindGroup bind_group_; +}}; """ 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_ +#pragma once #include "gpu/effect.h" +#include "gpu/wgpu_resource.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; + {class_name}(const GpuContext& ctx, + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time); + + void render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) override; private: - RenderPass pass_; + RenderPipeline pipeline_; + BindGroup bind_group_; }}; - -#endif /* {upper_name}_EFFECT_H_ */ """ def generate_implementation(effect_name, is_post_process=False): @@ -196,65 +194,96 @@ def generate_implementation(effect_name, is_post_process=False): // {effect_name} effect - ShaderToy conversion (post-process) // Generated by convert_shadertoy.py -#include "gpu/demo_effects.h" -#include "gpu/post_process_helper.h" +#include "effects/{snake_name}_effect.h" +#include "effects/shaders.h" #include "gpu/gpu.h" +#include "gpu/post_process_helper.h" +#include "util/fatal_error.h" -{class_name}::{class_name}(const GpuContext& ctx) : PostProcessEffect(ctx) {{ - pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, {snake_name}_shader_wgsl); +{class_name}::{class_name}(const GpuContext& ctx, + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time) + : Effect(ctx, inputs, outputs, start_time, end_time) {{ + HEADLESS_RETURN_IF_NULL(ctx_.device); + create_linear_sampler(); + pipeline_.set(create_post_process_pipeline( + ctx_.device, WGPUTextureFormat_RGBA8Unorm, {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); +void {class_name}::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) {{ + WGPUTextureView input_view = nodes.get_view(input_nodes_[0]); + WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - wgpuRenderPassEncoderSetPipeline(pass, pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); -}} + // uniforms_buffer_ auto-updated by base class dispatch_render() + pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), + input_view, uniforms_buffer_.get(), {{nullptr, 0}}); -void {class_name}::update_bind_group(WGPUTextureView input_view) {{ - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, uniforms_.get()); + WGPURenderPassColorAttachment color_attachment = {{}}; + gpu_init_color_attachment(color_attachment, output_view); + + WGPURenderPassDescriptor pass_desc = {{}}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); }} """ 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 "effects/{snake_name}_effect.h" +#include "effects/shaders.h" #include "gpu/gpu.h" +#include "gpu/post_process_helper.h" +#include "util/fatal_error.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; +{class_name}::{class_name}(const GpuContext& ctx, + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time) + : Effect(ctx, inputs, outputs, start_time, end_time) {{ + HEADLESS_RETURN_IF_NULL(ctx_.device); + create_nearest_sampler(); + create_dummy_scene_texture(); + pipeline_.set(create_post_process_pipeline( + ctx_.device, WGPUTextureFormat_RGBA8Unorm, {snake_name}_shader_wgsl)); }} -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); +void {class_name}::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) {{ + WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); + + // uniforms_buffer_ auto-updated by base class dispatch_render() + pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), + dummy_texture_view_.get(), uniforms_buffer_.get(), + {{nullptr, 0}}); + + WGPURenderPassColorAttachment color_attachment = {{}}; + gpu_init_color_attachment(color_attachment, output_view); + + WGPURenderPassDescriptor pass_desc = {{}}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; + + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); }} """ @@ -267,14 +296,16 @@ def generate_shader(effect_name, shadertoy_code, is_post_process=False): bindings = """@group(0) @binding(0) var smplr: sampler; @group(0) @binding(1) var txt: texture_2d<f32>; -#include "common_uniforms" +#include "sequence_uniforms" +#include "render/fullscreen_uv_vs" -@group(0) @binding(2) var<uniform> uniforms: CommonUniforms;""" +@group(0) @binding(2) var<uniform> uniforms: UniformsSequenceParams;""" else: - # Scene effect - only uniforms, no texture input - bindings = """#include "common_uniforms" + # Scene effect - no texture input + bindings = """#include "sequence_uniforms" +#include "render/fullscreen_uv_vs" -@group(0) @binding(0) var<uniform> uniforms: CommonUniforms;""" +@group(0) @binding(2) var<uniform> uniforms: UniformsSequenceParams;""" return f"""// {effect_name} effect shader - ShaderToy conversion // Generated by convert_shadertoy.py @@ -282,15 +313,6 @@ def generate_shader(effect_name, shadertoy_code, is_post_process=False): {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} """ @@ -305,13 +327,13 @@ def main(): print() print("Options:") print(" --post-process Generate post-process effect (operates on previous frame)") - print(" Default: scene effect (renders geometry)") + print(" Default: scene effect (renders from scratch)") print(" --shader-only Only regenerate .wgsl shader (skip .h/.cc files)") print() print("This will generate:") print(" src/effects/<effect>_effect.h") print(" src/effects/<effect>_effect.cc") - print(" workspaces/main/shaders/<effect>.wgsl") + print(" src/shaders/<effect>.wgsl") sys.exit(1) shader_file = sys.argv[1] @@ -337,9 +359,9 @@ def main(): # 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" + header_path = repo_root / "src" / "effects" / f"{snake_name}_effect.h" + impl_path = repo_root / "src" / "effects" / f"{snake_name}_effect.cc" + shader_path = repo_root / "src" / "shaders" / f"{snake_name}.wgsl" # Generate files if shader_only: @@ -367,8 +389,7 @@ def main(): 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(f" SHADER_{upper_name}, NONE, ../../src/shaders/{snake_name}.wgsl, \"{effect_name} effect\"") print() print("2. Add shader declaration to src/effects/shaders.h:") print(f" extern const char* {snake_name}_shader_wgsl;") @@ -379,15 +400,15 @@ def main(): print("4. Include header in src/gpu/demo_effects.h:") print(f' #include "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("5. Add to CMakeLists.txt GPU_SOURCES (both headless and normal mode sections):") print(f" src/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("6. Add to timeline in workspaces/main/timeline.seq:") + print(f" EFFECT + {effect_name}Effect source -> sink 0.0 10.0") + print() + print("7. Regenerate timeline.cc:") + print(" python3 tools/seq_compiler.py workspaces/main/timeline.seq \\") + print(" --output src/generated/timeline.cc") print() print("8. Build and test:") print(" cmake --build build -j4") diff --git a/tools/shadertoy/template.cc b/tools/shadertoy/template.cc index fdc4f70..81c39ba 100644 --- a/tools/shadertoy/template.cc +++ b/tools/shadertoy/template.cc @@ -3,118 +3,48 @@ // TODO: Update description, rename class #include "effects/shadertoy_effect.h" -#include "generated/assets.h" -#include "gpu/shader_composer.h" +#include "effects/shaders.h" +#include "gpu/gpu.h" +#include "gpu/post_process_helper.h" +#include "util/fatal_error.h" -// TODO: Rename class and adjust constructor parameters -ShaderToyEffect::ShaderToyEffect(const GpuContext& ctx) : Effect(ctx) { -} +// TODO: Rename class +ShaderToyEffect::ShaderToyEffect(const GpuContext& ctx, + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time) + : Effect(ctx, inputs, outputs, start_time, end_time) { + HEADLESS_RETURN_IF_NULL(ctx_.device); + create_nearest_sampler(); + create_dummy_scene_texture(); -ShaderToyEffect::~ShaderToyEffect() { - if (sampler_) - wgpuSamplerRelease(sampler_); - if (bind_group_) - wgpuBindGroupRelease(bind_group_); - if (pipeline_) - wgpuRenderPipelineRelease(pipeline_); + // TODO: Update shader name to match declaration in shaders.h + pipeline_.set(create_post_process_pipeline( + ctx_.device, WGPUTextureFormat_RGBA8Unorm, shadertoy_shader_wgsl)); } -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); +void ShaderToyEffect::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) { + WGPUTextureView output_view = nodes.get_view(output_nodes_[0]); - 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(UniformsSequenceParams)}, - {.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); -} + // uniforms_buffer_ is auto-updated by base class dispatch_render() + pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), + dummy_texture_view_.get(), uniforms_buffer_.get(), + {nullptr, 0}); -void ShaderToyEffect::render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) { - const UniformsSequenceParams uniforms = { - .resolution = {(float)(width_), (float)(height_)}, - .aspect_ratio = aspect_ratio, - .time = time, - .beat = beat, - .audio_intensity = intensity, - }; - uniforms_.update(ctx_.queue, uniforms); + WGPURenderPassColorAttachment color_attachment = {}; + gpu_init_color_attachment(color_attachment, output_view); - // TODO: Update parameters based on your effect - const ShaderToyParams params = { - .param1 = 1.0f, - .param2 = beat, - }; - params_.update(ctx_.queue, params); + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color_attachment; - wgpuRenderPassEncoderSetPipeline(pass, pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + WGPURenderPassEncoder pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr); wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); } diff --git a/tools/shadertoy/template.h b/tools/shadertoy/template.h index 74be9f2..5f105d5 100644 --- a/tools/shadertoy/template.h +++ b/tools/shadertoy/template.h @@ -2,40 +2,25 @@ // ShaderToy effect boilerplate - REPLACE THIS LINE WITH DESCRIPTION // TODO: Update description, rename class, adjust parameters -#ifndef SHADERTOY_EFFECT_H_ -#define SHADERTOY_EFFECT_H_ +#pragma once #include "gpu/effect.h" -#include "gpu/post_process_helper.h" -#include "gpu/uniform_helper.h" +#include "gpu/wgpu_resource.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; + // TODO: Rename class, keep same constructor signature + ShaderToyEffect(const GpuContext& ctx, + const std::vector<std::string>& inputs, + const std::vector<std::string>& outputs, + float start_time, float end_time); - void init(MainSequence* demo) override; - void render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) override; + void render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) 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_; + RenderPipeline pipeline_; + BindGroup bind_group_; }; - -#endif /* SHADERTOY_EFFECT_H_ */ diff --git a/tools/shadertoy/template.wgsl b/tools/shadertoy/template.wgsl index 37e7def..573fa97 100644 --- a/tools/shadertoy/template.wgsl +++ b/tools/shadertoy/template.wgsl @@ -1,32 +1,14 @@ // 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 "sequence_uniforms" +#include "render/fullscreen_uv_vs" // <- VertexOutput + vs_main -#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); -} +// Scene effect: only uniforms (bindings 0,1 unused) +// For post-process (input texture needed), add: +// @group(0) @binding(0) var smplr: sampler; +// @group(0) @binding(1) var txt: texture_2d<f32>; +@group(0) @binding(2) var<uniform> uniforms: UniformsSequenceParams; // ============================================================================ // PASTE SHADERTOY CODE HERE @@ -36,55 +18,64 @@ struct ShaderToyParams { // 1. Replace ShaderToy uniforms: // iResolution.xy → uniforms.resolution // iTime → uniforms.time -// fragCoord → p.xy (from @builtin(position)) +// fragCoord → in.position.xy (pixel coords) or in.uv * uniforms.resolution // fragColor → return value // // 2. Coordinate conversion: // vec2 uv = fragCoord / iResolution.xy; // becomes: -// let uv = p.xy / uniforms.resolution; +// let uv = in.uv; // [0..1], origin top-left +// // OR for ShaderToy convention (origin bottom-left): +// let uv = vec2f(in.uv.x, 1.0 - in.uv.y); // // 3. Type syntax changes: // float → f32 -// vec2/vec3/vec4 → vec2<f32>, vec3<f32>, vec4<f32> -// mat2/mat3/mat4 → mat2x2<f32>, mat3x3<f32>, mat4x4<f32> +// vec2/vec3/vec4 → vec2f, vec3f, vec4f +// mat2/mat3/mat4 → mat2x2f, mat3x3f, mat4x4f // // 4. Function syntax: -// float foo(vec2 p) → fn foo(p: vec2<f32>) -> f32 +// float foo(vec2 p) → fn foo(p: vec2f) -> 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) +// mix, sin, cos, length, normalize, dot, cross, fract(), etc. +// mod(x, y) → x % y // -// 6. Texture sampling: +// 6. Texture sampling (requires bindings 0,1 in C++ and shader): // 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. +// 8. Beat / audio: +// uniforms.beat_time → absolute musical beats +// uniforms.beat_phase → fractional beat [0..1] +// uniforms.audio_intensity → audio reactivity // // ============================================================================ -@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { // TODO: Paste and adapt ShaderToy mainImage() body here - // Example coordinate setup (typical ShaderToy pattern): - let uv = p.xy / uniforms.resolution; + // Standard UV (origin top-left, [0..1]) + let uv = in.uv; + + // For ShaderToy convention (origin bottom-left, [-1..1] centered): + // let q = vec2f(in.uv.x, 1.0 - in.uv.y); + // var coord = q * 2.0 - 1.0; + // coord.x *= uniforms.aspect_ratio; // TODO: Your effect code here - var col = vec3<f32>(uv.x, uv.y, 0.5); + var col = vec3f(uv.x, uv.y, 0.5); - // Optional: Sample previous frame + // Optional: Sample previous frame (requires bindings 0,1) // 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; + // col *= 1.0 + uniforms.beat_phase * 0.1; - return vec4<f32>(col, 1.0); + return vec4f(col, 1.0); } |
