# Effect Creation Workflow **Target Audience:** AI coding agents and developers Checklist for adding visual effects. --- ## Quick Reference **ShaderToy:** `tools/shadertoy/convert_shadertoy.py` then follow steps below **SDF/Raymarching:** See `doc/SDF_EFFECT_GUIDE.md` **Custom effects:** Follow all steps 1-6 --- ## Workflow ### 1. Create Effect Files **Files**: - Header: `src/effects/_effect.h` - Implementation: `src/effects/_effect.cc` - Shader: `workspaces/main/shaders/.wgsl` **Class name**: `Effect` (e.g., `TunnelEffect`) **Base class**: `Effect` (all effects) **Constructor**: ```cpp MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs); ``` **Required methods**: ```cpp void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) override; // Optional: for effects needing temp nodes (depth buffers, intermediate textures) void declare_nodes(NodeRegistry& registry) override; ``` **Uniforms**: ```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 **File**: `workspaces/main/assets.txt` ``` SHADER_, NONE, shaders/.wgsl, "Description" ``` Asset ID: `AssetId::ASSET_SHADER_` ### 3. Add to CMakeLists.txt **File**: `CMakeLists.txt` Add `src/effects/_effect.cc` to **BOTH** GPU_SOURCES sections: - Headless mode (around line 141-167) - Normal mode (around line 171-197) ### 4. Include in demo_effects.h **File**: `src/gpu/demo_effects.h` ```cpp #include "effects/_effect.h" ``` ### 5. Add to Timeline **File**: `workspaces/main/timeline.seq` ``` SEQUENCE "name" EFFECT + MyEffect source -> sink 0.0 4.0 ``` **Priority modifiers** (REQUIRED): `+` (increment), `=` (same), `-` (decrement) ### 6. Regenerate and Build ```bash # Regenerate timeline.cc python3 tools/seq_compiler.py workspaces/main/timeline.seq \ --output src/generated/timeline.cc # Build cmake --build build -j4 # Test ./build/demo64k ``` --- ## Templates ### Standard Post-Process Effect ```cpp // my_effect.h #pragma once #include "gpu/effect.h" #include "gpu/uniform_helper.h" class MyEffect : public Effect { public: MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs); ~MyEffect() override; void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) override; private: WGPURenderPipeline pipeline_; WGPUBindGroup bind_group_; UniformBuffer uniforms_buffer_; }; ``` ```cpp // my_effect.cc #include "effects/my_effect.h" #include "gpu/post_process_helper.h" #include "gpu/shaders.h" MyEffect::MyEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs) : Effect(ctx, inputs, outputs), pipeline_(nullptr), bind_group_(nullptr) { uniforms_buffer_.init(ctx_.device); pipeline_ = create_post_process_pipeline(ctx_.device, WGPUTextureFormat_RGBA8Unorm, my_shader_wgsl); } MyEffect::~MyEffect() { if (bind_group_) wgpuBindGroupRelease(bind_group_); if (pipeline_) wgpuRenderPipelineRelease(pipeline_); } 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]); uniforms_buffer_.update(ctx_.queue, params); pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, uniforms_buffer_.get(), {nullptr, 0}); WGPURenderPassColorAttachment color_attachment = { .view = output_view, #if !defined(DEMO_CROSS_COMPILE_WIN32) .depthSlice = WGPU_DEPTH_SLICE_UNDEFINED, #endif .loadOp = WGPULoadOp_Clear, .storeOp = WGPUStoreOp_Store, .clearValue = {0.0, 0.0, 0.0, 1.0} }; WGPURenderPassDescriptor pass_desc = { .colorAttachmentCount = 1, .colorAttachments = &color_attachment }; WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); wgpuRenderPassEncoderSetPipeline(pass, pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Fullscreen triangle wgpuRenderPassEncoderEnd(pass); wgpuRenderPassEncoderRelease(pass); } ``` ### 3D Effect with Depth ```cpp class My3DEffect : public Effect { std::string depth_node_; My3DEffect(const GpuContext& ctx, ...) : Effect(ctx, inputs, outputs), depth_node_(outputs[0] + "_depth") {} 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_); WGPURenderPassDepthStencilAttachment depth_attachment = { .view = depth_view, .depthLoadOp = WGPULoadOp_Clear, .depthStoreOp = WGPUStoreOp_Discard, .depthClearValue = 1.0f }; WGPURenderPassDescriptor pass_desc = { .colorAttachmentCount = 1, .colorAttachments = &color_attachment, .depthStencilAttachment = &depth_attachment }; // ... 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"** - Effect not in CMakeLists.txt GPU_SOURCES - Must add to BOTH sections (headless + normal) **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 --- ## See Also - `doc/SEQUENCE.md` - Timeline syntax and architecture - `tools/seq_compiler.py` - Compiler implementation - `src/effects/*.{h,cc}` - Example effects