Skip to main content

cvkg_render_gpu/passes/
opaque3d.rs

1//! Opaque 3D render pass Kvasir node — renders 3D meshes with PBR shading
2//! and reads the shadow map from the Shadow pass.
3
4use crate::kvasir::nodes::PassId;
5use crate::kvasir::{ExecutionContext, KvasirNode, ResourceId};
6use crate::passes::shadow::{DirectionalLight, GpuMesh3d};
7
8/// Opaque 3D render pass node — renders 3D meshes with PBR shading and PCF shadow sampling.
9pub struct Opaque3dNode {
10    /// GPU-ready mesh instances to render.
11    pub mesh_instances: Vec<GpuMesh3d>,
12    /// Active directional light for shading.
13    pub light: DirectionalLight,
14    /// Shadow map resource to sample for shadow attenuation.
15    pub shadow_map: ResourceId,
16}
17
18impl KvasirNode for Opaque3dNode {
19    fn label(&self) -> &'static str {
20        "Opaque3d"
21    }
22
23    fn inputs(&self) -> &[ResourceId] {
24        &[]
25    }
26
27    fn outputs(&self) -> &[ResourceId] {
28        &[]
29    }
30
31    fn pass_id(&self) -> PassId {
32        PassId::Opaque3d
33    }
34
35    fn execute(&self, ctx: &mut ExecutionContext) {
36        tracing::debug!(
37            "Opaque3dNode::execute — instances={}, shadow_map={:?}",
38            self.mesh_instances.len(),
39            self.shadow_map,
40        );
41
42        if self.mesh_instances.is_empty() {
43            return;
44        }
45
46        // Use the main scene render target (RES_SCENE) for color output.
47        let scene_view = match ctx
48            .registry
49            .get_texture_view(crate::kvasir::nodes::RES_SCENE)
50        {
51            Some(v) => v,
52            None => {
53                tracing::error!("Opaque3dNode: missing scene color target");
54                return;
55            }
56        };
57        let depth_view = ctx.depth_view;
58
59        // Get shadow resources and update scene uniforms.
60        let shadow_bind_group = match ctx.registry.get_texture_view(self.shadow_map) {
61            Some(shadow_view) => {
62                let shadow_sampler = match ctx.renderer.shadow_sampler.as_ref() {
63                    Some(s) => s,
64                    None => {
65                        tracing::warn!("Opaque3dNode: missing shadow sampler");
66                        return;
67                    }
68                };
69
70                // Compute light VP for the scene uniform update
71                let light_dir = self.light.direction;
72                let scene_radius = 100.0;
73                let light_pos = glam::Vec3::ZERO + light_dir * scene_radius * 2.0;
74                let light_view = glam::Mat4::look_at_lh(light_pos, glam::Vec3::ZERO, glam::Vec3::Y);
75                let light_proj = glam::Mat4::orthographic_lh(
76                    -scene_radius,
77                    scene_radius,
78                    -scene_radius,
79                    scene_radius,
80                    0.0,
81                    scene_radius * 4.0,
82                );
83                let light_vp = light_proj * light_view;
84
85                let mut scene = ctx.renderer.current_scene;
86                scene.light_direction = light_dir.to_array();
87                scene.light_color = self.light.color.to_array();
88                scene.light_vp = light_vp;
89                ctx.queue
90                    .write_buffer(&ctx.renderer.scene_buffer, 0, bytemuck::bytes_of(&scene));
91
92                let ibl_view_owned = ctx
93                    .registry
94                    .get_texture_view(crate::kvasir::nodes::RES_BLUR_A);
95                let ibl_view = match &ibl_view_owned {
96                    Some(view) => view,
97                    None => &ctx.renderer.dummy_view,
98                };
99
100                Some(ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
101                    label: Some("Opaque3d PBR Material Bind Group"),
102                    layout: &ctx.renderer.pbr_material_bind_group_layout,
103                    entries: &[
104                        wgpu::BindGroupEntry {
105                            binding: 0,
106                            resource: wgpu::BindingResource::TextureView(&shadow_view),
107                        },
108                        wgpu::BindGroupEntry {
109                            binding: 1,
110                            resource: wgpu::BindingResource::Sampler(shadow_sampler),
111                        },
112                        wgpu::BindGroupEntry {
113                            binding: 8,
114                            resource: wgpu::BindingResource::TextureView(ibl_view),
115                        },
116                        wgpu::BindGroupEntry {
117                            binding: 9,
118                            resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
119                        },
120                        wgpu::BindGroupEntry {
121                            binding: 6,
122                            resource: wgpu::BindingResource::TextureView(&ctx.renderer.dummy_view),
123                        },
124                        wgpu::BindGroupEntry {
125                            binding: 7,
126                            resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
127                        },
128                    ],
129                }))
130            }
131            None => {
132                tracing::warn!("Opaque3dNode: missing shadow map view, skipping shadow sampling");
133                return;
134            }
135        };
136
137        // Default dark background color for 3D scenes.
138        let bg = [0.02f32, 0.02, 0.05, 1.0];
139
140        // Set up PBR render pass with color + depth attachments.
141        let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
142            label: Some("Opaque3d Pass (PBR + Shadows)"),
143            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
144                view: &scene_view,
145                resolve_target: None,
146                ops: wgpu::Operations {
147                    load: wgpu::LoadOp::Clear(wgpu::Color {
148                        r: bg[0] as f64,
149                        g: bg[1] as f64,
150                        b: bg[2] as f64,
151                        a: bg[3] as f64,
152                    }),
153                    store: wgpu::StoreOp::Store,
154                },
155                depth_slice: None,
156            })],
157            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
158                view: depth_view,
159                depth_ops: Some(wgpu::Operations {
160                    load: wgpu::LoadOp::Clear(0.0),
161                    store: wgpu::StoreOp::Store,
162                }),
163                stencil_ops: None,
164            }),
165            timestamp_writes: None,
166            occlusion_query_set: None,
167            multiview_mask: None,
168        });
169
170        // Bind the PBR pipeline and required bind groups.
171        pass.set_pipeline(&ctx.renderer.pbr_pipeline);
172        pass.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
173        if let Some(ref bg) = shadow_bind_group {
174            pass.set_bind_group(3, bg, &[]);
175        }
176
177        if let Some(ref inst_buffer) = ctx.renderer.instance_buffer_3d {
178            pass.set_vertex_buffer(1, inst_buffer.slice(..));
179        }
180
181        // For each mesh instance, set vertex/index buffers and draw.
182        // TODO: Once SkinnedOutput is extended to include UV/color/tangent (matching Vertex3D layout),
183        // enable skinned_buffer binding here. Currently SkinnedOutput only has position+normal,
184        // which causes PBR shaders to read garbage for UVs/colors/tangents.
185        for mesh in self.mesh_instances.iter() {
186            // if let Some(skinned) = &mesh.skinned_buffer {
187            //     pass.set_vertex_buffer(0, skinned.slice(..));
188            // } else {
189            pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
190            // }
191            pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
192            let inst = mesh.instance_index;
193            pass.draw_indexed(0..mesh.index_count, 0, inst..(inst + 1));
194        }
195    }
196}