Skip to main content

threecrate_gpu/
renderer.rs

1use crate::device::GpuContext;
2use bytemuck::{Pod, Zeroable};
3use nalgebra::{Matrix4, Vector3};
4use threecrate_core::{ColoredPoint3f, Error, Point3f, PointCloud, Result};
5use wgpu::util::DeviceExt;
6use winit::window::Window;
7
8/// Vertex data for point cloud rendering with normals
9#[repr(C)]
10#[derive(Copy, Clone, Pod, Zeroable)]
11pub struct PointVertex {
12    pub position: [f32; 3],
13    pub color: [f32; 3],
14    pub size: f32,
15    pub normal: [f32; 3],
16}
17
18impl PointVertex {
19    /// Create vertex from Point3f with default color, size, and normal
20    pub fn from_point(point: &Point3f, color: [f32; 3], size: f32, normal: [f32; 3]) -> Self {
21        Self {
22            position: [point.x, point.y, point.z],
23            color,
24            size,
25            normal,
26        }
27    }
28
29    /// Create vertex from ColoredPoint3f with default size and normal
30    pub fn from_colored_point(point: &ColoredPoint3f, size: f32, normal: [f32; 3]) -> Self {
31        Self {
32            position: [point.position.x, point.position.y, point.position.z],
33            color: [
34                point.color[0] as f32 / 255.0,
35                point.color[1] as f32 / 255.0,
36                point.color[2] as f32 / 255.0,
37            ],
38            size,
39            normal,
40        }
41    }
42
43    /// Vertex buffer layout descriptor
44    pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
45        wgpu::VertexBufferLayout {
46            array_stride: std::mem::size_of::<PointVertex>() as wgpu::BufferAddress,
47            step_mode: wgpu::VertexStepMode::Vertex,
48            attributes: &[
49                // Position
50                wgpu::VertexAttribute {
51                    offset: 0,
52                    shader_location: 0,
53                    format: wgpu::VertexFormat::Float32x3,
54                },
55                // Color
56                wgpu::VertexAttribute {
57                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
58                    shader_location: 1,
59                    format: wgpu::VertexFormat::Float32x3,
60                },
61                // Size
62                wgpu::VertexAttribute {
63                    offset: std::mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
64                    shader_location: 2,
65                    format: wgpu::VertexFormat::Float32,
66                },
67                // Normal
68                wgpu::VertexAttribute {
69                    offset: std::mem::size_of::<[f32; 7]>() as wgpu::BufferAddress,
70                    shader_location: 3,
71                    format: wgpu::VertexFormat::Float32x3,
72                },
73            ],
74        }
75    }
76}
77
78/// Camera uniform data
79#[repr(C)]
80#[derive(Copy, Clone, Pod, Zeroable)]
81pub struct CameraUniform {
82    pub view_proj: [[f32; 4]; 4],
83    pub view_pos: [f32; 3],
84    pub _padding: f32,
85}
86
87/// Render parameters for point cloud splatting
88#[repr(C)]
89#[derive(Copy, Clone, Debug, Pod, Zeroable)]
90pub struct RenderParams {
91    pub point_size: f32,
92    pub alpha_threshold: f32,
93    pub enable_splatting: f32,
94    pub enable_lighting: f32,
95    pub ambient_strength: f32,
96    pub diffuse_strength: f32,
97    pub specular_strength: f32,
98    pub shininess: f32,
99}
100
101impl Default for RenderParams {
102    fn default() -> Self {
103        Self {
104            point_size: 4.0,
105            alpha_threshold: 0.1,
106            enable_splatting: 1.0,
107            enable_lighting: 1.0,
108            ambient_strength: 0.3,
109            diffuse_strength: 0.7,
110            specular_strength: 0.5,
111            shininess: 32.0,
112        }
113    }
114}
115
116/// Rendering configuration
117#[derive(Debug, Clone)]
118pub struct RenderConfig {
119    pub render_params: RenderParams,
120    pub background_color: [f64; 4],
121    pub enable_depth_test: bool,
122    pub enable_alpha_blending: bool,
123    pub enable_multisampling: bool,
124}
125
126impl Default for RenderConfig {
127    fn default() -> Self {
128        Self {
129            render_params: RenderParams::default(),
130            background_color: [0.1, 0.1, 0.1, 1.0],
131            enable_depth_test: true,
132            enable_alpha_blending: true,
133            enable_multisampling: true,
134        }
135    }
136}
137
138/// GPU-accelerated point cloud renderer with splatting support
139pub struct PointCloudRenderer<'window> {
140    pub gpu_context: GpuContext,
141    pub surface: wgpu::Surface<'window>,
142    pub surface_config: wgpu::SurfaceConfiguration,
143    pub render_pipeline: wgpu::RenderPipeline,
144    pub camera_uniform: CameraUniform,
145    pub camera_buffer: wgpu::Buffer,
146    pub render_params: RenderParams,
147    pub render_params_buffer: wgpu::Buffer,
148    pub bind_group: wgpu::BindGroup,
149    pub bind_group_layout: wgpu::BindGroupLayout,
150    pub config: RenderConfig,
151    pub msaa_texture: Option<wgpu::Texture>,
152    pub msaa_view: Option<wgpu::TextureView>,
153}
154
155impl<'window> PointCloudRenderer<'window> {
156    /// Create new point cloud renderer with splatting support
157    pub async fn new(window: &'window Window, config: RenderConfig) -> Result<Self> {
158        let gpu_context = GpuContext::new().await?;
159
160        let surface = gpu_context
161            .instance
162            .create_surface(window)
163            .map_err(|e| Error::Gpu(format!("Failed to create surface: {:?}", e)))?;
164
165        let surface_caps = surface.get_capabilities(&gpu_context.adapter);
166        let surface_format = surface_caps
167            .formats
168            .iter()
169            .copied()
170            .find(|f| f.is_srgb())
171            .unwrap_or(surface_caps.formats[0]);
172
173        let size = window.inner_size();
174        let sample_count = if config.enable_multisampling { 4 } else { 1 };
175
176        let surface_config = wgpu::SurfaceConfiguration {
177            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
178            format: surface_format,
179            width: size.width,
180            height: size.height,
181            present_mode: surface_caps.present_modes[0],
182            alpha_mode: surface_caps.alpha_modes[0],
183            view_formats: vec![],
184            desired_maximum_frame_latency: 2,
185        };
186        surface.configure(&gpu_context.device, &surface_config);
187
188        // Create MSAA texture if enabled
189        let (msaa_texture, msaa_view) = if config.enable_multisampling {
190            let msaa_texture = gpu_context.device.create_texture(&wgpu::TextureDescriptor {
191                label: Some("MSAA Texture"),
192                size: wgpu::Extent3d {
193                    width: size.width,
194                    height: size.height,
195                    depth_or_array_layers: 1,
196                },
197                mip_level_count: 1,
198                sample_count,
199                dimension: wgpu::TextureDimension::D2,
200                format: surface_format,
201                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
202                view_formats: &[],
203            });
204            let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
205            (Some(msaa_texture), Some(msaa_view))
206        } else {
207            (None, None)
208        };
209
210        // Create camera uniform
211        let camera_uniform = CameraUniform {
212            view_proj: Matrix4::identity().into(),
213            view_pos: [0.0, 0.0, 0.0],
214            _padding: 0.0,
215        };
216
217        let camera_buffer =
218            gpu_context
219                .device
220                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
221                    label: Some("Camera Buffer"),
222                    contents: bytemuck::bytes_of(&camera_uniform),
223                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
224                });
225
226        // Create render parameters buffer
227        let render_params = config.render_params;
228        let render_params_buffer =
229            gpu_context
230                .device
231                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
232                    label: Some("Render Params Buffer"),
233                    contents: bytemuck::bytes_of(&render_params),
234                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
235                });
236
237        // Create bind group layout
238        let bind_group_layout =
239            gpu_context
240                .device
241                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
242                    entries: &[
243                        wgpu::BindGroupLayoutEntry {
244                            binding: 0,
245                            visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
246                            ty: wgpu::BindingType::Buffer {
247                                ty: wgpu::BufferBindingType::Uniform,
248                                has_dynamic_offset: false,
249                                min_binding_size: None,
250                            },
251                            count: None,
252                        },
253                        wgpu::BindGroupLayoutEntry {
254                            binding: 1,
255                            visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
256                            ty: wgpu::BindingType::Buffer {
257                                ty: wgpu::BufferBindingType::Uniform,
258                                has_dynamic_offset: false,
259                                min_binding_size: None,
260                            },
261                            count: None,
262                        },
263                    ],
264                    label: Some("point_cloud_bind_group_layout"),
265                });
266
267        let bind_group = gpu_context
268            .device
269            .create_bind_group(&wgpu::BindGroupDescriptor {
270                layout: &bind_group_layout,
271                entries: &[
272                    wgpu::BindGroupEntry {
273                        binding: 0,
274                        resource: camera_buffer.as_entire_binding(),
275                    },
276                    wgpu::BindGroupEntry {
277                        binding: 1,
278                        resource: render_params_buffer.as_entire_binding(),
279                    },
280                ],
281                label: Some("point_cloud_bind_group"),
282            });
283
284        // Create render pipeline
285        let shader = gpu_context
286            .device
287            .create_shader_module(wgpu::ShaderModuleDescriptor {
288                label: Some("Point Cloud Shader"),
289                source: wgpu::ShaderSource::Wgsl(include_str!("shaders/point_cloud.wgsl").into()),
290            });
291
292        let render_pipeline_layout =
293            gpu_context
294                .device
295                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
296                    label: Some("Point Cloud Render Pipeline Layout"),
297                    bind_group_layouts: &[Some(&bind_group_layout)],
298                    immediate_size: 0,
299                });
300
301        let render_pipeline =
302            gpu_context
303                .device
304                .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
305                    label: Some("Point Cloud Render Pipeline"),
306                    layout: Some(&render_pipeline_layout),
307                    vertex: wgpu::VertexState {
308                        module: &shader,
309                        entry_point: Some("vs_main"),
310                        buffers: &[PointVertex::desc()],
311                        compilation_options: wgpu::PipelineCompilationOptions::default(),
312                    },
313                    fragment: Some(wgpu::FragmentState {
314                        module: &shader,
315                        entry_point: Some("fs_main"),
316                        targets: &[Some(wgpu::ColorTargetState {
317                            format: surface_config.format,
318                            blend: if config.enable_alpha_blending {
319                                Some(wgpu::BlendState::ALPHA_BLENDING)
320                            } else {
321                                Some(wgpu::BlendState::REPLACE)
322                            },
323                            write_mask: wgpu::ColorWrites::ALL,
324                        })],
325                        compilation_options: wgpu::PipelineCompilationOptions::default(),
326                    }),
327                    primitive: wgpu::PrimitiveState {
328                        topology: wgpu::PrimitiveTopology::TriangleList,
329                        strip_index_format: None,
330                        front_face: wgpu::FrontFace::Ccw,
331                        cull_mode: None,
332                        unclipped_depth: false,
333                        polygon_mode: wgpu::PolygonMode::Fill,
334                        conservative: false,
335                    },
336                    depth_stencil: if config.enable_depth_test {
337                        Some(wgpu::DepthStencilState {
338                            format: wgpu::TextureFormat::Depth32Float,
339                            depth_write_enabled: Some(true),
340                            depth_compare: Some(wgpu::CompareFunction::Less),
341                            stencil: wgpu::StencilState::default(),
342                            bias: wgpu::DepthBiasState::default(),
343                        })
344                    } else {
345                        None
346                    },
347                    multisample: wgpu::MultisampleState {
348                        count: sample_count,
349                        mask: !0,
350                        alpha_to_coverage_enabled: false,
351                    },
352                    multiview_mask: None,
353                    cache: None,
354                });
355
356        Ok(Self {
357            gpu_context,
358            surface,
359            surface_config,
360            render_pipeline,
361            camera_uniform,
362            camera_buffer,
363            render_params,
364            render_params_buffer,
365            bind_group,
366            bind_group_layout,
367            config,
368            msaa_texture,
369            msaa_view,
370        })
371    }
372
373    /// Update camera matrices and position
374    pub fn update_camera(
375        &mut self,
376        view_matrix: Matrix4<f32>,
377        proj_matrix: Matrix4<f32>,
378        camera_pos: Vector3<f32>,
379    ) {
380        self.camera_uniform.view_proj = (proj_matrix * view_matrix).into();
381        self.camera_uniform.view_pos = camera_pos.into();
382
383        self.gpu_context.queue.write_buffer(
384            &self.camera_buffer,
385            0,
386            bytemuck::bytes_of(&self.camera_uniform),
387        );
388    }
389
390    /// Update render parameters
391    pub fn update_render_params(&mut self, params: RenderParams) {
392        self.render_params = params;
393        self.gpu_context.queue.write_buffer(
394            &self.render_params_buffer,
395            0,
396            bytemuck::bytes_of(&self.render_params),
397        );
398    }
399
400    /// Resize renderer
401    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
402        self.surface_config.width = new_size.width;
403        self.surface_config.height = new_size.height;
404        self.surface
405            .configure(&self.gpu_context.device, &self.surface_config);
406
407        // Recreate MSAA texture if needed
408        if self.config.enable_multisampling {
409            let msaa_texture = self
410                .gpu_context
411                .device
412                .create_texture(&wgpu::TextureDescriptor {
413                    label: Some("MSAA Texture"),
414                    size: wgpu::Extent3d {
415                        width: new_size.width,
416                        height: new_size.height,
417                        depth_or_array_layers: 1,
418                    },
419                    mip_level_count: 1,
420                    sample_count: 4,
421                    dimension: wgpu::TextureDimension::D2,
422                    format: self.surface_config.format,
423                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
424                    view_formats: &[],
425                });
426            let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
427            self.msaa_texture = Some(msaa_texture);
428            self.msaa_view = Some(msaa_view);
429        }
430    }
431
432    /// Create vertex buffer from point vertices
433    pub fn create_vertex_buffer(&self, vertices: &[PointVertex]) -> wgpu::Buffer {
434        self.gpu_context
435            .device
436            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
437                label: Some("Point Cloud Vertex Buffer"),
438                contents: bytemuck::cast_slice(vertices),
439                usage: wgpu::BufferUsages::VERTEX,
440            })
441    }
442
443    /// Create depth texture
444    pub fn create_depth_texture(&self) -> wgpu::Texture {
445        let sample_count = if self.config.enable_multisampling {
446            4
447        } else {
448            1
449        };
450
451        self.gpu_context
452            .device
453            .create_texture(&wgpu::TextureDescriptor {
454                label: Some("Depth Texture"),
455                size: wgpu::Extent3d {
456                    width: self.surface_config.width,
457                    height: self.surface_config.height,
458                    depth_or_array_layers: 1,
459                },
460                mip_level_count: 1,
461                sample_count,
462                dimension: wgpu::TextureDimension::D2,
463                format: wgpu::TextureFormat::Depth32Float,
464                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
465                view_formats: &[],
466            })
467    }
468
469    /// Render point cloud with splatting
470    pub fn render(&self, vertices: &[PointVertex]) -> Result<()> {
471        let vertex_buffer = self.create_vertex_buffer(vertices);
472        let depth_texture = self.create_depth_texture();
473        let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
474
475        let output = match self.surface.get_current_texture() {
476            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
477            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
478            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
479                return Ok(())
480            }
481            _ => return Err(Error::Gpu("Failed to get surface texture".to_string())),
482        };
483
484        let view = output
485            .texture
486            .create_view(&wgpu::TextureViewDescriptor::default());
487
488        let mut encoder =
489            self.gpu_context
490                .device
491                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
492                    label: Some("Point Cloud Render Encoder"),
493                });
494
495        // Determine render target
496        let (color_attachment, resolve_target) = if let Some(ref msaa_view) = self.msaa_view {
497            (msaa_view, Some(&view))
498        } else {
499            (&view, None)
500        };
501
502        {
503            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
504                label: Some("Point Cloud Render Pass"),
505                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
506                    view: color_attachment,
507                    resolve_target,
508                    ops: wgpu::Operations {
509                        load: wgpu::LoadOp::Clear(wgpu::Color {
510                            r: self.config.background_color[0],
511                            g: self.config.background_color[1],
512                            b: self.config.background_color[2],
513                            a: self.config.background_color[3],
514                        }),
515                        store: wgpu::StoreOp::Store,
516                    },
517                    depth_slice: None,
518                })],
519                depth_stencil_attachment: if self.config.enable_depth_test {
520                    Some(wgpu::RenderPassDepthStencilAttachment {
521                        view: &depth_view,
522                        depth_ops: Some(wgpu::Operations {
523                            load: wgpu::LoadOp::Clear(1.0),
524                            store: wgpu::StoreOp::Store,
525                        }),
526                        stencil_ops: None,
527                    })
528                } else {
529                    None
530                },
531                timestamp_writes: None,
532                occlusion_query_set: None,
533                multiview_mask: None,
534            });
535
536            render_pass.set_pipeline(&self.render_pipeline);
537            render_pass.set_bind_group(0, &self.bind_group, &[]);
538            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
539            render_pass.draw(0..vertices.len() as u32, 0..1);
540        }
541
542        self.gpu_context
543            .queue
544            .submit(std::iter::once(encoder.finish()));
545        output.present();
546
547        Ok(())
548    }
549}
550
551/// Convert point cloud to vertices with estimated normals
552pub fn point_cloud_to_vertices(
553    point_cloud: &PointCloud<Point3f>,
554    color: [f32; 3],
555    size: f32,
556) -> Vec<PointVertex> {
557    let normals = estimate_point_normals(&point_cloud.points);
558    point_cloud
559        .points
560        .iter()
561        .zip(normals.iter())
562        .map(|(point, normal)| PointVertex::from_point(point, color, size, *normal))
563        .collect()
564}
565
566/// Convert colored point cloud to vertices with estimated normals
567pub fn point_cloud_to_vertices_colored(
568    point_cloud: &PointCloud<Point3f>,
569    size: f32,
570) -> Vec<PointVertex> {
571    let normals = estimate_point_normals(&point_cloud.points);
572    point_cloud
573        .points
574        .iter()
575        .zip(normals.iter())
576        .map(|(point, normal)| {
577            // Generate a color based on point position for visualization
578            let color = [
579                (point.x * 0.5 + 0.5).clamp(0.0, 1.0),
580                (point.y * 0.5 + 0.5).clamp(0.0, 1.0),
581                (point.z * 0.5 + 0.5).clamp(0.0, 1.0),
582            ];
583
584            PointVertex::from_point(point, color, size, *normal)
585        })
586        .collect()
587}
588
589/// Convert colored point cloud to vertices with estimated normals
590pub fn colored_point_cloud_to_vertices(
591    point_cloud: &PointCloud<ColoredPoint3f>,
592    size: f32,
593) -> Vec<PointVertex> {
594    let positions: Vec<Point3f> = point_cloud
595        .points
596        .iter()
597        .map(|p| Point3f::new(p.position.x, p.position.y, p.position.z))
598        .collect();
599    let normals = estimate_point_normals(&positions);
600
601    point_cloud
602        .points
603        .iter()
604        .zip(normals.iter())
605        .map(|(point, normal)| PointVertex::from_colored_point(point, size, *normal))
606        .collect()
607}
608
609/// Simple normal estimation using local point neighborhood
610fn estimate_point_normals(points: &[Point3f]) -> Vec<[f32; 3]> {
611    let mut normals = vec![[0.0, 0.0, 1.0]; points.len()];
612
613    for (i, point) in points.iter().enumerate() {
614        // Find nearby points for normal estimation
615        let mut neighbors = Vec::new();
616        let search_radius = 0.1;
617
618        for (j, other_point) in points.iter().enumerate() {
619            if i != j {
620                let distance = ((point.x - other_point.x).powi(2)
621                    + (point.y - other_point.y).powi(2)
622                    + (point.z - other_point.z).powi(2))
623                .sqrt();
624
625                if distance < search_radius && neighbors.len() < 10 {
626                    neighbors.push(*other_point);
627                }
628            }
629        }
630
631        if neighbors.len() >= 3 {
632            // Compute normal using PCA-like approach
633            let mut normal = estimate_normal_from_neighbors(point, &neighbors);
634
635            // Normalize the normal
636            let length = (normal[0].powi(2) + normal[1].powi(2) + normal[2].powi(2)).sqrt();
637            if length > 0.0 {
638                normal[0] /= length;
639                normal[1] /= length;
640                normal[2] /= length;
641            }
642
643            normals[i] = normal;
644        }
645    }
646
647    normals
648}
649
650/// Estimate normal from neighboring points using cross product
651fn estimate_normal_from_neighbors(center: &Point3f, neighbors: &[Point3f]) -> [f32; 3] {
652    if neighbors.len() < 2 {
653        return [0.0, 0.0, 1.0];
654    }
655
656    // Use first two neighbors to compute normal
657    let v1 = [
658        neighbors[0].x - center.x,
659        neighbors[0].y - center.y,
660        neighbors[0].z - center.z,
661    ];
662
663    let v2 = [
664        neighbors[1].x - center.x,
665        neighbors[1].y - center.y,
666        neighbors[1].z - center.z,
667    ];
668
669    // Cross product
670    let normal = [
671        v1[1] * v2[2] - v1[2] * v2[1],
672        v1[2] * v2[0] - v1[0] * v2[2],
673        v1[0] * v2[1] - v1[1] * v2[0],
674    ];
675
676    normal
677}
678
679// Note: Interactive rendering function commented out due to lifetime complexity
680// Users can create their own rendering loop using PointCloudRenderer::new()
681//
682// /// GPU-accelerated point cloud rendering function
683// pub async fn gpu_render_point_cloud(...) -> Result<()> {
684//     // Implementation requires complex lifetime management
685//     // Use PointCloudRenderer directly for custom rendering loops
686// }