summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/shadertoy/convert_shadertoy.py378
1 files changed, 176 insertions, 202 deletions
diff --git a/tools/shadertoy/convert_shadertoy.py b/tools/shadertoy/convert_shadertoy.py
index 29eca1d..48201bc 100755
--- a/tools/shadertoy/convert_shadertoy.py
+++ b/tools/shadertoy/convert_shadertoy.py
@@ -47,10 +47,28 @@ def to_camel_case(name):
def convert_shadertoy_to_wgsl(shader_code):
"""Basic ShaderToy to WGSL conversion."""
- # Replace common ShaderToy uniforms
+ # 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'\biResolution\b', 'uniforms.resolution'),
+ (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
@@ -62,36 +80,49 @@ def convert_shadertoy_to_wgsl(shader_code):
(r'\bmat3\b', 'mat3x3<f32>'),
(r'\bmat4\b', 'mat4x4<f32>'),
- # Function declarations
- (r'\bfloat\s+(\w+)\s*\(', r'fn \1('),
- (r'\bvec2\s+(\w+)\s*\(', r'fn \1('),
- (r'\bvec3\s+(\w+)\s*\(', r'fn \1('),
- (r'\bvec4\s+(\w+)\s*\(', r'fn \1('),
-
- # Function parameters (simple cases)
- (r'\(\s*vec2\s+(\w+)\s*\)', r'(\1: vec2<f32>)'),
- (r'\(\s*vec3\s+(\w+)\s*\)', r'(\1: vec3<f32>)'),
- (r'\(\s*float\s+(\w+)\s*\)', r'(\1: 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('),
- # Return types
- (r'\)\s*{', r') -> f32 {'), # Assume f32 return, may need manual fix
+ # 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 ='),
- # Texture sampling
- (r'\btexture\s*\(\s*iChannel0\s*,', 'textureSample(txt, smplr,'),
- (r'\btexture\s*\(\s*iChannel1\s*,', 'textureSample(txt, smplr,'),
+ # Function calls that need fixing
+ (r'\bfract\s*\(', 'fract('),
+ (r'\bmod\s*\(([^,]+),\s*([^)]+)\)', r'(\1 % \2)'),
]
- converted = shader_code
+ converted_helpers = helpers
for pattern, replacement in conversions:
- converted = re.sub(pattern, replacement, converted)
+ 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
+ fragment = f"""@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {{
+ let q = p.xy / uniforms.resolution;
+ var coord = -1.0 + 2.0 * q;
+ coord.x *= uniforms.resolution.x / uniforms.resolution.y;
- # Convert variable declarations
- converted = re.sub(r'\bfloat\s+(\w+)\s*=', r'var \1: f32 =', converted)
- converted = re.sub(r'\bvec2\s+(\w+)\s*=', r'var \1: vec2<f32> =', converted)
- converted = re.sub(r'\bvec3\s+(\w+)\s*=', r'var \1: vec3<f32> =', converted)
- converted = re.sub(r'\bvec4\s+(\w+)\s*=', r'var \1: vec4<f32> =', converted)
+{indented_main}
+}}"""
- return converted
+ return converted_helpers + '\n\n' + fragment
def extract_main_image(shader_code):
"""Extract mainImage function body from ShaderToy code."""
@@ -103,14 +134,14 @@ def extract_main_image(shader_code):
# If no mainImage found, return whole shader
return shader_code
-def generate_header(effect_name):
+def generate_header(effect_name, is_post_process=False):
"""Generate .h file content."""
class_name = f"{effect_name}Effect"
- snake_name = to_snake_case(effect_name)
upper_name = to_upper_snake_case(effect_name)
- return f"""// This file is part of the 64k demo project.
-// {effect_name} effect - ShaderToy conversion
+ 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_
@@ -118,193 +149,136 @@ def generate_header(effect_name):
#include "gpu/effect.h"
#include "gpu/effects/post_process_helper.h"
-#include "gpu/uniform_helper.h"
-class {class_name} : public Effect {{
+class {class_name} : public PostProcessEffect {{
public:
{class_name}(const GpuContext& ctx);
- ~{class_name}() override;
+ 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
- void init(MainSequence* demo) override;
+#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:
- // Effect-specific parameters - adjust as needed
- struct {effect_name}Params {{
- float param1;
- float param2;
- float _pad[2];
- }};
- static_assert(sizeof({effect_name}Params) == 16,
- "{effect_name}Params must be 16 bytes for WGSL alignment");
-
- MainSequence* demo_ = nullptr;
- WGPURenderPipeline pipeline_ = nullptr;
- WGPUBindGroup bind_group_ = nullptr;
- WGPUSampler sampler_ = nullptr;
- UniformBuffer<{effect_name}Params> params_;
+ RenderPass pass_;
}};
#endif /* {upper_name}_EFFECT_H_ */
"""
-def generate_implementation(effect_name):
+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)
- upper_name = to_upper_snake_case(effect_name)
- return f"""// This file is part of the 64k demo project.
-// {effect_name} effect implementation - ShaderToy conversion
+ 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/effects/{snake_name}_effect.h"
-#include "gpu/effects/shader_composer.h"
-#include "generated/assets.h"
-
-{class_name}::{class_name}(const GpuContext& ctx) : Effect(ctx) {{
-}}
-
-{class_name}::~{class_name}() {{
- if (sampler_)
- wgpuSamplerRelease(sampler_);
- if (bind_group_)
- wgpuBindGroupRelease(bind_group_);
- if (pipeline_)
- wgpuRenderPipelineRelease(pipeline_);
-}}
-
-void {class_name}::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);
-
- size_t shader_size;
- const char* shader_code = (const char*)GetAsset(
- AssetId::ASSET_{upper_name}_SHADER, &shader_size);
-
- std::string composed = ShaderComposer::Get().Compose({{}}, shader_code);
-
- WGPUShaderSourceWGSL wgsl = {{}};
- wgsl.chain.sType = WGPUSType_ShaderSourceWGSL;
- wgsl.code = str_view(composed.c_str());
-
- WGPUShaderModuleDescriptor desc = {{}};
- desc.nextInChain = &wgsl.chain;
- WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx_.device, &desc);
-
- const WGPUColorTargetState target = {{
- .format = ctx_.format,
- .writeMask = WGPUColorWriteMask_All,
- }};
- WGPUFragmentState frag = {{}};
- frag.module = module;
- frag.entryPoint = str_view("fs_main");
- frag.targetCount = 1;
- frag.targets = &target;
-
- const WGPUDepthStencilState depth_stencil = {{
- .format = WGPUTextureFormat_Depth24Plus,
- .depthWriteEnabled = WGPUOptionalBool_False,
- .depthCompare = WGPUCompareFunction_Always,
- }};
-
- WGPURenderPipelineDescriptor pipeline_desc = {{}};
- pipeline_desc.label = label_view("{class_name}");
- 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);
+#include "gpu/demo_effects.h"
+#include "gpu/effects/post_process_helper.h"
+#include "gpu/gpu.h"
- 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({effect_name}Params)}},
- }};
- const WGPUBindGroupDescriptor bg_desc = {{
- .layout = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0),
- .entryCount = 4,
- .entries = entries,
- }};
- bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc);
+{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 uniforms = {{
- .resolution = {{static_cast<float>(width_), static_cast<float>(height_)}},
+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, uniforms);
-
- const {effect_name}Params params = {{
- .param1 = 1.0f,
- .param2 = beat,
- }};
- params_.update(ctx_.queue, params);
+ 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
-def generate_shader(effect_name, shadertoy_code):
- """Generate .wgsl file content."""
- # Extract mainImage body
- main_body = extract_main_image(shadertoy_code)
+#include "gpu/demo_effects.h"
+#include "gpu/gpu.h"
- # Convert to WGSL
- converted = convert_shadertoy_to_wgsl(main_body)
+{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;
+}}
- # Indent the converted code
- indented = '\n'.join(' ' + line if line.strip() else ''
- for line in converted.split('\n'))
+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);
+}}
+"""
- return f"""// {effect_name} effect shader - ShaderToy conversion
-// Generated by convert_shadertoy.py
-// TODO: Review and fix conversion issues
+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)
-@group(0) @binding(0) var smplr: sampler;
+ 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;
+@group(0) @binding(2) var<uniform> uniforms: CommonUniforms;"""
+ else:
+ # Scene effect - only uniforms, no texture input
+ bindings = """#include "common_uniforms"
-struct {effect_name}Params {{
- param1: f32,
- param2: f32,
- _pad0: f32,
- _pad1: f32,
-}}
+@group(0) @binding(0) var<uniform> uniforms: CommonUniforms;"""
-@group(0) @binding(3) var<uniform> params: {effect_name}Params;
+ 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>(
@@ -315,29 +289,20 @@ struct {effect_name}Params {{
return vec4<f32>(pos[i], 0.0, 1.0);
}}
-// ============================================================================
-// CONVERTED SHADERTOY CODE (may need manual fixes)
-// ============================================================================
-// Original helper functions (if any) appear above mainImage conversion
-
-@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {{
- // Converted from mainImage()
-{indented}
-
- // TODO: Fix the return statement if needed
- // ShaderToy used: fragColor = ...
- // WGSL needs: return vec4<f32>(...);
-}}
+{converted}
"""
def main():
if len(sys.argv) < 3:
- print("Usage: convert_shadertoy.py <shader.txt> <EffectName>")
+ print("Usage: convert_shadertoy.py <shader.txt> <EffectName> [--post-process]")
print()
print("Examples:")
print(" ./tools/shadertoy/convert_shadertoy.py tunnel.txt Tunnel")
- print(" ./tools/shadertoy/convert_shadertoy.py plasma.txt Plasma")
- print(" ./tools/shadertoy/convert_shadertoy.py tools/shadertoy/example.txt Rainbow")
+ print(" ./tools/shadertoy/convert_shadertoy.py blur.txt Blur --post-process")
+ print()
+ print("Options:")
+ print(" --post-process Generate post-process effect (operates on previous frame)")
+ print(" Default: scene effect (renders geometry)")
print()
print("This will generate:")
print(" src/gpu/effects/<effect>_effect.h")
@@ -347,6 +312,7 @@ def main():
shader_file = sys.argv[1]
effect_name = sys.argv[2]
+ is_post_process = '--post-process' in sys.argv
# Ensure effect name is CamelCase
if '_' in effect_name:
@@ -362,6 +328,7 @@ def main():
# 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
@@ -377,36 +344,43 @@ def main():
print()
# Write files
- header_path.write_text(generate_header(effect_name))
- impl_path.write_text(generate_implementation(effect_name))
- shader_path.write_text(generate_shader(effect_name, shadertoy_code))
+ 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))
- print("✓ Files generated")
+ 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" shaders/{snake_name}.wgsl")
+ 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("2. Include header in src/gpu/demo_effects.h:")
+ print("4. Include header in src/gpu/demo_effects.h:")
print(f' #include "gpu/effects/{snake_name}_effect.h"')
print()
- print("3. Add to timeline in workspaces/main/timeline.seq:")
+ print("5. Add to timeline in workspaces/main/timeline.seq:")
print(f" EFFECT + {effect_name}Effect 0.0 10.0")
print()
- print("4. Add to CMakeLists.txt GPU_SOURCES (both headless and normal mode):")
+ print("6. Add to CMakeLists.txt GPU_SOURCES (both headless and normal mode):")
print(f" src/gpu/effects/{snake_name}_effect.cc")
print()
- print("5. Update src/tests/gpu/test_demo_effects.cc:")
- print(f' - Add "{{{effect_name}Effect", std::make_shared<{effect_name}Effect>(fixture.ctx())}}" to appropriate list')
- print(" - Use post_process_effects (lines 80-93) for post-process effects")
- print(" - Use scene_effects (lines 125-137) for scene effects")
+ 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("6. Build and test:")
+ print("8. Build and test:")
print(" cmake --build build -j4")
print(" ./build/demo64k")
print()
- print("Note: WGSL conversion is basic - review and fix shader manually!")
+ print("Note: Review generated shader for const expression issues (normalize, etc)")
if __name__ == '__main__':
main()