1use super::types::{ClipShape, SceneEffects, ViewportEffects};
2use super::*;
3use wgpu::util::DeviceExt;
4
5impl ViewportRenderer {
6 pub(super) fn prepare_scene_internal(
14 &mut self,
15 device: &wgpu::Device,
16 queue: &wgpu::Queue,
17 frame: &FrameData,
18 scene_fx: &SceneEffects<'_>,
19 ) {
20 if !scene_fx.compute_filter_items.is_empty() {
23 self.compute_filter_results =
24 self.resources
25 .run_compute_filters(device, queue, scene_fx.compute_filter_items);
26 } else {
27 self.compute_filter_results.clear();
28 }
29
30 self.resources.ensure_colormaps_initialized(device, queue);
32 self.resources.ensure_matcaps_initialized(device, queue);
33
34 let resources = &mut self.resources;
35 let lighting = scene_fx.lighting;
36
37 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
39 SurfaceSubmission::Flat(items) => items.as_ref(),
40 };
41
42 let (shadow_center, shadow_extent) = if let Some(extent) = lighting.shadow_extent_override {
44 (glam::Vec3::ZERO, extent)
45 } else {
46 (glam::Vec3::ZERO, 20.0)
47 };
48
49 fn compute_shadow_matrix(
51 kind: &LightKind,
52 shadow_center: glam::Vec3,
53 shadow_extent: f32,
54 ) -> glam::Mat4 {
55 match kind {
56 LightKind::Directional { direction } => {
57 let dir = glam::Vec3::from(*direction).normalize();
58 let light_up = if dir.z.abs() > 0.99 {
59 glam::Vec3::Y
60 } else {
61 glam::Vec3::Z
62 };
63 let light_pos = shadow_center + dir * shadow_extent * 2.0;
64 let light_view = glam::Mat4::look_at_rh(light_pos, shadow_center, light_up);
65 let light_proj = glam::Mat4::orthographic_rh(
66 -shadow_extent,
67 shadow_extent,
68 -shadow_extent,
69 shadow_extent,
70 0.01,
71 shadow_extent * 5.0,
72 );
73 light_proj * light_view
74 }
75 LightKind::Point { position, range } => {
76 let pos = glam::Vec3::from(*position);
77 let to_center = (shadow_center - pos).normalize();
78 let light_up = if to_center.z.abs() > 0.99 {
79 glam::Vec3::Y
80 } else {
81 glam::Vec3::Z
82 };
83 let light_view = glam::Mat4::look_at_rh(pos, shadow_center, light_up);
84 let light_proj =
85 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
86 light_proj * light_view
87 }
88 LightKind::Spot {
89 position,
90 direction,
91 range,
92 ..
93 } => {
94 let pos = glam::Vec3::from(*position);
95 let dir = glam::Vec3::from(*direction).normalize();
96 let look_target = pos + dir;
97 let up = if dir.z.abs() > 0.99 {
98 glam::Vec3::Y
99 } else {
100 glam::Vec3::Z
101 };
102 let light_view = glam::Mat4::look_at_rh(pos, look_target, up);
103 let light_proj =
104 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
105 light_proj * light_view
106 }
107 }
108 }
109
110 fn build_single_light_uniform(
112 src: &LightSource,
113 shadow_center: glam::Vec3,
114 shadow_extent: f32,
115 compute_shadow: bool,
116 ) -> SingleLightUniform {
117 let shadow_mat = if compute_shadow {
118 compute_shadow_matrix(&src.kind, shadow_center, shadow_extent)
119 } else {
120 glam::Mat4::IDENTITY
121 };
122
123 match &src.kind {
124 LightKind::Directional { direction } => SingleLightUniform {
125 light_view_proj: shadow_mat.to_cols_array_2d(),
126 pos_or_dir: *direction,
127 light_type: 0,
128 color: src.color,
129 intensity: src.intensity,
130 range: 0.0,
131 inner_angle: 0.0,
132 outer_angle: 0.0,
133 _pad_align: 0,
134 spot_direction: [0.0, -1.0, 0.0],
135 _pad: [0.0; 5],
136 },
137 LightKind::Point { position, range } => SingleLightUniform {
138 light_view_proj: shadow_mat.to_cols_array_2d(),
139 pos_or_dir: *position,
140 light_type: 1,
141 color: src.color,
142 intensity: src.intensity,
143 range: *range,
144 inner_angle: 0.0,
145 outer_angle: 0.0,
146 _pad_align: 0,
147 spot_direction: [0.0, -1.0, 0.0],
148 _pad: [0.0; 5],
149 },
150 LightKind::Spot {
151 position,
152 direction,
153 range,
154 inner_angle,
155 outer_angle,
156 } => SingleLightUniform {
157 light_view_proj: shadow_mat.to_cols_array_2d(),
158 pos_or_dir: *position,
159 light_type: 2,
160 color: src.color,
161 intensity: src.intensity,
162 range: *range,
163 inner_angle: *inner_angle,
164 outer_angle: *outer_angle,
165 _pad_align: 0,
166 spot_direction: *direction,
167 _pad: [0.0; 5],
168 },
169 }
170 }
171
172 let light_count = lighting.lights.len().min(8) as u32;
174 let mut lights_arr = [SingleLightUniform {
175 light_view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
176 pos_or_dir: [0.0; 3],
177 light_type: 0,
178 color: [1.0; 3],
179 intensity: 1.0,
180 range: 0.0,
181 inner_angle: 0.0,
182 outer_angle: 0.0,
183 _pad_align: 0,
184 spot_direction: [0.0, -1.0, 0.0],
185 _pad: [0.0; 5],
186 }; 8];
187
188 for (i, src) in lighting.lights.iter().take(8).enumerate() {
189 lights_arr[i] = build_single_light_uniform(src, shadow_center, shadow_extent, i == 0);
190 }
191
192 let cascade_count = lighting.shadow_cascade_count.clamp(1, 4) as usize;
197 let atlas_res = lighting.shadow_atlas_resolution.max(64);
198 let tile_size = atlas_res / 2;
199
200 let cascade_splits = compute_cascade_splits(
201 frame.camera.render_camera.near.max(0.01),
202 frame.camera.render_camera.far.max(1.0),
203 cascade_count as u32,
204 lighting.cascade_split_lambda,
205 );
206
207 let light_dir_for_csm = if light_count > 0 {
208 match &lighting.lights[0].kind {
209 LightKind::Directional { direction } => glam::Vec3::from(*direction).normalize(),
210 LightKind::Point { position, .. } => {
211 (glam::Vec3::from(*position) - shadow_center).normalize()
212 }
213 LightKind::Spot {
214 position,
215 direction,
216 ..
217 } => {
218 let _ = position;
219 glam::Vec3::from(*direction).normalize()
220 }
221 }
222 } else {
223 glam::Vec3::new(0.3, 1.0, 0.5).normalize()
224 };
225
226 let mut cascade_view_projs = [glam::Mat4::IDENTITY; 4];
227 let mut cascade_split_distances = [0.0f32; 4];
229
230 let use_csm = light_count > 0
232 && matches!(lighting.lights[0].kind, LightKind::Directional { .. })
233 && frame.camera.render_camera.view != glam::Mat4::IDENTITY;
234
235 if use_csm {
236 for i in 0..cascade_count {
237 let split_near = if i == 0 {
238 frame.camera.render_camera.near.max(0.01)
239 } else {
240 cascade_splits[i - 1]
241 };
242 let split_far = cascade_splits[i];
243 cascade_view_projs[i] = compute_cascade_matrix(
244 light_dir_for_csm,
245 frame.camera.render_camera.view,
246 frame.camera.render_camera.fov,
247 frame.camera.render_camera.aspect,
248 split_near,
249 split_far,
250 tile_size as f32,
251 );
252 cascade_split_distances[i] = split_far;
253 }
254 } else {
255 let primary_shadow_mat = if light_count > 0 {
257 compute_shadow_matrix(&lighting.lights[0].kind, shadow_center, shadow_extent)
258 } else {
259 glam::Mat4::IDENTITY
260 };
261 cascade_view_projs[0] = primary_shadow_mat;
262 cascade_split_distances[0] = frame.camera.render_camera.far;
263 }
264 let effective_cascade_count = if use_csm { cascade_count } else { 1 };
265
266 let atlas_rects: [[f32; 4]; 8] = [
269 [0.0, 0.0, 0.5, 0.5], [0.5, 0.0, 1.0, 0.5], [0.0, 0.5, 0.5, 1.0], [0.5, 0.5, 1.0, 1.0], [0.0; 4],
274 [0.0; 4],
275 [0.0; 4],
276 [0.0; 4], ];
278
279 {
281 let mut vp_data = [[0.0f32; 4]; 16]; for c in 0..4 {
283 let cols = cascade_view_projs[c].to_cols_array_2d();
284 for row in 0..4 {
285 vp_data[c * 4 + row] = cols[row];
286 }
287 }
288 let shadow_atlas_uniform = ShadowAtlasUniform {
289 cascade_view_proj: vp_data,
290 cascade_splits: cascade_split_distances,
291 cascade_count: effective_cascade_count as u32,
292 atlas_size: atlas_res as f32,
293 shadow_filter: match lighting.shadow_filter {
294 ShadowFilter::Pcf => 0,
295 ShadowFilter::Pcss => 1,
296 },
297 pcss_light_radius: lighting.pcss_light_radius,
298 atlas_rects,
299 };
300 queue.write_buffer(
301 &resources.shadow_info_buf,
302 0,
303 bytemuck::cast_slice(&[shadow_atlas_uniform]),
304 );
305 for slot in &self.viewport_slots {
308 queue.write_buffer(
309 &slot.shadow_info_buf,
310 0,
311 bytemuck::cast_slice(&[shadow_atlas_uniform]),
312 );
313 }
314 }
315
316 let _primary_shadow_mat = cascade_view_projs[0];
319 self.last_cascade0_shadow_mat = cascade_view_projs[0];
321
322 let (ibl_enabled, ibl_intensity, ibl_rotation, show_skybox) =
325 if let Some(env) = scene_fx.environment {
326 if resources.ibl_irradiance_view.is_some() {
327 (
328 1u32,
329 env.intensity,
330 env.rotation,
331 if env.show_skybox { 1u32 } else { 0 },
332 )
333 } else {
334 (0, 0.0, 0.0, 0)
335 }
336 } else {
337 (0, 0.0, 0.0, 0)
338 };
339
340 let lights_uniform = LightsUniform {
341 count: light_count,
342 shadow_bias: lighting.shadow_bias,
343 shadows_enabled: if lighting.shadows_enabled { 1 } else { 0 },
344 _pad: 0,
345 sky_color: lighting.sky_color,
346 hemisphere_intensity: lighting.hemisphere_intensity,
347 ground_color: lighting.ground_color,
348 _pad2: 0.0,
349 lights: lights_arr,
350 ibl_enabled,
351 ibl_intensity,
352 ibl_rotation,
353 show_skybox,
354 };
355 queue.write_buffer(
356 &resources.light_uniform_buf,
357 0,
358 bytemuck::cast_slice(&[lights_uniform]),
359 );
360
361 const SHADOW_SLOT_STRIDE: u64 = 256;
365 for c in 0..4usize {
366 queue.write_buffer(
367 &resources.shadow_uniform_buf,
368 c as u64 * SHADOW_SLOT_STRIDE,
369 bytemuck::cast_slice(&cascade_view_projs[c].to_cols_array_2d()),
370 );
371 }
372
373 let visible_count = scene_items.iter().filter(|i| i.visible).count();
376 let prev_use_instancing = self.use_instancing;
377 self.use_instancing = visible_count > INSTANCING_THRESHOLD;
378
379 if self.use_instancing != prev_use_instancing {
382 self.instanced_batches.clear();
383 self.last_scene_generation = u64::MAX;
384 self.last_scene_items_count = usize::MAX;
385 }
386
387 let has_scalar_items = scene_items.iter().any(|i| i.active_attribute.is_some());
391 let has_two_sided_items = scene_items
392 .iter()
393 .any(|i| i.material.is_two_sided());
394 let has_matcap_items = scene_items.iter().any(|i| i.material.matcap_id.is_some());
395 let has_param_vis_items = scene_items.iter().any(|i| i.material.param_vis.is_some());
396 let has_wireframe_items = scene_items.iter().any(|i| i.render_as_wireframe);
397 let mut wireframe_uniforms: Vec<ObjectUniform> = Vec::new();
401 let collect_wf_uniforms = frame.viewport.wireframe_mode;
402 if !self.use_instancing
403 || frame.viewport.wireframe_mode
404 || has_scalar_items
405 || has_two_sided_items
406 || has_matcap_items
407 || has_param_vis_items
408 || has_wireframe_items
409 {
410 for item in scene_items {
411 if self.use_instancing
416 && !frame.viewport.wireframe_mode
417 && item.active_attribute.is_none()
418 && !item.material.is_two_sided()
419 && item.material.matcap_id.is_none()
420 && item.material.param_vis.is_none()
421 && !item.render_as_wireframe
422 && item.warp_attribute.is_none()
423 {
424 continue;
425 }
426
427 if resources
428 .mesh_store
429 .get(item.mesh_id)
430 .is_none()
431 {
432 tracing::warn!(
433 mesh_index = item.mesh_id.index(),
434 "scene item mesh_index invalid, skipping"
435 );
436 continue;
437 };
438 let m = &item.material;
439 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
441 let range = item
442 .scalar_range
443 .or_else(|| {
444 resources
445 .mesh_store
446 .get(item.mesh_id)
447 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
448 })
449 .unwrap_or((0.0, 1.0));
450 (1u32, range.0, range.1)
451 } else {
452 (0u32, 0.0, 1.0)
453 };
454 let obj_uniform = ObjectUniform {
455 model: item.model,
456 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
457 selected: if item.selected { 1 } else { 0 },
458 wireframe: if frame.viewport.wireframe_mode || item.render_as_wireframe { 1 } else { 0 },
459 ambient: m.ambient,
460 diffuse: m.diffuse,
461 specular: m.specular,
462 shininess: m.shininess,
463 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
464 use_pbr: if m.use_pbr { 1 } else { 0 },
465 metallic: m.metallic,
466 roughness: m.roughness,
467 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
468 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
469 has_attribute: has_attr,
470 scalar_min: s_min,
471 scalar_max: s_max,
472 _pad_scalar: 0,
473 nan_color: item.nan_color.unwrap_or([0.0; 4]),
474 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
475 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
476 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
477 unlit: if m.unlit { 1 } else { 0 },
478 use_face_color: u32::from(item.active_attribute.as_ref().map_or(false, |a| {
479 a.kind == crate::resources::AttributeKind::FaceColor
480 })),
481 uv_vis_mode: m.param_vis.map_or(0, |pv| pv.mode as u32),
482 uv_vis_scale: m.param_vis.map_or(8.0, |pv| pv.scale),
483 backface_policy: match m.backface_policy {
484 crate::scene::material::BackfacePolicy::Cull => 0,
485 crate::scene::material::BackfacePolicy::Identical => 1,
486 crate::scene::material::BackfacePolicy::DifferentColor(_) => 2,
487 crate::scene::material::BackfacePolicy::Tint(_) => 3,
488 crate::scene::material::BackfacePolicy::Pattern(cfg) => {
489 4 + cfg.pattern as u32
490 }
491 },
492 backface_color: match m.backface_policy {
493 crate::scene::material::BackfacePolicy::DifferentColor(c) => {
494 [c[0], c[1], c[2], 1.0]
495 }
496 crate::scene::material::BackfacePolicy::Tint(factor) => {
497 [factor, 0.0, 0.0, 1.0]
498 }
499 crate::scene::material::BackfacePolicy::Pattern(cfg) => {
500 let world_extent = resources
501 .mesh_store
502 .get(item.mesh_id)
503 .map(|mesh| {
504 mesh.aabb
505 .transformed(&glam::Mat4::from_cols_array_2d(&item.model))
506 .longest_side()
507 })
508 .unwrap_or(1.0)
509 .max(1e-6);
510 let world_scale = cfg.scale / world_extent;
511 [cfg.color[0], cfg.color[1], cfg.color[2], world_scale]
512 }
513 _ => [0.0; 4],
514 },
515 has_warp: if item.warp_attribute.is_some() { 1 } else { 0 },
516 warp_scale: item.warp_scale,
517 _pad_warp: [0; 2],
518 };
519
520 let normal_obj_uniform = ObjectUniform {
521 model: item.model,
522 color: [1.0, 1.0, 1.0, 1.0],
523 selected: 0,
524 wireframe: 0,
525 ambient: 0.15,
526 diffuse: 0.75,
527 specular: 0.4,
528 shininess: 32.0,
529 has_texture: 0,
530 use_pbr: 0,
531 metallic: 0.0,
532 roughness: 0.5,
533 has_normal_map: 0,
534 has_ao_map: 0,
535 has_attribute: 0,
536 scalar_min: 0.0,
537 scalar_max: 1.0,
538 _pad_scalar: 0,
539 nan_color: [0.0; 4],
540 use_nan_color: 0,
541 use_matcap: 0,
542 matcap_blendable: 0,
543 unlit: 0,
544 use_face_color: 0,
545 uv_vis_mode: 0,
546 uv_vis_scale: 8.0,
547 backface_policy: 0,
548 backface_color: [0.0; 4],
549 has_warp: 0,
550 warp_scale: 1.0,
551 _pad_warp: [0; 2],
552 };
553
554 if collect_wf_uniforms && item.visible {
556 wireframe_uniforms.push(obj_uniform);
557 }
558
559 {
561 let mesh = resources
562 .mesh_store
563 .get(item.mesh_id)
564 .unwrap();
565 queue.write_buffer(
566 &mesh.object_uniform_buf,
567 0,
568 bytemuck::cast_slice(&[obj_uniform]),
569 );
570 queue.write_buffer(
571 &mesh.normal_uniform_buf,
572 0,
573 bytemuck::cast_slice(&[normal_obj_uniform]),
574 );
575 } resources.update_mesh_texture_bind_group(
579 device,
580 item.mesh_id,
581 item.material.texture_id,
582 item.material.normal_map_id,
583 item.material.ao_map_id,
584 item.colormap_id,
585 item.active_attribute.as_ref().map(|a| a.name.as_str()),
586 item.material.matcap_id,
587 item.warp_attribute.as_deref(),
588 );
589 }
590 }
591
592 if !wireframe_uniforms.is_empty() {
595 let n = wireframe_uniforms.len();
596 let uniform_size = std::mem::size_of::<ObjectUniform>() as u64;
597
598 while self.wireframe_uniform_bufs.len() < n {
600 let buf = device.create_buffer(&wgpu::BufferDescriptor {
601 label: Some("wireframe_item_uniform"),
602 size: uniform_size,
603 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
604 mapped_at_creation: false,
605 });
606 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
607 label: Some("wireframe_item_bg"),
608 layout: &resources.object_bind_group_layout,
609 entries: &[
610 wgpu::BindGroupEntry { binding: 0, resource: buf.as_entire_binding() },
611 wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&resources.fallback_texture.view) },
612 wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&resources.material_sampler) },
613 wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&resources.fallback_normal_map_view) },
614 wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&resources.fallback_ao_map_view) },
615 wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&resources.fallback_lut_view) },
616 wgpu::BindGroupEntry { binding: 6, resource: resources.fallback_scalar_buf.as_entire_binding() },
617 wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(resources.fallback_matcap_view.as_ref().unwrap_or(&resources.fallback_texture.view)) },
618 wgpu::BindGroupEntry { binding: 8, resource: resources.fallback_face_color_buf.as_entire_binding() },
619 wgpu::BindGroupEntry { binding: 9, resource: resources.fallback_warp_buf.as_entire_binding() },
620 ],
621 });
622 self.wireframe_uniform_bufs.push(buf);
623 self.wireframe_bind_groups.push(bg);
624 }
625
626 for (i, uniform) in wireframe_uniforms.iter().enumerate() {
628 queue.write_buffer(
629 &self.wireframe_uniform_bufs[i],
630 0,
631 bytemuck::cast_slice(std::slice::from_ref(uniform)),
632 );
633 }
634 }
635
636 if self.use_instancing {
637 resources.ensure_instanced_pipelines(device);
638
639 let instancable_count = scene_items.iter().filter(|item| {
653 item.visible
654 && item.active_attribute.is_none()
655 && !item.material.is_two_sided()
656 && item.material.matcap_id.is_none()
657 && item.material.param_vis.is_none()
658 && resources.mesh_store.get(item.mesh_id).is_some()
659 }).count();
660 let cache_valid = instancable_count == self.last_instancable_count
661 && frame.scene.generation == self.last_scene_generation
662 && frame.interaction.selection_generation == self.last_selection_generation
663 && scene_items.len() == self.last_scene_items_count;
664
665 if !cache_valid {
666 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
668 .iter()
669 .filter(|item| {
670 item.visible
671 && item.active_attribute.is_none()
672 && !item.material.is_two_sided()
673 && item.material.matcap_id.is_none()
674 && item.material.param_vis.is_none()
675 && resources
676 .mesh_store
677 .get(item.mesh_id)
678 .is_some()
679 })
680 .collect();
681
682 sorted_items.sort_unstable_by_key(|item| {
683 (
684 item.mesh_id.index(),
685 item.material.texture_id,
686 item.material.normal_map_id,
687 item.material.ao_map_id,
688 )
689 });
690
691 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
692 let mut all_aabbs: Vec<InstanceAabb> = Vec::with_capacity(sorted_items.len());
693 let mut batch_metas: Vec<BatchMeta> = Vec::new();
694 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
695
696 if !sorted_items.is_empty() {
697 let mut batch_start = 0usize;
698 for i in 1..=sorted_items.len() {
699 let at_end = i == sorted_items.len();
700 let key_changed = !at_end && {
701 let a = sorted_items[batch_start];
702 let b = sorted_items[i];
703 a.mesh_id != b.mesh_id
704 || a.material.texture_id != b.material.texture_id
705 || a.material.normal_map_id != b.material.normal_map_id
706 || a.material.ao_map_id != b.material.ao_map_id
707 };
708
709 if at_end || key_changed {
710 let batch_items = &sorted_items[batch_start..i];
711 let rep = batch_items[0];
712 let instance_offset = all_instances.len() as u32;
713 let is_transparent = rep.material.opacity < 1.0;
714
715 for item in batch_items {
716 let m = &item.material;
717 all_instances.push(InstanceData {
718 model: item.model,
719 color: [
720 m.base_color[0],
721 m.base_color[1],
722 m.base_color[2],
723 m.opacity,
724 ],
725 selected: if item.selected { 1 } else { 0 },
726 wireframe: 0, ambient: m.ambient,
728 diffuse: m.diffuse,
729 specular: m.specular,
730 shininess: m.shininess,
731 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
732 use_pbr: if m.use_pbr { 1 } else { 0 },
733 metallic: m.metallic,
734 roughness: m.roughness,
735 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
736 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
737 unlit: if m.unlit { 1 } else { 0 },
738 _pad_inst: [0; 3],
739 });
740 }
741
742 let batch_idx = instanced_batches.len() as u32;
746 let mesh_index_count = resources
747 .mesh_store
748 .get(rep.mesh_id)
749 .map(|m| m.index_count)
750 .unwrap_or(0);
751 for item in batch_items {
752 if let Some(mesh) = resources.mesh_store.get(item.mesh_id) {
753 let model =
754 glam::Mat4::from_cols_array_2d(&item.model);
755 let world_aabb = mesh.aabb.transformed(&model);
756 all_aabbs.push(InstanceAabb {
757 min: world_aabb.min.into(),
758 batch_index: batch_idx,
759 max: world_aabb.max.into(),
760 _pad: 0,
761 });
762 }
763 }
764
765 batch_metas.push(BatchMeta {
769 index_count: mesh_index_count,
770 first_index: 0,
771 instance_offset,
772 instance_count: batch_items.len() as u32,
773 vis_offset: instance_offset,
774 is_transparent: if is_transparent { 1 } else { 0 },
775 _pad: [0, 0],
776 });
777
778 instanced_batches.push(InstancedBatch {
779 mesh_id: rep.mesh_id,
780 texture_id: rep.material.texture_id,
781 normal_map_id: rep.material.normal_map_id,
782 ao_map_id: rep.material.ao_map_id,
783 instance_offset,
784 instance_count: batch_items.len() as u32,
785 is_transparent,
786 });
787
788 batch_start = i;
789 }
790 }
791 }
792
793 self.cached_instance_data = all_instances;
794 self.cached_instanced_batches = instanced_batches;
795
796 resources.upload_instance_data(device, queue, &self.cached_instance_data);
797 resources.upload_aabb_and_batch_meta(device, queue, &all_aabbs, &batch_metas);
798
799 self.instanced_batches = self.cached_instanced_batches.clone();
800
801 self.last_scene_generation = frame.scene.generation;
802 self.last_selection_generation = frame.interaction.selection_generation;
803 self.last_scene_items_count = scene_items.len();
804 self.last_instancable_count = sorted_items.len();
805
806 for batch in &self.instanced_batches {
807 resources.get_instance_bind_group(
808 device,
809 batch.texture_id,
810 batch.normal_map_id,
811 batch.ao_map_id,
812 );
813 }
814 } else {
815 for batch in &self.instanced_batches {
816 resources.get_instance_bind_group(
817 device,
818 batch.texture_id,
819 batch.normal_map_id,
820 batch.ao_map_id,
821 );
822 }
823 }
824
825 if self.gpu_culling_enabled
832 && !self.instanced_batches.is_empty()
833 && !self.cached_instance_data.is_empty()
834 {
835 let instance_count = self.cached_instance_data.len() as u32;
836 let batch_count = self.instanced_batches.len() as u32;
837
838 if self.cull_resources.is_none() {
840 self.cull_resources =
841 Some(crate::renderer::indirect::CullResources::new(device));
842 }
843 resources.ensure_cull_instance_pipelines(device);
844 for batch in &self.instanced_batches.clone() {
845 resources.get_instance_cull_bind_group(
846 device,
847 batch.texture_id,
848 batch.normal_map_id,
849 batch.ao_map_id,
850 );
851 }
852
853 if let (
855 Some(aabb_buf),
856 Some(meta_buf),
857 Some(counter_buf),
858 Some(vis_buf),
859 Some(indirect_buf),
860 ) = (
861 resources.instance_aabb_buf.as_ref(),
862 resources.batch_meta_buf.as_ref(),
863 resources.batch_counter_buf.as_ref(),
864 resources.visibility_index_buf.as_ref(),
865 resources.indirect_args_buf.as_ref(),
866 ) {
867 let vp_mat = frame.camera.render_camera.view_proj();
869 let cpu_frustum =
870 crate::camera::frustum::Frustum::from_view_proj(&vp_mat);
871 let frustum_uniform = crate::resources::FrustumUniform {
872 planes: std::array::from_fn(|i| crate::resources::FrustumPlane {
873 normal: cpu_frustum.planes[i].normal.into(),
874 distance: cpu_frustum.planes[i].d,
875 }),
876 instance_count,
877 batch_count,
878 _pad: [0; 2],
879 };
880
881 let cull = self.cull_resources.as_ref().unwrap();
882 let mut encoder =
883 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
884 label: Some("cull_encoder"),
885 });
886 cull.dispatch(
887 &mut encoder,
888 device,
889 queue,
890 &frustum_uniform,
891 aabb_buf,
892 meta_buf,
893 counter_buf,
894 vis_buf,
895 indirect_buf,
896 instance_count,
897 batch_count,
898 );
899
900 let indirect_bytes = batch_count as u64 * 20;
903 if self
904 .indirect_readback_buf
905 .as_ref()
906 .map_or(0, |b| b.size())
907 < indirect_bytes
908 {
909 self.indirect_readback_buf =
910 Some(device.create_buffer(&wgpu::BufferDescriptor {
911 label: Some("indirect_readback_buf"),
912 size: indirect_bytes,
913 usage: wgpu::BufferUsages::COPY_DST
914 | wgpu::BufferUsages::MAP_READ,
915 mapped_at_creation: false,
916 }));
917 }
918 if let Some(ref rb_buf) = self.indirect_readback_buf {
919 encoder.copy_buffer_to_buffer(
920 indirect_buf,
921 0,
922 rb_buf,
923 0,
924 indirect_bytes,
925 );
926 }
927 queue.submit(std::iter::once(encoder.finish()));
928 self.indirect_readback_batch_count = batch_count;
929 self.indirect_readback_pending = true;
930 }
931 }
932 }
933
934 self.point_cloud_gpu_data.clear();
938 if !frame.scene.point_clouds.is_empty() {
939 resources.ensure_point_cloud_pipeline(device);
940 for item in &frame.scene.point_clouds {
941 if item.positions.is_empty() {
942 continue;
943 }
944 let gpu_data = resources.upload_point_cloud(device, queue, item);
945 self.point_cloud_gpu_data.push(gpu_data);
946 }
947 }
948
949 self.glyph_gpu_data.clear();
950 if !frame.scene.glyphs.is_empty() {
951 resources.ensure_glyph_pipeline(device);
952 for item in &frame.scene.glyphs {
953 if item.positions.is_empty() || item.vectors.is_empty() {
954 continue;
955 }
956 let gpu_data = resources.upload_glyph_set(device, queue, item);
957 self.glyph_gpu_data.push(gpu_data);
958 }
959 }
960
961 self.sprite_gpu_data.clear();
965 if !frame.scene.sprite_items.is_empty() {
966 resources.ensure_sprite_pipelines(device);
967 for item in &frame.scene.sprite_items {
968 if item.positions.is_empty() {
969 continue;
970 }
971 let gd = resources.upload_sprite(device, queue, item);
972 self.sprite_gpu_data.push(gd);
973 }
974 }
975
976 self.tensor_glyph_gpu_data.clear();
980 if !frame.scene.tensor_glyphs.is_empty() {
981 resources.ensure_tensor_glyph_pipeline(device);
982 for item in &frame.scene.tensor_glyphs {
983 if item.positions.is_empty() {
984 continue;
985 }
986 let gd = resources.upload_tensor_glyph_set(device, queue, item);
987 self.tensor_glyph_gpu_data.push(gd);
988 }
989 }
990
991 self.polyline_gpu_data.clear();
995 let vp_size = frame.camera.viewport_size;
996 if !frame.scene.polylines.is_empty() {
997 resources.ensure_polyline_pipeline(device);
998 for item in &frame.scene.polylines {
999 if item.positions.is_empty() {
1000 continue;
1001 }
1002 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
1003 self.polyline_gpu_data.push(gpu_data);
1004
1005 if !item.node_vectors.is_empty() {
1007 resources.ensure_glyph_pipeline(device);
1008 let g = crate::quantities::polyline_node_vectors_to_glyphs(item);
1009 if !g.positions.is_empty() {
1010 let gd = resources.upload_glyph_set(device, queue, &g);
1011 self.glyph_gpu_data.push(gd);
1012 }
1013 }
1014 if !item.edge_vectors.is_empty() {
1015 resources.ensure_glyph_pipeline(device);
1016 let g = crate::quantities::polyline_edge_vectors_to_glyphs(item);
1017 if !g.positions.is_empty() {
1018 let gd = resources.upload_glyph_set(device, queue, &g);
1019 self.glyph_gpu_data.push(gd);
1020 }
1021 }
1022 }
1023 }
1024
1025 if !frame.scene.isolines.is_empty() {
1029 resources.ensure_polyline_pipeline(device);
1030 for item in &frame.scene.isolines {
1031 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
1032 continue;
1033 }
1034 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
1035 if positions.is_empty() {
1036 continue;
1037 }
1038 let polyline = PolylineItem {
1039 positions,
1040 scalars: Vec::new(),
1041 strip_lengths,
1042 scalar_range: None,
1043 colormap_id: None,
1044 default_color: item.color,
1045 line_width: item.line_width,
1046 id: 0,
1047 ..Default::default()
1048 };
1049 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
1050 self.polyline_gpu_data.push(gpu_data);
1051 }
1052 }
1053
1054 if !frame.scene.camera_frustums.is_empty() {
1058 resources.ensure_polyline_pipeline(device);
1059 for item in &frame.scene.camera_frustums {
1060 let polyline = item.to_polyline();
1061 if !polyline.positions.is_empty() {
1062 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
1063 self.polyline_gpu_data.push(gpu_data);
1064 }
1065 }
1066 }
1067
1068 self.implicit_gpu_data.clear();
1072 if !frame.scene.gpu_implicit.is_empty() {
1073 resources.ensure_implicit_pipeline(device);
1074 for item in &frame.scene.gpu_implicit {
1075 if item.primitives.is_empty() {
1076 continue;
1077 }
1078 let gpu = resources.upload_implicit_item(device, item);
1079 self.implicit_gpu_data.push(gpu);
1080 }
1081 }
1082
1083 self.mc_gpu_data.clear();
1087 if !frame.scene.gpu_mc_jobs.is_empty() {
1088 resources.ensure_mc_pipelines(device);
1089 self.mc_gpu_data =
1090 resources.run_mc_jobs(device, queue, &frame.scene.gpu_mc_jobs);
1091 }
1092
1093 self.screen_image_gpu_data.clear();
1097 if !frame.scene.screen_images.is_empty() {
1098 resources.ensure_screen_image_pipeline(device);
1099 if frame.scene.screen_images.iter().any(|i| i.depth.is_some()) {
1101 resources.ensure_screen_image_dc_pipeline(device);
1102 }
1103 let vp_w = vp_size[0];
1104 let vp_h = vp_size[1];
1105 for item in &frame.scene.screen_images {
1106 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
1107 continue;
1108 }
1109 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
1110 self.screen_image_gpu_data.push(gpu);
1111 }
1112 }
1113
1114 self.overlay_image_gpu_data.clear();
1118 if !frame.overlays.images.is_empty() {
1119 resources.ensure_screen_image_pipeline(device);
1120 let vp_w = vp_size[0];
1121 let vp_h = vp_size[1];
1122 for item in &frame.overlays.images {
1123 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
1124 continue;
1125 }
1126 let gpu = resources.upload_overlay_image(device, queue, item, vp_w, vp_h);
1127 self.overlay_image_gpu_data.push(gpu);
1128 }
1129 }
1130
1131 self.streamtube_gpu_data.clear();
1135 if !frame.scene.streamtube_items.is_empty() {
1136 resources.ensure_streamtube_pipeline(device);
1137 for item in &frame.scene.streamtube_items {
1138 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1139 continue;
1140 }
1141 let gpu_data = resources.upload_streamtube(device, queue, item);
1142 if gpu_data.index_count > 0 {
1143 self.streamtube_gpu_data.push(gpu_data);
1144 }
1145 }
1146 }
1147
1148 self.tube_gpu_data.clear();
1152 if !frame.scene.tube_items.is_empty() {
1153 resources.ensure_streamtube_pipeline(device);
1154 for item in &frame.scene.tube_items {
1155 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1156 continue;
1157 }
1158 let gpu_data = resources.upload_tube(device, queue, item);
1159 if gpu_data.index_count > 0 {
1160 self.tube_gpu_data.push(gpu_data);
1161 }
1162 }
1163 }
1164
1165 self.ribbon_gpu_data.clear();
1169 if !frame.scene.ribbon_items.is_empty() {
1170 resources.ensure_streamtube_pipeline(device);
1171 for item in &frame.scene.ribbon_items {
1172 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1173 continue;
1174 }
1175 let gpu_data = resources.upload_ribbon(device, queue, item);
1176 if gpu_data.index_count > 0 {
1177 self.ribbon_gpu_data.push(gpu_data);
1178 }
1179 }
1180 }
1181
1182 self.image_slice_gpu_data.clear();
1186 if !frame.scene.image_slices.is_empty() {
1187 resources.ensure_image_slice_pipeline(device);
1188 for item in &frame.scene.image_slices {
1189 if let Some(gpu_data) = resources.upload_image_slice(device, queue, item) {
1190 self.image_slice_gpu_data.push(gpu_data);
1191 }
1192 }
1193 }
1194
1195 self.volume_surface_slice_gpu_data.clear();
1199 if !frame.scene.volume_surface_slices.is_empty() {
1200 resources.ensure_volume_surface_slice_pipeline(device);
1201 for item in &frame.scene.volume_surface_slices {
1202 if let Some(gpu_data) = resources.upload_volume_surface_slice(device, queue, item) {
1203 self.volume_surface_slice_gpu_data.push(gpu_data);
1204 }
1205 }
1206 }
1207
1208 self.lic_gpu_data.clear();
1212 if !frame.scene.lic_items.is_empty() {
1213 for item in &frame.scene.lic_items {
1216 if item.vector_attribute.is_empty() {
1217 continue;
1218 }
1219 if let Some(mesh) = resources.mesh_store.get(item.mesh_id) {
1220 if mesh.vector_attribute_buffers.contains_key(&item.vector_attribute) {
1222 if let Some(bgl) = &resources.lic_surface_bgl {
1223 use crate::resources::LicObjectUniform;
1224 let model = item.model;
1225 let obj_data = LicObjectUniform { model };
1226 let obj_buf = device.create_buffer(&wgpu::BufferDescriptor {
1227 label: Some("lic_object_uniform"),
1228 size: std::mem::size_of::<LicObjectUniform>() as u64,
1229 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1230 mapped_at_creation: false,
1231 });
1232 queue.write_buffer(&obj_buf, 0, bytemuck::cast_slice(&[obj_data]));
1233 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1236 label: Some("lic_surface_item_bg"),
1237 layout: bgl,
1238 entries: &[
1239 wgpu::BindGroupEntry {
1240 binding: 0,
1241 resource: obj_buf.as_entire_binding(),
1242 },
1243 ],
1244 });
1245 self.lic_gpu_data.push(crate::resources::LicSurfaceGpuData {
1246 bind_group: bg,
1247 _object_uniform_buf: obj_buf,
1248 mesh_id: item.mesh_id,
1249 vector_attribute: item.vector_attribute.clone(),
1250 });
1251 }
1252 }
1253 }
1254 }
1255 if let Some(hdr) = self.viewport_slots[frame.camera.viewport_index].hdr.as_ref() {
1257 if let Some(first) = frame.scene.lic_items.first() {
1258 let [vw, vh] = hdr.size;
1259 let u = crate::resources::LicAdvectUniform {
1260 steps: first.config.steps,
1261 step_size: first.config.step_size,
1262 vp_width: vw as f32,
1263 vp_height: vh as f32,
1264 };
1265 queue.write_buffer(&hdr.lic_uniform_buf, 0, bytemuck::cast_slice(&[u]));
1266 }
1267 }
1268 }
1269
1270 self.volume_gpu_data.clear();
1276 if !frame.scene.volumes.is_empty() {
1277 resources.ensure_volume_pipeline(device);
1278 let clip_objects_for_vol = &frame.effects.clip_objects;
1279 let vol_step_multiplier = if self.degradation_volume_quality_reduced {
1282 2.0_f32
1283 } else {
1284 1.0_f32
1285 };
1286 for item in &frame.scene.volumes {
1287 let gpu = resources.upload_volume_frame(
1288 device,
1289 queue,
1290 item,
1291 clip_objects_for_vol,
1292 vol_step_multiplier,
1293 );
1294 self.volume_gpu_data.push(gpu);
1295 }
1296 }
1297
1298 {
1300 let total = scene_items.len() as u32;
1301 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
1302 let mut draw_calls = 0u32;
1303 let mut triangles = 0u64;
1304 let instanced_batch_count = if self.use_instancing {
1305 self.instanced_batches.len() as u32
1306 } else {
1307 0
1308 };
1309
1310 if self.use_instancing {
1311 for batch in &self.instanced_batches {
1312 if let Some(mesh) = resources
1313 .mesh_store
1314 .get(batch.mesh_id)
1315 {
1316 draw_calls += 1;
1317 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
1318 }
1319 }
1320 } else {
1321 for item in scene_items {
1322 if !item.visible {
1323 continue;
1324 }
1325 if let Some(mesh) = resources
1326 .mesh_store
1327 .get(item.mesh_id)
1328 {
1329 draw_calls += 1;
1330 triangles += (mesh.index_count / 3) as u64;
1331 }
1332 }
1333 }
1334
1335 self.last_stats = crate::renderer::stats::FrameStats {
1336 total_objects: total,
1337 visible_objects: visible,
1338 culled_objects: total.saturating_sub(visible),
1339 draw_calls,
1340 instanced_batches: instanced_batch_count,
1341 triangles_submitted: triangles,
1342 shadow_draw_calls: 0, gpu_culling_active: self.gpu_culling_enabled,
1344 gpu_visible_instances: if self.gpu_culling_enabled {
1346 self.last_stats.gpu_visible_instances
1347 } else {
1348 None
1349 },
1350 ..self.last_stats
1351 };
1352 }
1353
1354 let skip_shadows = self.degradation_shadows_skipped;
1359
1360 if lighting.shadows_enabled && (skip_shadows || scene_items.is_empty()) {
1364 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1365 label: Some("shadow_clear_encoder"),
1366 });
1367 let _ = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
1368 label: Some("shadow_clear_pass"),
1369 color_attachments: &[],
1370 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1371 view: &resources.shadow_map_view,
1372 depth_ops: Some(wgpu::Operations {
1373 load: wgpu::LoadOp::Clear(1.0),
1374 store: wgpu::StoreOp::Store,
1375 }),
1376 stencil_ops: None,
1377 }),
1378 timestamp_writes: None,
1379 occlusion_query_set: None,
1380 });
1381 queue.submit(std::iter::once(enc.finish()));
1382 }
1383
1384 if lighting.shadows_enabled && !scene_items.is_empty() && !skip_shadows {
1385 if self.gpu_culling_enabled
1395 && self.use_instancing
1396 && !self.instanced_batches.is_empty()
1397 && !self.cached_instance_data.is_empty()
1398 {
1399 if self.cull_resources.is_none() {
1401 self.cull_resources =
1402 Some(crate::renderer::indirect::CullResources::new(device));
1403 }
1404 resources.ensure_cull_instance_pipelines(device);
1405 for c in 0..effective_cascade_count {
1406 resources.get_shadow_cull_instance_bind_group(device, c);
1407 }
1408
1409 let instance_count = self.cached_instance_data.len() as u32;
1410 let batch_count = self.instanced_batches.len() as u32;
1411
1412 if let (Some(aabb_buf), Some(meta_buf), Some(counter_buf)) = (
1413 resources.instance_aabb_buf.as_ref(),
1414 resources.batch_meta_buf.as_ref(),
1415 resources.batch_counter_buf.as_ref(),
1416 ) {
1417 let cull = self.cull_resources.as_ref().unwrap();
1418 let mut shadow_cull_encoder =
1419 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1420 label: Some("shadow_cull_encoder"),
1421 });
1422 for c in 0..effective_cascade_count {
1423 if let (Some(shadow_vis_buf), Some(shadow_indirect_buf)) = (
1424 resources.shadow_vis_bufs[c].as_ref(),
1425 resources.shadow_indirect_bufs[c].as_ref(),
1426 ) {
1427 let cpu_frustum =
1428 crate::camera::frustum::Frustum::from_view_proj(
1429 &cascade_view_projs[c],
1430 );
1431 let frustum_uniform = crate::resources::FrustumUniform {
1432 planes: std::array::from_fn(|i| crate::resources::FrustumPlane {
1433 normal: cpu_frustum.planes[i].normal.into(),
1434 distance: cpu_frustum.planes[i].d,
1435 }),
1436 instance_count,
1437 batch_count,
1438 _pad: [0; 2],
1439 };
1440 cull.dispatch_shadow(
1441 &mut shadow_cull_encoder,
1442 device,
1443 queue,
1444 c,
1445 &frustum_uniform,
1446 aabb_buf,
1447 meta_buf,
1448 counter_buf,
1449 shadow_vis_buf,
1450 shadow_indirect_buf,
1451 instance_count,
1452 batch_count,
1453 );
1454 }
1455 }
1456 queue.submit(std::iter::once(shadow_cull_encoder.finish()));
1457 }
1458 }
1459
1460 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1461 label: Some("shadow_pass_encoder"),
1462 });
1463 {
1464 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1465 label: Some("shadow_pass"),
1466 color_attachments: &[],
1467 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1468 view: &resources.shadow_map_view,
1469 depth_ops: Some(wgpu::Operations {
1470 load: wgpu::LoadOp::Clear(1.0),
1471 store: wgpu::StoreOp::Store,
1472 }),
1473 stencil_ops: None,
1474 }),
1475 timestamp_writes: None,
1476 occlusion_query_set: None,
1477 });
1478
1479 let mut shadow_draws = 0u32;
1480 let tile_px = tile_size as f32;
1481
1482 if self.use_instancing {
1483 let use_shadow_indirect = self.gpu_culling_enabled
1484 && resources.shadow_instanced_cull_pipeline.is_some()
1485 && resources.shadow_vis_bufs[0].is_some();
1486
1487 if use_shadow_indirect {
1488 for cascade in 0..effective_cascade_count {
1490 let tile_col = (cascade % 2) as f32;
1491 let tile_row = (cascade / 2) as f32;
1492 shadow_pass.set_viewport(
1493 tile_col * tile_px,
1494 tile_row * tile_px,
1495 tile_px,
1496 tile_px,
1497 0.0,
1498 1.0,
1499 );
1500 shadow_pass.set_scissor_rect(
1501 (tile_col * tile_px) as u32,
1502 (tile_row * tile_px) as u32,
1503 tile_size,
1504 tile_size,
1505 );
1506
1507 queue.write_buffer(
1509 resources.shadow_instanced_cascade_bufs[cascade]
1510 .as_ref()
1511 .expect("shadow_instanced_cascade_bufs not allocated"),
1512 0,
1513 bytemuck::cast_slice(
1514 &cascade_view_projs[cascade].to_cols_array_2d(),
1515 ),
1516 );
1517
1518 let Some(pipeline) =
1519 resources.shadow_instanced_cull_pipeline.as_ref()
1520 else {
1521 continue;
1522 };
1523 let Some(cascade_bg) =
1524 resources.shadow_instanced_cascade_bgs[cascade].as_ref()
1525 else {
1526 continue;
1527 };
1528 let Some(inst_cull_bg) =
1529 resources.shadow_cull_instance_bgs[cascade].as_ref()
1530 else {
1531 continue;
1532 };
1533 let Some(shadow_indirect_buf) =
1534 resources.shadow_indirect_bufs[cascade].as_ref()
1535 else {
1536 continue;
1537 };
1538
1539 shadow_pass.set_pipeline(pipeline);
1540 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1541 shadow_pass.set_bind_group(1, inst_cull_bg, &[]);
1542
1543 for (bi, batch) in self.instanced_batches.iter().enumerate() {
1544 if batch.is_transparent {
1545 continue;
1546 }
1547 let Some(mesh) = resources.mesh_store.get(batch.mesh_id) else {
1548 continue;
1549 };
1550 shadow_pass
1551 .set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1552 shadow_pass.set_index_buffer(
1553 mesh.index_buffer.slice(..),
1554 wgpu::IndexFormat::Uint32,
1555 );
1556 shadow_pass.draw_indexed_indirect(
1557 shadow_indirect_buf,
1558 bi as u64 * 20,
1559 );
1560 shadow_draws += 1;
1561 }
1562 }
1563 } else if let (Some(pipeline), Some(instance_bg)) = (
1564 &resources.shadow_instanced_pipeline,
1565 self.instanced_batches.first().and_then(|b| {
1566 resources.instance_bind_groups.get(&(
1567 b.texture_id.unwrap_or(u64::MAX),
1568 b.normal_map_id.unwrap_or(u64::MAX),
1569 b.ao_map_id.unwrap_or(u64::MAX),
1570 ))
1571 }),
1572 ) {
1573 for cascade in 0..effective_cascade_count {
1575 let tile_col = (cascade % 2) as f32;
1576 let tile_row = (cascade / 2) as f32;
1577 shadow_pass.set_viewport(
1578 tile_col * tile_px,
1579 tile_row * tile_px,
1580 tile_px,
1581 tile_px,
1582 0.0,
1583 1.0,
1584 );
1585 shadow_pass.set_scissor_rect(
1586 (tile_col * tile_px) as u32,
1587 (tile_row * tile_px) as u32,
1588 tile_size,
1589 tile_size,
1590 );
1591
1592 shadow_pass.set_pipeline(pipeline);
1593
1594 queue.write_buffer(
1595 resources.shadow_instanced_cascade_bufs[cascade]
1596 .as_ref()
1597 .expect("shadow_instanced_cascade_bufs not allocated"),
1598 0,
1599 bytemuck::cast_slice(
1600 &cascade_view_projs[cascade].to_cols_array_2d(),
1601 ),
1602 );
1603
1604 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
1605 .as_ref()
1606 .expect("shadow_instanced_cascade_bgs not allocated");
1607 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1608 shadow_pass.set_bind_group(1, instance_bg, &[]);
1609
1610 for batch in &self.instanced_batches {
1611 if batch.is_transparent {
1612 continue;
1613 }
1614 let Some(mesh) = resources
1615 .mesh_store
1616 .get(batch.mesh_id)
1617 else {
1618 continue;
1619 };
1620 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1621 shadow_pass.set_index_buffer(
1622 mesh.index_buffer.slice(..),
1623 wgpu::IndexFormat::Uint32,
1624 );
1625 shadow_pass.draw_indexed(
1626 0..mesh.index_count,
1627 0,
1628 batch.instance_offset
1629 ..batch.instance_offset + batch.instance_count,
1630 );
1631 shadow_draws += 1;
1632 }
1633 }
1634 }
1635 } else {
1636 for cascade in 0..effective_cascade_count {
1637 let tile_col = (cascade % 2) as f32;
1638 let tile_row = (cascade / 2) as f32;
1639 shadow_pass.set_viewport(
1640 tile_col * tile_px,
1641 tile_row * tile_px,
1642 tile_px,
1643 tile_px,
1644 0.0,
1645 1.0,
1646 );
1647 shadow_pass.set_scissor_rect(
1648 (tile_col * tile_px) as u32,
1649 (tile_row * tile_px) as u32,
1650 tile_size,
1651 tile_size,
1652 );
1653
1654 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1655 shadow_pass.set_bind_group(
1656 0,
1657 &resources.shadow_bind_group,
1658 &[cascade as u32 * 256],
1659 );
1660
1661 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1662 &cascade_view_projs[cascade],
1663 );
1664
1665 for item in scene_items.iter() {
1666 if !item.visible {
1667 continue;
1668 }
1669 if item.material.opacity < 1.0 {
1670 continue;
1671 }
1672 let Some(mesh) = resources
1673 .mesh_store
1674 .get(item.mesh_id)
1675 else {
1676 continue;
1677 };
1678
1679 let world_aabb = mesh
1680 .aabb
1681 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1682 if cascade_frustum.cull_aabb(&world_aabb) {
1683 continue;
1684 }
1685
1686 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1687 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1688 shadow_pass.set_index_buffer(
1689 mesh.index_buffer.slice(..),
1690 wgpu::IndexFormat::Uint32,
1691 );
1692 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1693 shadow_draws += 1;
1694 }
1695 }
1696 }
1697 drop(shadow_pass);
1698 self.last_stats.shadow_draw_calls = shadow_draws;
1699 }
1700 queue.submit(std::iter::once(encoder.finish()));
1701 }
1702 }
1703
1704 pub(super) fn prepare_viewport_internal(
1709 &mut self,
1710 device: &wgpu::Device,
1711 queue: &wgpu::Queue,
1712 frame: &FrameData,
1713 viewport_fx: &ViewportEffects<'_>,
1714 ) {
1715 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1718
1719 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1720 SurfaceSubmission::Flat(items) => items.as_ref(),
1721 };
1722
1723 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1725
1726 {
1727 let resources = &mut self.resources;
1728
1729 {
1731 let mut planes = [[0.0f32; 4]; 6];
1732 let mut count = 0u32;
1733 let mut clip_vols_uniform: ClipVolumesUniform = bytemuck::Zeroable::zeroed();
1734
1735 for obj in viewport_fx
1736 .clip_objects
1737 .iter()
1738 .filter(|o| o.enabled && o.clip_geometry)
1739 {
1740 match obj.shape {
1741 ClipShape::Plane {
1742 normal, distance, ..
1743 } if count < 6 => {
1744 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1745 count += 1;
1746 }
1747 ClipShape::Box {
1748 center,
1749 half_extents,
1750 orientation,
1751 } if (clip_vols_uniform.count as usize) < CLIP_VOLUME_MAX => {
1752 let idx = clip_vols_uniform.count as usize;
1753 clip_vols_uniform.volumes[idx] =
1754 ClipVolumeEntry::from_box(center, half_extents, orientation);
1755 clip_vols_uniform.count += 1;
1756 }
1757 ClipShape::Sphere { center, radius }
1758 if (clip_vols_uniform.count as usize) < CLIP_VOLUME_MAX =>
1759 {
1760 let idx = clip_vols_uniform.count as usize;
1761 clip_vols_uniform.volumes[idx] =
1762 ClipVolumeEntry::from_sphere(center, radius);
1763 clip_vols_uniform.count += 1;
1764 }
1765 ClipShape::Cylinder {
1766 center,
1767 axis,
1768 radius,
1769 half_length,
1770 } if (clip_vols_uniform.count as usize) < CLIP_VOLUME_MAX => {
1771 let idx = clip_vols_uniform.count as usize;
1772 clip_vols_uniform.volumes[idx] =
1773 ClipVolumeEntry::from_cylinder(center, axis, radius, half_length);
1774 clip_vols_uniform.count += 1;
1775 }
1776 _ => {}
1777 }
1778 }
1779
1780 let clip_uniform = ClipPlanesUniform {
1781 planes,
1782 count,
1783 _pad0: 0,
1784 viewport_width: frame.camera.viewport_size[0].max(1.0),
1785 viewport_height: frame.camera.viewport_size[1].max(1.0),
1786 };
1787 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1789 queue.write_buffer(
1790 &slot.clip_planes_buf,
1791 0,
1792 bytemuck::cast_slice(&[clip_uniform]),
1793 );
1794 queue.write_buffer(
1795 &slot.clip_volume_buf,
1796 0,
1797 bytemuck::cast_slice(&[clip_vols_uniform]),
1798 );
1799 }
1800 queue.write_buffer(
1802 &resources.clip_planes_uniform_buf,
1803 0,
1804 bytemuck::cast_slice(&[clip_uniform]),
1805 );
1806 queue.write_buffer(
1807 &resources.clip_volume_uniform_buf,
1808 0,
1809 bytemuck::cast_slice(&[clip_vols_uniform]),
1810 );
1811 }
1812
1813 let camera_uniform = frame.camera.render_camera.camera_uniform();
1815 queue.write_buffer(
1817 &resources.camera_uniform_buf,
1818 0,
1819 bytemuck::cast_slice(&[camera_uniform]),
1820 );
1821 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1823 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1824 }
1825
1826 if frame.viewport.show_grid {
1828 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1829 if !eye.is_finite() {
1830 tracing::warn!(
1831 eye_x = eye.x,
1832 eye_y = eye.y,
1833 eye_z = eye.z,
1834 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1835 );
1836 } else {
1837 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1838
1839 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1840 (frame.viewport.grid_cell_size, 1.0_f32)
1841 } else {
1842 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1843 let world_per_pixel =
1844 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1845 / frame.camera.viewport_size[1].max(1.0);
1846 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1847 let mut s = 1.0_f32;
1848 let mut iters = 0u32;
1849 while s < target {
1850 s *= 10.0;
1851 iters += 1;
1852 }
1853 let ratio = (target / s).clamp(0.0, 1.0);
1854 let fade = if ratio < 0.5 {
1855 1.0_f32
1856 } else {
1857 let t = (ratio - 0.5) * 2.0;
1858 1.0 - t * t * (3.0 - 2.0 * t)
1859 };
1860 tracing::debug!(
1861 eye_z = eye.z,
1862 vertical_depth,
1863 world_per_pixel,
1864 target,
1865 spacing = s,
1866 lod_iters = iters,
1867 ratio,
1868 minor_fade = fade,
1869 "grid LOD"
1870 );
1871 (s, fade)
1872 };
1873
1874 let spacing_major = spacing * 10.0;
1875 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1876 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1877 tracing::debug!(
1878 spacing_minor = spacing,
1879 spacing_major,
1880 snap_x,
1881 snap_y,
1882 eye_x = eye.x,
1883 eye_y = eye.y,
1884 eye_z = eye.z,
1885 "grid snap"
1886 );
1887
1888 let orient = frame.camera.render_camera.orientation;
1889 let right = orient * glam::Vec3::X;
1890 let up = orient * glam::Vec3::Y;
1891 let back = orient * glam::Vec3::Z;
1892 let cam_to_world = [
1893 [right.x, right.y, right.z, 0.0_f32],
1894 [up.x, up.y, up.z, 0.0_f32],
1895 [back.x, back.y, back.z, 0.0_f32],
1896 ];
1897 let aspect =
1898 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1899 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1900
1901 let uniform = GridUniform {
1902 view_proj: view_proj_mat,
1903 cam_to_world,
1904 tan_half_fov,
1905 aspect,
1906 _pad_ivp: [0.0; 2],
1907 eye_pos: frame.camera.render_camera.eye_position,
1908 grid_z: frame.viewport.grid_z,
1909 spacing_minor: spacing,
1910 spacing_major,
1911 snap_origin: [snap_x, snap_y],
1912 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1913 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1914 };
1915 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1917 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1918 }
1919 queue.write_buffer(
1921 &resources.grid_uniform_buf,
1922 0,
1923 bytemuck::cast_slice(&[uniform]),
1924 );
1925 }
1926 }
1927 {
1931 let gp = &viewport_fx.ground_plane;
1932 let mode_u32: u32 = match gp.mode {
1933 crate::renderer::types::GroundPlaneMode::None => 0,
1934 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1935 crate::renderer::types::GroundPlaneMode::Tile => 2,
1936 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1937 };
1938 let orient = frame.camera.render_camera.orientation;
1939 let right = orient * glam::Vec3::X;
1940 let up = orient * glam::Vec3::Y;
1941 let back = orient * glam::Vec3::Z;
1942 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1943 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1944 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1945 let gp_uniform = crate::resources::GroundPlaneUniform {
1946 view_proj: vp,
1947 cam_right: [right.x, right.y, right.z, 0.0],
1948 cam_up: [up.x, up.y, up.z, 0.0],
1949 cam_back: [back.x, back.y, back.z, 0.0],
1950 eye_pos: frame.camera.render_camera.eye_position,
1951 height: gp.height,
1952 color: gp.color,
1953 shadow_color: gp.shadow_color,
1954 light_vp: gp_cascade0_mat,
1955 tan_half_fov,
1956 aspect,
1957 tile_size: gp.tile_size,
1958 shadow_bias: 0.002,
1959 mode: mode_u32,
1960 shadow_opacity: gp.shadow_opacity,
1961 _pad: [0.0; 2],
1962 };
1963 queue.write_buffer(
1964 &resources.ground_plane_uniform_buf,
1965 0,
1966 bytemuck::cast_slice(&[gp_uniform]),
1967 );
1968 }
1969 } let vp_idx = frame.camera.viewport_index;
1978
1979 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1981 if frame.interaction.outline_selected {
1982 let resources = &self.resources;
1983 for item in scene_items {
1984 if !item.visible || !item.selected {
1985 continue;
1986 }
1987 let uniform = OutlineUniform {
1988 model: item.model,
1989 color: [0.0; 4], pixel_offset: 0.0,
1991 _pad: [0.0; 3],
1992 };
1993 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1994 label: Some("outline_mask_uniform_buf"),
1995 size: std::mem::size_of::<OutlineUniform>() as u64,
1996 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1997 mapped_at_creation: false,
1998 });
1999 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
2000 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2001 label: Some("outline_mask_object_bg"),
2002 layout: &resources.outline_bind_group_layout,
2003 entries: &[wgpu::BindGroupEntry {
2004 binding: 0,
2005 resource: buf.as_entire_binding(),
2006 }],
2007 });
2008 outline_object_buffers.push(OutlineObjectBuffers {
2009 mesh_id: item.mesh_id,
2010 two_sided: item.material.is_two_sided(),
2011 _mask_uniform_buf: buf,
2012 mask_bind_group: bg,
2013 });
2014 }
2015 }
2016
2017 let mut splat_outline_buffers: Vec<crate::resources::SplatOutlineBuffers> = Vec::new();
2019 let mut glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)> = Vec::new();
2023 let mut tensor_glyph_outline_indices: Vec<(usize, Option<Vec<u32>>)> = Vec::new();
2024 let mut sprite_outline_indices: Vec<(usize, Option<Vec<u32>>)> = Vec::new();
2025 if frame.interaction.outline_selected {
2026 let resources = &self.resources;
2027 let view_proj = frame.camera.render_camera.view_proj();
2028 let [vp_w, vp_h] = frame.camera.viewport_size;
2029 for item in &frame.scene.gaussian_splats {
2030 let Some(gpu_set) = resources.gaussian_splat_store.get(item.id.0) else {
2031 continue;
2032 };
2033 if item.selected && !gpu_set.cpu_positions.is_empty() {
2034 let mean_max_scale: f32 = if gpu_set.cpu_scales.is_empty() {
2037 0.05
2038 } else {
2039 gpu_set.cpu_scales.iter()
2040 .map(|s| s[0].max(s[1]).max(s[2]))
2041 .sum::<f32>()
2042 / gpu_set.cpu_scales.len() as f32
2043 };
2044 let world_radius = mean_max_scale * 3.0;
2045
2046 let model = glam::Mat4::from_cols_array_2d(&item.model);
2050 let center_w = model.transform_point3(glam::Vec3::ZERO);
2051 let cam_right = frame.camera.render_camera.view.row(0).truncate().normalize();
2052 let p0_clip = view_proj * glam::Vec4::new(center_w.x, center_w.y, center_w.z, 1.0);
2053 let p1_world = center_w + cam_right * world_radius;
2054 let p1_clip = view_proj * glam::Vec4::new(p1_world.x, p1_world.y, p1_world.z, 1.0);
2055 let pixel_radius = if p0_clip.w.abs() > 1e-6 && p1_clip.w.abs() > 1e-6 {
2056 let p0_ndc = glam::Vec2::new(p0_clip.x, p0_clip.y) / p0_clip.w;
2057 let p1_ndc = glam::Vec2::new(p1_clip.x, p1_clip.y) / p1_clip.w;
2058 (p1_ndc - p0_ndc).length() * 0.5 * vp_w.max(vp_h)
2059 } else {
2060 world_radius * 100.0
2061 };
2062 let pixel_radius = pixel_radius.max(1.0);
2063
2064 let position_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2065 label: Some("splat_outline_pos_buf"),
2066 contents: bytemuck::cast_slice(gpu_set.cpu_positions.as_slice()),
2067 usage: wgpu::BufferUsages::VERTEX,
2068 });
2069
2070 let uniform = SplatOutlineMaskUniform {
2071 model: item.model,
2072 viewport_w: vp_w,
2073 viewport_h: vp_h,
2074 pixel_radius,
2075 _pad: [0.0; 5],
2076 };
2077 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2078 label: Some("splat_outline_uniform_buf"),
2079 contents: bytemuck::cast_slice(&[uniform]),
2080 usage: wgpu::BufferUsages::UNIFORM,
2081 });
2082 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2083 label: Some("splat_outline_bg"),
2084 layout: &resources.outline_bind_group_layout,
2085 entries: &[wgpu::BindGroupEntry {
2086 binding: 0,
2087 resource: uniform_buf.as_entire_binding(),
2088 }],
2089 });
2090
2091 let n = gpu_set.cpu_positions.len();
2092 let size_data: Vec<f32> = vec![pixel_radius; n];
2093 let size_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2094 label: Some("splat_outline_size_buf"),
2095 contents: bytemuck::cast_slice(&size_data),
2096 usage: wgpu::BufferUsages::VERTEX,
2097 });
2098
2099 splat_outline_buffers.push(crate::resources::SplatOutlineBuffers {
2100 position_buf,
2101 size_buf,
2102 instance_count: n as u32,
2103 _uniform_buf: uniform_buf,
2104 bind_group,
2105 });
2106 } else if !item.selected && item.pick_id != 0 {
2107 let sub_sel = frame.interaction.sub_selection.as_ref();
2109 let selected_indices: Vec<u32> = sub_sel
2110 .iter()
2111 .flat_map(|s| s.items.iter())
2112 .filter_map(|(node_id, sub)| {
2113 if *node_id == item.pick_id {
2114 if let crate::interaction::sub_object::SubObjectRef::Splat(i) = sub {
2115 return Some(*i);
2116 }
2117 }
2118 None
2119 })
2120 .collect();
2121 if selected_indices.is_empty() {
2122 continue;
2123 }
2124
2125 let model = glam::Mat4::from_cols_array_2d(&item.model);
2126 let cam_right = frame.camera.render_camera.view.row(0).truncate().normalize();
2127
2128 let mut positions: Vec<[f32; 3]> = Vec::with_capacity(selected_indices.len());
2129 let mut sizes: Vec<f32> = Vec::with_capacity(selected_indices.len());
2130 for &idx in &selected_indices {
2131 let i = idx as usize;
2132 if let Some(&pos) = gpu_set.cpu_positions.get(i) {
2133 positions.push(pos);
2134 let world_radius = if let Some(s) = gpu_set.cpu_scales.get(i) {
2135 s[0].max(s[1]).max(s[2]) * 3.0
2136 } else {
2137 0.15
2138 };
2139 let center_w = model.transform_point3(glam::Vec3::from(pos));
2140 let p0_clip = view_proj * glam::Vec4::new(center_w.x, center_w.y, center_w.z, 1.0);
2141 let p1_world = center_w + cam_right * world_radius;
2142 let p1_clip = view_proj * glam::Vec4::new(p1_world.x, p1_world.y, p1_world.z, 1.0);
2143 let px = if p0_clip.w.abs() > 1e-6 && p1_clip.w.abs() > 1e-6 {
2144 let p0_ndc = glam::Vec2::new(p0_clip.x, p0_clip.y) / p0_clip.w;
2145 let p1_ndc = glam::Vec2::new(p1_clip.x, p1_clip.y) / p1_clip.w;
2146 ((p1_ndc - p0_ndc).length() * 0.5 * vp_w.max(vp_h)).max(1.0)
2147 } else {
2148 world_radius * 100.0
2149 };
2150 sizes.push(px);
2151 }
2152 }
2153 if positions.is_empty() {
2154 continue;
2155 }
2156
2157 let pixel_radius = sizes.iter().cloned().fold(f32::NEG_INFINITY, f32::max).max(1.0);
2158 let uniform = SplatOutlineMaskUniform {
2159 model: item.model,
2160 viewport_w: vp_w,
2161 viewport_h: vp_h,
2162 pixel_radius,
2163 _pad: [0.0; 5],
2164 };
2165 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2166 label: Some("splat_sel_outline_uniform_buf"),
2167 contents: bytemuck::cast_slice(&[uniform]),
2168 usage: wgpu::BufferUsages::UNIFORM,
2169 });
2170 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2171 label: Some("splat_sel_outline_bg"),
2172 layout: &resources.outline_bind_group_layout,
2173 entries: &[wgpu::BindGroupEntry {
2174 binding: 0,
2175 resource: uniform_buf.as_entire_binding(),
2176 }],
2177 });
2178 let position_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2179 label: Some("splat_sel_outline_pos_buf"),
2180 contents: bytemuck::cast_slice(&positions),
2181 usage: wgpu::BufferUsages::VERTEX,
2182 });
2183 let size_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2184 label: Some("splat_sel_outline_size_buf"),
2185 contents: bytemuck::cast_slice(&sizes),
2186 usage: wgpu::BufferUsages::VERTEX,
2187 });
2188 splat_outline_buffers.push(crate::resources::SplatOutlineBuffers {
2189 position_buf,
2190 size_buf,
2191 instance_count: positions.len() as u32,
2192 _uniform_buf: uniform_buf,
2193 bind_group,
2194 });
2195 }
2196 }
2197
2198 for item in &frame.scene.point_clouds {
2200 if item.positions.is_empty() {
2201 continue;
2202 }
2203 let pixel_radius = (item.point_size * 0.5).max(1.0);
2204 if item.selected {
2205 let position_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2207 label: Some("pc_outline_pos_buf"),
2208 contents: bytemuck::cast_slice(item.positions.as_slice()),
2209 usage: wgpu::BufferUsages::VERTEX,
2210 });
2211 let uniform = SplatOutlineMaskUniform {
2212 model: item.model,
2213 viewport_w: vp_w,
2214 viewport_h: vp_h,
2215 pixel_radius,
2216 _pad: [0.0; 5],
2217 };
2218 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2219 label: Some("pc_outline_uniform_buf"),
2220 contents: bytemuck::cast_slice(&[uniform]),
2221 usage: wgpu::BufferUsages::UNIFORM,
2222 });
2223 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2224 label: Some("pc_outline_bg"),
2225 layout: &self.resources.outline_bind_group_layout,
2226 entries: &[wgpu::BindGroupEntry {
2227 binding: 0,
2228 resource: uniform_buf.as_entire_binding(),
2229 }],
2230 });
2231 let n = item.positions.len();
2232 let size_data: Vec<f32> = vec![pixel_radius; n];
2233 let size_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2234 label: Some("pc_outline_size_buf"),
2235 contents: bytemuck::cast_slice(&size_data),
2236 usage: wgpu::BufferUsages::VERTEX,
2237 });
2238 splat_outline_buffers.push(crate::resources::SplatOutlineBuffers {
2239 position_buf,
2240 size_buf,
2241 instance_count: n as u32,
2242 _uniform_buf: uniform_buf,
2243 bind_group,
2244 });
2245 } else if item.id != 0 {
2246 let sub_sel = frame.interaction.sub_selection.as_ref();
2248 let selected_positions: Vec<[f32; 3]> = sub_sel
2249 .iter()
2250 .flat_map(|s| s.items.iter())
2251 .filter_map(|(node_id, sub)| {
2252 if *node_id == item.id {
2253 if let crate::interaction::sub_object::SubObjectRef::Point(i) = sub {
2254 return item.positions.get(*i as usize).copied();
2255 }
2256 }
2257 None
2258 })
2259 .collect();
2260 if selected_positions.is_empty() {
2261 continue;
2262 }
2263 let n = selected_positions.len();
2264 let uniform = SplatOutlineMaskUniform {
2265 model: item.model,
2266 viewport_w: vp_w,
2267 viewport_h: vp_h,
2268 pixel_radius,
2269 _pad: [0.0; 5],
2270 };
2271 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2272 label: Some("pc_sel_outline_uniform_buf"),
2273 contents: bytemuck::cast_slice(&[uniform]),
2274 usage: wgpu::BufferUsages::UNIFORM,
2275 });
2276 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2277 label: Some("pc_sel_outline_bg"),
2278 layout: &self.resources.outline_bind_group_layout,
2279 entries: &[wgpu::BindGroupEntry {
2280 binding: 0,
2281 resource: uniform_buf.as_entire_binding(),
2282 }],
2283 });
2284 let position_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2285 label: Some("pc_sel_outline_pos_buf"),
2286 contents: bytemuck::cast_slice(&selected_positions),
2287 usage: wgpu::BufferUsages::VERTEX,
2288 });
2289 let size_data = vec![pixel_radius; n];
2290 let size_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2291 label: Some("pc_sel_outline_size_buf"),
2292 contents: bytemuck::cast_slice(&size_data),
2293 usage: wgpu::BufferUsages::VERTEX,
2294 });
2295 splat_outline_buffers.push(crate::resources::SplatOutlineBuffers {
2296 position_buf,
2297 size_buf,
2298 instance_count: n as u32,
2299 _uniform_buf: uniform_buf,
2300 bind_group,
2301 });
2302 }
2303 }
2304
2305 {
2308 let sub_sel = frame.interaction.sub_selection.as_ref();
2309 let mut gpu_idx = 0usize;
2310 for item in &frame.scene.glyphs {
2311 if item.positions.is_empty() || item.vectors.is_empty() {
2312 continue;
2313 }
2314 if item.selected {
2315 self.resources.ensure_glyph_outline_mask_pipeline(device);
2316 glyph_outline_indices.push((gpu_idx, None));
2317 } else if item.id != 0 {
2318 let instances: Vec<u32> = sub_sel
2320 .iter()
2321 .flat_map(|s| s.items.iter())
2322 .filter_map(|(node_id, sub)| {
2323 if *node_id == item.id {
2324 if let crate::interaction::sub_object::SubObjectRef::Instance(i) = sub {
2325 return Some(*i);
2326 }
2327 }
2328 None
2329 })
2330 .collect();
2331 if !instances.is_empty() {
2332 self.resources.ensure_glyph_outline_mask_pipeline(device);
2333 glyph_outline_indices.push((gpu_idx, Some(instances)));
2334 }
2335 }
2336 gpu_idx += 1;
2337 }
2338 }
2339
2340 for item in &frame.scene.polylines {
2342 if !item.selected || item.positions.is_empty() {
2343 continue;
2344 }
2345
2346 let pixel_radius = item.line_width.max(2.0);
2347
2348 let position_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2349 label: Some("polyline_outline_pos_buf"),
2350 contents: bytemuck::cast_slice(item.positions.as_slice()),
2351 usage: wgpu::BufferUsages::VERTEX,
2352 });
2353
2354 let uniform = SplatOutlineMaskUniform {
2355 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
2356 viewport_w: vp_w,
2357 viewport_h: vp_h,
2358 pixel_radius,
2359 _pad: [0.0; 5],
2360 };
2361 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2362 label: Some("polyline_outline_uniform_buf"),
2363 contents: bytemuck::cast_slice(&[uniform]),
2364 usage: wgpu::BufferUsages::UNIFORM,
2365 });
2366 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2367 label: Some("polyline_outline_bg"),
2368 layout: &self.resources.outline_bind_group_layout,
2369 entries: &[wgpu::BindGroupEntry {
2370 binding: 0,
2371 resource: uniform_buf.as_entire_binding(),
2372 }],
2373 });
2374
2375 let n = item.positions.len();
2376 let size_data: Vec<f32> = vec![pixel_radius; n];
2377 let size_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2378 label: Some("polyline_outline_size_buf"),
2379 contents: bytemuck::cast_slice(&size_data),
2380 usage: wgpu::BufferUsages::VERTEX,
2381 });
2382
2383 splat_outline_buffers.push(crate::resources::SplatOutlineBuffers {
2384 position_buf,
2385 size_buf,
2386 instance_count: n as u32,
2387 _uniform_buf: uniform_buf,
2388 bind_group,
2389 });
2390 }
2391
2392 {
2395 let sub_sel = frame.interaction.sub_selection.as_ref();
2396 for (i, item) in frame.scene.sprite_items.iter().enumerate() {
2397 if item.positions.is_empty() {
2398 continue;
2399 }
2400 if item.selected {
2401 self.resources.ensure_sprite_outline_mask_pipeline(device);
2402 sprite_outline_indices.push((i, None));
2403 } else if item.id != 0 {
2404 let instances: Vec<u32> = sub_sel
2405 .iter()
2406 .flat_map(|s| s.items.iter())
2407 .filter_map(|(node_id, sub)| {
2408 if *node_id == item.id {
2409 if let crate::interaction::sub_object::SubObjectRef::Instance(idx) = sub {
2410 return Some(*idx);
2411 }
2412 }
2413 None
2414 })
2415 .collect();
2416 if !instances.is_empty() {
2417 self.resources.ensure_sprite_outline_mask_pipeline(device);
2418 sprite_outline_indices.push((i, Some(instances)));
2419 }
2420 }
2421 }
2422 }
2423
2424 let curve_sets: Vec<(&[[f32; 3]], f32)> = frame
2427 .scene
2428 .streamtube_items
2429 .iter()
2430 .filter(|s| s.selected && !s.positions.is_empty())
2431 .map(|s| (s.positions.as_slice(), s.radius * 16.0))
2432 .chain(
2433 frame
2434 .scene
2435 .tube_items
2436 .iter()
2437 .filter(|s| s.selected && !s.positions.is_empty())
2438 .map(|s| (s.positions.as_slice(), s.radius * 16.0)),
2439 )
2440 .chain(
2441 frame
2442 .scene
2443 .ribbon_items
2444 .iter()
2445 .filter(|s| s.selected && !s.positions.is_empty())
2446 .map(|s| (s.positions.as_slice(), s.width * 8.0)),
2447 )
2448 .collect();
2449
2450 for (positions, pixel_radius) in curve_sets {
2451 let pixel_radius = pixel_radius.max(4.0);
2452
2453 let position_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2454 label: Some("curve_outline_pos_buf"),
2455 contents: bytemuck::cast_slice(positions),
2456 usage: wgpu::BufferUsages::VERTEX,
2457 });
2458
2459 let uniform = SplatOutlineMaskUniform {
2460 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
2461 viewport_w: vp_w,
2462 viewport_h: vp_h,
2463 pixel_radius,
2464 _pad: [0.0; 5],
2465 };
2466 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2467 label: Some("curve_outline_uniform_buf"),
2468 contents: bytemuck::cast_slice(&[uniform]),
2469 usage: wgpu::BufferUsages::UNIFORM,
2470 });
2471 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2472 label: Some("curve_outline_bg"),
2473 layout: &self.resources.outline_bind_group_layout,
2474 entries: &[wgpu::BindGroupEntry {
2475 binding: 0,
2476 resource: uniform_buf.as_entire_binding(),
2477 }],
2478 });
2479
2480 let n = positions.len();
2481 let size_data: Vec<f32> = vec![pixel_radius; n];
2482 let size_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2483 label: Some("curve_outline_size_buf"),
2484 contents: bytemuck::cast_slice(&size_data),
2485 usage: wgpu::BufferUsages::VERTEX,
2486 });
2487
2488 splat_outline_buffers.push(crate::resources::SplatOutlineBuffers {
2489 position_buf,
2490 size_buf,
2491 instance_count: n as u32,
2492 _uniform_buf: uniform_buf,
2493 bind_group,
2494 });
2495 }
2496
2497 {
2499 let sub_sel = frame.interaction.sub_selection.as_ref();
2500 let mut gpu_idx = 0usize;
2501 for item in &frame.scene.tensor_glyphs {
2502 if item.positions.is_empty() {
2503 continue;
2504 }
2505 if item.selected {
2506 self.resources.ensure_tensor_glyph_outline_mask_pipeline(device);
2507 tensor_glyph_outline_indices.push((gpu_idx, None));
2508 } else if item.id != 0 {
2509 let instances: Vec<u32> = sub_sel
2510 .iter()
2511 .flat_map(|s| s.items.iter())
2512 .filter_map(|(node_id, sub)| {
2513 if *node_id == item.id {
2514 if let crate::interaction::sub_object::SubObjectRef::Instance(i) = sub {
2515 return Some(*i);
2516 }
2517 }
2518 None
2519 })
2520 .collect();
2521 if !instances.is_empty() {
2522 self.resources.ensure_tensor_glyph_outline_mask_pipeline(device);
2523 tensor_glyph_outline_indices.push((gpu_idx, Some(instances)));
2524 }
2525 }
2526 gpu_idx += 1;
2527 }
2528 }
2529 }
2530
2531 let mut volume_outline_indices: Vec<usize> = Vec::new();
2535 if frame.interaction.outline_selected {
2536 self.resources.ensure_volume_cube(device);
2537 self.resources.ensure_volume_pipeline(device);
2538 self.resources.ensure_volume_outline_mask_pipeline(device);
2539 for (i, item) in frame.scene.volumes.iter().enumerate() {
2540 if item.selected {
2541 volume_outline_indices.push(i);
2542 }
2543 }
2544 }
2545
2546 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
2548 if frame.interaction.xray_selected {
2549 let resources = &self.resources;
2550 for item in scene_items {
2551 if !item.visible || !item.selected {
2552 continue;
2553 }
2554 let uniform = OutlineUniform {
2555 model: item.model,
2556 color: frame.interaction.xray_color,
2557 pixel_offset: 0.0,
2558 _pad: [0.0; 3],
2559 };
2560 let buf = device.create_buffer(&wgpu::BufferDescriptor {
2561 label: Some("xray_uniform_buf"),
2562 size: std::mem::size_of::<OutlineUniform>() as u64,
2563 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2564 mapped_at_creation: false,
2565 });
2566 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
2567 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
2568 label: Some("xray_object_bg"),
2569 layout: &resources.outline_bind_group_layout,
2570 entries: &[wgpu::BindGroupEntry {
2571 binding: 0,
2572 resource: buf.as_entire_binding(),
2573 }],
2574 });
2575 xray_object_buffers.push((item.mesh_id, buf, bg));
2576 }
2577 }
2578
2579 let mut constraint_line_buffers = Vec::new();
2581 for overlay in &frame.interaction.constraint_overlays {
2582 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
2583 }
2584
2585 let mut clip_plane_fill_buffers = Vec::new();
2587 let mut clip_plane_line_buffers = Vec::new();
2588 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
2589 if obj.color.is_none() && obj.edge_color.is_none() {
2591 continue;
2592 }
2593 if let ClipShape::Plane {
2594 normal,
2595 distance,
2596 display_center,
2597 ..
2598 } = obj.shape
2599 {
2600 let n = glam::Vec3::from(normal);
2601 let center = display_center
2606 .map(glam::Vec3::from)
2607 .unwrap_or_else(|| n * (-distance));
2608 let active = obj.active;
2609 let hovered = obj.hovered || active;
2610
2611 let fill_color = if let Some(base_color) = obj.color {
2613 if active {
2614 [
2615 base_color[0] * 0.5,
2616 base_color[1] * 0.5,
2617 base_color[2] * 0.5,
2618 base_color[3] * 0.5,
2619 ]
2620 } else if hovered {
2621 [
2622 base_color[0] * 0.8,
2623 base_color[1] * 0.8,
2624 base_color[2] * 0.8,
2625 base_color[3] * 0.6,
2626 ]
2627 } else {
2628 [
2629 base_color[0] * 0.5,
2630 base_color[1] * 0.5,
2631 base_color[2] * 0.5,
2632 base_color[3] * 0.3,
2633 ]
2634 }
2635 } else {
2636 [0.0, 0.0, 0.0, 0.0]
2637 };
2638
2639 let border_base = obj
2641 .edge_color
2642 .or(obj.color)
2643 .unwrap_or([1.0, 1.0, 1.0, 1.0]);
2644 let border_color = if active {
2645 [border_base[0], border_base[1], border_base[2], 0.9]
2646 } else if hovered {
2647 [border_base[0], border_base[1], border_base[2], 0.8]
2648 } else {
2649 [
2650 border_base[0] * 0.9,
2651 border_base[1] * 0.9,
2652 border_base[2] * 0.9,
2653 0.6,
2654 ]
2655 };
2656
2657 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
2658 center,
2659 normal: n,
2660 extent: obj.extent,
2661 fill_color,
2662 border_color,
2663 _hovered: hovered,
2664 _active: active,
2665 };
2666 if obj.color.is_some() {
2667 clip_plane_fill_buffers.push(
2668 self.resources
2669 .create_clip_plane_fill_overlay(device, &overlay),
2670 );
2671 }
2672 clip_plane_line_buffers.push(
2673 self.resources
2674 .create_clip_plane_line_overlay(device, &overlay),
2675 );
2676 } else {
2677 let base_color = obj.color.unwrap_or([1.0, 1.0, 1.0, 1.0]);
2682 self.resources.ensure_polyline_no_clip_pipeline(device);
2683 match obj.shape {
2684 ClipShape::Box {
2685 center,
2686 half_extents,
2687 orientation,
2688 } => {
2689 let polyline =
2690 clip_box_outline(center, half_extents, orientation, base_color);
2691 let vp_size = frame.camera.viewport_size;
2692 let mut gpu = self
2693 .resources
2694 .upload_polyline(device, queue, &polyline, vp_size);
2695 gpu.skip_clip = true;
2696 self.polyline_gpu_data.push(gpu);
2697 }
2698 ClipShape::Sphere { center, radius } => {
2699 let polyline = clip_sphere_outline(center, radius, base_color);
2700 let vp_size = frame.camera.viewport_size;
2701 let mut gpu = self
2702 .resources
2703 .upload_polyline(device, queue, &polyline, vp_size);
2704 gpu.skip_clip = true;
2705 self.polyline_gpu_data.push(gpu);
2706 }
2707 ClipShape::Cylinder {
2708 center,
2709 axis,
2710 radius,
2711 half_length,
2712 } => {
2713 let polyline =
2714 clip_cylinder_outline(center, axis, radius, half_length, base_color);
2715 let vp_size = frame.camera.viewport_size;
2716 let mut gpu = self
2717 .resources
2718 .upload_polyline(device, queue, &polyline, vp_size);
2719 gpu.skip_clip = true;
2720 self.polyline_gpu_data.push(gpu);
2721 }
2722 _ => {}
2723 }
2724 }
2725 }
2726
2727 let mut cap_buffers = Vec::new();
2729 if viewport_fx.cap_fill_enabled {
2730 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
2731 if let ClipShape::Plane {
2732 normal,
2733 distance,
2734 cap_color,
2735 ..
2736 } = obj.shape
2737 {
2738 let plane_n = glam::Vec3::from(normal);
2739 for item in scene_items.iter().filter(|i| i.visible) {
2740 let Some(mesh) = self
2741 .resources
2742 .mesh_store
2743 .get(item.mesh_id)
2744 else {
2745 continue;
2746 };
2747 let model = glam::Mat4::from_cols_array_2d(&item.model);
2748 let world_aabb = mesh.aabb.transformed(&model);
2749 if !world_aabb.intersects_plane(plane_n, distance) {
2750 continue;
2751 }
2752 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
2753 else {
2754 continue;
2755 };
2756 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
2757 pos, idx, &model, plane_n, distance,
2758 ) {
2759 let bc = item.material.base_color;
2760 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
2761 let buf = self.resources.upload_cap_geometry(device, &cap, color);
2762 cap_buffers.push(buf);
2763 }
2764 }
2765 }
2766 }
2767 }
2768
2769 let axes_verts = if frame.viewport.show_axes_indicator
2771 && frame.camera.viewport_size[0] > 0.0
2772 && frame.camera.viewport_size[1] > 0.0
2773 {
2774 let verts = crate::widgets::axes_indicator::build_axes_geometry(
2775 frame.camera.viewport_size[0],
2776 frame.camera.viewport_size[1],
2777 frame.camera.render_camera.orientation,
2778 );
2779 if verts.is_empty() { None } else { Some(verts) }
2780 } else {
2781 None
2782 };
2783
2784 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
2786 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
2787 frame.interaction.gizmo_mode,
2788 frame.interaction.gizmo_hovered,
2789 frame.interaction.gizmo_space_orientation,
2790 );
2791 (verts, indices, model)
2792 });
2793
2794 {
2798 let slot = &mut self.viewport_slots[vp_idx];
2799 slot.outline_object_buffers = outline_object_buffers;
2800 slot.splat_outline_buffers = splat_outline_buffers;
2801 slot.volume_outline_indices = volume_outline_indices;
2802 slot.glyph_outline_indices = glyph_outline_indices;
2803 slot.tensor_glyph_outline_indices = tensor_glyph_outline_indices;
2804 slot.sprite_outline_indices = sprite_outline_indices;
2805 slot.xray_object_buffers = xray_object_buffers;
2806 slot.constraint_line_buffers = constraint_line_buffers;
2807 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
2808 slot.clip_plane_line_buffers = clip_plane_line_buffers;
2809 slot.cap_buffers = cap_buffers;
2810
2811 if let Some(verts) = axes_verts {
2813 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
2814 if byte_size > slot.axes_vertex_buffer.size() {
2815 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2816 label: Some("vp_axes_vertex_buf"),
2817 size: byte_size,
2818 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2819 mapped_at_creation: false,
2820 });
2821 }
2822 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
2823 slot.axes_vertex_count = verts.len() as u32;
2824 } else {
2825 slot.axes_vertex_count = 0;
2826 }
2827
2828 if let Some((verts, indices, model)) = gizmo_update {
2830 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
2831 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
2832 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
2833 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2834 label: Some("vp_gizmo_vertex_buf"),
2835 size: vert_bytes.len() as u64,
2836 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2837 mapped_at_creation: false,
2838 });
2839 }
2840 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
2841 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2842 label: Some("vp_gizmo_index_buf"),
2843 size: idx_bytes.len() as u64,
2844 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2845 mapped_at_creation: false,
2846 });
2847 }
2848 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
2849 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
2850 slot.gizmo_index_count = indices.len() as u32;
2851 let uniform = crate::interaction::gizmo::GizmoUniform {
2852 model: model.to_cols_array_2d(),
2853 };
2854 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
2855 }
2856 }
2857
2858 if frame.interaction.outline_selected
2869 && (!self.viewport_slots[vp_idx].outline_object_buffers.is_empty()
2870 || !self.viewport_slots[vp_idx].splat_outline_buffers.is_empty()
2871 || !self.viewport_slots[vp_idx].volume_outline_indices.is_empty()
2872 || !self.viewport_slots[vp_idx].glyph_outline_indices.is_empty()
2873 || !self.viewport_slots[vp_idx].tensor_glyph_outline_indices.is_empty()
2874 || !self.viewport_slots[vp_idx].sprite_outline_indices.is_empty())
2875 {
2876 let ppp = frame.camera.pixels_per_point;
2877 let w = (frame.camera.viewport_size[0] * ppp).round() as u32;
2878 let h = (frame.camera.viewport_size[1] * ppp).round() as u32;
2879
2880 self.ensure_viewport_hdr(
2882 device,
2883 queue,
2884 vp_idx,
2885 w.max(1),
2886 h.max(1),
2887 frame.effects.post_process.ssaa_factor.max(1),
2888 );
2889
2890 {
2892 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2893 let edge_uniform = OutlineEdgeUniform {
2894 color: frame.interaction.outline_color,
2895 radius: frame.interaction.outline_width_px,
2896 viewport_w: w as f32,
2897 viewport_h: h as f32,
2898 _pad: 0.0,
2899 };
2900 queue.write_buffer(
2901 &slot_hdr.outline_edge_uniform_buf,
2902 0,
2903 bytemuck::cast_slice(&[edge_uniform]),
2904 );
2905 }
2906
2907 let slot_ref = &self.viewport_slots[vp_idx];
2910 let outlines_ptr =
2911 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
2912 let splat_outlines_ptr =
2913 &slot_ref.splat_outline_buffers as *const Vec<crate::resources::SplatOutlineBuffers>;
2914 let vol_outline_idx_ptr =
2915 &slot_ref.volume_outline_indices as *const Vec<usize>;
2916 let glyph_outline_idx_ptr =
2917 &slot_ref.glyph_outline_indices as *const Vec<(usize, Option<Vec<u32>>)>;
2918 let tensor_glyph_outline_idx_ptr =
2919 &slot_ref.tensor_glyph_outline_indices as *const Vec<(usize, Option<Vec<u32>>)>;
2920 let sprite_outline_idx_ptr =
2921 &slot_ref.sprite_outline_indices as *const Vec<(usize, Option<Vec<u32>>)>;
2922 let glyph_gpu_ptr =
2923 &self.glyph_gpu_data as *const Vec<crate::resources::GlyphGpuData>;
2924 let tensor_glyph_gpu_ptr =
2925 &self.tensor_glyph_gpu_data as *const Vec<crate::resources::TensorGlyphGpuData>;
2926 let sprite_gpu_ptr =
2927 &self.sprite_gpu_data as *const Vec<crate::resources::SpriteGpuData>;
2928 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
2929 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
2930 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
2931 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
2932 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
2933 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
2934 let (outlines, splat_outlines, vol_outline_indices,
2937 glyph_outline_indices, tensor_glyph_outline_indices,
2938 sprite_outline_indices,
2939 glyph_gpu_data, tensor_glyph_gpu_data, sprite_gpu_data,
2940 camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
2941 (
2942 &*outlines_ptr,
2943 &*splat_outlines_ptr,
2944 &*vol_outline_idx_ptr,
2945 &*glyph_outline_idx_ptr,
2946 &*tensor_glyph_outline_idx_ptr,
2947 &*sprite_outline_idx_ptr,
2948 &*glyph_gpu_ptr,
2949 &*tensor_glyph_gpu_ptr,
2950 &*sprite_gpu_ptr,
2951 &*camera_bg_ptr,
2952 &*mask_view_ptr,
2953 &*color_view_ptr,
2954 &*depth_view_ptr,
2955 &*edge_bg_ptr,
2956 )
2957 };
2958
2959 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2960 label: Some("outline_offscreen_encoder"),
2961 });
2962
2963 {
2965 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2966 label: Some("outline_mask_pass"),
2967 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2968 view: mask_view,
2969 resolve_target: None,
2970 ops: wgpu::Operations {
2971 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2972 store: wgpu::StoreOp::Store,
2973 },
2974 depth_slice: None,
2975 })],
2976 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2977 view: depth_view,
2978 depth_ops: Some(wgpu::Operations {
2979 load: wgpu::LoadOp::Clear(1.0),
2980 store: wgpu::StoreOp::Discard,
2981 }),
2982 stencil_ops: None,
2983 }),
2984 timestamp_writes: None,
2985 occlusion_query_set: None,
2986 });
2987
2988 pass.set_bind_group(0, camera_bg, &[]);
2989 for outlined in outlines {
2990 let Some(mesh) = self
2991 .resources
2992 .mesh_store
2993 .get(outlined.mesh_id)
2994 else {
2995 continue;
2996 };
2997 let pipeline = if outlined.two_sided {
2998 &self.resources.outline_mask_two_sided_pipeline
2999 } else {
3000 &self.resources.outline_mask_pipeline
3001 };
3002 pass.set_pipeline(pipeline);
3003 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
3004 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
3005 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3006 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
3007 }
3008
3009 pass.set_pipeline(&self.resources.splat_outline_mask_pipeline);
3014 for splat in splat_outlines {
3015 pass.set_bind_group(1, &splat.bind_group, &[]);
3016 pass.set_vertex_buffer(0, splat.position_buf.slice(..));
3017 pass.set_vertex_buffer(1, splat.size_buf.slice(..));
3018 pass.draw(0..6, 0..splat.instance_count);
3019 }
3020
3021 if !glyph_outline_indices.is_empty() {
3024 if let Some(pipeline) = self.resources.glyph_outline_mask_pipeline.as_ref() {
3025 pass.set_pipeline(pipeline);
3026 for (idx, instance_filter) in glyph_outline_indices {
3027 if let Some(glyph) = glyph_gpu_data.get(*idx) {
3028 pass.set_bind_group(0, camera_bg, &[]);
3029 pass.set_bind_group(1, &glyph.uniform_bind_group, &[]);
3030 pass.set_bind_group(2, &glyph.instance_bind_group, &[]);
3031 pass.set_vertex_buffer(0, glyph.mesh_vertex_buffer.slice(..));
3032 pass.set_index_buffer(glyph.mesh_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3033 match instance_filter {
3034 None => {
3035 pass.draw_indexed(0..glyph.mesh_index_count, 0, 0..glyph.instance_count);
3036 }
3037 Some(indices) => {
3038 for &i in indices {
3039 pass.draw_indexed(0..glyph.mesh_index_count, 0, i..i + 1);
3040 }
3041 }
3042 }
3043 }
3044 }
3045 }
3046 }
3047
3048 if !tensor_glyph_outline_indices.is_empty() {
3050 if let Some(pipeline) = self.resources.tensor_glyph_outline_mask_pipeline.as_ref() {
3051 pass.set_pipeline(pipeline);
3052 for (idx, instance_filter) in tensor_glyph_outline_indices {
3053 if let Some(tg) = tensor_glyph_gpu_data.get(*idx) {
3054 pass.set_bind_group(0, camera_bg, &[]);
3055 pass.set_bind_group(1, &tg.uniform_bind_group, &[]);
3056 pass.set_bind_group(2, &tg.instance_bind_group, &[]);
3057 pass.set_vertex_buffer(0, tg.mesh_vertex_buffer.slice(..));
3058 pass.set_index_buffer(tg.mesh_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3059 match instance_filter {
3060 None => {
3061 pass.draw_indexed(0..tg.mesh_index_count, 0, 0..tg.instance_count);
3062 }
3063 Some(indices) => {
3064 for &i in indices {
3065 pass.draw_indexed(0..tg.mesh_index_count, 0, i..i + 1);
3066 }
3067 }
3068 }
3069 }
3070 }
3071 }
3072 }
3073
3074 if !sprite_outline_indices.is_empty() {
3077 if let Some(pipeline) = self.resources.sprite_outline_mask_pipeline.as_ref() {
3078 pass.set_pipeline(pipeline);
3079 for (idx, instance_filter) in sprite_outline_indices {
3080 if let Some(sprite) = sprite_gpu_data.get(*idx) {
3081 pass.set_bind_group(0, camera_bg, &[]);
3082 pass.set_bind_group(1, &sprite.bind_group, &[]);
3083 pass.set_vertex_buffer(0, sprite.vertex_buffer.slice(..));
3084 match instance_filter {
3085 None => {
3086 pass.draw(0..6, 0..sprite.sprite_count);
3087 }
3088 Some(indices) => {
3089 for &i in indices {
3090 pass.draw(0..6, i..i + 1);
3091 }
3092 }
3093 }
3094 }
3095 }
3096 }
3097 }
3098
3099 if !vol_outline_indices.is_empty() {
3102 if let Some(pipeline) = self.resources.volume_outline_mask_pipeline.as_ref() {
3103 pass.set_pipeline(pipeline);
3104 for &idx in vol_outline_indices {
3105 if let Some(vol) = self.volume_gpu_data.get(idx) {
3106 pass.set_bind_group(1, &vol.bind_group, &[]);
3107 pass.set_vertex_buffer(0, vol.vertex_buffer.slice(..));
3108 pass.set_index_buffer(vol.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
3109 pass.draw_indexed(0..36, 0, 0..1);
3110 }
3111 }
3112 }
3113 }
3114 }
3115
3116 {
3118 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3119 label: Some("outline_edge_pass"),
3120 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3121 view: color_view,
3122 resolve_target: None,
3123 ops: wgpu::Operations {
3124 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3125 store: wgpu::StoreOp::Store,
3126 },
3127 depth_slice: None,
3128 })],
3129 depth_stencil_attachment: None,
3130 timestamp_writes: None,
3131 occlusion_query_set: None,
3132 });
3133 pass.set_pipeline(&self.resources.outline_edge_pipeline);
3134 pass.set_bind_group(0, edge_bg, &[]);
3135 pass.draw(0..3, 0..1);
3136 }
3137
3138 queue.submit(std::iter::once(encoder.finish()));
3139 }
3140
3141 {
3146 let w = frame.camera.viewport_size[0];
3147 let h = frame.camera.viewport_size[1];
3148
3149 let has_sub_sel = frame.interaction.sub_selection.is_some();
3150
3151 if has_sub_sel {
3152 let needs_rebuild = {
3153 let slot = &self.viewport_slots[vp_idx];
3154 let sel_version_changed = frame
3155 .interaction
3156 .sub_selection
3157 .as_ref()
3158 .map(|s| slot.sub_highlight_generation != s.version)
3159 .unwrap_or(slot.sub_highlight_generation != u64::MAX);
3160 sel_version_changed
3161 || slot.sub_highlight.is_none()
3162 };
3163 if needs_rebuild {
3164 self.resources.ensure_sub_highlight_pipelines(device);
3165 let sel_ref = frame.interaction.sub_selection.as_ref();
3166 let data = self.resources.build_sub_highlight(
3167 device,
3168 queue,
3169 sel_ref,
3170 &std::collections::HashMap::new(),
3171 &[],
3172 frame.interaction.sub_highlight_face_fill_color,
3173 frame.interaction.sub_highlight_edge_color,
3174 frame.interaction.sub_highlight_edge_width_px,
3175 frame.interaction.sub_highlight_vertex_size_px,
3176 w,
3177 h,
3178 );
3179 let new_gen = frame
3180 .interaction
3181 .sub_selection
3182 .as_ref()
3183 .map(|s| s.version)
3184 .unwrap_or(u64::MAX);
3185 let slot = &mut self.viewport_slots[vp_idx];
3186 slot.sub_highlight = Some(data);
3187 slot.sub_highlight_generation = new_gen;
3188 }
3189 } else {
3190 let slot = &mut self.viewport_slots[vp_idx];
3191 slot.sub_highlight = None;
3192 slot.sub_highlight_generation = u64::MAX;
3193 }
3194 }
3195
3196 self.label_gpu_data = None;
3200 if !frame.overlays.labels.is_empty() {
3201 self.resources.ensure_overlay_text_pipeline(device);
3202 let vp_w = frame.camera.viewport_size[0];
3203 let vp_h = frame.camera.viewport_size[1];
3204 if vp_w > 0.0 && vp_h > 0.0 {
3205 let view = &frame.camera.render_camera.view;
3206 let proj = &frame.camera.render_camera.projection;
3207
3208 let mut sorted_labels: Vec<&crate::renderer::types::LabelItem> =
3210 frame.overlays.labels.iter().collect();
3211 sorted_labels.sort_by_key(|l| l.z_order);
3212
3213 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
3214
3215 for label in &sorted_labels {
3216 if label.text.is_empty() || label.opacity <= 0.0 {
3217 continue;
3218 }
3219
3220 let screen_pos = if let Some(sa) = label.screen_anchor {
3222 Some(sa)
3223 } else if let Some(wa) = label.world_anchor {
3224 project_to_screen(wa, view, proj, vp_w, vp_h)
3225 } else {
3226 continue;
3227 };
3228 let Some(anchor_px) = screen_pos else {
3229 continue;
3230 };
3231
3232 let opacity = label.opacity.clamp(0.0, 1.0);
3233
3234 let layout = if let Some(max_w) = label.max_width {
3236 self.resources.glyph_atlas.layout_text_wrapped(
3237 &label.text,
3238 label.font_size,
3239 label.font,
3240 max_w,
3241 device,
3242 )
3243 } else {
3244 self.resources.glyph_atlas.layout_text(
3245 &label.text,
3246 label.font_size,
3247 label.font,
3248 device,
3249 )
3250 };
3251
3252 let font_index = label.font.map_or(0, |h| h.0);
3254 let ascent = self.resources.glyph_atlas.font_ascent(font_index, label.font_size);
3255
3256 let align_offset = match label.anchor_align {
3258 crate::renderer::types::LabelAnchor::Leading => 6.0,
3259 crate::renderer::types::LabelAnchor::Center => -layout.total_width * 0.5,
3260 crate::renderer::types::LabelAnchor::Trailing => -layout.total_width - 6.0,
3261 };
3262
3263 let text_x = anchor_px[0] + align_offset + label.offset[0];
3265 let text_y = anchor_px[1] - layout.height * 0.5 + label.offset[1];
3266
3267 if label.background {
3269 let pad = label.padding;
3270 let bx0 = text_x - pad;
3271 let by0 = text_y - pad;
3272 let bx1 = text_x + layout.total_width + pad;
3273 let by1 = text_y + layout.height + pad;
3274 let bg_color = apply_opacity(label.background_color, opacity);
3275 if label.border_radius > 0.0 {
3276 emit_rounded_quad(
3277 &mut verts,
3278 bx0, by0, bx1, by1,
3279 label.border_radius,
3280 bg_color,
3281 vp_w, vp_h,
3282 );
3283 } else {
3284 emit_solid_quad(
3285 &mut verts,
3286 bx0, by0, bx1, by1,
3287 bg_color,
3288 vp_w, vp_h,
3289 );
3290 }
3291 }
3292
3293 if label.leader_line {
3295 if let Some(wa) = label.world_anchor {
3296 let world_px = project_to_screen(wa, view, proj, vp_w, vp_h);
3297 if let Some(wp) = world_px {
3298 emit_line_quad(
3299 &mut verts,
3300 wp[0], wp[1],
3301 text_x, text_y + layout.height * 0.5,
3302 1.5,
3303 apply_opacity(label.leader_color, opacity),
3304 vp_w, vp_h,
3305 );
3306 }
3307 }
3308 }
3309
3310 let text_color = apply_opacity(label.color, opacity);
3312 for gq in &layout.quads {
3313 let gx = text_x + gq.pos[0];
3314 let gy = text_y + ascent + gq.pos[1];
3315 emit_textured_quad(
3316 &mut verts,
3317 gx, gy,
3318 gx + gq.size[0], gy + gq.size[1],
3319 gq.uv_min, gq.uv_max,
3320 text_color,
3321 vp_w, vp_h,
3322 );
3323 }
3324 }
3325
3326 self.resources.glyph_atlas.upload_if_dirty(queue);
3328
3329 if !verts.is_empty() {
3330 let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3331 label: Some("overlay_label_vbuf"),
3332 contents: bytemuck::cast_slice(&verts),
3333 usage: wgpu::BufferUsages::VERTEX,
3334 });
3335 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3336 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3337 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3338 label: Some("overlay_label_bg"),
3339 layout: bgl,
3340 entries: &[
3341 wgpu::BindGroupEntry {
3342 binding: 0,
3343 resource: wgpu::BindingResource::TextureView(
3344 &self.resources.glyph_atlas.view,
3345 ),
3346 },
3347 wgpu::BindGroupEntry {
3348 binding: 1,
3349 resource: wgpu::BindingResource::Sampler(sampler),
3350 },
3351 ],
3352 });
3353 self.label_gpu_data = Some(crate::resources::LabelGpuData {
3354 vertex_buf,
3355 vertex_count: verts.len() as u32,
3356 bind_group,
3357 });
3358 }
3359 }
3360 }
3361
3362 self.scalar_bar_gpu_data = None;
3366 if !frame.overlays.scalar_bars.is_empty() {
3367 self.resources.ensure_overlay_text_pipeline(device);
3368 let vp_w = frame.camera.viewport_size[0];
3369 let vp_h = frame.camera.viewport_size[1];
3370 if vp_w > 0.0 && vp_h > 0.0 {
3371 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
3372
3373 for bar in &frame.overlays.scalar_bars {
3374 let Some(lut) = self.resources.get_colormap_rgba(bar.colormap_id).map(|l| l.to_vec()) else {
3377 continue;
3378 };
3379
3380 let is_vertical = matches!(
3381 bar.orientation,
3382 crate::renderer::types::ScalarBarOrientation::Vertical
3383 );
3384 let reversed = bar.ticks_reversed;
3385
3386 let tick_fs = bar.font_size;
3388 let title_fs = bar.title_font_size.unwrap_or(bar.font_size);
3389 let font_index = bar.font.map_or(0, |h| h.0);
3390
3391 let (strip_w, strip_h) = if is_vertical {
3393 (bar.bar_width_px, bar.bar_length_px)
3394 } else {
3395 (bar.bar_length_px, bar.bar_width_px)
3396 };
3397
3398 let tick_count = bar.tick_count.max(2);
3401 let mut tick_data: Vec<(String, f32, f32)> = Vec::new(); let mut max_tick_w = 0.0f32;
3403 let mut tick_h = 0.0f32;
3404 for i in 0..tick_count {
3405 let t = i as f32 / (tick_count - 1) as f32;
3406 let value = bar.scalar_min + t * (bar.scalar_max - bar.scalar_min);
3407 let text = format!("{value:.2}");
3408 let layout = self.resources.glyph_atlas.layout_text(
3409 &text, tick_fs, bar.font, device,
3410 );
3411 max_tick_w = max_tick_w.max(layout.total_width);
3412 tick_h = layout.height;
3413 tick_data.push((text, layout.total_width, layout.height));
3414 }
3415
3416 let half_tick = tick_h / 2.0;
3421 let title_h = if bar.title.is_some() {
3422 title_fs + 4.0 + half_tick
3424 } else {
3425 half_tick
3427 };
3428
3429 let title_w = if let Some(ref t) = bar.title {
3432 self.resources.glyph_atlas.layout_text(t, title_fs, bar.font, device).total_width
3433 } else {
3434 0.0
3435 };
3436
3437 let bg_pad = 4.0;
3443 let (inset_left, inset_right) = if is_vertical {
3444 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
3445 let right_extent = 4.0 + max_tick_w + bg_pad; (title_oh + bg_pad, right_extent)
3447 } else {
3448 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
3449 let tick_oh = max_tick_w / 2.0;
3450 let side = title_oh.max(tick_oh) + bg_pad;
3451 (side, side)
3452 };
3453
3454 let bottom_overhang = if is_vertical { half_tick } else { 3.0 + tick_h };
3459
3460 let (bar_x, bar_y) = match bar.anchor {
3466 crate::renderer::types::ScalarBarAnchor::TopLeft => (
3467 bar.margin_px + inset_left,
3468 bar.margin_px + title_h + bg_pad,
3469 ),
3470 crate::renderer::types::ScalarBarAnchor::TopRight => (
3471 vp_w - bar.margin_px - strip_w - inset_right,
3472 bar.margin_px + title_h + bg_pad,
3473 ),
3474 crate::renderer::types::ScalarBarAnchor::BottomLeft => (
3475 bar.margin_px + inset_left,
3476 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
3477 ),
3478 crate::renderer::types::ScalarBarAnchor::BottomRight => (
3479 vp_w - bar.margin_px - strip_w - inset_right,
3480 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
3481 ),
3482 };
3483
3484 let (bg_x0, bg_y0, bg_x1, bg_y1) = if is_vertical {
3486 let title_right = bar_x + (strip_w + title_w) / 2.0;
3487 let ticks_right = bar_x + strip_w + 4.0 + max_tick_w;
3488 (
3489 bar_x - bg_pad - ((title_w - strip_w) / 2.0).max(0.0),
3490 bar_y - title_h - bg_pad,
3491 ticks_right.max(title_right) + bg_pad,
3492 bar_y + strip_h + half_tick + bg_pad,
3493 )
3494 } else {
3495 let title_overhang = ((title_w - strip_w) / 2.0).max(0.0);
3496 let tick_overhang = max_tick_w / 2.0;
3497 let side_pad = title_overhang.max(tick_overhang);
3498 let bottom = bar_y + strip_h + 3.0 + tick_h + bg_pad;
3499 (
3500 bar_x - bg_pad - side_pad,
3501 bar_y - title_h - bg_pad,
3502 bar_x + strip_w + bg_pad + side_pad,
3503 bottom,
3504 )
3505 };
3506 emit_rounded_quad(
3507 &mut verts,
3508 bg_x0, bg_y0, bg_x1, bg_y1,
3509 3.0,
3510 bar.background_color,
3511 vp_w, vp_h,
3512 );
3513
3514 let steps: usize = 64;
3516 for s in 0..steps {
3517 let (qx0, qy0, qx1, qy1, t) = if is_vertical {
3518 let t = if reversed {
3520 s as f32 / (steps - 1) as f32
3521 } else {
3522 1.0 - s as f32 / (steps - 1) as f32
3523 };
3524 let step_h = strip_h / steps as f32;
3525 let sy = bar_y + s as f32 * step_h;
3526 (bar_x, sy, bar_x + strip_w, sy + step_h + 0.5, t)
3527 } else {
3528 let t = if reversed {
3530 1.0 - s as f32 / (steps - 1) as f32
3531 } else {
3532 s as f32 / (steps - 1) as f32
3533 };
3534 let step_w = strip_w / steps as f32;
3535 let sx = bar_x + s as f32 * step_w;
3536 (sx, bar_y, sx + step_w + 0.5, bar_y + strip_h, t)
3537 };
3538 let lut_idx = (t * 255.0).clamp(0.0, 255.0) as usize;
3539 let [r, g, b, a] = lut[lut_idx];
3540 let color = [
3541 r as f32 / 255.0,
3542 g as f32 / 255.0,
3543 b as f32 / 255.0,
3544 a as f32 / 255.0,
3545 ];
3546 emit_solid_quad(&mut verts, qx0, qy0, qx1, qy1, color, vp_w, vp_h);
3547 }
3548
3549 let ascent = self.resources.glyph_atlas.font_ascent(font_index, tick_fs);
3551 for (i, (text, tw, th)) in tick_data.iter().enumerate() {
3552 let t = i as f32 / (tick_count - 1) as f32;
3553 let layout = self.resources.glyph_atlas.layout_text(
3554 text, tick_fs, bar.font, device,
3555 );
3556
3557 let (lx, ly) = if is_vertical {
3558 let progress = if reversed { t } else { 1.0 - t };
3563 let tick_y = bar_y + progress * strip_h;
3564 (bar_x + strip_w + 4.0, tick_y - th * 0.5)
3565 } else {
3566 let frac = if reversed { 1.0 - t } else { t };
3570 let tick_x = bar_x + frac * strip_w;
3571 (tick_x - tw * 0.5, bar_y + strip_h + 3.0)
3572 };
3573 let _ = (tw, th); for gq in &layout.quads {
3576 let gx = lx + gq.pos[0];
3577 let gy = ly + ascent + gq.pos[1];
3578 emit_textured_quad(
3579 &mut verts,
3580 gx, gy,
3581 gx + gq.size[0], gy + gq.size[1],
3582 gq.uv_min, gq.uv_max,
3583 bar.label_color,
3584 vp_w, vp_h,
3585 );
3586 }
3587 }
3588
3589 if let Some(ref title_text) = bar.title {
3591 let layout = self.resources.glyph_atlas.layout_text(
3592 title_text, title_fs, bar.font, device,
3593 );
3594 let title_ascent = self.resources.glyph_atlas.font_ascent(font_index, title_fs);
3595 let tx = bar_x + (strip_w - layout.total_width) * 0.5;
3597 let ty = bar_y - title_h;
3598 for gq in &layout.quads {
3599 let gx = tx + gq.pos[0];
3600 let gy = ty + title_ascent + gq.pos[1];
3601 emit_textured_quad(
3602 &mut verts,
3603 gx, gy,
3604 gx + gq.size[0], gy + gq.size[1],
3605 gq.uv_min, gq.uv_max,
3606 bar.label_color,
3607 vp_w, vp_h,
3608 );
3609 }
3610 }
3611 }
3612
3613 self.resources.glyph_atlas.upload_if_dirty(queue);
3615
3616 if !verts.is_empty() {
3617 let vertex_buf =
3618 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3619 label: Some("overlay_scalar_bar_vbuf"),
3620 contents: bytemuck::cast_slice(&verts),
3621 usage: wgpu::BufferUsages::VERTEX,
3622 });
3623 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3624 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3625 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3626 label: Some("overlay_scalar_bar_bg"),
3627 layout: bgl,
3628 entries: &[
3629 wgpu::BindGroupEntry {
3630 binding: 0,
3631 resource: wgpu::BindingResource::TextureView(
3632 &self.resources.glyph_atlas.view,
3633 ),
3634 },
3635 wgpu::BindGroupEntry {
3636 binding: 1,
3637 resource: wgpu::BindingResource::Sampler(sampler),
3638 },
3639 ],
3640 });
3641 self.scalar_bar_gpu_data = Some(crate::resources::LabelGpuData {
3642 vertex_buf,
3643 vertex_count: verts.len() as u32,
3644 bind_group,
3645 });
3646 }
3647 }
3648 }
3649
3650 self.ruler_gpu_data = None;
3654 if !frame.overlays.rulers.is_empty() {
3655 self.resources.ensure_overlay_text_pipeline(device);
3656 let vp_w = frame.camera.viewport_size[0];
3657 let vp_h = frame.camera.viewport_size[1];
3658 if vp_w > 0.0 && vp_h > 0.0 {
3659 let view = &frame.camera.render_camera.view;
3660 let proj = &frame.camera.render_camera.projection;
3661
3662 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
3663
3664 for ruler in &frame.overlays.rulers {
3665 let start_ndc = project_to_ndc(ruler.start, view, proj);
3667 let end_ndc = project_to_ndc(ruler.end, view, proj);
3668
3669 let (Some(sndc), Some(endc)) = (start_ndc, end_ndc) else { continue };
3671
3672 let Some((csndc, cendc)) = clip_line_ndc(sndc, endc) else { continue };
3675
3676 let [sx, sy] = ndc_to_screen_px(csndc, vp_w, vp_h);
3677 let [ex, ey] = ndc_to_screen_px(cendc, vp_w, vp_h);
3678
3679 let start_on_screen = ndc_in_viewport(sndc);
3681 let end_on_screen = ndc_in_viewport(endc);
3682
3683 emit_line_quad(
3685 &mut verts,
3686 sx, sy, ex, ey,
3687 ruler.line_width_px,
3688 ruler.color,
3689 vp_w, vp_h,
3690 );
3691
3692 if ruler.end_caps {
3694 let dx = ex - sx;
3695 let dy = ey - sy;
3696 let len = (dx * dx + dy * dy).sqrt().max(0.001);
3697 let cap_half = 5.0;
3698 let px = -dy / len * cap_half;
3699 let py = dx / len * cap_half;
3700
3701 if start_on_screen {
3702 emit_line_quad(
3703 &mut verts,
3704 sx - px, sy - py,
3705 sx + px, sy + py,
3706 ruler.line_width_px,
3707 ruler.color,
3708 vp_w, vp_h,
3709 );
3710 }
3711 if end_on_screen {
3712 emit_line_quad(
3713 &mut verts,
3714 ex - px, ey - py,
3715 ex + px, ey + py,
3716 ruler.line_width_px,
3717 ruler.color,
3718 vp_w, vp_h,
3719 );
3720 }
3721 }
3722
3723 let start_world = glam::Vec3::from(ruler.start);
3726 let end_world = glam::Vec3::from(ruler.end);
3727 let distance = (end_world - start_world).length();
3728 let text = format_ruler_distance(distance, ruler.label_format.as_deref());
3729
3730 let mid_x = (sx + ex) * 0.5;
3731 let mid_y = (sy + ey) * 0.5;
3732
3733 let layout = self.resources.glyph_atlas.layout_text(
3734 &text,
3735 ruler.font_size,
3736 ruler.font,
3737 device,
3738 );
3739 let font_index = ruler.font.map_or(0, |h| h.0);
3740 let ascent = self.resources.glyph_atlas.font_ascent(font_index, ruler.font_size);
3741
3742 let lx = mid_x - layout.total_width * 0.5;
3744 let ly = mid_y - layout.height - 6.0;
3745
3746 let pad = 3.0;
3748 emit_solid_quad(
3749 &mut verts,
3750 lx - pad, ly - pad,
3751 lx + layout.total_width + pad, ly + layout.height + pad,
3752 [0.0, 0.0, 0.0, 0.55],
3753 vp_w, vp_h,
3754 );
3755
3756 for gq in &layout.quads {
3758 let gx = lx + gq.pos[0];
3759 let gy = ly + ascent + gq.pos[1];
3760 emit_textured_quad(
3761 &mut verts,
3762 gx, gy,
3763 gx + gq.size[0], gy + gq.size[1],
3764 gq.uv_min, gq.uv_max,
3765 ruler.label_color,
3766 vp_w, vp_h,
3767 );
3768 }
3769 }
3770
3771 self.resources.glyph_atlas.upload_if_dirty(queue);
3773
3774 if !verts.is_empty() {
3775 let vertex_buf =
3776 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3777 label: Some("overlay_ruler_vbuf"),
3778 contents: bytemuck::cast_slice(&verts),
3779 usage: wgpu::BufferUsages::VERTEX,
3780 });
3781 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3782 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3783 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3784 label: Some("overlay_ruler_bg"),
3785 layout: bgl,
3786 entries: &[
3787 wgpu::BindGroupEntry {
3788 binding: 0,
3789 resource: wgpu::BindingResource::TextureView(
3790 &self.resources.glyph_atlas.view,
3791 ),
3792 },
3793 wgpu::BindGroupEntry {
3794 binding: 1,
3795 resource: wgpu::BindingResource::Sampler(sampler),
3796 },
3797 ],
3798 });
3799 self.ruler_gpu_data = Some(crate::resources::LabelGpuData {
3800 vertex_buf,
3801 vertex_count: verts.len() as u32,
3802 bind_group,
3803 });
3804 }
3805 }
3806 }
3807
3808 self.loading_bar_gpu_data = None;
3812 if !frame.overlays.loading_bars.is_empty() {
3813 self.resources.ensure_overlay_text_pipeline(device);
3814 let vp_w = frame.camera.viewport_size[0];
3815 let vp_h = frame.camera.viewport_size[1];
3816 if vp_w > 0.0 && vp_h > 0.0 {
3817 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
3818
3819 for bar in &frame.overlays.loading_bars {
3820 let bar_x = vp_w * 0.5 - bar.width_px * 0.5;
3822 let bar_y = match bar.anchor {
3823 crate::renderer::types::LoadingBarAnchor::TopCenter => bar.margin_px,
3824 crate::renderer::types::LoadingBarAnchor::Center => {
3825 vp_h * 0.5 - bar.height_px * 0.5
3826 }
3827 crate::renderer::types::LoadingBarAnchor::BottomCenter => {
3828 vp_h - bar.margin_px - bar.height_px
3829 }
3830 };
3831
3832 if let Some(ref text) = bar.label {
3834 let layout = self.resources.glyph_atlas.layout_text(
3835 text,
3836 bar.font_size,
3837 None,
3838 device,
3839 );
3840 let ascent =
3841 self.resources.glyph_atlas.font_ascent(0, bar.font_size);
3842 let label_gap = 5.0;
3843 let lx = bar_x + bar.width_px * 0.5 - layout.total_width * 0.5;
3844 let ly = match bar.anchor {
3845 crate::renderer::types::LoadingBarAnchor::TopCenter => {
3846 bar_y + bar.height_px + label_gap
3847 }
3848 _ => bar_y - layout.height - label_gap,
3849 };
3850 for gq in &layout.quads {
3851 let gx = lx + gq.pos[0];
3852 let gy = ly + ascent + gq.pos[1];
3853 emit_textured_quad(
3854 &mut verts,
3855 gx, gy,
3856 gx + gq.size[0], gy + gq.size[1],
3857 gq.uv_min, gq.uv_max,
3858 bar.label_color,
3859 vp_w, vp_h,
3860 );
3861 }
3862 }
3863
3864 emit_rounded_quad(
3866 &mut verts,
3867 bar_x, bar_y,
3868 bar_x + bar.width_px, bar_y + bar.height_px,
3869 bar.corner_radius,
3870 bar.background_color,
3871 vp_w, vp_h,
3872 );
3873
3874 let fill_w = bar.width_px * bar.progress.clamp(0.0, 1.0);
3876 if fill_w > 0.5 {
3877 emit_rounded_quad(
3878 &mut verts,
3879 bar_x, bar_y,
3880 bar_x + fill_w, bar_y + bar.height_px,
3881 bar.corner_radius,
3882 bar.fill_color,
3883 vp_w, vp_h,
3884 );
3885 }
3886 }
3887
3888 self.resources.glyph_atlas.upload_if_dirty(queue);
3889
3890 if !verts.is_empty() {
3891 let vertex_buf =
3892 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3893 label: Some("loading_bar_vbuf"),
3894 contents: bytemuck::cast_slice(&verts),
3895 usage: wgpu::BufferUsages::VERTEX,
3896 });
3897 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3898 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3899 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3900 label: Some("loading_bar_bg"),
3901 layout: bgl,
3902 entries: &[
3903 wgpu::BindGroupEntry {
3904 binding: 0,
3905 resource: wgpu::BindingResource::TextureView(
3906 &self.resources.glyph_atlas.view,
3907 ),
3908 },
3909 wgpu::BindGroupEntry {
3910 binding: 1,
3911 resource: wgpu::BindingResource::Sampler(sampler),
3912 },
3913 ],
3914 });
3915 self.loading_bar_gpu_data = Some(crate::resources::LabelGpuData {
3916 vertex_buf,
3917 vertex_count: verts.len() as u32,
3918 bind_group,
3919 });
3920 }
3921 }
3922 }
3923
3924 self.gaussian_splat_draw_data.clear();
3928 if !frame.scene.gaussian_splats.is_empty() {
3929 self.resources.ensure_gaussian_splat_pipelines(device);
3930 let vp_idx = frame.camera.viewport_index;
3931 let eye = frame.camera.render_camera.eye_position;
3932 let vp_w = frame.camera.viewport_size[0].max(1.0);
3933 let vp_h = frame.camera.viewport_size[1].max(1.0);
3934 for item in &frame.scene.gaussian_splats {
3935 let store_index = item.id.0;
3936 if self.resources.gaussian_splat_store.get(store_index).is_none() {
3937 continue;
3938 }
3939 let sh_degree = self
3940 .resources
3941 .gaussian_splat_store
3942 .get(store_index)
3943 .unwrap()
3944 .sh_degree;
3945 let count = self
3946 .resources
3947 .gaussian_splat_store
3948 .get(store_index)
3949 .unwrap()
3950 .count;
3951 self.resources.run_gaussian_splat_sort(
3952 device,
3953 queue,
3954 store_index,
3955 vp_idx,
3956 eye,
3957 item.model,
3958 vp_w,
3959 vp_h,
3960 sh_degree,
3961 );
3962 self.gaussian_splat_draw_data.push(
3963 crate::resources::GaussianSplatDrawData {
3964 store_index,
3965 viewport_index: vp_idx,
3966 model: item.model,
3967 count,
3968 },
3969 );
3970 }
3971 }
3972 }
3973
3974 pub fn prepare(
3979 &mut self,
3980 device: &wgpu::Device,
3981 queue: &wgpu::Queue,
3982 frame: &FrameData,
3983 ) -> crate::renderer::stats::FrameStats {
3984 let prepare_start = std::time::Instant::now();
3985
3986 if self.ts_needs_readback {
3990 if let Some(ref stg_buf) = self.ts_staging_buf {
3991 let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3992 stg_buf.slice(..).map_async(wgpu::MapMode::Read, move |r| {
3993 let _ = tx.send(r);
3994 });
3995 device
3998 .poll(wgpu::PollType::Wait {
3999 submission_index: None,
4000 timeout: Some(std::time::Duration::from_millis(100)),
4001 })
4002 .ok();
4003 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
4004 let data = stg_buf.slice(..).get_mapped_range();
4005 let t0 = u64::from_le_bytes(data[0..8].try_into().unwrap());
4006 let t1 = u64::from_le_bytes(data[8..16].try_into().unwrap());
4007 drop(data);
4008 let gpu_ms = t1.saturating_sub(t0) as f32 * self.ts_period / 1_000_000.0;
4010 self.last_stats.gpu_frame_ms = Some(gpu_ms);
4011 }
4012 stg_buf.unmap();
4013 }
4014 self.ts_needs_readback = false;
4015 }
4016
4017 if self.indirect_readback_pending {
4021 if let Some(ref stg_buf) = self.indirect_readback_buf {
4022 let bytes = self.indirect_readback_batch_count as u64 * 20;
4023 if bytes > 0 {
4024 let (tx, rx) =
4025 std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
4026 stg_buf.slice(..bytes).map_async(wgpu::MapMode::Read, move |r| {
4027 let _ = tx.send(r);
4028 });
4029 device
4030 .poll(wgpu::PollType::Wait {
4031 submission_index: None,
4032 timeout: Some(std::time::Duration::from_millis(100)),
4033 })
4034 .ok();
4035 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
4036 let data = stg_buf.slice(..bytes).get_mapped_range();
4037 let mut visible: u32 = 0;
4038 for i in 0..self.indirect_readback_batch_count as usize {
4039 let off = i * 20 + 4;
4042 let n = u32::from_le_bytes(data[off..off + 4].try_into().unwrap());
4043 visible = visible.saturating_add(n);
4044 }
4045 drop(data);
4046 self.last_stats.gpu_visible_instances = Some(visible);
4047 }
4048 stg_buf.unmap();
4049 }
4050 }
4051 self.indirect_readback_pending = false;
4052 }
4053
4054 let total_frame_ms = self
4056 .last_prepare_instant
4057 .map(|t| t.elapsed().as_secs_f32() * 1000.0)
4058 .unwrap_or(0.0);
4059
4060 let upload_bytes = self.resources.frame_upload_bytes;
4062 self.resources.frame_upload_bytes = 0;
4063
4064 let policy = self.performance_policy;
4068 let (eff_min_scale, eff_max_scale, eff_allow_shadows, eff_allow_volumes, eff_allow_effects) =
4069 match policy.preset {
4070 Some(crate::renderer::stats::QualityPreset::High) => {
4071 (1.0_f32, 1.0_f32, false, false, false)
4072 }
4073 Some(crate::renderer::stats::QualityPreset::Medium) => {
4074 (0.75_f32, 1.0_f32, true, false, true)
4075 }
4076 Some(crate::renderer::stats::QualityPreset::Low) => {
4077 (0.5_f32, 0.75_f32, true, true, true)
4078 }
4079 None => (
4080 policy.min_render_scale,
4081 policy.max_render_scale,
4082 policy.allow_shadow_reduction,
4083 policy.allow_volume_quality_reduction,
4084 policy.allow_effect_throttling,
4085 ),
4086 };
4087
4088 let in_capture = self.runtime_mode == crate::renderer::stats::RuntimeMode::Capture;
4091 if in_capture {
4092 self.current_render_scale = eff_max_scale;
4093 }
4094
4095 let hdr_active = frame.effects.post_process.enabled;
4101
4102 if !in_capture && !hdr_active && policy.preset.is_some() {
4107 self.current_render_scale =
4108 self.current_render_scale.clamp(eff_min_scale, eff_max_scale);
4109 }
4110
4111 let missed_prev = self.last_stats.missed_budget;
4120 let under_prev = !self.last_stats.missed_budget
4121 && policy
4122 .target_fps
4123 .map(|fps| {
4124 let budget = 1000.0 / fps;
4125 let sig = self
4126 .last_stats
4127 .gpu_frame_ms
4128 .unwrap_or(self.last_stats.total_frame_ms);
4129 sig < budget * 0.8
4130 })
4131 .unwrap_or(true);
4132 if in_capture {
4133 self.degradation_tier = 0;
4134 } else if !hdr_active {
4135 let at_min = !policy.allow_dynamic_resolution
4136 || self.current_render_scale <= eff_min_scale + 0.001;
4137 if missed_prev && at_min {
4138 self.degradation_tier = (self.degradation_tier + 1).min(3);
4139 } else if under_prev {
4140 self.degradation_tier = self.degradation_tier.saturating_sub(1);
4141 }
4142 }
4143
4144 self.degradation_shadows_skipped =
4147 !in_capture && self.degradation_tier >= 1 && eff_allow_shadows;
4148 self.degradation_volume_quality_reduced =
4149 !in_capture && self.degradation_tier >= 2 && eff_allow_volumes;
4150 self.degradation_effects_throttled =
4151 !in_capture && self.degradation_tier >= 3 && eff_allow_effects;
4152
4153 {
4155 let surfaces = match &frame.scene.surfaces {
4156 SurfaceSubmission::Flat(items) => items.as_ref(),
4157 };
4158 self.pick_scene_items = surfaces.to_vec();
4159 self.pick_point_cloud_items = frame.scene.point_clouds.clone();
4160 self.pick_splat_items = frame.scene.gaussian_splats.clone();
4161 self.pick_volume_items = frame.scene.volumes.clone();
4162 self.pick_tvm_items = frame.scene.transparent_volume_meshes.clone();
4163 self.pick_volume_mesh_items = frame.scene.volume_mesh_items.clone();
4164 self.pick_polyline_items = frame.scene.polylines.clone();
4165 self.pick_glyph_items = frame.scene.glyphs.clone();
4166 self.pick_tensor_glyph_items = frame.scene.tensor_glyphs.clone();
4167 self.pick_sprite_items = frame.scene.sprite_items.clone();
4168 self.pick_streamtube_items = frame.scene.streamtube_items.clone();
4169 self.pick_tube_items = frame.scene.tube_items.clone();
4170 self.pick_ribbon_items = frame.scene.ribbon_items.clone();
4171 }
4172
4173 let (scene_fx, viewport_fx) = frame.effects.split();
4174 self.prepare_scene_internal(device, queue, frame, &scene_fx);
4175 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
4176
4177 let cpu_prepare_ms = prepare_start.elapsed().as_secs_f32() * 1000.0;
4178
4179 let budget_ms = policy.target_fps.map(|fps| 1000.0 / fps);
4180
4181 let controller_ms = self
4187 .last_stats
4188 .gpu_frame_ms
4189 .unwrap_or(total_frame_ms);
4190
4191 let missed_budget = !in_capture
4193 && budget_ms.map(|b| controller_ms > b).unwrap_or(false);
4194
4195 if policy.allow_dynamic_resolution && !in_capture && !hdr_active {
4200 if let Some(budget) = budget_ms {
4201 if controller_ms > budget {
4202 self.current_render_scale =
4204 (self.current_render_scale - 0.1).max(eff_min_scale);
4205 } else if controller_ms < budget * 0.8 {
4206 self.current_render_scale =
4208 (self.current_render_scale + 0.05).min(eff_max_scale);
4209 }
4210 }
4211 }
4212
4213 self.last_prepare_instant = Some(prepare_start);
4214 self.frame_counter = self.frame_counter.wrapping_add(1);
4215
4216 let reported_render_scale = if hdr_active { 1.0 } else { self.current_render_scale };
4219
4220 let stats = crate::renderer::stats::FrameStats {
4221 cpu_prepare_ms,
4222 gpu_frame_ms: self.last_stats.gpu_frame_ms,
4225 total_frame_ms,
4226 render_scale: reported_render_scale,
4227 missed_budget,
4228 upload_bytes,
4229 shadows_skipped: self.degradation_shadows_skipped,
4230 volume_quality_reduced: self.degradation_volume_quality_reduced,
4231 effects_throttled: self.degradation_effects_throttled,
4235 ..self.last_stats
4236 };
4237 self.last_stats = stats;
4238 stats
4239 }
4240}
4241
4242fn clip_box_outline(
4248 center: [f32; 3],
4249 half: [f32; 3],
4250 orientation: [[f32; 3]; 3],
4251 color: [f32; 4],
4252) -> PolylineItem {
4253 let ax = glam::Vec3::from(orientation[0]) * half[0];
4254 let ay = glam::Vec3::from(orientation[1]) * half[1];
4255 let az = glam::Vec3::from(orientation[2]) * half[2];
4256 let c = glam::Vec3::from(center);
4257
4258 let corners = [
4259 c - ax - ay - az,
4260 c + ax - ay - az,
4261 c + ax + ay - az,
4262 c - ax + ay - az,
4263 c - ax - ay + az,
4264 c + ax - ay + az,
4265 c + ax + ay + az,
4266 c - ax + ay + az,
4267 ];
4268 let edges: [(usize, usize); 12] = [
4269 (0, 1),
4270 (1, 2),
4271 (2, 3),
4272 (3, 0), (4, 5),
4274 (5, 6),
4275 (6, 7),
4276 (7, 4), (0, 4),
4278 (1, 5),
4279 (2, 6),
4280 (3, 7), ];
4282
4283 let mut positions = Vec::with_capacity(24);
4284 let mut strip_lengths = Vec::with_capacity(12);
4285 for (a, b) in edges {
4286 positions.push(corners[a].to_array());
4287 positions.push(corners[b].to_array());
4288 strip_lengths.push(2u32);
4289 }
4290
4291 let mut item = PolylineItem::default();
4292 item.positions = positions;
4293 item.strip_lengths = strip_lengths;
4294 item.default_color = color;
4295 item.line_width = 2.0;
4296 item
4297}
4298
4299fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
4301 let c = glam::Vec3::from(center);
4302 let segs = 64usize;
4303 let mut positions = Vec::with_capacity((segs + 1) * 3);
4304 let mut strip_lengths = Vec::with_capacity(3);
4305
4306 for axis in 0..3usize {
4307 let start = positions.len();
4308 for i in 0..=segs {
4309 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
4310 let (s, cs) = t.sin_cos();
4311 let p = c + match axis {
4312 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
4313 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
4314 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
4315 };
4316 positions.push(p.to_array());
4317 }
4318 strip_lengths.push((positions.len() - start) as u32);
4319 }
4320
4321 let mut item = PolylineItem::default();
4322 item.positions = positions;
4323 item.strip_lengths = strip_lengths;
4324 item.default_color = color;
4325 item.line_width = 2.0;
4326 item
4327}
4328
4329fn clip_cylinder_outline(
4331 center: [f32; 3],
4332 axis: [f32; 3],
4333 radius: f32,
4334 half_length: f32,
4335 color: [f32; 4],
4336) -> PolylineItem {
4337 let c = glam::Vec3::from(center);
4338 let ax = glam::Vec3::from(axis).normalize();
4339
4340 let ref_v = if ax.y.abs() < 0.99 {
4342 glam::Vec3::Y
4343 } else {
4344 glam::Vec3::X
4345 };
4346 let perp_u = ref_v.cross(ax).normalize();
4347 let perp_v = ax.cross(perp_u);
4348
4349 let segs = 32usize;
4350 let long_lines = 8usize;
4351 let cap_verts = segs + 1;
4352 let total_cap = cap_verts * 2 + long_lines * 2;
4353 let mut positions = Vec::with_capacity(total_cap);
4354 let mut strip_lengths = Vec::with_capacity(2 + long_lines);
4355
4356 for sign in [-1.0f32, 1.0] {
4358 let cap_center = c + ax * (sign * half_length);
4359 let start = positions.len();
4360 for i in 0..=segs {
4361 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
4362 let (s, cs) = t.sin_cos();
4363 let p = cap_center + perp_u * (cs * radius) + perp_v * (s * radius);
4364 positions.push(p.to_array());
4365 }
4366 strip_lengths.push((positions.len() - start) as u32);
4367 }
4368
4369 for i in 0..long_lines {
4371 let t = i as f32 / long_lines as f32 * std::f32::consts::TAU;
4372 let (s, cs) = t.sin_cos();
4373 let offset = perp_u * (cs * radius) + perp_v * (s * radius);
4374 positions.push((c + ax * (-half_length) + offset).to_array());
4375 positions.push((c + ax * half_length + offset).to_array());
4376 strip_lengths.push(2);
4377 }
4378
4379 let mut item = PolylineItem::default();
4380 item.positions = positions;
4381 item.strip_lengths = strip_lengths;
4382 item.default_color = color;
4383 item.line_width = 2.0;
4384 item
4385}
4386
4387fn project_to_ndc(
4395 pos: [f32; 3],
4396 view: &glam::Mat4,
4397 proj: &glam::Mat4,
4398) -> Option<[f32; 2]> {
4399 let clip = *proj * *view * glam::Vec3::from(pos).extend(1.0);
4400 if clip.w <= 0.0 { return None; }
4401 Some([clip.x / clip.w, clip.y / clip.w])
4402}
4403
4404fn ndc_to_screen_px(ndc: [f32; 2], vp_w: f32, vp_h: f32) -> [f32; 2] {
4406 [
4407 (ndc[0] * 0.5 + 0.5) * vp_w,
4408 (1.0 - (ndc[1] * 0.5 + 0.5)) * vp_h,
4409 ]
4410}
4411
4412fn ndc_in_viewport(ndc: [f32; 2]) -> bool {
4414 ndc[0] >= -1.0 && ndc[0] <= 1.0 && ndc[1] >= -1.0 && ndc[1] <= 1.0
4415}
4416
4417fn clip_line_ndc(a: [f32; 2], b: [f32; 2]) -> Option<([f32; 2], [f32; 2])> {
4421 let dx = b[0] - a[0];
4422 let dy = b[1] - a[1];
4423 let mut t0 = 0.0f32;
4424 let mut t1 = 1.0f32;
4425
4426 for (p, q) in [
4428 (-dx, a[0] + 1.0),
4429 ( dx, 1.0 - a[0]),
4430 (-dy, a[1] + 1.0),
4431 ( dy, 1.0 - a[1]),
4432 ] {
4433 if p == 0.0 {
4434 if q < 0.0 { return None; }
4435 } else {
4436 let r = q / p;
4437 if p < 0.0 { t0 = t0.max(r); } else { t1 = t1.min(r); }
4438 }
4439 }
4440
4441 if t0 > t1 { return None; }
4442 Some((
4443 [a[0] + t0 * dx, a[1] + t0 * dy],
4444 [a[0] + t1 * dx, a[1] + t1 * dy],
4445 ))
4446}
4447
4448fn project_to_screen(
4451 pos: [f32; 3],
4452 view: &glam::Mat4,
4453 proj: &glam::Mat4,
4454 vp_w: f32,
4455 vp_h: f32,
4456) -> Option<[f32; 2]> {
4457 let p = glam::Vec3::from(pos);
4458 let clip = *proj * *view * p.extend(1.0);
4459 if clip.w <= 0.0 {
4460 return None;
4461 }
4462 let ndc_x = clip.x / clip.w;
4463 let ndc_y = clip.y / clip.w;
4464 if ndc_x < -1.0 || ndc_x > 1.0 || ndc_y < -1.0 || ndc_y > 1.0 {
4465 return None;
4466 }
4467 let x = (ndc_x * 0.5 + 0.5) * vp_w;
4468 let y = (1.0 - (ndc_y * 0.5 + 0.5)) * vp_h;
4469 Some([x, y])
4470}
4471
4472#[inline]
4474fn px_to_ndc(px_x: f32, px_y: f32, vp_w: f32, vp_h: f32) -> [f32; 2] {
4475 [
4476 px_x / vp_w * 2.0 - 1.0,
4477 1.0 - px_y / vp_h * 2.0,
4478 ]
4479}
4480
4481fn emit_solid_quad(
4483 verts: &mut Vec<crate::resources::OverlayTextVertex>,
4484 x0: f32, y0: f32,
4485 x1: f32, y1: f32,
4486 color: [f32; 4],
4487 vp_w: f32, vp_h: f32,
4488) {
4489 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
4490 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
4491 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
4492 let br = px_to_ndc(x1, y1, vp_w, vp_h);
4493 let uv = [0.0, 0.0];
4494 let tex = 0.0;
4495 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
4496 position: pos, uv, color, use_texture: tex, _pad: 0.0,
4497 };
4498 verts.extend_from_slice(&[v(tl), v(bl), v(tr), v(tr), v(bl), v(br)]);
4499}
4500
4501fn emit_textured_quad(
4503 verts: &mut Vec<crate::resources::OverlayTextVertex>,
4504 x0: f32, y0: f32,
4505 x1: f32, y1: f32,
4506 uv_min: [f32; 2],
4507 uv_max: [f32; 2],
4508 color: [f32; 4],
4509 vp_w: f32, vp_h: f32,
4510) {
4511 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
4512 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
4513 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
4514 let br = px_to_ndc(x1, y1, vp_w, vp_h);
4515 let tex = 1.0;
4516 let v = |pos: [f32; 2], uv: [f32; 2]| crate::resources::OverlayTextVertex {
4517 position: pos, uv, color, use_texture: tex, _pad: 0.0,
4518 };
4519 verts.extend_from_slice(&[
4521 v(tl, uv_min),
4522 v(bl, [uv_min[0], uv_max[1]]),
4523 v(tr, [uv_max[0], uv_min[1]]),
4524 v(tr, [uv_max[0], uv_min[1]]),
4525 v(bl, [uv_min[0], uv_max[1]]),
4526 v(br, uv_max),
4527 ]);
4528}
4529
4530fn emit_line_quad(
4532 verts: &mut Vec<crate::resources::OverlayTextVertex>,
4533 x0: f32, y0: f32,
4534 x1: f32, y1: f32,
4535 thickness: f32,
4536 color: [f32; 4],
4537 vp_w: f32, vp_h: f32,
4538) {
4539 let dx = x1 - x0;
4540 let dy = y1 - y0;
4541 let len = (dx * dx + dy * dy).sqrt();
4542 if len < 0.001 {
4543 return;
4544 }
4545 let half = thickness * 0.5;
4546 let nx = -dy / len * half;
4547 let ny = dx / len * half;
4548
4549 let p0 = px_to_ndc(x0 + nx, y0 + ny, vp_w, vp_h);
4550 let p1 = px_to_ndc(x0 - nx, y0 - ny, vp_w, vp_h);
4551 let p2 = px_to_ndc(x1 + nx, y1 + ny, vp_w, vp_h);
4552 let p3 = px_to_ndc(x1 - nx, y1 - ny, vp_w, vp_h);
4553 let uv = [0.0, 0.0];
4554 let tex = 0.0;
4555 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
4556 position: pos, uv, color, use_texture: tex, _pad: 0.0,
4557 };
4558 verts.extend_from_slice(&[v(p0), v(p1), v(p2), v(p2), v(p1), v(p3)]);
4559}
4560
4561#[inline]
4563fn apply_opacity(color: [f32; 4], opacity: f32) -> [f32; 4] {
4564 [color[0], color[1], color[2], color[3] * opacity]
4565}
4566
4567fn emit_rounded_quad(
4571 verts: &mut Vec<crate::resources::OverlayTextVertex>,
4572 x0: f32, y0: f32,
4573 x1: f32, y1: f32,
4574 radius: f32,
4575 color: [f32; 4],
4576 vp_w: f32, vp_h: f32,
4577) {
4578 let w = x1 - x0;
4579 let h = y1 - y0;
4580 let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
4581
4582 if r < 0.5 {
4583 emit_solid_quad(verts, x0, y0, x1, y1, color, vp_w, vp_h);
4584 return;
4585 }
4586
4587 emit_solid_quad(verts, x0, y0 + r, x1, y1 - r, color, vp_w, vp_h);
4590 emit_solid_quad(verts, x0 + r, y0, x1 - r, y0 + r, color, vp_w, vp_h);
4592 emit_solid_quad(verts, x0 + r, y1 - r, x1 - r, y1, color, vp_w, vp_h);
4594
4595 let corners = [
4597 (x0 + r, y0 + r, std::f32::consts::PI, std::f32::consts::FRAC_PI_2 * 3.0), (x1 - r, y0 + r, std::f32::consts::FRAC_PI_2 * 3.0, std::f32::consts::TAU), (x1 - r, y1 - r, 0.0, std::f32::consts::FRAC_PI_2), (x0 + r, y1 - r, std::f32::consts::FRAC_PI_2, std::f32::consts::PI), ];
4602 let segments = 6;
4603 let uv = [0.0, 0.0];
4604 let tex = 0.0;
4605 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
4606 position: pos, uv, color, use_texture: tex, _pad: 0.0,
4607 };
4608 for (cx, cy, start, end) in corners {
4609 let center = px_to_ndc(cx, cy, vp_w, vp_h);
4610 for i in 0..segments {
4611 let a0 = start + (end - start) * i as f32 / segments as f32;
4612 let a1 = start + (end - start) * (i + 1) as f32 / segments as f32;
4613 let p0 = px_to_ndc(cx + a0.cos() * r, cy + a0.sin() * r, vp_w, vp_h);
4614 let p1 = px_to_ndc(cx + a1.cos() * r, cy + a1.sin() * r, vp_w, vp_h);
4615 verts.extend_from_slice(&[v(center), v(p0), v(p1)]);
4616 }
4617 }
4618}
4619
4620fn format_ruler_distance(distance: f32, fmt: Option<&str>) -> String {
4631 let pattern = fmt.unwrap_or("{:.3}");
4632 if let Some(open) = pattern.find('{') {
4634 if let Some(close_rel) = pattern[open..].find('}') {
4635 let close = open + close_rel;
4636 let spec = &pattern[open + 1..close]; let prefix = &pattern[..open];
4638 let suffix = &pattern[close + 1..];
4639 let formatted = if let Some(prec_str) = spec.strip_prefix(":.") {
4640 let prec_str = prec_str.trim_end_matches('f');
4642 if let Ok(prec) = prec_str.parse::<usize>() {
4643 format!("{distance:.prec$}")
4644 } else {
4645 format!("{distance:.3}")
4646 }
4647 } else if spec.is_empty() || spec == ":" {
4648 format!("{distance}")
4649 } else {
4650 format!("{distance:.3}")
4651 };
4652 return format!("{prefix}{formatted}{suffix}");
4653 }
4654 }
4655 format!("{distance:.3}")
4656}