Skip to main content

polyscope_render/engine/
mod.rs

1//! The main rendering engine.
2
3mod pick;
4mod pipelines;
5mod postprocessing;
6mod rendering;
7mod textures;
8
9use std::collections::HashMap;
10use std::num::NonZeroU64;
11use std::sync::Arc;
12
13use wgpu::util::DeviceExt;
14
15use polyscope_core::slice_plane::{MAX_SLICE_PLANES, SlicePlaneUniforms};
16
17use crate::camera::Camera;
18use crate::color_maps::ColorMapRegistry;
19use crate::error::{RenderError, RenderResult};
20use crate::ground_plane::GroundPlaneRenderData;
21use crate::materials::{self, MatcapTextureSet, Material, MaterialRegistry};
22use crate::shadow_map::ShadowMapPass;
23use crate::slice_plane_render::SlicePlaneRenderData;
24use crate::tone_mapping::ToneMapPass;
25
26/// Camera uniforms for GPU.
27///
28/// `is_orthographic` is encoded as a float (0.0 = perspective, 1.0 = ortho) so that
29/// shaders aliasing `camera_pos` as `vec4<f32>` can read it as the `.w` component
30/// without type punning. Ray-cast primitive shaders (curve tubes) branch on this to
31/// emit parallel rays in ortho mode instead of perspective rays.
32#[repr(C)]
33#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
34pub struct CameraUniforms {
35    pub view: [[f32; 4]; 4],
36    pub proj: [[f32; 4]; 4],
37    pub view_proj: [[f32; 4]; 4],
38    pub inv_proj: [[f32; 4]; 4],
39    pub camera_pos: [f32; 3],
40    pub is_orthographic: f32,
41}
42
43impl Default for CameraUniforms {
44    fn default() -> Self {
45        Self {
46            view: glam::Mat4::IDENTITY.to_cols_array_2d(),
47            proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
48            view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
49            inv_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
50            camera_pos: [0.0, 0.0, 5.0],
51            is_orthographic: 0.0,
52        }
53    }
54}
55
56impl CameraUniforms {
57    /// Packs a `Camera` into the GPU uniform layout. Single source of truth for
58    /// matrix derivation and the `is_orthographic` flag — `update_camera_uniforms`
59    /// uses this, and tests assert against this directly to catch field drift.
60    #[must_use]
61    pub fn from_camera(camera: &crate::camera::Camera) -> Self {
62        let view = camera.view_matrix();
63        let proj = camera.projection_matrix();
64        let view_proj = proj * view;
65        let inv_proj = proj.inverse();
66        let is_orthographic = match camera.projection_mode {
67            crate::camera::ProjectionMode::Orthographic => 1.0,
68            crate::camera::ProjectionMode::Perspective => 0.0,
69        };
70        Self {
71            view: view.to_cols_array_2d(),
72            proj: proj.to_cols_array_2d(),
73            view_proj: view_proj.to_cols_array_2d(),
74            inv_proj: inv_proj.to_cols_array_2d(),
75            camera_pos: camera.position.to_array(),
76            is_orthographic,
77        }
78    }
79}
80
81/// The main rendering engine backed by wgpu.
82pub struct RenderEngine {
83    /// The wgpu instance.
84    pub instance: wgpu::Instance,
85    /// The wgpu adapter.
86    pub adapter: wgpu::Adapter,
87    /// The wgpu device.
88    pub device: wgpu::Device,
89    /// The wgpu queue.
90    pub queue: wgpu::Queue,
91    /// The render surface (None for headless).
92    pub surface: Option<wgpu::Surface<'static>>,
93    /// Surface configuration.
94    pub surface_config: wgpu::SurfaceConfiguration,
95    /// Depth texture.
96    pub depth_texture: wgpu::Texture,
97    /// Depth texture view.
98    pub depth_view: wgpu::TextureView,
99    /// Depth-only texture view (for SSAO sampling, excludes stencil aspect).
100    pub(crate) depth_only_view: wgpu::TextureView,
101    /// Material registry.
102    pub materials: MaterialRegistry,
103    /// Color map registry.
104    pub color_maps: ColorMapRegistry,
105    /// Matcap bind group layout (Group 2: 4 textures + 1 sampler).
106    pub matcap_bind_group_layout: wgpu::BindGroupLayout,
107    /// Matcap texture sets keyed by material name.
108    pub matcap_textures: HashMap<String, MatcapTextureSet>,
109    /// Main camera.
110    pub camera: Camera,
111    /// Current viewport width.
112    pub width: u32,
113    /// Current viewport height.
114    pub height: u32,
115    /// Point cloud render pipeline.
116    pub point_pipeline: Option<wgpu::RenderPipeline>,
117    /// Point cloud bind group layout.
118    pub point_bind_group_layout: Option<wgpu::BindGroupLayout>,
119    /// Camera uniform buffer.
120    pub camera_buffer: wgpu::Buffer,
121    /// Slice plane uniform buffer.
122    pub slice_plane_buffer: wgpu::Buffer,
123    /// Slice plane bind group layout (shared by all structure shaders).
124    pub slice_plane_bind_group_layout: wgpu::BindGroupLayout,
125    /// Slice plane bind group (updated each frame).
126    pub slice_plane_bind_group: wgpu::BindGroup,
127    /// Vector arrow render pipeline.
128    pub vector_pipeline: Option<wgpu::RenderPipeline>,
129    /// Vector bind group layout.
130    pub vector_bind_group_layout: Option<wgpu::BindGroupLayout>,
131    /// Surface mesh render pipeline (alpha blending, depth write enabled).
132    pub mesh_pipeline: Option<wgpu::RenderPipeline>,
133    /// Surface mesh depth/normal-only pipeline (Pretty mode prepass).
134    pub mesh_depth_normal_pipeline: Option<wgpu::RenderPipeline>,
135    /// Mesh bind group layout.
136    pub(crate) mesh_bind_group_layout: Option<wgpu::BindGroupLayout>,
137    /// Curve network edge render pipeline (line rendering).
138    pub curve_network_edge_pipeline: Option<wgpu::RenderPipeline>,
139    /// Curve network edge bind group layout.
140    pub(crate) curve_network_edge_bind_group_layout: Option<wgpu::BindGroupLayout>,
141    /// Curve network tube render pipeline (cylinder impostor rendering).
142    pub curve_network_tube_pipeline: Option<wgpu::RenderPipeline>,
143    /// Curve network tube compute pipeline (generates bounding box geometry).
144    pub curve_network_tube_compute_pipeline: Option<wgpu::ComputePipeline>,
145    /// Curve network tube render bind group layout.
146    pub(crate) curve_network_tube_bind_group_layout: Option<wgpu::BindGroupLayout>,
147    /// Curve network tube compute bind group layout.
148    pub(crate) curve_network_tube_compute_bind_group_layout: Option<wgpu::BindGroupLayout>,
149    /// Ground plane render pipeline.
150    pub(crate) ground_plane_pipeline: wgpu::RenderPipeline,
151    /// Ground plane bind group layout.
152    pub(crate) ground_plane_bind_group_layout: wgpu::BindGroupLayout,
153    /// Ground plane render data (lazily initialized).
154    pub(crate) ground_plane_render_data: Option<GroundPlaneRenderData>,
155    /// Slice plane visualization pipeline.
156    pub(crate) slice_plane_vis_pipeline: wgpu::RenderPipeline,
157    /// Slice plane visualization bind group layout.
158    pub(crate) slice_plane_vis_bind_group_layout: wgpu::BindGroupLayout,
159    /// Slice plane render data (per-plane, lazily initialized).
160    pub(crate) slice_plane_render_data: Vec<SlicePlaneRenderData>,
161    /// Screenshot capture texture (lazily initialized).
162    pub(crate) screenshot_texture: Option<wgpu::Texture>,
163    /// Screenshot capture buffer (lazily initialized).
164    pub(crate) screenshot_buffer: Option<wgpu::Buffer>,
165    /// Screenshot HDR texture for rendering (lazily initialized).
166    pub(crate) screenshot_hdr_texture: Option<wgpu::Texture>,
167    /// Screenshot HDR texture view.
168    pub(crate) screenshot_hdr_view: Option<wgpu::TextureView>,
169    /// HDR intermediate texture for tone mapping.
170    pub(crate) hdr_texture: Option<wgpu::Texture>,
171    /// HDR texture view.
172    pub(crate) hdr_view: Option<wgpu::TextureView>,
173    /// Normal G-buffer texture for SSAO.
174    pub(crate) normal_texture: Option<wgpu::Texture>,
175    /// Normal G-buffer texture view.
176    pub(crate) normal_view: Option<wgpu::TextureView>,
177    /// SSAO noise texture (4x4 random rotation vectors).
178    pub(crate) ssao_noise_texture: Option<wgpu::Texture>,
179    /// SSAO noise texture view.
180    pub(crate) ssao_noise_view: Option<wgpu::TextureView>,
181    /// SSAO pass.
182    pub(crate) ssao_pass: Option<crate::ssao_pass::SsaoPass>,
183    /// SSAO output texture (blurred result).
184    pub(crate) ssao_output_texture: Option<wgpu::Texture>,
185    /// SSAO output texture view.
186    pub(crate) ssao_output_view: Option<wgpu::TextureView>,
187    /// Depth peeling transparency pass.
188    pub(crate) depth_peel_pass: Option<crate::depth_peel_pass::DepthPeelPass>,
189    /// Tone mapping post-processing pass.
190    pub(crate) tone_map_pass: Option<ToneMapPass>,
191    /// SSAA (supersampling) pass for anti-aliasing.
192    pub(crate) ssaa_pass: Option<crate::ssaa_pass::SsaaPass>,
193    /// Current SSAA factor (1 = off, 2 = 2x, 4 = 4x).
194    pub(crate) ssaa_factor: u32,
195    /// Intermediate HDR texture for SSAA (screen resolution, used after downsampling).
196    pub(crate) ssaa_intermediate_texture: Option<wgpu::Texture>,
197    /// Intermediate HDR texture view.
198    pub(crate) ssaa_intermediate_view: Option<wgpu::TextureView>,
199    /// Shadow map pass for ground plane shadows.
200    pub(crate) shadow_map_pass: Option<ShadowMapPass>,
201    /// Shadow render pipeline (depth-only, renders objects from light's perspective).
202    pub(crate) shadow_pipeline: Option<wgpu::RenderPipeline>,
203    /// Shadow bind group layout for shadow pass rendering.
204    pub(crate) shadow_bind_group_layout: Option<wgpu::BindGroupLayout>,
205    /// Reflection pass for ground plane reflections.
206    pub(crate) reflection_pass: Option<crate::reflection_pass::ReflectionPass>,
207    /// Stencil pipeline for ground plane reflection mask.
208    pub(crate) ground_stencil_pipeline: Option<wgpu::RenderPipeline>,
209    /// Pipeline for rendering reflected surface meshes.
210    pub(crate) reflected_mesh_pipeline: Option<wgpu::RenderPipeline>,
211    /// Bind group layout for reflected mesh (includes reflection uniforms).
212    pub(crate) reflected_mesh_bind_group_layout: Option<wgpu::BindGroupLayout>,
213    /// Pipeline for rendering reflected point clouds.
214    pub(crate) reflected_point_cloud_pipeline: Option<wgpu::RenderPipeline>,
215    /// Bind group layout for reflected point cloud.
216    pub(crate) reflected_point_cloud_bind_group_layout: Option<wgpu::BindGroupLayout>,
217    /// Pipeline for rendering reflected curve networks.
218    pub(crate) reflected_curve_network_pipeline: Option<wgpu::RenderPipeline>,
219    /// Bind group layout for reflected curve network.
220    pub(crate) reflected_curve_network_bind_group_layout: Option<wgpu::BindGroupLayout>,
221    /// Simple mesh pipeline (for isosurface rendering).
222    pub simple_mesh_pipeline: Option<wgpu::RenderPipeline>,
223    /// Simple mesh bind group layout.
224    pub(crate) simple_mesh_bind_group_layout: Option<wgpu::BindGroupLayout>,
225    /// Gridcube pipeline (for volume grid scalar visualization).
226    pub gridcube_pipeline: Option<wgpu::RenderPipeline>,
227    /// Gridcube bind group layout.
228    pub(crate) gridcube_bind_group_layout: Option<wgpu::BindGroupLayout>,
229
230    // Pick system - range-based ID management (flat 24-bit global index)
231    /// Map from (`type_name`, name) to pick range.
232    pub(crate) pick_ranges: HashMap<(String, String), pick::PickRange>,
233    /// Next available global index (0 is reserved for background).
234    pub(crate) next_global_index: u32,
235
236    // Pick system - GPU resources
237    /// Pick color texture for element selection.
238    pub(crate) pick_texture: Option<wgpu::Texture>,
239    /// Pick color texture view.
240    pub(crate) pick_texture_view: Option<wgpu::TextureView>,
241    /// Pick depth texture.
242    pub(crate) pick_depth_texture: Option<wgpu::Texture>,
243    /// Pick depth texture view.
244    pub(crate) pick_depth_view: Option<wgpu::TextureView>,
245    /// Staging buffer for pick pixel readback.
246    pub(crate) pick_staging_buffer: Option<wgpu::Buffer>,
247    /// Current size of pick buffers (for resize detection).
248    pub(crate) pick_buffer_size: (u32, u32),
249    /// Pick pipeline for point clouds.
250    pub(crate) point_pick_pipeline: Option<wgpu::RenderPipeline>,
251    /// Pick pipeline for curve networks (line mode).
252    pub(crate) curve_network_pick_pipeline: Option<wgpu::RenderPipeline>,
253    /// Pick pipeline for curve networks (tube mode) - uses ray-cylinder intersection.
254    pub(crate) curve_network_tube_pick_pipeline: Option<wgpu::RenderPipeline>,
255    /// Tube pick bind group layout.
256    pub(crate) curve_network_tube_pick_bind_group_layout: Option<wgpu::BindGroupLayout>,
257    /// Pick bind group layout (shared across point cloud and curve network pick pipelines).
258    pub(crate) pick_bind_group_layout: Option<wgpu::BindGroupLayout>,
259    /// Pick pipeline for surface meshes (face picking).
260    pub(crate) mesh_pick_pipeline: Option<wgpu::RenderPipeline>,
261    /// Mesh pick bind group layout (has extra `face_indices` binding).
262    pub(crate) mesh_pick_bind_group_layout: Option<wgpu::BindGroupLayout>,
263    /// Pick pipeline for volume grid gridcube instances.
264    pub(crate) gridcube_pick_pipeline: Option<wgpu::RenderPipeline>,
265    /// Gridcube pick bind group layout (camera, pick uniforms, positions).
266    pub(crate) gridcube_pick_bind_group_layout: Option<wgpu::BindGroupLayout>,
267}
268
269impl RenderEngine {
270    /// Creates a new windowed render engine.
271    pub async fn new_windowed(window: Arc<winit::window::Window>) -> RenderResult<Self> {
272        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
273            backends: wgpu::Backends::all(),
274            ..wgpu::InstanceDescriptor::default()
275        });
276
277        let surface = instance.create_surface(window.clone())?;
278
279        let adapter = instance
280            .request_adapter(&wgpu::RequestAdapterOptions {
281                power_preference: wgpu::PowerPreference::HighPerformance,
282                compatible_surface: Some(&surface),
283                force_fallback_adapter: false,
284            })
285            .await
286            .map_err(|_| RenderError::AdapterCreationFailed)?;
287
288        let (device, queue) = adapter
289            .request_device(&wgpu::DeviceDescriptor {
290                label: Some("polyscope device"),
291                required_features: wgpu::Features::empty(),
292                required_limits: wgpu::Limits::default(),
293                memory_hints: wgpu::MemoryHints::default(),
294                trace: wgpu::Trace::default(),
295                experimental_features: wgpu::ExperimentalFeatures::default(),
296            })
297            .await?;
298
299        let size = window.inner_size();
300        let width = size.width.max(1);
301        let height = size.height.max(1);
302
303        let surface_caps = surface.get_capabilities(&adapter);
304        let surface_format = surface_caps
305            .formats
306            .iter()
307            .find(|f| f.is_srgb())
308            .copied()
309            .unwrap_or(surface_caps.formats[0]);
310
311        let surface_config = wgpu::SurfaceConfiguration {
312            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
313            format: surface_format,
314            width,
315            height,
316            present_mode: wgpu::PresentMode::AutoVsync,
317            alpha_mode: surface_caps.alpha_modes[0],
318            view_formats: vec![],
319            desired_maximum_frame_latency: 2,
320        };
321        surface.configure(&device, &surface_config);
322
323        let (depth_texture, depth_view, depth_only_view) =
324            Self::create_depth_texture(&device, width, height);
325
326        let camera = Camera::new(width as f32 / height as f32);
327
328        let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
329            label: Some("camera uniforms"),
330            contents: bytemuck::cast_slice(&[CameraUniforms::default()]),
331            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
332        });
333
334        // Create slice plane buffer and bind group
335        let slice_planes_data = [SlicePlaneUniforms::default(); MAX_SLICE_PLANES];
336        let slice_plane_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
337            label: Some("Slice Plane Buffer"),
338            contents: bytemuck::cast_slice(&slice_planes_data),
339            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
340        });
341
342        let slice_plane_bind_group_layout =
343            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
344                label: Some("Slice Plane Bind Group Layout"),
345                entries: &[wgpu::BindGroupLayoutEntry {
346                    binding: 0,
347                    visibility: wgpu::ShaderStages::FRAGMENT,
348                    ty: wgpu::BindingType::Buffer {
349                        ty: wgpu::BufferBindingType::Uniform,
350                        has_dynamic_offset: false,
351                        min_binding_size: NonZeroU64::new(128),
352                    },
353                    count: None,
354                }],
355            });
356
357        let slice_plane_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
358            label: Some("Slice Plane Bind Group"),
359            layout: &slice_plane_bind_group_layout,
360            entries: &[wgpu::BindGroupEntry {
361                binding: 0,
362                resource: slice_plane_buffer.as_entire_binding(),
363            }],
364        });
365
366        // Create shadow map pass first (needed for bind group)
367        let shadow_map_pass = ShadowMapPass::new(&device);
368
369        // Ground plane bind group layout (includes shadow bindings)
370        let ground_plane_bind_group_layout =
371            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
372                label: Some("Ground Plane Bind Group Layout"),
373                entries: &[
374                    // Camera uniforms
375                    wgpu::BindGroupLayoutEntry {
376                        binding: 0,
377                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
378                        ty: wgpu::BindingType::Buffer {
379                            ty: wgpu::BufferBindingType::Uniform,
380                            has_dynamic_offset: false,
381                            min_binding_size: NonZeroU64::new(272),
382                        },
383                        count: None,
384                    },
385                    // Ground uniforms
386                    wgpu::BindGroupLayoutEntry {
387                        binding: 1,
388                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
389                        ty: wgpu::BindingType::Buffer {
390                            ty: wgpu::BufferBindingType::Uniform,
391                            has_dynamic_offset: false,
392                            min_binding_size: NonZeroU64::new(96),
393                        },
394                        count: None,
395                    },
396                    // Light uniforms
397                    wgpu::BindGroupLayoutEntry {
398                        binding: 2,
399                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
400                        ty: wgpu::BindingType::Buffer {
401                            ty: wgpu::BufferBindingType::Uniform,
402                            has_dynamic_offset: false,
403                            min_binding_size: NonZeroU64::new(80),
404                        },
405                        count: None,
406                    },
407                    // Shadow map texture
408                    wgpu::BindGroupLayoutEntry {
409                        binding: 3,
410                        visibility: wgpu::ShaderStages::FRAGMENT,
411                        ty: wgpu::BindingType::Texture {
412                            sample_type: wgpu::TextureSampleType::Depth,
413                            view_dimension: wgpu::TextureViewDimension::D2,
414                            multisampled: false,
415                        },
416                        count: None,
417                    },
418                    // Shadow comparison sampler
419                    wgpu::BindGroupLayoutEntry {
420                        binding: 4,
421                        visibility: wgpu::ShaderStages::FRAGMENT,
422                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
423                        count: None,
424                    },
425                ],
426            });
427
428        // Ground plane shader
429        let ground_plane_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
430            label: Some("Ground Plane Shader"),
431            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/ground_plane.wgsl").into()),
432        });
433
434        // Ground plane pipeline layout
435        let ground_plane_pipeline_layout =
436            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
437                label: Some("Ground Plane Pipeline Layout"),
438                bind_group_layouts: &[&ground_plane_bind_group_layout],
439                push_constant_ranges: &[],
440            });
441
442        // Ground plane render pipeline (with alpha blending)
443        let ground_plane_pipeline =
444            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
445                label: Some("Ground Plane Pipeline"),
446                layout: Some(&ground_plane_pipeline_layout),
447                vertex: wgpu::VertexState {
448                    module: &ground_plane_shader,
449                    entry_point: Some("vs_main"),
450                    buffers: &[],
451                    compilation_options: wgpu::PipelineCompilationOptions::default(),
452                },
453                fragment: Some(wgpu::FragmentState {
454                    module: &ground_plane_shader,
455                    entry_point: Some("fs_main"),
456                    targets: &[Some(wgpu::ColorTargetState {
457                        format: wgpu::TextureFormat::Rgba16Float, // HDR format for scene rendering
458                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
459                        write_mask: wgpu::ColorWrites::ALL,
460                    })],
461                    compilation_options: wgpu::PipelineCompilationOptions::default(),
462                }),
463                primitive: wgpu::PrimitiveState {
464                    topology: wgpu::PrimitiveTopology::TriangleList,
465                    ..wgpu::PrimitiveState::default()
466                },
467                depth_stencil: Some(wgpu::DepthStencilState {
468                    format: wgpu::TextureFormat::Depth24PlusStencil8,
469                    depth_write_enabled: true,
470                    depth_compare: wgpu::CompareFunction::LessEqual,
471                    stencil: wgpu::StencilState::default(),
472                    bias: wgpu::DepthBiasState::default(),
473                }),
474                multisample: wgpu::MultisampleState::default(),
475                multiview: None,
476                cache: None,
477            });
478
479        // Slice plane visualization pipeline
480        let slice_plane_vis_bind_group_layout =
481            crate::slice_plane_render::create_slice_plane_bind_group_layout(&device);
482        let slice_plane_vis_pipeline = crate::slice_plane_render::create_slice_plane_pipeline(
483            &device,
484            &slice_plane_vis_bind_group_layout,
485            wgpu::TextureFormat::Rgba16Float,
486            wgpu::TextureFormat::Depth24PlusStencil8,
487        );
488
489        // Create matcap bind group layout and load all matcap textures
490        let matcap_bind_group_layout = materials::create_matcap_bind_group_layout(&device);
491        let matcap_textures =
492            materials::init_matcap_textures(&device, &queue, &matcap_bind_group_layout);
493
494        let mut engine = Self {
495            instance,
496            adapter,
497            device,
498            queue,
499            surface: Some(surface),
500            surface_config,
501            depth_texture,
502            depth_view,
503            depth_only_view,
504            materials: MaterialRegistry::new(),
505            color_maps: ColorMapRegistry::new(),
506            matcap_bind_group_layout,
507            matcap_textures,
508            camera,
509            width,
510            height,
511            point_pipeline: None,
512            point_bind_group_layout: None,
513            camera_buffer,
514            slice_plane_buffer,
515            slice_plane_bind_group_layout,
516            slice_plane_bind_group,
517            vector_pipeline: None,
518            vector_bind_group_layout: None,
519            mesh_pipeline: None,
520
521            mesh_depth_normal_pipeline: None,
522            mesh_bind_group_layout: None,
523            curve_network_edge_pipeline: None,
524            curve_network_edge_bind_group_layout: None,
525            curve_network_tube_pipeline: None,
526            curve_network_tube_compute_pipeline: None,
527            curve_network_tube_bind_group_layout: None,
528            curve_network_tube_compute_bind_group_layout: None,
529            ground_plane_pipeline,
530            ground_plane_bind_group_layout,
531            ground_plane_render_data: None,
532            slice_plane_vis_pipeline,
533            slice_plane_vis_bind_group_layout,
534            slice_plane_render_data: Vec::new(),
535            screenshot_texture: None,
536            screenshot_buffer: None,
537            screenshot_hdr_texture: None,
538            screenshot_hdr_view: None,
539            hdr_texture: None,
540            hdr_view: None,
541            normal_texture: None,
542            normal_view: None,
543            ssao_noise_texture: None,
544            ssao_noise_view: None,
545            ssao_pass: None,
546            ssao_output_texture: None,
547            ssao_output_view: None,
548            depth_peel_pass: None,
549            tone_map_pass: None,
550            ssaa_pass: None,
551            ssaa_factor: 1,
552            ssaa_intermediate_texture: None,
553            ssaa_intermediate_view: None,
554            shadow_map_pass: Some(shadow_map_pass),
555            shadow_pipeline: None,
556            shadow_bind_group_layout: None,
557            reflection_pass: None,
558            ground_stencil_pipeline: None,
559            reflected_mesh_pipeline: None,
560            reflected_mesh_bind_group_layout: None,
561            reflected_point_cloud_pipeline: None,
562            reflected_point_cloud_bind_group_layout: None,
563            reflected_curve_network_pipeline: None,
564            reflected_curve_network_bind_group_layout: None,
565            simple_mesh_pipeline: None,
566            simple_mesh_bind_group_layout: None,
567            gridcube_pipeline: None,
568            gridcube_bind_group_layout: None,
569            pick_ranges: HashMap::new(),
570            next_global_index: 1, // 0 is reserved for background
571            pick_texture: None,
572            pick_texture_view: None,
573            pick_depth_texture: None,
574            pick_depth_view: None,
575            pick_staging_buffer: None,
576            pick_buffer_size: (0, 0),
577            point_pick_pipeline: None,
578            curve_network_pick_pipeline: None,
579            curve_network_tube_pick_pipeline: None,
580            curve_network_tube_pick_bind_group_layout: None,
581            pick_bind_group_layout: None,
582            mesh_pick_pipeline: None,
583            mesh_pick_bind_group_layout: None,
584            gridcube_pick_pipeline: None,
585            gridcube_pick_bind_group_layout: None,
586        };
587
588        engine.init_point_pipeline();
589        engine.init_vector_pipeline();
590        engine.create_mesh_pipeline();
591        engine.create_curve_network_edge_pipeline();
592        engine.create_curve_network_tube_pipelines();
593        engine.create_simple_mesh_pipeline();
594        engine.create_gridcube_pipeline();
595        engine.create_shadow_pipeline();
596        engine.init_tone_mapping();
597        engine.init_ssaa_pass();
598        engine.init_reflection_pass();
599        engine.create_ground_stencil_pipeline();
600        engine.create_reflected_mesh_pipeline();
601        engine.create_reflected_point_cloud_pipeline();
602        engine.create_reflected_curve_network_pipeline();
603        engine.init_pick_pipeline();
604        engine.init_mesh_pick_pipeline();
605
606        Ok(engine)
607    }
608
609    /// Creates a new headless render engine.
610    pub async fn new_headless(width: u32, height: u32) -> RenderResult<Self> {
611        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
612            backends: wgpu::Backends::all(),
613            ..wgpu::InstanceDescriptor::default()
614        });
615
616        let adapter = instance
617            .request_adapter(&wgpu::RequestAdapterOptions {
618                power_preference: wgpu::PowerPreference::HighPerformance,
619                compatible_surface: None,
620                force_fallback_adapter: false,
621            })
622            .await
623            .map_err(|_| RenderError::AdapterCreationFailed)?;
624
625        let (device, queue) = adapter
626            .request_device(&wgpu::DeviceDescriptor {
627                label: Some("polyscope device (headless)"),
628                required_features: wgpu::Features::empty(),
629                required_limits: wgpu::Limits::default(),
630                memory_hints: wgpu::MemoryHints::default(),
631                trace: wgpu::Trace::default(),
632                experimental_features: wgpu::ExperimentalFeatures::default(),
633            })
634            .await?;
635
636        let surface_config = wgpu::SurfaceConfiguration {
637            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
638            format: wgpu::TextureFormat::Rgba8UnormSrgb,
639            width,
640            height,
641            present_mode: wgpu::PresentMode::Fifo,
642            alpha_mode: wgpu::CompositeAlphaMode::Auto,
643            view_formats: vec![],
644            desired_maximum_frame_latency: 2,
645        };
646
647        let (depth_texture, depth_view, depth_only_view) =
648            Self::create_depth_texture(&device, width, height);
649
650        let camera = Camera::new(width as f32 / height as f32);
651
652        let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
653            label: Some("camera uniforms"),
654            contents: bytemuck::cast_slice(&[CameraUniforms::default()]),
655            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
656        });
657
658        // Create slice plane buffer and bind group
659        let slice_planes_data = [SlicePlaneUniforms::default(); MAX_SLICE_PLANES];
660        let slice_plane_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
661            label: Some("Slice Plane Buffer"),
662            contents: bytemuck::cast_slice(&slice_planes_data),
663            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
664        });
665
666        let slice_plane_bind_group_layout =
667            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
668                label: Some("Slice Plane Bind Group Layout"),
669                entries: &[wgpu::BindGroupLayoutEntry {
670                    binding: 0,
671                    visibility: wgpu::ShaderStages::FRAGMENT,
672                    ty: wgpu::BindingType::Buffer {
673                        ty: wgpu::BufferBindingType::Uniform,
674                        has_dynamic_offset: false,
675                        min_binding_size: NonZeroU64::new(128),
676                    },
677                    count: None,
678                }],
679            });
680
681        let slice_plane_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
682            label: Some("Slice Plane Bind Group"),
683            layout: &slice_plane_bind_group_layout,
684            entries: &[wgpu::BindGroupEntry {
685                binding: 0,
686                resource: slice_plane_buffer.as_entire_binding(),
687            }],
688        });
689
690        // Create shadow map pass first (needed for bind group)
691        let shadow_map_pass = ShadowMapPass::new(&device);
692
693        // Ground plane bind group layout (includes shadow bindings)
694        let ground_plane_bind_group_layout =
695            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
696                label: Some("Ground Plane Bind Group Layout"),
697                entries: &[
698                    // Camera uniforms
699                    wgpu::BindGroupLayoutEntry {
700                        binding: 0,
701                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
702                        ty: wgpu::BindingType::Buffer {
703                            ty: wgpu::BufferBindingType::Uniform,
704                            has_dynamic_offset: false,
705                            min_binding_size: NonZeroU64::new(272),
706                        },
707                        count: None,
708                    },
709                    // Ground uniforms
710                    wgpu::BindGroupLayoutEntry {
711                        binding: 1,
712                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
713                        ty: wgpu::BindingType::Buffer {
714                            ty: wgpu::BufferBindingType::Uniform,
715                            has_dynamic_offset: false,
716                            min_binding_size: NonZeroU64::new(96),
717                        },
718                        count: None,
719                    },
720                    // Light uniforms
721                    wgpu::BindGroupLayoutEntry {
722                        binding: 2,
723                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
724                        ty: wgpu::BindingType::Buffer {
725                            ty: wgpu::BufferBindingType::Uniform,
726                            has_dynamic_offset: false,
727                            min_binding_size: NonZeroU64::new(80),
728                        },
729                        count: None,
730                    },
731                    // Shadow map texture
732                    wgpu::BindGroupLayoutEntry {
733                        binding: 3,
734                        visibility: wgpu::ShaderStages::FRAGMENT,
735                        ty: wgpu::BindingType::Texture {
736                            sample_type: wgpu::TextureSampleType::Depth,
737                            view_dimension: wgpu::TextureViewDimension::D2,
738                            multisampled: false,
739                        },
740                        count: None,
741                    },
742                    // Shadow comparison sampler
743                    wgpu::BindGroupLayoutEntry {
744                        binding: 4,
745                        visibility: wgpu::ShaderStages::FRAGMENT,
746                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
747                        count: None,
748                    },
749                ],
750            });
751
752        // Ground plane shader
753        let ground_plane_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
754            label: Some("Ground Plane Shader"),
755            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/ground_plane.wgsl").into()),
756        });
757
758        // Ground plane pipeline layout
759        let ground_plane_pipeline_layout =
760            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
761                label: Some("Ground Plane Pipeline Layout"),
762                bind_group_layouts: &[&ground_plane_bind_group_layout],
763                push_constant_ranges: &[],
764            });
765
766        // Ground plane render pipeline (with alpha blending)
767        let ground_plane_pipeline =
768            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
769                label: Some("Ground Plane Pipeline"),
770                layout: Some(&ground_plane_pipeline_layout),
771                vertex: wgpu::VertexState {
772                    module: &ground_plane_shader,
773                    entry_point: Some("vs_main"),
774                    buffers: &[],
775                    compilation_options: wgpu::PipelineCompilationOptions::default(),
776                },
777                fragment: Some(wgpu::FragmentState {
778                    module: &ground_plane_shader,
779                    entry_point: Some("fs_main"),
780                    targets: &[Some(wgpu::ColorTargetState {
781                        format: wgpu::TextureFormat::Rgba16Float, // HDR format for scene rendering
782                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
783                        write_mask: wgpu::ColorWrites::ALL,
784                    })],
785                    compilation_options: wgpu::PipelineCompilationOptions::default(),
786                }),
787                primitive: wgpu::PrimitiveState {
788                    topology: wgpu::PrimitiveTopology::TriangleList,
789                    ..wgpu::PrimitiveState::default()
790                },
791                depth_stencil: Some(wgpu::DepthStencilState {
792                    format: wgpu::TextureFormat::Depth24PlusStencil8,
793                    depth_write_enabled: true,
794                    depth_compare: wgpu::CompareFunction::LessEqual,
795                    stencil: wgpu::StencilState::default(),
796                    bias: wgpu::DepthBiasState::default(),
797                }),
798                multisample: wgpu::MultisampleState::default(),
799                multiview: None,
800                cache: None,
801            });
802
803        // Slice plane visualization pipeline
804        let slice_plane_vis_bind_group_layout =
805            crate::slice_plane_render::create_slice_plane_bind_group_layout(&device);
806        let slice_plane_vis_pipeline = crate::slice_plane_render::create_slice_plane_pipeline(
807            &device,
808            &slice_plane_vis_bind_group_layout,
809            wgpu::TextureFormat::Rgba16Float,
810            wgpu::TextureFormat::Depth24PlusStencil8,
811        );
812
813        // Create matcap bind group layout and load all matcap textures
814        let matcap_bind_group_layout = materials::create_matcap_bind_group_layout(&device);
815        let matcap_textures =
816            materials::init_matcap_textures(&device, &queue, &matcap_bind_group_layout);
817
818        let mut engine = Self {
819            instance,
820            adapter,
821            device,
822            queue,
823            surface: None,
824            surface_config,
825            depth_texture,
826            depth_view,
827            depth_only_view,
828            materials: MaterialRegistry::new(),
829            color_maps: ColorMapRegistry::new(),
830            matcap_bind_group_layout,
831            matcap_textures,
832            camera,
833            width,
834            height,
835            point_pipeline: None,
836            point_bind_group_layout: None,
837            camera_buffer,
838            slice_plane_buffer,
839            slice_plane_bind_group_layout,
840            slice_plane_bind_group,
841            vector_pipeline: None,
842            vector_bind_group_layout: None,
843            mesh_pipeline: None,
844
845            mesh_depth_normal_pipeline: None,
846            mesh_bind_group_layout: None,
847            curve_network_edge_pipeline: None,
848            curve_network_edge_bind_group_layout: None,
849            curve_network_tube_pipeline: None,
850            curve_network_tube_compute_pipeline: None,
851            curve_network_tube_bind_group_layout: None,
852            curve_network_tube_compute_bind_group_layout: None,
853            ground_plane_pipeline,
854            ground_plane_bind_group_layout,
855            ground_plane_render_data: None,
856            slice_plane_vis_pipeline,
857            slice_plane_vis_bind_group_layout,
858            slice_plane_render_data: Vec::new(),
859            screenshot_texture: None,
860            screenshot_buffer: None,
861            screenshot_hdr_texture: None,
862            screenshot_hdr_view: None,
863            hdr_texture: None,
864            hdr_view: None,
865            normal_texture: None,
866            normal_view: None,
867            ssao_noise_texture: None,
868            ssao_noise_view: None,
869            ssao_pass: None,
870            ssao_output_texture: None,
871            ssao_output_view: None,
872            depth_peel_pass: None,
873            tone_map_pass: None,
874            ssaa_pass: None,
875            ssaa_factor: 1,
876            ssaa_intermediate_texture: None,
877            ssaa_intermediate_view: None,
878            shadow_map_pass: Some(shadow_map_pass),
879            shadow_pipeline: None,
880            shadow_bind_group_layout: None,
881            reflection_pass: None,
882            ground_stencil_pipeline: None,
883            reflected_mesh_pipeline: None,
884            reflected_mesh_bind_group_layout: None,
885            reflected_point_cloud_pipeline: None,
886            reflected_point_cloud_bind_group_layout: None,
887            reflected_curve_network_pipeline: None,
888            reflected_curve_network_bind_group_layout: None,
889            simple_mesh_pipeline: None,
890            simple_mesh_bind_group_layout: None,
891            gridcube_pipeline: None,
892            gridcube_bind_group_layout: None,
893            pick_ranges: HashMap::new(),
894            next_global_index: 1, // 0 is reserved for background
895            pick_texture: None,
896            pick_texture_view: None,
897            pick_depth_texture: None,
898            pick_depth_view: None,
899            pick_staging_buffer: None,
900            pick_buffer_size: (0, 0),
901            point_pick_pipeline: None,
902            curve_network_pick_pipeline: None,
903            curve_network_tube_pick_pipeline: None,
904            curve_network_tube_pick_bind_group_layout: None,
905            pick_bind_group_layout: None,
906            mesh_pick_pipeline: None,
907            mesh_pick_bind_group_layout: None,
908            gridcube_pick_pipeline: None,
909            gridcube_pick_bind_group_layout: None,
910        };
911
912        engine.init_point_pipeline();
913        engine.init_vector_pipeline();
914        engine.create_mesh_pipeline();
915        engine.create_curve_network_edge_pipeline();
916        engine.create_curve_network_tube_pipelines();
917        engine.create_simple_mesh_pipeline();
918        engine.create_gridcube_pipeline();
919        engine.create_shadow_pipeline();
920        engine.init_tone_mapping();
921        engine.init_ssaa_pass();
922        engine.init_reflection_pass();
923        engine.create_ground_stencil_pipeline();
924        engine.create_reflected_mesh_pipeline();
925        engine.create_reflected_point_cloud_pipeline();
926        engine.create_reflected_curve_network_pipeline();
927        engine.init_pick_pipeline();
928        engine.init_mesh_pick_pipeline();
929
930        Ok(engine)
931    }
932
933    /// Resizes the render target.
934    pub fn resize(&mut self, width: u32, height: u32) {
935        if width == 0 || height == 0 {
936            return;
937        }
938
939        self.width = width;
940        self.height = height;
941        self.surface_config.width = width;
942        self.surface_config.height = height;
943
944        if let Some(ref surface) = self.surface {
945            surface.configure(&self.device, &self.surface_config);
946        }
947
948        // Calculate SSAA-scaled dimensions
949        let ssaa_width = width * self.ssaa_factor;
950        let ssaa_height = height * self.ssaa_factor;
951
952        let (depth_texture, depth_view, depth_only_view) =
953            Self::create_depth_texture(&self.device, ssaa_width, ssaa_height);
954        self.depth_texture = depth_texture;
955        self.depth_view = depth_view;
956        self.depth_only_view = depth_only_view;
957
958        // Recreate HDR texture for tone mapping (at SSAA resolution)
959        self.create_hdr_texture_with_size(ssaa_width, ssaa_height);
960
961        // Recreate normal G-buffer for SSAO (at SSAA resolution)
962        self.create_normal_texture_with_size(ssaa_width, ssaa_height);
963
964        // Resize SSAO pass and output texture (at SSAA resolution)
965        if let Some(ref mut ssao_pass) = self.ssao_pass {
966            ssao_pass.resize(&self.device, &self.queue, ssaa_width, ssaa_height);
967        }
968        self.create_ssao_output_texture_with_size(ssaa_width, ssaa_height);
969
970        // Recreate intermediate texture for SSAA downsampling
971        if self.ssaa_factor > 1 {
972            self.create_ssaa_intermediate_texture();
973        }
974
975        self.camera.set_aspect_ratio(width as f32 / height as f32);
976    }
977
978    fn create_depth_texture(
979        device: &wgpu::Device,
980        width: u32,
981        height: u32,
982    ) -> (wgpu::Texture, wgpu::TextureView, wgpu::TextureView) {
983        let texture = device.create_texture(&wgpu::TextureDescriptor {
984            label: Some("depth texture"),
985            size: wgpu::Extent3d {
986                width,
987                height,
988                depth_or_array_layers: 1,
989            },
990            mip_level_count: 1,
991            sample_count: 1,
992            dimension: wgpu::TextureDimension::D2,
993            format: wgpu::TextureFormat::Depth24PlusStencil8,
994            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
995            view_formats: &[],
996        });
997
998        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
999
1000        // Create depth-only view for SSAO (excludes stencil aspect)
1001        let depth_only_view = texture.create_view(&wgpu::TextureViewDescriptor {
1002            label: Some("depth only view"),
1003            aspect: wgpu::TextureAspect::DepthOnly,
1004            ..Default::default()
1005        });
1006
1007        (texture, view, depth_only_view)
1008    }
1009
1010    /// Updates camera uniforms.
1011    pub fn update_camera_uniforms(&self) {
1012        let uniforms = CameraUniforms::from_camera(&self.camera);
1013        self.queue
1014            .write_buffer(&self.camera_buffer, 0, bytemuck::cast_slice(&[uniforms]));
1015    }
1016
1017    /// Updates slice plane uniforms from the provided slice plane data.
1018    ///
1019    /// Takes an iterator of `SlicePlaneUniforms` and uploads them to the GPU buffer.
1020    /// Up to `MAX_SLICE_PLANES` planes are used; remaining slots are disabled.
1021    pub fn update_slice_plane_uniforms(&self, planes: impl Iterator<Item = SlicePlaneUniforms>) {
1022        let mut uniforms = [SlicePlaneUniforms::default(); MAX_SLICE_PLANES];
1023        for (i, plane) in planes.take(MAX_SLICE_PLANES).enumerate() {
1024            uniforms[i] = plane;
1025        }
1026
1027        self.queue
1028            .write_buffer(&self.slice_plane_buffer, 0, bytemuck::cast_slice(&uniforms));
1029    }
1030
1031    /// Gets the camera buffer.
1032    pub fn camera_buffer(&self) -> &wgpu::Buffer {
1033        &self.camera_buffer
1034    }
1035
1036    /// Gets the shadow map pass (if initialized).
1037    pub fn shadow_map_pass(&self) -> Option<&ShadowMapPass> {
1038        self.shadow_map_pass.as_ref()
1039    }
1040
1041    /// Returns the depth texture view.
1042    pub fn depth_view(&self) -> &wgpu::TextureView {
1043        &self.depth_view
1044    }
1045
1046    /// Returns the HDR texture view if available.
1047    pub fn hdr_texture_view(&self) -> Option<&wgpu::TextureView> {
1048        self.hdr_view.as_ref()
1049    }
1050
1051    /// Returns the viewport dimensions.
1052    #[must_use]
1053    pub fn dimensions(&self) -> (u32, u32) {
1054        (self.width, self.height)
1055    }
1056
1057    /// Returns the render dimensions (accounting for SSAA).
1058    #[must_use]
1059    pub fn render_dimensions(&self) -> (u32, u32) {
1060        (
1061            self.width * self.ssaa_factor,
1062            self.height * self.ssaa_factor,
1063        )
1064    }
1065
1066    /// Loads a blendable (4-channel RGB-tintable) material from disk.
1067    ///
1068    /// Takes 4 image file paths for R, G, B, K matcap channels.
1069    /// Supports HDR, JPEG, PNG, EXR, and other formats via the `image` crate.
1070    pub fn load_blendable_material(
1071        &mut self,
1072        name: &str,
1073        filenames: [&str; 4],
1074    ) -> std::result::Result<(), polyscope_core::PolyscopeError> {
1075        use polyscope_core::PolyscopeError;
1076
1077        if self.matcap_textures.contains_key(name) {
1078            return Err(PolyscopeError::MaterialExists(name.to_string()));
1079        }
1080
1081        let channel_labels = ["r", "g", "b", "k"];
1082        let mut views = Vec::with_capacity(4);
1083
1084        for (i, filename) in filenames.iter().enumerate() {
1085            let path = std::path::Path::new(filename);
1086            let (w, h, rgba) = materials::decode_matcap_image_from_file(path)
1087                .map_err(PolyscopeError::MaterialLoadError)?;
1088            let tex = materials::upload_matcap_texture(
1089                &self.device,
1090                &self.queue,
1091                &format!("matcap_{name}_{}", channel_labels[i]),
1092                w,
1093                h,
1094                &rgba,
1095            );
1096            views.push(tex.create_view(&wgpu::TextureViewDescriptor::default()));
1097        }
1098
1099        let sampler = materials::create_matcap_sampler(&self.device);
1100
1101        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1102            label: Some(&format!("matcap_{name}_bind_group")),
1103            layout: &self.matcap_bind_group_layout,
1104            entries: &[
1105                wgpu::BindGroupEntry {
1106                    binding: 0,
1107                    resource: wgpu::BindingResource::TextureView(&views[0]),
1108                },
1109                wgpu::BindGroupEntry {
1110                    binding: 1,
1111                    resource: wgpu::BindingResource::TextureView(&views[1]),
1112                },
1113                wgpu::BindGroupEntry {
1114                    binding: 2,
1115                    resource: wgpu::BindingResource::TextureView(&views[2]),
1116                },
1117                wgpu::BindGroupEntry {
1118                    binding: 3,
1119                    resource: wgpu::BindingResource::TextureView(&views[3]),
1120                },
1121                wgpu::BindGroupEntry {
1122                    binding: 4,
1123                    resource: wgpu::BindingResource::Sampler(&sampler),
1124                },
1125            ],
1126        });
1127
1128        // Move views into individual fields
1129        let mut drain = views.into_iter();
1130        let tex_r = drain.next().unwrap();
1131        let tex_g = drain.next().unwrap();
1132        let tex_b = drain.next().unwrap();
1133        let tex_k = drain.next().unwrap();
1134
1135        self.matcap_textures.insert(
1136            name.to_string(),
1137            MatcapTextureSet {
1138                tex_r,
1139                tex_g,
1140                tex_b,
1141                tex_k,
1142                sampler,
1143                bind_group,
1144            },
1145        );
1146
1147        self.materials
1148            .register(Material::blendable(name, 0.2, 0.7, 0.3, 32.0));
1149
1150        Ok(())
1151    }
1152
1153    /// Loads a static (single-texture, non-RGB-tintable) material from disk.
1154    ///
1155    /// The same texture is used for all 4 matcap channels.
1156    /// Supports HDR, JPEG, PNG, EXR, and other formats via the `image` crate.
1157    pub fn load_static_material(
1158        &mut self,
1159        name: &str,
1160        filename: &str,
1161    ) -> std::result::Result<(), polyscope_core::PolyscopeError> {
1162        use polyscope_core::PolyscopeError;
1163
1164        if self.matcap_textures.contains_key(name) {
1165            return Err(PolyscopeError::MaterialExists(name.to_string()));
1166        }
1167
1168        let path = std::path::Path::new(filename);
1169        let (w, h, rgba) = materials::decode_matcap_image_from_file(path)
1170            .map_err(PolyscopeError::MaterialLoadError)?;
1171        let tex = materials::upload_matcap_texture(
1172            &self.device,
1173            &self.queue,
1174            &format!("matcap_{name}"),
1175            w,
1176            h,
1177            &rgba,
1178        );
1179
1180        let view_r = tex.create_view(&wgpu::TextureViewDescriptor::default());
1181        let view_g = tex.create_view(&wgpu::TextureViewDescriptor::default());
1182        let view_b = tex.create_view(&wgpu::TextureViewDescriptor::default());
1183        let view_k = tex.create_view(&wgpu::TextureViewDescriptor::default());
1184
1185        let sampler = materials::create_matcap_sampler(&self.device);
1186
1187        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1188            label: Some(&format!("matcap_{name}_bind_group")),
1189            layout: &self.matcap_bind_group_layout,
1190            entries: &[
1191                wgpu::BindGroupEntry {
1192                    binding: 0,
1193                    resource: wgpu::BindingResource::TextureView(&view_r),
1194                },
1195                wgpu::BindGroupEntry {
1196                    binding: 1,
1197                    resource: wgpu::BindingResource::TextureView(&view_g),
1198                },
1199                wgpu::BindGroupEntry {
1200                    binding: 2,
1201                    resource: wgpu::BindingResource::TextureView(&view_b),
1202                },
1203                wgpu::BindGroupEntry {
1204                    binding: 3,
1205                    resource: wgpu::BindingResource::TextureView(&view_k),
1206                },
1207                wgpu::BindGroupEntry {
1208                    binding: 4,
1209                    resource: wgpu::BindingResource::Sampler(&sampler),
1210                },
1211            ],
1212        });
1213
1214        self.matcap_textures.insert(
1215            name.to_string(),
1216            MatcapTextureSet {
1217                tex_r: tex.create_view(&wgpu::TextureViewDescriptor::default()),
1218                tex_g: tex.create_view(&wgpu::TextureViewDescriptor::default()),
1219                tex_b: tex.create_view(&wgpu::TextureViewDescriptor::default()),
1220                tex_k: tex.create_view(&wgpu::TextureViewDescriptor::default()),
1221                sampler,
1222                bind_group,
1223            },
1224        );
1225
1226        self.materials
1227            .register(Material::static_mat(name, 0.2, 0.7, 0.3, 32.0));
1228
1229        Ok(())
1230    }
1231}