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/src/gbuffer_effect.cc | 162 ++++++++++++++++++++++++++++++++++++++++--- cnn_v3/src/gbuffer_effect.h | 4 ++ 2 files changed, 156 insertions(+), 10 deletions(-) (limited to 'cnn_v3/src') 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