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