summaryrefslogtreecommitdiff
path: root/cnn_v3/src
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-22 19:29:01 +0100
committerskal <pascal.massimino@gmail.com>2026-03-22 19:29:01 +0100
commit7b89a7130a998017de98dde363a8d9be61d7d44e (patch)
tree2b45a3253a5770170d787915b5c3d18fd69e40ac /cnn_v3/src
parentc5e66964c0463219019d0439ec20b79248637fa4 (diff)
feat(cnn_v3): GBufferEffect Pass 2 — SDF shadow raymarching
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.
Diffstat (limited to 'cnn_v3/src')
-rw-r--r--cnn_v3/src/gbuffer_effect.cc162
-rw-r--r--cnn_v3/src/gbuffer_effect.h4
2 files changed, 156 insertions, 10 deletions
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,62 @@ 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);
- // 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) {
+ 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(name);
+ att.view = nodes.get_view(node_shadow_);
att.loadOp = WGPULoadOp_Clear;
att.storeOp = WGPUStoreOp_Store;
- att.clearValue = {value, value, value, value};
+ att.clearValue = {1.0f, 1.0f, 1.0f, 1.0f};
att.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
WGPURenderPassDescriptor pd = {};
pd.colorAttachmentCount = 1;
@@ -305,9 +352,24 @@ 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 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(node_transp_);
+ att.loadOp = WGPULoadOp_Clear;
+ att.storeOp = WGPUStoreOp_Store;
+ att.clearValue = {0.0f, 0.0f, 0.0f, 0.0f};
+ att.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
+ WGPURenderPassDescriptor pd = {};
+ pd.colorAttachmentCount = 1;
+ pd.colorAttachments = &att;
+ WGPURenderPassEncoder p = wgpuCommandEncoderBeginRenderPass(encoder, &pd);
+ wgpuRenderPassEncoderEnd(p);
+ wgpuRenderPassEncoderRelease(p);
+ }
// --- 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);