diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-10 12:48:43 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-10 12:48:43 +0100 |
| commit | 6944733a6a2f05c18e7e0b73f847a4c9144801fd (patch) | |
| tree | 10713cd41a0e038a016a2e6b357471690f232834 | |
| parent | cc9cbeb75353181193e3afb880dc890aa8bf8985 (diff) | |
feat: Add multi-layer CNN support with framebuffer capture and blend control
Implements automatic layer chaining and generic framebuffer capture API for
multi-layer neural network effects with proper original input preservation.
Key changes:
- Effect::needs_framebuffer_capture() - generic API for pre-render capture
- MainSequence: auto-capture to "captured_frame" auxiliary texture
- CNNEffect: multi-layer support via layer_index/total_layers params
- seq_compiler: expands "layers=N" to N chained effect instances
- Shader: @binding(4) original_input available to all layers
- Training: generates layer switches and original input binding
- Blend: mix(original, result, blend_amount) uses layer 0 input
Timeline syntax: CNNEffect layers=3 blend=0.7
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | doc/CNN_EFFECT.md | 150 | ||||
| -rw-r--r-- | src/gpu/effect.cc | 37 | ||||
| -rw-r--r-- | src/gpu/effect.h | 6 | ||||
| -rw-r--r-- | src/gpu/effects/cnn_effect.cc | 130 | ||||
| -rw-r--r-- | src/gpu/effects/cnn_effect.h | 22 | ||||
| -rw-r--r-- | src/tests/gpu/test_demo_effects.cc | 2 | ||||
| -rw-r--r-- | tools/seq_compiler.cc | 32 | ||||
| -rwxr-xr-x | training/train_cnn.py | 159 | ||||
| -rw-r--r-- | workspaces/main/shaders/cnn/cnn_layer.wgsl | 25 | ||||
| -rw-r--r-- | workspaces/main/shaders/cnn/cnn_weights_generated.wgsl | 194 | ||||
| -rw-r--r-- | workspaces/main/timeline.seq | 190 |
11 files changed, 777 insertions, 170 deletions
diff --git a/doc/CNN_EFFECT.md b/doc/CNN_EFFECT.md index ec70b13..ae0f38a 100644 --- a/doc/CNN_EFFECT.md +++ b/doc/CNN_EFFECT.md @@ -10,10 +10,11 @@ Trainable convolutional neural network layers for artistic stylization (painterl **Key Features:** - Position-aware layer 0 (coordinate input for vignetting, edge effects) -- Multi-layer convolutions (3×3, 5×5, 7×7 kernels) +- Multi-layer convolutions (3×3, 5×5, 7×7 kernels) with automatic chaining +- Original input available to all layers via framebuffer capture +- Configurable final blend with original scene - Modular WGSL shader architecture - Hardcoded weights (trained offline via PyTorch) -- Residual connections for stable learning - ~5-8 KB binary footprint --- @@ -42,19 +43,34 @@ fn cnn_conv3x3_with_coord( **Use cases:** Position-dependent stylization (vignettes, corner darkening, radial gradients) +### Multi-Layer Architecture + +CNNEffect supports multi-layer networks via automatic effect chaining: + +1. **Timeline specifies total layers**: `CNNEffect layers=3 blend=0.7` +2. **Compiler expands to chain**: 3 separate CNNEffect instances (layer 0→1→2) +3. **Framebuffer capture**: Layer 0 captures original input to `"captured_frame"` +4. **Original input binding**: All layers access original via `@binding(4)` +5. **Final blend**: Last layer blends result with original: `mix(original, result, 0.7)` + +**Framebuffer Capture API:** +- `Effect::needs_framebuffer_capture()` - effect requests pre-capture +- MainSequence automatically blits input → `"captured_frame"` auxiliary texture +- Generic mechanism usable by any effect + ### File Structure ``` src/gpu/effects/ - cnn_effect.h/cc # CNNEffect class + cnn_effect.h/cc # CNNEffect class + framebuffer capture workspaces/main/shaders/cnn/ cnn_activation.wgsl # tanh, ReLU, sigmoid, leaky_relu cnn_conv3x3.wgsl # 3×3 convolution (standard + coord-aware) cnn_conv5x5.wgsl # 5×5 convolution (standard + coord-aware) cnn_conv7x7.wgsl # 7×7 convolution (standard + coord-aware) - cnn_weights_generated.wgsl # Weight arrays (auto-generated) - cnn_layer.wgsl # Main shader (composes above snippets) + cnn_weights_generated.wgsl # Weight arrays (auto-generated by train_cnn.py) + cnn_layer.wgsl # Main shader with layer switches (auto-generated by train_cnn.py) ``` --- @@ -89,7 +105,7 @@ python3 training/train_cnn.py \ --checkpoint-every 50 ``` -**Multi-layer example:** +**Multi-layer example (3 layers with varying kernel sizes):** ```bash python3 training/train_cnn.py \ --input training/input \ @@ -100,6 +116,10 @@ python3 training/train_cnn.py \ --checkpoint-every 100 ``` +**Note:** Training script auto-generates: +- `cnn_weights_generated.wgsl` - weight arrays for all layers +- `cnn_layer.wgsl` - shader with layer switches and original input binding + **Resume from checkpoint:** ```bash python3 training/train_cnn.py \ @@ -108,9 +128,16 @@ python3 training/train_cnn.py \ --resume training/checkpoints/checkpoint_epoch_200.pth ``` +**Export WGSL from checkpoint (no training):** +```bash +python3 training/train_cnn.py \ + --export-only training/checkpoints/checkpoint_epoch_200.pth \ + --output workspaces/main/shaders/cnn/cnn_weights_generated.wgsl +``` + ### 3. Rebuild Demo -Training script auto-generates `cnn_weights_generated.wgsl`: +Training script auto-generates both `cnn_weights_generated.wgsl` and `cnn_layer.wgsl`: ```bash cmake --build build -j4 ./build/demo64k @@ -122,23 +149,101 @@ cmake --build build -j4 ### C++ Integration +**Single layer (manual):** ```cpp #include "gpu/effects/cnn_effect.h" -auto cnn = std::make_shared<CNNEffect>(ctx, /*num_layers=*/1); +CNNEffectParams p; +p.layer_index = 0; +p.total_layers = 1; +p.blend_amount = 1.0f; +auto cnn = std::make_shared<CNNEffect>(ctx, p); timeline.add_effect(cnn, start_time, end_time); ``` -### Timeline Example +**Multi-layer (automatic via timeline compiler):** +Use timeline syntax - `seq_compiler` expands to multiple instances. + +### Timeline Examples + +**Single-layer CNN (full stylization):** +``` +SEQUENCE 10.0 0 + EFFECT + Hybrid3DEffect 0.00 5.00 + EFFECT + CNNEffect 0.50 5.00 layers=1 +``` + +**Multi-layer CNN with blend:** ``` SEQUENCE 10.0 0 - EFFECT CNNEffect 10.0 15.0 0 + EFFECT + Hybrid3DEffect 0.00 5.00 + EFFECT + CNNEffect 0.50 5.00 layers=3 blend=0.7 +``` + +Expands to: +```cpp +// Layer 0 (captures original, blend=1.0) +{ + CNNEffectParams p; + p.layer_index = 0; + p.total_layers = 3; + p.blend_amount = 1.0f; + seq->add_effect(std::make_shared<CNNEffect>(ctx, p), 0.50f, 5.00f, 1); +} +// Layer 1 (blend=1.0) +{ + CNNEffectParams p; + p.layer_index = 1; + p.total_layers = 3; + p.blend_amount = 1.0f; + seq->add_effect(std::make_shared<CNNEffect>(ctx, p), 0.50f, 5.00f, 2); +} +// Layer 2 (final blend=0.7) +{ + CNNEffectParams p; + p.layer_index = 2; + p.total_layers = 3; + p.blend_amount = 0.7f; + seq->add_effect(std::make_shared<CNNEffect>(ctx, p), 0.50f, 5.00f, 3); +} ``` --- -## Weight Storage +## Shader Structure + +**Bindings:** +```wgsl +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d<f32>; // Current layer input +@group(0) @binding(2) var<uniform> uniforms: CommonUniforms; +@group(0) @binding(3) var<uniform> params: CNNLayerParams; +@group(0) @binding(4) var original_input: texture_2d<f32>; // Layer 0 input (captured) +``` + +**Fragment shader logic:** +```wgsl +@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { + let uv = p.xy / uniforms.resolution; + let input = textureSample(txt, smplr, uv); // Layer N-1 output + let original = textureSample(original_input, smplr, uv); // Layer 0 input + + var result = vec4<f32>(0.0); + + if (params.layer_index == 0) { + result = cnn_conv3x3_with_coord(txt, smplr, uv, uniforms.resolution, + rgba_weights_layer0, coord_weights_layer0, bias_layer0); + result = cnn_tanh(result); + } + // ... other layers + + // Blend with ORIGINAL input (not previous layer) + return mix(original, result, params.blend_amount); +} +``` + +**Weight Storage:** **Layer 0 (coordinate-aware):** ```wgsl @@ -188,15 +293,36 @@ const bias_layer1 = vec4<f32>(0.0, 0.0, 0.0, 0.0); --- +## Blend Parameter Behavior + +**blend_amount** controls final compositing with original: +- `blend=0.0`: Pure original (no CNN effect) +- `blend=0.5`: 50% original + 50% CNN +- `blend=1.0`: Pure CNN output (full stylization) + +**Important:** Blend uses captured layer 0 input, not previous layer output. + +**Example use cases:** +- `blend=1.0`: Full stylization (default) +- `blend=0.7`: Subtle effect preserving original details +- `blend=0.3`: Light artistic touch + ## Troubleshooting **Shader compilation fails:** - Check `cnn_weights_generated.wgsl` syntax - Verify snippets registered in `shaders.cc::InitShaderComposer()` +- Ensure `cnn_layer.wgsl` has 5 bindings (including `original_input`) **Black/corrupted output:** - Weights untrained (identity placeholder) -- Check residual blending (0.3 default) +- Check `captured_frame` auxiliary texture is registered +- Verify layer priorities in timeline are sequential + +**Wrong blend result:** +- Ensure layer 0 has `needs_framebuffer_capture() == true` +- Check MainSequence framebuffer capture logic +- Verify `original_input` binding is populated **Training loss not decreasing:** - Lower learning rate (`--learning-rate 0.0001`) diff --git a/src/gpu/effect.cc b/src/gpu/effect.cc index 6a4762c..0662f26 100644 --- a/src/gpu/effect.cc +++ b/src/gpu/effect.cc @@ -65,7 +65,7 @@ void Sequence::update_active_list(float seq_time) { #if !defined(STRIP_ALL) Effect* effect_ptr = item.effect.get(); const char* effect_name = typeid(*effect_ptr).name(); - printf(" [EFFECT START] %s (priority=%d, time=%.2f-%.2f)\n", effect_name, + printf(" [EFFECT START] <%s> (priority=%d, time=%.2f-%.2f)\n", effect_name, item.priority, item.start_time, item.end_time); #endif item.effect->start(); @@ -74,7 +74,7 @@ void Sequence::update_active_list(float seq_time) { #if !defined(STRIP_ALL) Effect* effect_ptr = item.effect.get(); const char* effect_name = typeid(*effect_ptr).name(); - printf(" [EFFECT END] %s (priority=%d)\n", effect_name, item.priority); + printf(" [EFFECT END] <%s> (priority=%d)\n", effect_name, item.priority); #endif item.effect->end(); item.active = false; @@ -339,6 +339,39 @@ void MainSequence::render_frame(float global_time, float beat, float peak, PostProcessEffect* pp = (PostProcessEffect*)(post_effects[i]->effect.get()); + + // Capture framebuffer if effect needs it + if (pp->needs_framebuffer_capture()) { + WGPUTextureView captured_view = get_auxiliary_view("captured_frame"); + if (captured_view) { + // Get source texture from current_input view + // Note: This is a simplified blit using a render pass + WGPURenderPassColorAttachment capture_attachment = {}; + capture_attachment.view = captured_view; + capture_attachment.resolveTarget = nullptr; + capture_attachment.loadOp = WGPULoadOp_Load; + capture_attachment.storeOp = WGPUStoreOp_Store; +#if !defined(DEMO_CROSS_COMPILE_WIN32) + capture_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; +#endif + WGPURenderPassDescriptor capture_desc = { + .colorAttachmentCount = 1, .colorAttachments = &capture_attachment}; + WGPURenderPassEncoder capture_pass = + wgpuCommandEncoderBeginRenderPass(encoder, &capture_desc); + wgpuRenderPassEncoderSetViewport(capture_pass, 0.0f, 0.0f, + (float)width_, (float)height_, 0.0f, + 1.0f); + + // Use passthrough effect to copy current_input to captured_frame + PostProcessEffect* passthrough = + (PostProcessEffect*)passthrough_effect_.get(); + passthrough->update_bind_group(current_input); + passthrough->render(capture_pass, 0, 0, 0, aspect_ratio); + + wgpuRenderPassEncoderEnd(capture_pass); + } + } + pp->update_bind_group(current_input); WGPURenderPassColorAttachment pp_attachment = {}; diff --git a/src/gpu/effect.h b/src/gpu/effect.h index 8f35f3c..f008c8d 100644 --- a/src/gpu/effect.h +++ b/src/gpu/effect.h @@ -44,6 +44,12 @@ class Effect { return false; } + // If true, MainSequence will capture current framebuffer to "captured_frame" + // auxiliary texture before rendering this effect + virtual bool needs_framebuffer_capture() const { + return false; + } + protected: const GpuContext& ctx_; UniformBuffer<CommonPostProcessUniforms> uniforms_; diff --git a/src/gpu/effects/cnn_effect.cc b/src/gpu/effects/cnn_effect.cc index 25db0c2..f5d0a51 100644 --- a/src/gpu/effects/cnn_effect.cc +++ b/src/gpu/effects/cnn_effect.cc @@ -4,19 +4,101 @@ #include "gpu/effects/cnn_effect.h" #include "gpu/effects/post_process_helper.h" #include "gpu/effects/shaders.h" +#include "gpu/effects/shader_composer.h" +#include "gpu/effect.h" -CNNEffect::CNNEffect(const GpuContext& ctx, int num_layers) - : PostProcessEffect(ctx), num_layers_(num_layers), input_view_(nullptr), +// Create custom pipeline with 5 bindings (includes original texture) +static WGPURenderPipeline create_cnn_pipeline(WGPUDevice device, + WGPUTextureFormat format, + const char* shader_code) { + std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code); + + WGPUShaderModuleDescriptor shader_desc = {}; + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = str_view(composed_shader.c_str()); + shader_desc.nextInChain = &wgsl_src.chain; + WGPUShaderModule shader_module = + wgpuDeviceCreateShaderModule(device, &shader_desc); + + WGPUBindGroupLayoutEntry bgl_entries[5] = {}; + bgl_entries[0].binding = 0; // sampler + bgl_entries[0].visibility = WGPUShaderStage_Fragment; + bgl_entries[0].sampler.type = WGPUSamplerBindingType_Filtering; + bgl_entries[1].binding = 1; // input texture + bgl_entries[1].visibility = WGPUShaderStage_Fragment; + bgl_entries[1].texture.sampleType = WGPUTextureSampleType_Float; + bgl_entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; + bgl_entries[2].binding = 2; // uniforms + bgl_entries[2].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + bgl_entries[2].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[3].binding = 3; // effect params + bgl_entries[3].visibility = WGPUShaderStage_Fragment; + bgl_entries[3].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[4].binding = 4; // original texture + bgl_entries[4].visibility = WGPUShaderStage_Fragment; + bgl_entries[4].texture.sampleType = WGPUTextureSampleType_Float; + bgl_entries[4].texture.viewDimension = WGPUTextureViewDimension_2D; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 5; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc); + + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &bgl; + WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(device, &pl_desc); + + WGPUColorTargetState color_target = {}; + color_target.format = format; + color_target.writeMask = WGPUColorWriteMask_All; + + WGPUFragmentState fragment_state = {}; + fragment_state.module = shader_module; + fragment_state.entryPoint = str_view("fs_main"); + fragment_state.targetCount = 1; + fragment_state.targets = &color_target; + + WGPURenderPipelineDescriptor pipeline_desc = {}; + pipeline_desc.layout = pl; + pipeline_desc.vertex.module = shader_module; + pipeline_desc.vertex.entryPoint = str_view("vs_main"); + pipeline_desc.fragment = &fragment_state; + pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = 0xFFFFFFFF; + + return wgpuDeviceCreateRenderPipeline(device, &pipeline_desc); +} + +CNNEffect::CNNEffect(const GpuContext& ctx) + : PostProcessEffect(ctx), layer_index_(0), total_layers_(1), + blend_amount_(1.0f), input_view_(nullptr), original_view_(nullptr), bind_group_(nullptr) { - pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, - cnn_layer_shader_wgsl); + pipeline_ = create_cnn_pipeline(ctx_.device, ctx_.format, + cnn_layer_shader_wgsl); +} + +CNNEffect::CNNEffect(const GpuContext& ctx, const CNNEffectParams& params) + : PostProcessEffect(ctx), layer_index_(params.layer_index), + total_layers_(params.total_layers), blend_amount_(params.blend_amount), + input_view_(nullptr), original_view_(nullptr), bind_group_(nullptr) { + pipeline_ = create_cnn_pipeline(ctx_.device, ctx_.format, + cnn_layer_shader_wgsl); } void CNNEffect::init(MainSequence* demo) { PostProcessEffect::init(demo); + demo_ = demo; params_buffer_.init(ctx_.device); - CNNLayerParams params = {0, 1, {0.0f, 0.0f}}; + // Register captured_frame texture (used by all layers for original input) + if (layer_index_ == 0) { + demo_->register_auxiliary_texture("captured_frame", width_, height_); + } + + CNNLayerParams params = {layer_index_, blend_amount_, {0.0f, 0.0f}}; params_buffer_.update(ctx_.queue, params); } @@ -31,6 +113,40 @@ void CNNEffect::render(WGPURenderPassEncoder pass, float time, float beat, void CNNEffect::update_bind_group(WGPUTextureView input_view) { input_view_ = input_view; - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, - input_view_, uniforms_.get(), params_buffer_.get()); + + // All layers: get captured frame (original input from layer 0) + if (demo_) { + original_view_ = demo_->get_auxiliary_view("captured_frame"); + } + + // Create bind group with original texture + if (bind_group_) + wgpuBindGroupRelease(bind_group_); + + WGPUBindGroupLayout bgl = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0); + WGPUSamplerDescriptor sd = {}; + sd.magFilter = WGPUFilterMode_Linear; + sd.minFilter = WGPUFilterMode_Linear; + sd.maxAnisotropy = 1; + WGPUSampler sampler = wgpuDeviceCreateSampler(ctx_.device, &sd); + + WGPUBindGroupEntry bge[5] = {}; + bge[0].binding = 0; + bge[0].sampler = sampler; + bge[1].binding = 1; + bge[1].textureView = input_view_; + bge[2].binding = 2; + bge[2].buffer = uniforms_.get().buffer; + bge[2].size = uniforms_.get().size; + bge[3].binding = 3; + bge[3].buffer = params_buffer_.get().buffer; + bge[3].size = params_buffer_.get().size; + bge[4].binding = 4; + bge[4].textureView = original_view_ ? original_view_ : input_view_; // Fallback + + WGPUBindGroupDescriptor bgd = {}; + bgd.layout = bgl; + bgd.entryCount = 5; + bgd.entries = bge; + bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bgd); } diff --git a/src/gpu/effects/cnn_effect.h b/src/gpu/effects/cnn_effect.h index 9cc4935..bc074d2 100644 --- a/src/gpu/effects/cnn_effect.h +++ b/src/gpu/effects/cnn_effect.h @@ -7,23 +7,39 @@ struct CNNLayerParams { int layer_index; - int use_residual; + float blend_amount; // Blend: mix(input, output, blend_amount) float _pad[2]; }; static_assert(sizeof(CNNLayerParams) == 16); +struct CNNEffectParams { + int layer_index = 0; // Which layer to render (0-based) + int total_layers = 1; // Total number of layers in the CNN + float blend_amount = 1.0f; // Final blend with original input +}; + class CNNEffect : public PostProcessEffect { public: - explicit CNNEffect(const GpuContext& ctx, int num_layers = 1); + explicit CNNEffect(const GpuContext& ctx); + explicit CNNEffect(const GpuContext& ctx, const CNNEffectParams& params); void init(MainSequence* demo) override; void render(WGPURenderPassEncoder pass, float time, float beat, float intensity, float aspect_ratio) override; void update_bind_group(WGPUTextureView input_view) override; + // Layer 0 needs framebuffer capture for original input + bool needs_framebuffer_capture() const override { + return layer_index_ == 0; + } + private: - int num_layers_; + int layer_index_; + int total_layers_; + float blend_amount_; WGPUTextureView input_view_; + WGPUTextureView original_view_; UniformBuffer<CNNLayerParams> params_buffer_; WGPUBindGroup bind_group_; + MainSequence* demo_ = nullptr; }; diff --git a/src/tests/gpu/test_demo_effects.cc b/src/tests/gpu/test_demo_effects.cc index 9281413..619b9c9 100644 --- a/src/tests/gpu/test_demo_effects.cc +++ b/src/tests/gpu/test_demo_effects.cc @@ -89,7 +89,7 @@ static void test_post_process_effects() { {"ThemeModulationEffect", std::make_shared<ThemeModulationEffect>(fixture.ctx())}, {"VignetteEffect", std::make_shared<VignetteEffect>(fixture.ctx())}, - {"CNNEffect", std::make_shared<CNNEffect>(fixture.ctx(), 1)}, + {"CNNEffect", std::make_shared<CNNEffect>(fixture.ctx())}, }; int passed = 0; diff --git a/tools/seq_compiler.cc b/tools/seq_compiler.cc index 4a671f4..fad2d88 100644 --- a/tools/seq_compiler.cc +++ b/tools/seq_compiler.cc @@ -995,6 +995,38 @@ int main(int argc, char* argv[]) { << eff.class_name << ">(ctx, p), " << eff.start << "f, " << eff.end << "f, " << eff.priority << ");\n"; out_file << " }\n"; + } else if (!eff.params.empty() && eff.class_name == "CNNEffect") { + // Generate parameter struct initialization for CNNEffect + // If layers>1, expand into multiple chained effect instances + int num_layers = 1; + float blend_amount = 1.0f; + + for (const auto& [key, value] : eff.params) { + if (key == "layers") { + num_layers = std::stoi(value); + } else if (key == "blend") { + blend_amount = std::stof(value); + } + } + + // Generate one effect per layer + for (int layer = 0; layer < num_layers; ++layer) { + out_file << " {\n"; + out_file << " CNNEffectParams p;\n"; + out_file << " p.layer_index = " << layer << ";\n"; + out_file << " p.total_layers = " << num_layers << ";\n"; + // Only apply blend_amount on the last layer + if (layer == num_layers - 1) { + out_file << " p.blend_amount = " << blend_amount << "f;\n"; + } else { + out_file << " p.blend_amount = 1.0f;\n"; + } + out_file << " seq->add_effect(std::make_shared<" + << eff.class_name << ">(ctx, p), " << eff.start << "f, " + << eff.end << "f, " << (std::stoi(eff.priority) + layer) + << ");\n"; + out_file << " }\n"; + } } else { // No parameters or unsupported effect - use default constructor out_file << " seq->add_effect(std::make_shared<" << eff.class_name diff --git a/training/train_cnn.py b/training/train_cnn.py index 82f0b48..1cd6579 100755 --- a/training/train_cnn.py +++ b/training/train_cnn.py @@ -112,8 +112,6 @@ class SimpleCNN(nn.Module): else: self.layers.append(nn.Conv2d(3, 3, kernel_size=kernel_size, padding=padding, bias=True)) - self.use_residual = True - def forward(self, x): B, C, H, W = x.shape y_coords = torch.linspace(0, 1, H, device=x.device).view(1,1,H,1).expand(B,1,H,W) @@ -128,11 +126,77 @@ class SimpleCNN(nn.Module): if i < len(self.layers) - 1: out = torch.tanh(out) - if self.use_residual: - out = x + out * 0.3 return out +def generate_layer_shader(output_path, num_layers, kernel_sizes): + """Generate cnn_layer.wgsl with proper layer switches""" + + with open(output_path, 'w') as f: + f.write("// CNN layer shader - uses modular convolution snippets\n") + f.write("// Supports multi-pass rendering with residual connections\n") + f.write("// DO NOT EDIT - Generated by train_cnn.py\n\n") + f.write("@group(0) @binding(0) var smplr: sampler;\n") + f.write("@group(0) @binding(1) var txt: texture_2d<f32>;\n\n") + f.write("#include \"common_uniforms\"\n") + f.write("#include \"cnn_activation\"\n") + + # Include necessary conv functions + conv_sizes = set(kernel_sizes) + for ks in sorted(conv_sizes): + f.write(f"#include \"cnn_conv{ks}x{ks}\"\n") + f.write("#include \"cnn_weights_generated\"\n\n") + + f.write("struct CNNLayerParams {\n") + f.write(" layer_index: i32,\n") + f.write(" blend_amount: f32,\n") + f.write(" _pad: vec2<f32>,\n") + f.write("};\n\n") + f.write("@group(0) @binding(2) var<uniform> uniforms: CommonUniforms;\n") + f.write("@group(0) @binding(3) var<uniform> params: CNNLayerParams;\n") + f.write("@group(0) @binding(4) var original_input: texture_2d<f32>;\n\n") + f.write("@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {\n") + f.write(" var pos = array<vec2<f32>, 3>(\n") + f.write(" vec2<f32>(-1.0, -1.0), vec2<f32>(3.0, -1.0), vec2<f32>(-1.0, 3.0)\n") + f.write(" );\n") + f.write(" return vec4<f32>(pos[i], 0.0, 1.0);\n") + f.write("}\n\n") + f.write("@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {\n") + f.write(" let uv = p.xy / uniforms.resolution;\n") + f.write(" let input = textureSample(txt, smplr, uv);\n") + f.write(" let original = textureSample(original_input, smplr, uv);\n") + f.write(" var result = vec4<f32>(0.0);\n\n") + + # Generate layer switches + for layer_idx in range(num_layers): + ks = kernel_sizes[layer_idx] + if layer_idx == 0: + f.write(f" // Layer 0 uses coordinate-aware convolution\n") + f.write(f" if (params.layer_index == {layer_idx}) {{\n") + f.write(f" result = cnn_conv{ks}x{ks}_with_coord(txt, smplr, uv, uniforms.resolution,\n") + f.write(f" rgba_weights_layer{layer_idx}, coord_weights_layer{layer_idx}, bias_layer{layer_idx});\n") + f.write(f" result = cnn_tanh(result);\n") + f.write(f" }}\n") + else: + is_last = layer_idx == num_layers - 1 + f.write(f" {'else ' if layer_idx > 0 else ''}if (params.layer_index == {layer_idx}) {{\n") + f.write(f" result = cnn_conv{ks}x{ks}(txt, smplr, uv, uniforms.resolution,\n") + f.write(f" weights_layer{layer_idx}, bias_layer{layer_idx});\n") + if not is_last: + f.write(f" result = cnn_tanh(result);\n") + f.write(f" }}\n") + + # Add else clause for invalid layer index + if num_layers > 1: + f.write(f" else {{\n") + f.write(f" result = input;\n") + f.write(f" }}\n") + + f.write("\n // Blend with ORIGINAL input from layer 0\n") + f.write(" return mix(original, result, params.blend_amount);\n") + f.write("}\n") + + def export_weights_to_wgsl(model, output_path, kernel_sizes): """Export trained weights to WGSL format""" @@ -154,10 +218,13 @@ def export_weights_to_wgsl(model, output_path, kernel_sizes): row = pos // kw col = pos % kw f.write(" mat4x4<f32>(\n") - for out_c in range(min(4, out_ch)): + for out_c in range(4): vals = [] - for in_c in range(min(4, in_ch)): - vals.append(f"{weights[out_c, in_c, row, col]:.6f}") + for in_c in range(4): + if out_c < out_ch and in_c < in_ch: + vals.append(f"{weights[out_c, in_c, row, col]:.6f}") + else: + vals.append("0.0") f.write(f" {', '.join(vals)},\n") f.write(" )") if pos < num_positions - 1: @@ -170,7 +237,12 @@ def export_weights_to_wgsl(model, output_path, kernel_sizes): coord_w = layer.coord_weights.data.cpu().numpy() f.write(f"const coord_weights_layer{layer_idx} = mat2x4<f32>(\n") for c in range(2): - vals = [f"{coord_w[out_c, c]:.6f}" for out_c in range(min(4, coord_w.shape[0]))] + vals = [] + for out_c in range(4): + if out_c < coord_w.shape[0]: + vals.append(f"{coord_w[out_c, c]:.6f}") + else: + vals.append("0.0") f.write(f" {', '.join(vals)}") if c < 1: f.write(",\n") @@ -180,8 +252,9 @@ def export_weights_to_wgsl(model, output_path, kernel_sizes): # Export bias bias = layer.bias.data.cpu().numpy() + bias_vals = [f"{bias[i]:.6f}" if i < len(bias) else "0.0" for i in range(4)] f.write(f"const bias_layer{layer_idx} = vec4<f32>(") - f.write(", ".join([f"{b:.6f}" for b in bias[:4]])) + f.write(", ".join(bias_vals)) f.write(");\n\n") layer_idx += 1 @@ -197,10 +270,13 @@ def export_weights_to_wgsl(model, output_path, kernel_sizes): row = pos // kw col = pos % kw f.write(" mat4x4<f32>(\n") - for out_c in range(min(4, out_ch)): + for out_c in range(4): vals = [] - for in_c in range(min(4, in_ch)): - vals.append(f"{weights[out_c, in_c, row, col]:.6f}") + for in_c in range(4): + if out_c < out_ch and in_c < in_ch: + vals.append(f"{weights[out_c, in_c, row, col]:.6f}") + else: + vals.append("0.0") f.write(f" {', '.join(vals)},\n") f.write(" )") if pos < num_positions - 1: @@ -211,8 +287,9 @@ def export_weights_to_wgsl(model, output_path, kernel_sizes): # Export bias bias = layer.bias.data.cpu().numpy() + bias_vals = [f"{bias[i]:.6f}" if i < len(bias) else "0.0" for i in range(4)] f.write(f"const bias_layer{layer_idx} = vec4<f32>(") - f.write(", ".join([f"{b:.6f}" for b in bias[:4]])) + f.write(", ".join(bias_vals)) f.write(");\n\n") layer_idx += 1 @@ -293,19 +370,57 @@ def train(args): }, checkpoint_path) print(f"Saved checkpoint to {checkpoint_path}") - # Export weights + # Export weights and shader output_path = args.output or 'workspaces/main/shaders/cnn/cnn_weights_generated.wgsl' print(f"\nExporting weights to {output_path}...") os.makedirs(os.path.dirname(output_path), exist_ok=True) export_weights_to_wgsl(model, output_path, kernel_sizes) + # Generate layer shader + shader_dir = os.path.dirname(output_path) + shader_path = os.path.join(shader_dir, 'cnn_layer.wgsl') + print(f"Generating layer shader to {shader_path}...") + generate_layer_shader(shader_path, args.layers, kernel_sizes) + print("Training complete!") +def export_from_checkpoint(checkpoint_path, output_path=None): + """Export WGSL files from checkpoint without training""" + + if not os.path.exists(checkpoint_path): + print(f"Error: Checkpoint file '{checkpoint_path}' not found") + sys.exit(1) + + print(f"Loading checkpoint from {checkpoint_path}...") + checkpoint = torch.load(checkpoint_path, map_location='cpu') + + kernel_sizes = checkpoint['kernel_sizes'] + num_layers = checkpoint['num_layers'] + + # Recreate model + model = SimpleCNN(num_layers=num_layers, kernel_sizes=kernel_sizes) + model.load_state_dict(checkpoint['model_state']) + + # Export weights + output_path = output_path or 'workspaces/main/shaders/cnn/cnn_weights_generated.wgsl' + print(f"Exporting weights to {output_path}...") + os.makedirs(os.path.dirname(output_path), exist_ok=True) + export_weights_to_wgsl(model, output_path, kernel_sizes) + + # Generate layer shader + shader_dir = os.path.dirname(output_path) + shader_path = os.path.join(shader_dir, 'cnn_layer.wgsl') + print(f"Generating layer shader to {shader_path}...") + generate_layer_shader(shader_path, num_layers, kernel_sizes) + + print("Export complete!") + + def main(): parser = argparse.ArgumentParser(description='Train CNN for image-to-image transformation') - parser.add_argument('--input', required=True, help='Input image directory') - parser.add_argument('--target', required=True, help='Target image directory') + parser.add_argument('--input', help='Input image directory') + parser.add_argument('--target', help='Target image directory') parser.add_argument('--layers', type=int, default=1, help='Number of CNN layers (default: 1)') parser.add_argument('--kernel_sizes', default='3', help='Comma-separated kernel sizes (default: 3)') parser.add_argument('--epochs', type=int, default=100, help='Number of training epochs (default: 100)') @@ -315,10 +430,20 @@ def main(): parser.add_argument('--checkpoint-every', type=int, default=0, help='Save checkpoint every N epochs (default: 0 = disabled)') parser.add_argument('--checkpoint-dir', help='Checkpoint directory (default: training/checkpoints)') parser.add_argument('--resume', help='Resume from checkpoint file') + parser.add_argument('--export-only', help='Export WGSL from checkpoint without training') args = parser.parse_args() - # Validate directories + # Export-only mode + if args.export_only: + export_from_checkpoint(args.export_only, args.output) + return + + # Validate directories for training + if not args.input or not args.target: + print("Error: --input and --target required for training (or use --export-only)") + sys.exit(1) + if not os.path.isdir(args.input): print(f"Error: Input directory '{args.input}' does not exist") sys.exit(1) diff --git a/workspaces/main/shaders/cnn/cnn_layer.wgsl b/workspaces/main/shaders/cnn/cnn_layer.wgsl index b2bab26..2285ef9 100644 --- a/workspaces/main/shaders/cnn/cnn_layer.wgsl +++ b/workspaces/main/shaders/cnn/cnn_layer.wgsl @@ -1,5 +1,6 @@ // CNN layer shader - uses modular convolution snippets // Supports multi-pass rendering with residual connections +// DO NOT EDIT - Generated by train_cnn.py @group(0) @binding(0) var smplr: sampler; @group(0) @binding(1) var txt: texture_2d<f32>; @@ -11,12 +12,13 @@ struct CNNLayerParams { layer_index: i32, - use_residual: i32, + blend_amount: f32, _pad: vec2<f32>, }; @group(0) @binding(2) var<uniform> uniforms: CommonUniforms; @group(0) @binding(3) var<uniform> params: CNNLayerParams; +@group(0) @binding(4) var original_input: texture_2d<f32>; @vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> { var pos = array<vec2<f32>, 3>( @@ -27,6 +29,8 @@ struct CNNLayerParams { @fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { let uv = p.xy / uniforms.resolution; + let input = textureSample(txt, smplr, uv); + let original = textureSample(original_input, smplr, uv); var result = vec4<f32>(0.0); // Layer 0 uses coordinate-aware convolution @@ -35,12 +39,19 @@ struct CNNLayerParams { rgba_weights_layer0, coord_weights_layer0, bias_layer0); result = cnn_tanh(result); } - - // Residual connection - if (params.use_residual != 0) { - let input = textureSample(txt, smplr, uv); - result = input + result * 0.3; + else if (params.layer_index == 1) { + result = cnn_conv3x3(txt, smplr, uv, uniforms.resolution, + weights_layer1, bias_layer1); + result = cnn_tanh(result); + } + else if (params.layer_index == 2) { + result = cnn_conv3x3(txt, smplr, uv, uniforms.resolution, + weights_layer2, bias_layer2); + } + else { + result = input; } - return result; + // Blend with ORIGINAL input from layer 0 + return mix(original, result, params.blend_amount); } diff --git a/workspaces/main/shaders/cnn/cnn_weights_generated.wgsl b/workspaces/main/shaders/cnn/cnn_weights_generated.wgsl index e0a7dc4..6052ac5 100644 --- a/workspaces/main/shaders/cnn/cnn_weights_generated.wgsl +++ b/workspaces/main/shaders/cnn/cnn_weights_generated.wgsl @@ -1,23 +1,185 @@ -// Generated CNN weights and biases -// DO NOT EDIT MANUALLY - regenerate with scripts/train_cnn.py +// Auto-generated CNN weights +// DO NOT EDIT - Generated by train_cnn.py -// Placeholder identity-like weights for initial testing -// Layer 0: 3x3 convolution with coordinate awareness const rgba_weights_layer0: array<mat4x4<f32>, 9> = array( - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), - mat4x4<f32>(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + mat4x4<f32>( + -0.181929, -0.244329, -0.354404, 0.0, + -0.291597, -0.195653, 0.081896, 0.0, + 0.081595, 0.164081, -0.236318, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 0.731888, 0.717648, 0.524081, 0.0, + -0.029760, -0.208000, 0.008438, 0.0, + 0.442082, 0.354681, 0.049288, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.623141, -0.695759, -0.087885, 0.0, + 0.043135, 0.071979, 0.213065, 0.0, + 0.011581, 0.110995, 0.034100, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 0.170016, 0.188298, 0.134083, 0.0, + -0.222954, -0.088011, 0.015668, 0.0, + 0.921836, 0.437158, 0.061577, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 1.431940, 1.148113, 1.238067, 0.0, + -0.212535, 0.366860, 0.320956, 0.0, + 0.771192, 0.765570, 0.029189, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 0.171088, 0.000155, 0.212552, 0.0, + 0.029536, 0.447892, 0.041381, 0.0, + 0.011807, -0.167281, -0.200702, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.668151, -0.813927, -0.132108, 0.0, + -0.156250, 0.179112, -0.069585, 0.0, + 0.403347, 0.482877, 0.182611, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.609871, -0.768480, -0.590538, 0.0, + -0.171854, 0.150167, 0.105694, 0.0, + -0.059052, 0.066999, -0.244222, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.112983, -0.066299, 0.117696, 0.0, + -0.172541, 0.095008, -0.160754, 0.0, + -0.369667, -0.000628, 0.163602, 0.0, + 0.0, 0.0, 0.0, 0.0, + ) ); const coord_weights_layer0 = mat2x4<f32>( - 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0 + 0.059076, -0.026617, -0.005155, 0.0, + 0.135407, -0.090329, 0.058216, 0.0 ); -const bias_layer0 = vec4<f32>(0.0, 0.0, 0.0, 0.0); +const bias_layer0 = vec4<f32>(-0.526177, -0.569862, -1.370040, 0.0); + +const weights_layer1: array<mat4x4<f32>, 9> = array( + mat4x4<f32>( + 0.180029, -1.107249, 0.570741, 0.0, + -0.098536, 0.079545, -0.083257, 0.0, + -0.020066, 0.333084, 0.039506, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 3.068946, -1.783570, -0.550517, 0.0, + -0.296369, -0.080958, 0.040260, 0.0, + -0.093713, -0.212577, -0.110011, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 2.282564, -0.538192, -0.793214, 0.0, + -0.395788, 0.130881, 0.078571, 0.0, + -0.041375, 0.061666, 0.045651, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.267284, -1.971639, -0.099616, 0.0, + -0.084432, 0.139794, 0.007091, 0.0, + -0.103042, -0.104340, 0.067299, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -5.233469, -2.252747, -3.555217, 0.0, + 0.647940, -0.178858, 0.351633, 0.0, + -0.014237, -0.505881, 0.165940, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.121700, -0.677386, -2.435040, 0.0, + 0.084806, -0.028000, 0.380387, 0.0, + -0.020906, -0.279161, 0.041915, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 2.982562, -0.298441, -0.147775, 0.0, + -0.291832, 0.102875, -0.128590, 0.0, + -0.091786, 0.104389, -0.188678, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -4.434978, -0.261830, -2.436411, 0.0, + 0.349188, -0.245908, 0.272592, 0.0, + 0.010322, -0.148525, -0.031531, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + 0.129886, 1.516168, -0.755576, 0.0, + 0.133138, -0.260276, 0.028059, 0.0, + 0.001185, 0.141547, -0.003606, 0.0, + 0.0, 0.0, 0.0, 0.0, + ) +); + +const bias_layer1 = vec4<f32>(1.367986, -1.148709, -0.650040, 0.0); + +const weights_layer2: array<mat4x4<f32>, 9> = array( + mat4x4<f32>( + -0.137003, -0.289376, 0.625000, 0.0, + -0.120120, -0.238968, 0.448432, 0.0, + -0.142094, -0.253706, 0.458181, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.337017, -0.757585, 0.135953, 0.0, + -0.304432, -0.553491, 0.419907, 0.0, + -0.313585, -0.467667, 0.615326, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.161089, -0.328735, 0.612679, 0.0, + -0.137144, -0.172882, 0.176362, 0.0, + -0.153195, -0.061571, 0.173977, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.227814, -0.544193, -0.564658, 0.0, + -0.211743, -0.430586, 0.080349, 0.0, + -0.214442, -0.417501, 0.880266, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.435370, -0.295169, -0.865976, 0.0, + -0.423147, -0.274780, 0.323049, 0.0, + -0.411180, -0.062517, 1.099769, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.199573, -0.488030, -0.396440, 0.0, + -0.187844, -0.360516, -0.156646, 0.0, + -0.188681, -0.292304, -0.134645, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.123218, -0.287990, 0.154656, 0.0, + -0.112954, -0.282778, 0.498742, 0.0, + -0.139083, -0.319337, 1.112621, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.267477, -0.691374, -0.028960, 0.0, + -0.246348, -0.585583, 0.401194, 0.0, + -0.253279, -0.562875, 1.105818, 0.0, + 0.0, 0.0, 0.0, 0.0, + ), + mat4x4<f32>( + -0.083133, -0.131627, 0.460039, 0.0, + -0.071126, -0.108601, 0.163545, 0.0, + -0.092579, -0.110020, 0.131282, 0.0, + 0.0, 0.0, 0.0, 0.0, + ) +); + +const bias_layer2 = vec4<f32>(-1.805686, -0.798340, 0.462318, 0.0); + diff --git a/workspaces/main/timeline.seq b/workspaces/main/timeline.seq index 3670825..5947ff8 100644 --- a/workspaces/main/timeline.seq +++ b/workspaces/main/timeline.seq @@ -1,121 +1,101 @@ -# WORKSPACE: main -# ============================================================================ -# DEMO SEQUENCE DEFINITION -# ============================================================================ -# Defines the timeline and layering of visual effects for the demo. -# Compiled by seq_compiler into src/generated/timeline.cc at build time. -# -# DOCUMENTATION: See doc/SEQUENCE.md for complete syntax reference -# -# QUICK REFERENCE: -# SEQUENCE <start> <priority> ["optional_name"] [optional_end] -# EFFECT <+|=|-> <ClassName> <start> <end> -# -# Priority modifiers: + (increment), = (same), - (decrement/background) -# Time notation: 0b (beats), 0.0 (seconds) -# Optional name: Displayed in Gantt charts (e.g., "Opening Scene") -# -# VALIDATION & VISUALIZATION: -# ./build/seq_compiler assets/demo.seq # Validate only -# ./build/seq_compiler assets/demo.seq --gantt-html=t.html # HTML Gantt -# -# ============================================================================ - +# Demo Timeline +# Generated by Timeline Editor # BPM 120 -SEQUENCE 0b 0 - EFFECT - FlashCubeEffect .2 3 # Background cube (priority -1 = behind everything) - EFFECT + FlashEffect 0.0 1. color=1.0,0.5,0.5 decay=0.95 # Red-tinted flash - EFFECT + FadeEffect 0.1 1. # Priority 1 - EFFECT + SolarizeEffect 0 4b # Priority 2 (was 3, now contiguous) - EFFECT + VignetteEffect 0 6 radius=0.6 softness=0.1 +SEQUENCE 0.00 0 + EFFECT - FlashCubeEffect 0.00 2.44 + EFFECT + FlashEffect 0.00 1.00 color=1.0,0.5,0.5 decay=0.95 + EFFECT + FadeEffect 0.10 1.00 + EFFECT + SolarizeEffect 0.00 2.00 + EFFECT + VignetteEffect 0.00 2.50 radius=0.6 softness=0.1 + +SEQUENCE 2.50 0 "rotating cube" + EFFECT + CircleMaskEffect 0.00 4.00 0.50 + EFFECT + RotatingCubeEffect 0.00 4.00 + EFFECT + GaussianBlurEffect 1.00 2.00 strength=1.0 + EFFECT + GaussianBlurEffect 3.00 4.00 strength=2.0 + +SEQUENCE 5.93 0 + EFFECT - FlashCubeEffect 0.11 1.45 + EFFECT + FlashEffect 0.00 0.20 -SEQUENCE 2.0 0 - EFFECT + CircleMaskEffect 0.0 4.0 0.50 # Priority 0 mask generator - EFFECT + RotatingCubeEffect 0.0 4.0 # Priority 1 (renders inside circle) - EFFECT + GaussianBlurEffect 1.0 2.0 strength=1.0 - EFFECT + GaussianBlurEffect 3.0 4.0 strength=2.0 +SEQUENCE 6.90 1 "spray" + EFFECT + ParticleSprayEffect 0.00 2.00 + EFFECT + ParticlesEffect 0.00 3.00 + EFFECT = GaussianBlurEffect 0.00 2.00 strength=3.0 -SEQUENCE 4b 0 - EFFECT - FlashCubeEffect 0.1 3. # Priority -1 - EFFECT + FlashEffect 0.0 0.2 # Priority 0 (was 4, now contiguous) +SEQUENCE 8.50 2 + EFFECT + ThemeModulationEffect 0.00 2.00 + EFFECT + HeptagonEffect 0.20 2.00 + EFFECT + ParticleSprayEffect 0.00 2.00 + EFFECT = ParticlesEffect 0.00 2.00 + EFFECT + Hybrid3DEffect 0.00 2.00 + EFFECT + GaussianBlurEffect 0.00 2.00 + EFFECT + ChromaAberrationEffect 0.00 1.50 offset=0.01 angle=1.57 -SEQUENCE 6b 1 - EFFECT + ParticleSprayEffect 0 4 # Priority 0 (spray particles) - EFFECT + ParticlesEffect 0 4 # Priority 1 - EFFECT = GaussianBlurEffect 0 8 strength=3.0 # Priority 1 (stronger blur) +SEQUENCE 10.50 0 + EFFECT + HeptagonEffect 0.00 0.20 + EFFECT + FadeEffect 0.10 1.00 -SEQUENCE 7b 0 - EFFECT + HeptagonEffect 0.0 .2 # Priority 0 - EFFECT + FadeEffect 0.1 1.0 # Priority 1 (was 5, now contiguous) +SEQUENCE 10.50 0 + EFFECT + Hybrid3DEffect 0.00 5.59 + EFFECT + CNNEffect 0.54 4.97 layers=3 blend=0.2 -# Post-processing chain (priority 10 = applied after scene rendering) -# Effects are applied in priority order: lower numbers first -SEQUENCE 8b 3 - EFFECT + ThemeModulationEffect 0 4 # Priority 0 - EFFECT = HeptagonEffect 0.0 4.0 # Priority 0 (same layer) - EFFECT + GaussianBlurEffect 0 8 strength=1.5 # Priority 1 (subtle blur) - EFFECT + ChromaAberrationEffect 0 6 offset=0.03 angle=0.785 # Priority 2 (diagonal, stronger) - EFFECT + SolarizeEffect 0 10 # Priority 3 +SEQUENCE 16.14 3 + EFFECT + ThemeModulationEffect 0.00 4.00 + EFFECT = HeptagonEffect 0.00 4.00 + EFFECT + GaussianBlurEffect 0.00 5.00 strength=1.5 + EFFECT + ChromaAberrationEffect 0.00 5.00 offset=0.03 angle=0.785 + EFFECT + SolarizeEffect 0.00 5.00 -SEQUENCE 12b 2 - EFFECT - FlashCubeEffect .2 3 # Priority -1 (background) - EFFECT + HeptagonEffect 0 4 # Priority 0 - EFFECT + ParticleSprayEffect 0 4 # Priority 1 (spray particles) - EFFECT + ParticlesEffect 0 4 # Priority 2 +SEQUENCE 21.00 2 + EFFECT - FlashCubeEffect 0.20 1.50 + EFFECT + HeptagonEffect 0.00 2.00 + EFFECT + ParticleSprayEffect 0.00 2.00 + EFFECT + ParticlesEffect 0.00 2.00 -SEQUENCE 15b 2 - EFFECT - FlashCubeEffect .2 3 # Priority -1 (background) - EFFECT + FlashEffect 0.0 1 # Priority 0 +SEQUENCE 22.75 2 + EFFECT - FlashCubeEffect 0.20 1.50 + EFFECT + FlashEffect 0.00 1.00 -SEQUENCE 16b 10 - EFFECT - FlashCubeEffect .2 3 # Priority -1 (background) - EFFECT + GaussianBlurEffect 0 8 # Priority 0 - EFFECT + FlashEffect 0.0 0.2 # Priority 1 - EFFECT = FlashEffect 1b 0.2 # Priority 1 (same layer) +SEQUENCE 23.88 10 + EFFECT - FlashCubeEffect 0.20 1.50 + EFFECT + GaussianBlurEffect 0.00 2.00 + EFFECT + FlashEffect 0.00 0.20 + EFFECT = FlashEffect 0.50 0.20 -SEQUENCE 17b 2 - EFFECT + ThemeModulationEffect 0 4 # Priority 0 - EFFECT + HeptagonEffect 0.2 2.0 # Priority 1 - EFFECT + ParticleSprayEffect 0 4 # Priority 2 (spray particles) - EFFECT = ParticlesEffect 0 4 # Priority 2 (same layer) - EFFECT + Hybrid3DEffect 0 4 # Priority 3 - EFFECT + GaussianBlurEffect 0 8 # Priority 4 - EFFECT + ChromaAberrationEffect 0 6 offset=0.01 angle=1.57 # Priority 5 (vertical, subtle) +SEQUENCE 25.59 1 + EFFECT + ThemeModulationEffect 0.00 8.00 + EFFECT + HeptagonEffect 0.20 2.00 + EFFECT + ParticleSprayEffect 0.00 8.00 + EFFECT + Hybrid3DEffect 0.00 10.00 + EFFECT + GaussianBlurEffect 0.00 8.00 + EFFECT + ChromaAberrationEffect 0.00 10.00 + EFFECT + SolarizeEffect 0.00 10.00 -SEQUENCE 24b 1 - EFFECT + ThemeModulationEffect 0 8 # Priority 0 - EFFECT + HeptagonEffect 0.2 2.0 # Priority 1 - EFFECT + ParticleSprayEffect 0 8 # Priority 2 (spray particles - longer duration) - EFFECT + Hybrid3DEffect 0 20 # Priority 3 - EFFECT + GaussianBlurEffect 0 8 # Priority 4 - EFFECT + ChromaAberrationEffect 0 10 # Priority 5 - EFFECT + SolarizeEffect 0 10 # Priority 6 +SEQUENCE 35.31 0 + EFFECT + ThemeModulationEffect 0.00 4.00 + EFFECT + HeptagonEffect 0.20 2.00 + EFFECT + GaussianBlurEffect 0.00 8.00 + EFFECT + SolarizeEffect 0.00 2.00 -SEQUENCE 32b 0 - EFFECT + ThemeModulationEffect 0 4 # Priority 0 - EFFECT + HeptagonEffect 0 16 # Priority 1 - EFFECT + ChromaAberrationEffect 0 16 # Priority 2 - EFFECT + GaussianBlurEffect 0 8 # Priority 3 +SEQUENCE 42.29 0 + EFFECT + ThemeModulationEffect 0.00 6.00 + EFFECT = HeptagonEffect 0.20 2.00 + EFFECT + Hybrid3DEffect 0.00 4.00 + EFFECT + ParticleSprayEffect 0.00 5.50 + EFFECT + HeptagonEffect 0.00 8.00 + EFFECT + ChromaAberrationEffect 0.00 7.50 + EFFECT + GaussianBlurEffect 0.00 8.00 -SEQUENCE 48b 0 - EFFECT + ThemeModulationEffect 0 4 # Priority 0 - EFFECT + HeptagonEffect 0.2 2.0 # Priority 1 - EFFECT + GaussianBlurEffect 0 8 # Priority 2 - EFFECT + SolarizeEffect 0 2 # Priority 3 +SEQUENCE 50.02 0 + EFFECT + ThemeModulationEffect 0.00 4.00 + EFFECT + HeptagonEffect 0.00 9.50 + EFFECT + ChromaAberrationEffect 0.00 9.00 + EFFECT + GaussianBlurEffect 0.00 8.00 -SEQUENCE 56b 0 - EFFECT + ThemeModulationEffect 0 8 # Priority 0 - EFFECT = HeptagonEffect 0.2 2.0 # Priority 0 (same layer) - EFFECT + Hybrid3DEffect 0 4 # Priority 1 - EFFECT + ParticleSprayEffect 0 8 # Priority 2 (spray particles) - EFFECT + HeptagonEffect 0 16 # Priority 3 - EFFECT + ChromaAberrationEffect 0 16 # Priority 4 - EFFECT + GaussianBlurEffect 0 8 # Priority 5 +SEQUENCE 31.00 0 + EFFECT + ThemeModulationEffect 0.00 3.00 + EFFECT + VignetteEffect 0.00 3.00 radius=0.6 softness=0.3 + EFFECT + SolarizeEffect 0.00 3.00 -SEQUENCE 62b 0 - EFFECT + ThemeModulationEffect 0 3 # Priority 0 - EFFECT + VignetteEffect 0 3 radius=0.6 softness=0.3 # New effect - EFFECT + SolarizeEffect 0 3 # Priority 2 -# Demo automatically exits at this time (supports beat notation) -END_DEMO 65b |
