summaryrefslogtreecommitdiff
path: root/tools/shadertoy/convert_shadertoy.py
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-28 09:25:35 +0100
committerskal <pascal.massimino@gmail.com>2026-02-28 09:25:35 +0100
commitbc1beb58ba259263eb98d43d2aa742307764591c (patch)
treeeae97b1496811ed6f2fde60b1b914f2bcf2d825f /tools/shadertoy/convert_shadertoy.py
parent34bee8b09566a52cedced99b7bd13d29907512ce (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-xtools/shadertoy/convert_shadertoy.py255
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")