Skip to main content

engvis_renderer/
renderer.rs

1use wgpu::util::DeviceExt;
2use engvis_core::{OrbitCamera, Scene, VertexRenderOptions, EdgeRenderOptions};
3use glam::Affine3A;
4
5use crate::depth::DepthTexture;
6use crate::grid_renderer::GridRenderer;
7use crate::lighting::LightingBuffer;
8use crate::material_pipeline::MaterialPipeline;
9use crate::mesh_renderer::MeshRenderer;
10use crate::overlay_renderer::OverlayRenderer;
11use crate::texture_cache::TextureCache;
12
13const MSAA_SAMPLE_COUNT: u32 = 4;
14
15#[derive(Debug, Clone, Copy)]
16enum OverlayDrawMode {
17    Vertices,
18    Edges,
19}
20
21/// Scene uniform data (group 0)
22#[repr(C)]
23#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
24pub struct SceneUniforms {
25    pub view_proj: [[f32; 4]; 4],
26    pub camera_pos: [f32; 4],
27    pub viewport: [f32; 4],
28    pub global_opacity: [f32; 4],
29}
30
31pub struct Renderer {
32    pub depth: DepthTexture,
33    pub msaa_texture: wgpu::Texture,
34    pub msaa_view: wgpu::TextureView,
35    pub surface_format: wgpu::TextureFormat,
36    pub scene_uniform_buffer: wgpu::Buffer,
37    pub scene_bind_group: wgpu::BindGroup,
38    pub scene_bind_group_layout: wgpu::BindGroupLayout,
39    pub lighting: LightingBuffer,
40    pub material_pipeline: MaterialPipeline,
41    pub mesh_renderer: MeshRenderer,
42    pub grid_renderer: GridRenderer,
43    pub overlay_renderer: OverlayRenderer,
44    pub texture_cache: TextureCache,
45    pub show_surface: bool,
46    pub show_grid: bool,
47    pub vertex_opts: VertexRenderOptions,
48    pub edge_opts: EdgeRenderOptions,
49    pub opacity: f32,
50}
51
52impl Renderer {
53    pub fn new(
54        device: &wgpu::Device,
55        queue: &wgpu::Queue,
56        surface_format: wgpu::TextureFormat,
57        scene: &Scene,
58        width: u32,
59        height: u32,
60    ) -> Self {
61        let depth = DepthTexture::new(device, width.max(1), height.max(1), MSAA_SAMPLE_COUNT);
62
63        // MSAA color target: render into this multisampled texture, then resolve to surface
64        let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
65            label: Some("MSAA Color Texture"),
66            size: wgpu::Extent3d {
67                width: width.max(1),
68                height: height.max(1),
69                depth_or_array_layers: 1,
70            },
71            mip_level_count: 1,
72            sample_count: MSAA_SAMPLE_COUNT,
73            dimension: wgpu::TextureDimension::D2,
74            format: surface_format,
75            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
76            view_formats: &[],
77        });
78        let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
79
80        // Scene uniform bind group (group 0)
81        let scene_bind_group_layout =
82            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
83                label: Some("Scene Bind Group Layout"),
84                entries: &[wgpu::BindGroupLayoutEntry {
85                    binding: 0,
86                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
87                    ty: wgpu::BindingType::Buffer {
88                        ty: wgpu::BufferBindingType::Uniform,
89                        has_dynamic_offset: false,
90                        min_binding_size: None,
91                    },
92                    count: None,
93                }],
94            });
95
96        let initial_uniforms = SceneUniforms {
97            view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
98            camera_pos: [0.0; 4],
99            viewport: [width as f32, height as f32, 0.0, 0.0],
100            global_opacity: [1.0, 0.0, 0.0, 0.0],
101        };
102
103        let scene_uniform_buffer =
104            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
105                label: Some("Scene Uniform Buffer"),
106                contents: bytemuck::cast_slice(&[initial_uniforms]),
107                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
108            });
109
110        let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
111            label: Some("Scene Bind Group"),
112            layout: &scene_bind_group_layout,
113            entries: &[wgpu::BindGroupEntry {
114                binding: 0,
115                resource: scene_uniform_buffer.as_entire_binding(),
116            }],
117        });
118
119        // Lighting (group 1)
120        let lighting = LightingBuffer::new(device, &scene.lighting);
121
122        // Material pipeline
123        // Grid renderer (needs its own reference to scene layout)
124        let scene_layout_for_grid = scene_bind_group_layout.clone();
125        let grid_renderer = GridRenderer::new(device, surface_format, &scene_layout_for_grid);
126
127        let mesh_renderer = MeshRenderer::new(device);
128        let material_pipeline = MaterialPipeline::new(
129            device,
130            surface_format,
131            &scene_bind_group_layout,
132            &lighting.bind_group_layout,
133            &mesh_renderer.object_bind_group_layout,
134        );
135
136        // Overlay renderer (needs scene layout + object layout)
137        let scene_layout_for_overlay = scene_bind_group_layout.clone();
138        let overlay_renderer = OverlayRenderer::new(
139            device,
140            surface_format,
141            crate::depth::DepthTexture::FORMAT,
142            &scene_layout_for_overlay,
143            &mesh_renderer.object_bind_group_layout,
144        );
145
146        // Texture cache
147        let texture_cache = TextureCache::new(device, queue);
148
149        // Upload meshes
150        let mut renderer = Self {
151            depth,
152            msaa_texture,
153            msaa_view,
154            surface_format,
155            scene_uniform_buffer,
156            scene_bind_group,
157            scene_bind_group_layout,
158            lighting,
159            material_pipeline,
160            mesh_renderer,
161            grid_renderer,
162            overlay_renderer,
163            texture_cache,
164            show_surface: true,
165            show_grid: true,
166            vertex_opts: VertexRenderOptions::default(),
167            edge_opts: EdgeRenderOptions::default(),
168            opacity: 1.0,
169        };
170
171        // Upload scene meshes
172        for mesh in &scene.meshes {
173            renderer.mesh_renderer.upload_mesh(device, mesh);
174        }
175
176        renderer
177    }
178
179    pub fn upload_scene(
180        &mut self,
181        device: &wgpu::Device,
182        queue: &wgpu::Queue,
183        scene: &Scene,
184    ) {
185        // Re-upload meshes
186        self.mesh_renderer.mesh_buffers.clear();
187        for mesh in &scene.meshes {
188            self.mesh_renderer.upload_mesh(device, mesh);
189        }
190
191        // Create material bind groups and store uniform buffers for runtime updates
192        self.mesh_renderer.material_bind_groups.clear();
193        self.mesh_renderer.material_uniform_buffers.clear();
194        for material in &scene.materials {
195            let (bg, buf) = self
196                .material_pipeline
197                .create_material_bind_group(device, material, &self.texture_cache);
198            self.mesh_renderer.material_bind_groups.push(bg);
199            self.mesh_renderer.material_uniform_buffers.push(buf);
200        }
201
202        // Update lighting
203        self.lighting.update(queue, &scene.lighting);
204    }
205
206    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
207        if width > 0 && height > 0 {
208            self.depth.resize(device, width, height);
209
210            // Recreate MSAA color texture
211            self.msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
212                label: Some("MSAA Color Texture"),
213                size: wgpu::Extent3d {
214                    width: width.max(1),
215                    height: height.max(1),
216                    depth_or_array_layers: 1,
217                },
218                mip_level_count: 1,
219                sample_count: MSAA_SAMPLE_COUNT,
220                dimension: wgpu::TextureDimension::D2,
221                format: self.surface_format,
222                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
223                view_formats: &[],
224            });
225            self.msaa_view = self.msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
226        }
227    }
228
229    pub fn render_frame(
230        &mut self,
231        device: &wgpu::Device,
232        queue: &wgpu::Queue,
233        view: &wgpu::TextureView,
234        scene: &Scene,
235        camera: &OrbitCamera,
236    ) -> wgpu::CommandBuffer {
237        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
238            label: Some("Scene Encoder"),
239        });
240        self.render_scene_pass(device, queue, view, &mut encoder, scene, camera);
241        encoder.finish()
242    }
243
244    /// Record the 3D scene pass onto an existing encoder (shared with egui).
245    /// The view is the same surface texture view that egui will use with LoadOp::Load.
246    pub fn render_scene_pass(
247        &self,
248        device: &wgpu::Device,
249        queue: &wgpu::Queue,
250        view: &wgpu::TextureView,
251        encoder: &mut wgpu::CommandEncoder,
252        scene: &Scene,
253        camera: &OrbitCamera,
254    ) {
255        // Update scene uniforms
256        let vp = camera.view_projection();
257        let pos = camera.position();
258        let uniforms = SceneUniforms {
259            view_proj: vp.to_cols_array_2d(),
260            camera_pos: [pos.x, pos.y, pos.z, 0.0],
261            viewport: [
262                self.depth.texture.width() as f32,
263                self.depth.texture.height() as f32,
264                0.0,
265                0.0,
266            ],
267            global_opacity: [self.opacity, 0.0, 0.0, 0.0],
268        };
269        queue.write_buffer(
270            &self.scene_uniform_buffer,
271            0,
272            bytemuck::cast_slice(&[uniforms]),
273        );
274        self.lighting.update(queue, &scene.lighting);
275
276        // Sync material uniforms from scene (allows real-time UI editing)
277        for (i, material) in scene.materials.iter().enumerate() {
278            if i < self.mesh_renderer.material_uniform_buffers.len() {
279                let mat_uniforms = crate::material_pipeline::MaterialUniforms {
280                    albedo: material.albedo,
281                    emissive: [material.emissive[0], material.emissive[1], material.emissive[2], 0.0],
282                    metallic: material.metallic,
283                    roughness: material.roughness,
284                    normal_scale: material.normal_scale,
285                    alpha_cutoff: material.alpha_cutoff,
286                };
287                queue.write_buffer(
288                    &self.mesh_renderer.material_uniform_buffers[i],
289                    0,
290                    bytemuck::cast_slice(&[mat_uniforms]),
291                );
292            }
293        }
294
295        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
296            label: Some("Scene Render Pass"),
297            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
298                view: &self.msaa_view,
299                resolve_target: Some(view),
300                ops: wgpu::Operations {
301                    load: wgpu::LoadOp::Clear(wgpu::Color {
302                        r: 0.18,
303                        g: 0.20,
304                        b: 0.24,
305                        a: 1.0,
306                    }),
307                    store: wgpu::StoreOp::Store,
308                },
309                depth_slice: None,
310            })],
311            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
312                view: &self.depth.view,
313                depth_ops: Some(wgpu::Operations {
314                    load: wgpu::LoadOp::Clear(1.0),
315                    store: wgpu::StoreOp::Store,
316                }),
317                stencil_ops: None,
318            }),
319            occlusion_query_set: None,
320            timestamp_writes: None,
321        });
322
323        render_pass.set_bind_group(0, &self.scene_bind_group, &[]);
324        render_pass.set_bind_group(1, &self.lighting.bind_group, &[]);
325
326        if self.show_surface {
327            render_pass.set_pipeline(&self.material_pipeline.solid_pipeline);
328            self.render_scene_nodes(&mut render_pass, scene, device, Affine3A::IDENTITY);
329        }
330
331        if self.show_grid {
332            self.grid_renderer.render(&mut render_pass);
333        }
334
335        // --- Edge overlay ---
336        if self.edge_opts.enabled {
337            let (_buf, edge_overlay_bg) = self
338                .overlay_renderer
339                .create_uniform_bind_group(device, self.edge_opts.color, 0.0, self.edge_opts.line_width);
340
341            render_pass.set_pipeline(&self.overlay_renderer.line_pipeline);
342            self.render_overlay_nodes(&mut render_pass, scene, device, Affine3A::IDENTITY, &edge_overlay_bg, OverlayDrawMode::Edges);
343        }
344
345        // Vertex overlay renders in the same MSAA pass
346        if self.vertex_opts.enabled {
347            render_pass.set_pipeline(&self.overlay_renderer.point_pipeline);
348            let (_buf, point_overlay_bg) = self
349                .overlay_renderer
350                .create_uniform_bind_group(device, self.vertex_opts.color, self.vertex_opts.point_size, 0.0);
351            self.render_overlay_nodes(&mut render_pass, scene, device, Affine3A::IDENTITY, &point_overlay_bg, OverlayDrawMode::Vertices);
352        }
353    }
354
355    fn render_overlay_nodes(
356        &self,
357        render_pass: &mut wgpu::RenderPass,
358        scene: &Scene,
359        device: &wgpu::Device,
360        parent_transform: Affine3A,
361        overlay_bind_group: &wgpu::BindGroup,
362        mode: OverlayDrawMode,
363    ) {
364        for node in &scene.nodes {
365            self.render_overlay_node(render_pass, scene, device, node, parent_transform, overlay_bind_group, mode);
366        }
367    }
368
369    fn render_overlay_node(
370        &self,
371        render_pass: &mut wgpu::RenderPass,
372        scene: &Scene,
373        device: &wgpu::Device,
374        node: &engvis_core::SceneNode,
375        parent_transform: Affine3A,
376        overlay_bind_group: &wgpu::BindGroup,
377        mode: OverlayDrawMode,
378    ) {
379        if !node.visible {
380            return;
381        }
382
383        let world_transform = parent_transform * node.local_transform;
384
385        if let Some(mesh_idx) = node.mesh_index {
386            if mesh_idx < self.mesh_renderer.mesh_buffers.len() {
387                let mesh_buf = &self.mesh_renderer.mesh_buffers[mesh_idx];
388
389                render_pass.set_bind_group(0, &self.scene_bind_group, &[]);
390
391                let (_obj_buf, obj_bg) = self
392                    .mesh_renderer
393                    .create_object_bind_group(device, world_transform);
394                render_pass.set_bind_group(1, &obj_bg, &[]);
395                render_pass.set_bind_group(2, overlay_bind_group, &[]);
396
397                match mode {
398                    OverlayDrawMode::Vertices => {
399                        // Point pipeline: slot 0 = mesh vertices (per-instance), slot 1 = point quad
400                        render_pass.set_vertex_buffer(0, mesh_buf.vertex_buffer.slice(..));
401                        render_pass.set_vertex_buffer(1, self.overlay_renderer.point_quad_buffer.slice(..));
402                        render_pass.draw(0..6, 0..mesh_buf.vertex_count);
403                    }
404                    OverlayDrawMode::Edges => {
405                        // Line pipeline: slot 0 = edge endpoints (per-instance), slot 1 = line quad
406                        render_pass.set_vertex_buffer(0, mesh_buf.edge_endpoint_buffer.slice(..));
407                        render_pass.set_vertex_buffer(1, self.overlay_renderer.line_quad_buffer.slice(..));
408                        render_pass.draw(0..6, 0..mesh_buf.edge_instance_count);
409                    }
410                }
411            }
412        }
413
414        for child in &node.children {
415            self.render_overlay_node(render_pass, scene, device, child, world_transform, overlay_bind_group, mode);
416        }
417    }
418
419    pub fn render_scene_nodes(
420        &self,
421        render_pass: &mut wgpu::RenderPass,
422        scene: &Scene,
423        device: &wgpu::Device,
424        parent_transform: Affine3A,
425    ) {
426        for node in &scene.nodes {
427            self.render_node(render_pass, scene, device, node, parent_transform);
428        }
429    }
430
431    fn render_node(
432        &self,
433        render_pass: &mut wgpu::RenderPass,
434        scene: &Scene,
435        device: &wgpu::Device,
436        node: &engvis_core::SceneNode,
437        parent_transform: Affine3A,
438    ) {
439        if !node.visible {
440            return;
441        }
442
443        let world_transform = parent_transform * node.local_transform;
444
445        if let Some(mesh_idx) = node.mesh_index {
446            if mesh_idx < self.mesh_renderer.mesh_buffers.len() {
447                let mesh_buf = &self.mesh_renderer.mesh_buffers[mesh_idx];
448                let mesh = &scene.meshes[mesh_idx];
449
450                render_pass.set_vertex_buffer(0, mesh_buf.vertex_buffer.slice(..));
451                render_pass.set_index_buffer(
452                    mesh_buf.index_buffer.slice(..),
453                    wgpu::IndexFormat::Uint32,
454                );
455
456                // Create object bind group for this node
457                let (_obj_buf, obj_bg) = self
458                    .mesh_renderer
459                    .create_object_bind_group(device, world_transform);
460
461                render_pass.set_bind_group(3, &obj_bg, &[]);
462
463                for sub_mesh in &mesh.sub_meshes {
464                    let mat_idx = sub_mesh.material_index;
465                    if mat_idx < self.mesh_renderer.material_bind_groups.len() {
466                        render_pass.set_bind_group(
467                            2,
468                            &self.mesh_renderer.material_bind_groups[mat_idx],
469                            &[],
470                        );
471                    }
472                    render_pass.draw_indexed(
473                        sub_mesh.index_offset..sub_mesh.index_offset + sub_mesh.index_count,
474                        0,
475                        0..1,
476                    );
477                }
478            }
479        }
480
481        for child in &node.children {
482            self.render_node(render_pass, scene, device, child, world_transform);
483        }
484    }
485}