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/convert_shadertoy.py | |
| 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/convert_shadertoy.py')
| -rwxr-xr-x | tools/shadertoy/convert_shadertoy.py | 255 |
1 files changed, 138 insertions, 117 deletions
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") |
