summaryrefslogtreecommitdiff
path: root/cnn_v3
diff options
context:
space:
mode:
Diffstat (limited to 'cnn_v3')
-rw-r--r--cnn_v3/docs/HOWTO.md6
-rw-r--r--cnn_v3/shaders/gbuf_shadow.wgsl92
-rw-r--r--cnn_v3/src/gbuffer_effect.cc162
-rw-r--r--cnn_v3/src/gbuffer_effect.h4
4 files changed, 251 insertions, 13 deletions
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<uniform> globals: GlobalUniforms;
+@group(0) @binding(1) var<storage, read> 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<GBufLight, 2>,
+ params: vec4f, // x = num_lights
+}
+@group(0) @binding(3) var<uniform> 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,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);