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 if !self.use_instancing
398 || frame.viewport.wireframe_mode
399 || has_scalar_items
400 || has_two_sided_items
401 || has_matcap_items
402 || has_param_vis_items
403 || has_wireframe_items
404 {
405 for item in scene_items {
406 if self.use_instancing
411 && !frame.viewport.wireframe_mode
412 && item.active_attribute.is_none()
413 && !item.material.is_two_sided()
414 && item.material.matcap_id.is_none()
415 && item.material.param_vis.is_none()
416 && !item.render_as_wireframe
417 && item.warp_attribute.is_none()
418 {
419 continue;
420 }
421
422 if resources
423 .mesh_store
424 .get(item.mesh_id)
425 .is_none()
426 {
427 tracing::warn!(
428 mesh_index = item.mesh_id.index(),
429 "scene item mesh_index invalid, skipping"
430 );
431 continue;
432 };
433 let m = &item.material;
434 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
436 let range = item
437 .scalar_range
438 .or_else(|| {
439 resources
440 .mesh_store
441 .get(item.mesh_id)
442 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
443 })
444 .unwrap_or((0.0, 1.0));
445 (1u32, range.0, range.1)
446 } else {
447 (0u32, 0.0, 1.0)
448 };
449 let obj_uniform = ObjectUniform {
450 model: item.model,
451 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
452 selected: if item.selected { 1 } else { 0 },
453 wireframe: if frame.viewport.wireframe_mode || item.render_as_wireframe { 1 } else { 0 },
454 ambient: m.ambient,
455 diffuse: m.diffuse,
456 specular: m.specular,
457 shininess: m.shininess,
458 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
459 use_pbr: if m.use_pbr { 1 } else { 0 },
460 metallic: m.metallic,
461 roughness: m.roughness,
462 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
463 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
464 has_attribute: has_attr,
465 scalar_min: s_min,
466 scalar_max: s_max,
467 _pad_scalar: 0,
468 nan_color: item.nan_color.unwrap_or([0.0; 4]),
469 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
470 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
471 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
472 unlit: if m.unlit { 1 } else { 0 },
473 use_face_color: u32::from(item.active_attribute.as_ref().map_or(false, |a| {
474 a.kind == crate::resources::AttributeKind::FaceColor
475 })),
476 uv_vis_mode: m.param_vis.map_or(0, |pv| pv.mode as u32),
477 uv_vis_scale: m.param_vis.map_or(8.0, |pv| pv.scale),
478 backface_policy: match m.backface_policy {
479 crate::scene::material::BackfacePolicy::Cull => 0,
480 crate::scene::material::BackfacePolicy::Identical => 1,
481 crate::scene::material::BackfacePolicy::DifferentColor(_) => 2,
482 crate::scene::material::BackfacePolicy::Tint(_) => 3,
483 crate::scene::material::BackfacePolicy::Pattern(cfg) => {
484 4 + cfg.pattern as u32
485 }
486 },
487 backface_color: match m.backface_policy {
488 crate::scene::material::BackfacePolicy::DifferentColor(c) => {
489 [c[0], c[1], c[2], 1.0]
490 }
491 crate::scene::material::BackfacePolicy::Tint(factor) => {
492 [factor, 0.0, 0.0, 1.0]
493 }
494 crate::scene::material::BackfacePolicy::Pattern(cfg) => {
495 let world_extent = resources
496 .mesh_store
497 .get(item.mesh_id)
498 .map(|mesh| {
499 mesh.aabb
500 .transformed(&glam::Mat4::from_cols_array_2d(&item.model))
501 .longest_side()
502 })
503 .unwrap_or(1.0)
504 .max(1e-6);
505 let world_scale = cfg.scale / world_extent;
506 [cfg.color[0], cfg.color[1], cfg.color[2], world_scale]
507 }
508 _ => [0.0; 4],
509 },
510 has_warp: if item.warp_attribute.is_some() { 1 } else { 0 },
511 warp_scale: item.warp_scale,
512 _pad_warp: [0; 2],
513 };
514
515 let normal_obj_uniform = ObjectUniform {
516 model: item.model,
517 color: [1.0, 1.0, 1.0, 1.0],
518 selected: 0,
519 wireframe: 0,
520 ambient: 0.15,
521 diffuse: 0.75,
522 specular: 0.4,
523 shininess: 32.0,
524 has_texture: 0,
525 use_pbr: 0,
526 metallic: 0.0,
527 roughness: 0.5,
528 has_normal_map: 0,
529 has_ao_map: 0,
530 has_attribute: 0,
531 scalar_min: 0.0,
532 scalar_max: 1.0,
533 _pad_scalar: 0,
534 nan_color: [0.0; 4],
535 use_nan_color: 0,
536 use_matcap: 0,
537 matcap_blendable: 0,
538 unlit: 0,
539 use_face_color: 0,
540 uv_vis_mode: 0,
541 uv_vis_scale: 8.0,
542 backface_policy: 0,
543 backface_color: [0.0; 4],
544 has_warp: 0,
545 warp_scale: 1.0,
546 _pad_warp: [0; 2],
547 };
548
549 {
551 let mesh = resources
552 .mesh_store
553 .get(item.mesh_id)
554 .unwrap();
555 queue.write_buffer(
556 &mesh.object_uniform_buf,
557 0,
558 bytemuck::cast_slice(&[obj_uniform]),
559 );
560 queue.write_buffer(
561 &mesh.normal_uniform_buf,
562 0,
563 bytemuck::cast_slice(&[normal_obj_uniform]),
564 );
565 } resources.update_mesh_texture_bind_group(
569 device,
570 item.mesh_id,
571 item.material.texture_id,
572 item.material.normal_map_id,
573 item.material.ao_map_id,
574 item.colormap_id,
575 item.active_attribute.as_ref().map(|a| a.name.as_str()),
576 item.material.matcap_id,
577 item.warp_attribute.as_deref(),
578 );
579 }
580 }
581
582 if self.use_instancing {
583 resources.ensure_instanced_pipelines(device);
584
585 let instancable_count = scene_items.iter().filter(|item| {
599 item.visible
600 && item.active_attribute.is_none()
601 && !item.material.is_two_sided()
602 && item.material.matcap_id.is_none()
603 && item.material.param_vis.is_none()
604 && resources.mesh_store.get(item.mesh_id).is_some()
605 }).count();
606 let cache_valid = instancable_count == self.last_instancable_count
607 && frame.scene.generation == self.last_scene_generation
608 && frame.interaction.selection_generation == self.last_selection_generation
609 && scene_items.len() == self.last_scene_items_count;
610
611 if !cache_valid {
612 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
614 .iter()
615 .filter(|item| {
616 item.visible
617 && item.active_attribute.is_none()
618 && !item.material.is_two_sided()
619 && item.material.matcap_id.is_none()
620 && item.material.param_vis.is_none()
621 && resources
622 .mesh_store
623 .get(item.mesh_id)
624 .is_some()
625 })
626 .collect();
627
628 sorted_items.sort_unstable_by_key(|item| {
629 (
630 item.mesh_id.index(),
631 item.material.texture_id,
632 item.material.normal_map_id,
633 item.material.ao_map_id,
634 )
635 });
636
637 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
638 let mut all_aabbs: Vec<InstanceAabb> = Vec::with_capacity(sorted_items.len());
639 let mut batch_metas: Vec<BatchMeta> = Vec::new();
640 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
641
642 if !sorted_items.is_empty() {
643 let mut batch_start = 0usize;
644 for i in 1..=sorted_items.len() {
645 let at_end = i == sorted_items.len();
646 let key_changed = !at_end && {
647 let a = sorted_items[batch_start];
648 let b = sorted_items[i];
649 a.mesh_id != b.mesh_id
650 || a.material.texture_id != b.material.texture_id
651 || a.material.normal_map_id != b.material.normal_map_id
652 || a.material.ao_map_id != b.material.ao_map_id
653 };
654
655 if at_end || key_changed {
656 let batch_items = &sorted_items[batch_start..i];
657 let rep = batch_items[0];
658 let instance_offset = all_instances.len() as u32;
659 let is_transparent = rep.material.opacity < 1.0;
660
661 for item in batch_items {
662 let m = &item.material;
663 all_instances.push(InstanceData {
664 model: item.model,
665 color: [
666 m.base_color[0],
667 m.base_color[1],
668 m.base_color[2],
669 m.opacity,
670 ],
671 selected: if item.selected { 1 } else { 0 },
672 wireframe: 0, ambient: m.ambient,
674 diffuse: m.diffuse,
675 specular: m.specular,
676 shininess: m.shininess,
677 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
678 use_pbr: if m.use_pbr { 1 } else { 0 },
679 metallic: m.metallic,
680 roughness: m.roughness,
681 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
682 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
683 unlit: if m.unlit { 1 } else { 0 },
684 _pad_inst: [0; 3],
685 });
686 }
687
688 let batch_idx = instanced_batches.len() as u32;
692 let mesh_index_count = resources
693 .mesh_store
694 .get(rep.mesh_id)
695 .map(|m| m.index_count)
696 .unwrap_or(0);
697 for item in batch_items {
698 if let Some(mesh) = resources.mesh_store.get(item.mesh_id) {
699 let model =
700 glam::Mat4::from_cols_array_2d(&item.model);
701 let world_aabb = mesh.aabb.transformed(&model);
702 all_aabbs.push(InstanceAabb {
703 min: world_aabb.min.into(),
704 batch_index: batch_idx,
705 max: world_aabb.max.into(),
706 _pad: 0,
707 });
708 }
709 }
710
711 batch_metas.push(BatchMeta {
715 index_count: mesh_index_count,
716 first_index: 0,
717 instance_offset,
718 instance_count: batch_items.len() as u32,
719 vis_offset: instance_offset,
720 is_transparent: if is_transparent { 1 } else { 0 },
721 _pad: [0, 0],
722 });
723
724 instanced_batches.push(InstancedBatch {
725 mesh_id: rep.mesh_id,
726 texture_id: rep.material.texture_id,
727 normal_map_id: rep.material.normal_map_id,
728 ao_map_id: rep.material.ao_map_id,
729 instance_offset,
730 instance_count: batch_items.len() as u32,
731 is_transparent,
732 });
733
734 batch_start = i;
735 }
736 }
737 }
738
739 self.cached_instance_data = all_instances;
740 self.cached_instanced_batches = instanced_batches;
741
742 resources.upload_instance_data(device, queue, &self.cached_instance_data);
743 resources.upload_aabb_and_batch_meta(device, queue, &all_aabbs, &batch_metas);
744
745 self.instanced_batches = self.cached_instanced_batches.clone();
746
747 self.last_scene_generation = frame.scene.generation;
748 self.last_selection_generation = frame.interaction.selection_generation;
749 self.last_scene_items_count = scene_items.len();
750 self.last_instancable_count = sorted_items.len();
751
752 for batch in &self.instanced_batches {
753 resources.get_instance_bind_group(
754 device,
755 batch.texture_id,
756 batch.normal_map_id,
757 batch.ao_map_id,
758 );
759 }
760 } else {
761 for batch in &self.instanced_batches {
762 resources.get_instance_bind_group(
763 device,
764 batch.texture_id,
765 batch.normal_map_id,
766 batch.ao_map_id,
767 );
768 }
769 }
770
771 if self.gpu_culling_enabled
778 && !self.instanced_batches.is_empty()
779 && !self.cached_instance_data.is_empty()
780 {
781 let instance_count = self.cached_instance_data.len() as u32;
782 let batch_count = self.instanced_batches.len() as u32;
783
784 if self.cull_resources.is_none() {
786 self.cull_resources =
787 Some(crate::renderer::indirect::CullResources::new(device));
788 }
789 resources.ensure_cull_instance_pipelines(device);
790 for batch in &self.instanced_batches.clone() {
791 resources.get_instance_cull_bind_group(
792 device,
793 batch.texture_id,
794 batch.normal_map_id,
795 batch.ao_map_id,
796 );
797 }
798
799 if let (
801 Some(aabb_buf),
802 Some(meta_buf),
803 Some(counter_buf),
804 Some(vis_buf),
805 Some(indirect_buf),
806 ) = (
807 resources.instance_aabb_buf.as_ref(),
808 resources.batch_meta_buf.as_ref(),
809 resources.batch_counter_buf.as_ref(),
810 resources.visibility_index_buf.as_ref(),
811 resources.indirect_args_buf.as_ref(),
812 ) {
813 let vp_mat = frame.camera.render_camera.view_proj();
815 let cpu_frustum =
816 crate::camera::frustum::Frustum::from_view_proj(&vp_mat);
817 let frustum_uniform = crate::resources::FrustumUniform {
818 planes: std::array::from_fn(|i| crate::resources::FrustumPlane {
819 normal: cpu_frustum.planes[i].normal.into(),
820 distance: cpu_frustum.planes[i].d,
821 }),
822 instance_count,
823 batch_count,
824 _pad: [0; 2],
825 };
826
827 let cull = self.cull_resources.as_ref().unwrap();
828 let mut encoder =
829 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
830 label: Some("cull_encoder"),
831 });
832 cull.dispatch(
833 &mut encoder,
834 device,
835 queue,
836 &frustum_uniform,
837 aabb_buf,
838 meta_buf,
839 counter_buf,
840 vis_buf,
841 indirect_buf,
842 instance_count,
843 batch_count,
844 );
845
846 let indirect_bytes = batch_count as u64 * 20;
849 if self
850 .indirect_readback_buf
851 .as_ref()
852 .map_or(0, |b| b.size())
853 < indirect_bytes
854 {
855 self.indirect_readback_buf =
856 Some(device.create_buffer(&wgpu::BufferDescriptor {
857 label: Some("indirect_readback_buf"),
858 size: indirect_bytes,
859 usage: wgpu::BufferUsages::COPY_DST
860 | wgpu::BufferUsages::MAP_READ,
861 mapped_at_creation: false,
862 }));
863 }
864 if let Some(ref rb_buf) = self.indirect_readback_buf {
865 encoder.copy_buffer_to_buffer(
866 indirect_buf,
867 0,
868 rb_buf,
869 0,
870 indirect_bytes,
871 );
872 }
873 queue.submit(std::iter::once(encoder.finish()));
874 self.indirect_readback_batch_count = batch_count;
875 self.indirect_readback_pending = true;
876 }
877 }
878 }
879
880 self.point_cloud_gpu_data.clear();
884 if !frame.scene.point_clouds.is_empty() {
885 resources.ensure_point_cloud_pipeline(device);
886 for item in &frame.scene.point_clouds {
887 if item.positions.is_empty() {
888 continue;
889 }
890 let gpu_data = resources.upload_point_cloud(device, queue, item);
891 self.point_cloud_gpu_data.push(gpu_data);
892 }
893 }
894
895 self.glyph_gpu_data.clear();
896 if !frame.scene.glyphs.is_empty() {
897 resources.ensure_glyph_pipeline(device);
898 for item in &frame.scene.glyphs {
899 if item.positions.is_empty() || item.vectors.is_empty() {
900 continue;
901 }
902 let gpu_data = resources.upload_glyph_set(device, queue, item);
903 self.glyph_gpu_data.push(gpu_data);
904 }
905 }
906
907 self.tensor_glyph_gpu_data.clear();
911 if !frame.scene.tensor_glyphs.is_empty() {
912 resources.ensure_tensor_glyph_pipeline(device);
913 for item in &frame.scene.tensor_glyphs {
914 if item.positions.is_empty() {
915 continue;
916 }
917 let gd = resources.upload_tensor_glyph_set(device, queue, item);
918 self.tensor_glyph_gpu_data.push(gd);
919 }
920 }
921
922 self.polyline_gpu_data.clear();
926 let vp_size = frame.camera.viewport_size;
927 if !frame.scene.polylines.is_empty() {
928 resources.ensure_polyline_pipeline(device);
929 for item in &frame.scene.polylines {
930 if item.positions.is_empty() {
931 continue;
932 }
933 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
934 self.polyline_gpu_data.push(gpu_data);
935
936 if !item.node_vectors.is_empty() {
938 resources.ensure_glyph_pipeline(device);
939 let g = crate::quantities::polyline_node_vectors_to_glyphs(item);
940 if !g.positions.is_empty() {
941 let gd = resources.upload_glyph_set(device, queue, &g);
942 self.glyph_gpu_data.push(gd);
943 }
944 }
945 if !item.edge_vectors.is_empty() {
946 resources.ensure_glyph_pipeline(device);
947 let g = crate::quantities::polyline_edge_vectors_to_glyphs(item);
948 if !g.positions.is_empty() {
949 let gd = resources.upload_glyph_set(device, queue, &g);
950 self.glyph_gpu_data.push(gd);
951 }
952 }
953 }
954 }
955
956 if !frame.scene.isolines.is_empty() {
960 resources.ensure_polyline_pipeline(device);
961 for item in &frame.scene.isolines {
962 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
963 continue;
964 }
965 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
966 if positions.is_empty() {
967 continue;
968 }
969 let polyline = PolylineItem {
970 positions,
971 scalars: Vec::new(),
972 strip_lengths,
973 scalar_range: None,
974 colormap_id: None,
975 default_color: item.color,
976 line_width: item.line_width,
977 id: 0,
978 ..Default::default()
979 };
980 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
981 self.polyline_gpu_data.push(gpu_data);
982 }
983 }
984
985 if !frame.scene.camera_frustums.is_empty() {
989 resources.ensure_polyline_pipeline(device);
990 for item in &frame.scene.camera_frustums {
991 let polyline = item.to_polyline();
992 if !polyline.positions.is_empty() {
993 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
994 self.polyline_gpu_data.push(gpu_data);
995 }
996 }
997 }
998
999 self.implicit_gpu_data.clear();
1003 if !frame.scene.gpu_implicit.is_empty() {
1004 resources.ensure_implicit_pipeline(device);
1005 for item in &frame.scene.gpu_implicit {
1006 if item.primitives.is_empty() {
1007 continue;
1008 }
1009 let gpu = resources.upload_implicit_item(device, item);
1010 self.implicit_gpu_data.push(gpu);
1011 }
1012 }
1013
1014 self.mc_gpu_data.clear();
1018 if !frame.scene.gpu_mc_jobs.is_empty() {
1019 resources.ensure_mc_pipelines(device);
1020 self.mc_gpu_data =
1021 resources.run_mc_jobs(device, queue, &frame.scene.gpu_mc_jobs);
1022 }
1023
1024 self.screen_image_gpu_data.clear();
1028 if !frame.scene.screen_images.is_empty() {
1029 resources.ensure_screen_image_pipeline(device);
1030 if frame.scene.screen_images.iter().any(|i| i.depth.is_some()) {
1032 resources.ensure_screen_image_dc_pipeline(device);
1033 }
1034 let vp_w = vp_size[0];
1035 let vp_h = vp_size[1];
1036 for item in &frame.scene.screen_images {
1037 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
1038 continue;
1039 }
1040 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
1041 self.screen_image_gpu_data.push(gpu);
1042 }
1043 }
1044
1045 self.overlay_image_gpu_data.clear();
1049 if !frame.overlays.images.is_empty() {
1050 resources.ensure_screen_image_pipeline(device);
1051 let vp_w = vp_size[0];
1052 let vp_h = vp_size[1];
1053 for item in &frame.overlays.images {
1054 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
1055 continue;
1056 }
1057 let gpu = resources.upload_overlay_image(device, queue, item, vp_w, vp_h);
1058 self.overlay_image_gpu_data.push(gpu);
1059 }
1060 }
1061
1062 self.streamtube_gpu_data.clear();
1066 if !frame.scene.streamtube_items.is_empty() {
1067 resources.ensure_streamtube_pipeline(device);
1068 for item in &frame.scene.streamtube_items {
1069 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1070 continue;
1071 }
1072 let gpu_data = resources.upload_streamtube(device, queue, item);
1073 if gpu_data.index_count > 0 {
1074 self.streamtube_gpu_data.push(gpu_data);
1075 }
1076 }
1077 }
1078
1079 self.tube_gpu_data.clear();
1083 if !frame.scene.tube_items.is_empty() {
1084 resources.ensure_streamtube_pipeline(device);
1085 for item in &frame.scene.tube_items {
1086 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1087 continue;
1088 }
1089 let gpu_data = resources.upload_tube(device, queue, item);
1090 if gpu_data.index_count > 0 {
1091 self.tube_gpu_data.push(gpu_data);
1092 }
1093 }
1094 }
1095
1096 self.ribbon_gpu_data.clear();
1100 if !frame.scene.ribbon_items.is_empty() {
1101 resources.ensure_streamtube_pipeline(device);
1102 for item in &frame.scene.ribbon_items {
1103 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1104 continue;
1105 }
1106 let gpu_data = resources.upload_ribbon(device, queue, item);
1107 if gpu_data.index_count > 0 {
1108 self.ribbon_gpu_data.push(gpu_data);
1109 }
1110 }
1111 }
1112
1113 self.image_slice_gpu_data.clear();
1117 if !frame.scene.image_slices.is_empty() {
1118 resources.ensure_image_slice_pipeline(device);
1119 for item in &frame.scene.image_slices {
1120 if let Some(gpu_data) = resources.upload_image_slice(device, queue, item) {
1121 self.image_slice_gpu_data.push(gpu_data);
1122 }
1123 }
1124 }
1125
1126 self.lic_gpu_data.clear();
1130 if !frame.scene.lic_items.is_empty() {
1131 for item in &frame.scene.lic_items {
1134 if item.vector_attribute.is_empty() {
1135 continue;
1136 }
1137 if let Some(mesh) = resources.mesh_store.get(item.mesh_id) {
1138 if mesh.vector_attribute_buffers.contains_key(&item.vector_attribute) {
1140 if let Some(bgl) = &resources.lic_surface_bgl {
1141 use crate::resources::LicObjectUniform;
1142 let model = item.model;
1143 let obj_data = LicObjectUniform { model };
1144 let obj_buf = device.create_buffer(&wgpu::BufferDescriptor {
1145 label: Some("lic_object_uniform"),
1146 size: std::mem::size_of::<LicObjectUniform>() as u64,
1147 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1148 mapped_at_creation: false,
1149 });
1150 queue.write_buffer(&obj_buf, 0, bytemuck::cast_slice(&[obj_data]));
1151 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1154 label: Some("lic_surface_item_bg"),
1155 layout: bgl,
1156 entries: &[
1157 wgpu::BindGroupEntry {
1158 binding: 0,
1159 resource: obj_buf.as_entire_binding(),
1160 },
1161 ],
1162 });
1163 self.lic_gpu_data.push(crate::resources::LicSurfaceGpuData {
1164 bind_group: bg,
1165 _object_uniform_buf: obj_buf,
1166 mesh_id: item.mesh_id,
1167 vector_attribute: item.vector_attribute.clone(),
1168 });
1169 }
1170 }
1171 }
1172 }
1173 if let Some(hdr) = self.viewport_slots[frame.camera.viewport_index].hdr.as_ref() {
1175 if let Some(first) = frame.scene.lic_items.first() {
1176 let [vw, vh] = hdr.size;
1177 let u = crate::resources::LicAdvectUniform {
1178 steps: first.config.steps,
1179 step_size: first.config.step_size,
1180 vp_width: vw as f32,
1181 vp_height: vh as f32,
1182 };
1183 queue.write_buffer(&hdr.lic_uniform_buf, 0, bytemuck::cast_slice(&[u]));
1184 }
1185 }
1186 }
1187
1188 self.volume_gpu_data.clear();
1194 if !frame.scene.volumes.is_empty() {
1195 resources.ensure_volume_pipeline(device);
1196 let clip_objects_for_vol = &frame.effects.clip_objects;
1197 let vol_step_multiplier = if self.degradation_volume_quality_reduced {
1200 2.0_f32
1201 } else {
1202 1.0_f32
1203 };
1204 for item in &frame.scene.volumes {
1205 let gpu = resources.upload_volume_frame(
1206 device,
1207 queue,
1208 item,
1209 clip_objects_for_vol,
1210 vol_step_multiplier,
1211 );
1212 self.volume_gpu_data.push(gpu);
1213 }
1214 }
1215
1216 {
1218 let total = scene_items.len() as u32;
1219 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
1220 let mut draw_calls = 0u32;
1221 let mut triangles = 0u64;
1222 let instanced_batch_count = if self.use_instancing {
1223 self.instanced_batches.len() as u32
1224 } else {
1225 0
1226 };
1227
1228 if self.use_instancing {
1229 for batch in &self.instanced_batches {
1230 if let Some(mesh) = resources
1231 .mesh_store
1232 .get(batch.mesh_id)
1233 {
1234 draw_calls += 1;
1235 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
1236 }
1237 }
1238 } else {
1239 for item in scene_items {
1240 if !item.visible {
1241 continue;
1242 }
1243 if let Some(mesh) = resources
1244 .mesh_store
1245 .get(item.mesh_id)
1246 {
1247 draw_calls += 1;
1248 triangles += (mesh.index_count / 3) as u64;
1249 }
1250 }
1251 }
1252
1253 self.last_stats = crate::renderer::stats::FrameStats {
1254 total_objects: total,
1255 visible_objects: visible,
1256 culled_objects: total.saturating_sub(visible),
1257 draw_calls,
1258 instanced_batches: instanced_batch_count,
1259 triangles_submitted: triangles,
1260 shadow_draw_calls: 0, gpu_culling_active: self.gpu_culling_enabled,
1262 gpu_visible_instances: if self.gpu_culling_enabled {
1264 self.last_stats.gpu_visible_instances
1265 } else {
1266 None
1267 },
1268 ..self.last_stats
1269 };
1270 }
1271
1272 let skip_shadows = self.degradation_shadows_skipped;
1277
1278 if lighting.shadows_enabled && (skip_shadows || scene_items.is_empty()) {
1282 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1283 label: Some("shadow_clear_encoder"),
1284 });
1285 let _ = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
1286 label: Some("shadow_clear_pass"),
1287 color_attachments: &[],
1288 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1289 view: &resources.shadow_map_view,
1290 depth_ops: Some(wgpu::Operations {
1291 load: wgpu::LoadOp::Clear(1.0),
1292 store: wgpu::StoreOp::Store,
1293 }),
1294 stencil_ops: None,
1295 }),
1296 timestamp_writes: None,
1297 occlusion_query_set: None,
1298 });
1299 queue.submit(std::iter::once(enc.finish()));
1300 }
1301
1302 if lighting.shadows_enabled && !scene_items.is_empty() && !skip_shadows {
1303 if self.gpu_culling_enabled
1313 && self.use_instancing
1314 && !self.instanced_batches.is_empty()
1315 && !self.cached_instance_data.is_empty()
1316 {
1317 if self.cull_resources.is_none() {
1319 self.cull_resources =
1320 Some(crate::renderer::indirect::CullResources::new(device));
1321 }
1322 resources.ensure_cull_instance_pipelines(device);
1323 for c in 0..effective_cascade_count {
1324 resources.get_shadow_cull_instance_bind_group(device, c);
1325 }
1326
1327 let instance_count = self.cached_instance_data.len() as u32;
1328 let batch_count = self.instanced_batches.len() as u32;
1329
1330 if let (Some(aabb_buf), Some(meta_buf), Some(counter_buf)) = (
1331 resources.instance_aabb_buf.as_ref(),
1332 resources.batch_meta_buf.as_ref(),
1333 resources.batch_counter_buf.as_ref(),
1334 ) {
1335 let cull = self.cull_resources.as_ref().unwrap();
1336 let mut shadow_cull_encoder =
1337 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1338 label: Some("shadow_cull_encoder"),
1339 });
1340 for c in 0..effective_cascade_count {
1341 if let (Some(shadow_vis_buf), Some(shadow_indirect_buf)) = (
1342 resources.shadow_vis_bufs[c].as_ref(),
1343 resources.shadow_indirect_bufs[c].as_ref(),
1344 ) {
1345 let cpu_frustum =
1346 crate::camera::frustum::Frustum::from_view_proj(
1347 &cascade_view_projs[c],
1348 );
1349 let frustum_uniform = crate::resources::FrustumUniform {
1350 planes: std::array::from_fn(|i| crate::resources::FrustumPlane {
1351 normal: cpu_frustum.planes[i].normal.into(),
1352 distance: cpu_frustum.planes[i].d,
1353 }),
1354 instance_count,
1355 batch_count,
1356 _pad: [0; 2],
1357 };
1358 cull.dispatch_shadow(
1359 &mut shadow_cull_encoder,
1360 device,
1361 queue,
1362 c,
1363 &frustum_uniform,
1364 aabb_buf,
1365 meta_buf,
1366 counter_buf,
1367 shadow_vis_buf,
1368 shadow_indirect_buf,
1369 instance_count,
1370 batch_count,
1371 );
1372 }
1373 }
1374 queue.submit(std::iter::once(shadow_cull_encoder.finish()));
1375 }
1376 }
1377
1378 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1379 label: Some("shadow_pass_encoder"),
1380 });
1381 {
1382 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1383 label: Some("shadow_pass"),
1384 color_attachments: &[],
1385 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1386 view: &resources.shadow_map_view,
1387 depth_ops: Some(wgpu::Operations {
1388 load: wgpu::LoadOp::Clear(1.0),
1389 store: wgpu::StoreOp::Store,
1390 }),
1391 stencil_ops: None,
1392 }),
1393 timestamp_writes: None,
1394 occlusion_query_set: None,
1395 });
1396
1397 let mut shadow_draws = 0u32;
1398 let tile_px = tile_size as f32;
1399
1400 if self.use_instancing {
1401 let use_shadow_indirect = self.gpu_culling_enabled
1402 && resources.shadow_instanced_cull_pipeline.is_some()
1403 && resources.shadow_vis_bufs[0].is_some();
1404
1405 if use_shadow_indirect {
1406 for cascade in 0..effective_cascade_count {
1408 let tile_col = (cascade % 2) as f32;
1409 let tile_row = (cascade / 2) as f32;
1410 shadow_pass.set_viewport(
1411 tile_col * tile_px,
1412 tile_row * tile_px,
1413 tile_px,
1414 tile_px,
1415 0.0,
1416 1.0,
1417 );
1418 shadow_pass.set_scissor_rect(
1419 (tile_col * tile_px) as u32,
1420 (tile_row * tile_px) as u32,
1421 tile_size,
1422 tile_size,
1423 );
1424
1425 queue.write_buffer(
1427 resources.shadow_instanced_cascade_bufs[cascade]
1428 .as_ref()
1429 .expect("shadow_instanced_cascade_bufs not allocated"),
1430 0,
1431 bytemuck::cast_slice(
1432 &cascade_view_projs[cascade].to_cols_array_2d(),
1433 ),
1434 );
1435
1436 let Some(pipeline) =
1437 resources.shadow_instanced_cull_pipeline.as_ref()
1438 else {
1439 continue;
1440 };
1441 let Some(cascade_bg) =
1442 resources.shadow_instanced_cascade_bgs[cascade].as_ref()
1443 else {
1444 continue;
1445 };
1446 let Some(inst_cull_bg) =
1447 resources.shadow_cull_instance_bgs[cascade].as_ref()
1448 else {
1449 continue;
1450 };
1451 let Some(shadow_indirect_buf) =
1452 resources.shadow_indirect_bufs[cascade].as_ref()
1453 else {
1454 continue;
1455 };
1456
1457 shadow_pass.set_pipeline(pipeline);
1458 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1459 shadow_pass.set_bind_group(1, inst_cull_bg, &[]);
1460
1461 for (bi, batch) in self.instanced_batches.iter().enumerate() {
1462 if batch.is_transparent {
1463 continue;
1464 }
1465 let Some(mesh) = resources.mesh_store.get(batch.mesh_id) else {
1466 continue;
1467 };
1468 shadow_pass
1469 .set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1470 shadow_pass.set_index_buffer(
1471 mesh.index_buffer.slice(..),
1472 wgpu::IndexFormat::Uint32,
1473 );
1474 shadow_pass.draw_indexed_indirect(
1475 shadow_indirect_buf,
1476 bi as u64 * 20,
1477 );
1478 shadow_draws += 1;
1479 }
1480 }
1481 } else if let (Some(pipeline), Some(instance_bg)) = (
1482 &resources.shadow_instanced_pipeline,
1483 self.instanced_batches.first().and_then(|b| {
1484 resources.instance_bind_groups.get(&(
1485 b.texture_id.unwrap_or(u64::MAX),
1486 b.normal_map_id.unwrap_or(u64::MAX),
1487 b.ao_map_id.unwrap_or(u64::MAX),
1488 ))
1489 }),
1490 ) {
1491 for cascade in 0..effective_cascade_count {
1493 let tile_col = (cascade % 2) as f32;
1494 let tile_row = (cascade / 2) as f32;
1495 shadow_pass.set_viewport(
1496 tile_col * tile_px,
1497 tile_row * tile_px,
1498 tile_px,
1499 tile_px,
1500 0.0,
1501 1.0,
1502 );
1503 shadow_pass.set_scissor_rect(
1504 (tile_col * tile_px) as u32,
1505 (tile_row * tile_px) as u32,
1506 tile_size,
1507 tile_size,
1508 );
1509
1510 shadow_pass.set_pipeline(pipeline);
1511
1512 queue.write_buffer(
1513 resources.shadow_instanced_cascade_bufs[cascade]
1514 .as_ref()
1515 .expect("shadow_instanced_cascade_bufs not allocated"),
1516 0,
1517 bytemuck::cast_slice(
1518 &cascade_view_projs[cascade].to_cols_array_2d(),
1519 ),
1520 );
1521
1522 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
1523 .as_ref()
1524 .expect("shadow_instanced_cascade_bgs not allocated");
1525 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1526 shadow_pass.set_bind_group(1, instance_bg, &[]);
1527
1528 for batch in &self.instanced_batches {
1529 if batch.is_transparent {
1530 continue;
1531 }
1532 let Some(mesh) = resources
1533 .mesh_store
1534 .get(batch.mesh_id)
1535 else {
1536 continue;
1537 };
1538 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1539 shadow_pass.set_index_buffer(
1540 mesh.index_buffer.slice(..),
1541 wgpu::IndexFormat::Uint32,
1542 );
1543 shadow_pass.draw_indexed(
1544 0..mesh.index_count,
1545 0,
1546 batch.instance_offset
1547 ..batch.instance_offset + batch.instance_count,
1548 );
1549 shadow_draws += 1;
1550 }
1551 }
1552 }
1553 } else {
1554 for cascade in 0..effective_cascade_count {
1555 let tile_col = (cascade % 2) as f32;
1556 let tile_row = (cascade / 2) as f32;
1557 shadow_pass.set_viewport(
1558 tile_col * tile_px,
1559 tile_row * tile_px,
1560 tile_px,
1561 tile_px,
1562 0.0,
1563 1.0,
1564 );
1565 shadow_pass.set_scissor_rect(
1566 (tile_col * tile_px) as u32,
1567 (tile_row * tile_px) as u32,
1568 tile_size,
1569 tile_size,
1570 );
1571
1572 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1573 shadow_pass.set_bind_group(
1574 0,
1575 &resources.shadow_bind_group,
1576 &[cascade as u32 * 256],
1577 );
1578
1579 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1580 &cascade_view_projs[cascade],
1581 );
1582
1583 for item in scene_items.iter() {
1584 if !item.visible {
1585 continue;
1586 }
1587 if item.material.opacity < 1.0 {
1588 continue;
1589 }
1590 let Some(mesh) = resources
1591 .mesh_store
1592 .get(item.mesh_id)
1593 else {
1594 continue;
1595 };
1596
1597 let world_aabb = mesh
1598 .aabb
1599 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1600 if cascade_frustum.cull_aabb(&world_aabb) {
1601 continue;
1602 }
1603
1604 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1605 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1606 shadow_pass.set_index_buffer(
1607 mesh.index_buffer.slice(..),
1608 wgpu::IndexFormat::Uint32,
1609 );
1610 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1611 shadow_draws += 1;
1612 }
1613 }
1614 }
1615 drop(shadow_pass);
1616 self.last_stats.shadow_draw_calls = shadow_draws;
1617 }
1618 queue.submit(std::iter::once(encoder.finish()));
1619 }
1620 }
1621
1622 pub(super) fn prepare_viewport_internal(
1627 &mut self,
1628 device: &wgpu::Device,
1629 queue: &wgpu::Queue,
1630 frame: &FrameData,
1631 viewport_fx: &ViewportEffects<'_>,
1632 ) {
1633 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1636
1637 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1638 SurfaceSubmission::Flat(items) => items.as_ref(),
1639 };
1640
1641 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1643
1644 {
1645 let resources = &mut self.resources;
1646
1647 {
1649 let mut planes = [[0.0f32; 4]; 6];
1650 let mut count = 0u32;
1651 let mut clip_vol_uniform: ClipVolumeUniform = bytemuck::Zeroable::zeroed(); for obj in viewport_fx
1654 .clip_objects
1655 .iter()
1656 .filter(|o| o.enabled && o.clip_geometry)
1657 {
1658 match obj.shape {
1659 ClipShape::Plane {
1660 normal, distance, ..
1661 } if count < 6 => {
1662 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1663 count += 1;
1664 }
1665 ClipShape::Box {
1666 center,
1667 half_extents,
1668 orientation,
1669 } if clip_vol_uniform.volume_type == 0 => {
1670 clip_vol_uniform.volume_type = 2;
1671 clip_vol_uniform.box_center = center;
1672 clip_vol_uniform.box_half_extents = half_extents;
1673 clip_vol_uniform.box_col0 = orientation[0];
1674 clip_vol_uniform.box_col1 = orientation[1];
1675 clip_vol_uniform.box_col2 = orientation[2];
1676 }
1677 ClipShape::Sphere { center, radius }
1678 if clip_vol_uniform.volume_type == 0 =>
1679 {
1680 clip_vol_uniform.volume_type = 3;
1681 clip_vol_uniform.sphere_center = center;
1682 clip_vol_uniform.sphere_radius = radius;
1683 }
1684 _ => {}
1685 }
1686 }
1687
1688 let clip_uniform = ClipPlanesUniform {
1689 planes,
1690 count,
1691 _pad0: 0,
1692 viewport_width: frame.camera.viewport_size[0].max(1.0),
1693 viewport_height: frame.camera.viewport_size[1].max(1.0),
1694 };
1695 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1697 queue.write_buffer(
1698 &slot.clip_planes_buf,
1699 0,
1700 bytemuck::cast_slice(&[clip_uniform]),
1701 );
1702 queue.write_buffer(
1703 &slot.clip_volume_buf,
1704 0,
1705 bytemuck::cast_slice(&[clip_vol_uniform]),
1706 );
1707 }
1708 queue.write_buffer(
1710 &resources.clip_planes_uniform_buf,
1711 0,
1712 bytemuck::cast_slice(&[clip_uniform]),
1713 );
1714 queue.write_buffer(
1715 &resources.clip_volume_uniform_buf,
1716 0,
1717 bytemuck::cast_slice(&[clip_vol_uniform]),
1718 );
1719 }
1720
1721 let camera_uniform = frame.camera.render_camera.camera_uniform();
1723 queue.write_buffer(
1725 &resources.camera_uniform_buf,
1726 0,
1727 bytemuck::cast_slice(&[camera_uniform]),
1728 );
1729 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1731 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1732 }
1733
1734 if frame.viewport.show_grid {
1736 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1737 if !eye.is_finite() {
1738 tracing::warn!(
1739 eye_x = eye.x,
1740 eye_y = eye.y,
1741 eye_z = eye.z,
1742 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1743 );
1744 } else {
1745 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1746
1747 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1748 (frame.viewport.grid_cell_size, 1.0_f32)
1749 } else {
1750 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1751 let world_per_pixel =
1752 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1753 / frame.camera.viewport_size[1].max(1.0);
1754 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1755 let mut s = 1.0_f32;
1756 let mut iters = 0u32;
1757 while s < target {
1758 s *= 10.0;
1759 iters += 1;
1760 }
1761 let ratio = (target / s).clamp(0.0, 1.0);
1762 let fade = if ratio < 0.5 {
1763 1.0_f32
1764 } else {
1765 let t = (ratio - 0.5) * 2.0;
1766 1.0 - t * t * (3.0 - 2.0 * t)
1767 };
1768 tracing::debug!(
1769 eye_z = eye.z,
1770 vertical_depth,
1771 world_per_pixel,
1772 target,
1773 spacing = s,
1774 lod_iters = iters,
1775 ratio,
1776 minor_fade = fade,
1777 "grid LOD"
1778 );
1779 (s, fade)
1780 };
1781
1782 let spacing_major = spacing * 10.0;
1783 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1784 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1785 tracing::debug!(
1786 spacing_minor = spacing,
1787 spacing_major,
1788 snap_x,
1789 snap_y,
1790 eye_x = eye.x,
1791 eye_y = eye.y,
1792 eye_z = eye.z,
1793 "grid snap"
1794 );
1795
1796 let orient = frame.camera.render_camera.orientation;
1797 let right = orient * glam::Vec3::X;
1798 let up = orient * glam::Vec3::Y;
1799 let back = orient * glam::Vec3::Z;
1800 let cam_to_world = [
1801 [right.x, right.y, right.z, 0.0_f32],
1802 [up.x, up.y, up.z, 0.0_f32],
1803 [back.x, back.y, back.z, 0.0_f32],
1804 ];
1805 let aspect =
1806 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1807 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1808
1809 let uniform = GridUniform {
1810 view_proj: view_proj_mat,
1811 cam_to_world,
1812 tan_half_fov,
1813 aspect,
1814 _pad_ivp: [0.0; 2],
1815 eye_pos: frame.camera.render_camera.eye_position,
1816 grid_z: frame.viewport.grid_z,
1817 spacing_minor: spacing,
1818 spacing_major,
1819 snap_origin: [snap_x, snap_y],
1820 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1821 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1822 };
1823 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1825 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1826 }
1827 queue.write_buffer(
1829 &resources.grid_uniform_buf,
1830 0,
1831 bytemuck::cast_slice(&[uniform]),
1832 );
1833 }
1834 }
1835 {
1839 let gp = &viewport_fx.ground_plane;
1840 let mode_u32: u32 = match gp.mode {
1841 crate::renderer::types::GroundPlaneMode::None => 0,
1842 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1843 crate::renderer::types::GroundPlaneMode::Tile => 2,
1844 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1845 };
1846 let orient = frame.camera.render_camera.orientation;
1847 let right = orient * glam::Vec3::X;
1848 let up = orient * glam::Vec3::Y;
1849 let back = orient * glam::Vec3::Z;
1850 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1851 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1852 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1853 let gp_uniform = crate::resources::GroundPlaneUniform {
1854 view_proj: vp,
1855 cam_right: [right.x, right.y, right.z, 0.0],
1856 cam_up: [up.x, up.y, up.z, 0.0],
1857 cam_back: [back.x, back.y, back.z, 0.0],
1858 eye_pos: frame.camera.render_camera.eye_position,
1859 height: gp.height,
1860 color: gp.color,
1861 shadow_color: gp.shadow_color,
1862 light_vp: gp_cascade0_mat,
1863 tan_half_fov,
1864 aspect,
1865 tile_size: gp.tile_size,
1866 shadow_bias: 0.002,
1867 mode: mode_u32,
1868 shadow_opacity: gp.shadow_opacity,
1869 _pad: [0.0; 2],
1870 };
1871 queue.write_buffer(
1872 &resources.ground_plane_uniform_buf,
1873 0,
1874 bytemuck::cast_slice(&[gp_uniform]),
1875 );
1876 }
1877 } let vp_idx = frame.camera.viewport_index;
1886
1887 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1889 if frame.interaction.outline_selected {
1890 let resources = &self.resources;
1891 for item in scene_items {
1892 if !item.visible || !item.selected {
1893 continue;
1894 }
1895 let uniform = OutlineUniform {
1896 model: item.model,
1897 color: [0.0; 4], pixel_offset: 0.0,
1899 _pad: [0.0; 3],
1900 };
1901 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1902 label: Some("outline_mask_uniform_buf"),
1903 size: std::mem::size_of::<OutlineUniform>() as u64,
1904 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1905 mapped_at_creation: false,
1906 });
1907 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1908 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1909 label: Some("outline_mask_object_bg"),
1910 layout: &resources.outline_bind_group_layout,
1911 entries: &[wgpu::BindGroupEntry {
1912 binding: 0,
1913 resource: buf.as_entire_binding(),
1914 }],
1915 });
1916 outline_object_buffers.push(OutlineObjectBuffers {
1917 mesh_id: item.mesh_id,
1918 two_sided: item.material.is_two_sided(),
1919 _mask_uniform_buf: buf,
1920 mask_bind_group: bg,
1921 });
1922 }
1923 }
1924
1925 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1927 if frame.interaction.xray_selected {
1928 let resources = &self.resources;
1929 for item in scene_items {
1930 if !item.visible || !item.selected {
1931 continue;
1932 }
1933 let uniform = OutlineUniform {
1934 model: item.model,
1935 color: frame.interaction.xray_color,
1936 pixel_offset: 0.0,
1937 _pad: [0.0; 3],
1938 };
1939 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1940 label: Some("xray_uniform_buf"),
1941 size: std::mem::size_of::<OutlineUniform>() as u64,
1942 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1943 mapped_at_creation: false,
1944 });
1945 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1946 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1947 label: Some("xray_object_bg"),
1948 layout: &resources.outline_bind_group_layout,
1949 entries: &[wgpu::BindGroupEntry {
1950 binding: 0,
1951 resource: buf.as_entire_binding(),
1952 }],
1953 });
1954 xray_object_buffers.push((item.mesh_id, buf, bg));
1955 }
1956 }
1957
1958 let mut constraint_line_buffers = Vec::new();
1960 for overlay in &frame.interaction.constraint_overlays {
1961 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1962 }
1963
1964 let mut clip_plane_fill_buffers = Vec::new();
1966 let mut clip_plane_line_buffers = Vec::new();
1967 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1968 if obj.color.is_none() && obj.edge_color.is_none() {
1970 continue;
1971 }
1972 if let ClipShape::Plane {
1973 normal, distance, ..
1974 } = obj.shape
1975 {
1976 let n = glam::Vec3::from(normal);
1977 let center = n * (-distance);
1980 let active = obj.active;
1981 let hovered = obj.hovered || active;
1982
1983 let fill_color = if let Some(base_color) = obj.color {
1985 if active {
1986 [
1987 base_color[0] * 0.5,
1988 base_color[1] * 0.5,
1989 base_color[2] * 0.5,
1990 base_color[3] * 0.5,
1991 ]
1992 } else if hovered {
1993 [
1994 base_color[0] * 0.8,
1995 base_color[1] * 0.8,
1996 base_color[2] * 0.8,
1997 base_color[3] * 0.6,
1998 ]
1999 } else {
2000 [
2001 base_color[0] * 0.5,
2002 base_color[1] * 0.5,
2003 base_color[2] * 0.5,
2004 base_color[3] * 0.3,
2005 ]
2006 }
2007 } else {
2008 [0.0, 0.0, 0.0, 0.0]
2009 };
2010
2011 let border_base = obj
2013 .edge_color
2014 .or(obj.color)
2015 .unwrap_or([1.0, 1.0, 1.0, 1.0]);
2016 let border_color = if active {
2017 [border_base[0], border_base[1], border_base[2], 0.9]
2018 } else if hovered {
2019 [border_base[0], border_base[1], border_base[2], 0.8]
2020 } else {
2021 [
2022 border_base[0] * 0.9,
2023 border_base[1] * 0.9,
2024 border_base[2] * 0.9,
2025 0.6,
2026 ]
2027 };
2028
2029 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
2030 center,
2031 normal: n,
2032 extent: obj.extent,
2033 fill_color,
2034 border_color,
2035 _hovered: hovered,
2036 _active: active,
2037 };
2038 if obj.color.is_some() {
2039 clip_plane_fill_buffers.push(
2040 self.resources
2041 .create_clip_plane_fill_overlay(device, &overlay),
2042 );
2043 }
2044 clip_plane_line_buffers.push(
2045 self.resources
2046 .create_clip_plane_line_overlay(device, &overlay),
2047 );
2048 } else {
2049 let base_color = obj.color.unwrap_or([1.0, 1.0, 1.0, 1.0]);
2053 self.resources.ensure_polyline_pipeline(device);
2054 match obj.shape {
2055 ClipShape::Box {
2056 center,
2057 half_extents,
2058 orientation,
2059 } => {
2060 let polyline =
2061 clip_box_outline(center, half_extents, orientation, base_color);
2062 let vp_size = frame.camera.viewport_size;
2063 let gpu = self
2064 .resources
2065 .upload_polyline(device, queue, &polyline, vp_size);
2066 self.polyline_gpu_data.push(gpu);
2067 }
2068 ClipShape::Sphere { center, radius } => {
2069 let polyline = clip_sphere_outline(center, radius, base_color);
2070 let vp_size = frame.camera.viewport_size;
2071 let gpu = self
2072 .resources
2073 .upload_polyline(device, queue, &polyline, vp_size);
2074 self.polyline_gpu_data.push(gpu);
2075 }
2076 _ => {}
2077 }
2078 }
2079 }
2080
2081 let mut cap_buffers = Vec::new();
2083 if viewport_fx.cap_fill_enabled {
2084 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
2085 if let ClipShape::Plane {
2086 normal,
2087 distance,
2088 cap_color,
2089 } = obj.shape
2090 {
2091 let plane_n = glam::Vec3::from(normal);
2092 for item in scene_items.iter().filter(|i| i.visible) {
2093 let Some(mesh) = self
2094 .resources
2095 .mesh_store
2096 .get(item.mesh_id)
2097 else {
2098 continue;
2099 };
2100 let model = glam::Mat4::from_cols_array_2d(&item.model);
2101 let world_aabb = mesh.aabb.transformed(&model);
2102 if !world_aabb.intersects_plane(plane_n, distance) {
2103 continue;
2104 }
2105 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
2106 else {
2107 continue;
2108 };
2109 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
2110 pos, idx, &model, plane_n, distance,
2111 ) {
2112 let bc = item.material.base_color;
2113 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
2114 let buf = self.resources.upload_cap_geometry(device, &cap, color);
2115 cap_buffers.push(buf);
2116 }
2117 }
2118 }
2119 }
2120 }
2121
2122 let axes_verts = if frame.viewport.show_axes_indicator
2124 && frame.camera.viewport_size[0] > 0.0
2125 && frame.camera.viewport_size[1] > 0.0
2126 {
2127 let verts = crate::widgets::axes_indicator::build_axes_geometry(
2128 frame.camera.viewport_size[0],
2129 frame.camera.viewport_size[1],
2130 frame.camera.render_camera.orientation,
2131 );
2132 if verts.is_empty() { None } else { Some(verts) }
2133 } else {
2134 None
2135 };
2136
2137 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
2139 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
2140 frame.interaction.gizmo_mode,
2141 frame.interaction.gizmo_hovered,
2142 frame.interaction.gizmo_space_orientation,
2143 );
2144 (verts, indices, model)
2145 });
2146
2147 {
2151 let slot = &mut self.viewport_slots[vp_idx];
2152 slot.outline_object_buffers = outline_object_buffers;
2153 slot.xray_object_buffers = xray_object_buffers;
2154 slot.constraint_line_buffers = constraint_line_buffers;
2155 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
2156 slot.clip_plane_line_buffers = clip_plane_line_buffers;
2157 slot.cap_buffers = cap_buffers;
2158
2159 if let Some(verts) = axes_verts {
2161 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
2162 if byte_size > slot.axes_vertex_buffer.size() {
2163 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2164 label: Some("vp_axes_vertex_buf"),
2165 size: byte_size,
2166 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2167 mapped_at_creation: false,
2168 });
2169 }
2170 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
2171 slot.axes_vertex_count = verts.len() as u32;
2172 } else {
2173 slot.axes_vertex_count = 0;
2174 }
2175
2176 if let Some((verts, indices, model)) = gizmo_update {
2178 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
2179 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
2180 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
2181 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2182 label: Some("vp_gizmo_vertex_buf"),
2183 size: vert_bytes.len() as u64,
2184 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2185 mapped_at_creation: false,
2186 });
2187 }
2188 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
2189 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2190 label: Some("vp_gizmo_index_buf"),
2191 size: idx_bytes.len() as u64,
2192 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2193 mapped_at_creation: false,
2194 });
2195 }
2196 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
2197 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
2198 slot.gizmo_index_count = indices.len() as u32;
2199 let uniform = crate::interaction::gizmo::GizmoUniform {
2200 model: model.to_cols_array_2d(),
2201 };
2202 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
2203 }
2204 }
2205
2206 if frame.interaction.outline_selected
2217 && !self.viewport_slots[vp_idx]
2218 .outline_object_buffers
2219 .is_empty()
2220 {
2221 let w = frame.camera.viewport_size[0] as u32;
2222 let h = frame.camera.viewport_size[1] as u32;
2223
2224 self.ensure_viewport_hdr(
2226 device,
2227 queue,
2228 vp_idx,
2229 w.max(1),
2230 h.max(1),
2231 frame.effects.post_process.ssaa_factor.max(1),
2232 );
2233
2234 {
2236 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2237 let edge_uniform = OutlineEdgeUniform {
2238 color: frame.interaction.outline_color,
2239 radius: frame.interaction.outline_width_px,
2240 viewport_w: w as f32,
2241 viewport_h: h as f32,
2242 _pad: 0.0,
2243 };
2244 queue.write_buffer(
2245 &slot_hdr.outline_edge_uniform_buf,
2246 0,
2247 bytemuck::cast_slice(&[edge_uniform]),
2248 );
2249 }
2250
2251 let slot_ref = &self.viewport_slots[vp_idx];
2254 let outlines_ptr =
2255 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
2256 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
2257 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
2258 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
2259 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
2260 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
2261 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
2262 let (outlines, camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
2265 (
2266 &*outlines_ptr,
2267 &*camera_bg_ptr,
2268 &*mask_view_ptr,
2269 &*color_view_ptr,
2270 &*depth_view_ptr,
2271 &*edge_bg_ptr,
2272 )
2273 };
2274
2275 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2276 label: Some("outline_offscreen_encoder"),
2277 });
2278
2279 {
2281 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2282 label: Some("outline_mask_pass"),
2283 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2284 view: mask_view,
2285 resolve_target: None,
2286 ops: wgpu::Operations {
2287 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2288 store: wgpu::StoreOp::Store,
2289 },
2290 depth_slice: None,
2291 })],
2292 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2293 view: depth_view,
2294 depth_ops: Some(wgpu::Operations {
2295 load: wgpu::LoadOp::Clear(1.0),
2296 store: wgpu::StoreOp::Discard,
2297 }),
2298 stencil_ops: None,
2299 }),
2300 timestamp_writes: None,
2301 occlusion_query_set: None,
2302 });
2303
2304 pass.set_bind_group(0, camera_bg, &[]);
2305 for outlined in outlines {
2306 let Some(mesh) = self
2307 .resources
2308 .mesh_store
2309 .get(outlined.mesh_id)
2310 else {
2311 continue;
2312 };
2313 let pipeline = if outlined.two_sided {
2314 &self.resources.outline_mask_two_sided_pipeline
2315 } else {
2316 &self.resources.outline_mask_pipeline
2317 };
2318 pass.set_pipeline(pipeline);
2319 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
2320 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2321 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2322 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
2323 }
2324 }
2325
2326 {
2328 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2329 label: Some("outline_edge_pass"),
2330 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2331 view: color_view,
2332 resolve_target: None,
2333 ops: wgpu::Operations {
2334 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2335 store: wgpu::StoreOp::Store,
2336 },
2337 depth_slice: None,
2338 })],
2339 depth_stencil_attachment: None,
2340 timestamp_writes: None,
2341 occlusion_query_set: None,
2342 });
2343 pass.set_pipeline(&self.resources.outline_edge_pipeline);
2344 pass.set_bind_group(0, edge_bg, &[]);
2345 pass.draw(0..3, 0..1);
2346 }
2347
2348 queue.submit(std::iter::once(encoder.finish()));
2349 }
2350
2351 {
2356 let w = frame.camera.viewport_size[0];
2357 let h = frame.camera.viewport_size[1];
2358 if let Some(sel_ref) = &frame.interaction.sub_selection {
2359 let needs_rebuild = {
2360 let slot = &self.viewport_slots[vp_idx];
2361 slot.sub_highlight_generation != sel_ref.version
2362 || slot.sub_highlight.is_none()
2363 };
2364 if needs_rebuild {
2365 self.resources.ensure_sub_highlight_pipelines(device);
2366 let data = self.resources.build_sub_highlight(
2367 device,
2368 queue,
2369 sel_ref,
2370 frame.interaction.sub_highlight_face_fill_color,
2371 frame.interaction.sub_highlight_edge_color,
2372 frame.interaction.sub_highlight_edge_width_px,
2373 frame.interaction.sub_highlight_vertex_size_px,
2374 w,
2375 h,
2376 );
2377 let slot = &mut self.viewport_slots[vp_idx];
2378 slot.sub_highlight = Some(data);
2379 slot.sub_highlight_generation = sel_ref.version;
2380 }
2381 } else {
2382 let slot = &mut self.viewport_slots[vp_idx];
2383 slot.sub_highlight = None;
2384 slot.sub_highlight_generation = u64::MAX;
2385 }
2386 }
2387
2388 self.label_gpu_data = None;
2392 if !frame.overlays.labels.is_empty() {
2393 self.resources.ensure_overlay_text_pipeline(device);
2394 let vp_w = frame.camera.viewport_size[0];
2395 let vp_h = frame.camera.viewport_size[1];
2396 if vp_w > 0.0 && vp_h > 0.0 {
2397 let view = &frame.camera.render_camera.view;
2398 let proj = &frame.camera.render_camera.projection;
2399
2400 let mut sorted_labels: Vec<&crate::renderer::types::LabelItem> =
2402 frame.overlays.labels.iter().collect();
2403 sorted_labels.sort_by_key(|l| l.z_order);
2404
2405 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2406
2407 for label in &sorted_labels {
2408 if label.text.is_empty() || label.opacity <= 0.0 {
2409 continue;
2410 }
2411
2412 let screen_pos = if let Some(sa) = label.screen_anchor {
2414 Some(sa)
2415 } else if let Some(wa) = label.world_anchor {
2416 project_to_screen(wa, view, proj, vp_w, vp_h)
2417 } else {
2418 continue;
2419 };
2420 let Some(anchor_px) = screen_pos else {
2421 continue;
2422 };
2423
2424 let opacity = label.opacity.clamp(0.0, 1.0);
2425
2426 let layout = if let Some(max_w) = label.max_width {
2428 self.resources.glyph_atlas.layout_text_wrapped(
2429 &label.text,
2430 label.font_size,
2431 label.font,
2432 max_w,
2433 device,
2434 )
2435 } else {
2436 self.resources.glyph_atlas.layout_text(
2437 &label.text,
2438 label.font_size,
2439 label.font,
2440 device,
2441 )
2442 };
2443
2444 let font_index = label.font.map_or(0, |h| h.0);
2446 let ascent = self.resources.glyph_atlas.font_ascent(font_index, label.font_size);
2447
2448 let align_offset = match label.anchor_align {
2450 crate::renderer::types::LabelAnchor::Leading => 6.0,
2451 crate::renderer::types::LabelAnchor::Center => -layout.total_width * 0.5,
2452 crate::renderer::types::LabelAnchor::Trailing => -layout.total_width - 6.0,
2453 };
2454
2455 let text_x = anchor_px[0] + align_offset + label.offset[0];
2457 let text_y = anchor_px[1] - layout.height * 0.5 + label.offset[1];
2458
2459 if label.background {
2461 let pad = label.padding;
2462 let bx0 = text_x - pad;
2463 let by0 = text_y - pad;
2464 let bx1 = text_x + layout.total_width + pad;
2465 let by1 = text_y + layout.height + pad;
2466 let bg_color = apply_opacity(label.background_color, opacity);
2467 if label.border_radius > 0.0 {
2468 emit_rounded_quad(
2469 &mut verts,
2470 bx0, by0, bx1, by1,
2471 label.border_radius,
2472 bg_color,
2473 vp_w, vp_h,
2474 );
2475 } else {
2476 emit_solid_quad(
2477 &mut verts,
2478 bx0, by0, bx1, by1,
2479 bg_color,
2480 vp_w, vp_h,
2481 );
2482 }
2483 }
2484
2485 if label.leader_line {
2487 if let Some(wa) = label.world_anchor {
2488 let world_px = project_to_screen(wa, view, proj, vp_w, vp_h);
2489 if let Some(wp) = world_px {
2490 emit_line_quad(
2491 &mut verts,
2492 wp[0], wp[1],
2493 text_x, text_y + layout.height * 0.5,
2494 1.5,
2495 apply_opacity(label.leader_color, opacity),
2496 vp_w, vp_h,
2497 );
2498 }
2499 }
2500 }
2501
2502 let text_color = apply_opacity(label.color, opacity);
2504 for gq in &layout.quads {
2505 let gx = text_x + gq.pos[0];
2506 let gy = text_y + ascent + gq.pos[1];
2507 emit_textured_quad(
2508 &mut verts,
2509 gx, gy,
2510 gx + gq.size[0], gy + gq.size[1],
2511 gq.uv_min, gq.uv_max,
2512 text_color,
2513 vp_w, vp_h,
2514 );
2515 }
2516 }
2517
2518 self.resources.glyph_atlas.upload_if_dirty(queue);
2520
2521 if !verts.is_empty() {
2522 let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2523 label: Some("overlay_label_vbuf"),
2524 contents: bytemuck::cast_slice(&verts),
2525 usage: wgpu::BufferUsages::VERTEX,
2526 });
2527 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2528 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2529 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2530 label: Some("overlay_label_bg"),
2531 layout: bgl,
2532 entries: &[
2533 wgpu::BindGroupEntry {
2534 binding: 0,
2535 resource: wgpu::BindingResource::TextureView(
2536 &self.resources.glyph_atlas.view,
2537 ),
2538 },
2539 wgpu::BindGroupEntry {
2540 binding: 1,
2541 resource: wgpu::BindingResource::Sampler(sampler),
2542 },
2543 ],
2544 });
2545 self.label_gpu_data = Some(crate::resources::LabelGpuData {
2546 vertex_buf,
2547 vertex_count: verts.len() as u32,
2548 bind_group,
2549 });
2550 }
2551 }
2552 }
2553
2554 self.scalar_bar_gpu_data = None;
2558 if !frame.overlays.scalar_bars.is_empty() {
2559 self.resources.ensure_overlay_text_pipeline(device);
2560 let vp_w = frame.camera.viewport_size[0];
2561 let vp_h = frame.camera.viewport_size[1];
2562 if vp_w > 0.0 && vp_h > 0.0 {
2563 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2564
2565 for bar in &frame.overlays.scalar_bars {
2566 let Some(lut) = self.resources.get_colormap_rgba(bar.colormap_id).map(|l| l.to_vec()) else {
2569 continue;
2570 };
2571
2572 let is_vertical = matches!(
2573 bar.orientation,
2574 crate::renderer::types::ScalarBarOrientation::Vertical
2575 );
2576 let reversed = bar.ticks_reversed;
2577
2578 let tick_fs = bar.font_size;
2580 let title_fs = bar.title_font_size.unwrap_or(bar.font_size);
2581 let font_index = bar.font.map_or(0, |h| h.0);
2582
2583 let (strip_w, strip_h) = if is_vertical {
2585 (bar.bar_width_px, bar.bar_length_px)
2586 } else {
2587 (bar.bar_length_px, bar.bar_width_px)
2588 };
2589
2590 let tick_count = bar.tick_count.max(2);
2593 let mut tick_data: Vec<(String, f32, f32)> = Vec::new(); let mut max_tick_w = 0.0f32;
2595 let mut tick_h = 0.0f32;
2596 for i in 0..tick_count {
2597 let t = i as f32 / (tick_count - 1) as f32;
2598 let value = bar.scalar_min + t * (bar.scalar_max - bar.scalar_min);
2599 let text = format!("{value:.2}");
2600 let layout = self.resources.glyph_atlas.layout_text(
2601 &text, tick_fs, bar.font, device,
2602 );
2603 max_tick_w = max_tick_w.max(layout.total_width);
2604 tick_h = layout.height;
2605 tick_data.push((text, layout.total_width, layout.height));
2606 }
2607
2608 let half_tick = tick_h / 2.0;
2613 let title_h = if bar.title.is_some() {
2614 title_fs + 4.0 + half_tick
2616 } else {
2617 half_tick
2619 };
2620
2621 let title_w = if let Some(ref t) = bar.title {
2624 self.resources.glyph_atlas.layout_text(t, title_fs, bar.font, device).total_width
2625 } else {
2626 0.0
2627 };
2628
2629 let bg_pad = 4.0;
2635 let (inset_left, inset_right) = if is_vertical {
2636 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
2637 let right_extent = 4.0 + max_tick_w + bg_pad; (title_oh + bg_pad, right_extent)
2639 } else {
2640 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
2641 let tick_oh = max_tick_w / 2.0;
2642 let side = title_oh.max(tick_oh) + bg_pad;
2643 (side, side)
2644 };
2645
2646 let bottom_overhang = if is_vertical { half_tick } else { 3.0 + tick_h };
2651
2652 let (bar_x, bar_y) = match bar.anchor {
2658 crate::renderer::types::ScalarBarAnchor::TopLeft => (
2659 bar.margin_px + inset_left,
2660 bar.margin_px + title_h + bg_pad,
2661 ),
2662 crate::renderer::types::ScalarBarAnchor::TopRight => (
2663 vp_w - bar.margin_px - strip_w - inset_right,
2664 bar.margin_px + title_h + bg_pad,
2665 ),
2666 crate::renderer::types::ScalarBarAnchor::BottomLeft => (
2667 bar.margin_px + inset_left,
2668 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
2669 ),
2670 crate::renderer::types::ScalarBarAnchor::BottomRight => (
2671 vp_w - bar.margin_px - strip_w - inset_right,
2672 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
2673 ),
2674 };
2675
2676 let (bg_x0, bg_y0, bg_x1, bg_y1) = if is_vertical {
2678 let title_right = bar_x + (strip_w + title_w) / 2.0;
2679 let ticks_right = bar_x + strip_w + 4.0 + max_tick_w;
2680 (
2681 bar_x - bg_pad - ((title_w - strip_w) / 2.0).max(0.0),
2682 bar_y - title_h - bg_pad,
2683 ticks_right.max(title_right) + bg_pad,
2684 bar_y + strip_h + half_tick + bg_pad,
2685 )
2686 } else {
2687 let title_overhang = ((title_w - strip_w) / 2.0).max(0.0);
2688 let tick_overhang = max_tick_w / 2.0;
2689 let side_pad = title_overhang.max(tick_overhang);
2690 let bottom = bar_y + strip_h + 3.0 + tick_h + bg_pad;
2691 (
2692 bar_x - bg_pad - side_pad,
2693 bar_y - title_h - bg_pad,
2694 bar_x + strip_w + bg_pad + side_pad,
2695 bottom,
2696 )
2697 };
2698 emit_rounded_quad(
2699 &mut verts,
2700 bg_x0, bg_y0, bg_x1, bg_y1,
2701 3.0,
2702 bar.background_color,
2703 vp_w, vp_h,
2704 );
2705
2706 let steps: usize = 64;
2708 for s in 0..steps {
2709 let (qx0, qy0, qx1, qy1, t) = if is_vertical {
2710 let t = if reversed {
2712 s as f32 / (steps - 1) as f32
2713 } else {
2714 1.0 - s as f32 / (steps - 1) as f32
2715 };
2716 let step_h = strip_h / steps as f32;
2717 let sy = bar_y + s as f32 * step_h;
2718 (bar_x, sy, bar_x + strip_w, sy + step_h + 0.5, t)
2719 } else {
2720 let t = if reversed {
2722 1.0 - s as f32 / (steps - 1) as f32
2723 } else {
2724 s as f32 / (steps - 1) as f32
2725 };
2726 let step_w = strip_w / steps as f32;
2727 let sx = bar_x + s as f32 * step_w;
2728 (sx, bar_y, sx + step_w + 0.5, bar_y + strip_h, t)
2729 };
2730 let lut_idx = (t * 255.0).clamp(0.0, 255.0) as usize;
2731 let [r, g, b, a] = lut[lut_idx];
2732 let color = [
2733 r as f32 / 255.0,
2734 g as f32 / 255.0,
2735 b as f32 / 255.0,
2736 a as f32 / 255.0,
2737 ];
2738 emit_solid_quad(&mut verts, qx0, qy0, qx1, qy1, color, vp_w, vp_h);
2739 }
2740
2741 let ascent = self.resources.glyph_atlas.font_ascent(font_index, tick_fs);
2743 for (i, (text, tw, th)) in tick_data.iter().enumerate() {
2744 let t = i as f32 / (tick_count - 1) as f32;
2745 let layout = self.resources.glyph_atlas.layout_text(
2746 text, tick_fs, bar.font, device,
2747 );
2748
2749 let (lx, ly) = if is_vertical {
2750 let progress = if reversed { t } else { 1.0 - t };
2755 let tick_y = bar_y + progress * strip_h;
2756 (bar_x + strip_w + 4.0, tick_y - th * 0.5)
2757 } else {
2758 let frac = if reversed { 1.0 - t } else { t };
2762 let tick_x = bar_x + frac * strip_w;
2763 (tick_x - tw * 0.5, bar_y + strip_h + 3.0)
2764 };
2765 let _ = (tw, th); for gq in &layout.quads {
2768 let gx = lx + gq.pos[0];
2769 let gy = ly + ascent + gq.pos[1];
2770 emit_textured_quad(
2771 &mut verts,
2772 gx, gy,
2773 gx + gq.size[0], gy + gq.size[1],
2774 gq.uv_min, gq.uv_max,
2775 bar.label_color,
2776 vp_w, vp_h,
2777 );
2778 }
2779 }
2780
2781 if let Some(ref title_text) = bar.title {
2783 let layout = self.resources.glyph_atlas.layout_text(
2784 title_text, title_fs, bar.font, device,
2785 );
2786 let title_ascent = self.resources.glyph_atlas.font_ascent(font_index, title_fs);
2787 let tx = bar_x + (strip_w - layout.total_width) * 0.5;
2789 let ty = bar_y - title_h;
2790 for gq in &layout.quads {
2791 let gx = tx + gq.pos[0];
2792 let gy = ty + title_ascent + gq.pos[1];
2793 emit_textured_quad(
2794 &mut verts,
2795 gx, gy,
2796 gx + gq.size[0], gy + gq.size[1],
2797 gq.uv_min, gq.uv_max,
2798 bar.label_color,
2799 vp_w, vp_h,
2800 );
2801 }
2802 }
2803 }
2804
2805 self.resources.glyph_atlas.upload_if_dirty(queue);
2807
2808 if !verts.is_empty() {
2809 let vertex_buf =
2810 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2811 label: Some("overlay_scalar_bar_vbuf"),
2812 contents: bytemuck::cast_slice(&verts),
2813 usage: wgpu::BufferUsages::VERTEX,
2814 });
2815 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2816 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2817 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2818 label: Some("overlay_scalar_bar_bg"),
2819 layout: bgl,
2820 entries: &[
2821 wgpu::BindGroupEntry {
2822 binding: 0,
2823 resource: wgpu::BindingResource::TextureView(
2824 &self.resources.glyph_atlas.view,
2825 ),
2826 },
2827 wgpu::BindGroupEntry {
2828 binding: 1,
2829 resource: wgpu::BindingResource::Sampler(sampler),
2830 },
2831 ],
2832 });
2833 self.scalar_bar_gpu_data = Some(crate::resources::LabelGpuData {
2834 vertex_buf,
2835 vertex_count: verts.len() as u32,
2836 bind_group,
2837 });
2838 }
2839 }
2840 }
2841
2842 self.ruler_gpu_data = None;
2846 if !frame.overlays.rulers.is_empty() {
2847 self.resources.ensure_overlay_text_pipeline(device);
2848 let vp_w = frame.camera.viewport_size[0];
2849 let vp_h = frame.camera.viewport_size[1];
2850 if vp_w > 0.0 && vp_h > 0.0 {
2851 let view = &frame.camera.render_camera.view;
2852 let proj = &frame.camera.render_camera.projection;
2853
2854 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2855
2856 for ruler in &frame.overlays.rulers {
2857 let start_ndc = project_to_ndc(ruler.start, view, proj);
2859 let end_ndc = project_to_ndc(ruler.end, view, proj);
2860
2861 let (Some(sndc), Some(endc)) = (start_ndc, end_ndc) else { continue };
2863
2864 let Some((csndc, cendc)) = clip_line_ndc(sndc, endc) else { continue };
2867
2868 let [sx, sy] = ndc_to_screen_px(csndc, vp_w, vp_h);
2869 let [ex, ey] = ndc_to_screen_px(cendc, vp_w, vp_h);
2870
2871 let start_on_screen = ndc_in_viewport(sndc);
2873 let end_on_screen = ndc_in_viewport(endc);
2874
2875 emit_line_quad(
2877 &mut verts,
2878 sx, sy, ex, ey,
2879 ruler.line_width_px,
2880 ruler.color,
2881 vp_w, vp_h,
2882 );
2883
2884 if ruler.end_caps {
2886 let dx = ex - sx;
2887 let dy = ey - sy;
2888 let len = (dx * dx + dy * dy).sqrt().max(0.001);
2889 let cap_half = 5.0;
2890 let px = -dy / len * cap_half;
2891 let py = dx / len * cap_half;
2892
2893 if start_on_screen {
2894 emit_line_quad(
2895 &mut verts,
2896 sx - px, sy - py,
2897 sx + px, sy + py,
2898 ruler.line_width_px,
2899 ruler.color,
2900 vp_w, vp_h,
2901 );
2902 }
2903 if end_on_screen {
2904 emit_line_quad(
2905 &mut verts,
2906 ex - px, ey - py,
2907 ex + px, ey + py,
2908 ruler.line_width_px,
2909 ruler.color,
2910 vp_w, vp_h,
2911 );
2912 }
2913 }
2914
2915 let start_world = glam::Vec3::from(ruler.start);
2918 let end_world = glam::Vec3::from(ruler.end);
2919 let distance = (end_world - start_world).length();
2920 let text = format_ruler_distance(distance, ruler.label_format.as_deref());
2921
2922 let mid_x = (sx + ex) * 0.5;
2923 let mid_y = (sy + ey) * 0.5;
2924
2925 let layout = self.resources.glyph_atlas.layout_text(
2926 &text,
2927 ruler.font_size,
2928 ruler.font,
2929 device,
2930 );
2931 let font_index = ruler.font.map_or(0, |h| h.0);
2932 let ascent = self.resources.glyph_atlas.font_ascent(font_index, ruler.font_size);
2933
2934 let lx = mid_x - layout.total_width * 0.5;
2936 let ly = mid_y - layout.height - 6.0;
2937
2938 let pad = 3.0;
2940 emit_solid_quad(
2941 &mut verts,
2942 lx - pad, ly - pad,
2943 lx + layout.total_width + pad, ly + layout.height + pad,
2944 [0.0, 0.0, 0.0, 0.55],
2945 vp_w, vp_h,
2946 );
2947
2948 for gq in &layout.quads {
2950 let gx = lx + gq.pos[0];
2951 let gy = ly + ascent + gq.pos[1];
2952 emit_textured_quad(
2953 &mut verts,
2954 gx, gy,
2955 gx + gq.size[0], gy + gq.size[1],
2956 gq.uv_min, gq.uv_max,
2957 ruler.label_color,
2958 vp_w, vp_h,
2959 );
2960 }
2961 }
2962
2963 self.resources.glyph_atlas.upload_if_dirty(queue);
2965
2966 if !verts.is_empty() {
2967 let vertex_buf =
2968 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2969 label: Some("overlay_ruler_vbuf"),
2970 contents: bytemuck::cast_slice(&verts),
2971 usage: wgpu::BufferUsages::VERTEX,
2972 });
2973 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2974 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2975 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2976 label: Some("overlay_ruler_bg"),
2977 layout: bgl,
2978 entries: &[
2979 wgpu::BindGroupEntry {
2980 binding: 0,
2981 resource: wgpu::BindingResource::TextureView(
2982 &self.resources.glyph_atlas.view,
2983 ),
2984 },
2985 wgpu::BindGroupEntry {
2986 binding: 1,
2987 resource: wgpu::BindingResource::Sampler(sampler),
2988 },
2989 ],
2990 });
2991 self.ruler_gpu_data = Some(crate::resources::LabelGpuData {
2992 vertex_buf,
2993 vertex_count: verts.len() as u32,
2994 bind_group,
2995 });
2996 }
2997 }
2998 }
2999
3000 self.loading_bar_gpu_data = None;
3004 if !frame.overlays.loading_bars.is_empty() {
3005 self.resources.ensure_overlay_text_pipeline(device);
3006 let vp_w = frame.camera.viewport_size[0];
3007 let vp_h = frame.camera.viewport_size[1];
3008 if vp_w > 0.0 && vp_h > 0.0 {
3009 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
3010
3011 for bar in &frame.overlays.loading_bars {
3012 let bar_x = vp_w * 0.5 - bar.width_px * 0.5;
3014 let bar_y = match bar.anchor {
3015 crate::renderer::types::LoadingBarAnchor::TopCenter => bar.margin_px,
3016 crate::renderer::types::LoadingBarAnchor::Center => {
3017 vp_h * 0.5 - bar.height_px * 0.5
3018 }
3019 crate::renderer::types::LoadingBarAnchor::BottomCenter => {
3020 vp_h - bar.margin_px - bar.height_px
3021 }
3022 };
3023
3024 if let Some(ref text) = bar.label {
3026 let layout = self.resources.glyph_atlas.layout_text(
3027 text,
3028 bar.font_size,
3029 None,
3030 device,
3031 );
3032 let ascent =
3033 self.resources.glyph_atlas.font_ascent(0, bar.font_size);
3034 let label_gap = 5.0;
3035 let lx = bar_x + bar.width_px * 0.5 - layout.total_width * 0.5;
3036 let ly = match bar.anchor {
3037 crate::renderer::types::LoadingBarAnchor::TopCenter => {
3038 bar_y + bar.height_px + label_gap
3039 }
3040 _ => bar_y - layout.height - label_gap,
3041 };
3042 for gq in &layout.quads {
3043 let gx = lx + gq.pos[0];
3044 let gy = ly + ascent + gq.pos[1];
3045 emit_textured_quad(
3046 &mut verts,
3047 gx, gy,
3048 gx + gq.size[0], gy + gq.size[1],
3049 gq.uv_min, gq.uv_max,
3050 bar.label_color,
3051 vp_w, vp_h,
3052 );
3053 }
3054 }
3055
3056 emit_rounded_quad(
3058 &mut verts,
3059 bar_x, bar_y,
3060 bar_x + bar.width_px, bar_y + bar.height_px,
3061 bar.corner_radius,
3062 bar.background_color,
3063 vp_w, vp_h,
3064 );
3065
3066 let fill_w = bar.width_px * bar.progress.clamp(0.0, 1.0);
3068 if fill_w > 0.5 {
3069 emit_rounded_quad(
3070 &mut verts,
3071 bar_x, bar_y,
3072 bar_x + fill_w, bar_y + bar.height_px,
3073 bar.corner_radius,
3074 bar.fill_color,
3075 vp_w, vp_h,
3076 );
3077 }
3078 }
3079
3080 self.resources.glyph_atlas.upload_if_dirty(queue);
3081
3082 if !verts.is_empty() {
3083 let vertex_buf =
3084 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3085 label: Some("loading_bar_vbuf"),
3086 contents: bytemuck::cast_slice(&verts),
3087 usage: wgpu::BufferUsages::VERTEX,
3088 });
3089 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3090 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3091 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3092 label: Some("loading_bar_bg"),
3093 layout: bgl,
3094 entries: &[
3095 wgpu::BindGroupEntry {
3096 binding: 0,
3097 resource: wgpu::BindingResource::TextureView(
3098 &self.resources.glyph_atlas.view,
3099 ),
3100 },
3101 wgpu::BindGroupEntry {
3102 binding: 1,
3103 resource: wgpu::BindingResource::Sampler(sampler),
3104 },
3105 ],
3106 });
3107 self.loading_bar_gpu_data = Some(crate::resources::LabelGpuData {
3108 vertex_buf,
3109 vertex_count: verts.len() as u32,
3110 bind_group,
3111 });
3112 }
3113 }
3114 }
3115 }
3116
3117 pub fn prepare(
3122 &mut self,
3123 device: &wgpu::Device,
3124 queue: &wgpu::Queue,
3125 frame: &FrameData,
3126 ) -> crate::renderer::stats::FrameStats {
3127 let prepare_start = std::time::Instant::now();
3128
3129 if self.ts_needs_readback {
3133 if let Some(ref stg_buf) = self.ts_staging_buf {
3134 let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3135 stg_buf.slice(..).map_async(wgpu::MapMode::Read, move |r| {
3136 let _ = tx.send(r);
3137 });
3138 device
3141 .poll(wgpu::PollType::Wait {
3142 submission_index: None,
3143 timeout: Some(std::time::Duration::from_millis(100)),
3144 })
3145 .ok();
3146 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
3147 let data = stg_buf.slice(..).get_mapped_range();
3148 let t0 = u64::from_le_bytes(data[0..8].try_into().unwrap());
3149 let t1 = u64::from_le_bytes(data[8..16].try_into().unwrap());
3150 drop(data);
3151 let gpu_ms = t1.saturating_sub(t0) as f32 * self.ts_period / 1_000_000.0;
3153 self.last_stats.gpu_frame_ms = Some(gpu_ms);
3154 }
3155 stg_buf.unmap();
3156 }
3157 self.ts_needs_readback = false;
3158 }
3159
3160 if self.indirect_readback_pending {
3164 if let Some(ref stg_buf) = self.indirect_readback_buf {
3165 let bytes = self.indirect_readback_batch_count as u64 * 20;
3166 if bytes > 0 {
3167 let (tx, rx) =
3168 std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3169 stg_buf.slice(..bytes).map_async(wgpu::MapMode::Read, move |r| {
3170 let _ = tx.send(r);
3171 });
3172 device
3173 .poll(wgpu::PollType::Wait {
3174 submission_index: None,
3175 timeout: Some(std::time::Duration::from_millis(100)),
3176 })
3177 .ok();
3178 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
3179 let data = stg_buf.slice(..bytes).get_mapped_range();
3180 let mut visible: u32 = 0;
3181 for i in 0..self.indirect_readback_batch_count as usize {
3182 let off = i * 20 + 4;
3185 let n = u32::from_le_bytes(data[off..off + 4].try_into().unwrap());
3186 visible = visible.saturating_add(n);
3187 }
3188 drop(data);
3189 self.last_stats.gpu_visible_instances = Some(visible);
3190 }
3191 stg_buf.unmap();
3192 }
3193 }
3194 self.indirect_readback_pending = false;
3195 }
3196
3197 let total_frame_ms = self
3199 .last_prepare_instant
3200 .map(|t| t.elapsed().as_secs_f32() * 1000.0)
3201 .unwrap_or(0.0);
3202
3203 let upload_bytes = self.resources.frame_upload_bytes;
3205 self.resources.frame_upload_bytes = 0;
3206
3207 let policy = self.performance_policy;
3211 let (eff_min_scale, eff_max_scale, eff_allow_shadows, eff_allow_volumes, eff_allow_effects) =
3212 match policy.preset {
3213 Some(crate::renderer::stats::QualityPreset::High) => {
3214 (1.0_f32, 1.0_f32, false, false, false)
3215 }
3216 Some(crate::renderer::stats::QualityPreset::Medium) => {
3217 (0.75_f32, 1.0_f32, true, false, true)
3218 }
3219 Some(crate::renderer::stats::QualityPreset::Low) => {
3220 (0.5_f32, 0.75_f32, true, true, true)
3221 }
3222 None => (
3223 policy.min_render_scale,
3224 policy.max_render_scale,
3225 policy.allow_shadow_reduction,
3226 policy.allow_volume_quality_reduction,
3227 policy.allow_effect_throttling,
3228 ),
3229 };
3230
3231 let in_capture = self.runtime_mode == crate::renderer::stats::RuntimeMode::Capture;
3234 if in_capture {
3235 self.current_render_scale = eff_max_scale;
3236 }
3237
3238 let hdr_active = frame.effects.post_process.enabled;
3244
3245 if !in_capture && !hdr_active && policy.preset.is_some() {
3250 self.current_render_scale =
3251 self.current_render_scale.clamp(eff_min_scale, eff_max_scale);
3252 }
3253
3254 let missed_prev = self.last_stats.missed_budget;
3263 let under_prev = !self.last_stats.missed_budget
3264 && policy
3265 .target_fps
3266 .map(|fps| {
3267 let budget = 1000.0 / fps;
3268 let sig = self
3269 .last_stats
3270 .gpu_frame_ms
3271 .unwrap_or(self.last_stats.total_frame_ms);
3272 sig < budget * 0.8
3273 })
3274 .unwrap_or(true);
3275 if in_capture {
3276 self.degradation_tier = 0;
3277 } else if !hdr_active {
3278 let at_min = !policy.allow_dynamic_resolution
3279 || self.current_render_scale <= eff_min_scale + 0.001;
3280 if missed_prev && at_min {
3281 self.degradation_tier = (self.degradation_tier + 1).min(3);
3282 } else if under_prev {
3283 self.degradation_tier = self.degradation_tier.saturating_sub(1);
3284 }
3285 }
3286
3287 self.degradation_shadows_skipped =
3290 !in_capture && self.degradation_tier >= 1 && eff_allow_shadows;
3291 self.degradation_volume_quality_reduced =
3292 !in_capture && self.degradation_tier >= 2 && eff_allow_volumes;
3293 self.degradation_effects_throttled =
3294 !in_capture && self.degradation_tier >= 3 && eff_allow_effects;
3295
3296 let (scene_fx, viewport_fx) = frame.effects.split();
3297 self.prepare_scene_internal(device, queue, frame, &scene_fx);
3298 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
3299
3300 let cpu_prepare_ms = prepare_start.elapsed().as_secs_f32() * 1000.0;
3301
3302 let budget_ms = policy.target_fps.map(|fps| 1000.0 / fps);
3303
3304 let controller_ms = self
3310 .last_stats
3311 .gpu_frame_ms
3312 .unwrap_or(total_frame_ms);
3313
3314 let missed_budget = !in_capture
3316 && budget_ms.map(|b| controller_ms > b).unwrap_or(false);
3317
3318 if policy.allow_dynamic_resolution && !in_capture && !hdr_active {
3323 if let Some(budget) = budget_ms {
3324 if controller_ms > budget {
3325 self.current_render_scale =
3327 (self.current_render_scale - 0.1).max(eff_min_scale);
3328 } else if controller_ms < budget * 0.8 {
3329 self.current_render_scale =
3331 (self.current_render_scale + 0.05).min(eff_max_scale);
3332 }
3333 }
3334 }
3335
3336 self.last_prepare_instant = Some(prepare_start);
3337 self.frame_counter = self.frame_counter.wrapping_add(1);
3338
3339 let reported_render_scale = if hdr_active { 1.0 } else { self.current_render_scale };
3342
3343 let stats = crate::renderer::stats::FrameStats {
3344 cpu_prepare_ms,
3345 gpu_frame_ms: self.last_stats.gpu_frame_ms,
3348 total_frame_ms,
3349 render_scale: reported_render_scale,
3350 missed_budget,
3351 upload_bytes,
3352 shadows_skipped: self.degradation_shadows_skipped,
3353 volume_quality_reduced: self.degradation_volume_quality_reduced,
3354 effects_throttled: self.degradation_effects_throttled,
3358 ..self.last_stats
3359 };
3360 self.last_stats = stats;
3361 stats
3362 }
3363}
3364
3365fn clip_box_outline(
3371 center: [f32; 3],
3372 half: [f32; 3],
3373 orientation: [[f32; 3]; 3],
3374 color: [f32; 4],
3375) -> PolylineItem {
3376 let ax = glam::Vec3::from(orientation[0]) * half[0];
3377 let ay = glam::Vec3::from(orientation[1]) * half[1];
3378 let az = glam::Vec3::from(orientation[2]) * half[2];
3379 let c = glam::Vec3::from(center);
3380
3381 let corners = [
3382 c - ax - ay - az,
3383 c + ax - ay - az,
3384 c + ax + ay - az,
3385 c - ax + ay - az,
3386 c - ax - ay + az,
3387 c + ax - ay + az,
3388 c + ax + ay + az,
3389 c - ax + ay + az,
3390 ];
3391 let edges: [(usize, usize); 12] = [
3392 (0, 1),
3393 (1, 2),
3394 (2, 3),
3395 (3, 0), (4, 5),
3397 (5, 6),
3398 (6, 7),
3399 (7, 4), (0, 4),
3401 (1, 5),
3402 (2, 6),
3403 (3, 7), ];
3405
3406 let mut positions = Vec::with_capacity(24);
3407 let mut strip_lengths = Vec::with_capacity(12);
3408 for (a, b) in edges {
3409 positions.push(corners[a].to_array());
3410 positions.push(corners[b].to_array());
3411 strip_lengths.push(2u32);
3412 }
3413
3414 let mut item = PolylineItem::default();
3415 item.positions = positions;
3416 item.strip_lengths = strip_lengths;
3417 item.default_color = color;
3418 item.line_width = 2.0;
3419 item
3420}
3421
3422fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
3424 let c = glam::Vec3::from(center);
3425 let segs = 64usize;
3426 let mut positions = Vec::with_capacity((segs + 1) * 3);
3427 let mut strip_lengths = Vec::with_capacity(3);
3428
3429 for axis in 0..3usize {
3430 let start = positions.len();
3431 for i in 0..=segs {
3432 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
3433 let (s, cs) = t.sin_cos();
3434 let p = c + match axis {
3435 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
3436 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
3437 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
3438 };
3439 positions.push(p.to_array());
3440 }
3441 strip_lengths.push((positions.len() - start) as u32);
3442 }
3443
3444 let mut item = PolylineItem::default();
3445 item.positions = positions;
3446 item.strip_lengths = strip_lengths;
3447 item.default_color = color;
3448 item.line_width = 2.0;
3449 item
3450}
3451
3452fn project_to_ndc(
3460 pos: [f32; 3],
3461 view: &glam::Mat4,
3462 proj: &glam::Mat4,
3463) -> Option<[f32; 2]> {
3464 let clip = *proj * *view * glam::Vec3::from(pos).extend(1.0);
3465 if clip.w <= 0.0 { return None; }
3466 Some([clip.x / clip.w, clip.y / clip.w])
3467}
3468
3469fn ndc_to_screen_px(ndc: [f32; 2], vp_w: f32, vp_h: f32) -> [f32; 2] {
3471 [
3472 (ndc[0] * 0.5 + 0.5) * vp_w,
3473 (1.0 - (ndc[1] * 0.5 + 0.5)) * vp_h,
3474 ]
3475}
3476
3477fn ndc_in_viewport(ndc: [f32; 2]) -> bool {
3479 ndc[0] >= -1.0 && ndc[0] <= 1.0 && ndc[1] >= -1.0 && ndc[1] <= 1.0
3480}
3481
3482fn clip_line_ndc(a: [f32; 2], b: [f32; 2]) -> Option<([f32; 2], [f32; 2])> {
3486 let dx = b[0] - a[0];
3487 let dy = b[1] - a[1];
3488 let mut t0 = 0.0f32;
3489 let mut t1 = 1.0f32;
3490
3491 for (p, q) in [
3493 (-dx, a[0] + 1.0),
3494 ( dx, 1.0 - a[0]),
3495 (-dy, a[1] + 1.0),
3496 ( dy, 1.0 - a[1]),
3497 ] {
3498 if p == 0.0 {
3499 if q < 0.0 { return None; }
3500 } else {
3501 let r = q / p;
3502 if p < 0.0 { t0 = t0.max(r); } else { t1 = t1.min(r); }
3503 }
3504 }
3505
3506 if t0 > t1 { return None; }
3507 Some((
3508 [a[0] + t0 * dx, a[1] + t0 * dy],
3509 [a[0] + t1 * dx, a[1] + t1 * dy],
3510 ))
3511}
3512
3513fn project_to_screen(
3516 pos: [f32; 3],
3517 view: &glam::Mat4,
3518 proj: &glam::Mat4,
3519 vp_w: f32,
3520 vp_h: f32,
3521) -> Option<[f32; 2]> {
3522 let p = glam::Vec3::from(pos);
3523 let clip = *proj * *view * p.extend(1.0);
3524 if clip.w <= 0.0 {
3525 return None;
3526 }
3527 let ndc_x = clip.x / clip.w;
3528 let ndc_y = clip.y / clip.w;
3529 if ndc_x < -1.0 || ndc_x > 1.0 || ndc_y < -1.0 || ndc_y > 1.0 {
3530 return None;
3531 }
3532 let x = (ndc_x * 0.5 + 0.5) * vp_w;
3533 let y = (1.0 - (ndc_y * 0.5 + 0.5)) * vp_h;
3534 Some([x, y])
3535}
3536
3537#[inline]
3539fn px_to_ndc(px_x: f32, px_y: f32, vp_w: f32, vp_h: f32) -> [f32; 2] {
3540 [
3541 px_x / vp_w * 2.0 - 1.0,
3542 1.0 - px_y / vp_h * 2.0,
3543 ]
3544}
3545
3546fn emit_solid_quad(
3548 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3549 x0: f32, y0: f32,
3550 x1: f32, y1: f32,
3551 color: [f32; 4],
3552 vp_w: f32, vp_h: f32,
3553) {
3554 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
3555 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
3556 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
3557 let br = px_to_ndc(x1, y1, vp_w, vp_h);
3558 let uv = [0.0, 0.0];
3559 let tex = 0.0;
3560 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
3561 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3562 };
3563 verts.extend_from_slice(&[v(tl), v(bl), v(tr), v(tr), v(bl), v(br)]);
3564}
3565
3566fn emit_textured_quad(
3568 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3569 x0: f32, y0: f32,
3570 x1: f32, y1: f32,
3571 uv_min: [f32; 2],
3572 uv_max: [f32; 2],
3573 color: [f32; 4],
3574 vp_w: f32, vp_h: f32,
3575) {
3576 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
3577 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
3578 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
3579 let br = px_to_ndc(x1, y1, vp_w, vp_h);
3580 let tex = 1.0;
3581 let v = |pos: [f32; 2], uv: [f32; 2]| crate::resources::OverlayTextVertex {
3582 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3583 };
3584 verts.extend_from_slice(&[
3586 v(tl, uv_min),
3587 v(bl, [uv_min[0], uv_max[1]]),
3588 v(tr, [uv_max[0], uv_min[1]]),
3589 v(tr, [uv_max[0], uv_min[1]]),
3590 v(bl, [uv_min[0], uv_max[1]]),
3591 v(br, uv_max),
3592 ]);
3593}
3594
3595fn emit_line_quad(
3597 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3598 x0: f32, y0: f32,
3599 x1: f32, y1: f32,
3600 thickness: f32,
3601 color: [f32; 4],
3602 vp_w: f32, vp_h: f32,
3603) {
3604 let dx = x1 - x0;
3605 let dy = y1 - y0;
3606 let len = (dx * dx + dy * dy).sqrt();
3607 if len < 0.001 {
3608 return;
3609 }
3610 let half = thickness * 0.5;
3611 let nx = -dy / len * half;
3612 let ny = dx / len * half;
3613
3614 let p0 = px_to_ndc(x0 + nx, y0 + ny, vp_w, vp_h);
3615 let p1 = px_to_ndc(x0 - nx, y0 - ny, vp_w, vp_h);
3616 let p2 = px_to_ndc(x1 + nx, y1 + ny, vp_w, vp_h);
3617 let p3 = px_to_ndc(x1 - nx, y1 - ny, vp_w, vp_h);
3618 let uv = [0.0, 0.0];
3619 let tex = 0.0;
3620 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
3621 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3622 };
3623 verts.extend_from_slice(&[v(p0), v(p1), v(p2), v(p2), v(p1), v(p3)]);
3624}
3625
3626#[inline]
3628fn apply_opacity(color: [f32; 4], opacity: f32) -> [f32; 4] {
3629 [color[0], color[1], color[2], color[3] * opacity]
3630}
3631
3632fn emit_rounded_quad(
3636 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3637 x0: f32, y0: f32,
3638 x1: f32, y1: f32,
3639 radius: f32,
3640 color: [f32; 4],
3641 vp_w: f32, vp_h: f32,
3642) {
3643 let w = x1 - x0;
3644 let h = y1 - y0;
3645 let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
3646
3647 if r < 0.5 {
3648 emit_solid_quad(verts, x0, y0, x1, y1, color, vp_w, vp_h);
3649 return;
3650 }
3651
3652 emit_solid_quad(verts, x0, y0 + r, x1, y1 - r, color, vp_w, vp_h);
3655 emit_solid_quad(verts, x0 + r, y0, x1 - r, y0 + r, color, vp_w, vp_h);
3657 emit_solid_quad(verts, x0 + r, y1 - r, x1 - r, y1, color, vp_w, vp_h);
3659
3660 let corners = [
3662 (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), ];
3667 let segments = 6;
3668 let uv = [0.0, 0.0];
3669 let tex = 0.0;
3670 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
3671 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3672 };
3673 for (cx, cy, start, end) in corners {
3674 let center = px_to_ndc(cx, cy, vp_w, vp_h);
3675 for i in 0..segments {
3676 let a0 = start + (end - start) * i as f32 / segments as f32;
3677 let a1 = start + (end - start) * (i + 1) as f32 / segments as f32;
3678 let p0 = px_to_ndc(cx + a0.cos() * r, cy + a0.sin() * r, vp_w, vp_h);
3679 let p1 = px_to_ndc(cx + a1.cos() * r, cy + a1.sin() * r, vp_w, vp_h);
3680 verts.extend_from_slice(&[v(center), v(p0), v(p1)]);
3681 }
3682 }
3683}
3684
3685fn format_ruler_distance(distance: f32, fmt: Option<&str>) -> String {
3696 let pattern = fmt.unwrap_or("{:.3}");
3697 if let Some(open) = pattern.find('{') {
3699 if let Some(close_rel) = pattern[open..].find('}') {
3700 let close = open + close_rel;
3701 let spec = &pattern[open + 1..close]; let prefix = &pattern[..open];
3703 let suffix = &pattern[close + 1..];
3704 let formatted = if let Some(prec_str) = spec.strip_prefix(":.") {
3705 let prec_str = prec_str.trim_end_matches('f');
3707 if let Ok(prec) = prec_str.parse::<usize>() {
3708 format!("{distance:.prec$}")
3709 } else {
3710 format!("{distance:.3}")
3711 }
3712 } else if spec.is_empty() || spec == ":" {
3713 format!("{distance}")
3714 } else {
3715 format!("{distance:.3}")
3716 };
3717 return format!("{prefix}{formatted}{suffix}");
3718 }
3719 }
3720 format!("{distance:.3}")
3721}