runmat_plot/core/
renderer.rs

1//! WGPU-based rendering backend for high-performance plotting
2//!
3//! This module provides GPU-accelerated rendering using WGPU, supporting
4//! both desktop and web targets for maximum compatibility.
5
6use bytemuck::{Pod, Zeroable};
7use glam::{Mat4, Vec3, Vec4};
8use std::sync::Arc;
9use wgpu::util::DeviceExt;
10
11/// Vertex data for rendering points, lines, and triangles
12#[repr(C)]
13#[derive(Clone, Copy, Debug, Pod, Zeroable)]
14pub struct Vertex {
15    pub position: [f32; 3],
16    pub color: [f32; 4],
17    pub normal: [f32; 3],
18    pub tex_coords: [f32; 2],
19}
20
21impl Vertex {
22    pub fn new(position: Vec3, color: Vec4) -> Self {
23        Self {
24            position: position.to_array(),
25            color: color.to_array(),
26            normal: [0.0, 0.0, 1.0], // Default normal
27            tex_coords: [0.0, 0.0],  // Default UV
28        }
29    }
30
31    pub fn desc() -> wgpu::VertexBufferLayout<'static> {
32        let stride = std::mem::size_of::<Vertex>() as wgpu::BufferAddress;
33        println!(
34            "VERTEX: Struct size = {}, stride = {}",
35            std::mem::size_of::<Vertex>(),
36            stride
37        );
38        wgpu::VertexBufferLayout {
39            array_stride: stride,
40            step_mode: wgpu::VertexStepMode::Vertex,
41            attributes: &[
42                // Position
43                wgpu::VertexAttribute {
44                    offset: 0,
45                    shader_location: 0,
46                    format: wgpu::VertexFormat::Float32x3,
47                },
48                // Color
49                wgpu::VertexAttribute {
50                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
51                    shader_location: 1,
52                    format: wgpu::VertexFormat::Float32x4,
53                },
54                // Normal
55                wgpu::VertexAttribute {
56                    offset: std::mem::size_of::<[f32; 7]>() as wgpu::BufferAddress,
57                    shader_location: 2,
58                    format: wgpu::VertexFormat::Float32x3,
59                },
60                // Texture coordinates
61                wgpu::VertexAttribute {
62                    offset: std::mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
63                    shader_location: 3,
64                    format: wgpu::VertexFormat::Float32x2,
65                },
66            ],
67        }
68    }
69}
70
71/// Uniform buffer for camera and transformation matrices
72#[repr(C)]
73#[derive(Clone, Copy, Debug, Pod, Zeroable)]
74pub struct Uniforms {
75    pub view_proj: [[f32; 4]; 4],
76    pub model: [[f32; 4]; 4],
77    pub normal_matrix: [[f32; 4]; 3], // Use 4x3 for proper alignment instead of 3x3
78}
79
80/// Optimized uniform buffer for direct coordinate transformation rendering
81/// Enables precise viewport-constrained data visualization
82#[repr(C)]
83#[derive(Clone, Copy, Debug, Pod, Zeroable)]
84pub struct DirectUniforms {
85    pub data_min: [f32; 2],     // (x_min, y_min) in data space
86    pub data_max: [f32; 2],     // (x_max, y_max) in data space
87    pub viewport_min: [f32; 2], // NDC coordinates of viewport bottom-left
88    pub viewport_max: [f32; 2], // NDC coordinates of viewport top-right
89}
90
91impl Default for Uniforms {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl Uniforms {
98    pub fn new() -> Self {
99        Self {
100            view_proj: Mat4::IDENTITY.to_cols_array_2d(),
101            model: Mat4::IDENTITY.to_cols_array_2d(),
102            normal_matrix: [
103                [1.0, 0.0, 0.0, 0.0],
104                [0.0, 1.0, 0.0, 0.0],
105                [0.0, 0.0, 1.0, 0.0],
106            ],
107        }
108    }
109
110    pub fn update_view_proj(&mut self, view_proj: Mat4) {
111        self.view_proj = view_proj.to_cols_array_2d();
112    }
113
114    pub fn update_model(&mut self, model: Mat4) {
115        self.model = model.to_cols_array_2d();
116        // Update normal matrix (upper 3x3 of inverse transpose) with proper alignment
117        let normal_mat = model.inverse().transpose();
118        self.normal_matrix = [
119            [
120                normal_mat.x_axis.x,
121                normal_mat.x_axis.y,
122                normal_mat.x_axis.z,
123                0.0,
124            ],
125            [
126                normal_mat.y_axis.x,
127                normal_mat.y_axis.y,
128                normal_mat.y_axis.z,
129                0.0,
130            ],
131            [
132                normal_mat.z_axis.x,
133                normal_mat.z_axis.y,
134                normal_mat.z_axis.z,
135                0.0,
136            ],
137        ];
138    }
139}
140
141impl DirectUniforms {
142    pub fn new(
143        data_min: [f32; 2],
144        data_max: [f32; 2],
145        viewport_min: [f32; 2],
146        viewport_max: [f32; 2],
147    ) -> Self {
148        Self {
149            data_min,
150            data_max,
151            viewport_min,
152            viewport_max,
153        }
154    }
155}
156
157/// Rendering pipeline types
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum PipelineType {
160    Points,
161    Lines,
162    Triangles,
163    PointCloud,
164}
165
166/// High-performance WGPU renderer for interactive plotting
167pub struct WgpuRenderer {
168    pub device: Arc<wgpu::Device>,
169    pub queue: Arc<wgpu::Queue>,
170    pub surface_config: wgpu::SurfaceConfiguration,
171
172    // Rendering pipelines (traditional camera-based)
173    point_pipeline: Option<wgpu::RenderPipeline>,
174    line_pipeline: Option<wgpu::RenderPipeline>,
175    triangle_pipeline: Option<wgpu::RenderPipeline>,
176
177    // Direct rendering pipelines (optimized coordinate transformation)
178    pub direct_line_pipeline: Option<wgpu::RenderPipeline>,
179
180    // Uniform resources (traditional)
181    uniform_buffer: wgpu::Buffer,
182    uniform_bind_group: wgpu::BindGroup,
183    uniform_bind_group_layout: wgpu::BindGroupLayout,
184
185    // Direct uniform resources (optimized coordinate transformation)
186    direct_uniform_buffer: wgpu::Buffer,
187    pub direct_uniform_bind_group: wgpu::BindGroup,
188    direct_uniform_bind_group_layout: wgpu::BindGroupLayout,
189
190    // Current uniforms
191    uniforms: Uniforms,
192    direct_uniforms: DirectUniforms,
193}
194
195impl WgpuRenderer {
196    /// Create a new WGPU renderer
197    pub async fn new(
198        device: Arc<wgpu::Device>,
199        queue: Arc<wgpu::Queue>,
200        surface_config: wgpu::SurfaceConfiguration,
201    ) -> Self {
202        // Create uniform buffer
203        let uniforms = Uniforms::new();
204        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
205            label: Some("Uniform Buffer"),
206            contents: bytemuck::cast_slice(&[uniforms]),
207            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
208        });
209
210        // Create bind group layout for uniforms
211        let uniform_bind_group_layout =
212            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
213                entries: &[wgpu::BindGroupLayoutEntry {
214                    binding: 0,
215                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
216                    ty: wgpu::BindingType::Buffer {
217                        ty: wgpu::BufferBindingType::Uniform,
218                        has_dynamic_offset: false,
219                        min_binding_size: None,
220                    },
221                    count: None,
222                }],
223                label: Some("uniform_bind_group_layout"),
224            });
225
226        // Create bind group
227        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
228            layout: &uniform_bind_group_layout,
229            entries: &[wgpu::BindGroupEntry {
230                binding: 0,
231                resource: uniform_buffer.as_entire_binding(),
232            }],
233            label: Some("uniform_bind_group"),
234        });
235
236        // Create direct rendering uniform buffer
237        let direct_uniforms = DirectUniforms::new(
238            [0.0, 0.0],   // data_min
239            [1.0, 1.0],   // data_max
240            [-1.0, -1.0], // viewport_min (full NDC)
241            [1.0, 1.0],   // viewport_max (full NDC)
242        );
243        let direct_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
244            label: Some("Direct Uniform Buffer"),
245            contents: bytemuck::cast_slice(&[direct_uniforms]),
246            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
247        });
248
249        // Create direct bind group layout for uniforms
250        let direct_uniform_bind_group_layout =
251            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
252                entries: &[wgpu::BindGroupLayoutEntry {
253                    binding: 0,
254                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
255                    ty: wgpu::BindingType::Buffer {
256                        ty: wgpu::BufferBindingType::Uniform,
257                        has_dynamic_offset: false,
258                        min_binding_size: None,
259                    },
260                    count: None,
261                }],
262                label: Some("direct_uniform_bind_group_layout"),
263            });
264
265        // Create direct bind group
266        let direct_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
267            layout: &direct_uniform_bind_group_layout,
268            entries: &[wgpu::BindGroupEntry {
269                binding: 0,
270                resource: direct_uniform_buffer.as_entire_binding(),
271            }],
272            label: Some("direct_uniform_bind_group"),
273        });
274
275        Self {
276            device,
277            queue,
278            surface_config,
279            point_pipeline: None,
280            line_pipeline: None,
281            triangle_pipeline: None,
282            direct_line_pipeline: None,
283            uniform_buffer,
284            uniform_bind_group,
285            uniform_bind_group_layout,
286            direct_uniform_buffer,
287            direct_uniform_bind_group,
288            direct_uniform_bind_group_layout,
289            uniforms,
290            direct_uniforms,
291        }
292    }
293
294    /// Create a vertex buffer from vertex data
295    pub fn create_vertex_buffer(&self, vertices: &[Vertex]) -> wgpu::Buffer {
296        self.device
297            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
298                label: Some("Vertex Buffer"),
299                contents: bytemuck::cast_slice(vertices),
300                usage: wgpu::BufferUsages::VERTEX,
301            })
302    }
303
304    /// Create an index buffer from index data
305    pub fn create_index_buffer(&self, indices: &[u32]) -> wgpu::Buffer {
306        self.device
307            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
308                label: Some("Index Buffer"),
309                contents: bytemuck::cast_slice(indices),
310                usage: wgpu::BufferUsages::INDEX,
311            })
312    }
313
314    /// Update uniform buffer with new matrices
315    pub fn update_uniforms(&mut self, view_proj: Mat4, model: Mat4) {
316        self.uniforms.update_view_proj(view_proj);
317        self.uniforms.update_model(model);
318
319        self.queue.write_buffer(
320            &self.uniform_buffer,
321            0,
322            bytemuck::cast_slice(&[self.uniforms]),
323        );
324    }
325
326    /// Get the uniform bind group for rendering
327    pub fn get_uniform_bind_group(&self) -> &wgpu::BindGroup {
328        &self.uniform_bind_group
329    }
330
331    /// Ensure pipeline exists for the specified type
332    pub fn ensure_pipeline(&mut self, pipeline_type: PipelineType) {
333        match pipeline_type {
334            PipelineType::Points => {
335                if self.point_pipeline.is_none() {
336                    self.point_pipeline = Some(self.create_point_pipeline());
337                }
338            }
339            PipelineType::Lines => {
340                if self.line_pipeline.is_none() {
341                    self.line_pipeline = Some(self.create_line_pipeline());
342                }
343            }
344            PipelineType::Triangles => {
345                if self.triangle_pipeline.is_none() {
346                    self.triangle_pipeline = Some(self.create_triangle_pipeline());
347                }
348            }
349            PipelineType::PointCloud => {
350                // For now, use points pipeline - will optimize later
351                self.ensure_pipeline(PipelineType::Points);
352            }
353        }
354    }
355
356    /// Get a pipeline reference (pipeline must already exist)
357    pub fn get_pipeline(&self, pipeline_type: PipelineType) -> &wgpu::RenderPipeline {
358        match pipeline_type {
359            PipelineType::Points => self.point_pipeline.as_ref().unwrap(),
360            PipelineType::Lines => self.line_pipeline.as_ref().unwrap(),
361            PipelineType::Triangles => self.triangle_pipeline.as_ref().unwrap(),
362            PipelineType::PointCloud => self.get_pipeline(PipelineType::Points),
363        }
364    }
365
366    /// Create point rendering pipeline
367    fn create_point_pipeline(&self) -> wgpu::RenderPipeline {
368        let shader = self
369            .device
370            .create_shader_module(wgpu::ShaderModuleDescriptor {
371                label: Some("Point Shader"),
372                source: wgpu::ShaderSource::Wgsl(
373                    include_str!("../../shaders/vertex/point.wgsl").into(),
374                ),
375            });
376
377        let pipeline_layout = self
378            .device
379            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
380                label: Some("Point Pipeline Layout"),
381                bind_group_layouts: &[&self.uniform_bind_group_layout],
382                push_constant_ranges: &[],
383            });
384
385        self.device
386            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
387                label: Some("Point Pipeline"),
388                layout: Some(&pipeline_layout),
389                vertex: wgpu::VertexState {
390                    module: &shader,
391                    entry_point: "vs_main",
392                    buffers: &[Vertex::desc()],
393                },
394                fragment: Some(wgpu::FragmentState {
395                    module: &shader,
396                    entry_point: "fs_main",
397                    targets: &[Some(wgpu::ColorTargetState {
398                        format: self.surface_config.format,
399                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
400                        write_mask: wgpu::ColorWrites::ALL,
401                    })],
402                }),
403                primitive: wgpu::PrimitiveState {
404                    topology: wgpu::PrimitiveTopology::PointList,
405                    strip_index_format: None,
406                    front_face: wgpu::FrontFace::Ccw,
407                    cull_mode: None,
408                    polygon_mode: wgpu::PolygonMode::Fill,
409                    unclipped_depth: false,
410                    conservative: false,
411                },
412                depth_stencil: None, // Disable depth testing for 2D point plots
413                multisample: wgpu::MultisampleState {
414                    count: 1,
415                    mask: !0,
416                    alpha_to_coverage_enabled: false,
417                },
418                multiview: None,
419            })
420    }
421
422    /// Create line rendering pipeline
423    fn create_line_pipeline(&self) -> wgpu::RenderPipeline {
424        let shader = self
425            .device
426            .create_shader_module(wgpu::ShaderModuleDescriptor {
427                label: Some("Line Shader"),
428                source: wgpu::ShaderSource::Wgsl(
429                    include_str!("../../shaders/vertex/line.wgsl").into(),
430                ),
431            });
432
433        let pipeline_layout = self
434            .device
435            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
436                label: Some("Line Pipeline Layout"),
437                bind_group_layouts: &[&self.uniform_bind_group_layout],
438                push_constant_ranges: &[],
439            });
440
441        self.device
442            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
443                label: Some("Line Pipeline"),
444                layout: Some(&pipeline_layout),
445                vertex: wgpu::VertexState {
446                    module: &shader,
447                    entry_point: "vs_main",
448                    buffers: &[Vertex::desc()],
449                },
450                fragment: Some(wgpu::FragmentState {
451                    module: &shader,
452                    entry_point: "fs_main",
453                    targets: &[Some(wgpu::ColorTargetState {
454                        format: self.surface_config.format,
455                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
456                        write_mask: wgpu::ColorWrites::ALL,
457                    })],
458                }),
459                primitive: wgpu::PrimitiveState {
460                    topology: wgpu::PrimitiveTopology::LineList,
461                    strip_index_format: None,
462                    front_face: wgpu::FrontFace::Ccw,
463                    cull_mode: None,
464                    polygon_mode: wgpu::PolygonMode::Fill,
465                    unclipped_depth: false,
466                    conservative: false,
467                },
468                depth_stencil: None, // Disable depth testing for 2D line plots
469                multisample: wgpu::MultisampleState {
470                    count: 1,
471                    mask: !0,
472                    alpha_to_coverage_enabled: false,
473                },
474                multiview: None,
475            })
476    }
477
478    /// Create optimized direct rendering pipeline for precise viewport mapping
479    fn create_direct_line_pipeline(&self) -> wgpu::RenderPipeline {
480        let shader = self
481            .device
482            .create_shader_module(wgpu::ShaderModuleDescriptor {
483                label: Some("Direct Line Shader"),
484                source: wgpu::ShaderSource::Wgsl(
485                    include_str!("../../shaders/vertex/line_direct.wgsl").into(),
486                ),
487            });
488
489        let pipeline_layout = self
490            .device
491            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
492                label: Some("Direct Line Pipeline Layout"),
493                bind_group_layouts: &[&self.direct_uniform_bind_group_layout],
494                push_constant_ranges: &[],
495            });
496
497        self.device
498            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
499                label: Some("Direct Line Pipeline"),
500                layout: Some(&pipeline_layout),
501                vertex: wgpu::VertexState {
502                    module: &shader,
503                    entry_point: "vs_main",
504                    buffers: &[Vertex::desc()],
505                },
506                fragment: Some(wgpu::FragmentState {
507                    module: &shader,
508                    entry_point: "fs_main",
509                    targets: &[Some(wgpu::ColorTargetState {
510                        format: self.surface_config.format,
511                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
512                        write_mask: wgpu::ColorWrites::ALL,
513                    })],
514                }),
515                primitive: wgpu::PrimitiveState {
516                    topology: wgpu::PrimitiveTopology::LineList,
517                    strip_index_format: None,
518                    front_face: wgpu::FrontFace::Ccw,
519                    cull_mode: None,
520                    polygon_mode: wgpu::PolygonMode::Fill,
521                    unclipped_depth: false,
522                    conservative: false,
523                },
524                depth_stencil: None, // Disable depth testing for 2D line plots
525                multisample: wgpu::MultisampleState {
526                    count: 1,
527                    mask: !0,
528                    alpha_to_coverage_enabled: false,
529                },
530                multiview: None,
531            })
532    }
533
534    /// Create triangle rendering pipeline
535    fn create_triangle_pipeline(&self) -> wgpu::RenderPipeline {
536        let shader = self
537            .device
538            .create_shader_module(wgpu::ShaderModuleDescriptor {
539                label: Some("Triangle Shader"),
540                source: wgpu::ShaderSource::Wgsl(
541                    include_str!("../../shaders/vertex/triangle.wgsl").into(),
542                ),
543            });
544
545        let pipeline_layout = self
546            .device
547            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
548                label: Some("Triangle Pipeline Layout"),
549                bind_group_layouts: &[&self.uniform_bind_group_layout],
550                push_constant_ranges: &[],
551            });
552
553        self.device
554            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
555                label: Some("Triangle Pipeline"),
556                layout: Some(&pipeline_layout),
557                vertex: wgpu::VertexState {
558                    module: &shader,
559                    entry_point: "vs_main",
560                    buffers: &[Vertex::desc()],
561                },
562                fragment: Some(wgpu::FragmentState {
563                    module: &shader,
564                    entry_point: "fs_main",
565                    targets: &[Some(wgpu::ColorTargetState {
566                        format: self.surface_config.format,
567                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
568                        write_mask: wgpu::ColorWrites::ALL,
569                    })],
570                }),
571                primitive: wgpu::PrimitiveState {
572                    topology: wgpu::PrimitiveTopology::TriangleList,
573                    strip_index_format: None,
574                    front_face: wgpu::FrontFace::Ccw,
575                    cull_mode: None, // Disable culling for 2D plotting
576                    polygon_mode: wgpu::PolygonMode::Fill,
577                    unclipped_depth: false,
578                    conservative: false,
579                },
580                depth_stencil: None, // Disable depth testing for 2D plotting
581                multisample: wgpu::MultisampleState {
582                    count: 1,
583                    mask: !0,
584                    alpha_to_coverage_enabled: false,
585                },
586                multiview: None,
587            })
588    }
589
590    /// Begin a render pass
591    pub fn begin_render_pass<'a>(
592        &'a self,
593        encoder: &'a mut wgpu::CommandEncoder,
594        view: &'a wgpu::TextureView,
595        _depth_view: &'a wgpu::TextureView,
596    ) -> wgpu::RenderPass<'a> {
597        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
598            label: Some("Render Pass"),
599            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
600                view,
601                resolve_target: None,
602                ops: wgpu::Operations {
603                    load: wgpu::LoadOp::Clear(wgpu::Color {
604                        r: 0.1,
605                        g: 0.1,
606                        b: 0.1,
607                        a: 1.0,
608                    }),
609                    store: wgpu::StoreOp::Store,
610                },
611            })],
612            depth_stencil_attachment: None, // No depth testing for 2D plotting
613            occlusion_query_set: None,
614            timestamp_writes: None,
615        })
616    }
617
618    /// Render vertices with the specified pipeline
619    pub fn render_vertices<'a>(
620        &'a mut self,
621        render_pass: &mut wgpu::RenderPass<'a>,
622        pipeline_type: PipelineType,
623        vertex_buffer: &'a wgpu::Buffer,
624        vertex_count: u32,
625        index_buffer: Option<(&'a wgpu::Buffer, u32)>,
626    ) {
627        // Ensure the pipeline exists first
628        self.ensure_pipeline(pipeline_type);
629
630        // Now get the pipeline and render
631        let pipeline = self.get_pipeline(pipeline_type);
632        render_pass.set_pipeline(pipeline);
633        render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
634        render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
635
636        match index_buffer {
637            Some((indices, index_count)) => {
638                render_pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
639                render_pass.draw_indexed(0..index_count, 0, 0..1);
640            }
641            None => {
642                render_pass.draw(0..vertex_count, 0..1);
643            }
644        }
645    }
646
647    /// Ensure direct line pipeline exists
648    pub fn ensure_direct_line_pipeline(&mut self) {
649        if self.direct_line_pipeline.is_none() {
650            self.direct_line_pipeline = Some(self.create_direct_line_pipeline());
651        }
652    }
653
654    /// Update transformation uniforms for direct viewport rendering
655    pub fn update_direct_uniforms(
656        &mut self,
657        data_min: [f32; 2],
658        data_max: [f32; 2],
659        viewport_min: [f32; 2],
660        viewport_max: [f32; 2],
661    ) {
662        self.direct_uniforms = DirectUniforms::new(data_min, data_max, viewport_min, viewport_max);
663        self.queue.write_buffer(
664            &self.direct_uniform_buffer,
665            0,
666            bytemuck::cast_slice(&[self.direct_uniforms]),
667        );
668    }
669}
670
671/// Utility functions for creating common vertex patterns
672pub mod vertex_utils {
673    use super::*;
674
675    /// Create vertices for a line from start to end point
676    pub fn create_line(start: Vec3, end: Vec3, color: Vec4) -> Vec<Vertex> {
677        vec![Vertex::new(start, color), Vertex::new(end, color)]
678    }
679
680    /// Create vertices for a triangle
681    pub fn create_triangle(p1: Vec3, p2: Vec3, p3: Vec3, color: Vec4) -> Vec<Vertex> {
682        vec![
683            Vertex::new(p1, color),
684            Vertex::new(p2, color),
685            Vertex::new(p3, color),
686        ]
687    }
688
689    /// Create vertices for a point cloud
690    pub fn create_point_cloud(points: &[Vec3], colors: &[Vec4]) -> Vec<Vertex> {
691        points
692            .iter()
693            .zip(colors.iter())
694            .map(|(&pos, &color)| Vertex::new(pos, color))
695            .collect()
696    }
697
698    /// Create vertices for a parametric line plot
699    pub fn create_line_plot(x_data: &[f64], y_data: &[f64], color: Vec4) -> Vec<Vertex> {
700        let mut vertices = Vec::new();
701
702        for i in 1..x_data.len() {
703            let start = Vec3::new(x_data[i - 1] as f32, y_data[i - 1] as f32, 0.0);
704            let end = Vec3::new(x_data[i] as f32, y_data[i] as f32, 0.0);
705            vertices.extend(create_line(start, end, color));
706        }
707
708        vertices
709    }
710
711    /// Create vertices for a scatter plot
712    pub fn create_scatter_plot(x_data: &[f64], y_data: &[f64], color: Vec4) -> Vec<Vertex> {
713        x_data
714            .iter()
715            .zip(y_data.iter())
716            .map(|(&x, &y)| Vertex::new(Vec3::new(x as f32, y as f32, 0.0), color))
717            .collect()
718    }
719}