summaryrefslogtreecommitdiff
path: root/src/gpu/demo_effects.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-01-31 15:36:45 +0100
committerskal <pascal.massimino@gmail.com>2026-01-31 15:36:45 +0100
commit72c0cbcb38732b698d33d52459fed67af56ee2ec (patch)
tree83cabb089d76c7dda49ac786ad829b6f37d17bbf /src/gpu/demo_effects.cc
parente96f282d77bd114493c8d9097c7fb7eaaad2338b (diff)
feat: Implement Sequence and Effect system for demo choreography
Refactors the rendering pipeline into a modular Sequence/Effect system. 'MainSequence' manages the final frame rendering and a list of 'Sequence' layers. 'Sequence' manages a timeline of 'Effect' objects (start/end/priority). Concrete effects (Heptagon, Particles) are moved to 'demo_effects.cc'. Updates main loop to pass beat and aspect ratio.
Diffstat (limited to 'src/gpu/demo_effects.cc')
-rw-r--r--src/gpu/demo_effects.cc314
1 files changed, 314 insertions, 0 deletions
diff --git a/src/gpu/demo_effects.cc b/src/gpu/demo_effects.cc
new file mode 100644
index 0000000..5fc7c15
--- /dev/null
+++ b/src/gpu/demo_effects.cc
@@ -0,0 +1,314 @@
+// This file is part of the 64k demo project.
+// It implements the concrete effects used in the demo.
+
+#include "demo_effects.h"
+#include <cmath>
+#include <cstdlib>
+#include <cstring>
+#include <vector>
+
+static const int NUM_PARTICLES = 10000;
+
+struct Particle {
+ float pos[4]; // x, y, z, life
+ float vel[4]; // vx, vy, vz, padding
+ float rot[4]; // angle, speed, padding, padding
+ float color[4]; // r, g, b, a
+};
+
+const char *main_shader_wgsl = R"(
+struct Uniforms {
+ audio_peak : f32,
+ aspect_ratio: f32,
+ time: f32,
+};
+
+@group(0) @binding(0) var<uniform> uniforms : Uniforms;
+
+@vertex
+fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
+ let PI = 3.14159265;
+ let num_sides = 7.0;
+
+ // Pulse scale based on audio peak
+ let base_scale = 0.5;
+ let pulse_scale = 0.3 * uniforms.audio_peak;
+ let scale = base_scale + pulse_scale;
+
+ let tri_idx = f32(vertex_index / 3u);
+ let sub_idx = vertex_index % 3u;
+
+ if (sub_idx == 0u) {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+
+ // Apply rotation based on time
+ let rotation = uniforms.time * 0.5;
+ let i = tri_idx + f32(sub_idx - 1u);
+ let angle = i * 2.0 * PI / num_sides + rotation;
+ let x = scale * cos(angle) / uniforms.aspect_ratio;
+ let y = scale * sin(angle);
+
+ return vec4<f32>(x, y, 0.0, 1.0);
+}
+
+@fragment
+fn fs_main() -> @location(0) vec4<f32> {
+ // Dynamic color shifting based on time and responsiveness to peak
+ let h = uniforms.time * 2.0 + uniforms.audio_peak * 3.0;
+ let r = sin(h + 0.0) * 0.5 + 0.5;
+ let g = sin(h + 2.0) * 0.9 + 0.3;
+ let b = sin(h + 4.0) * 0.5 + 0.5;
+
+ let boost = uniforms.audio_peak * 0.5;
+ return vec4<f32>(r + boost, g + boost, b + boost, 0.5); // Alpha 0.5 for blending
+}
+)";
+
+const char *particle_compute_wgsl = R"(
+struct Particle {
+ pos : vec4<f32>,
+ vel : vec4<f32>,
+ rot : vec4<f32>,
+ color : vec4<f32>,
+};
+
+struct Uniforms {
+ audio_peak : f32,
+ aspect_ratio: f32,
+ time: f32,
+};
+
+@group(0) @binding(0) var<storage, read_write> particles : array<Particle>;
+@group(0) @binding(1) var<uniform> uniforms : Uniforms;
+
+@compute @workgroup_size(64)
+fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
+ let index = GlobalInvocationID.x;
+ if (index >= arrayLength(&particles)) {
+ return;
+ }
+
+ var p = particles[index];
+
+ // Update Position
+ p.pos.x = p.pos.x + p.vel.x * 0.016;
+ p.pos.y = p.pos.y + p.vel.y * 0.016;
+ p.pos.z = p.pos.z + p.vel.z * 0.016;
+
+ // Gravity / Audio attraction
+ p.vel.y = p.vel.y - 0.01 * (1.0 + uniforms.audio_peak * 5.0);
+
+ // Rotate
+ p.rot.x = p.rot.x + p.rot.y * 0.016;
+
+ // Reset if out of bounds
+ if (p.pos.y < -1.5) {
+ p.pos.y = 1.5;
+ p.pos.x = (f32(index % 100u) / 50.0) - 1.0 + (uniforms.audio_peak * 0.5);
+ p.vel.y = 0.0;
+ p.vel.x = (f32(index % 10u) - 5.0) * 0.1;
+ }
+
+ particles[index] = p;
+}
+)";
+
+const char *particle_render_wgsl = R"(
+struct Particle {
+ pos : vec4<f32>,
+ vel : vec4<f32>,
+ rot : vec4<f32>,
+ color : vec4<f32>,
+};
+
+struct Uniforms {
+ audio_peak : f32,
+ aspect_ratio: f32,
+ time: f32,
+};
+
+@group(0) @binding(0) var<storage, read> particles : array<Particle>;
+@group(0) @binding(1) var<uniform> uniforms : Uniforms;
+
+struct VertexOutput {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) Color : vec4<f32>,
+};
+
+@vertex
+fn vs_main(@builtin(vertex_index) vertex_index : u32, @builtin(instance_index) instance_index : u32) -> VertexOutput {
+ let p = particles[instance_index];
+
+ // Simple quad expansion
+ let size = 0.02 + p.pos.z * 0.01 + uniforms.audio_peak * 0.02;
+
+ // Vertex ID 0..5 for 2 triangles (Quad)
+ // 0 1 2, 2 1 3 (Strip-like order manually mapped)
+ var offsets = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>( 1.0, 1.0)
+ );
+
+ let offset = offsets[vertex_index];
+
+ // Rotate
+ let c = cos(p.rot.x);
+ let s = sin(p.rot.x);
+ let rot_x = offset.x * c - offset.y * s;
+ let rot_y = offset.x * s + offset.y * c;
+
+ let x = p.pos.x + rot_x * size / uniforms.aspect_ratio;
+ let y = p.pos.y + rot_y * size;
+
+ var output : VertexOutput;
+ output.Position = vec4<f32>(x, y, 0.0, 1.0);
+ output.Color = p.color * (0.5 + 0.5 * uniforms.audio_peak);
+ return output;
+}
+
+@fragment
+fn fs_main(@location(0) Color : vec4<f32>) -> @location(0) vec4<f32> {
+ return Color;
+}
+)";
+
+// --- HeptagonEffect ---
+
+HeptagonEffect::HeptagonEffect(WGPUDevice device, WGPUQueue queue,
+ WGPUTextureFormat format)
+ : queue_(queue) {
+ uniforms_ = gpu_create_buffer(
+ device, sizeof(float) * 4,
+ WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, nullptr);
+
+ ResourceBinding bindings[] = {{uniforms_, WGPUBufferBindingType_Uniform}};
+ pass_ = gpu_create_render_pass(device, format, main_shader_wgsl, bindings, 1);
+ pass_.vertex_count = 21;
+}
+
+void HeptagonEffect::render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) {
+ struct {
+ float audio_peak;
+ float aspect_ratio;
+ float time;
+ float padding;
+ } u = {intensity, aspect_ratio, time, 0.0f};
+
+ wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u));
+
+ wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, pass_.vertex_count, 1, 0, 0);
+}
+
+// --- ParticlesEffect ---
+
+ParticlesEffect::ParticlesEffect(WGPUDevice device, WGPUQueue queue,
+ WGPUTextureFormat format)
+ : queue_(queue) {
+ uniforms_ = gpu_create_buffer(
+ device, sizeof(float) * 4,
+ WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, nullptr);
+
+ std::vector<Particle> initial_particles(NUM_PARTICLES);
+ for (int i = 0; i < NUM_PARTICLES; ++i) {
+ initial_particles[i].pos[0] = ((float)(rand() % 100) / 50.0f) - 1.0f;
+ initial_particles[i].pos[1] = ((float)(rand() % 100) / 50.0f) - 1.0f;
+ initial_particles[i].pos[2] = 0.0f;
+ initial_particles[i].pos[3] = 1.0f;
+
+ initial_particles[i].vel[0] = 0.0f;
+ initial_particles[i].vel[1] = 0.0f;
+
+ initial_particles[i].rot[0] = 0.0f;
+ initial_particles[i].rot[1] = ((float)(rand() % 10) / 100.0f);
+
+ initial_particles[i].color[0] = (float)(rand() % 10) / 10.0f;
+ initial_particles[i].color[1] = (float)(rand() % 10) / 10.0f;
+ initial_particles[i].color[2] = 1.0f;
+ initial_particles[i].color[3] = 1.0f;
+ }
+
+ particles_buffer_ = gpu_create_buffer(
+ device, sizeof(Particle) * NUM_PARTICLES,
+ (WGPUBufferUsage)(WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst |
+ WGPUBufferUsage_Vertex),
+ initial_particles.data());
+
+ ResourceBinding compute_bindings[] = {
+ {particles_buffer_, WGPUBufferBindingType_Storage},
+ {uniforms_, WGPUBufferBindingType_Uniform}};
+ compute_pass_ = gpu_create_compute_pass(device, particle_compute_wgsl,
+ compute_bindings, 2);
+ compute_pass_.workgroup_size_x = (NUM_PARTICLES + 63) / 64;
+ compute_pass_.workgroup_size_y = 1;
+ compute_pass_.workgroup_size_z = 1;
+
+ ResourceBinding render_bindings[] = {
+ {particles_buffer_, WGPUBufferBindingType_ReadOnlyStorage},
+ {uniforms_, WGPUBufferBindingType_Uniform}};
+ render_pass_ = gpu_create_render_pass(device, format, particle_render_wgsl,
+ render_bindings, 2);
+ render_pass_.vertex_count = 6;
+ render_pass_.instance_count = NUM_PARTICLES;
+}
+
+void ParticlesEffect::compute(WGPUCommandEncoder encoder, float time,
+ float beat, float intensity, float aspect_ratio) {
+ struct {
+ float audio_peak;
+ float aspect_ratio;
+ float time;
+ float padding;
+ } u = {intensity, aspect_ratio, time, 0.0f};
+
+ wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u));
+
+ WGPUComputePassDescriptor compute_desc = {};
+ WGPUComputePassEncoder pass =
+ wgpuCommandEncoderBeginComputePass(encoder, &compute_desc);
+ wgpuComputePassEncoderSetPipeline(pass, compute_pass_.pipeline);
+ wgpuComputePassEncoderSetBindGroup(pass, 0, compute_pass_.bind_group, 0,
+ nullptr);
+ wgpuComputePassEncoderDispatchWorkgroups(pass, compute_pass_.workgroup_size_x,
+ 1, 1);
+ wgpuComputePassEncoderEnd(pass);
+}
+
+void ParticlesEffect::render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) {
+ // Update uniforms again? Technically redundant if compute happened same frame.
+ // But safer if render is called without compute (e.g. debugging).
+ struct {
+ float audio_peak;
+ float aspect_ratio;
+ float time;
+ float padding;
+ } u = {intensity, aspect_ratio, time, 0.0f};
+
+ wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u));
+
+ wgpuRenderPassEncoderSetPipeline(pass, render_pass_.pipeline);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, render_pass_.bind_group, 0,
+ nullptr);
+ wgpuRenderPassEncoderDraw(pass, render_pass_.vertex_count,
+ render_pass_.instance_count, 0, 0);
+}
+
+std::shared_ptr<Sequence> create_demo_sequence(WGPUDevice device,
+ WGPUQueue queue,
+ WGPUTextureFormat format) {
+ auto seq = std::make_shared<Sequence>();
+ // Overlap them for now to replicate original behavior
+ seq->add_effect(std::make_shared<HeptagonEffect>(device, queue, format), 0.0f,
+ 1000.0f);
+ seq->add_effect(std::make_shared<ParticlesEffect>(device, queue, format),
+ 0.0f, 1000.0f);
+ return seq;
+}