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