Skip to main content

polyscope_render/engine/
rendering.rs

1use super::RenderEngine;
2use crate::ground_plane::GroundPlaneRenderData;
3use crate::slice_plane_render::SlicePlaneRenderData;
4
5const DEFAULT_MATERIAL: &str = "clay";
6
7impl RenderEngine {
8    /// Returns the matcap bind group for the default material ("clay").
9    pub fn default_matcap_bind_group(&self) -> &wgpu::BindGroup {
10        &self.matcap_textures[DEFAULT_MATERIAL].bind_group
11    }
12
13    /// Returns the matcap bind group for a given material name.
14    /// Falls back to the default material if the name is not found.
15    pub fn matcap_bind_group_for(&self, material_name: &str) -> &wgpu::BindGroup {
16        &self
17            .matcap_textures
18            .get(material_name)
19            .unwrap_or(&self.matcap_textures[DEFAULT_MATERIAL])
20            .bind_group
21    }
22    /// Renders the ground plane.
23    ///
24    /// # Arguments
25    /// * `encoder` - The command encoder
26    /// * `view` - The render target view
27    /// * `enabled` - Whether the ground plane is enabled
28    /// * `scene_center` - Center of the scene bounding box
29    /// * `scene_min_y` - Minimum Y coordinate of scene bounding box
30    /// * `length_scale` - Scene length scale
31    /// * `height_override` - Optional manual height (None = auto below scene)
32    /// * `shadow_darkness` - Shadow darkness (0.0 = no shadow, 1.0 = full black)
33    /// * `shadow_mode` - Shadow mode: 0=none, `1=shadow_only`, `2=tile_with_shadow`
34    /// * `reflection_intensity` - Reflection intensity (0.0 = opaque, affects transparency)
35    #[allow(clippy::too_many_arguments)]
36    pub fn render_ground_plane(
37        &mut self,
38        encoder: &mut wgpu::CommandEncoder,
39        surface_view: &wgpu::TextureView,
40        enabled: bool,
41        scene_center: [f32; 3],
42        scene_min_y: f32,
43        length_scale: f32,
44        height_override: Option<f32>,
45        shadow_darkness: f32,
46        shadow_mode: u32,
47        reflection_intensity: f32,
48    ) {
49        // Check if camera is in orthographic mode
50        let is_orthographic =
51            self.camera.projection_mode == crate::camera::ProjectionMode::Orthographic;
52        if !enabled {
53            return;
54        }
55
56        // Always use HDR texture for ground plane rendering (pipelines use HDR format)
57        let view = self.hdr_view.as_ref().unwrap_or(surface_view);
58
59        // Initialize render data if needed
60        if self.ground_plane_render_data.is_none() {
61            if let Some(ref shadow_pass) = self.shadow_map_pass {
62                self.ground_plane_render_data = Some(GroundPlaneRenderData::new(
63                    &self.device,
64                    &self.ground_plane_bind_group_layout,
65                    &self.camera_buffer,
66                    shadow_pass.light_buffer(),
67                    shadow_pass.depth_view(),
68                    shadow_pass.comparison_sampler(),
69                ));
70            }
71        }
72
73        // Get camera height
74        let camera_height = self.camera.position.y;
75
76        if let Some(render_data) = &self.ground_plane_render_data {
77            render_data.update(
78                &self.queue,
79                scene_center,
80                scene_min_y,
81                length_scale,
82                camera_height,
83                height_override,
84                shadow_darkness,
85                shadow_mode,
86                is_orthographic,
87                reflection_intensity,
88            );
89
90            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
91                label: Some("Ground Plane Pass"),
92                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
93                    view,
94                    resolve_target: None,
95                    ops: wgpu::Operations {
96                        load: wgpu::LoadOp::Load, // Preserve existing content
97                        store: wgpu::StoreOp::Store,
98                    },
99                    depth_slice: None,
100                })],
101                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
102                    view: &self.depth_view,
103                    depth_ops: Some(wgpu::Operations {
104                        load: wgpu::LoadOp::Load,
105                        store: wgpu::StoreOp::Store,
106                    }),
107                    stencil_ops: None,
108                }),
109                ..Default::default()
110            });
111
112            render_pass.set_pipeline(&self.ground_plane_pipeline);
113            render_pass.set_bind_group(0, render_data.bind_group(), &[]);
114            // 4 triangles * 3 vertices = 12 vertices for infinite plane
115            render_pass.draw(0..12, 0..1);
116        }
117    }
118
119    /// Renders slice plane visualizations.
120    ///
121    /// Renders enabled slice planes as semi-transparent grids.
122    /// Should be called after rendering structures, before tone mapping.
123    pub fn render_slice_planes(
124        &mut self,
125        encoder: &mut wgpu::CommandEncoder,
126        planes: &[polyscope_core::slice_plane::SlicePlane],
127        length_scale: f32,
128    ) {
129        // Use HDR texture if available
130        let Some(view) = &self.hdr_view else {
131            return;
132        };
133
134        // Ensure we have enough render data for all planes
135        while self.slice_plane_render_data.len() < planes.len() {
136            let data = SlicePlaneRenderData::new(
137                &self.device,
138                &self.slice_plane_vis_bind_group_layout,
139                &self.camera_buffer,
140            );
141            self.slice_plane_render_data.push(data);
142        }
143
144        // Render each enabled plane that should be drawn
145        for (i, plane) in planes.iter().enumerate() {
146            if !plane.is_enabled() || !plane.draw_plane() {
147                continue;
148            }
149
150            // Update uniforms for this plane
151            self.slice_plane_render_data[i].update(&self.queue, plane, length_scale);
152
153            // Begin render pass for this plane
154            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
155                label: Some("Slice Plane Visualization Pass"),
156                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
157                    view,
158                    resolve_target: None,
159                    ops: wgpu::Operations {
160                        load: wgpu::LoadOp::Load, // Preserve existing content
161                        store: wgpu::StoreOp::Store,
162                    },
163                    depth_slice: None,
164                })],
165                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
166                    view: &self.depth_view,
167                    depth_ops: Some(wgpu::Operations {
168                        load: wgpu::LoadOp::Load,
169                        store: wgpu::StoreOp::Store,
170                    }),
171                    stencil_ops: None,
172                }),
173                ..Default::default()
174            });
175
176            render_pass.set_pipeline(&self.slice_plane_vis_pipeline);
177            self.slice_plane_render_data[i].draw(&mut render_pass);
178        }
179    }
180
181    /// Renders slice plane visualizations with clearing.
182    ///
183    /// Clears the HDR texture and depth buffer first, then renders slice planes.
184    /// This should be called BEFORE rendering scene geometry so that geometry
185    /// can properly occlude the slice planes.
186    pub fn render_slice_planes_with_clear(
187        &mut self,
188        encoder: &mut wgpu::CommandEncoder,
189        planes: &[polyscope_core::slice_plane::SlicePlane],
190        length_scale: f32,
191        clear_color: [f32; 3],
192    ) {
193        // Use HDR texture if available
194        let Some(view) = &self.hdr_view else {
195            return;
196        };
197
198        // Ensure we have enough render data for all planes
199        while self.slice_plane_render_data.len() < planes.len() {
200            let data = SlicePlaneRenderData::new(
201                &self.device,
202                &self.slice_plane_vis_bind_group_layout,
203                &self.camera_buffer,
204            );
205            self.slice_plane_render_data.push(data);
206        }
207
208        // Check if any planes need to be rendered
209        let has_visible_planes = planes.iter().any(|p| p.is_enabled() && p.draw_plane());
210
211        // First pass: clear the buffers
212        {
213            let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
214                label: Some("Slice Plane Clear Pass"),
215                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
216                    view,
217                    resolve_target: None,
218                    ops: wgpu::Operations {
219                        load: wgpu::LoadOp::Clear(wgpu::Color {
220                            r: f64::from(clear_color[0]),
221                            g: f64::from(clear_color[1]),
222                            b: f64::from(clear_color[2]),
223                            a: 1.0,
224                        }),
225                        store: wgpu::StoreOp::Store,
226                    },
227                    depth_slice: None,
228                })],
229                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
230                    view: &self.depth_view,
231                    depth_ops: Some(wgpu::Operations {
232                        load: wgpu::LoadOp::Clear(1.0),
233                        store: wgpu::StoreOp::Store,
234                    }),
235                    stencil_ops: None,
236                }),
237                ..Default::default()
238            });
239            // Pass ends here, clearing is done
240        }
241
242        // Only render planes if there are visible ones
243        if !has_visible_planes {
244            return;
245        }
246
247        // Render each enabled plane that should be drawn
248        for (i, plane) in planes.iter().enumerate() {
249            if !plane.is_enabled() || !plane.draw_plane() {
250                continue;
251            }
252
253            // Update uniforms for this plane
254            self.slice_plane_render_data[i].update(&self.queue, plane, length_scale);
255
256            // Begin render pass for this plane (loads existing content)
257            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
258                label: Some("Slice Plane Visualization Pass"),
259                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
260                    view,
261                    resolve_target: None,
262                    ops: wgpu::Operations {
263                        load: wgpu::LoadOp::Load,
264                        store: wgpu::StoreOp::Store,
265                    },
266                    depth_slice: None,
267                })],
268                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
269                    view: &self.depth_view,
270                    depth_ops: Some(wgpu::Operations {
271                        load: wgpu::LoadOp::Load,
272                        store: wgpu::StoreOp::Store,
273                    }),
274                    stencil_ops: None,
275                }),
276                ..Default::default()
277            });
278
279            render_pass.set_pipeline(&self.slice_plane_vis_pipeline);
280            self.slice_plane_render_data[i].draw(&mut render_pass);
281        }
282    }
283
284    /// Renders the ground plane to the stencil buffer for reflection masking.
285    ///
286    /// This should be called before rendering reflected geometry.
287    /// The stencil buffer will have value 1 where the ground plane is visible.
288    #[allow(clippy::too_many_arguments)]
289    pub fn render_stencil_pass(
290        &mut self,
291        encoder: &mut wgpu::CommandEncoder,
292        color_view: &wgpu::TextureView,
293        ground_height: f32,
294        scene_center: [f32; 3],
295        length_scale: f32,
296    ) {
297        let Some(pipeline) = &self.ground_stencil_pipeline else {
298            return;
299        };
300
301        // Initialize render data if needed
302        if self.ground_plane_render_data.is_none() {
303            if let Some(ref shadow_pass) = self.shadow_map_pass {
304                self.ground_plane_render_data = Some(GroundPlaneRenderData::new(
305                    &self.device,
306                    &self.ground_plane_bind_group_layout,
307                    &self.camera_buffer,
308                    shadow_pass.light_buffer(),
309                    shadow_pass.depth_view(),
310                    shadow_pass.comparison_sampler(),
311                ));
312            }
313        }
314
315        let Some(render_data) = &self.ground_plane_render_data else {
316            return;
317        };
318
319        // Check if camera is in orthographic mode
320        let is_orthographic =
321            self.camera.projection_mode == crate::camera::ProjectionMode::Orthographic;
322        let camera_height = self.camera.position.y;
323
324        // Update ground uniforms for stencil pass
325        render_data.update(
326            &self.queue,
327            scene_center,
328            scene_center[1] - length_scale * 0.5, // scene_min_y estimate
329            length_scale,
330            camera_height,
331            Some(ground_height),
332            0.0, // shadow_darkness (unused in stencil)
333            0,   // shadow_mode (unused in stencil)
334            is_orthographic,
335            0.0, // reflection_intensity (unused in stencil)
336        );
337
338        let view = self.hdr_view.as_ref().unwrap_or(color_view);
339
340        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
341            label: Some("Stencil Pass"),
342            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
343                view,
344                resolve_target: None,
345                ops: wgpu::Operations {
346                    load: wgpu::LoadOp::Load, // Don't clear color
347                    store: wgpu::StoreOp::Store,
348                },
349                depth_slice: None,
350            })],
351            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
352                view: &self.depth_view,
353                depth_ops: Some(wgpu::Operations {
354                    load: wgpu::LoadOp::Load, // Keep existing depth
355                    store: wgpu::StoreOp::Store,
356                }),
357                stencil_ops: Some(wgpu::Operations {
358                    load: wgpu::LoadOp::Clear(0), // Clear stencil to 0
359                    store: wgpu::StoreOp::Store,
360                }),
361            }),
362            ..Default::default()
363        });
364
365        render_pass.set_pipeline(pipeline);
366        render_pass.set_bind_group(0, render_data.bind_group(), &[]);
367        render_pass.set_stencil_reference(1); // Write 1 to stencil
368        render_pass.draw(0..12, 0..1); // 4 triangles = 12 vertices
369    }
370
371    /// Initializes reflection pass resources.
372    pub(crate) fn init_reflection_pass(&mut self) {
373        self.reflection_pass = Some(crate::reflection_pass::ReflectionPass::new(&self.device));
374    }
375
376    /// Returns the reflection pass.
377    pub fn reflection_pass(&self) -> Option<&crate::reflection_pass::ReflectionPass> {
378        self.reflection_pass.as_ref()
379    }
380
381    /// Updates reflection uniforms.
382    pub fn update_reflection(
383        &self,
384        reflection_matrix: glam::Mat4,
385        intensity: f32,
386        ground_height: f32,
387    ) {
388        if let Some(reflection) = &self.reflection_pass {
389            reflection.update_uniforms(&self.queue, reflection_matrix, intensity, ground_height);
390        }
391    }
392
393    /// Creates a bind group for reflected mesh rendering.
394    pub fn create_reflected_mesh_bind_group(
395        &self,
396        mesh_render_data: &crate::surface_mesh_render::SurfaceMeshRenderData,
397    ) -> Option<wgpu::BindGroup> {
398        let layout = self.reflected_mesh_bind_group_layout.as_ref()?;
399
400        Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
401            label: Some("Reflected Mesh Bind Group"),
402            layout,
403            entries: &[
404                wgpu::BindGroupEntry {
405                    binding: 0,
406                    resource: self.camera_buffer.as_entire_binding(),
407                },
408                wgpu::BindGroupEntry {
409                    binding: 1,
410                    resource: mesh_render_data.uniform_buffer().as_entire_binding(),
411                },
412                wgpu::BindGroupEntry {
413                    binding: 2,
414                    resource: mesh_render_data.position_buffer().as_entire_binding(),
415                },
416                wgpu::BindGroupEntry {
417                    binding: 3,
418                    resource: mesh_render_data.normal_buffer().as_entire_binding(),
419                },
420                wgpu::BindGroupEntry {
421                    binding: 4,
422                    resource: mesh_render_data.barycentric_buffer().as_entire_binding(),
423                },
424                wgpu::BindGroupEntry {
425                    binding: 5,
426                    resource: mesh_render_data.color_buffer().as_entire_binding(),
427                },
428                wgpu::BindGroupEntry {
429                    binding: 6,
430                    resource: mesh_render_data.edge_is_real_buffer().as_entire_binding(),
431                },
432            ],
433        }))
434    }
435
436    /// Renders a single reflected mesh.
437    ///
438    /// Call this for each visible surface mesh after `render_stencil_pass`.
439    pub fn render_reflected_mesh(
440        &self,
441        render_pass: &mut wgpu::RenderPass,
442        mesh_bind_group: &wgpu::BindGroup,
443        vertex_count: u32,
444        material_name: &str,
445    ) {
446        let Some(pipeline) = &self.reflected_mesh_pipeline else {
447            return;
448        };
449        let Some(reflection) = &self.reflection_pass else {
450            return;
451        };
452
453        render_pass.set_pipeline(pipeline);
454        render_pass.set_bind_group(0, mesh_bind_group, &[]);
455        render_pass.set_bind_group(1, reflection.bind_group(), &[]);
456        render_pass.set_bind_group(2, self.matcap_bind_group_for(material_name), &[]);
457        render_pass.set_bind_group(3, &self.slice_plane_bind_group, &[]);
458        render_pass.set_stencil_reference(1); // Test against stencil value 1
459        render_pass.draw(0..vertex_count, 0..1);
460    }
461
462    /// Creates a bind group for reflected point cloud rendering.
463    pub fn create_reflected_point_cloud_bind_group(
464        &self,
465        point_render_data: &crate::point_cloud_render::PointCloudRenderData,
466    ) -> Option<wgpu::BindGroup> {
467        let layout = self.reflected_point_cloud_bind_group_layout.as_ref()?;
468
469        Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
470            label: Some("Reflected Point Cloud Bind Group"),
471            layout,
472            entries: &[
473                wgpu::BindGroupEntry {
474                    binding: 0,
475                    resource: self.camera_buffer.as_entire_binding(),
476                },
477                wgpu::BindGroupEntry {
478                    binding: 1,
479                    resource: point_render_data.uniform_buffer.as_entire_binding(),
480                },
481                wgpu::BindGroupEntry {
482                    binding: 2,
483                    resource: point_render_data.position_buffer.as_entire_binding(),
484                },
485                wgpu::BindGroupEntry {
486                    binding: 3,
487                    resource: point_render_data.color_buffer.as_entire_binding(),
488                },
489            ],
490        }))
491    }
492
493    /// Renders a single reflected point cloud.
494    pub fn render_reflected_point_cloud(
495        &self,
496        render_pass: &mut wgpu::RenderPass,
497        point_bind_group: &wgpu::BindGroup,
498        point_count: u32,
499        material_name: &str,
500    ) {
501        let Some(pipeline) = &self.reflected_point_cloud_pipeline else {
502            return;
503        };
504        let Some(reflection) = &self.reflection_pass else {
505            return;
506        };
507
508        render_pass.set_pipeline(pipeline);
509        render_pass.set_bind_group(0, point_bind_group, &[]);
510        render_pass.set_bind_group(1, reflection.bind_group(), &[]);
511        render_pass.set_bind_group(2, self.matcap_bind_group_for(material_name), &[]);
512        render_pass.set_bind_group(3, &self.slice_plane_bind_group, &[]);
513        render_pass.set_stencil_reference(1);
514        // 6 vertices per point (billboard quad as 2 triangles)
515        render_pass.draw(0..6, 0..point_count);
516    }
517
518    /// Creates a bind group for reflected curve network rendering.
519    pub fn create_reflected_curve_network_bind_group(
520        &self,
521        curve_render_data: &crate::curve_network_render::CurveNetworkRenderData,
522    ) -> Option<wgpu::BindGroup> {
523        let layout = self.reflected_curve_network_bind_group_layout.as_ref()?;
524
525        Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
526            label: Some("Reflected Curve Network Bind Group"),
527            layout,
528            entries: &[
529                wgpu::BindGroupEntry {
530                    binding: 0,
531                    resource: self.camera_buffer.as_entire_binding(),
532                },
533                wgpu::BindGroupEntry {
534                    binding: 1,
535                    resource: curve_render_data.uniform_buffer.as_entire_binding(),
536                },
537                wgpu::BindGroupEntry {
538                    binding: 2,
539                    resource: curve_render_data.edge_vertex_buffer.as_entire_binding(),
540                },
541                wgpu::BindGroupEntry {
542                    binding: 3,
543                    resource: curve_render_data.edge_color_buffer.as_entire_binding(),
544                },
545            ],
546        }))
547    }
548
549    /// Renders a single reflected curve network (tube mode).
550    pub fn render_reflected_curve_network(
551        &self,
552        render_pass: &mut wgpu::RenderPass,
553        curve_bind_group: &wgpu::BindGroup,
554        curve_render_data: &crate::curve_network_render::CurveNetworkRenderData,
555        material_name: &str,
556    ) {
557        let Some(pipeline) = &self.reflected_curve_network_pipeline else {
558            return;
559        };
560        let Some(reflection) = &self.reflection_pass else {
561            return;
562        };
563        let Some(tube_vertex_buffer) = &curve_render_data.generated_vertex_buffer else {
564            return;
565        };
566
567        // 36 vertices per edge (tube geometry)
568        let tube_vertex_count = curve_render_data.num_edges * 36;
569
570        render_pass.set_pipeline(pipeline);
571        render_pass.set_bind_group(0, curve_bind_group, &[]);
572        render_pass.set_bind_group(1, reflection.bind_group(), &[]);
573        render_pass.set_bind_group(2, self.matcap_bind_group_for(material_name), &[]);
574        render_pass.set_bind_group(3, &self.slice_plane_bind_group, &[]);
575        render_pass.set_vertex_buffer(0, tube_vertex_buffer.slice(..));
576        render_pass.set_stencil_reference(1);
577        render_pass.draw(0..tube_vertex_count, 0..1);
578    }
579}