summaryrefslogtreecommitdiff
path: root/tools/shadertoy
diff options
context:
space:
mode:
Diffstat (limited to 'tools/shadertoy')
-rw-r--r--tools/shadertoy/README.md211
-rwxr-xr-xtools/shadertoy/convert_shadertoy.py255
-rw-r--r--tools/shadertoy/template.cc140
-rw-r--r--tools/shadertoy/template.h39
-rw-r--r--tools/shadertoy/template.wgsl75
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 = &target;
-
- 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);
}