From 7b89a7130a998017de98dde363a8d9be61d7d44e Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 22 Mar 2026 19:29:01 +0100 Subject: feat(cnn_v3): GBufferEffect Pass 2 — SDF shadow raymarching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements gbuf_shadow.wgsl: fullscreen render pass that reads depth from Pass 1, reconstructs world-space positions, evaluates a proxy-box SDF for each object (via inv_model), computes soft shadows for both directional lights using shadowWithStoredDistance(), and writes shadow factor to the RGBA8Unorm node_shadow_ target consumed by gbuf_pack.wgsl. Bind layout: B0=GlobalUniforms, B1=ObjectsBuffer (storage-read), B2=texture_depth_2d, B3=GBufLightsUniforms. Sky fragments (depth=1.0) are output as 1.0 (fully lit). Falls back to clear(1.0) if pipeline is not ready. 36/36 tests pass. handoff(Gemini): Pass 2 done. Pass 3 (transparency) still TODO. Phase 4 (type-aware SDF) optional after visual validation. --- cnn_v3/docs/HOWTO.md | 6 +- cnn_v3/shaders/gbuf_shadow.wgsl | 92 +++++++++++++++++++++++ cnn_v3/src/gbuffer_effect.cc | 162 +++++++++++++++++++++++++++++++++++++--- cnn_v3/src/gbuffer_effect.h | 4 + 4 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 cnn_v3/shaders/gbuf_shadow.wgsl (limited to 'cnn_v3') diff --git a/cnn_v3/docs/HOWTO.md b/cnn_v3/docs/HOWTO.md index 2d88019..765d80b 100644 --- a/cnn_v3/docs/HOWTO.md +++ b/cnn_v3/docs/HOWTO.md @@ -73,7 +73,7 @@ Each frame, `GBufferEffect::render()` executes: - 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 +2. **Pass 2 — SDF shadow raymarching** (`gbuf_shadow.wgsl`) ✅ - See implementation plan below. 3. **Pass 3 — Transparency** — TODO (deferred; transp=0 for opaque scenes) @@ -104,7 +104,7 @@ outputs[1] → feat_tex1 (rgba32uint: mat_id, prev.rgb, mip1.rgb, mip2.rgb, s | 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 2: SDF shadow | ✅ Done | `gbuf_shadow.wgsl`, proxy-box SDF per object | | Pass 3: Transparency | ❌ TODO | low priority, opaque scenes only | | Phase 4: type-aware SDF | ❌ TODO | optional refinement | @@ -337,7 +337,7 @@ Test vectors generated by `cnn_v3/training/gen_test_vectors.py` (PyTorch referen | Phase | Status | Notes | |-------|--------|-------| | 1 — G-buffer (raster + pack) | ✅ Done | Integrated, 36/36 tests pass | -| 1 — G-buffer (SDF + shadow passes) | TODO | Placeholder: shadow=1, transp=0 | +| 1 — G-buffer (SDF shadow pass) | ✅ Done | `gbuf_shadow.wgsl`, proxy-box SDF | | 2 — Training infrastructure | ✅ Done | blender_export.py, pack_*_sample.py | | 3 — WGSL U-Net shaders | ✅ Done | 5 compute shaders + cnn_v3/common snippet | | 4 — C++ CNNv3Effect | ✅ Done | FiLM uniform upload, 36/36 tests pass | diff --git a/cnn_v3/shaders/gbuf_shadow.wgsl b/cnn_v3/shaders/gbuf_shadow.wgsl new file mode 100644 index 0000000..36e7b28 --- /dev/null +++ b/cnn_v3/shaders/gbuf_shadow.wgsl @@ -0,0 +1,92 @@ +// G-buffer shadow raymarching shader for CNN v3 +// Pass 2: Reads depth from Pass 1, marches shadow rays toward lights, +// outputs shadow factor (1.0=lit, 0.0=shadow) to RGBA8Unorm render target (.r). + +#include "common_uniforms" +#include "camera_common" +#include "math/sdf_shapes" +#include "render/raymarching_id" + +@group(0) @binding(0) var globals: GlobalUniforms; +@group(0) @binding(1) var object_data: ObjectsBuffer; +@group(0) @binding(2) var depth_tex: texture_depth_2d; + +struct GBufLight { + direction: vec4f, // xyz = toward light (world space, normalized) + color: vec4f, // rgb = color, a = intensity +} +struct GBufLightsUniforms { + lights: array, + params: vec4f, // x = num_lights +} +@group(0) @binding(3) var lights: GBufLightsUniforms; + +// ---- SDF scene (proxy box per object in local space) ---- + +// Stub required by render/raymarching (shadow() / rayMarch() call df()). +fn df(p: vec3f) -> f32 { return MAX_RAY_LENGTH; } + +// SDF of the full scene: proxy box for each object transformed to local space. +fn dfWithID(p: vec3f) -> RayMarchResult { + var res: RayMarchResult; + res.distance = MAX_RAY_LENGTH; + res.distance_max = MAX_RAY_LENGTH; + res.object_id = 0.0; + + let n = u32(globals.params.x); + for (var i = 0u; i < n; i++) { + let obj = object_data.objects[i]; + let lp = (obj.inv_model * vec4f(p, 1.0)).xyz; + let d = sdBox(lp, vec3f(1.0)); + if (d < res.distance) { + res.distance = d; + res.object_id = f32(i + 1u); + } + } + return res; +} + +// ---- Vertex: fullscreen triangle ---- + +@vertex +fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4f { + let x = f32((vid & 1u) << 2u) - 1.0; + let y = f32((vid & 2u) << 1u) - 1.0; + return vec4f(x, y, 0.0, 1.0); +} + +// ---- Fragment: shadow factor per pixel ---- + +@fragment +fn fs_main(@builtin(position) pos: vec4f) -> @location(0) vec4f { + let depth = textureLoad(depth_tex, vec2i(pos.xy), 0); + + // Sky / background: fully lit. + if (depth >= 1.0) { + return vec4f(1.0); + } + + // Reconstruct world-space position from NDC + depth. + let res = globals.resolution; + let ndc = vec2f( + (pos.x / res.x) * 2.0 - 1.0, + 1.0 - (pos.y / res.y) * 2.0 + ); + let clip = globals.inv_view_proj * vec4f(ndc, depth, 1.0); + let world = clip.xyz / clip.w; + + // Surface normal estimated from SDF gradient. + let nor = normalWithID(world); + let bias_pos = world + nor * 0.02; + + // March shadow rays toward each light; take the darkest value. + var shadow_val = 1.0; + let num_lights = u32(lights.params.x); + for (var i = 0u; i < num_lights; i++) { + let ld = lights.lights[i].direction.xyz; + let s = shadowWithStoredDistance(bias_pos, ld, MAX_RAY_LENGTH); + shadow_val = min(shadow_val, s); + } + + return vec4f(shadow_val, shadow_val, shadow_val, 1.0); +} diff --git a/cnn_v3/src/gbuffer_effect.cc b/cnn_v3/src/gbuffer_effect.cc index f529d2b..89ed8fc 100644 --- a/cnn_v3/src/gbuffer_effect.cc +++ b/cnn_v3/src/gbuffer_effect.cc @@ -14,6 +14,7 @@ // For standalone use outside the asset system, the caller must ensure the WGSL // source strings are available. They are declared here as weak-linkable externs. extern const char* gbuf_raster_wgsl; +extern const char* gbuf_shadow_wgsl; extern const char* gbuf_pack_wgsl; // Maximum number of objects the G-buffer supports per frame. @@ -86,6 +87,7 @@ GBufferEffect::GBufferEffect(const GpuContext& ctx, create_linear_sampler(); create_raster_pipeline(); + create_shadow_pipeline(); create_pack_pipeline(); } @@ -287,17 +289,79 @@ void GBufferEffect::render(WGPUCommandEncoder encoder, wgpuRenderPassEncoderRelease(raster_pass); } - // Pass 2: SDF raymarching — TODO - // Pass 3: Lighting/shadow — TODO + // --- Pass 2: SDF shadow raymarching --- + if (shadow_pipeline_.get() != nullptr) { + WGPUBindGroupEntry shadow_entries[4] = {}; + shadow_entries[0].binding = 0; + shadow_entries[0].buffer = global_uniforms_buf_.buffer; + shadow_entries[0].size = sizeof(GBufGlobalUniforms); + + shadow_entries[1].binding = 1; + shadow_entries[1].buffer = objects_buf_.buffer; + shadow_entries[1].size = (size_t)objects_buf_capacity_ * sizeof(GBufObjectData); + + shadow_entries[2].binding = 2; + shadow_entries[2].textureView = depth_view; + + shadow_entries[3].binding = 3; + shadow_entries[3].buffer = lights_uniform_.get().buffer; + shadow_entries[3].size = sizeof(GBufLightsUniforms); + + WGPUBindGroupLayout shadow_bgl = + wgpuRenderPipelineGetBindGroupLayout(shadow_pipeline_.get(), 0); + + WGPUBindGroupDescriptor shadow_bg_desc = {}; + shadow_bg_desc.layout = shadow_bgl; + shadow_bg_desc.entryCount = 4; + shadow_bg_desc.entries = shadow_entries; + + WGPUBindGroup shadow_bg = + wgpuDeviceCreateBindGroup(ctx_.device, &shadow_bg_desc); + wgpuBindGroupLayoutRelease(shadow_bgl); + + WGPURenderPassColorAttachment shadow_att = {}; + shadow_att.view = nodes.get_view(node_shadow_); + shadow_att.loadOp = WGPULoadOp_Clear; + shadow_att.storeOp = WGPUStoreOp_Store; + shadow_att.clearValue = {1.0f, 1.0f, 1.0f, 1.0f}; + shadow_att.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + + WGPURenderPassDescriptor shadow_pass_desc = {}; + shadow_pass_desc.colorAttachmentCount = 1; + shadow_pass_desc.colorAttachments = &shadow_att; + + WGPURenderPassEncoder shadow_pass = + wgpuCommandEncoderBeginRenderPass(encoder, &shadow_pass_desc); + wgpuRenderPassEncoderSetPipeline(shadow_pass, shadow_pipeline_.get()); + wgpuRenderPassEncoderSetBindGroup(shadow_pass, 0, shadow_bg, 0, nullptr); + wgpuRenderPassEncoderDraw(shadow_pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(shadow_pass); + wgpuRenderPassEncoderRelease(shadow_pass); + wgpuBindGroupRelease(shadow_bg); + } else { + // Fallback: clear to 1.0 (fully lit) if pipeline not ready. + WGPURenderPassColorAttachment att = {}; + att.view = nodes.get_view(node_shadow_); + att.loadOp = WGPULoadOp_Clear; + att.storeOp = WGPUStoreOp_Store; + att.clearValue = {1.0f, 1.0f, 1.0f, 1.0f}; + att.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + WGPURenderPassDescriptor pd = {}; + pd.colorAttachmentCount = 1; + pd.colorAttachments = &att; + WGPURenderPassEncoder p = wgpuCommandEncoderBeginRenderPass(encoder, &pd); + wgpuRenderPassEncoderEnd(p); + wgpuRenderPassEncoderRelease(p); + } - // 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) { + // Pass 3: Transparency — TODO (deferred; opaque scenes only) + // Clear transp node to 0.0 (fully opaque) until pass 3 is implemented. + { WGPURenderPassColorAttachment att = {}; - att.view = nodes.get_view(name); + att.view = nodes.get_view(node_transp_); att.loadOp = WGPULoadOp_Clear; att.storeOp = WGPUStoreOp_Store; - att.clearValue = {value, value, value, value}; + att.clearValue = {0.0f, 0.0f, 0.0f, 0.0f}; att.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; WGPURenderPassDescriptor pd = {}; pd.colorAttachmentCount = 1; @@ -305,9 +369,7 @@ void GBufferEffect::render(WGPUCommandEncoder encoder, 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. @@ -517,6 +579,86 @@ void GBufferEffect::create_raster_pipeline() { wgpuShaderModuleRelease(shader); } +void GBufferEffect::create_shadow_pipeline() { + HEADLESS_RETURN_IF_NULL(ctx_.device); + + const char* src = gbuf_shadow_wgsl; + if (!src) { + return; + } + + const std::string composed = ShaderComposer::Get().Compose({}, src); + + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = str_view(composed.c_str()); + + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = &wgsl_src.chain; + WGPUShaderModule shader = wgpuDeviceCreateShaderModule(ctx_.device, &shader_desc); + + // BGL: B0=GlobalUniforms, B1=ObjectsBuffer, B2=texture_depth_2d, B3=GBufLightsUniforms + WGPUBindGroupLayoutEntry bgl_entries[4] = {}; + + bgl_entries[0].binding = 0; + bgl_entries[0].visibility = + (WGPUShaderStage)(WGPUShaderStage_Vertex | WGPUShaderStage_Fragment); + bgl_entries[0].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[0].buffer.minBindingSize = sizeof(GBufGlobalUniforms); + + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Fragment; + bgl_entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage; + bgl_entries[1].buffer.minBindingSize = sizeof(GBufObjectData); + + bgl_entries[2].binding = 2; + bgl_entries[2].visibility = WGPUShaderStage_Fragment; + bgl_entries[2].texture.sampleType = WGPUTextureSampleType_Depth; + bgl_entries[2].texture.viewDimension = WGPUTextureViewDimension_2D; + + bgl_entries[3].binding = 3; + bgl_entries[3].visibility = WGPUShaderStage_Fragment; + bgl_entries[3].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[3].buffer.minBindingSize = sizeof(GBufLightsUniforms); + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 4; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(ctx_.device, &bgl_desc); + + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &bgl; + WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(ctx_.device, &pl_desc); + + // Color target: RGBA8Unorm (NodeType::GBUF_R8) + WGPUColorTargetState color_target = {}; + color_target.format = WGPUTextureFormat_RGBA8Unorm; + color_target.writeMask = WGPUColorWriteMask_All; + + WGPUFragmentState frag = {}; + frag.module = shader; + frag.entryPoint = str_view("fs_main"); + frag.targetCount = 1; + frag.targets = &color_target; + + WGPURenderPipelineDescriptor pipe_desc = {}; + pipe_desc.layout = pl; + pipe_desc.vertex.module = shader; + pipe_desc.vertex.entryPoint = str_view("vs_main"); + pipe_desc.fragment = &frag; + pipe_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pipe_desc.primitive.cullMode = WGPUCullMode_None; + pipe_desc.multisample.count = 1; + pipe_desc.multisample.mask = 0xFFFFFFFF; + + shadow_pipeline_.set(wgpuDeviceCreateRenderPipeline(ctx_.device, &pipe_desc)); + + wgpuPipelineLayoutRelease(pl); + wgpuBindGroupLayoutRelease(bgl); + wgpuShaderModuleRelease(shader); +} + void GBufferEffect::create_pack_pipeline() { HEADLESS_RETURN_IF_NULL(ctx_.device); diff --git a/cnn_v3/src/gbuffer_effect.h b/cnn_v3/src/gbuffer_effect.h index d45be75..c39219b 100644 --- a/cnn_v3/src/gbuffer_effect.h +++ b/cnn_v3/src/gbuffer_effect.h @@ -83,6 +83,9 @@ class GBufferEffect : public Effect { RenderPipeline raster_pipeline_; BindGroup raster_bind_group_; + // Pass 2: SDF shadow pipeline + RenderPipeline shadow_pipeline_; + // Pass 4: Pack compute pipeline ComputePipeline pack_pipeline_; BindGroup pack_bind_group_; @@ -95,6 +98,7 @@ class GBufferEffect : public Effect { int objects_buf_capacity_ = 0; void create_raster_pipeline(); + void create_shadow_pipeline(); void create_pack_pipeline(); void update_raster_bind_group(NodeRegistry& nodes); -- cgit v1.2.3