Skip to main content

viewport_lib/resources/
extra_impls.rs

1use super::*;
2
3pub(super) fn generate_edge_indices(triangle_indices: &[u32]) -> Vec<u32> {
4    use std::collections::HashSet;
5    let mut edges: HashSet<(u32, u32)> = HashSet::new();
6    let mut result = Vec::new();
7
8    for tri in triangle_indices.chunks(3) {
9        if tri.len() < 3 {
10            continue;
11        }
12        let pairs = [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])];
13        for (a, b) in &pairs {
14            // Canonical form: smaller index first, so (a,b) and (b,a) map to the same edge.
15            let edge = if a < b { (*a, *b) } else { (*b, *a) };
16            if edges.insert(edge) {
17                result.push(*a);
18                result.push(*b);
19            }
20        }
21    }
22    result
23}
24
25// ---------------------------------------------------------------------------
26// Procedural unit cube mesh (24 vertices, 4 per face, 36 indices)
27// ---------------------------------------------------------------------------
28
29/// Generate a unit cube centered at the origin.
30///
31/// 24 vertices (4 per face with shared normals), 36 indices (2 triangles per face).
32/// All vertices are white [1,1,1,1].
33pub(super) fn build_unit_cube() -> (Vec<Vertex>, Vec<u32>) {
34    let white = [1.0f32, 1.0, 1.0, 1.0];
35    let mut verts: Vec<Vertex> = Vec::with_capacity(24);
36    let mut idx: Vec<u32> = Vec::with_capacity(36);
37
38    // Helper: add a face quad (4 vertices in CCW order) and its 2 triangles.
39    let mut add_face = |positions: [[f32; 3]; 4], normal: [f32; 3]| {
40        let base = verts.len() as u32;
41        for pos in &positions {
42            verts.push(Vertex {
43                position: *pos,
44                normal,
45                colour: white,
46                uv: [0.0, 0.0],
47                tangent: [0.0, 0.0, 0.0, 1.0],
48            });
49        }
50        // Two triangles: (base, base+1, base+2) and (base, base+2, base+3)
51        idx.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
52    };
53
54    // +X face (right), normal [1, 0, 0]
55    add_face(
56        [
57            [0.5, -0.5, -0.5],
58            [0.5, 0.5, -0.5],
59            [0.5, 0.5, 0.5],
60            [0.5, -0.5, 0.5],
61        ],
62        [1.0, 0.0, 0.0],
63    );
64
65    // -X face (left), normal [-1, 0, 0]
66    add_face(
67        [
68            [-0.5, -0.5, 0.5],
69            [-0.5, 0.5, 0.5],
70            [-0.5, 0.5, -0.5],
71            [-0.5, -0.5, -0.5],
72        ],
73        [-1.0, 0.0, 0.0],
74    );
75
76    // +Y face (top), normal [0, 1, 0]
77    add_face(
78        [
79            [-0.5, 0.5, -0.5],
80            [-0.5, 0.5, 0.5],
81            [0.5, 0.5, 0.5],
82            [0.5, 0.5, -0.5],
83        ],
84        [0.0, 1.0, 0.0],
85    );
86
87    // -Y face (bottom), normal [0, -1, 0]
88    add_face(
89        [
90            [-0.5, -0.5, 0.5],
91            [-0.5, -0.5, -0.5],
92            [0.5, -0.5, -0.5],
93            [0.5, -0.5, 0.5],
94        ],
95        [0.0, -1.0, 0.0],
96    );
97
98    // +Z face (front), normal [0, 0, 1]
99    add_face(
100        [
101            [-0.5, -0.5, 0.5],
102            [0.5, -0.5, 0.5],
103            [0.5, 0.5, 0.5],
104            [-0.5, 0.5, 0.5],
105        ],
106        [0.0, 0.0, 1.0],
107    );
108
109    // -Z face (back), normal [0, 0, -1]
110    add_face(
111        [
112            [0.5, -0.5, -0.5],
113            [-0.5, -0.5, -0.5],
114            [-0.5, 0.5, -0.5],
115            [0.5, 0.5, -0.5],
116        ],
117        [0.0, 0.0, -1.0],
118    );
119
120    (verts, idx)
121}
122
123// ---------------------------------------------------------------------------
124// Procedural glyph arrow mesh (cone tip + cylinder shaft, local +Z axis)
125// ---------------------------------------------------------------------------
126
127/// Generate a unit arrow mesh aligned to local +Z.
128///
129/// The arrow consists of:
130/// - A cylinder shaft from Z=0 to Z=0.7, radius 0.05.
131/// - A cone tip from Z=0.7 to Z=1.0, base radius 0.12.
132///
133/// 16 segments around the circumference gives ~300 vertices.
134pub(super) fn build_glyph_arrow() -> (Vec<Vertex>, Vec<u32>) {
135    let white = [1.0f32, 1.0, 1.0, 1.0];
136    let segments = 16usize;
137    let mut verts: Vec<Vertex> = Vec::new();
138    let mut idx: Vec<u32> = Vec::new();
139
140    let shaft_r = 0.05f32;
141    let shaft_bot = 0.0f32;
142    let shaft_top = 0.7f32;
143    let cone_r = 0.12f32;
144    let cone_bot = shaft_top;
145    let cone_tip = 1.0f32;
146
147    // Helper: append ring vertices at a given Z and radius with outward normals.
148    let ring_verts = |verts: &mut Vec<Vertex>, z: f32, r: f32, normal_z: f32| {
149        for i in 0..segments {
150            let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
151            let (s, c) = angle.sin_cos();
152            let nx = if r > 0.0 { c } else { 0.0 };
153            let ny = if r > 0.0 { s } else { 0.0 };
154            let len = (nx * nx + ny * ny + normal_z * normal_z).sqrt();
155            verts.push(Vertex {
156                position: [c * r, s * r, z],
157                normal: [nx / len, ny / len, normal_z / len],
158                colour: white,
159                uv: [0.0, 0.0],
160                tangent: [0.0, 0.0, 0.0, 1.0],
161            });
162        }
163    };
164
165    // --- Shaft ---
166    // Bottom ring (face down for the cap).
167    let shaft_bot_base = verts.len() as u32;
168    ring_verts(&mut verts, shaft_bot, shaft_r, 0.0);
169
170    // Bottom cap center.
171    let shaft_bot_center = verts.len() as u32;
172    verts.push(Vertex {
173        position: [0.0, 0.0, shaft_bot],
174        normal: [0.0, 0.0, -1.0],
175        colour: white,
176        uv: [0.0, 0.0],
177        tangent: [0.0, 0.0, 0.0, 1.0],
178    });
179
180    // Bottom cap triangles.
181    for i in 0..segments {
182        let a = shaft_bot_base + i as u32;
183        let b = shaft_bot_base + ((i + 1) % segments) as u32;
184        idx.extend_from_slice(&[shaft_bot_center, b, a]);
185    }
186
187    // Side quads: two rings of shaft.
188    let shaft_top_ring_base = verts.len() as u32;
189    ring_verts(&mut verts, shaft_bot, shaft_r, 0.0); // duplicate bottom ring for side normals
190    let shaft_top_ring_top = verts.len() as u32;
191    ring_verts(&mut verts, shaft_top, shaft_r, 0.0);
192    for i in 0..segments {
193        let a = shaft_top_ring_base + i as u32;
194        let b = shaft_top_ring_base + ((i + 1) % segments) as u32;
195        let c = shaft_top_ring_top + i as u32;
196        let d = shaft_top_ring_top + ((i + 1) % segments) as u32;
197        idx.extend_from_slice(&[a, b, d, a, d, c]);
198    }
199
200    // --- Cone ---
201    // Slanted normal angle for cone surface: rise=(cone_tip-cone_bot), run=cone_r.
202    let cone_len = ((cone_tip - cone_bot).powi(2) + cone_r * cone_r).sqrt();
203    let normal_z_cone = cone_r / cone_len; // outward Z component of slanted normal
204    let normal_r_cone = (cone_tip - cone_bot) / cone_len;
205
206    let cone_base_ring = verts.len() as u32;
207    for i in 0..segments {
208        let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
209        let (s, c) = angle.sin_cos();
210        verts.push(Vertex {
211            position: [c * cone_r, s * cone_r, cone_bot],
212            normal: [c * normal_r_cone, s * normal_r_cone, normal_z_cone],
213            colour: white,
214            uv: [0.0, 0.0],
215            tangent: [0.0, 0.0, 0.0, 1.0],
216        });
217    }
218
219    // Cone tip vertex (normals averaged around tip : just point up).
220    let cone_tip_v = verts.len() as u32;
221    verts.push(Vertex {
222        position: [0.0, 0.0, cone_tip],
223        normal: [0.0, 0.0, 1.0],
224        colour: white,
225        uv: [0.0, 0.0],
226        tangent: [0.0, 0.0, 0.0, 1.0],
227    });
228
229    for i in 0..segments {
230        let a = cone_base_ring + i as u32;
231        let b = cone_base_ring + ((i + 1) % segments) as u32;
232        idx.extend_from_slice(&[a, b, cone_tip_v]);
233    }
234
235    // Cone base cap (flat, faces -Z).
236    let cone_cap_base = verts.len() as u32;
237    for i in 0..segments {
238        let angle = 2.0 * std::f32::consts::PI * (i as f32) / (segments as f32);
239        let (s, c) = angle.sin_cos();
240        verts.push(Vertex {
241            position: [c * cone_r, s * cone_r, cone_bot],
242            normal: [0.0, 0.0, -1.0],
243            colour: white,
244            uv: [0.0, 0.0],
245            tangent: [0.0, 0.0, 0.0, 1.0],
246        });
247    }
248    let cone_cap_center = verts.len() as u32;
249    verts.push(Vertex {
250        position: [0.0, 0.0, cone_bot],
251        normal: [0.0, 0.0, -1.0],
252        colour: white,
253        uv: [0.0, 0.0],
254        tangent: [0.0, 0.0, 0.0, 1.0],
255    });
256    for i in 0..segments {
257        let a = cone_cap_base + i as u32;
258        let b = cone_cap_base + ((i + 1) % segments) as u32;
259        idx.extend_from_slice(&[cone_cap_center, b, a]);
260    }
261
262    (verts, idx)
263}
264
265// ---------------------------------------------------------------------------
266// Procedural icosphere (2 subdivisions, ~240 triangles)
267// ---------------------------------------------------------------------------
268
269/// Generate a unit sphere as an icosphere with 2 subdivisions.
270///
271/// Starts from a regular icosahedron and subdivides each triangle 2x.
272pub(super) fn build_glyph_sphere() -> (Vec<Vertex>, Vec<u32>) {
273    let white = [1.0f32, 1.0, 1.0, 1.0];
274
275    // Icosahedron constants.
276    let t = (1.0 + 5.0f32.sqrt()) / 2.0;
277
278    // 12 vertices of a regular icosahedron (not yet normalised).
279    let raw_verts = [
280        [-1.0, t, 0.0],
281        [1.0, t, 0.0],
282        [-1.0, -t, 0.0],
283        [1.0, -t, 0.0],
284        [0.0, -1.0, t],
285        [0.0, 1.0, t],
286        [0.0, -1.0, -t],
287        [0.0, 1.0, -t],
288        [t, 0.0, -1.0],
289        [t, 0.0, 1.0],
290        [-t, 0.0, -1.0],
291        [-t, 0.0, 1.0],
292    ];
293
294    let mut positions: Vec<[f32; 3]> = raw_verts
295        .iter()
296        .map(|v| {
297            let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
298            [v[0] / l, v[1] / l, v[2] / l]
299        })
300        .collect();
301
302    // 20 base triangles.
303    let mut triangles: Vec<[usize; 3]> = vec![
304        [0, 11, 5],
305        [0, 5, 1],
306        [0, 1, 7],
307        [0, 7, 10],
308        [0, 10, 11],
309        [1, 5, 9],
310        [5, 11, 4],
311        [11, 10, 2],
312        [10, 7, 6],
313        [7, 1, 8],
314        [3, 9, 4],
315        [3, 4, 2],
316        [3, 2, 6],
317        [3, 6, 8],
318        [3, 8, 9],
319        [4, 9, 5],
320        [2, 4, 11],
321        [6, 2, 10],
322        [8, 6, 7],
323        [9, 8, 1],
324    ];
325
326    // Subdivide 2 times.
327    for _ in 0..2 {
328        let mut mid_cache: std::collections::HashMap<(usize, usize), usize> =
329            std::collections::HashMap::new();
330        let mut new_triangles: Vec<[usize; 3]> = Vec::with_capacity(triangles.len() * 4);
331
332        let midpoint = |positions: &mut Vec<[f32; 3]>,
333                        a: usize,
334                        b: usize,
335                        cache: &mut std::collections::HashMap<(usize, usize), usize>|
336         -> usize {
337            let key = if a < b { (a, b) } else { (b, a) };
338            if let Some(&idx) = cache.get(&key) {
339                return idx;
340            }
341            let pa = positions[a];
342            let pb = positions[b];
343            let mx = (pa[0] + pb[0]) * 0.5;
344            let my = (pa[1] + pb[1]) * 0.5;
345            let mz = (pa[2] + pb[2]) * 0.5;
346            let l = (mx * mx + my * my + mz * mz).sqrt();
347            let idx = positions.len();
348            positions.push([mx / l, my / l, mz / l]);
349            cache.insert(key, idx);
350            idx
351        };
352
353        for tri in &triangles {
354            let a = tri[0];
355            let b = tri[1];
356            let c = tri[2];
357            let ab = midpoint(&mut positions, a, b, &mut mid_cache);
358            let bc = midpoint(&mut positions, b, c, &mut mid_cache);
359            let ca = midpoint(&mut positions, c, a, &mut mid_cache);
360            new_triangles.push([a, ab, ca]);
361            new_triangles.push([b, bc, ab]);
362            new_triangles.push([c, ca, bc]);
363            new_triangles.push([ab, bc, ca]);
364        }
365        triangles = new_triangles;
366    }
367
368    let verts: Vec<Vertex> = positions
369        .iter()
370        .map(|&p| Vertex {
371            position: p,
372            normal: p, // unit sphere: position = normal
373            colour: white,
374            uv: [0.0, 0.0],
375            tangent: [0.0, 0.0, 0.0, 1.0],
376        })
377        .collect();
378
379    let idx: Vec<u32> = triangles
380        .iter()
381        .flat_map(|t| [t[0] as u32, t[1] as u32, t[2] as u32])
382        .collect();
383
384    (verts, idx)
385}
386
387// ---------------------------------------------------------------------------
388// Attribute interpolation utilities
389// ---------------------------------------------------------------------------
390
391// ---------------------------------------------------------------------------
392// in-place attribute hot-swap
393// ---------------------------------------------------------------------------
394
395impl ViewportGpuResources {
396    /// Write new scalar data into an existing attribute buffer in-place.
397    ///
398    /// No GPU buffer reallocation, no mesh re-upload, no bind group rebuild is
399    /// required. The attribute bind group *will* be rebuilt on the next
400    /// `prepare()` call if the scalar range changes (tracked via `last_tex_key`).
401    ///
402    /// # Errors
403    ///
404    /// - [`ViewportError::MeshSlotEmpty`](crate::error::ViewportError::MeshSlotEmpty) : `mesh_id` not found in the store.
405    /// - [`ViewportError::AttributeNotFound`](crate::error::ViewportError::AttributeNotFound) : `name` not present on the mesh.
406    /// - [`ViewportError::AttributeLengthMismatch`](crate::error::ViewportError::AttributeLengthMismatch) : `data.len()` differs from
407    ///   the original upload (same-topology requirement).
408    pub fn replace_attribute(
409        &mut self,
410        queue: &wgpu::Queue,
411        mesh_id: crate::resources::mesh_store::MeshId,
412        name: &str,
413        data: &[f32],
414    ) -> crate::error::ViewportResult<()> {
415        // Resolve the mesh.
416        let gpu_mesh =
417            self.mesh_store
418                .get_mut(mesh_id)
419                .ok_or(crate::error::ViewportError::MeshSlotEmpty {
420                    index: mesh_id.index(),
421                })?;
422
423        // Find the existing attribute buffer.
424        let buffer = gpu_mesh.attribute_buffers.get(name).ok_or_else(|| {
425            crate::error::ViewportError::AttributeNotFound {
426                mesh_id: mesh_id.index(),
427                name: name.to_string(),
428            }
429        })?;
430
431        // Validate same topology (buffer size must match).
432        let expected_elems = (buffer.size() / 4) as usize;
433        if data.len() != expected_elems {
434            return Err(crate::error::ViewportError::AttributeLengthMismatch {
435                expected: expected_elems,
436                got: data.len(),
437            });
438        }
439
440        // Zero-copy in-place write via the wgpu staging belt.
441        queue.write_buffer(buffer, 0, bytemuck::cast_slice(data));
442
443        // Recompute scalar range so LUT mapping stays accurate.
444        let (min, max) = data
445            .iter()
446            .fold((f32::MAX, f32::MIN), |(mn, mx), &v| (mn.min(v), mx.max(v)));
447        let range = if min > max { (0.0, 1.0) } else { (min, max) };
448        gpu_mesh.attribute_ranges.insert(name.to_string(), range);
449
450        // Force bind group rebuild on next prepare() by invalidating the key.
451        gpu_mesh.last_tex_key = (
452            gpu_mesh.last_tex_key.0,
453            gpu_mesh.last_tex_key.1,
454            gpu_mesh.last_tex_key.2,
455            gpu_mesh.last_tex_key.3,
456            u64::MAX, // attribute hash component
457            gpu_mesh.last_tex_key.5,
458            gpu_mesh.last_tex_key.6,
459            gpu_mesh.last_tex_key.7,
460            gpu_mesh.last_tex_key.8,
461            gpu_mesh.last_tex_key.9,
462            gpu_mesh.last_tex_key.10,
463        );
464
465        Ok(())
466    }
467
468    /// Create a camera bind group (group 0) for the given per-viewport buffers.
469    ///
470    /// Per-viewport buffers (camera, clip planes, shadow info, clip volume) are
471    /// passed explicitly. Scene-global resources (lights, shadow atlas, IBL) come
472    /// from shared resources on `self`.
473    ///
474    /// NOTE: The initial bind group in `init.rs` is constructed inline (before
475    /// `Self` exists). Keep the binding layout in sync when modifying either site.
476    pub(crate) fn create_camera_bind_group(
477        &self,
478        device: &wgpu::Device,
479        camera_buf: &wgpu::Buffer,
480        clip_planes_buf: &wgpu::Buffer,
481        shadow_info_buf: &wgpu::Buffer,
482        clip_volume_buf: &wgpu::Buffer,
483        debug_frag_buf: &wgpu::Buffer,
484        label: &str,
485    ) -> wgpu::BindGroup {
486        let irr = self
487            .ibl_irradiance_view
488            .as_ref()
489            .unwrap_or(&self.ibl_fallback_view);
490        let spec = self
491            .ibl_prefiltered_view
492            .as_ref()
493            .unwrap_or(&self.ibl_fallback_view);
494        let brdf = self
495            .ibl_brdf_lut_view
496            .as_ref()
497            .unwrap_or(&self.ibl_fallback_brdf_view);
498        let skybox = self
499            .ibl_skybox_view
500            .as_ref()
501            .unwrap_or(&self.ibl_fallback_view);
502
503        device.create_bind_group(&wgpu::BindGroupDescriptor {
504            label: Some(label),
505            layout: &self.camera_bind_group_layout,
506            entries: &[
507                wgpu::BindGroupEntry {
508                    binding: 0,
509                    resource: camera_buf.as_entire_binding(),
510                },
511                wgpu::BindGroupEntry {
512                    binding: 1,
513                    resource: wgpu::BindingResource::TextureView(&self.shadow_map_view),
514                },
515                wgpu::BindGroupEntry {
516                    binding: 2,
517                    resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
518                },
519                wgpu::BindGroupEntry {
520                    binding: 3,
521                    resource: self.light_uniform_buf.as_entire_binding(),
522                },
523                wgpu::BindGroupEntry {
524                    binding: 4,
525                    resource: clip_planes_buf.as_entire_binding(),
526                },
527                wgpu::BindGroupEntry {
528                    binding: 5,
529                    resource: shadow_info_buf.as_entire_binding(),
530                },
531                wgpu::BindGroupEntry {
532                    binding: 6,
533                    resource: clip_volume_buf.as_entire_binding(),
534                },
535                wgpu::BindGroupEntry {
536                    binding: 7,
537                    resource: wgpu::BindingResource::TextureView(irr),
538                },
539                wgpu::BindGroupEntry {
540                    binding: 8,
541                    resource: wgpu::BindingResource::TextureView(spec),
542                },
543                wgpu::BindGroupEntry {
544                    binding: 9,
545                    resource: wgpu::BindingResource::TextureView(brdf),
546                },
547                wgpu::BindGroupEntry {
548                    binding: 10,
549                    resource: wgpu::BindingResource::Sampler(&self.ibl_sampler),
550                },
551                wgpu::BindGroupEntry {
552                    binding: 11,
553                    resource: wgpu::BindingResource::TextureView(skybox),
554                },
555                wgpu::BindGroupEntry {
556                    binding: 12,
557                    resource: debug_frag_buf.as_entire_binding(),
558                },
559                wgpu::BindGroupEntry {
560                    binding: 13,
561                    resource: self.light_storage_buf.as_entire_binding(),
562                },
563                wgpu::BindGroupEntry {
564                    binding: 14,
565                    resource: self.clustered.grid_uniform_buf.as_entire_binding(),
566                },
567                wgpu::BindGroupEntry {
568                    binding: 15,
569                    resource: self.clustered.cluster_grid_buf.as_entire_binding(),
570                },
571                wgpu::BindGroupEntry {
572                    binding: 16,
573                    resource: self.clustered.light_index_buf.as_entire_binding(),
574                },
575                wgpu::BindGroupEntry {
576                    binding: 17,
577                    resource: wgpu::BindingResource::TextureView(&self.point_shadow_cube_view),
578                },
579            ],
580        })
581    }
582}
583
584// ---------------------------------------------------------------------------
585// GPU compute filter pipeline and dispatch
586// ---------------------------------------------------------------------------
587
588/// Output from a single GPU compute filter dispatch.
589///
590/// Contains a compacted index buffer (triangles that passed the filter)
591/// and the count of valid indices. The renderer swaps this in during draw.
592pub struct ComputeFilterResult {
593    /// Output index buffer containing only passing triangles.
594    pub index_buffer: wgpu::Buffer,
595    /// Number of valid indices in `index_buffer` (may be 0 if all filtered).
596    pub index_count: u32,
597    /// `MeshId` this result corresponds to.
598    pub mesh_id: crate::resources::mesh_store::MeshId,
599}
600
601impl ViewportGpuResources {
602    /// Lazily create the GPU compute filter pipeline on first use.
603    fn ensure_compute_filter_pipeline(&mut self, device: &wgpu::Device) {
604        if self.compute_filter_pipeline.is_some() {
605            return;
606        }
607
608        // Build bind group layout.
609        let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
610            label: Some("compute_filter_bgl"),
611            entries: &[
612                // binding 0: params uniform
613                wgpu::BindGroupLayoutEntry {
614                    binding: 0,
615                    visibility: wgpu::ShaderStages::COMPUTE,
616                    ty: wgpu::BindingType::Buffer {
617                        ty: wgpu::BufferBindingType::Uniform,
618                        has_dynamic_offset: false,
619                        min_binding_size: None,
620                    },
621                    count: None,
622                },
623                // binding 1: vertices (f32 storage, read)
624                wgpu::BindGroupLayoutEntry {
625                    binding: 1,
626                    visibility: wgpu::ShaderStages::COMPUTE,
627                    ty: wgpu::BindingType::Buffer {
628                        ty: wgpu::BufferBindingType::Storage { read_only: true },
629                        has_dynamic_offset: false,
630                        min_binding_size: None,
631                    },
632                    count: None,
633                },
634                // binding 2: source indices (u32 storage, read)
635                wgpu::BindGroupLayoutEntry {
636                    binding: 2,
637                    visibility: wgpu::ShaderStages::COMPUTE,
638                    ty: wgpu::BindingType::Buffer {
639                        ty: wgpu::BufferBindingType::Storage { read_only: true },
640                        has_dynamic_offset: false,
641                        min_binding_size: None,
642                    },
643                    count: None,
644                },
645                // binding 3: scalars (f32 storage, read) : dummy for Clip
646                wgpu::BindGroupLayoutEntry {
647                    binding: 3,
648                    visibility: wgpu::ShaderStages::COMPUTE,
649                    ty: wgpu::BindingType::Buffer {
650                        ty: wgpu::BufferBindingType::Storage { read_only: true },
651                        has_dynamic_offset: false,
652                        min_binding_size: None,
653                    },
654                    count: None,
655                },
656                // binding 4: output compacted indices (read_write)
657                wgpu::BindGroupLayoutEntry {
658                    binding: 4,
659                    visibility: wgpu::ShaderStages::COMPUTE,
660                    ty: wgpu::BindingType::Buffer {
661                        ty: wgpu::BufferBindingType::Storage { read_only: false },
662                        has_dynamic_offset: false,
663                        min_binding_size: None,
664                    },
665                    count: None,
666                },
667                // binding 5: atomic counter (read_write)
668                wgpu::BindGroupLayoutEntry {
669                    binding: 5,
670                    visibility: wgpu::ShaderStages::COMPUTE,
671                    ty: wgpu::BindingType::Buffer {
672                        ty: wgpu::BufferBindingType::Storage { read_only: false },
673                        has_dynamic_offset: false,
674                        min_binding_size: None,
675                    },
676                    count: None,
677                },
678            ],
679        });
680
681        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
682            label: Some("compute_filter_layout"),
683            bind_group_layouts: &[&bgl],
684            push_constant_ranges: &[],
685        });
686
687        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
688            label: Some("compute_filter_shader"),
689            source: wgpu::ShaderSource::Wgsl(
690                include_str!(concat!(env!("OUT_DIR"), "/compute_filter.wgsl")).into(),
691            ),
692        });
693
694        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
695            label: Some("compute_filter_pipeline"),
696            layout: Some(&pipeline_layout),
697            module: &shader,
698            entry_point: Some("main"),
699            compilation_options: Default::default(),
700            cache: None,
701        });
702
703        self.compute_filter_bgl = Some(bgl);
704        self.compute_filter_pipeline = Some(pipeline);
705    }
706
707    /// Dispatch GPU compute filters for all items in the list.
708    ///
709    /// Returns one [`ComputeFilterResult`] per item. The renderer uses these
710    /// during `paint()` to override the mesh's default index buffer.
711    ///
712    /// This is a synchronous v1 implementation: it submits each dispatch
713    /// individually and polls the device to read back the counter. This is
714    /// acceptable for v1; async readback can be added later.
715    pub fn run_compute_filters(
716        &mut self,
717        device: &wgpu::Device,
718        queue: &wgpu::Queue,
719        items: &[crate::renderer::ComputeFilterItem],
720    ) -> Vec<ComputeFilterResult> {
721        if items.is_empty() {
722            return Vec::new();
723        }
724
725        self.ensure_compute_filter_pipeline(device);
726
727        // Dummy 4-byte buffer used as the scalar binding when doing a Clip filter.
728        let dummy_scalar_buf = device.create_buffer(&wgpu::BufferDescriptor {
729            label: Some("compute_filter_dummy_scalar"),
730            size: 4,
731            usage: wgpu::BufferUsages::STORAGE,
732            mapped_at_creation: false,
733        });
734
735        let mut results = Vec::with_capacity(items.len());
736
737        for item in items {
738            // Resolve the mesh.
739            let gpu_mesh = match self.mesh_store.get(item.mesh_id) {
740                Some(m) => m,
741                None => continue,
742            };
743
744            let triangle_count = gpu_mesh.index_count / 3;
745            if triangle_count == 0 {
746                continue;
747            }
748
749            // Vertex stride: the Vertex struct is 64 bytes = 16 f32s.
750            const VERTEX_STRIDE_F32: u32 = 16;
751
752            // Build params uniform matching compute_filter.wgsl Params struct layout.
753            #[repr(C)]
754            #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
755            struct FilterParams {
756                mode: u32,
757                clip_type: u32,
758                threshold_min: f32,
759                threshold_max: f32,
760                triangle_count: u32,
761                vertex_stride_f32: u32,
762                _pad: [u32; 2],
763                // Plane params
764                plane_nx: f32,
765                plane_ny: f32,
766                plane_nz: f32,
767                plane_dist: f32,
768                // Box params
769                box_cx: f32,
770                box_cy: f32,
771                box_cz: f32,
772                _padb0: f32,
773                box_hex: f32,
774                box_hey: f32,
775                box_hez: f32,
776                _padb1: f32,
777                box_col0x: f32,
778                box_col0y: f32,
779                box_col0z: f32,
780                _padb2: f32,
781                box_col1x: f32,
782                box_col1y: f32,
783                box_col1z: f32,
784                _padb3: f32,
785                box_col2x: f32,
786                box_col2y: f32,
787                box_col2z: f32,
788                _padb4: f32,
789                // Sphere params
790                sphere_cx: f32,
791                sphere_cy: f32,
792                sphere_cz: f32,
793                sphere_radius: f32,
794            }
795
796            let mut params: FilterParams = bytemuck::Zeroable::zeroed();
797            params.triangle_count = triangle_count;
798            params.vertex_stride_f32 = VERTEX_STRIDE_F32;
799
800            match item.kind {
801                crate::renderer::ComputeFilterKind::Clip {
802                    plane_normal,
803                    plane_dist,
804                } => {
805                    params.mode = 0;
806                    params.clip_type = 1;
807                    params.plane_nx = plane_normal[0];
808                    params.plane_ny = plane_normal[1];
809                    params.plane_nz = plane_normal[2];
810                    params.plane_dist = plane_dist;
811                }
812                crate::renderer::ComputeFilterKind::ClipBox {
813                    center,
814                    half_extents,
815                    orientation,
816                } => {
817                    params.mode = 0;
818                    params.clip_type = 2;
819                    params.box_cx = center[0];
820                    params.box_cy = center[1];
821                    params.box_cz = center[2];
822                    params.box_hex = half_extents[0];
823                    params.box_hey = half_extents[1];
824                    params.box_hez = half_extents[2];
825                    params.box_col0x = orientation[0][0];
826                    params.box_col0y = orientation[0][1];
827                    params.box_col0z = orientation[0][2];
828                    params.box_col1x = orientation[1][0];
829                    params.box_col1y = orientation[1][1];
830                    params.box_col1z = orientation[1][2];
831                    params.box_col2x = orientation[2][0];
832                    params.box_col2y = orientation[2][1];
833                    params.box_col2z = orientation[2][2];
834                }
835                crate::renderer::ComputeFilterKind::ClipSphere { center, radius } => {
836                    params.mode = 0;
837                    params.clip_type = 3;
838                    params.sphere_cx = center[0];
839                    params.sphere_cy = center[1];
840                    params.sphere_cz = center[2];
841                    params.sphere_radius = radius;
842                }
843                crate::renderer::ComputeFilterKind::Threshold { min, max } => {
844                    params.mode = 1;
845                    params.threshold_min = min;
846                    params.threshold_max = max;
847                }
848            }
849
850            let params_buf = device.create_buffer(&wgpu::BufferDescriptor {
851                label: Some("compute_filter_params"),
852                size: std::mem::size_of::<FilterParams>() as u64,
853                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
854                mapped_at_creation: false,
855            });
856            queue.write_buffer(&params_buf, 0, bytemuck::bytes_of(&params));
857
858            // Output index buffer (worst-case: all triangles pass).
859            let out_index_size = (gpu_mesh.index_count as u64) * 4;
860            let out_index_buf = device.create_buffer(&wgpu::BufferDescriptor {
861                label: Some("compute_filter_out_indices"),
862                size: out_index_size.max(4),
863                usage: wgpu::BufferUsages::STORAGE
864                    | wgpu::BufferUsages::INDEX
865                    | wgpu::BufferUsages::COPY_SRC,
866                mapped_at_creation: false,
867            });
868
869            // 4-byte atomic counter buffer (cleared to 0).
870            let counter_buf = device.create_buffer(&wgpu::BufferDescriptor {
871                label: Some("compute_filter_counter"),
872                size: 4,
873                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
874                mapped_at_creation: true,
875            });
876            {
877                let mut view = counter_buf.slice(..).get_mapped_range_mut();
878                view[0..4].copy_from_slice(&0u32.to_le_bytes());
879            }
880            counter_buf.unmap();
881
882            // Staging buffer to read back the counter.
883            let staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
884                label: Some("compute_filter_counter_staging"),
885                size: 4,
886                usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
887                mapped_at_creation: false,
888            });
889
890            // Pick the scalar buffer: named attribute or dummy.
891            let scalar_buf_ref: &wgpu::Buffer = match &item.kind {
892                crate::renderer::ComputeFilterKind::Threshold { .. } => {
893                    if let Some(attr_name) = &item.attribute_name {
894                        gpu_mesh
895                            .attribute_buffers
896                            .get(attr_name.as_str())
897                            .unwrap_or(&dummy_scalar_buf)
898                    } else {
899                        &dummy_scalar_buf
900                    }
901                }
902                // Clip variants don't use the scalar buffer.
903                _ => &dummy_scalar_buf,
904            };
905
906            // Build bind group.
907            let bgl = self.compute_filter_bgl.as_ref().unwrap();
908            let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
909                label: Some("compute_filter_bg"),
910                layout: bgl,
911                entries: &[
912                    wgpu::BindGroupEntry {
913                        binding: 0,
914                        resource: params_buf.as_entire_binding(),
915                    },
916                    wgpu::BindGroupEntry {
917                        binding: 1,
918                        resource: gpu_mesh.vertex_buffer.as_entire_binding(),
919                    },
920                    wgpu::BindGroupEntry {
921                        binding: 2,
922                        resource: gpu_mesh.index_buffer.as_entire_binding(),
923                    },
924                    wgpu::BindGroupEntry {
925                        binding: 3,
926                        resource: scalar_buf_ref.as_entire_binding(),
927                    },
928                    wgpu::BindGroupEntry {
929                        binding: 4,
930                        resource: out_index_buf.as_entire_binding(),
931                    },
932                    wgpu::BindGroupEntry {
933                        binding: 5,
934                        resource: counter_buf.as_entire_binding(),
935                    },
936                ],
937            });
938
939            // Encode and submit compute + counter copy.
940            let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
941                label: Some("compute_filter_encoder"),
942            });
943
944            {
945                let pipeline = self.compute_filter_pipeline.as_ref().unwrap();
946                let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
947                    label: Some("compute_filter_pass"),
948                    timestamp_writes: None,
949                });
950                cpass.set_pipeline(pipeline);
951                cpass.set_bind_group(0, &bind_group, &[]);
952                let workgroups = triangle_count.div_ceil(64);
953                cpass.dispatch_workgroups(workgroups, 1, 1);
954            }
955
956            encoder.copy_buffer_to_buffer(&counter_buf, 0, &staging_buf, 0, 4);
957            queue.submit(std::iter::once(encoder.finish()));
958
959            // Synchronous readback (v1 : acceptable; async readback can follow later).
960            let slice = staging_buf.slice(..);
961            slice.map_async(wgpu::MapMode::Read, |_| {});
962            let _ = device.poll(wgpu::PollType::Wait {
963                submission_index: None,
964                timeout: Some(std::time::Duration::from_secs(5)),
965            });
966
967            let index_count = {
968                let data = slice.get_mapped_range();
969                u32::from_le_bytes([data[0], data[1], data[2], data[3]])
970            };
971            staging_buf.unmap();
972
973            results.push(ComputeFilterResult {
974                index_buffer: out_index_buf,
975                index_count,
976                mesh_id: item.mesh_id,
977            });
978        }
979
980        results
981    }
982
983    // -----------------------------------------------------------------------
984    // GPU object-ID picking pipeline (lazily created)
985    // -----------------------------------------------------------------------
986
987    /// Lazily create the GPU pick pipeline and associated bind group layouts.
988    ///
989    /// No-op if already created. Called from `ViewportRenderer::pick_scene_gpu`
990    /// on first invocation : zero overhead when GPU picking is never used.
991    pub(crate) fn ensure_pick_pipeline(&mut self, device: &wgpu::Device) {
992        if self.pick_pipeline.is_some() {
993            return;
994        }
995
996        // --- group 0: pick camera bind group layout ---
997        // Includes binding 0 (CameraUniform) and binding 6 (ClipVolumesUniform).
998        // The full camera_bind_group_layout has many more bindings; a separate
999        // minimal layout is cleaner and avoids binding unused resources.
1000        let pick_camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1001            label: Some("pick_camera_bgl"),
1002            entries: &[
1003                wgpu::BindGroupLayoutEntry {
1004                    binding: 0,
1005                    visibility: wgpu::ShaderStages::VERTEX,
1006                    ty: wgpu::BindingType::Buffer {
1007                        ty: wgpu::BufferBindingType::Uniform,
1008                        has_dynamic_offset: false,
1009                        min_binding_size: None,
1010                    },
1011                    count: None,
1012                },
1013                wgpu::BindGroupLayoutEntry {
1014                    binding: 6,
1015                    visibility: wgpu::ShaderStages::FRAGMENT,
1016                    ty: wgpu::BindingType::Buffer {
1017                        ty: wgpu::BufferBindingType::Uniform,
1018                        has_dynamic_offset: false,
1019                        min_binding_size: None,
1020                    },
1021                    count: None,
1022                },
1023            ],
1024        });
1025
1026        // --- group 1: PickInstance storage buffer ---
1027        let pick_instance_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1028            label: Some("pick_instance_bgl"),
1029            entries: &[wgpu::BindGroupLayoutEntry {
1030                binding: 0,
1031                visibility: wgpu::ShaderStages::VERTEX,
1032                ty: wgpu::BindingType::Buffer {
1033                    ty: wgpu::BufferBindingType::Storage { read_only: true },
1034                    has_dynamic_offset: false,
1035                    min_binding_size: None,
1036                },
1037                count: None,
1038            }],
1039        });
1040
1041        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1042            label: Some("pick_id_shader"),
1043            source: wgpu::ShaderSource::Wgsl(
1044                include_str!(concat!(env!("OUT_DIR"), "/pick_id.wgsl")).into(),
1045            ),
1046        });
1047
1048        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1049            label: Some("pick_pipeline_layout"),
1050            bind_group_layouts: &[&pick_camera_bgl, &pick_instance_bgl],
1051            push_constant_ranges: &[],
1052        });
1053
1054        // Vertex layout: reuse the 64-byte Vertex stride but only declare position (location 0).
1055        let pick_vertex_layout = wgpu::VertexBufferLayout {
1056            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, // 64 bytes
1057            step_mode: wgpu::VertexStepMode::Vertex,
1058            attributes: &[wgpu::VertexAttribute {
1059                offset: 0,
1060                shader_location: 0,
1061                format: wgpu::VertexFormat::Float32x3,
1062            }],
1063        };
1064
1065        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1066            label: Some("pick_pipeline"),
1067            layout: Some(&layout),
1068            vertex: wgpu::VertexState {
1069                module: &shader,
1070                entry_point: Some("vs_main"),
1071                buffers: &[pick_vertex_layout],
1072                compilation_options: wgpu::PipelineCompilationOptions::default(),
1073            },
1074            fragment: Some(wgpu::FragmentState {
1075                module: &shader,
1076                entry_point: Some("fs_main"),
1077                targets: &[
1078                    // location 0: R32Uint object ID
1079                    Some(wgpu::ColorTargetState {
1080                        format: wgpu::TextureFormat::R32Uint,
1081                        blend: None, // replace : no blending for integer targets
1082                        write_mask: wgpu::ColorWrites::ALL,
1083                    }),
1084                    // location 1: R32Float depth
1085                    Some(wgpu::ColorTargetState {
1086                        format: wgpu::TextureFormat::R32Float,
1087                        blend: None,
1088                        write_mask: wgpu::ColorWrites::ALL,
1089                    }),
1090                ],
1091                compilation_options: wgpu::PipelineCompilationOptions::default(),
1092            }),
1093            primitive: wgpu::PrimitiveState {
1094                topology: wgpu::PrimitiveTopology::TriangleList,
1095                front_face: wgpu::FrontFace::Ccw,
1096                cull_mode: None, // No culling: 3D meshes are often rendered two-sided; pick both faces.
1097                ..Default::default()
1098            },
1099            depth_stencil: Some(wgpu::DepthStencilState {
1100                format: wgpu::TextureFormat::Depth24PlusStencil8,
1101                depth_write_enabled: true,
1102                depth_compare: wgpu::CompareFunction::Less,
1103                stencil: wgpu::StencilState::default(),
1104                bias: wgpu::DepthBiasState::default(),
1105            }),
1106            multisample: wgpu::MultisampleState {
1107                count: 1, // pick pass is always 1x (no MSAA)
1108                ..Default::default()
1109            },
1110            multiview: None,
1111            cache: None,
1112        });
1113
1114        self.pick_camera_bgl = Some(pick_camera_bgl);
1115        self.pick_bind_group_layout_1 = Some(pick_instance_bgl);
1116        self.pick_pipeline = Some(pipeline);
1117    }
1118}
1119
1120// ---------------------------------------------------------------------------
1121// Attribute interpolation utilities
1122// ---------------------------------------------------------------------------
1123
1124/// Linearly interpolate between two attribute buffers element-wise.
1125///
1126/// Both slices must have the same length. `t` is clamped to `[0.0, 1.0]`.
1127/// Returns a new `Vec<f32>` with `a[i] * (1 - t) + b[i] * t`.
1128///
1129/// Use this to blend per-vertex scalar attributes between two consecutive
1130/// timesteps when scrubbing the timeline at sub-frame resolution.
1131pub fn lerp_attributes(a: &[f32], b: &[f32], t: f32) -> Vec<f32> {
1132    let t = t.clamp(0.0, 1.0);
1133    let one_minus_t = 1.0 - t;
1134    a.iter()
1135        .zip(b.iter())
1136        .map(|(&av, &bv)| av * one_minus_t + bv * t)
1137        .collect()
1138}