summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-22 19:15:55 +0100
committerskal <pascal.massimino@gmail.com>2026-03-22 19:15:55 +0100
commitc5e66964c0463219019d0439ec20b79248637fa4 (patch)
tree270895c85c2058ff53c2a8c3fa4242b16f2de0ff
parent01df602ea6580edc418b70f121e521a8217f954c (diff)
feat(cnn_v3): GBufferEffect internal scene + GBufViewEffect debug wiring
GBufferEffect: - set_scene() now owns Scene/Camera internally; no external pointers needed - 20 randomly rotating cubes (xorshift32 seed, axis-angle animation) - 4 pumping spheres (radius = base_r * (1 + audio_intensity * 0.8)) - Camera at (0,2.5,6) looking at origin; aspect updated per-frame - GBufLightsUniforms: 2 directional lights (warm key + cool fill) - object_type written to ObjectData.params.x (ready for SDF shadow) - shadow/transp nodes cleared via zero-draw render passes (placeholder) - bilinear sampler cached via create_linear_sampler() / sampler_.get() - dead placeholder textures removed GBufViewEffect: - gbuf_view.wgsl: all channels now fully grayscale (removed color tint) - seq_compiler.py: GBufViewEffect added to CLASS_TO_HEADER - timeline.seq: cnn_v3_test uses GBufViewEffect -> sink for debug view Docs: HOWTO.md §1 updated with set_scene() description + §1b implementation plan for Pass 2 SDF shadow (shader spec, bind layout, C++ additions) handoff(Gemini): GBufferEffect has internal scene, 36/36 tests green. Next: implement Pass 2 shadow (gbuf_shadow.wgsl) per §1b plan in HOWTO.md.
-rw-r--r--cnn_v3/docs/HOWTO.md136
-rw-r--r--cnn_v3/shaders/gbuf_view.wgsl5
-rw-r--r--cnn_v3/src/gbuffer_effect.cc197
-rw-r--r--cnn_v3/src/gbuffer_effect.h51
-rwxr-xr-xtools/seq_compiler.py5
-rw-r--r--workspaces/main/timeline.seq4
6 files changed, 279 insertions, 119 deletions
diff --git a/cnn_v3/docs/HOWTO.md b/cnn_v3/docs/HOWTO.md
index 08979e7..2d88019 100644
--- a/cnn_v3/docs/HOWTO.md
+++ b/cnn_v3/docs/HOWTO.md
@@ -22,57 +22,141 @@ It rasterizes proxy geometry to MRT G-buffer textures and packs them into two
### Adding to a Sequence
-`GBufferEffect` does not exist in `seq_compiler.py` as a named effect yet
-(no `.seq` syntax integration for Phase 1). Wire it directly in C++ alongside
-your scene code, or add it to the timeline when the full CNNv3Effect is ready.
+Both `GBufferEffect` and `GBufViewEffect` are registered in `seq_compiler.py`
+(`CLASS_TO_HEADER`) and can be wired directly in `timeline.seq`.
-**C++ wiring example** (e.g. inside a Sequence or main.cc):
+**Debug view (G-buffer → sink)**:
+```seq
+SEQUENCE 12.00 0 "cnn_v3_test"
+ NODE gbuf_feat0 gbuf_rgba32uint
+ NODE gbuf_feat1 gbuf_rgba32uint
+ EFFECT + GBufferEffect source -> gbuf_feat0 gbuf_feat1 0.00 8.00
+ EFFECT + GBufViewEffect gbuf_feat0 gbuf_feat1 -> sink 0.00 8.00
+```
-```cpp
-#include "../../cnn_v3/src/gbuffer_effect.h"
+**Full CNN pipeline**:
+```seq
+SEQUENCE 12.00 0 "cnn_v3_test"
+ NODE gbuf_feat0 gbuf_rgba32uint
+ NODE gbuf_feat1 gbuf_rgba32uint
+ NODE cnn_v3_out gbuf_albedo
+ EFFECT + GBufferEffect source -> gbuf_feat0 gbuf_feat1 0.00 8.00
+ EFFECT + CNNv3Effect gbuf_feat0 gbuf_feat1 -> cnn_v3_out 0.00 8.00
+ EFFECT + Passthrough cnn_v3_out -> sink 0.00 8.00
+```
-// Allocate once alongside your scene
-auto gbuf = std::make_shared<GBufferEffect>(
- ctx, /*inputs=*/{"prev_cnn"}, // or any dummy node
- /*outputs=*/{"gbuf_feat0", "gbuf_feat1"},
- /*start=*/0.0f, /*end=*/60.0f);
+### Internal scene
-gbuf->set_scene(&my_scene, &my_camera);
+Call `set_scene()` once before the first render to populate the built-in demo
+scene. No external `Scene` or `Camera` pointer is required — the effect owns
+them.
-// In render loop, call before CNN pass:
-gbuf->render(encoder, params, nodes);
-```
+**What `set_scene()` creates:**
+- **20 small cubes** — random positions in [-2,2]×[-1.5,1.5]³, scale 0.1–0.25,
+ random colors. Each has a random rotation axis and speed; animated each frame
+ via `quat::from_axis(axis, time * speed)`.
+- **4 pumping spheres** — at fixed world positions, base radii 0.25–0.35.
+ Scale driven by `audio_intensity`: `r = base_r * (1 + audio_intensity * 0.8)`.
+- **Camera** — position (0, 2.5, 6), target (0, 0, 0), 45° FOV.
+ Aspect ratio updated each frame from `params.aspect_ratio`.
+- **Two directional lights** (uploaded to `lights_uniform_`, ready for shadow pass):
+ - Key: warm white (1.0, 0.92, 0.78), direction `normalize(1, 2, 1)` (upper-right-front)
+ - Fill: cool blue (0.4, 0.45, 0.8 × 0.4), direction `normalize(-1, 1, -1)` (upper-left-back)
### Internal passes
Each frame, `GBufferEffect::render()` executes:
-1. **Pass 1 — MRT rasterization** (`gbuf_raster.wgsl`)
+1. **Pass 1 — MRT rasterization** (`gbuf_raster.wgsl`) ✅
- Proxy box (36 verts) × N objects, instanced
- MRT outputs: `gbuf_albedo` (rgba16float), `gbuf_normal_mat` (rgba16float)
- Depth test + write into `gbuf_depth` (depth32float)
+ - `obj.type` written to `ObjectData.params.x` for future SDF branching
+
+2. **Pass 2 — SDF shadow raymarching** (`gbuf_shadow.wgsl`) — TODO
+ - See implementation plan below.
-2. **Pass 2/3 — SDF + Lighting** — TODO (placeholder: shadow=1, transp=0)
+3. **Pass 3 — Transparency** — TODO (deferred; transp=0 for opaque scenes)
-3. **Pass 4 — Pack compute** (`gbuf_pack.wgsl`)
+4. **Pass 4 — Pack compute** (`gbuf_pack.wgsl`) ✅
- Reads all G-buffer textures + `prev_cnn` input
- Writes `feat_tex0` + `feat_tex1` (rgba32uint, 20 channels, 32 bytes/pixel)
+ - Shadow / transp nodes cleared to 1.0 / 0.0 via zero-draw render passes
+ until Pass 2/3 are implemented.
### Output node names
-By default the outputs are named from the `outputs` vector passed to the
-constructor. Use these names when binding the CNN effect input:
+Outputs are named from the `outputs` vector passed to the constructor:
```
outputs[0] → feat_tex0 (rgba32uint: albedo.rgb, normal.xy, depth, depth_grad.xy)
outputs[1] → feat_tex1 (rgba32uint: mat_id, prev.rgb, mip1.rgb, mip2.rgb, shadow, transp)
```
-### Scene data
+---
+
+## 1b. GBufferEffect — Implementation Plan (Pass 2: SDF Shadow)
+
+### What remains
+
+| Item | Status | Notes |
+|------|--------|-------|
+| Pass 1: MRT raster | ✅ Done | proxy box, all object types |
+| Pass 4: Pack compute | ✅ Done | 20 channels packed |
+| Internal scene + animation | ✅ Done | cubes + spheres + 2 lights |
+| Pass 2: SDF shadow | ❌ TODO | main missing piece |
+| Pass 3: Transparency | ❌ TODO | low priority, opaque scenes only |
+| Phase 4: type-aware SDF | ❌ TODO | optional refinement |
+
+### Pass 2: SDF shadow raymarching
+
+**New file: `cnn_v3/shaders/gbuf_shadow.wgsl`** — fullscreen render pass.
+
+Bind layout:
+
+| Binding | Type | Content |
+|---------|------|---------|
+| 0 | `uniform` | `GlobalUniforms` (`#include "common_uniforms"`) |
+| 1 | `storage read` | `ObjectsBuffer` |
+| 2 | `texture_depth_2d` | depth from Pass 1 |
+| 3 | `sampler` (non-filtering) | depth load |
+| 4 | `uniform` | `GBufLightsUniforms` (2 lights) |
+
+Algorithm per fragment:
+1. Reconstruct world position from NDC depth + `globals.inv_view_proj`
+2. For each object: `sdBox((inv_model * world_pos).xyz, vec3(1.0))` — proxy box in local space
+3. For each light: offset ray origin by `0.02 * surface_normal`; march shadow ray toward `light.direction`
+4. Soft shadow via `shadowWithStoredDistance()` from `render/raymarching_id`
+5. Combine lights: `shadow = min(shadow_light0, shadow_light1)`
+6. Discard fragments where depth == 1.0 (sky/background → shadow = 1.0)
+7. Output shadow factor to RGBA8Unorm render target (`.r` = shadow)
+
+**C++ additions (`gbuffer_effect.h/.cc`):**
+```cpp
+RenderPipeline shadow_pipeline_;
+void create_shadow_pipeline();
+```
+In `render()` between Pass 1 and the shadow/transp node clears:
+- Build bind group (global_uniforms_buf_, objects_buf_, depth_view, sampler_, lights_uniform_)
+- Run fullscreen triangle → `node_shadow_` color attachment
+- Remove the `clear_node(node_shadow_, 1.0f)` placeholder once the pass is live
+
+**Register:**
+- `cnn_v3/shaders/gbuf_shadow.wgsl` → `SHADER_GBUF_SHADOW` in `assets.txt`
+- `extern const char* gbuf_shadow_wgsl;` in `gbuffer_effect.cc`
+
+### Phase 4: Object-type-aware SDF (optional)
+
+Branch on `obj.params.x` (populated since this commit) using `math/sdf_shapes`:
+
+| Type value | ObjectType | SDF |
+|------------|-----------|-----|
+| 0 | CUBE | `sdBox(local_p, vec3(1))` |
+| 1 | SPHERE | `sdSphere(local_p, 1.0)` |
+| 2 | PLANE | `sdPlane(local_p, vec3(0,1,0), obj.params.y)` |
+| 3 | TORUS | `sdTorus(local_p, vec2(0.8, 0.2))` |
-Call `set_scene(scene, camera)` before the first render. The effect uploads
-`GlobalUniforms` (view-proj, camera pos, resolution) and `ObjectData` (model
-matrix, color) to GPU storage buffers each frame.
+Only worth adding after Pass 2 is validated visually.
---
@@ -386,13 +470,13 @@ auto gview = std::make_shared<GBufViewEffect>(ctx,
| Row | Col 0 | Col 1 | Col 2 | Col 3 |
|-----|-------|-------|-------|-------|
-| 0 | `alb.r` (red tint) | `alb.g` (green tint) | `alb.b` (blue tint) | `nrm.x` remap→[0,1] |
+| 0 | `alb.r` | `alb.g` | `alb.b` | `nrm.x` remap→[0,1] |
| 1 | `nrm.y` remap→[0,1] | `depth` (inverted) | `dzdx` ×20+0.5 | `dzdy` ×20+0.5 |
| 2 | `mat_id` | `prev.r` | `prev.g` | `prev.b` |
| 3 | `mip1.r` | `mip1.g` | `mip1.b` | `mip2.r` |
| 4 | `mip2.g` | `mip2.b` | `shadow` | `transp` |
-1-pixel gray grid lines separate cells. Dark background for out-of-range cells.
+All channels displayed as grayscale. 1-pixel gray grid lines separate cells. Dark background for out-of-range cells.
**Shader binding layout** (no sampler needed — integer texture):
diff --git a/cnn_v3/shaders/gbuf_view.wgsl b/cnn_v3/shaders/gbuf_view.wgsl
index f2ae085..a5e6c91 100644
--- a/cnn_v3/shaders/gbuf_view.wgsl
+++ b/cnn_v3/shaders/gbuf_view.wgsl
@@ -93,10 +93,5 @@ fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
disp = clamp(v, 0.0, 1.0);
}
- // Albedo channels: tint for identification (ch0=red, ch1=green, ch2=blue)
- if (ch == 0u) { return vec4f(disp, 0.0, 0.0, 1.0); }
- else if (ch == 1u) { return vec4f(0.0, disp, 0.0, 1.0); }
- else if (ch == 2u) { return vec4f(0.0, 0.0, disp, 1.0); }
-
return vec4f(disp, disp, disp, 1.0);
}
diff --git a/cnn_v3/src/gbuffer_effect.cc b/cnn_v3/src/gbuffer_effect.cc
index 750188f..f529d2b 100644
--- a/cnn_v3/src/gbuffer_effect.cc
+++ b/cnn_v3/src/gbuffer_effect.cc
@@ -41,41 +41,6 @@ struct GBufGlobalUniforms {
static_assert(sizeof(GBufGlobalUniforms) == sizeof(float) * 44,
"GBufGlobalUniforms must be 176 bytes");
-// Helper: create a 1×1 placeholder texture of a given format cleared to `value`.
-static WGPUTexture create_placeholder_tex(WGPUDevice device,
- WGPUTextureFormat format,
- float value) {
- WGPUTextureDescriptor desc = {};
- desc.usage = (WGPUTextureUsage)(WGPUTextureUsage_TextureBinding |
- WGPUTextureUsage_CopyDst);
- desc.dimension = WGPUTextureDimension_2D;
- desc.size = {1, 1, 1};
- desc.format = format;
- desc.mipLevelCount = 1;
- desc.sampleCount = 1;
- WGPUTexture tex = wgpuDeviceCreateTexture(device, &desc);
- return tex;
-}
-
-// Helper: write a single RGBA float pixel to a texture via queue.
-static void write_placeholder_pixel(WGPUQueue queue, WGPUTexture tex,
- float r, float g, float b, float a) {
- const float data[4] = {r, g, b, a};
- WGPUTexelCopyTextureInfo dst = {};
- dst.texture = tex;
- dst.mipLevel = 0;
- dst.origin = {0, 0, 0};
- dst.aspect = WGPUTextureAspect_All;
-
- WGPUTexelCopyBufferLayout layout = {};
- layout.offset = 0;
- layout.bytesPerRow = 16; // 4 × sizeof(float)
- layout.rowsPerImage = 1;
-
- const WGPUExtent3D extent = {1, 1, 1};
- wgpuQueueWriteTexture(queue, &dst, data, sizeof(data), &layout, &extent);
-}
-
// Create bilinear sampler.
static WGPUSampler create_bilinear_sampler(WGPUDevice device) {
WGPUSamplerDescriptor desc = {};
@@ -116,31 +81,9 @@ GBufferEffect::GBufferEffect(const GpuContext& ctx,
// Resolution uniform for pack shader.
pack_res_uniform_.init(ctx_.device);
+ lights_uniform_.init(ctx_.device);
- // Placeholder shadow (1.0 = fully lit) and transp (0.0 = opaque) textures.
- shadow_placeholder_tex_.set(
- create_placeholder_tex(ctx_.device, WGPUTextureFormat_RGBA32Float, 1.0f));
- write_placeholder_pixel(ctx_.queue,
- shadow_placeholder_tex_.get(), 1.0f, 0.0f, 0.0f, 1.0f);
-
- transp_placeholder_tex_.set(
- create_placeholder_tex(ctx_.device, WGPUTextureFormat_RGBA32Float, 0.0f));
- write_placeholder_pixel(ctx_.queue,
- transp_placeholder_tex_.get(), 0.0f, 0.0f, 0.0f, 1.0f);
-
- WGPUTextureViewDescriptor vd = {};
- vd.format = WGPUTextureFormat_RGBA32Float;
- vd.dimension = WGPUTextureViewDimension_2D;
- vd.baseMipLevel = 0;
- vd.mipLevelCount = 1;
- vd.baseArrayLayer = 0;
- vd.arrayLayerCount = 1;
- vd.aspect = WGPUTextureAspect_All;
-
- shadow_placeholder_view_.set(
- wgpuTextureCreateView(shadow_placeholder_tex_.get(), &vd));
- transp_placeholder_view_.set(
- wgpuTextureCreateView(transp_placeholder_tex_.get(), &vd));
+ create_linear_sampler();
create_raster_pipeline();
create_pack_pipeline();
@@ -162,19 +105,113 @@ void GBufferEffect::declare_nodes(NodeRegistry& registry) {
}
}
-void GBufferEffect::set_scene(const Scene* scene, const Camera* camera) {
- scene_ = scene;
- camera_ = camera;
+void GBufferEffect::set_scene() {
+ scene_.clear();
+ cube_anims_.clear();
+ sphere_anims_.clear();
+
+ // Deterministic pseudo-random (xorshift32).
+ uint32_t seed = 0xBEEF1234u;
+ auto rnd = [&]() -> float {
+ seed ^= seed << 13;
+ seed ^= seed >> 17;
+ seed ^= seed << 5;
+ return (float)(seed >> 8) / 16777216.0f; // [0, 1)
+ };
+ auto rrange = [&](float lo, float hi) { return lo + rnd() * (hi - lo); };
+
+ // 20 small cubes scattered in a [-2,2]×[-1.5,1.5]×[-1.5,1.5] volume.
+ static const int kNumCubes = 20;
+ for (int i = 0; i < kNumCubes; ++i) {
+ Object3D obj(ObjectType::CUBE);
+ obj.position = vec3(rrange(-2.0f, 2.0f),
+ rrange(-1.5f, 1.5f),
+ rrange(-1.5f, 1.5f));
+ const float s = rrange(0.10f, 0.25f);
+ obj.scale = vec3(s, s, s);
+ obj.color = vec4(rrange(0.4f, 1.0f),
+ rrange(0.4f, 1.0f),
+ rrange(0.4f, 1.0f), 1.0f);
+
+ // Random rotation axis (avoid degenerate zero-length axis).
+ vec3 axis = vec3(rrange(-1.0f, 1.0f),
+ rrange(-1.0f, 1.0f),
+ rrange(-1.0f, 1.0f));
+ if (axis.len() < 0.01f) axis = vec3(0.0f, 1.0f, 0.0f);
+ axis = axis.normalize();
+ const float speed = rrange(0.3f, 1.5f) * (rnd() > 0.5f ? 1.0f : -1.0f);
+
+ scene_.add_object(obj);
+ cube_anims_.push_back({axis, speed});
+ }
+
+ // 4 pumping spheres at fixed positions; radius modulated by audio_intensity.
+ static const vec3 kSpherePos[4] = {
+ { 0.0f, 0.0f, 0.0f},
+ { 1.5f, 0.5f, -0.5f},
+ {-1.5f, -0.5f, 0.5f},
+ { 0.0f, 1.0f, 1.0f},
+ };
+ static const float kBaseSphereRadius[4] = {0.35f, 0.28f, 0.30f, 0.25f};
+ for (int i = 0; i < 4; ++i) {
+ Object3D obj(ObjectType::SPHERE);
+ obj.position = kSpherePos[i];
+ const float r = kBaseSphereRadius[i];
+ obj.scale = vec3(r, r, r);
+ obj.color = vec4(0.85f, 0.60f, 0.95f, 1.0f);
+ const int idx = (int)scene_.objects.size();
+ scene_.add_object(obj);
+ sphere_anims_.push_back({idx, r});
+ }
+
+ // Camera: above and in front of the scene, looking at origin.
+ camera_.set_look_at(vec3(0.0f, 2.5f, 6.0f),
+ vec3(0.0f, 0.0f, 0.0f),
+ vec3(0.0f, 1.0f, 0.0f));
+ camera_.fov_y_rad = 0.7854f; // 45°
+ camera_.near_plane = 0.1f;
+ camera_.far_plane = 20.0f;
+ // aspect_ratio is updated each frame from params.resolution.
+
+ scene_ready_ = true;
}
void GBufferEffect::render(WGPUCommandEncoder encoder,
const UniformsSequenceParams& params,
NodeRegistry& nodes) {
- if (!scene_ || !camera_) {
+ if (!scene_ready_) {
return;
}
- upload_scene_data(*scene_, *camera_, params.time);
+ // Update camera aspect ratio from current resolution.
+ camera_.aspect_ratio = params.aspect_ratio;
+
+ // Animate cubes: axis-angle rotation driven by physical time.
+ for (int i = 0; i < (int)cube_anims_.size(); ++i) {
+ const CubeAnim& a = cube_anims_[(size_t)i];
+ scene_.objects[(size_t)i].rotation =
+ quat::from_axis(a.axis, params.time * a.speed);
+ }
+ // Pump spheres: scale with audio_intensity.
+ for (const SphereAnim& a : sphere_anims_) {
+ const float r = a.base_radius * (1.0f + params.audio_intensity * 0.8f);
+ scene_.objects[(size_t)a.obj_idx].scale = vec3(r, r, r);
+ }
+
+ // Upload two directional lights.
+ {
+ GBufLightsUniforms lu = {};
+ lu.params = vec4(2.0f, 0.0f, 0.0f, 0.0f);
+ // Key: warm sun, upper-right-front.
+ lu.lights[0].direction = vec4(0.408f, 0.816f, 0.408f, 0.0f); // norm(1,2,1)
+ lu.lights[0].color = vec4(1.00f, 0.92f, 0.78f, 1.0f);
+ // Fill: cool sky, upper-left-back.
+ lu.lights[1].direction = vec4(-0.577f, 0.577f, -0.577f, 0.0f); // norm(-1,1,-1)
+ lu.lights[1].color = vec4(0.40f, 0.45f, 0.80f, 0.4f);
+ lights_uniform_.update(ctx_.queue, lu);
+ }
+
+ upload_scene_data(scene_, camera_, params.time);
// Update resolution uniform for pack shader.
GBufResUniforms res_uni;
@@ -228,8 +265,8 @@ void GBufferEffect::render(WGPUCommandEncoder encoder,
raster_pass_desc.depthStencilAttachment = &depth_attachment;
const int num_objects =
- (int)(scene_->objects.size() < (size_t)kGBufMaxObjects
- ? scene_->objects.size()
+ (int)(scene_.objects.size() < (size_t)kGBufMaxObjects
+ ? scene_.objects.size()
: (size_t)kGBufMaxObjects);
if (num_objects > 0 && raster_pipeline_.get() != nullptr) {
@@ -250,13 +287,31 @@ void GBufferEffect::render(WGPUCommandEncoder encoder,
wgpuRenderPassEncoderRelease(raster_pass);
}
- // Pass 2: SDF raymarching — TODO (placeholder: shadow=1, transp=0 already set)
+ // Pass 2: SDF raymarching — TODO
// Pass 3: Lighting/shadow — TODO
+ // Clear shadow node to 1.0 (fully lit) and transp to 0.0 (fully opaque)
+ // until passes 2-3 are implemented.
+ auto clear_node = [&](const std::string& name, float value) {
+ WGPURenderPassColorAttachment att = {};
+ att.view = nodes.get_view(name);
+ att.loadOp = WGPULoadOp_Clear;
+ att.storeOp = WGPUStoreOp_Store;
+ att.clearValue = {value, value, value, value};
+ att.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
+ WGPURenderPassDescriptor pd = {};
+ pd.colorAttachmentCount = 1;
+ pd.colorAttachments = &att;
+ WGPURenderPassEncoder p = wgpuCommandEncoderBeginRenderPass(encoder, &pd);
+ wgpuRenderPassEncoderEnd(p);
+ wgpuRenderPassEncoderRelease(p);
+ };
+ clear_node(node_shadow_, 1.0f);
+ clear_node(node_transp_, 0.0f);
+
// --- Pass 4: Pack compute ---
// Rebuild pack bind group with current node views.
- // Construct a temporary bilinear sampler for this pass.
- WGPUSampler bilinear = create_bilinear_sampler(ctx_.device);
+ WGPUSampler bilinear = sampler_.get();
// Get texture views from nodes.
// shadow / transp are GBUF_R8 nodes; use their views.
@@ -320,7 +375,7 @@ void GBufferEffect::render(WGPUCommandEncoder encoder,
wgpuComputePassEncoderRelease(compute_pass);
wgpuBindGroupRelease(pack_bg);
- wgpuSamplerRelease(bilinear);
+ // bilinear is owned by sampler_ — no release here.
}
// ---- private helpers ----
@@ -373,7 +428,7 @@ void GBufferEffect::upload_scene_data(const Scene& scene,
d.model = m;
d.inv_model = m.inverse();
d.color = obj.color;
- d.params = vec4(0.0f, 0.0f, 0.0f, 0.0f);
+ d.params = vec4((float)(int)obj.type, 0.0f, 0.0f, 0.0f);
obj_data.push_back(d);
}
wgpuQueueWriteBuffer(ctx_.queue, objects_buf_.buffer, 0,
diff --git a/cnn_v3/src/gbuffer_effect.h b/cnn_v3/src/gbuffer_effect.h
index 42fb0ec..d45be75 100644
--- a/cnn_v3/src/gbuffer_effect.h
+++ b/cnn_v3/src/gbuffer_effect.h
@@ -10,6 +10,7 @@
#include "gpu/uniform_helper.h"
#include "gpu/wgpu_resource.h"
#include "util/mini_math.h"
+#include <vector>
// Uniform for the pack compute shader
struct GBufResUniforms {
@@ -20,6 +21,20 @@ struct GBufResUniforms {
static_assert(sizeof(GBufResUniforms) == 16,
"GBufResUniforms must be 16 bytes");
+// Single directional light: direction points *toward* the light source (world space).
+struct GBufLight {
+ vec4 direction; // xyz = normalized direction toward light, w = unused
+ vec4 color; // rgb = color, a = intensity
+};
+static_assert(sizeof(GBufLight) == 32, "GBufLight must be 32 bytes");
+
+struct GBufLightsUniforms {
+ GBufLight lights[2];
+ vec4 params; // x = num_lights
+};
+static_assert(sizeof(GBufLightsUniforms) == 80,
+ "GBufLightsUniforms must be 80 bytes");
+
class GBufferEffect : public Effect {
public:
GBufferEffect(const GpuContext& ctx, const std::vector<std::string>& inputs,
@@ -31,9 +46,22 @@ class GBufferEffect : public Effect {
void render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params,
NodeRegistry& nodes) override;
- void set_scene(const Scene* scene, const Camera* camera);
+ // Populate the internal scene with ~20 rotating cubes and a few pumping
+ // spheres. Must be called once before the first render().
+ void set_scene();
private:
+ // Per-cube animation state (axis-angle rotation)
+ struct CubeAnim {
+ vec3 axis;
+ float speed; // radians/second, may be negative
+ };
+ // Per-sphere animation state (radius driven by audio_intensity)
+ struct SphereAnim {
+ int obj_idx; // index into scene_.objects
+ float base_radius;
+ };
+
// Internal G-buffer node names
std::string node_albedo_;
std::string node_normal_mat_;
@@ -43,8 +71,13 @@ class GBufferEffect : public Effect {
std::string node_feat0_;
std::string node_feat1_;
- const Scene* scene_ = nullptr;
- const Camera* camera_ = nullptr;
+ // Owned scene and camera — populated by set_scene()
+ Scene scene_;
+ Camera camera_;
+ bool scene_ready_ = false;
+
+ std::vector<CubeAnim> cube_anims_;
+ std::vector<SphereAnim> sphere_anims_;
// Pass 1: MRT rasterization pipeline
RenderPipeline raster_pipeline_;
@@ -53,19 +86,13 @@ class GBufferEffect : public Effect {
// Pass 4: Pack compute pipeline
ComputePipeline pack_pipeline_;
BindGroup pack_bind_group_;
- UniformBuffer<GBufResUniforms> pack_res_uniform_;
-
- // Placeholder textures for shadow/transp (white/black cleared once)
- Texture shadow_placeholder_tex_;
- TextureView shadow_placeholder_view_;
- Texture transp_placeholder_tex_;
- TextureView transp_placeholder_view_;
+ UniformBuffer<GBufResUniforms> pack_res_uniform_;
+ UniformBuffer<GBufLightsUniforms> lights_uniform_;
// GPU-side object data buffers (global uniforms + objects storage)
- // These mirror the layout expected by gbuf_raster.wgsl
GpuBuffer global_uniforms_buf_;
GpuBuffer objects_buf_;
- int objects_buf_capacity_ = 0; // number of ObjectData slots allocated
+ int objects_buf_capacity_ = 0;
void create_raster_pipeline();
void create_pack_pipeline();
diff --git a/tools/seq_compiler.py b/tools/seq_compiler.py
index 09188a5..2d802b2 100755
--- a/tools/seq_compiler.py
+++ b/tools/seq_compiler.py
@@ -397,8 +397,9 @@ def generate_cpp(seq: SequenceDecl, sorted_effects: List[EffectDecl],
# Use a full #include string to override the path entirely.
CLASS_TO_HEADER = {
'NtscYiq': 'ntsc',
- 'GBufferEffect': '#include "../../cnn_v3/src/gbuffer_effect.h"',
- 'CNNv3Effect': '#include "../../cnn_v3/src/cnn_v3_effect.h"',
+ 'GBufferEffect': '#include "../../cnn_v3/src/gbuffer_effect.h"',
+ 'CNNv3Effect': '#include "../../cnn_v3/src/cnn_v3_effect.h"',
+ 'GBufViewEffect': '#include "../../cnn_v3/src/gbuf_view_effect.h"',
}
includes = set()
for effect in seq.effects:
diff --git a/workspaces/main/timeline.seq b/workspaces/main/timeline.seq
index 557e73d..0e1ca74 100644
--- a/workspaces/main/timeline.seq
+++ b/workspaces/main/timeline.seq
@@ -13,10 +13,8 @@ SEQUENCE 8.00 0 "scene 2"
SEQUENCE 12.00 0 "cnn_v3_test"
NODE gbuf_feat0 gbuf_rgba32uint
NODE gbuf_feat1 gbuf_rgba32uint
- NODE cnn_v3_out gbuf_albedo
EFFECT + GBufferEffect source -> gbuf_feat0 gbuf_feat1 0.00 8.00
- EFFECT + CNNv3Effect gbuf_feat0 gbuf_feat1 -> cnn_v3_out 0.00 8.00
- EFFECT + Passthrough cnn_v3_out -> sink 0.00 8.00
+ EFFECT + GBufViewEffect gbuf_feat0 gbuf_feat1 -> sink 0.00 8.00
SEQUENCE 20.00 2 "hybrid_heptagon"
# Heptagon -> Hybrid3D -> sink