Skip to main content

engvis_renderer/
renderer.rs

1use wgpu::util::DeviceExt;
2use engvis_core::{OrbitCamera, Scene, RenderState};
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;
12use crate::custom_material::CustomMaterial;
13
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
46    /// Internal render state. Set via `set_state()` or `set_state_from_camera()`.
47    state: RenderState,
48    /// MSAA sample count.
49    sample_count: u32,
50    /// Optional custom material override.
51    custom_material: Option<Box<dyn CustomMaterial>>,
52}
53
54impl Renderer {
55    pub fn new(
56        device: &wgpu::Device,
57        queue: &wgpu::Queue,
58        surface_format: wgpu::TextureFormat,
59        scene: &Scene,
60        width: u32,
61        height: u32,
62        sample_count: u32,
63    ) -> Self {
64        let sc = sample_count.max(1);
65        let depth = DepthTexture::new(device, width.max(1), height.max(1), sc);
66
67        let msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
68            label: Some("MSAA Color Texture"),
69            size: wgpu::Extent3d {
70                width: width.max(1),
71                height: height.max(1),
72                depth_or_array_layers: 1,
73            },
74            mip_level_count: 1,
75            sample_count: sc,
76            dimension: wgpu::TextureDimension::D2,
77            format: surface_format,
78            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
79            view_formats: &[],
80        });
81        let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
82
83        let scene_bind_group_layout =
84            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
85                label: Some("Scene Bind Group Layout"),
86                entries: &[wgpu::BindGroupLayoutEntry {
87                    binding: 0,
88                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
89                    ty: wgpu::BindingType::Buffer {
90                        ty: wgpu::BufferBindingType::Uniform,
91                        has_dynamic_offset: false,
92                        min_binding_size: None,
93                    },
94                    count: None,
95                }],
96            });
97
98        let initial_uniforms = SceneUniforms {
99            view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
100            camera_pos: [0.0; 4],
101            viewport: [width as f32, height as f32, 0.0, 0.0],
102            global_opacity: [1.0, 0.0, 0.0, 0.0],
103        };
104
105        let scene_uniform_buffer =
106            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
107                label: Some("Scene Uniform Buffer"),
108                contents: bytemuck::cast_slice(&[initial_uniforms]),
109                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
110            });
111
112        let scene_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
113            label: Some("Scene Bind Group"),
114            layout: &scene_bind_group_layout,
115            entries: &[wgpu::BindGroupEntry {
116                binding: 0,
117                resource: scene_uniform_buffer.as_entire_binding(),
118            }],
119        });
120
121        let lighting = LightingBuffer::new(device, &scene.lighting);
122
123        let scene_layout_for_grid = scene_bind_group_layout.clone();
124        let grid_renderer = GridRenderer::new(device, surface_format, &scene_layout_for_grid, sample_count);
125
126        let mesh_renderer = MeshRenderer::new(device);
127        let material_pipeline = MaterialPipeline::new(
128            device,
129            surface_format,
130            &scene_bind_group_layout,
131            &lighting.bind_group_layout,
132            &mesh_renderer.object_bind_group_layout,
133            sample_count,
134        );
135
136        let scene_layout_for_overlay = scene_bind_group_layout.clone();
137        let overlay_renderer = OverlayRenderer::new(
138            device,
139            surface_format,
140            crate::depth::DepthTexture::FORMAT,
141            &scene_layout_for_overlay,
142            &mesh_renderer.object_bind_group_layout,
143            sample_count,
144        );
145
146        let texture_cache = TextureCache::new(device, queue);
147
148        let mut renderer = Self {
149            depth,
150            msaa_texture,
151            msaa_view,
152            surface_format,
153            scene_uniform_buffer,
154            scene_bind_group,
155            scene_bind_group_layout,
156            lighting,
157            material_pipeline,
158            mesh_renderer,
159            grid_renderer,
160            overlay_renderer,
161            texture_cache,
162            state: RenderState::default(),
163            sample_count: sc,
164            custom_material: None,
165        };
166
167        renderer.upload_scene(device, queue, scene);
168        renderer
169    }
170
171    // ── State management ─────────────────────────────────────
172
173    /// Replace the entire render state at once.
174    pub fn set_state(&mut self, state: &RenderState) {
175        self.state = *state;
176    }
177
178    /// Update render state from camera clipping planes (deprecated — clipping planes
179    /// are now managed directly via `FrameCtx::set_clip_planes()`).
180    pub fn set_state_from_camera(&mut self, _camera: &OrbitCamera) {}
181
182    /// Read-only access to current state.
183    pub fn state(&self) -> &RenderState {
184        &self.state
185    }
186
187    /// Mutable access to current state (for per-field changes).
188    pub fn state_mut(&mut self) -> &mut RenderState {
189        &mut self.state
190    }
191
192    /// Set a custom material override. Pass `None` to clear.
193    pub fn set_custom_material(&mut self, material: Option<Box<dyn CustomMaterial>>) {
194        self.custom_material = material;
195    }
196
197    /// Check if a custom material is active.
198    pub fn has_custom_material(&self) -> bool {
199        self.custom_material.is_some()
200    }
201
202    // ── Upload and resize ───────────────────────────────────
203
204    pub fn upload_scene(
205        &mut self,
206        device: &wgpu::Device,
207        queue: &wgpu::Queue,
208        scene: &Scene,
209    ) {
210        // Re-upload meshes
211        self.mesh_renderer.mesh_buffers.clear();
212        for mesh in &scene.meshes {
213            self.mesh_renderer.upload_mesh(device, mesh);
214        }
215
216        // Create material bind groups
217        self.mesh_renderer.material_bind_groups.clear();
218        self.mesh_renderer.material_uniform_buffers.clear();
219        for material in &scene.materials {
220            let (bg, buf) = self
221                .material_pipeline
222                .create_material_bind_group(device, material, &self.texture_cache);
223            self.mesh_renderer.material_bind_groups.push(bg);
224            self.mesh_renderer.material_uniform_buffers.push(buf);
225        }
226
227        // Update lighting
228        self.lighting.update(queue, &scene.lighting);
229    }
230
231    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
232        let w = width.max(1);
233        let h = height.max(1);
234        let sc = self.sample_count.max(1);
235        self.depth = DepthTexture::new(device, w, h, sc);
236        self.msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
237            label: Some("MSAA Color Texture"),
238            size: wgpu::Extent3d {
239                width: w,
240                height: h,
241                depth_or_array_layers: 1,
242            },
243            mip_level_count: 1,
244            sample_count: sc,
245            dimension: wgpu::TextureDimension::D2,
246            format: self.surface_format,
247            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
248            view_formats: &[],
249        });
250        self.msaa_view = self.msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
251    }
252
253    // ── Render pass ─────────────────────────────────────────
254
255   #[allow(clippy::too_many_arguments)]
256   pub fn render_scene_pass(
257       &self,
258       device: &wgpu::Device,
259       queue: &wgpu::Queue,
260       view: &wgpu::TextureView,
261       encoder: &mut wgpu::CommandEncoder,
262       scene: &Scene,
263       camera: &OrbitCamera,
264       width: u32,
265       height: u32,
266   ) {
267        // Update uniforms
268       let scene_uniforms = SceneUniforms {
269           view_proj: camera.view_projection().to_cols_array_2d(),
270           camera_pos: [camera.position().x, camera.position().y, camera.position().z, 1.0],
271           viewport: [width as f32, height as f32, 0.0, 0.0],
272           global_opacity: [self.state.opacity, 0.0, 0.0, 0.0],
273       };
274        queue.write_buffer(
275            &self.scene_uniform_buffer,
276            0,
277            bytemuck::cast_slice(&[scene_uniforms]),
278        );
279
280        // Update lighting
281        self.lighting.update(queue, &scene.lighting);
282
283        // Update depth texture sizes if camera planes changed
284        let s = &self.state;
285        let depth_tex = &self.depth;
286
287        // Begin render pass with shared MSAA target (or direct to view if no MSAA)
288        let resolve_target = if self.sample_count > 1 { Some(view) } else { None };
289        let color_view = if self.sample_count > 1 { &self.msaa_view } else { view };
290        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
291            label: Some("Main Render Pass"),
292            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
293                view: color_view,
294                resolve_target,
295                ops: wgpu::Operations {
296                    load: wgpu::LoadOp::Clear(wgpu::Color {
297                        r: 0.15,
298                        g: 0.17,
299                        b: 0.19,
300                        a: 1.0,
301                    }),
302                    store: wgpu::StoreOp::Store,
303                },
304                depth_slice: None,
305            })],
306            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
307                view: &depth_tex.view,
308                depth_ops: Some(wgpu::Operations {
309                    load: wgpu::LoadOp::Clear(1.0),
310                    store: wgpu::StoreOp::Store,
311                }),
312                stencil_ops: None,
313            }),
314            occlusion_query_set: None,
315            timestamp_writes: None,
316        });
317
318       render_pass.set_bind_group(0, &self.scene_bind_group, &[]);
319       render_pass.set_bind_group(1, &self.lighting.bind_group, &[]);
320
321      // --- Grid (opaque, drawn first so transparent surface can show through) ---
322      if s.show_grid {
323          self.grid_renderer.render(&mut render_pass);
324          // Re-bind scene + lighting bind groups after grid renderer
325          render_pass.set_bind_group(0, &self.scene_bind_group, &[]);
326          render_pass.set_bind_group(1, &self.lighting.bind_group, &[]);
327      }
328
329       // --- Surface rendering ---
330       if s.show_surface {
331           render_pass.set_pipeline(&self.material_pipeline.solid_pipeline);
332           self.render_scene_nodes(&mut render_pass, scene, device, Affine3A::IDENTITY);
333       }
334
335       // --- Edge overlay ---
336      if s.edge_opts.enabled {
337          let (_buf, overlay_bg) = self
338              .overlay_renderer
339              .create_uniform_bind_group(device, s.edge_opts.color, 0.0, s.edge_opts.line_width);
340          render_pass.set_pipeline(&self.overlay_renderer.line_pipeline);
341          self.render_overlay_nodes(&mut render_pass, scene, device, Affine3A::IDENTITY, &overlay_bg, OverlayDrawMode::Edges);
342      }
343
344        // --- Vertex overlay ---
345        if s.vertex_opts.enabled {
346            let (_buf, overlay_bg) = self
347                .overlay_renderer
348                .create_uniform_bind_group(device, s.vertex_opts.color, s.vertex_opts.point_size, 0.0);
349            render_pass.set_pipeline(&self.overlay_renderer.point_pipeline);
350            self.render_overlay_nodes(&mut render_pass, scene, device, Affine3A::IDENTITY, &overlay_bg, OverlayDrawMode::Vertices);
351        }
352    }
353
354    fn render_scene_nodes(
355        &self,
356        render_pass: &mut wgpu::RenderPass,
357        scene: &Scene,
358        device: &wgpu::Device,
359        parent_transform: Affine3A,
360    ) {
361        for node in &scene.nodes {
362            self.render_node(render_pass, scene, device, node, parent_transform);
363        }
364    }
365
366    fn render_node(
367        &self,
368        render_pass: &mut wgpu::RenderPass,
369        scene: &Scene,
370        device: &wgpu::Device,
371        node: &engvis_core::SceneNode,
372        parent_transform: Affine3A,
373    ) {
374        if !node.visible {
375            return;
376        }
377
378        let world_transform = parent_transform * node.local_transform;
379
380        if let Some(mesh_idx) = node.mesh_index {
381            if mesh_idx < self.mesh_renderer.mesh_buffers.len() {
382                let mesh_buf = &self.mesh_renderer.mesh_buffers[mesh_idx];
383                let mesh = &scene.meshes[mesh_idx];
384
385                render_pass.set_vertex_buffer(0, mesh_buf.vertex_buffer.slice(..));
386                render_pass.set_index_buffer(
387                    mesh_buf.index_buffer.slice(..),
388                    wgpu::IndexFormat::Uint32,
389                );
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(3, &obj_bg, &[]);
395
396                for sub_mesh in &mesh.sub_meshes {
397                    let mat_idx = sub_mesh.material_index;
398                    if mat_idx < self.mesh_renderer.material_bind_groups.len() {
399                        render_pass.set_bind_group(
400                            2,
401                            &self.mesh_renderer.material_bind_groups[mat_idx],
402                            &[],
403                        );
404                    }
405                    render_pass.draw_indexed(
406                        sub_mesh.index_offset..sub_mesh.index_offset + sub_mesh.index_count,
407                        0,
408                        0..1,
409                    );
410                }
411            }
412        }
413
414        for child in &node.children {
415            self.render_node(render_pass, scene, device, child, world_transform);
416        }
417    }
418
419    fn render_overlay_nodes(
420        &self,
421        render_pass: &mut wgpu::RenderPass,
422        scene: &Scene,
423        device: &wgpu::Device,
424        parent_transform: Affine3A,
425        overlay_bind_group: &wgpu::BindGroup,
426        mode: OverlayDrawMode,
427    ) {
428       for node in &scene.nodes {
429           self.render_overlay_node(render_pass, device, node, parent_transform, overlay_bind_group, mode);
430       }
431    }
432
433   fn render_overlay_node(
434       &self,
435       render_pass: &mut wgpu::RenderPass,
436       device: &wgpu::Device,
437        node: &engvis_core::SceneNode,
438        parent_transform: Affine3A,
439        overlay_bind_group: &wgpu::BindGroup,
440        mode: OverlayDrawMode,
441    ) {
442        if !node.visible {
443            return;
444        }
445
446        let world_transform = parent_transform * node.local_transform;
447
448        if let Some(mesh_idx) = node.mesh_index {
449            if mesh_idx < self.mesh_renderer.mesh_buffers.len() {
450                let mesh_buf = &self.mesh_renderer.mesh_buffers[mesh_idx];
451
452                render_pass.set_bind_group(0, &self.scene_bind_group, &[]);
453
454                let (_obj_buf, obj_bg) = self
455                    .mesh_renderer
456                    .create_object_bind_group(device, world_transform);
457                render_pass.set_bind_group(1, &obj_bg, &[]);
458                render_pass.set_bind_group(2, overlay_bind_group, &[]);
459
460                match mode {
461                    OverlayDrawMode::Vertices => {
462                        render_pass.set_vertex_buffer(0, mesh_buf.vertex_buffer.slice(..));
463                        render_pass.set_vertex_buffer(1, self.overlay_renderer.point_quad_buffer.slice(..));
464                        render_pass.draw(0..6, 0..mesh_buf.vertex_count);
465                    }
466                    OverlayDrawMode::Edges => {
467                        render_pass.set_vertex_buffer(0, mesh_buf.edge_endpoint_buffer.slice(..));
468                        render_pass.set_vertex_buffer(1, self.overlay_renderer.line_quad_buffer.slice(..));
469                        render_pass.draw(0..6, 0..mesh_buf.edge_instance_count);
470                    }
471                }
472            }
473        }
474
475       for child in &node.children {
476           self.render_overlay_node(render_pass, device, child, world_transform, overlay_bind_group, mode);
477       }
478    }
479}