From 2e33a435d6f1efee5a6c88cbfc99b97241fe2ad3 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 16 Feb 2026 10:03:18 +0100 Subject: feat(sequence): port rotating_cube_effect to v2 - Add RotatingCubeEffectV2 with 3D rendering + depth buffer - Create rotating_cube_v2.wgsl (hardcoded cube geometry) - Simplified: no auxiliary mask texture dependency - Declare depth node via NodeRegistry - Update timeline_v2.seq rotating_cube sequence - Add shader exports to shaders.{h,cc} - All 36 tests passing handoff(Claude): RotatingCube v2 complete, hybrid_3d next --- cmake/DemoSourceLists.cmake | 1 + src/effects/rotating_cube_effect_v2.cc | 184 ++++++++++++++++++++++++++ src/effects/rotating_cube_effect_v2.h | 49 +++++++ src/gpu/shaders.cc | 4 + src/gpu/shaders.h | 1 + workspaces/main/assets.txt | 1 + workspaces/main/shaders/rotating_cube_v2.wgsl | 89 +++++++++++++ workspaces/main/timeline_v2.seq | 7 +- 8 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 src/effects/rotating_cube_effect_v2.cc create mode 100644 src/effects/rotating_cube_effect_v2.h create mode 100644 workspaces/main/shaders/rotating_cube_v2.wgsl diff --git a/cmake/DemoSourceLists.cmake b/cmake/DemoSourceLists.cmake index 5325f6a..1b8ad28 100644 --- a/cmake/DemoSourceLists.cmake +++ b/cmake/DemoSourceLists.cmake @@ -36,6 +36,7 @@ set(COMMON_GPU_EFFECTS src/effects/gaussian_blur_effect_v2.cc src/effects/heptagon_effect_v2.cc src/effects/particles_effect_v2.cc + src/effects/rotating_cube_effect_v2.cc src/effects/heptagon_effect.cc src/effects/particles_effect.cc src/effects/passthrough_effect.cc diff --git a/src/effects/rotating_cube_effect_v2.cc b/src/effects/rotating_cube_effect_v2.cc new file mode 100644 index 0000000..02ed2d3 --- /dev/null +++ b/src/effects/rotating_cube_effect_v2.cc @@ -0,0 +1,184 @@ +// This file is part of the 64k demo project. +// It implements RotatingCubeEffectV2 (simplified v2 port). + +#include "effects/rotating_cube_effect_v2.h" +#include "gpu/bind_group_builder.h" +#include "gpu/gpu.h" +#include "gpu/shaders.h" + +RotatingCubeEffectV2::RotatingCubeEffectV2(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs) + : EffectV2(ctx, inputs, outputs), depth_node_(outputs[0] + "_depth") { + // Create uniform buffers + uniform_buffer_ = + gpu_create_buffer(ctx_.device, sizeof(Uniforms), + WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); + object_buffer_ = + gpu_create_buffer(ctx_.device, sizeof(ObjectData), + WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst); + + // Create bind group layout + WGPUBindGroupLayout bgl = + BindGroupLayoutBuilder() + .uniform(0, WGPUShaderStage_Vertex | WGPUShaderStage_Fragment, + sizeof(Uniforms)) + .storage(1, WGPUShaderStage_Vertex | WGPUShaderStage_Fragment, + sizeof(ObjectData)) + .build(ctx_.device); + + const WGPUBindGroupLayout bgls[] = {bgl}; + const WGPUPipelineLayoutDescriptor pl_desc = { + .bindGroupLayoutCount = 1, + .bindGroupLayouts = bgls, + }; + WGPUPipelineLayout pipeline_layout = + wgpuDeviceCreatePipelineLayout(ctx_.device, &pl_desc); + + // Load shader (TODO: create rotating_cube_v2.wgsl) + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = str_view(rotating_cube_v2_wgsl); + + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = &wgsl_src.chain; + WGPUShaderModule shader_module = + wgpuDeviceCreateShaderModule(ctx_.device, &shader_desc); + + const WGPUColorTargetState color_target = { + .format = WGPUTextureFormat_RGBA8Unorm, + .writeMask = WGPUColorWriteMask_All, + }; + + const WGPUDepthStencilState depth_stencil = { + .format = WGPUTextureFormat_Depth24Plus, + .depthWriteEnabled = WGPUOptionalBool_True, + .depthCompare = WGPUCompareFunction_Less, + }; + + WGPUFragmentState fragment = {}; + fragment.module = shader_module; + fragment.entryPoint = str_view("fs_main"); + fragment.targetCount = 1; + fragment.targets = &color_target; + + WGPURenderPipelineDescriptor pipeline_desc = {}; + pipeline_desc.layout = pipeline_layout; + pipeline_desc.vertex.module = shader_module; + pipeline_desc.vertex.entryPoint = str_view("vs_main"); + pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pipeline_desc.primitive.cullMode = WGPUCullMode_Back; + pipeline_desc.depthStencil = &depth_stencil; + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = 0xFFFFFFFF; + pipeline_desc.fragment = &fragment; + + pipeline_ = wgpuDeviceCreateRenderPipeline(ctx_.device, &pipeline_desc); + wgpuShaderModuleRelease(shader_module); + wgpuPipelineLayoutRelease(pipeline_layout); + + // Create bind group + const WGPUBindGroupEntry entries[] = { + {.binding = 0, + .buffer = uniform_buffer_.buffer, + .size = sizeof(Uniforms)}, + {.binding = 1, + .buffer = object_buffer_.buffer, + .size = sizeof(ObjectData)}, + }; + + const WGPUBindGroupDescriptor bg_desc = { + .layout = bgl, + .entryCount = 2, + .entries = entries, + }; + bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc); + wgpuBindGroupLayoutRelease(bgl); +} + +RotatingCubeEffectV2::~RotatingCubeEffectV2() { + if (bind_group_) + wgpuBindGroupRelease(bind_group_); + if (pipeline_) + wgpuRenderPipelineRelease(pipeline_); +} + +void RotatingCubeEffectV2::declare_nodes(NodeRegistry& registry) { + // Declare depth buffer node + registry.declare_node(depth_node_, NodeType::DEPTH24, -1, -1); +} + +void RotatingCubeEffectV2::render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) { + rotation_ += 0.016f * 1.5f; + + // Camera setup + const vec3 camera_pos = vec3(0, 0, 5); + const vec3 target = vec3(0, 0, 0); + const vec3 up = vec3(0, 1, 0); + + const mat4 view = mat4::look_at(camera_pos, target, up); + const float fov = 60.0f * 3.14159f / 180.0f; + const mat4 proj = mat4::perspective(fov, params.aspect_ratio, 0.1f, 100.0f); + const mat4 view_proj = proj * view; + + // Cube transform + const quat rot = quat::from_axis(vec3(0.3f, 1.0f, 0.2f), rotation_); + const mat4 T = mat4::translate(vec3(0, 0, 0)); + const mat4 R = rot.to_mat(); + const mat4 S = mat4::scale(vec3(1.5f, 1.5f, 1.5f)); + const mat4 model = T * R * S; + + // Update uniforms + const Uniforms uniforms = { + .view_proj = view_proj, + .inv_view_proj = view_proj.inverse(), + .camera_pos_time = vec4(camera_pos.x, camera_pos.y, camera_pos.z, params.time), + .params = vec4(1.0f, 0.0f, 0.0f, 0.0f), + .resolution = params.resolution, + .aspect_ratio = params.aspect_ratio, + }; + + const ObjectData obj_data = { + .model = model, + .inv_model = model.inverse(), + .color = vec4(0.8f, 0.4f, 0.2f, 1.0f), + .params = vec4(1.0f, 0.0f, 0.0f, 0.0f), + }; + + wgpuQueueWriteBuffer(ctx_.queue, uniform_buffer_.buffer, 0, &uniforms, + sizeof(Uniforms)); + wgpuQueueWriteBuffer(ctx_.queue, object_buffer_.buffer, 0, &obj_data, + sizeof(ObjectData)); + + // Get output views + WGPUTextureView color_view = nodes.get_view(output_nodes_[0]); + WGPUTextureView depth_view = nodes.get_view(depth_node_); + + // Render pass with depth + WGPURenderPassColorAttachment color_attachment = { + .view = color_view, + .depthSlice = WGPU_DEPTH_SLICE_UNDEFINED, + .loadOp = WGPULoadOp_Clear, + .storeOp = WGPUStoreOp_Store, + .clearValue = {0.0, 0.0, 0.0, 1.0}}; + + 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}; + + WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 36, 1, 0, 0); // 36 vertices for cube + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); +} diff --git a/src/effects/rotating_cube_effect_v2.h b/src/effects/rotating_cube_effect_v2.h new file mode 100644 index 0000000..19ef410 --- /dev/null +++ b/src/effects/rotating_cube_effect_v2.h @@ -0,0 +1,49 @@ +// This file is part of the 64k demo project. +// It declares RotatingCubeEffectV2 (simplified v2 port). + +#pragma once + +#include "gpu/effect_v2.h" +#include "gpu/gpu.h" +#include "gpu/uniform_helper.h" +#include "util/mini_math.h" + +class RotatingCubeEffectV2 : public EffectV2 { + public: + RotatingCubeEffectV2(const GpuContext& ctx, + const std::vector& inputs, + const std::vector& outputs); + ~RotatingCubeEffectV2() override; + + void declare_nodes(NodeRegistry& registry) override; + void render(WGPUCommandEncoder encoder, + const UniformsSequenceParams& params, + NodeRegistry& nodes) override; + + private: + struct Uniforms { + mat4 view_proj; + mat4 inv_view_proj; + vec4 camera_pos_time; + vec4 params; + vec2 resolution; + float aspect_ratio; + float _pad; + }; + static_assert(sizeof(Uniforms) == 176, "Uniforms size mismatch"); + + struct ObjectData { + mat4 model; + mat4 inv_model; + vec4 color; + vec4 params; + }; + static_assert(sizeof(ObjectData) == 160, "ObjectData size mismatch"); + + WGPURenderPipeline pipeline_ = nullptr; + WGPUBindGroup bind_group_ = nullptr; + GpuBuffer uniform_buffer_; + GpuBuffer object_buffer_; + float rotation_ = 0.0f; + std::string depth_node_; +}; diff --git a/src/gpu/shaders.cc b/src/gpu/shaders.cc index bea1eb9..9c8beca 100644 --- a/src/gpu/shaders.cc +++ b/src/gpu/shaders.cc @@ -181,3 +181,7 @@ const char* particle_compute_v2_wgsl = const char* particle_render_v2_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_PARTICLE_RENDER_V2); + +const char* rotating_cube_v2_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_ROTATING_CUBE_V2); diff --git a/src/gpu/shaders.h b/src/gpu/shaders.h index b7ee226..ab5ddf2 100644 --- a/src/gpu/shaders.h +++ b/src/gpu/shaders.h @@ -35,3 +35,4 @@ extern const char* gaussian_blur_v2_shader_wgsl; extern const char* heptagon_v2_shader_wgsl; extern const char* particle_compute_v2_wgsl; extern const char* particle_render_v2_wgsl; +extern const char* rotating_cube_v2_wgsl; diff --git a/workspaces/main/assets.txt b/workspaces/main/assets.txt index fe15cdf..9d15213 100644 --- a/workspaces/main/assets.txt +++ b/workspaces/main/assets.txt @@ -35,6 +35,7 @@ SHADER_PARTICLE_COMPUTE, NONE, shaders/particle_compute.wgsl, "Particle Compute SHADER_PARTICLE_RENDER, NONE, shaders/particle_render.wgsl, "Particle Render Shader" SHADER_PARTICLE_COMPUTE_V2, NONE, shaders/particle_compute_v2.wgsl, "Particle Compute Shader V2" SHADER_PARTICLE_RENDER_V2, NONE, shaders/particle_render_v2.wgsl, "Particle Render Shader V2" +SHADER_ROTATING_CUBE_V2, NONE, shaders/rotating_cube_v2.wgsl, "Rotating Cube Shader V2" SHADER_PASSTHROUGH, NONE, ../../common/shaders/passthrough.wgsl, "Passthrough Shader" SHADER_ELLIPSE, NONE, shaders/ellipse.wgsl, "Ellipse Shader" SHADER_PARTICLE_SPRAY_COMPUTE, NONE, shaders/particle_spray_compute.wgsl, "Particle Spray Compute" diff --git a/workspaces/main/shaders/rotating_cube_v2.wgsl b/workspaces/main/shaders/rotating_cube_v2.wgsl new file mode 100644 index 0000000..d7e4cae --- /dev/null +++ b/workspaces/main/shaders/rotating_cube_v2.wgsl @@ -0,0 +1,89 @@ +// Rotating cube shader v2 (simplified, no masking) + +struct Uniforms { + view_proj: mat4x4, + inv_view_proj: mat4x4, + camera_pos_time: vec4, + params: vec4, + resolution: vec2, + aspect_ratio: f32, + _pad: f32, +}; + +struct ObjectData { + model: mat4x4, + inv_model: mat4x4, + color: vec4, + params: vec4, +}; + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var object: ObjectData; + +struct VSOut { + @builtin(position) pos: vec4, + @location(0) world_pos: vec3, + @location(1) normal: vec3, +}; + +// Cube vertices (hardcoded) +fn get_cube_vertex(vid: u32) -> vec3 { + let positions = array, 36>( + // Front face + vec3(-1, -1, 1), vec3( 1, -1, 1), vec3( 1, 1, 1), + vec3(-1, -1, 1), vec3( 1, 1, 1), vec3(-1, 1, 1), + // Back face + vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1, 1, -1), + vec3( 1, -1, -1), vec3(-1, 1, -1), vec3( 1, 1, -1), + // Right face + vec3( 1, -1, 1), vec3( 1, -1, -1), vec3( 1, 1, -1), + vec3( 1, -1, 1), vec3( 1, 1, -1), vec3( 1, 1, 1), + // Left face + vec3(-1, -1, -1), vec3(-1, -1, 1), vec3(-1, 1, 1), + vec3(-1, -1, -1), vec3(-1, 1, 1), vec3(-1, 1, -1), + // Top face + vec3(-1, 1, 1), vec3( 1, 1, 1), vec3( 1, 1, -1), + vec3(-1, 1, 1), vec3( 1, 1, -1), vec3(-1, 1, -1), + // Bottom face + vec3(-1, -1, -1), vec3( 1, -1, -1), vec3( 1, -1, 1), + vec3(-1, -1, -1), vec3( 1, -1, 1), vec3(-1, -1, 1) + ); + return positions[vid]; +} + +fn get_cube_normal(vid: u32) -> vec3 { + let face_id = vid / 6u; + let normals = array, 6>( + vec3( 0, 0, 1), // Front + vec3( 0, 0, -1), // Back + vec3( 1, 0, 0), // Right + vec3(-1, 0, 0), // Left + vec3( 0, 1, 0), // Top + vec3( 0, -1, 0) // Bottom + ); + return normals[face_id]; +} + +@vertex fn vs_main(@builtin(vertex_index) vid: u32) -> VSOut { + let local_pos = get_cube_vertex(vid); + let local_normal = get_cube_normal(vid); + + let world_pos = object.model * vec4(local_pos, 1.0); + let world_normal = normalize((object.model * vec4(local_normal, 0.0)).xyz); + + let clip_pos = uniforms.view_proj * world_pos; + + return VSOut(clip_pos, world_pos.xyz, world_normal); +} + +@fragment fn fs_main(@location(0) world_pos: vec3, @location(1) normal: vec3) -> @location(0) vec4 { + let N = normalize(normal); + let light_dir = normalize(vec3(1.0, 1.0, 1.0)); + let diffuse = max(dot(N, light_dir), 0.0); + + let ambient = 0.3; + let lighting = ambient + diffuse * 0.7; + + let color = object.color.rgb * lighting; + return vec4(color, 1.0); +} diff --git a/workspaces/main/timeline_v2.seq b/workspaces/main/timeline_v2.seq index 2dc1720..3c97f7b 100644 --- a/workspaces/main/timeline_v2.seq +++ b/workspaces/main/timeline_v2.seq @@ -7,10 +7,9 @@ SEQUENCE 0.00 0 "intro" EFFECT + PlaceholderEffect temp1 -> sink 0.00 4.00 SEQUENCE 4.00 0 "rotating_cube" - # CircleMask (placeholder) -> RotatingCube (placeholder) -> Blur -> sink - EFFECT + PlaceholderEffect source -> temp1 0.00 4.00 - EFFECT + PlaceholderEffect temp1 -> temp2 0.00 4.00 - EFFECT + GaussianBlurEffect temp2 -> sink 1.00 4.00 + # RotatingCube -> Blur -> sink + EFFECT + RotatingCubeEffectV2 source -> temp1 0.00 4.00 + EFFECT + GaussianBlurEffect temp1 -> sink 1.00 4.00 SEQUENCE 8.00 0 "flash_cube" # FlashCube (placeholder) -> Flash (placeholder) -> sink -- cgit v1.2.3