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