# Effect Creation Workflow **Target Audience:** AI coding agents and developers Checklist for adding visual effects. --- ## Quick Reference | Effect type | Path | |---|---| | Simple post-process (shader only) | **WgslEffect** — 3 files, see below | | Custom uniforms / multi-pass / compute / 3D | **Full Effect class** — 6 steps | | SDF / Raymarching | See `doc/SDF_EFFECT_GUIDE.md` | | ShaderToy port | `tools/shadertoy/convert_shadertoy.py` then Full Effect | --- ## Path A: WgslEffect (simple post-process) Use when the effect needs only a WGSL shader and standard per-frame uniforms (`time`, `beat_phase`, `audio_intensity`, `resolution`). No custom C++ logic. **3 files to touch:** ### 1. Write the shader `src/effects/.wgsl` Standard bindings (all provided automatically): ```wgsl #include "sequence_uniforms" // or "common_uniforms" #include "render/fullscreen_vs" @group(0) @binding(0) var smplr: sampler; @group(0) @binding(1) var txt: texture_2d; @group(0) @binding(2) var uniforms: UniformsSequenceParams; // Optional: generic params from WgslEffectParams (see below) // struct WgslEffectParams { p: vec4f, c: vec4f } // @group(0) @binding(3) var effect_params: WgslEffectParams; @fragment fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f { let uv = pos.xy / uniforms.resolution; // ... your effect return textureSample(txt, smplr, uv); } ``` ### 2. Register shader as asset `workspaces/main/assets.txt`: ``` SHADER_, NONE, ../../src/effects/.wgsl, "Description" ``` ### 3. Add to timeline `workspaces/main/timeline.seq`: ``` EFFECT + WgslEffect source -> sink 0.0 4.0 ``` Wait — the seq_compiler instantiates effects by class name. For a named effect (so it appears in test_demo_effects and can be referenced by name), create a **thin wrapper header** instead: `src/effects/_effect.h`: ```cpp #pragma once #include "effects/shaders.h" #include "gpu/wgsl_effect.h" struct MyEffect : public WgslEffect { MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, float end_time) : WgslEffect(ctx, inputs, outputs, start_time, end_time, my_shader_wgsl) {} }; ``` Then add `extern const char* my_shader_wgsl;` to `src/effects/shaders.h` and `const char* my_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_);` to `src/effects/shaders.cc`. Finally include the header in `src/gpu/demo_effects.h` and add a test entry in `src/tests/gpu/test_demo_effects.cc`. **No `.cc` file and no CMake entry needed.** ### WgslEffectParams (optional static or dynamic params) `WgslEffect` has a public `effect_params` member (`WgslEffectParams`) uploaded to binding 3 each frame. Use it for static configuration or per-frame modulation. ```cpp struct WgslEffectParams { float p[4]; // vec4: generic floats (strength, scale, etc.) float c[4]; // vec4: color or secondary params }; ``` **Static** (set at construction via thin wrapper): ```cpp : WgslEffect(ctx, inputs, outputs, start, end, my_shader_wgsl, WGPULoadOp_Clear, WgslEffectParams{{8.0f, 0.5f, 1.0f, 0.0f}, {}}) ``` **Dynamic** (per-frame, from generated Sequence subclass or external code): ```cpp WgslEffect* fx = ...; // store typed ptr alongside shared_ptr // each frame before render: fx->effect_params.p[0] = 8.0f + seq_params.audio_intensity * 4.0f; ``` Prefer computing animated values in WGSL via `uniforms.time/beat_phase/audio_intensity` when possible — no CPU→GPU upload needed. **loadOp** (default `WGPULoadOp_Clear`): pass `WGPULoadOp_Load` to overlay on the existing buffer (e.g. HUD overlays like PeakMeter). --- ## Path B: Full Effect Class (complex effects) Use when the effect needs: custom uniforms (binding 3 typed struct), multi-pass, compute shaders, 3D depth buffers, or non-trivial C++ render logic. ### 1. Create Effect Files - Header: `src/effects/_effect.h` - Implementation: `src/effects/_effect.cc` - Shader: `src/effects/.wgsl` **Base class**: `Effect` **Constructor**: ```cpp MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, float end_time); ``` **Required methods**: ```cpp void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) override; // Optional: for effects needing temp nodes (depth buffers, etc.) void declare_nodes(NodeRegistry& registry) override; ``` **Uniforms** (auto-updated by base class): ```cpp params.time; // Physical seconds params.beat_time; // Musical beats params.beat_phase; // Fractional beat 0.0-1.0 params.audio_intensity; // Audio peak params.resolution; // vec2(width, height) params.aspect_ratio; // width/height ``` ### 2. Add Shader to Assets `workspaces/main/assets.txt`: ``` SHADER_, NONE, ../../src/effects/.wgsl, "Description" ``` ### 3. Add to DemoSourceLists.cmake `cmake/DemoSourceLists.cmake` — add to `COMMON_GPU_EFFECTS`: ```cmake src/effects/_effect.cc ``` ### 4. Include in demo_effects.h `src/gpu/demo_effects.h`: ```cpp #include "effects/_effect.h" ``` ### 5. Add to test_demo_effects.cc `src/tests/gpu/test_demo_effects.cc`: ```cpp {"MyEffect", std::make_shared( fixture.ctx(), inputs, outputs, 0.0f, 1000.0f)}, ``` ### 6. Add to Timeline `workspaces/main/timeline.seq`: ``` SEQUENCE "name" EFFECT + MyEffect source -> sink 0.0 4.0 ``` **Priority modifiers** (REQUIRED): `+` (increment), `=` (same), `-` (decrement) ### 7. Build and Verify ```bash cmake --build build -j4 cd build && ./test_demo_effects ./demo64k ``` --- ## Templates ### WgslEffect thin wrapper ```cpp // src/effects/my_effect.h #pragma once #include "effects/shaders.h" #include "gpu/wgsl_effect.h" struct MyEffect : public WgslEffect { MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, float end_time) : WgslEffect(ctx, inputs, outputs, start_time, end_time, my_shader_wgsl) {} }; ``` No `.cc` file. No CMake entry. ### Full post-process effect ```cpp // my_effect.cc #include "effects/my_effect.h" #include "gpu/post_process_helper.h" #include "effects/shaders.h" MyEffect::MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& 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, my_shader_wgsl)); } void MyEffect::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]); pp_update_bind_group(ctx_.device, pipeline_.get(), bind_group_.get_address(), input_view, 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); } ``` ### 3D Effect with Depth ```cpp class My3DEffect : public Effect { std::string depth_node_; My3DEffect(const GpuContext& ctx, ..., float start_time, float end_time) : Effect(ctx, inputs, outputs, start_time, end_time), depth_node_(outputs[0] + "_depth") { HEADLESS_RETURN_IF_NULL(ctx_.device); } void declare_nodes(NodeRegistry& registry) override { registry.declare_node(depth_node_, NodeType::DEPTH24, -1, -1); } void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) override { WGPUTextureView color_view = nodes.get_view(output_nodes_[0]); WGPUTextureView depth_view = nodes.get_view(depth_node_); // ... render 3D scene } }; ``` --- ## Common Issues **Build Error: "no member named 'ASSET_..._SHADER'"** - Shader not in `assets.txt` or wrong name - Asset ID is `ASSET_` + uppercase entry name **Build Error: "undefined symbol"** - Full effect `.cc` not in `cmake/DemoSourceLists.cmake` COMMON_GPU_EFFECTS - WgslEffect thin wrappers do NOT need a CMake entry **Runtime Error: "Node not found"** - Forgot `declare_nodes()` for temp nodes - `init_effect_nodes()` not called (check generated timeline.cc) **Runtime Error: "invalid bind group"** - Pipeline format doesn't match framebuffer (use RGBA8Unorm) - Missing texture view or null resource **Build Error: generated `timeline.cc` includes wrong header (e.g. `ntsc_yiq_effect.h`)** - Occurs when multiple effect classes share one header file (e.g. `Ntsc` and `NtscYiq` both in `ntsc_effect.h`) - `seq_compiler.py` derives the header from the class name by default - Fix: add an entry to `CLASS_TO_HEADER` in `tools/seq_compiler.py`: ```python CLASS_TO_HEADER = { 'NtscYiq': 'ntsc', # NtscYiq lives in ntsc_effect.h alongside Ntsc } ``` --- ## See Also - `src/gpu/wgsl_effect.h` — WgslEffect and WgslEffectParams definitions - `src/effects/scratch_effect.h` — minimal WgslEffect thin wrapper example - `src/effects/gaussian_blur_effect.h` — WgslEffect with static params example - `doc/SEQUENCE.md` — Timeline syntax and architecture - `doc/SDF_EFFECT_GUIDE.md` — SDF/raymarching effects - `tools/seq_compiler.py` — Compiler implementation