Skip to main content

threecrate_gpu/
mesh.rs

1//! GPU-accelerated mesh rendering with PBR and flat shading
2
3use threecrate_core::{Result, Error};
4use threecrate_simplification::ProgressiveMesh;
5use crate::GpuContext;
6use nalgebra::{Matrix4, Vector3, Point3};
7use bytemuck::{Pod, Zeroable};
8use wgpu::util::DeviceExt;
9use winit::window::Window;
10
11/// Vertex data for mesh rendering with full PBR attributes
12#[repr(C)]
13#[derive(Copy, Clone, Debug, Pod, Zeroable)]
14pub struct MeshVertex {
15    pub position: [f32; 3],
16    pub normal: [f32; 3],
17    pub tangent: [f32; 3],
18    pub bitangent: [f32; 3],
19    pub uv: [f32; 2],
20    pub color: [f32; 3],
21    pub _padding: f32,
22}
23
24impl MeshVertex {
25    /// Create a new mesh vertex
26    pub fn new(
27        position: [f32; 3],
28        normal: [f32; 3],
29        uv: [f32; 2],
30        color: [f32; 3],
31    ) -> Self {
32        // Calculate tangent and bitangent vectors (simplified)
33        let tangent = if normal[0].abs() > 0.9 {
34            [0.0, 1.0, 0.0]
35        } else {
36            [1.0, 0.0, 0.0]
37        };
38        
39        let bitangent = [
40            normal[1] * tangent[2] - normal[2] * tangent[1],
41            normal[2] * tangent[0] - normal[0] * tangent[2],
42            normal[0] * tangent[1] - normal[1] * tangent[0],
43        ];
44        
45        Self {
46            position,
47            normal,
48            tangent,
49            bitangent,
50            uv,
51            color,
52            _padding: 0.0,
53        }
54    }
55    
56    /// Create vertex from position and normal with default color
57    pub fn from_pos_normal(position: [f32; 3], normal: [f32; 3]) -> Self {
58        Self::new(position, normal, [0.0, 0.0], [0.8, 0.8, 0.8])
59    }
60    
61    /// Vertex buffer layout descriptor
62    pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
63        wgpu::VertexBufferLayout {
64            array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
65            step_mode: wgpu::VertexStepMode::Vertex,
66            attributes: &[
67                // Position
68                wgpu::VertexAttribute {
69                    offset: 0,
70                    shader_location: 0,
71                    format: wgpu::VertexFormat::Float32x3,
72                },
73                // Normal
74                wgpu::VertexAttribute {
75                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
76                    shader_location: 1,
77                    format: wgpu::VertexFormat::Float32x3,
78                },
79                // Tangent
80                wgpu::VertexAttribute {
81                    offset: std::mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
82                    shader_location: 2,
83                    format: wgpu::VertexFormat::Float32x3,
84                },
85                // Bitangent
86                wgpu::VertexAttribute {
87                    offset: std::mem::size_of::<[f32; 9]>() as wgpu::BufferAddress,
88                    shader_location: 3,
89                    format: wgpu::VertexFormat::Float32x3,
90                },
91                // UV
92                wgpu::VertexAttribute {
93                    offset: std::mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
94                    shader_location: 4,
95                    format: wgpu::VertexFormat::Float32x2,
96                },
97                // Color
98                wgpu::VertexAttribute {
99                    offset: std::mem::size_of::<[f32; 14]>() as wgpu::BufferAddress,
100                    shader_location: 5,
101                    format: wgpu::VertexFormat::Float32x3,
102                },
103            ],
104        }
105    }
106}
107
108/// Camera uniform data for mesh rendering
109#[repr(C)]
110#[derive(Copy, Clone, Pod, Zeroable)]
111pub struct MeshCameraUniform {
112    pub view_proj: [[f32; 4]; 4],
113    pub view_pos: [f32; 3],
114    pub _padding: f32,
115}
116
117/// PBR material properties
118#[repr(C)]
119#[derive(Copy, Clone, Debug, Pod, Zeroable)]
120pub struct PbrMaterial {
121    pub albedo: [f32; 3],      // offset  0 (12 bytes)
122    pub metallic: f32,         // offset 12 (4 bytes)
123    pub roughness: f32,        // offset 16 (4 bytes)
124    pub ao: f32,               // offset 20 (4 bytes)
125    pub _padding1: [f32; 2],   // offset 24 (8 bytes) — aligns emission to offset 32 per WGSL std140
126    pub emission: [f32; 3],    // offset 32 (12 bytes)
127    pub _padding2: f32,        // offset 44 (4 bytes) — total: 48 bytes
128}
129
130impl Default for PbrMaterial {
131    fn default() -> Self {
132        Self {
133            albedo: [0.7, 0.7, 0.7],
134            metallic: 0.0,
135            roughness: 0.5,
136            ao: 1.0,
137            _padding1: [0.0, 0.0],
138            emission: [0.0, 0.0, 0.0],
139            _padding2: 0.0,
140        }
141    }
142}
143
144/// Flat shading material properties
145#[repr(C)]
146#[derive(Copy, Clone, Debug, Pod, Zeroable)]
147pub struct FlatMaterial {
148    pub color: [f32; 3],
149    pub _padding: f32,
150}
151
152impl Default for FlatMaterial {
153    fn default() -> Self {
154        Self {
155            color: [0.8, 0.8, 0.8],
156            _padding: 0.0,
157        }
158    }
159}
160
161/// Lighting parameters for mesh rendering
162#[repr(C)]
163#[derive(Copy, Clone, Debug, Pod, Zeroable)]
164pub struct MeshLightingParams {
165    pub light_position: [f32; 3],
166    pub light_intensity: f32,
167    pub light_color: [f32; 3],
168    pub ambient_strength: f32,
169    pub gamma: f32,
170    pub exposure: f32,
171    pub _padding: [f32; 2],
172}
173
174impl Default for MeshLightingParams {
175    fn default() -> Self {
176        Self {
177            light_position: [10.0, 10.0, 10.0],
178            light_intensity: 1.0,
179            light_color: [1.0, 1.0, 1.0],
180            ambient_strength: 0.03,
181            gamma: 2.2,
182            exposure: 1.0,
183            _padding: [0.0, 0.0],
184        }
185    }
186}
187
188/// Mesh rendering configuration
189#[derive(Debug, Clone)]
190pub struct MeshRenderConfig {
191    pub lighting_params: MeshLightingParams,
192    pub background_color: [f64; 4],
193    pub enable_depth_test: bool,
194    pub enable_backface_culling: bool,
195    pub enable_multisampling: bool,
196    pub wireframe_mode: bool,
197}
198
199impl Default for MeshRenderConfig {
200    fn default() -> Self {
201        Self {
202            lighting_params: MeshLightingParams::default(),
203            background_color: [0.1, 0.1, 0.1, 1.0],
204            enable_depth_test: true,
205            enable_backface_culling: true,
206            enable_multisampling: true,
207            wireframe_mode: false,
208        }
209    }
210}
211
212/// Mesh data structure for GPU rendering
213#[derive(Debug, Clone)]
214pub struct GpuMesh {
215    pub vertices: Vec<MeshVertex>,
216    pub indices: Vec<u32>,
217    pub material: PbrMaterial,
218}
219
220impl GpuMesh {
221    /// Create a new GPU mesh
222    pub fn new(vertices: Vec<MeshVertex>, indices: Vec<u32>, material: PbrMaterial) -> Self {
223        Self {
224            vertices,
225            indices,
226            material,
227        }
228    }
229    
230    /// Create a simple triangle mesh for testing
231    pub fn triangle() -> Self {
232        let vertices = vec![
233            MeshVertex::new([-0.5, -0.5, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0], [1.0, 0.0, 0.0]),
234            MeshVertex::new([0.5, -0.5, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0], [0.0, 1.0, 0.0]),
235            MeshVertex::new([0.0, 0.5, 0.0], [0.0, 0.0, 1.0], [0.5, 1.0], [0.0, 0.0, 1.0]),
236        ];
237        
238        let indices = vec![0, 1, 2];
239        
240        Self::new(vertices, indices, PbrMaterial::default())
241    }
242    
243    /// Create a cube mesh for testing
244    pub fn cube() -> Self {
245        let vertices = vec![
246            // Front face
247            MeshVertex::new([-1.0, -1.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0], [0.8, 0.2, 0.2]),
248            MeshVertex::new([1.0, -1.0, 1.0], [0.0, 0.0, 1.0], [1.0, 0.0], [0.8, 0.2, 0.2]),
249            MeshVertex::new([1.0, 1.0, 1.0], [0.0, 0.0, 1.0], [1.0, 1.0], [0.8, 0.2, 0.2]),
250            MeshVertex::new([-1.0, 1.0, 1.0], [0.0, 0.0, 1.0], [0.0, 1.0], [0.8, 0.2, 0.2]),
251            
252            // Back face
253            MeshVertex::new([1.0, -1.0, -1.0], [0.0, 0.0, -1.0], [0.0, 0.0], [0.2, 0.8, 0.2]),
254            MeshVertex::new([-1.0, -1.0, -1.0], [0.0, 0.0, -1.0], [1.0, 0.0], [0.2, 0.8, 0.2]),
255            MeshVertex::new([-1.0, 1.0, -1.0], [0.0, 0.0, -1.0], [1.0, 1.0], [0.2, 0.8, 0.2]),
256            MeshVertex::new([1.0, 1.0, -1.0], [0.0, 0.0, -1.0], [0.0, 1.0], [0.2, 0.8, 0.2]),
257            
258            // Top face
259            MeshVertex::new([-1.0, 1.0, 1.0], [0.0, 1.0, 0.0], [0.0, 0.0], [0.2, 0.2, 0.8]),
260            MeshVertex::new([1.0, 1.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0], [0.2, 0.2, 0.8]),
261            MeshVertex::new([1.0, 1.0, -1.0], [0.0, 1.0, 0.0], [1.0, 1.0], [0.2, 0.2, 0.8]),
262            MeshVertex::new([-1.0, 1.0, -1.0], [0.0, 1.0, 0.0], [0.0, 1.0], [0.2, 0.2, 0.8]),
263            
264            // Bottom face
265            MeshVertex::new([-1.0, -1.0, -1.0], [0.0, -1.0, 0.0], [0.0, 0.0], [0.8, 0.8, 0.2]),
266            MeshVertex::new([1.0, -1.0, -1.0], [0.0, -1.0, 0.0], [1.0, 0.0], [0.8, 0.8, 0.2]),
267            MeshVertex::new([1.0, -1.0, 1.0], [0.0, -1.0, 0.0], [1.0, 1.0], [0.8, 0.8, 0.2]),
268            MeshVertex::new([-1.0, -1.0, 1.0], [0.0, -1.0, 0.0], [0.0, 1.0], [0.8, 0.8, 0.2]),
269            
270            // Right face
271            MeshVertex::new([1.0, -1.0, 1.0], [1.0, 0.0, 0.0], [0.0, 0.0], [0.8, 0.2, 0.8]),
272            MeshVertex::new([1.0, -1.0, -1.0], [1.0, 0.0, 0.0], [1.0, 0.0], [0.8, 0.2, 0.8]),
273            MeshVertex::new([1.0, 1.0, -1.0], [1.0, 0.0, 0.0], [1.0, 1.0], [0.8, 0.2, 0.8]),
274            MeshVertex::new([1.0, 1.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0], [0.8, 0.2, 0.8]),
275            
276            // Left face
277            MeshVertex::new([-1.0, -1.0, -1.0], [-1.0, 0.0, 0.0], [0.0, 0.0], [0.2, 0.8, 0.8]),
278            MeshVertex::new([-1.0, -1.0, 1.0], [-1.0, 0.0, 0.0], [1.0, 0.0], [0.2, 0.8, 0.8]),
279            MeshVertex::new([-1.0, 1.0, 1.0], [-1.0, 0.0, 0.0], [1.0, 1.0], [0.2, 0.8, 0.8]),
280            MeshVertex::new([-1.0, 1.0, -1.0], [-1.0, 0.0, 0.0], [0.0, 1.0], [0.2, 0.8, 0.8]),
281        ];
282        
283        let indices = vec![
284            // Front face
285            0, 1, 2, 2, 3, 0,
286            // Back face
287            4, 5, 6, 6, 7, 4,
288            // Top face
289            8, 9, 10, 10, 11, 8,
290            // Bottom face
291            12, 13, 14, 14, 15, 12,
292            // Right face
293            16, 17, 18, 18, 19, 16,
294            // Left face
295            20, 21, 22, 22, 23, 20,
296        ];
297        
298        Self::new(vertices, indices, PbrMaterial::default())
299    }
300    
301    /// Create a mesh from point cloud with estimated normals
302    pub fn from_point_cloud(points: &[Point3<f32>], color: [f32; 3]) -> Self {
303        let vertices: Vec<MeshVertex> = points.iter().map(|p| {
304            // Simple normal estimation (could use the GPU normal computation)
305            let normal = [0.0, 0.0, 1.0]; // Default normal
306            MeshVertex::new([p.x, p.y, p.z], normal, [0.0, 0.0], color)
307        }).collect();
308        
309        // Create indices for point rendering (each point is a degenerate triangle)
310        let indices: Vec<u32> = (0..vertices.len() as u32).collect();
311        
312        Self::new(vertices, indices, PbrMaterial::default())
313    }
314}
315
316/// Shading mode for mesh rendering
317#[derive(Debug, Clone, Copy, PartialEq)]
318pub enum ShadingMode {
319    Flat,
320    Pbr,
321}
322
323/// GPU-accelerated mesh renderer with PBR and flat shading
324pub struct MeshRenderer<'window> {
325    pub gpu_context: GpuContext,
326    pub surface: wgpu::Surface<'window>,
327    pub surface_config: wgpu::SurfaceConfiguration,
328    pub pbr_pipeline: wgpu::RenderPipeline,
329    pub flat_pipeline: wgpu::RenderPipeline,
330    /// Single-sample PBR pipeline used for screenshot capture (no MSAA)
331    pub screenshot_pbr_pipeline: wgpu::RenderPipeline,
332    /// Single-sample flat pipeline used for screenshot capture (no MSAA)
333    pub screenshot_flat_pipeline: wgpu::RenderPipeline,
334    pub camera_uniform: MeshCameraUniform,
335    pub camera_buffer: wgpu::Buffer,
336    pub lighting_params: MeshLightingParams,
337    pub lighting_buffer: wgpu::Buffer,
338    pub bind_group_layout: wgpu::BindGroupLayout,
339    pub config: MeshRenderConfig,
340    pub msaa_texture: Option<wgpu::Texture>,
341    pub msaa_view: Option<wgpu::TextureView>,
342}
343
344impl<'window> MeshRenderer<'window> {
345    /// Create new mesh renderer with PBR and flat shading support
346    pub async fn new(window: &'window Window, config: MeshRenderConfig) -> Result<Self> {
347        let gpu_context = GpuContext::new().await?;
348        
349        let surface = gpu_context.instance.create_surface(window)
350            .map_err(|e| Error::Gpu(format!("Failed to create surface: {:?}", e)))?;
351
352        let surface_caps = surface.get_capabilities(&gpu_context.adapter);
353        let surface_format = surface_caps.formats.iter()
354            .copied()
355            .find(|f| f.is_srgb())
356            .unwrap_or(surface_caps.formats[0]);
357
358        let size = window.inner_size();
359        let sample_count = if config.enable_multisampling { 4 } else { 1 };
360        
361        let surface_config = wgpu::SurfaceConfiguration {
362            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
363            format: surface_format,
364            width: size.width,
365            height: size.height,
366            present_mode: surface_caps.present_modes[0],
367            alpha_mode: surface_caps.alpha_modes[0],
368            view_formats: vec![],
369            desired_maximum_frame_latency: 2,
370        };
371        surface.configure(&gpu_context.device, &surface_config);
372
373        // Create MSAA texture if enabled
374        let (msaa_texture, msaa_view) = if config.enable_multisampling {
375            let msaa_texture = gpu_context.device.create_texture(&wgpu::TextureDescriptor {
376                label: Some("MSAA Texture"),
377                size: wgpu::Extent3d {
378                    width: size.width,
379                    height: size.height,
380                    depth_or_array_layers: 1,
381                },
382                mip_level_count: 1,
383                sample_count,
384                dimension: wgpu::TextureDimension::D2,
385                format: surface_format,
386                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
387                view_formats: &[],
388            });
389            let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
390            (Some(msaa_texture), Some(msaa_view))
391        } else {
392            (None, None)
393        };
394
395        // Create camera uniform
396        let camera_uniform = MeshCameraUniform {
397            view_proj: Matrix4::identity().into(),
398            view_pos: [0.0, 0.0, 0.0],
399            _padding: 0.0,
400        };
401
402        let camera_buffer = gpu_context.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
403            label: Some("Camera Buffer"),
404            contents: bytemuck::bytes_of(&camera_uniform),
405            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
406        });
407
408        // Create lighting parameters buffer
409        let lighting_params = config.lighting_params;
410        let lighting_buffer = gpu_context.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
411            label: Some("Lighting Buffer"),
412            contents: bytemuck::bytes_of(&lighting_params),
413            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
414        });
415
416        // Create bind group layout
417        let bind_group_layout = gpu_context.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
418            entries: &[
419                wgpu::BindGroupLayoutEntry {
420                    binding: 0,
421                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
422                    ty: wgpu::BindingType::Buffer {
423                        ty: wgpu::BufferBindingType::Uniform,
424                        has_dynamic_offset: false,
425                        min_binding_size: None,
426                    },
427                    count: None,
428                },
429                wgpu::BindGroupLayoutEntry {
430                    binding: 1,
431                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
432                    ty: wgpu::BindingType::Buffer {
433                        ty: wgpu::BufferBindingType::Uniform,
434                        has_dynamic_offset: false,
435                        min_binding_size: None,
436                    },
437                    count: None,
438                },
439                wgpu::BindGroupLayoutEntry {
440                    binding: 2,
441                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
442                    ty: wgpu::BindingType::Buffer {
443                        ty: wgpu::BufferBindingType::Uniform,
444                        has_dynamic_offset: false,
445                        min_binding_size: None,
446                    },
447                    count: None,
448                },
449            ],
450            label: Some("mesh_bind_group_layout"),
451        });
452
453        // Create PBR pipeline
454        let pbr_shader = gpu_context.device.create_shader_module(wgpu::ShaderModuleDescriptor {
455            label: Some("PBR Mesh Shader"),
456            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/mesh_pbr.wgsl").into()),
457        });
458
459        let pbr_pipeline = Self::create_render_pipeline(
460            &gpu_context.device,
461            &bind_group_layout,
462            &pbr_shader,
463            surface_format,
464            sample_count,
465            &config,
466            "PBR",
467        );
468
469        // Create flat pipeline
470        let flat_shader = gpu_context.device.create_shader_module(wgpu::ShaderModuleDescriptor {
471            label: Some("Flat Mesh Shader"),
472            source: wgpu::ShaderSource::Wgsl(include_str!("shaders/mesh_flat.wgsl").into()),
473        });
474
475        let flat_pipeline = Self::create_render_pipeline(
476            &gpu_context.device,
477            &bind_group_layout,
478            &flat_shader,
479            surface_format,
480            sample_count,
481            &config,
482            "Flat",
483        );
484
485        // Screenshot pipelines always use sample_count=1 (no MSAA)
486        let screenshot_pbr_pipeline = Self::create_render_pipeline(
487            &gpu_context.device,
488            &bind_group_layout,
489            &pbr_shader,
490            surface_format,
491            1,
492            &config,
493            "Screenshot PBR",
494        );
495
496        let screenshot_flat_pipeline = Self::create_render_pipeline(
497            &gpu_context.device,
498            &bind_group_layout,
499            &flat_shader,
500            surface_format,
501            1,
502            &config,
503            "Screenshot Flat",
504        );
505
506        Ok(Self {
507            gpu_context,
508            surface,
509            surface_config,
510            pbr_pipeline,
511            flat_pipeline,
512            screenshot_pbr_pipeline,
513            screenshot_flat_pipeline,
514            camera_uniform,
515            camera_buffer,
516            lighting_params,
517            lighting_buffer,
518            bind_group_layout,
519            config,
520            msaa_texture,
521            msaa_view,
522        })
523    }
524
525    /// Create a render pipeline for mesh rendering
526    fn create_render_pipeline(
527        device: &wgpu::Device,
528        bind_group_layout: &wgpu::BindGroupLayout,
529        shader: &wgpu::ShaderModule,
530        surface_format: wgpu::TextureFormat,
531        sample_count: u32,
532        config: &MeshRenderConfig,
533        label: &str,
534    ) -> wgpu::RenderPipeline {
535        let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
536            label: Some(&format!("{} Mesh Render Pipeline Layout", label)),
537            bind_group_layouts: &[Some(bind_group_layout)],
538            immediate_size: 0,
539        });
540
541        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
542            label: Some(&format!("{} Mesh Render Pipeline", label)),
543            layout: Some(&render_pipeline_layout),
544            vertex: wgpu::VertexState {
545                module: shader,
546                entry_point: Some("vs_main"),
547                buffers: &[MeshVertex::desc()],
548                compilation_options: wgpu::PipelineCompilationOptions::default(),
549            },
550            fragment: Some(wgpu::FragmentState {
551                module: shader,
552                entry_point: Some("fs_main"),
553                targets: &[Some(wgpu::ColorTargetState {
554                    format: surface_format,
555                    blend: Some(wgpu::BlendState::REPLACE),
556                    write_mask: wgpu::ColorWrites::ALL,
557                })],
558                compilation_options: wgpu::PipelineCompilationOptions::default(),
559            }),
560            primitive: wgpu::PrimitiveState {
561                topology: if config.wireframe_mode {
562                    wgpu::PrimitiveTopology::LineList
563                } else {
564                    wgpu::PrimitiveTopology::TriangleList
565                },
566                strip_index_format: None,
567                front_face: wgpu::FrontFace::Ccw,
568                cull_mode: if config.enable_backface_culling {
569                    Some(wgpu::Face::Back)
570                } else {
571                    None
572                },
573                unclipped_depth: false,
574                polygon_mode: wgpu::PolygonMode::Fill,
575                conservative: false,
576            },
577            depth_stencil: if config.enable_depth_test {
578                Some(wgpu::DepthStencilState {
579                    format: wgpu::TextureFormat::Depth32Float,
580                    depth_write_enabled: Some(true),
581                    depth_compare: Some(wgpu::CompareFunction::Less),
582                    stencil: wgpu::StencilState::default(),
583                    bias: wgpu::DepthBiasState::default(),
584                })
585            } else {
586                None
587            },
588            multisample: wgpu::MultisampleState {
589                count: sample_count,
590                mask: !0,
591                alpha_to_coverage_enabled: false,
592            },
593            multiview_mask: None,
594            cache: None,
595        })
596    }
597
598    /// Update camera matrices and position
599    pub fn update_camera(&mut self, view_matrix: Matrix4<f32>, proj_matrix: Matrix4<f32>, camera_pos: Vector3<f32>) {
600        self.camera_uniform.view_proj = (proj_matrix * view_matrix).into();
601        self.camera_uniform.view_pos = camera_pos.into();
602        
603        self.gpu_context.queue.write_buffer(
604            &self.camera_buffer,
605            0,
606            bytemuck::bytes_of(&self.camera_uniform),
607        );
608    }
609
610    /// Update lighting parameters
611    pub fn update_lighting(&mut self, params: MeshLightingParams) {
612        self.lighting_params = params;
613        self.gpu_context.queue.write_buffer(
614            &self.lighting_buffer,
615            0,
616            bytemuck::bytes_of(&self.lighting_params),
617        );
618    }
619
620    /// Resize renderer
621    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
622        if new_size.width > 0 && new_size.height > 0 {
623            self.surface_config.width = new_size.width;
624            self.surface_config.height = new_size.height;
625            self.surface.configure(&self.gpu_context.device, &self.surface_config);
626            
627            // Recreate MSAA texture if needed
628            if self.config.enable_multisampling {
629                let msaa_texture = self.gpu_context.device.create_texture(&wgpu::TextureDescriptor {
630                    label: Some("MSAA Texture"),
631                    size: wgpu::Extent3d {
632                        width: new_size.width,
633                        height: new_size.height,
634                        depth_or_array_layers: 1,
635                    },
636                    mip_level_count: 1,
637                    sample_count: 4,
638                    dimension: wgpu::TextureDimension::D2,
639                    format: self.surface_config.format,
640                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
641                    view_formats: &[],
642                });
643                let msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
644                self.msaa_texture = Some(msaa_texture);
645                self.msaa_view = Some(msaa_view);
646            }
647        }
648    }
649
650    /// Create vertex buffer
651    pub fn create_vertex_buffer(&self, vertices: &[MeshVertex]) -> wgpu::Buffer {
652        self.gpu_context.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
653            label: Some("Mesh Vertex Buffer"),
654            contents: bytemuck::cast_slice(vertices),
655            usage: wgpu::BufferUsages::VERTEX,
656        })
657    }
658
659    /// Create index buffer
660    pub fn create_index_buffer(&self, indices: &[u32]) -> wgpu::Buffer {
661        self.gpu_context.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
662            label: Some("Mesh Index Buffer"),
663            contents: bytemuck::cast_slice(indices),
664            usage: wgpu::BufferUsages::INDEX,
665        })
666    }
667
668    /// Create depth texture
669    pub fn create_depth_texture(&self) -> wgpu::Texture {
670        let sample_count = if self.config.enable_multisampling { 4 } else { 1 };
671        
672        self.gpu_context.device.create_texture(&wgpu::TextureDescriptor {
673            label: Some("Depth Texture"),
674            size: wgpu::Extent3d {
675                width: self.surface_config.width,
676                height: self.surface_config.height,
677                depth_or_array_layers: 1,
678            },
679            mip_level_count: 1,
680            sample_count,
681            dimension: wgpu::TextureDimension::D2,
682            format: wgpu::TextureFormat::Depth32Float,
683            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
684            view_formats: &[],
685        })
686    }
687
688    /// Render mesh with specified shading mode
689    pub fn render(&self, mesh: &GpuMesh, shading_mode: ShadingMode) -> Result<()> {
690        let vertex_buffer = self.create_vertex_buffer(&mesh.vertices);
691        let index_buffer = self.create_index_buffer(&mesh.indices);
692        let depth_texture = self.create_depth_texture();
693        let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
694
695        let output = match self.surface.get_current_texture() {
696            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
697            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
698            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return Ok(()),
699            _ => return Err(Error::Gpu("Failed to get surface texture".to_string())),
700        };
701        
702        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
703
704        // Create material buffer based on shading mode
705        let material_buffer = match shading_mode {
706            ShadingMode::Pbr => {
707                self.gpu_context.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
708                    label: Some("PBR Material Buffer"),
709                    contents: bytemuck::bytes_of(&mesh.material),
710                    usage: wgpu::BufferUsages::UNIFORM,
711                })
712            }
713            ShadingMode::Flat => {
714                let flat_material = FlatMaterial {
715                    color: mesh.material.albedo,
716                    _padding: 0.0,
717                };
718                self.gpu_context.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
719                    label: Some("Flat Material Buffer"),
720                    contents: bytemuck::bytes_of(&flat_material),
721                    usage: wgpu::BufferUsages::UNIFORM,
722                })
723            }
724        };
725
726        let bind_group = self.gpu_context.device.create_bind_group(&wgpu::BindGroupDescriptor {
727            layout: &self.bind_group_layout,
728            entries: &[
729                wgpu::BindGroupEntry {
730                    binding: 0,
731                    resource: self.camera_buffer.as_entire_binding(),
732                },
733                wgpu::BindGroupEntry {
734                    binding: 1,
735                    resource: material_buffer.as_entire_binding(),
736                },
737                wgpu::BindGroupEntry {
738                    binding: 2,
739                    resource: self.lighting_buffer.as_entire_binding(),
740                },
741            ],
742            label: Some("mesh_bind_group"),
743        });
744
745        let mut encoder = self.gpu_context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
746            label: Some("Mesh Render Encoder"),
747        });
748
749        // Determine render target
750        let (color_attachment, resolve_target) = if let Some(ref msaa_view) = self.msaa_view {
751            (msaa_view, Some(&view))
752        } else {
753            (&view, None)
754        };
755
756        {
757            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
758                label: Some("Mesh Render Pass"),
759                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
760                    view: color_attachment,
761                    resolve_target,
762                    ops: wgpu::Operations {
763                        load: wgpu::LoadOp::Clear(wgpu::Color {
764                            r: self.config.background_color[0],
765                            g: self.config.background_color[1],
766                            b: self.config.background_color[2],
767                            a: self.config.background_color[3],
768                        }),
769                        store: wgpu::StoreOp::Store,
770                    },
771                    depth_slice: None,
772                })],
773                depth_stencil_attachment: if self.config.enable_depth_test {
774                    Some(wgpu::RenderPassDepthStencilAttachment {
775                        view: &depth_view,
776                        depth_ops: Some(wgpu::Operations {
777                            load: wgpu::LoadOp::Clear(1.0),
778                            store: wgpu::StoreOp::Store,
779                        }),
780                        stencil_ops: None,
781                    })
782                } else {
783                    None
784                },
785                timestamp_writes: None,
786                occlusion_query_set: None,
787                multiview_mask: None,
788            });
789
790            let pipeline = match shading_mode {
791                ShadingMode::Pbr => &self.pbr_pipeline,
792                ShadingMode::Flat => &self.flat_pipeline,
793            };
794
795            render_pass.set_pipeline(pipeline);
796            render_pass.set_bind_group(0, &bind_group, &[]);
797            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
798            render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
799            render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
800        }
801
802        self.gpu_context.queue.submit(std::iter::once(encoder.finish()));
803        output.present();
804
805        Ok(())
806    }
807
808    /// Render mesh to an offscreen texture and return raw RGBA pixel bytes.
809    ///
810    /// Returns `(pixels, format, width, height)`.  The caller is responsible
811    /// for interpreting the format (BGRA vs RGBA) when encoding the image.
812    pub fn render_to_texture(
813        &self,
814        mesh: &GpuMesh,
815        shading_mode: ShadingMode,
816    ) -> Result<(Vec<u8>, wgpu::TextureFormat, u32, u32)> {
817        let width = self.surface_config.width;
818        let height = self.surface_config.height;
819        let format = self.surface_config.format;
820
821        // Offscreen render target (no MSAA, COPY_SRC so we can read it back)
822        let render_texture = self.gpu_context.device.create_texture(&wgpu::TextureDescriptor {
823            label: Some("Screenshot Render Texture"),
824            size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
825            mip_level_count: 1,
826            sample_count: 1,
827            dimension: wgpu::TextureDimension::D2,
828            format,
829            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
830            view_formats: &[],
831        });
832        let render_view = render_texture.create_view(&wgpu::TextureViewDescriptor::default());
833
834        let depth_texture = self.gpu_context.device.create_texture(&wgpu::TextureDescriptor {
835            label: Some("Screenshot Depth Texture"),
836            size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
837            mip_level_count: 1,
838            sample_count: 1,
839            dimension: wgpu::TextureDimension::D2,
840            format: wgpu::TextureFormat::Depth32Float,
841            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
842            view_formats: &[],
843        });
844        let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
845
846        let vertex_buffer = self.create_vertex_buffer(&mesh.vertices);
847        let index_buffer = self.create_index_buffer(&mesh.indices);
848
849        let material_buffer = match shading_mode {
850            ShadingMode::Pbr => self.gpu_context.device.create_buffer_init(
851                &wgpu::util::BufferInitDescriptor {
852                    label: Some("Screenshot PBR Material Buffer"),
853                    contents: bytemuck::bytes_of(&mesh.material),
854                    usage: wgpu::BufferUsages::UNIFORM,
855                },
856            ),
857            ShadingMode::Flat => {
858                let flat_material = FlatMaterial { color: mesh.material.albedo, _padding: 0.0 };
859                self.gpu_context.device.create_buffer_init(
860                    &wgpu::util::BufferInitDescriptor {
861                        label: Some("Screenshot Flat Material Buffer"),
862                        contents: bytemuck::bytes_of(&flat_material),
863                        usage: wgpu::BufferUsages::UNIFORM,
864                    },
865                )
866            }
867        };
868
869        let bind_group = self.gpu_context.device.create_bind_group(&wgpu::BindGroupDescriptor {
870            layout: &self.bind_group_layout,
871            entries: &[
872                wgpu::BindGroupEntry { binding: 0, resource: self.camera_buffer.as_entire_binding() },
873                wgpu::BindGroupEntry { binding: 1, resource: material_buffer.as_entire_binding() },
874                wgpu::BindGroupEntry { binding: 2, resource: self.lighting_buffer.as_entire_binding() },
875            ],
876            label: Some("screenshot_bind_group"),
877        });
878
879        // wgpu requires bytes_per_row to be aligned to COPY_BYTES_PER_ROW_ALIGNMENT (256)
880        let bytes_per_pixel = 4u32;
881        let unpadded_bytes_per_row = width * bytes_per_pixel;
882        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
883        let padded_bytes_per_row = (unpadded_bytes_per_row + align - 1) / align * align;
884
885        let staging_buffer = self.gpu_context.device.create_buffer(&wgpu::BufferDescriptor {
886            label: Some("Screenshot Staging Buffer"),
887            size: (padded_bytes_per_row * height) as wgpu::BufferAddress,
888            usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
889            mapped_at_creation: false,
890        });
891
892        let mut encoder = self.gpu_context.device.create_command_encoder(
893            &wgpu::CommandEncoderDescriptor { label: Some("Screenshot Encoder") },
894        );
895
896        {
897            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
898                label: Some("Screenshot Render Pass"),
899                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
900                    view: &render_view,
901                    resolve_target: None,
902                    ops: wgpu::Operations {
903                        load: wgpu::LoadOp::Clear(wgpu::Color {
904                            r: self.config.background_color[0],
905                            g: self.config.background_color[1],
906                            b: self.config.background_color[2],
907                            a: self.config.background_color[3],
908                        }),
909                        store: wgpu::StoreOp::Store,
910                    },
911                    depth_slice: None,
912                })],
913                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
914                    view: &depth_view,
915                    depth_ops: Some(wgpu::Operations {
916                        load: wgpu::LoadOp::Clear(1.0),
917                        store: wgpu::StoreOp::Store,
918                    }),
919                    stencil_ops: None,
920                }),
921                timestamp_writes: None,
922                occlusion_query_set: None,
923                multiview_mask: None,
924            });
925
926            let pipeline = match shading_mode {
927                ShadingMode::Pbr => &self.screenshot_pbr_pipeline,
928                ShadingMode::Flat => &self.screenshot_flat_pipeline,
929            };
930
931            render_pass.set_pipeline(pipeline);
932            render_pass.set_bind_group(0, &bind_group, &[]);
933            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
934            render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
935            render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
936        }
937
938        // Copy rendered texture into staging buffer for CPU readback
939        encoder.copy_texture_to_buffer(
940            wgpu::TexelCopyTextureInfo {
941                texture: &render_texture,
942                mip_level: 0,
943                origin: wgpu::Origin3d::ZERO,
944                aspect: wgpu::TextureAspect::All,
945            },
946            wgpu::TexelCopyBufferInfo {
947                buffer: &staging_buffer,
948                layout: wgpu::TexelCopyBufferLayout {
949                    offset: 0,
950                    bytes_per_row: Some(padded_bytes_per_row),
951                    rows_per_image: None,
952                },
953            },
954            wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
955        );
956
957        self.gpu_context.queue.submit(std::iter::once(encoder.finish()));
958
959        // Map the buffer and wait for the GPU to finish
960        let buffer_slice = staging_buffer.slice(..);
961        let (tx, rx) = std::sync::mpsc::channel::<std::result::Result<(), wgpu::BufferAsyncError>>();
962        buffer_slice.map_async(wgpu::MapMode::Read, move |v| { let _ = tx.send(v); });
963        self.gpu_context.device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
964        rx.recv()
965            .map_err(|_| Error::Gpu("Screenshot buffer map channel error".to_string()))?
966            .map_err(|e| Error::Gpu(format!("Screenshot buffer map error: {:?}", e)))?;
967
968        let data = buffer_slice.get_mapped_range();
969
970        // Strip row padding before returning
971        let mut pixels: Vec<u8> = Vec::with_capacity((unpadded_bytes_per_row * height) as usize);
972        for row in 0..height as usize {
973            let start = row * padded_bytes_per_row as usize;
974            let end = start + unpadded_bytes_per_row as usize;
975            pixels.extend_from_slice(&data[start..end]);
976        }
977        drop(data);
978        staging_buffer.unmap();
979
980        Ok((pixels, format, width, height))
981    }
982}
983
984/// Convert threecrate mesh to GPU mesh format
985pub fn mesh_to_gpu_mesh(
986    vertices: &[Point3<f32>],
987    indices: &[u32],
988    normals: Option<&[Vector3<f32>]>,
989    colors: Option<&[[f32; 3]]>,
990    material: Option<PbrMaterial>,
991) -> GpuMesh {
992    let gpu_vertices: Vec<MeshVertex> = vertices
993        .iter()
994        .enumerate()
995        .map(|(i, vertex)| {
996            let normal = normals
997                .and_then(|n| n.get(i))
998                .map(|n| [n.x, n.y, n.z])
999                .unwrap_or([0.0, 0.0, 1.0]);
1000            
1001            let color = colors
1002                .and_then(|c| c.get(i))
1003                .copied()
1004                .unwrap_or([0.8, 0.8, 0.8]);
1005            
1006            MeshVertex::new([vertex.x, vertex.y, vertex.z], normal, [0.0, 0.0], color)
1007        })
1008        .collect();
1009
1010    GpuMesh::new(
1011        gpu_vertices,
1012        indices.to_vec(),
1013        material.unwrap_or_default(),
1014    )
1015}
1016
1017/// Pre-computed LOD levels for a mesh, generated from a progressive mesh.
1018///
1019/// Levels are ordered from coarsest (index 0) to finest (last index).
1020/// Use `select_level` to pick the appropriate LOD for a given camera distance.
1021pub struct LodMesh {
1022    /// GPU meshes at each LOD level, index 0 = coarsest
1023    pub levels: Vec<GpuMesh>,
1024    /// Distance thresholds for switching between levels.
1025    /// `thresholds[i]` is the maximum distance at which `levels[i+1]` should be used.
1026    pub thresholds: Vec<f32>,
1027}
1028
1029impl LodMesh {
1030    /// Create LOD levels by sampling the progressive mesh at evenly-spaced detail ratios.
1031    ///
1032    /// `num_levels` is the total number of LOD levels to generate (minimum 2).
1033    pub fn from_progressive_mesh(pm: &ProgressiveMesh, num_levels: usize) -> Self {
1034        let num_levels = num_levels.max(2);
1035
1036        let levels: Vec<GpuMesh> = (0..num_levels)
1037            .map(|i| {
1038                let ratio = i as f32 / (num_levels - 1) as f32;
1039                let mesh = pm.reconstruct_at_ratio(ratio);
1040                triangle_mesh_to_gpu_mesh(&mesh)
1041            })
1042            .collect();
1043
1044        // Generate default distance thresholds (evenly spaced)
1045        let thresholds: Vec<f32> = (0..num_levels.saturating_sub(1))
1046            .map(|i| {
1047                let t = (num_levels - 1 - i) as f32 / (num_levels - 1) as f32;
1048                t * 100.0
1049            })
1050            .collect();
1051
1052        LodMesh { levels, thresholds }
1053    }
1054
1055    /// Select the appropriate LOD level for a given camera distance.
1056    ///
1057    /// Returns the coarsest mesh that still looks acceptable at the given distance.
1058    /// Closer distances get finer detail; farther distances get coarser meshes.
1059    pub fn select_level(&self, distance: f32) -> &GpuMesh {
1060        for (i, &threshold) in self.thresholds.iter().enumerate() {
1061            if distance > threshold {
1062                return &self.levels[i];
1063            }
1064        }
1065        // Closest distance: return finest level
1066        self.levels.last().unwrap_or(&self.levels[0])
1067    }
1068
1069    /// Number of LOD levels.
1070    pub fn num_levels(&self) -> usize {
1071        self.levels.len()
1072    }
1073}
1074
1075/// Convert a `TriangleMesh` to a `GpuMesh` with default material.
1076fn triangle_mesh_to_gpu_mesh(mesh: &threecrate_core::TriangleMesh) -> GpuMesh {
1077    let normals_slice = mesh.normals.as_deref();
1078    let colors_f32: Option<Vec<[f32; 3]>> = mesh.colors.as_ref().map(|colors| {
1079        colors
1080            .iter()
1081            .map(|c| [c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0])
1082            .collect()
1083    });
1084
1085    let indices: Vec<u32> = mesh
1086        .faces
1087        .iter()
1088        .flat_map(|f| [f[0] as u32, f[1] as u32, f[2] as u32])
1089        .collect();
1090
1091    mesh_to_gpu_mesh(
1092        &mesh.vertices,
1093        &indices,
1094        normals_slice,
1095        colors_f32.as_deref(),
1096        None,
1097    )
1098}