1use super::types::{ClipShape, SceneEffects, ViewportEffects};
2use super::*;
3use wgpu::util::DeviceExt;
4
5impl ViewportRenderer {
6 pub(super) fn prepare_scene_internal(
14 &mut self,
15 device: &wgpu::Device,
16 queue: &wgpu::Queue,
17 frame: &FrameData,
18 scene_fx: &SceneEffects<'_>,
19 ) {
20 if !scene_fx.compute_filter_items.is_empty() {
23 self.compute_filter_results =
24 self.resources
25 .run_compute_filters(device, queue, scene_fx.compute_filter_items);
26 } else {
27 self.compute_filter_results.clear();
28 }
29
30 self.resources.ensure_colormaps_initialized(device, queue);
32 self.resources.ensure_matcaps_initialized(device, queue);
33
34 let resources = &mut self.resources;
35 let lighting = scene_fx.lighting;
36
37 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
39 SurfaceSubmission::Flat(items) => items.as_ref(),
40 };
41
42 let (shadow_center, shadow_extent) = if let Some(extent) = lighting.shadow_extent_override {
44 (glam::Vec3::ZERO, extent)
45 } else {
46 (glam::Vec3::ZERO, 20.0)
47 };
48
49 fn compute_shadow_matrix(
51 kind: &LightKind,
52 shadow_center: glam::Vec3,
53 shadow_extent: f32,
54 ) -> glam::Mat4 {
55 match kind {
56 LightKind::Directional { direction } => {
57 let dir = glam::Vec3::from(*direction).normalize();
58 let light_up = if dir.z.abs() > 0.99 {
59 glam::Vec3::Y
60 } else {
61 glam::Vec3::Z
62 };
63 let light_pos = shadow_center + dir * shadow_extent * 2.0;
64 let light_view = glam::Mat4::look_at_rh(light_pos, shadow_center, light_up);
65 let light_proj = glam::Mat4::orthographic_rh(
66 -shadow_extent,
67 shadow_extent,
68 -shadow_extent,
69 shadow_extent,
70 0.01,
71 shadow_extent * 5.0,
72 );
73 light_proj * light_view
74 }
75 LightKind::Point { position, range } => {
76 let pos = glam::Vec3::from(*position);
77 let to_center = (shadow_center - pos).normalize();
78 let light_up = if to_center.z.abs() > 0.99 {
79 glam::Vec3::Y
80 } else {
81 glam::Vec3::Z
82 };
83 let light_view = glam::Mat4::look_at_rh(pos, shadow_center, light_up);
84 let light_proj =
85 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
86 light_proj * light_view
87 }
88 LightKind::Spot {
89 position,
90 direction,
91 range,
92 ..
93 } => {
94 let pos = glam::Vec3::from(*position);
95 let dir = glam::Vec3::from(*direction).normalize();
96 let look_target = pos + dir;
97 let up = if dir.z.abs() > 0.99 {
98 glam::Vec3::Y
99 } else {
100 glam::Vec3::Z
101 };
102 let light_view = glam::Mat4::look_at_rh(pos, look_target, up);
103 let light_proj =
104 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
105 light_proj * light_view
106 }
107 }
108 }
109
110 fn build_single_light_uniform(
112 src: &LightSource,
113 shadow_center: glam::Vec3,
114 shadow_extent: f32,
115 compute_shadow: bool,
116 ) -> SingleLightUniform {
117 let shadow_mat = if compute_shadow {
118 compute_shadow_matrix(&src.kind, shadow_center, shadow_extent)
119 } else {
120 glam::Mat4::IDENTITY
121 };
122
123 match &src.kind {
124 LightKind::Directional { direction } => SingleLightUniform {
125 light_view_proj: shadow_mat.to_cols_array_2d(),
126 pos_or_dir: *direction,
127 light_type: 0,
128 color: src.color,
129 intensity: src.intensity,
130 range: 0.0,
131 inner_angle: 0.0,
132 outer_angle: 0.0,
133 _pad_align: 0,
134 spot_direction: [0.0, -1.0, 0.0],
135 _pad: [0.0; 5],
136 },
137 LightKind::Point { position, range } => SingleLightUniform {
138 light_view_proj: shadow_mat.to_cols_array_2d(),
139 pos_or_dir: *position,
140 light_type: 1,
141 color: src.color,
142 intensity: src.intensity,
143 range: *range,
144 inner_angle: 0.0,
145 outer_angle: 0.0,
146 _pad_align: 0,
147 spot_direction: [0.0, -1.0, 0.0],
148 _pad: [0.0; 5],
149 },
150 LightKind::Spot {
151 position,
152 direction,
153 range,
154 inner_angle,
155 outer_angle,
156 } => SingleLightUniform {
157 light_view_proj: shadow_mat.to_cols_array_2d(),
158 pos_or_dir: *position,
159 light_type: 2,
160 color: src.color,
161 intensity: src.intensity,
162 range: *range,
163 inner_angle: *inner_angle,
164 outer_angle: *outer_angle,
165 _pad_align: 0,
166 spot_direction: *direction,
167 _pad: [0.0; 5],
168 },
169 }
170 }
171
172 let light_count = lighting.lights.len().min(8) as u32;
174 let mut lights_arr = [SingleLightUniform {
175 light_view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
176 pos_or_dir: [0.0; 3],
177 light_type: 0,
178 color: [1.0; 3],
179 intensity: 1.0,
180 range: 0.0,
181 inner_angle: 0.0,
182 outer_angle: 0.0,
183 _pad_align: 0,
184 spot_direction: [0.0, -1.0, 0.0],
185 _pad: [0.0; 5],
186 }; 8];
187
188 for (i, src) in lighting.lights.iter().take(8).enumerate() {
189 lights_arr[i] = build_single_light_uniform(src, shadow_center, shadow_extent, i == 0);
190 }
191
192 let cascade_count = lighting.shadow_cascade_count.clamp(1, 4) as usize;
197 let atlas_res = lighting.shadow_atlas_resolution.max(64);
198 let tile_size = atlas_res / 2;
199
200 let cascade_splits = compute_cascade_splits(
201 frame.camera.render_camera.near.max(0.01),
202 frame.camera.render_camera.far.max(1.0),
203 cascade_count as u32,
204 lighting.cascade_split_lambda,
205 );
206
207 let light_dir_for_csm = if light_count > 0 {
208 match &lighting.lights[0].kind {
209 LightKind::Directional { direction } => glam::Vec3::from(*direction).normalize(),
210 LightKind::Point { position, .. } => {
211 (glam::Vec3::from(*position) - shadow_center).normalize()
212 }
213 LightKind::Spot {
214 position,
215 direction,
216 ..
217 } => {
218 let _ = position;
219 glam::Vec3::from(*direction).normalize()
220 }
221 }
222 } else {
223 glam::Vec3::new(0.3, 1.0, 0.5).normalize()
224 };
225
226 let mut cascade_view_projs = [glam::Mat4::IDENTITY; 4];
227 let mut cascade_split_distances = [0.0f32; 4];
229
230 let use_csm = light_count > 0
232 && matches!(lighting.lights[0].kind, LightKind::Directional { .. })
233 && frame.camera.render_camera.view != glam::Mat4::IDENTITY;
234
235 if use_csm {
236 for i in 0..cascade_count {
237 let split_near = if i == 0 {
238 frame.camera.render_camera.near.max(0.01)
239 } else {
240 cascade_splits[i - 1]
241 };
242 let split_far = cascade_splits[i];
243 cascade_view_projs[i] = compute_cascade_matrix(
244 light_dir_for_csm,
245 frame.camera.render_camera.view,
246 frame.camera.render_camera.fov,
247 frame.camera.render_camera.aspect,
248 split_near,
249 split_far,
250 tile_size as f32,
251 );
252 cascade_split_distances[i] = split_far;
253 }
254 } else {
255 let primary_shadow_mat = if light_count > 0 {
257 compute_shadow_matrix(&lighting.lights[0].kind, shadow_center, shadow_extent)
258 } else {
259 glam::Mat4::IDENTITY
260 };
261 cascade_view_projs[0] = primary_shadow_mat;
262 cascade_split_distances[0] = frame.camera.render_camera.far;
263 }
264 let effective_cascade_count = if use_csm { cascade_count } else { 1 };
265
266 let atlas_rects: [[f32; 4]; 8] = [
269 [0.0, 0.0, 0.5, 0.5], [0.5, 0.0, 1.0, 0.5], [0.0, 0.5, 0.5, 1.0], [0.5, 0.5, 1.0, 1.0], [0.0; 4],
274 [0.0; 4],
275 [0.0; 4],
276 [0.0; 4], ];
278
279 {
281 let mut vp_data = [[0.0f32; 4]; 16]; for c in 0..4 {
283 let cols = cascade_view_projs[c].to_cols_array_2d();
284 for row in 0..4 {
285 vp_data[c * 4 + row] = cols[row];
286 }
287 }
288 let shadow_atlas_uniform = ShadowAtlasUniform {
289 cascade_view_proj: vp_data,
290 cascade_splits: cascade_split_distances,
291 cascade_count: effective_cascade_count as u32,
292 atlas_size: atlas_res as f32,
293 shadow_filter: match lighting.shadow_filter {
294 ShadowFilter::Pcf => 0,
295 ShadowFilter::Pcss => 1,
296 },
297 pcss_light_radius: lighting.pcss_light_radius,
298 atlas_rects,
299 };
300 queue.write_buffer(
301 &resources.shadow_info_buf,
302 0,
303 bytemuck::cast_slice(&[shadow_atlas_uniform]),
304 );
305 for slot in &self.viewport_slots {
308 queue.write_buffer(
309 &slot.shadow_info_buf,
310 0,
311 bytemuck::cast_slice(&[shadow_atlas_uniform]),
312 );
313 }
314 }
315
316 let _primary_shadow_mat = cascade_view_projs[0];
319 self.last_cascade0_shadow_mat = cascade_view_projs[0];
321
322 let (ibl_enabled, ibl_intensity, ibl_rotation, show_skybox) =
325 if let Some(env) = scene_fx.environment {
326 if resources.ibl_irradiance_view.is_some() {
327 (
328 1u32,
329 env.intensity,
330 env.rotation,
331 if env.show_skybox { 1u32 } else { 0 },
332 )
333 } else {
334 (0, 0.0, 0.0, 0)
335 }
336 } else {
337 (0, 0.0, 0.0, 0)
338 };
339
340 let lights_uniform = LightsUniform {
341 count: light_count,
342 shadow_bias: lighting.shadow_bias,
343 shadows_enabled: if lighting.shadows_enabled { 1 } else { 0 },
344 _pad: 0,
345 sky_color: lighting.sky_color,
346 hemisphere_intensity: lighting.hemisphere_intensity,
347 ground_color: lighting.ground_color,
348 _pad2: 0.0,
349 lights: lights_arr,
350 ibl_enabled,
351 ibl_intensity,
352 ibl_rotation,
353 show_skybox,
354 };
355 queue.write_buffer(
356 &resources.light_uniform_buf,
357 0,
358 bytemuck::cast_slice(&[lights_uniform]),
359 );
360
361 const SHADOW_SLOT_STRIDE: u64 = 256;
365 for c in 0..4usize {
366 queue.write_buffer(
367 &resources.shadow_uniform_buf,
368 c as u64 * SHADOW_SLOT_STRIDE,
369 bytemuck::cast_slice(&cascade_view_projs[c].to_cols_array_2d()),
370 );
371 }
372
373 let visible_count = scene_items.iter().filter(|i| i.visible).count();
376 let prev_use_instancing = self.use_instancing;
377 self.use_instancing = visible_count > INSTANCING_THRESHOLD;
378
379 if self.use_instancing != prev_use_instancing {
382 self.instanced_batches.clear();
383 self.last_scene_generation = u64::MAX;
384 self.last_scene_items_count = usize::MAX;
385 }
386
387 let has_scalar_items = scene_items.iter().any(|i| i.active_attribute.is_some());
391 let has_two_sided_items = scene_items
392 .iter()
393 .any(|i| i.material.is_two_sided());
394 let has_matcap_items = scene_items.iter().any(|i| i.material.matcap_id.is_some());
395 let has_param_vis_items = scene_items.iter().any(|i| i.material.param_vis.is_some());
396 let has_wireframe_items = scene_items.iter().any(|i| i.render_as_wireframe);
397 if !self.use_instancing
398 || frame.viewport.wireframe_mode
399 || has_scalar_items
400 || has_two_sided_items
401 || has_matcap_items
402 || has_param_vis_items
403 || has_wireframe_items
404 {
405 for item in scene_items {
406 if self.use_instancing
411 && !frame.viewport.wireframe_mode
412 && item.active_attribute.is_none()
413 && !item.material.is_two_sided()
414 && item.material.matcap_id.is_none()
415 && item.material.param_vis.is_none()
416 && !item.render_as_wireframe
417 && item.warp_attribute.is_none()
418 {
419 continue;
420 }
421
422 if resources
423 .mesh_store
424 .get(item.mesh_id)
425 .is_none()
426 {
427 tracing::warn!(
428 mesh_index = item.mesh_id.index(),
429 "scene item mesh_index invalid, skipping"
430 );
431 continue;
432 };
433 let m = &item.material;
434 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
436 let range = item
437 .scalar_range
438 .or_else(|| {
439 resources
440 .mesh_store
441 .get(item.mesh_id)
442 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
443 })
444 .unwrap_or((0.0, 1.0));
445 (1u32, range.0, range.1)
446 } else {
447 (0u32, 0.0, 1.0)
448 };
449 let obj_uniform = ObjectUniform {
450 model: item.model,
451 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
452 selected: if item.selected { 1 } else { 0 },
453 wireframe: if frame.viewport.wireframe_mode || item.render_as_wireframe { 1 } else { 0 },
454 ambient: m.ambient,
455 diffuse: m.diffuse,
456 specular: m.specular,
457 shininess: m.shininess,
458 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
459 use_pbr: if m.use_pbr { 1 } else { 0 },
460 metallic: m.metallic,
461 roughness: m.roughness,
462 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
463 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
464 has_attribute: has_attr,
465 scalar_min: s_min,
466 scalar_max: s_max,
467 _pad_scalar: 0,
468 nan_color: item.nan_color.unwrap_or([0.0; 4]),
469 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
470 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
471 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
472 unlit: if m.unlit { 1 } else { 0 },
473 use_face_color: u32::from(item.active_attribute.as_ref().map_or(false, |a| {
474 a.kind == crate::resources::AttributeKind::FaceColor
475 })),
476 uv_vis_mode: m.param_vis.map_or(0, |pv| pv.mode as u32),
477 uv_vis_scale: m.param_vis.map_or(8.0, |pv| pv.scale),
478 backface_policy: match m.backface_policy {
479 crate::scene::material::BackfacePolicy::Cull => 0,
480 crate::scene::material::BackfacePolicy::Identical => 1,
481 crate::scene::material::BackfacePolicy::DifferentColor(_) => 2,
482 crate::scene::material::BackfacePolicy::Tint(_) => 3,
483 crate::scene::material::BackfacePolicy::Pattern(cfg) => {
484 4 + cfg.pattern as u32
485 }
486 },
487 backface_color: match m.backface_policy {
488 crate::scene::material::BackfacePolicy::DifferentColor(c) => {
489 [c[0], c[1], c[2], 1.0]
490 }
491 crate::scene::material::BackfacePolicy::Tint(factor) => {
492 [factor, 0.0, 0.0, 1.0]
493 }
494 crate::scene::material::BackfacePolicy::Pattern(cfg) => {
495 let world_extent = resources
496 .mesh_store
497 .get(item.mesh_id)
498 .map(|mesh| {
499 mesh.aabb
500 .transformed(&glam::Mat4::from_cols_array_2d(&item.model))
501 .longest_side()
502 })
503 .unwrap_or(1.0)
504 .max(1e-6);
505 let world_scale = cfg.scale / world_extent;
506 [cfg.color[0], cfg.color[1], cfg.color[2], world_scale]
507 }
508 _ => [0.0; 4],
509 },
510 has_warp: if item.warp_attribute.is_some() { 1 } else { 0 },
511 warp_scale: item.warp_scale,
512 _pad_warp: [0; 2],
513 };
514
515 let normal_obj_uniform = ObjectUniform {
516 model: item.model,
517 color: [1.0, 1.0, 1.0, 1.0],
518 selected: 0,
519 wireframe: 0,
520 ambient: 0.15,
521 diffuse: 0.75,
522 specular: 0.4,
523 shininess: 32.0,
524 has_texture: 0,
525 use_pbr: 0,
526 metallic: 0.0,
527 roughness: 0.5,
528 has_normal_map: 0,
529 has_ao_map: 0,
530 has_attribute: 0,
531 scalar_min: 0.0,
532 scalar_max: 1.0,
533 _pad_scalar: 0,
534 nan_color: [0.0; 4],
535 use_nan_color: 0,
536 use_matcap: 0,
537 matcap_blendable: 0,
538 unlit: 0,
539 use_face_color: 0,
540 uv_vis_mode: 0,
541 uv_vis_scale: 8.0,
542 backface_policy: 0,
543 backface_color: [0.0; 4],
544 has_warp: 0,
545 warp_scale: 1.0,
546 _pad_warp: [0; 2],
547 };
548
549 {
551 let mesh = resources
552 .mesh_store
553 .get(item.mesh_id)
554 .unwrap();
555 queue.write_buffer(
556 &mesh.object_uniform_buf,
557 0,
558 bytemuck::cast_slice(&[obj_uniform]),
559 );
560 queue.write_buffer(
561 &mesh.normal_uniform_buf,
562 0,
563 bytemuck::cast_slice(&[normal_obj_uniform]),
564 );
565 } resources.update_mesh_texture_bind_group(
569 device,
570 item.mesh_id,
571 item.material.texture_id,
572 item.material.normal_map_id,
573 item.material.ao_map_id,
574 item.colormap_id,
575 item.active_attribute.as_ref().map(|a| a.name.as_str()),
576 item.material.matcap_id,
577 item.warp_attribute.as_deref(),
578 );
579 }
580 }
581
582 if self.use_instancing {
583 resources.ensure_instanced_pipelines(device);
584
585 let instancable_count = scene_items.iter().filter(|item| {
599 item.visible
600 && item.active_attribute.is_none()
601 && !item.material.is_two_sided()
602 && item.material.matcap_id.is_none()
603 && item.material.param_vis.is_none()
604 && resources.mesh_store.get(item.mesh_id).is_some()
605 }).count();
606 let cache_valid = instancable_count == self.last_instancable_count
607 && frame.scene.generation == self.last_scene_generation
608 && frame.interaction.selection_generation == self.last_selection_generation
609 && scene_items.len() == self.last_scene_items_count;
610
611 if !cache_valid {
612 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
614 .iter()
615 .filter(|item| {
616 item.visible
617 && item.active_attribute.is_none()
618 && !item.material.is_two_sided()
619 && item.material.matcap_id.is_none()
620 && item.material.param_vis.is_none()
621 && resources
622 .mesh_store
623 .get(item.mesh_id)
624 .is_some()
625 })
626 .collect();
627
628 sorted_items.sort_unstable_by_key(|item| {
629 (
630 item.mesh_id.index(),
631 item.material.texture_id,
632 item.material.normal_map_id,
633 item.material.ao_map_id,
634 )
635 });
636
637 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
638 let mut all_aabbs: Vec<InstanceAabb> = Vec::with_capacity(sorted_items.len());
639 let mut batch_metas: Vec<BatchMeta> = Vec::new();
640 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
641
642 if !sorted_items.is_empty() {
643 let mut batch_start = 0usize;
644 for i in 1..=sorted_items.len() {
645 let at_end = i == sorted_items.len();
646 let key_changed = !at_end && {
647 let a = sorted_items[batch_start];
648 let b = sorted_items[i];
649 a.mesh_id != b.mesh_id
650 || a.material.texture_id != b.material.texture_id
651 || a.material.normal_map_id != b.material.normal_map_id
652 || a.material.ao_map_id != b.material.ao_map_id
653 };
654
655 if at_end || key_changed {
656 let batch_items = &sorted_items[batch_start..i];
657 let rep = batch_items[0];
658 let instance_offset = all_instances.len() as u32;
659 let is_transparent = rep.material.opacity < 1.0;
660
661 for item in batch_items {
662 let m = &item.material;
663 all_instances.push(InstanceData {
664 model: item.model,
665 color: [
666 m.base_color[0],
667 m.base_color[1],
668 m.base_color[2],
669 m.opacity,
670 ],
671 selected: if item.selected { 1 } else { 0 },
672 wireframe: 0, ambient: m.ambient,
674 diffuse: m.diffuse,
675 specular: m.specular,
676 shininess: m.shininess,
677 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
678 use_pbr: if m.use_pbr { 1 } else { 0 },
679 metallic: m.metallic,
680 roughness: m.roughness,
681 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
682 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
683 unlit: if m.unlit { 1 } else { 0 },
684 _pad_inst: [0; 3],
685 });
686 }
687
688 let batch_idx = instanced_batches.len() as u32;
692 let mesh_index_count = resources
693 .mesh_store
694 .get(rep.mesh_id)
695 .map(|m| m.index_count)
696 .unwrap_or(0);
697 for item in batch_items {
698 if let Some(mesh) = resources.mesh_store.get(item.mesh_id) {
699 let model =
700 glam::Mat4::from_cols_array_2d(&item.model);
701 let world_aabb = mesh.aabb.transformed(&model);
702 all_aabbs.push(InstanceAabb {
703 min: world_aabb.min.into(),
704 batch_index: batch_idx,
705 max: world_aabb.max.into(),
706 _pad: 0,
707 });
708 }
709 }
710
711 batch_metas.push(BatchMeta {
715 index_count: mesh_index_count,
716 first_index: 0,
717 instance_offset,
718 instance_count: batch_items.len() as u32,
719 vis_offset: instance_offset,
720 is_transparent: if is_transparent { 1 } else { 0 },
721 _pad: [0, 0],
722 });
723
724 instanced_batches.push(InstancedBatch {
725 mesh_id: rep.mesh_id,
726 texture_id: rep.material.texture_id,
727 normal_map_id: rep.material.normal_map_id,
728 ao_map_id: rep.material.ao_map_id,
729 instance_offset,
730 instance_count: batch_items.len() as u32,
731 is_transparent,
732 });
733
734 batch_start = i;
735 }
736 }
737 }
738
739 self.cached_instance_data = all_instances;
740 self.cached_instanced_batches = instanced_batches;
741
742 resources.upload_instance_data(device, queue, &self.cached_instance_data);
743 resources.upload_aabb_and_batch_meta(device, queue, &all_aabbs, &batch_metas);
744
745 self.instanced_batches = self.cached_instanced_batches.clone();
746
747 self.last_scene_generation = frame.scene.generation;
748 self.last_selection_generation = frame.interaction.selection_generation;
749 self.last_scene_items_count = scene_items.len();
750 self.last_instancable_count = sorted_items.len();
751
752 for batch in &self.instanced_batches {
753 resources.get_instance_bind_group(
754 device,
755 batch.texture_id,
756 batch.normal_map_id,
757 batch.ao_map_id,
758 );
759 }
760 } else {
761 for batch in &self.instanced_batches {
762 resources.get_instance_bind_group(
763 device,
764 batch.texture_id,
765 batch.normal_map_id,
766 batch.ao_map_id,
767 );
768 }
769 }
770
771 if self.gpu_culling_enabled
778 && !self.instanced_batches.is_empty()
779 && !self.cached_instance_data.is_empty()
780 {
781 let instance_count = self.cached_instance_data.len() as u32;
782 let batch_count = self.instanced_batches.len() as u32;
783
784 if self.cull_resources.is_none() {
786 self.cull_resources =
787 Some(crate::renderer::indirect::CullResources::new(device));
788 }
789 resources.ensure_cull_instance_pipelines(device);
790 for batch in &self.instanced_batches.clone() {
791 resources.get_instance_cull_bind_group(
792 device,
793 batch.texture_id,
794 batch.normal_map_id,
795 batch.ao_map_id,
796 );
797 }
798
799 if let (
801 Some(aabb_buf),
802 Some(meta_buf),
803 Some(counter_buf),
804 Some(vis_buf),
805 Some(indirect_buf),
806 ) = (
807 resources.instance_aabb_buf.as_ref(),
808 resources.batch_meta_buf.as_ref(),
809 resources.batch_counter_buf.as_ref(),
810 resources.visibility_index_buf.as_ref(),
811 resources.indirect_args_buf.as_ref(),
812 ) {
813 let vp_mat = frame.camera.render_camera.view_proj();
815 let cpu_frustum =
816 crate::camera::frustum::Frustum::from_view_proj(&vp_mat);
817 let frustum_uniform = crate::resources::FrustumUniform {
818 planes: std::array::from_fn(|i| crate::resources::FrustumPlane {
819 normal: cpu_frustum.planes[i].normal.into(),
820 distance: cpu_frustum.planes[i].d,
821 }),
822 instance_count,
823 batch_count,
824 _pad: [0; 2],
825 };
826
827 let cull = self.cull_resources.as_ref().unwrap();
828 let mut encoder =
829 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
830 label: Some("cull_encoder"),
831 });
832 cull.dispatch(
833 &mut encoder,
834 device,
835 queue,
836 &frustum_uniform,
837 aabb_buf,
838 meta_buf,
839 counter_buf,
840 vis_buf,
841 indirect_buf,
842 instance_count,
843 batch_count,
844 );
845
846 let indirect_bytes = batch_count as u64 * 20;
849 if self
850 .indirect_readback_buf
851 .as_ref()
852 .map_or(0, |b| b.size())
853 < indirect_bytes
854 {
855 self.indirect_readback_buf =
856 Some(device.create_buffer(&wgpu::BufferDescriptor {
857 label: Some("indirect_readback_buf"),
858 size: indirect_bytes,
859 usage: wgpu::BufferUsages::COPY_DST
860 | wgpu::BufferUsages::MAP_READ,
861 mapped_at_creation: false,
862 }));
863 }
864 if let Some(ref rb_buf) = self.indirect_readback_buf {
865 encoder.copy_buffer_to_buffer(
866 indirect_buf,
867 0,
868 rb_buf,
869 0,
870 indirect_bytes,
871 );
872 }
873 queue.submit(std::iter::once(encoder.finish()));
874 self.indirect_readback_batch_count = batch_count;
875 self.indirect_readback_pending = true;
876 }
877 }
878 }
879
880 self.point_cloud_gpu_data.clear();
884 if !frame.scene.point_clouds.is_empty() {
885 resources.ensure_point_cloud_pipeline(device);
886 for item in &frame.scene.point_clouds {
887 if item.positions.is_empty() {
888 continue;
889 }
890 let gpu_data = resources.upload_point_cloud(device, queue, item);
891 self.point_cloud_gpu_data.push(gpu_data);
892 }
893 }
894
895 self.glyph_gpu_data.clear();
896 if !frame.scene.glyphs.is_empty() {
897 resources.ensure_glyph_pipeline(device);
898 for item in &frame.scene.glyphs {
899 if item.positions.is_empty() || item.vectors.is_empty() {
900 continue;
901 }
902 let gpu_data = resources.upload_glyph_set(device, queue, item);
903 self.glyph_gpu_data.push(gpu_data);
904 }
905 }
906
907 self.sprite_gpu_data.clear();
911 if !frame.scene.sprite_items.is_empty() {
912 resources.ensure_sprite_pipelines(device);
913 for item in &frame.scene.sprite_items {
914 if item.positions.is_empty() {
915 continue;
916 }
917 let gd = resources.upload_sprite(device, queue, item);
918 self.sprite_gpu_data.push(gd);
919 }
920 }
921
922 self.tensor_glyph_gpu_data.clear();
926 if !frame.scene.tensor_glyphs.is_empty() {
927 resources.ensure_tensor_glyph_pipeline(device);
928 for item in &frame.scene.tensor_glyphs {
929 if item.positions.is_empty() {
930 continue;
931 }
932 let gd = resources.upload_tensor_glyph_set(device, queue, item);
933 self.tensor_glyph_gpu_data.push(gd);
934 }
935 }
936
937 self.polyline_gpu_data.clear();
941 let vp_size = frame.camera.viewport_size;
942 if !frame.scene.polylines.is_empty() {
943 resources.ensure_polyline_pipeline(device);
944 for item in &frame.scene.polylines {
945 if item.positions.is_empty() {
946 continue;
947 }
948 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
949 self.polyline_gpu_data.push(gpu_data);
950
951 if !item.node_vectors.is_empty() {
953 resources.ensure_glyph_pipeline(device);
954 let g = crate::quantities::polyline_node_vectors_to_glyphs(item);
955 if !g.positions.is_empty() {
956 let gd = resources.upload_glyph_set(device, queue, &g);
957 self.glyph_gpu_data.push(gd);
958 }
959 }
960 if !item.edge_vectors.is_empty() {
961 resources.ensure_glyph_pipeline(device);
962 let g = crate::quantities::polyline_edge_vectors_to_glyphs(item);
963 if !g.positions.is_empty() {
964 let gd = resources.upload_glyph_set(device, queue, &g);
965 self.glyph_gpu_data.push(gd);
966 }
967 }
968 }
969 }
970
971 if !frame.scene.isolines.is_empty() {
975 resources.ensure_polyline_pipeline(device);
976 for item in &frame.scene.isolines {
977 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
978 continue;
979 }
980 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
981 if positions.is_empty() {
982 continue;
983 }
984 let polyline = PolylineItem {
985 positions,
986 scalars: Vec::new(),
987 strip_lengths,
988 scalar_range: None,
989 colormap_id: None,
990 default_color: item.color,
991 line_width: item.line_width,
992 id: 0,
993 ..Default::default()
994 };
995 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
996 self.polyline_gpu_data.push(gpu_data);
997 }
998 }
999
1000 if !frame.scene.camera_frustums.is_empty() {
1004 resources.ensure_polyline_pipeline(device);
1005 for item in &frame.scene.camera_frustums {
1006 let polyline = item.to_polyline();
1007 if !polyline.positions.is_empty() {
1008 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
1009 self.polyline_gpu_data.push(gpu_data);
1010 }
1011 }
1012 }
1013
1014 self.implicit_gpu_data.clear();
1018 if !frame.scene.gpu_implicit.is_empty() {
1019 resources.ensure_implicit_pipeline(device);
1020 for item in &frame.scene.gpu_implicit {
1021 if item.primitives.is_empty() {
1022 continue;
1023 }
1024 let gpu = resources.upload_implicit_item(device, item);
1025 self.implicit_gpu_data.push(gpu);
1026 }
1027 }
1028
1029 self.mc_gpu_data.clear();
1033 if !frame.scene.gpu_mc_jobs.is_empty() {
1034 resources.ensure_mc_pipelines(device);
1035 self.mc_gpu_data =
1036 resources.run_mc_jobs(device, queue, &frame.scene.gpu_mc_jobs);
1037 }
1038
1039 self.screen_image_gpu_data.clear();
1043 if !frame.scene.screen_images.is_empty() {
1044 resources.ensure_screen_image_pipeline(device);
1045 if frame.scene.screen_images.iter().any(|i| i.depth.is_some()) {
1047 resources.ensure_screen_image_dc_pipeline(device);
1048 }
1049 let vp_w = vp_size[0];
1050 let vp_h = vp_size[1];
1051 for item in &frame.scene.screen_images {
1052 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
1053 continue;
1054 }
1055 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
1056 self.screen_image_gpu_data.push(gpu);
1057 }
1058 }
1059
1060 self.overlay_image_gpu_data.clear();
1064 if !frame.overlays.images.is_empty() {
1065 resources.ensure_screen_image_pipeline(device);
1066 let vp_w = vp_size[0];
1067 let vp_h = vp_size[1];
1068 for item in &frame.overlays.images {
1069 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
1070 continue;
1071 }
1072 let gpu = resources.upload_overlay_image(device, queue, item, vp_w, vp_h);
1073 self.overlay_image_gpu_data.push(gpu);
1074 }
1075 }
1076
1077 self.streamtube_gpu_data.clear();
1081 if !frame.scene.streamtube_items.is_empty() {
1082 resources.ensure_streamtube_pipeline(device);
1083 for item in &frame.scene.streamtube_items {
1084 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1085 continue;
1086 }
1087 let gpu_data = resources.upload_streamtube(device, queue, item);
1088 if gpu_data.index_count > 0 {
1089 self.streamtube_gpu_data.push(gpu_data);
1090 }
1091 }
1092 }
1093
1094 self.tube_gpu_data.clear();
1098 if !frame.scene.tube_items.is_empty() {
1099 resources.ensure_streamtube_pipeline(device);
1100 for item in &frame.scene.tube_items {
1101 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1102 continue;
1103 }
1104 let gpu_data = resources.upload_tube(device, queue, item);
1105 if gpu_data.index_count > 0 {
1106 self.tube_gpu_data.push(gpu_data);
1107 }
1108 }
1109 }
1110
1111 self.ribbon_gpu_data.clear();
1115 if !frame.scene.ribbon_items.is_empty() {
1116 resources.ensure_streamtube_pipeline(device);
1117 for item in &frame.scene.ribbon_items {
1118 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1119 continue;
1120 }
1121 let gpu_data = resources.upload_ribbon(device, queue, item);
1122 if gpu_data.index_count > 0 {
1123 self.ribbon_gpu_data.push(gpu_data);
1124 }
1125 }
1126 }
1127
1128 self.image_slice_gpu_data.clear();
1132 if !frame.scene.image_slices.is_empty() {
1133 resources.ensure_image_slice_pipeline(device);
1134 for item in &frame.scene.image_slices {
1135 if let Some(gpu_data) = resources.upload_image_slice(device, queue, item) {
1136 self.image_slice_gpu_data.push(gpu_data);
1137 }
1138 }
1139 }
1140
1141 self.volume_surface_slice_gpu_data.clear();
1145 if !frame.scene.volume_surface_slices.is_empty() {
1146 resources.ensure_volume_surface_slice_pipeline(device);
1147 for item in &frame.scene.volume_surface_slices {
1148 if let Some(gpu_data) = resources.upload_volume_surface_slice(device, queue, item) {
1149 self.volume_surface_slice_gpu_data.push(gpu_data);
1150 }
1151 }
1152 }
1153
1154 self.lic_gpu_data.clear();
1158 if !frame.scene.lic_items.is_empty() {
1159 for item in &frame.scene.lic_items {
1162 if item.vector_attribute.is_empty() {
1163 continue;
1164 }
1165 if let Some(mesh) = resources.mesh_store.get(item.mesh_id) {
1166 if mesh.vector_attribute_buffers.contains_key(&item.vector_attribute) {
1168 if let Some(bgl) = &resources.lic_surface_bgl {
1169 use crate::resources::LicObjectUniform;
1170 let model = item.model;
1171 let obj_data = LicObjectUniform { model };
1172 let obj_buf = device.create_buffer(&wgpu::BufferDescriptor {
1173 label: Some("lic_object_uniform"),
1174 size: std::mem::size_of::<LicObjectUniform>() as u64,
1175 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1176 mapped_at_creation: false,
1177 });
1178 queue.write_buffer(&obj_buf, 0, bytemuck::cast_slice(&[obj_data]));
1179 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1182 label: Some("lic_surface_item_bg"),
1183 layout: bgl,
1184 entries: &[
1185 wgpu::BindGroupEntry {
1186 binding: 0,
1187 resource: obj_buf.as_entire_binding(),
1188 },
1189 ],
1190 });
1191 self.lic_gpu_data.push(crate::resources::LicSurfaceGpuData {
1192 bind_group: bg,
1193 _object_uniform_buf: obj_buf,
1194 mesh_id: item.mesh_id,
1195 vector_attribute: item.vector_attribute.clone(),
1196 });
1197 }
1198 }
1199 }
1200 }
1201 if let Some(hdr) = self.viewport_slots[frame.camera.viewport_index].hdr.as_ref() {
1203 if let Some(first) = frame.scene.lic_items.first() {
1204 let [vw, vh] = hdr.size;
1205 let u = crate::resources::LicAdvectUniform {
1206 steps: first.config.steps,
1207 step_size: first.config.step_size,
1208 vp_width: vw as f32,
1209 vp_height: vh as f32,
1210 };
1211 queue.write_buffer(&hdr.lic_uniform_buf, 0, bytemuck::cast_slice(&[u]));
1212 }
1213 }
1214 }
1215
1216 self.volume_gpu_data.clear();
1222 if !frame.scene.volumes.is_empty() {
1223 resources.ensure_volume_pipeline(device);
1224 let clip_objects_for_vol = &frame.effects.clip_objects;
1225 let vol_step_multiplier = if self.degradation_volume_quality_reduced {
1228 2.0_f32
1229 } else {
1230 1.0_f32
1231 };
1232 for item in &frame.scene.volumes {
1233 let gpu = resources.upload_volume_frame(
1234 device,
1235 queue,
1236 item,
1237 clip_objects_for_vol,
1238 vol_step_multiplier,
1239 );
1240 self.volume_gpu_data.push(gpu);
1241 }
1242 }
1243
1244 {
1246 let total = scene_items.len() as u32;
1247 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
1248 let mut draw_calls = 0u32;
1249 let mut triangles = 0u64;
1250 let instanced_batch_count = if self.use_instancing {
1251 self.instanced_batches.len() as u32
1252 } else {
1253 0
1254 };
1255
1256 if self.use_instancing {
1257 for batch in &self.instanced_batches {
1258 if let Some(mesh) = resources
1259 .mesh_store
1260 .get(batch.mesh_id)
1261 {
1262 draw_calls += 1;
1263 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
1264 }
1265 }
1266 } else {
1267 for item in scene_items {
1268 if !item.visible {
1269 continue;
1270 }
1271 if let Some(mesh) = resources
1272 .mesh_store
1273 .get(item.mesh_id)
1274 {
1275 draw_calls += 1;
1276 triangles += (mesh.index_count / 3) as u64;
1277 }
1278 }
1279 }
1280
1281 self.last_stats = crate::renderer::stats::FrameStats {
1282 total_objects: total,
1283 visible_objects: visible,
1284 culled_objects: total.saturating_sub(visible),
1285 draw_calls,
1286 instanced_batches: instanced_batch_count,
1287 triangles_submitted: triangles,
1288 shadow_draw_calls: 0, gpu_culling_active: self.gpu_culling_enabled,
1290 gpu_visible_instances: if self.gpu_culling_enabled {
1292 self.last_stats.gpu_visible_instances
1293 } else {
1294 None
1295 },
1296 ..self.last_stats
1297 };
1298 }
1299
1300 let skip_shadows = self.degradation_shadows_skipped;
1305
1306 if lighting.shadows_enabled && (skip_shadows || scene_items.is_empty()) {
1310 let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1311 label: Some("shadow_clear_encoder"),
1312 });
1313 let _ = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
1314 label: Some("shadow_clear_pass"),
1315 color_attachments: &[],
1316 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1317 view: &resources.shadow_map_view,
1318 depth_ops: Some(wgpu::Operations {
1319 load: wgpu::LoadOp::Clear(1.0),
1320 store: wgpu::StoreOp::Store,
1321 }),
1322 stencil_ops: None,
1323 }),
1324 timestamp_writes: None,
1325 occlusion_query_set: None,
1326 });
1327 queue.submit(std::iter::once(enc.finish()));
1328 }
1329
1330 if lighting.shadows_enabled && !scene_items.is_empty() && !skip_shadows {
1331 if self.gpu_culling_enabled
1341 && self.use_instancing
1342 && !self.instanced_batches.is_empty()
1343 && !self.cached_instance_data.is_empty()
1344 {
1345 if self.cull_resources.is_none() {
1347 self.cull_resources =
1348 Some(crate::renderer::indirect::CullResources::new(device));
1349 }
1350 resources.ensure_cull_instance_pipelines(device);
1351 for c in 0..effective_cascade_count {
1352 resources.get_shadow_cull_instance_bind_group(device, c);
1353 }
1354
1355 let instance_count = self.cached_instance_data.len() as u32;
1356 let batch_count = self.instanced_batches.len() as u32;
1357
1358 if let (Some(aabb_buf), Some(meta_buf), Some(counter_buf)) = (
1359 resources.instance_aabb_buf.as_ref(),
1360 resources.batch_meta_buf.as_ref(),
1361 resources.batch_counter_buf.as_ref(),
1362 ) {
1363 let cull = self.cull_resources.as_ref().unwrap();
1364 let mut shadow_cull_encoder =
1365 device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1366 label: Some("shadow_cull_encoder"),
1367 });
1368 for c in 0..effective_cascade_count {
1369 if let (Some(shadow_vis_buf), Some(shadow_indirect_buf)) = (
1370 resources.shadow_vis_bufs[c].as_ref(),
1371 resources.shadow_indirect_bufs[c].as_ref(),
1372 ) {
1373 let cpu_frustum =
1374 crate::camera::frustum::Frustum::from_view_proj(
1375 &cascade_view_projs[c],
1376 );
1377 let frustum_uniform = crate::resources::FrustumUniform {
1378 planes: std::array::from_fn(|i| crate::resources::FrustumPlane {
1379 normal: cpu_frustum.planes[i].normal.into(),
1380 distance: cpu_frustum.planes[i].d,
1381 }),
1382 instance_count,
1383 batch_count,
1384 _pad: [0; 2],
1385 };
1386 cull.dispatch_shadow(
1387 &mut shadow_cull_encoder,
1388 device,
1389 queue,
1390 c,
1391 &frustum_uniform,
1392 aabb_buf,
1393 meta_buf,
1394 counter_buf,
1395 shadow_vis_buf,
1396 shadow_indirect_buf,
1397 instance_count,
1398 batch_count,
1399 );
1400 }
1401 }
1402 queue.submit(std::iter::once(shadow_cull_encoder.finish()));
1403 }
1404 }
1405
1406 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1407 label: Some("shadow_pass_encoder"),
1408 });
1409 {
1410 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1411 label: Some("shadow_pass"),
1412 color_attachments: &[],
1413 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1414 view: &resources.shadow_map_view,
1415 depth_ops: Some(wgpu::Operations {
1416 load: wgpu::LoadOp::Clear(1.0),
1417 store: wgpu::StoreOp::Store,
1418 }),
1419 stencil_ops: None,
1420 }),
1421 timestamp_writes: None,
1422 occlusion_query_set: None,
1423 });
1424
1425 let mut shadow_draws = 0u32;
1426 let tile_px = tile_size as f32;
1427
1428 if self.use_instancing {
1429 let use_shadow_indirect = self.gpu_culling_enabled
1430 && resources.shadow_instanced_cull_pipeline.is_some()
1431 && resources.shadow_vis_bufs[0].is_some();
1432
1433 if use_shadow_indirect {
1434 for cascade in 0..effective_cascade_count {
1436 let tile_col = (cascade % 2) as f32;
1437 let tile_row = (cascade / 2) as f32;
1438 shadow_pass.set_viewport(
1439 tile_col * tile_px,
1440 tile_row * tile_px,
1441 tile_px,
1442 tile_px,
1443 0.0,
1444 1.0,
1445 );
1446 shadow_pass.set_scissor_rect(
1447 (tile_col * tile_px) as u32,
1448 (tile_row * tile_px) as u32,
1449 tile_size,
1450 tile_size,
1451 );
1452
1453 queue.write_buffer(
1455 resources.shadow_instanced_cascade_bufs[cascade]
1456 .as_ref()
1457 .expect("shadow_instanced_cascade_bufs not allocated"),
1458 0,
1459 bytemuck::cast_slice(
1460 &cascade_view_projs[cascade].to_cols_array_2d(),
1461 ),
1462 );
1463
1464 let Some(pipeline) =
1465 resources.shadow_instanced_cull_pipeline.as_ref()
1466 else {
1467 continue;
1468 };
1469 let Some(cascade_bg) =
1470 resources.shadow_instanced_cascade_bgs[cascade].as_ref()
1471 else {
1472 continue;
1473 };
1474 let Some(inst_cull_bg) =
1475 resources.shadow_cull_instance_bgs[cascade].as_ref()
1476 else {
1477 continue;
1478 };
1479 let Some(shadow_indirect_buf) =
1480 resources.shadow_indirect_bufs[cascade].as_ref()
1481 else {
1482 continue;
1483 };
1484
1485 shadow_pass.set_pipeline(pipeline);
1486 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1487 shadow_pass.set_bind_group(1, inst_cull_bg, &[]);
1488
1489 for (bi, batch) in self.instanced_batches.iter().enumerate() {
1490 if batch.is_transparent {
1491 continue;
1492 }
1493 let Some(mesh) = resources.mesh_store.get(batch.mesh_id) else {
1494 continue;
1495 };
1496 shadow_pass
1497 .set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1498 shadow_pass.set_index_buffer(
1499 mesh.index_buffer.slice(..),
1500 wgpu::IndexFormat::Uint32,
1501 );
1502 shadow_pass.draw_indexed_indirect(
1503 shadow_indirect_buf,
1504 bi as u64 * 20,
1505 );
1506 shadow_draws += 1;
1507 }
1508 }
1509 } else if let (Some(pipeline), Some(instance_bg)) = (
1510 &resources.shadow_instanced_pipeline,
1511 self.instanced_batches.first().and_then(|b| {
1512 resources.instance_bind_groups.get(&(
1513 b.texture_id.unwrap_or(u64::MAX),
1514 b.normal_map_id.unwrap_or(u64::MAX),
1515 b.ao_map_id.unwrap_or(u64::MAX),
1516 ))
1517 }),
1518 ) {
1519 for cascade in 0..effective_cascade_count {
1521 let tile_col = (cascade % 2) as f32;
1522 let tile_row = (cascade / 2) as f32;
1523 shadow_pass.set_viewport(
1524 tile_col * tile_px,
1525 tile_row * tile_px,
1526 tile_px,
1527 tile_px,
1528 0.0,
1529 1.0,
1530 );
1531 shadow_pass.set_scissor_rect(
1532 (tile_col * tile_px) as u32,
1533 (tile_row * tile_px) as u32,
1534 tile_size,
1535 tile_size,
1536 );
1537
1538 shadow_pass.set_pipeline(pipeline);
1539
1540 queue.write_buffer(
1541 resources.shadow_instanced_cascade_bufs[cascade]
1542 .as_ref()
1543 .expect("shadow_instanced_cascade_bufs not allocated"),
1544 0,
1545 bytemuck::cast_slice(
1546 &cascade_view_projs[cascade].to_cols_array_2d(),
1547 ),
1548 );
1549
1550 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
1551 .as_ref()
1552 .expect("shadow_instanced_cascade_bgs not allocated");
1553 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1554 shadow_pass.set_bind_group(1, instance_bg, &[]);
1555
1556 for batch in &self.instanced_batches {
1557 if batch.is_transparent {
1558 continue;
1559 }
1560 let Some(mesh) = resources
1561 .mesh_store
1562 .get(batch.mesh_id)
1563 else {
1564 continue;
1565 };
1566 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1567 shadow_pass.set_index_buffer(
1568 mesh.index_buffer.slice(..),
1569 wgpu::IndexFormat::Uint32,
1570 );
1571 shadow_pass.draw_indexed(
1572 0..mesh.index_count,
1573 0,
1574 batch.instance_offset
1575 ..batch.instance_offset + batch.instance_count,
1576 );
1577 shadow_draws += 1;
1578 }
1579 }
1580 }
1581 } else {
1582 for cascade in 0..effective_cascade_count {
1583 let tile_col = (cascade % 2) as f32;
1584 let tile_row = (cascade / 2) as f32;
1585 shadow_pass.set_viewport(
1586 tile_col * tile_px,
1587 tile_row * tile_px,
1588 tile_px,
1589 tile_px,
1590 0.0,
1591 1.0,
1592 );
1593 shadow_pass.set_scissor_rect(
1594 (tile_col * tile_px) as u32,
1595 (tile_row * tile_px) as u32,
1596 tile_size,
1597 tile_size,
1598 );
1599
1600 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1601 shadow_pass.set_bind_group(
1602 0,
1603 &resources.shadow_bind_group,
1604 &[cascade as u32 * 256],
1605 );
1606
1607 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1608 &cascade_view_projs[cascade],
1609 );
1610
1611 for item in scene_items.iter() {
1612 if !item.visible {
1613 continue;
1614 }
1615 if item.material.opacity < 1.0 {
1616 continue;
1617 }
1618 let Some(mesh) = resources
1619 .mesh_store
1620 .get(item.mesh_id)
1621 else {
1622 continue;
1623 };
1624
1625 let world_aabb = mesh
1626 .aabb
1627 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1628 if cascade_frustum.cull_aabb(&world_aabb) {
1629 continue;
1630 }
1631
1632 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1633 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1634 shadow_pass.set_index_buffer(
1635 mesh.index_buffer.slice(..),
1636 wgpu::IndexFormat::Uint32,
1637 );
1638 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1639 shadow_draws += 1;
1640 }
1641 }
1642 }
1643 drop(shadow_pass);
1644 self.last_stats.shadow_draw_calls = shadow_draws;
1645 }
1646 queue.submit(std::iter::once(encoder.finish()));
1647 }
1648 }
1649
1650 pub(super) fn prepare_viewport_internal(
1655 &mut self,
1656 device: &wgpu::Device,
1657 queue: &wgpu::Queue,
1658 frame: &FrameData,
1659 viewport_fx: &ViewportEffects<'_>,
1660 ) {
1661 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1664
1665 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1666 SurfaceSubmission::Flat(items) => items.as_ref(),
1667 };
1668
1669 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1671
1672 {
1673 let resources = &mut self.resources;
1674
1675 {
1677 let mut planes = [[0.0f32; 4]; 6];
1678 let mut count = 0u32;
1679 let mut clip_vols_uniform: ClipVolumesUniform = bytemuck::Zeroable::zeroed();
1680
1681 for obj in viewport_fx
1682 .clip_objects
1683 .iter()
1684 .filter(|o| o.enabled && o.clip_geometry)
1685 {
1686 match obj.shape {
1687 ClipShape::Plane {
1688 normal, distance, ..
1689 } if count < 6 => {
1690 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1691 count += 1;
1692 }
1693 ClipShape::Box {
1694 center,
1695 half_extents,
1696 orientation,
1697 } if (clip_vols_uniform.count as usize) < CLIP_VOLUME_MAX => {
1698 let idx = clip_vols_uniform.count as usize;
1699 clip_vols_uniform.volumes[idx] =
1700 ClipVolumeEntry::from_box(center, half_extents, orientation);
1701 clip_vols_uniform.count += 1;
1702 }
1703 ClipShape::Sphere { center, radius }
1704 if (clip_vols_uniform.count as usize) < CLIP_VOLUME_MAX =>
1705 {
1706 let idx = clip_vols_uniform.count as usize;
1707 clip_vols_uniform.volumes[idx] =
1708 ClipVolumeEntry::from_sphere(center, radius);
1709 clip_vols_uniform.count += 1;
1710 }
1711 ClipShape::Cylinder {
1712 center,
1713 axis,
1714 radius,
1715 half_length,
1716 } if (clip_vols_uniform.count as usize) < CLIP_VOLUME_MAX => {
1717 let idx = clip_vols_uniform.count as usize;
1718 clip_vols_uniform.volumes[idx] =
1719 ClipVolumeEntry::from_cylinder(center, axis, radius, half_length);
1720 clip_vols_uniform.count += 1;
1721 }
1722 _ => {}
1723 }
1724 }
1725
1726 let clip_uniform = ClipPlanesUniform {
1727 planes,
1728 count,
1729 _pad0: 0,
1730 viewport_width: frame.camera.viewport_size[0].max(1.0),
1731 viewport_height: frame.camera.viewport_size[1].max(1.0),
1732 };
1733 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1735 queue.write_buffer(
1736 &slot.clip_planes_buf,
1737 0,
1738 bytemuck::cast_slice(&[clip_uniform]),
1739 );
1740 queue.write_buffer(
1741 &slot.clip_volume_buf,
1742 0,
1743 bytemuck::cast_slice(&[clip_vols_uniform]),
1744 );
1745 }
1746 queue.write_buffer(
1748 &resources.clip_planes_uniform_buf,
1749 0,
1750 bytemuck::cast_slice(&[clip_uniform]),
1751 );
1752 queue.write_buffer(
1753 &resources.clip_volume_uniform_buf,
1754 0,
1755 bytemuck::cast_slice(&[clip_vols_uniform]),
1756 );
1757 }
1758
1759 let camera_uniform = frame.camera.render_camera.camera_uniform();
1761 queue.write_buffer(
1763 &resources.camera_uniform_buf,
1764 0,
1765 bytemuck::cast_slice(&[camera_uniform]),
1766 );
1767 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1769 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1770 }
1771
1772 if frame.viewport.show_grid {
1774 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1775 if !eye.is_finite() {
1776 tracing::warn!(
1777 eye_x = eye.x,
1778 eye_y = eye.y,
1779 eye_z = eye.z,
1780 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1781 );
1782 } else {
1783 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1784
1785 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1786 (frame.viewport.grid_cell_size, 1.0_f32)
1787 } else {
1788 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1789 let world_per_pixel =
1790 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1791 / frame.camera.viewport_size[1].max(1.0);
1792 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1793 let mut s = 1.0_f32;
1794 let mut iters = 0u32;
1795 while s < target {
1796 s *= 10.0;
1797 iters += 1;
1798 }
1799 let ratio = (target / s).clamp(0.0, 1.0);
1800 let fade = if ratio < 0.5 {
1801 1.0_f32
1802 } else {
1803 let t = (ratio - 0.5) * 2.0;
1804 1.0 - t * t * (3.0 - 2.0 * t)
1805 };
1806 tracing::debug!(
1807 eye_z = eye.z,
1808 vertical_depth,
1809 world_per_pixel,
1810 target,
1811 spacing = s,
1812 lod_iters = iters,
1813 ratio,
1814 minor_fade = fade,
1815 "grid LOD"
1816 );
1817 (s, fade)
1818 };
1819
1820 let spacing_major = spacing * 10.0;
1821 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1822 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1823 tracing::debug!(
1824 spacing_minor = spacing,
1825 spacing_major,
1826 snap_x,
1827 snap_y,
1828 eye_x = eye.x,
1829 eye_y = eye.y,
1830 eye_z = eye.z,
1831 "grid snap"
1832 );
1833
1834 let orient = frame.camera.render_camera.orientation;
1835 let right = orient * glam::Vec3::X;
1836 let up = orient * glam::Vec3::Y;
1837 let back = orient * glam::Vec3::Z;
1838 let cam_to_world = [
1839 [right.x, right.y, right.z, 0.0_f32],
1840 [up.x, up.y, up.z, 0.0_f32],
1841 [back.x, back.y, back.z, 0.0_f32],
1842 ];
1843 let aspect =
1844 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1845 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1846
1847 let uniform = GridUniform {
1848 view_proj: view_proj_mat,
1849 cam_to_world,
1850 tan_half_fov,
1851 aspect,
1852 _pad_ivp: [0.0; 2],
1853 eye_pos: frame.camera.render_camera.eye_position,
1854 grid_z: frame.viewport.grid_z,
1855 spacing_minor: spacing,
1856 spacing_major,
1857 snap_origin: [snap_x, snap_y],
1858 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1859 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1860 };
1861 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1863 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1864 }
1865 queue.write_buffer(
1867 &resources.grid_uniform_buf,
1868 0,
1869 bytemuck::cast_slice(&[uniform]),
1870 );
1871 }
1872 }
1873 {
1877 let gp = &viewport_fx.ground_plane;
1878 let mode_u32: u32 = match gp.mode {
1879 crate::renderer::types::GroundPlaneMode::None => 0,
1880 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1881 crate::renderer::types::GroundPlaneMode::Tile => 2,
1882 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1883 };
1884 let orient = frame.camera.render_camera.orientation;
1885 let right = orient * glam::Vec3::X;
1886 let up = orient * glam::Vec3::Y;
1887 let back = orient * glam::Vec3::Z;
1888 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1889 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1890 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1891 let gp_uniform = crate::resources::GroundPlaneUniform {
1892 view_proj: vp,
1893 cam_right: [right.x, right.y, right.z, 0.0],
1894 cam_up: [up.x, up.y, up.z, 0.0],
1895 cam_back: [back.x, back.y, back.z, 0.0],
1896 eye_pos: frame.camera.render_camera.eye_position,
1897 height: gp.height,
1898 color: gp.color,
1899 shadow_color: gp.shadow_color,
1900 light_vp: gp_cascade0_mat,
1901 tan_half_fov,
1902 aspect,
1903 tile_size: gp.tile_size,
1904 shadow_bias: 0.002,
1905 mode: mode_u32,
1906 shadow_opacity: gp.shadow_opacity,
1907 _pad: [0.0; 2],
1908 };
1909 queue.write_buffer(
1910 &resources.ground_plane_uniform_buf,
1911 0,
1912 bytemuck::cast_slice(&[gp_uniform]),
1913 );
1914 }
1915 } let vp_idx = frame.camera.viewport_index;
1924
1925 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1927 if frame.interaction.outline_selected {
1928 let resources = &self.resources;
1929 for item in scene_items {
1930 if !item.visible || !item.selected {
1931 continue;
1932 }
1933 let uniform = OutlineUniform {
1934 model: item.model,
1935 color: [0.0; 4], pixel_offset: 0.0,
1937 _pad: [0.0; 3],
1938 };
1939 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1940 label: Some("outline_mask_uniform_buf"),
1941 size: std::mem::size_of::<OutlineUniform>() as u64,
1942 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1943 mapped_at_creation: false,
1944 });
1945 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1946 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1947 label: Some("outline_mask_object_bg"),
1948 layout: &resources.outline_bind_group_layout,
1949 entries: &[wgpu::BindGroupEntry {
1950 binding: 0,
1951 resource: buf.as_entire_binding(),
1952 }],
1953 });
1954 outline_object_buffers.push(OutlineObjectBuffers {
1955 mesh_id: item.mesh_id,
1956 two_sided: item.material.is_two_sided(),
1957 _mask_uniform_buf: buf,
1958 mask_bind_group: bg,
1959 });
1960 }
1961 }
1962
1963 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1965 if frame.interaction.xray_selected {
1966 let resources = &self.resources;
1967 for item in scene_items {
1968 if !item.visible || !item.selected {
1969 continue;
1970 }
1971 let uniform = OutlineUniform {
1972 model: item.model,
1973 color: frame.interaction.xray_color,
1974 pixel_offset: 0.0,
1975 _pad: [0.0; 3],
1976 };
1977 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1978 label: Some("xray_uniform_buf"),
1979 size: std::mem::size_of::<OutlineUniform>() as u64,
1980 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1981 mapped_at_creation: false,
1982 });
1983 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1984 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1985 label: Some("xray_object_bg"),
1986 layout: &resources.outline_bind_group_layout,
1987 entries: &[wgpu::BindGroupEntry {
1988 binding: 0,
1989 resource: buf.as_entire_binding(),
1990 }],
1991 });
1992 xray_object_buffers.push((item.mesh_id, buf, bg));
1993 }
1994 }
1995
1996 let mut constraint_line_buffers = Vec::new();
1998 for overlay in &frame.interaction.constraint_overlays {
1999 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
2000 }
2001
2002 let mut clip_plane_fill_buffers = Vec::new();
2004 let mut clip_plane_line_buffers = Vec::new();
2005 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
2006 if obj.color.is_none() && obj.edge_color.is_none() {
2008 continue;
2009 }
2010 if let ClipShape::Plane {
2011 normal, distance, ..
2012 } = obj.shape
2013 {
2014 let n = glam::Vec3::from(normal);
2015 let center = n * (-distance);
2018 let active = obj.active;
2019 let hovered = obj.hovered || active;
2020
2021 let fill_color = if let Some(base_color) = obj.color {
2023 if active {
2024 [
2025 base_color[0] * 0.5,
2026 base_color[1] * 0.5,
2027 base_color[2] * 0.5,
2028 base_color[3] * 0.5,
2029 ]
2030 } else if hovered {
2031 [
2032 base_color[0] * 0.8,
2033 base_color[1] * 0.8,
2034 base_color[2] * 0.8,
2035 base_color[3] * 0.6,
2036 ]
2037 } else {
2038 [
2039 base_color[0] * 0.5,
2040 base_color[1] * 0.5,
2041 base_color[2] * 0.5,
2042 base_color[3] * 0.3,
2043 ]
2044 }
2045 } else {
2046 [0.0, 0.0, 0.0, 0.0]
2047 };
2048
2049 let border_base = obj
2051 .edge_color
2052 .or(obj.color)
2053 .unwrap_or([1.0, 1.0, 1.0, 1.0]);
2054 let border_color = if active {
2055 [border_base[0], border_base[1], border_base[2], 0.9]
2056 } else if hovered {
2057 [border_base[0], border_base[1], border_base[2], 0.8]
2058 } else {
2059 [
2060 border_base[0] * 0.9,
2061 border_base[1] * 0.9,
2062 border_base[2] * 0.9,
2063 0.6,
2064 ]
2065 };
2066
2067 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
2068 center,
2069 normal: n,
2070 extent: obj.extent,
2071 fill_color,
2072 border_color,
2073 _hovered: hovered,
2074 _active: active,
2075 };
2076 if obj.color.is_some() {
2077 clip_plane_fill_buffers.push(
2078 self.resources
2079 .create_clip_plane_fill_overlay(device, &overlay),
2080 );
2081 }
2082 clip_plane_line_buffers.push(
2083 self.resources
2084 .create_clip_plane_line_overlay(device, &overlay),
2085 );
2086 } else {
2087 let base_color = obj.color.unwrap_or([1.0, 1.0, 1.0, 1.0]);
2092 self.resources.ensure_polyline_no_clip_pipeline(device);
2093 match obj.shape {
2094 ClipShape::Box {
2095 center,
2096 half_extents,
2097 orientation,
2098 } => {
2099 let polyline =
2100 clip_box_outline(center, half_extents, orientation, base_color);
2101 let vp_size = frame.camera.viewport_size;
2102 let mut gpu = self
2103 .resources
2104 .upload_polyline(device, queue, &polyline, vp_size);
2105 gpu.skip_clip = true;
2106 self.polyline_gpu_data.push(gpu);
2107 }
2108 ClipShape::Sphere { center, radius } => {
2109 let polyline = clip_sphere_outline(center, radius, base_color);
2110 let vp_size = frame.camera.viewport_size;
2111 let mut gpu = self
2112 .resources
2113 .upload_polyline(device, queue, &polyline, vp_size);
2114 gpu.skip_clip = true;
2115 self.polyline_gpu_data.push(gpu);
2116 }
2117 ClipShape::Cylinder {
2118 center,
2119 axis,
2120 radius,
2121 half_length,
2122 } => {
2123 let polyline =
2124 clip_cylinder_outline(center, axis, radius, half_length, base_color);
2125 let vp_size = frame.camera.viewport_size;
2126 let mut gpu = self
2127 .resources
2128 .upload_polyline(device, queue, &polyline, vp_size);
2129 gpu.skip_clip = true;
2130 self.polyline_gpu_data.push(gpu);
2131 }
2132 _ => {}
2133 }
2134 }
2135 }
2136
2137 let mut cap_buffers = Vec::new();
2139 if viewport_fx.cap_fill_enabled {
2140 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
2141 if let ClipShape::Plane {
2142 normal,
2143 distance,
2144 cap_color,
2145 } = obj.shape
2146 {
2147 let plane_n = glam::Vec3::from(normal);
2148 for item in scene_items.iter().filter(|i| i.visible) {
2149 let Some(mesh) = self
2150 .resources
2151 .mesh_store
2152 .get(item.mesh_id)
2153 else {
2154 continue;
2155 };
2156 let model = glam::Mat4::from_cols_array_2d(&item.model);
2157 let world_aabb = mesh.aabb.transformed(&model);
2158 if !world_aabb.intersects_plane(plane_n, distance) {
2159 continue;
2160 }
2161 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
2162 else {
2163 continue;
2164 };
2165 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
2166 pos, idx, &model, plane_n, distance,
2167 ) {
2168 let bc = item.material.base_color;
2169 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
2170 let buf = self.resources.upload_cap_geometry(device, &cap, color);
2171 cap_buffers.push(buf);
2172 }
2173 }
2174 }
2175 }
2176 }
2177
2178 let axes_verts = if frame.viewport.show_axes_indicator
2180 && frame.camera.viewport_size[0] > 0.0
2181 && frame.camera.viewport_size[1] > 0.0
2182 {
2183 let verts = crate::widgets::axes_indicator::build_axes_geometry(
2184 frame.camera.viewport_size[0],
2185 frame.camera.viewport_size[1],
2186 frame.camera.render_camera.orientation,
2187 );
2188 if verts.is_empty() { None } else { Some(verts) }
2189 } else {
2190 None
2191 };
2192
2193 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
2195 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
2196 frame.interaction.gizmo_mode,
2197 frame.interaction.gizmo_hovered,
2198 frame.interaction.gizmo_space_orientation,
2199 );
2200 (verts, indices, model)
2201 });
2202
2203 {
2207 let slot = &mut self.viewport_slots[vp_idx];
2208 slot.outline_object_buffers = outline_object_buffers;
2209 slot.xray_object_buffers = xray_object_buffers;
2210 slot.constraint_line_buffers = constraint_line_buffers;
2211 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
2212 slot.clip_plane_line_buffers = clip_plane_line_buffers;
2213 slot.cap_buffers = cap_buffers;
2214
2215 if let Some(verts) = axes_verts {
2217 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
2218 if byte_size > slot.axes_vertex_buffer.size() {
2219 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2220 label: Some("vp_axes_vertex_buf"),
2221 size: byte_size,
2222 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2223 mapped_at_creation: false,
2224 });
2225 }
2226 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
2227 slot.axes_vertex_count = verts.len() as u32;
2228 } else {
2229 slot.axes_vertex_count = 0;
2230 }
2231
2232 if let Some((verts, indices, model)) = gizmo_update {
2234 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
2235 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
2236 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
2237 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2238 label: Some("vp_gizmo_vertex_buf"),
2239 size: vert_bytes.len() as u64,
2240 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2241 mapped_at_creation: false,
2242 });
2243 }
2244 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
2245 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2246 label: Some("vp_gizmo_index_buf"),
2247 size: idx_bytes.len() as u64,
2248 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2249 mapped_at_creation: false,
2250 });
2251 }
2252 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
2253 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
2254 slot.gizmo_index_count = indices.len() as u32;
2255 let uniform = crate::interaction::gizmo::GizmoUniform {
2256 model: model.to_cols_array_2d(),
2257 };
2258 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
2259 }
2260 }
2261
2262 if frame.interaction.outline_selected
2273 && !self.viewport_slots[vp_idx]
2274 .outline_object_buffers
2275 .is_empty()
2276 {
2277 let w = frame.camera.viewport_size[0] as u32;
2278 let h = frame.camera.viewport_size[1] as u32;
2279
2280 self.ensure_viewport_hdr(
2282 device,
2283 queue,
2284 vp_idx,
2285 w.max(1),
2286 h.max(1),
2287 frame.effects.post_process.ssaa_factor.max(1),
2288 );
2289
2290 {
2292 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
2293 let edge_uniform = OutlineEdgeUniform {
2294 color: frame.interaction.outline_color,
2295 radius: frame.interaction.outline_width_px,
2296 viewport_w: w as f32,
2297 viewport_h: h as f32,
2298 _pad: 0.0,
2299 };
2300 queue.write_buffer(
2301 &slot_hdr.outline_edge_uniform_buf,
2302 0,
2303 bytemuck::cast_slice(&[edge_uniform]),
2304 );
2305 }
2306
2307 let slot_ref = &self.viewport_slots[vp_idx];
2310 let outlines_ptr =
2311 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
2312 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
2313 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
2314 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
2315 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
2316 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
2317 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
2318 let (outlines, camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
2321 (
2322 &*outlines_ptr,
2323 &*camera_bg_ptr,
2324 &*mask_view_ptr,
2325 &*color_view_ptr,
2326 &*depth_view_ptr,
2327 &*edge_bg_ptr,
2328 )
2329 };
2330
2331 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2332 label: Some("outline_offscreen_encoder"),
2333 });
2334
2335 {
2337 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2338 label: Some("outline_mask_pass"),
2339 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2340 view: mask_view,
2341 resolve_target: None,
2342 ops: wgpu::Operations {
2343 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2344 store: wgpu::StoreOp::Store,
2345 },
2346 depth_slice: None,
2347 })],
2348 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2349 view: depth_view,
2350 depth_ops: Some(wgpu::Operations {
2351 load: wgpu::LoadOp::Clear(1.0),
2352 store: wgpu::StoreOp::Discard,
2353 }),
2354 stencil_ops: None,
2355 }),
2356 timestamp_writes: None,
2357 occlusion_query_set: None,
2358 });
2359
2360 pass.set_bind_group(0, camera_bg, &[]);
2361 for outlined in outlines {
2362 let Some(mesh) = self
2363 .resources
2364 .mesh_store
2365 .get(outlined.mesh_id)
2366 else {
2367 continue;
2368 };
2369 let pipeline = if outlined.two_sided {
2370 &self.resources.outline_mask_two_sided_pipeline
2371 } else {
2372 &self.resources.outline_mask_pipeline
2373 };
2374 pass.set_pipeline(pipeline);
2375 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
2376 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
2377 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2378 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
2379 }
2380 }
2381
2382 {
2384 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2385 label: Some("outline_edge_pass"),
2386 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2387 view: color_view,
2388 resolve_target: None,
2389 ops: wgpu::Operations {
2390 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2391 store: wgpu::StoreOp::Store,
2392 },
2393 depth_slice: None,
2394 })],
2395 depth_stencil_attachment: None,
2396 timestamp_writes: None,
2397 occlusion_query_set: None,
2398 });
2399 pass.set_pipeline(&self.resources.outline_edge_pipeline);
2400 pass.set_bind_group(0, edge_bg, &[]);
2401 pass.draw(0..3, 0..1);
2402 }
2403
2404 queue.submit(std::iter::once(encoder.finish()));
2405 }
2406
2407 {
2412 let w = frame.camera.viewport_size[0];
2413 let h = frame.camera.viewport_size[1];
2414 if let Some(sel_ref) = &frame.interaction.sub_selection {
2415 let needs_rebuild = {
2416 let slot = &self.viewport_slots[vp_idx];
2417 slot.sub_highlight_generation != sel_ref.version
2418 || slot.sub_highlight.is_none()
2419 };
2420 if needs_rebuild {
2421 self.resources.ensure_sub_highlight_pipelines(device);
2422 let data = self.resources.build_sub_highlight(
2423 device,
2424 queue,
2425 sel_ref,
2426 frame.interaction.sub_highlight_face_fill_color,
2427 frame.interaction.sub_highlight_edge_color,
2428 frame.interaction.sub_highlight_edge_width_px,
2429 frame.interaction.sub_highlight_vertex_size_px,
2430 w,
2431 h,
2432 );
2433 let slot = &mut self.viewport_slots[vp_idx];
2434 slot.sub_highlight = Some(data);
2435 slot.sub_highlight_generation = sel_ref.version;
2436 }
2437 } else {
2438 let slot = &mut self.viewport_slots[vp_idx];
2439 slot.sub_highlight = None;
2440 slot.sub_highlight_generation = u64::MAX;
2441 }
2442 }
2443
2444 self.label_gpu_data = None;
2448 if !frame.overlays.labels.is_empty() {
2449 self.resources.ensure_overlay_text_pipeline(device);
2450 let vp_w = frame.camera.viewport_size[0];
2451 let vp_h = frame.camera.viewport_size[1];
2452 if vp_w > 0.0 && vp_h > 0.0 {
2453 let view = &frame.camera.render_camera.view;
2454 let proj = &frame.camera.render_camera.projection;
2455
2456 let mut sorted_labels: Vec<&crate::renderer::types::LabelItem> =
2458 frame.overlays.labels.iter().collect();
2459 sorted_labels.sort_by_key(|l| l.z_order);
2460
2461 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2462
2463 for label in &sorted_labels {
2464 if label.text.is_empty() || label.opacity <= 0.0 {
2465 continue;
2466 }
2467
2468 let screen_pos = if let Some(sa) = label.screen_anchor {
2470 Some(sa)
2471 } else if let Some(wa) = label.world_anchor {
2472 project_to_screen(wa, view, proj, vp_w, vp_h)
2473 } else {
2474 continue;
2475 };
2476 let Some(anchor_px) = screen_pos else {
2477 continue;
2478 };
2479
2480 let opacity = label.opacity.clamp(0.0, 1.0);
2481
2482 let layout = if let Some(max_w) = label.max_width {
2484 self.resources.glyph_atlas.layout_text_wrapped(
2485 &label.text,
2486 label.font_size,
2487 label.font,
2488 max_w,
2489 device,
2490 )
2491 } else {
2492 self.resources.glyph_atlas.layout_text(
2493 &label.text,
2494 label.font_size,
2495 label.font,
2496 device,
2497 )
2498 };
2499
2500 let font_index = label.font.map_or(0, |h| h.0);
2502 let ascent = self.resources.glyph_atlas.font_ascent(font_index, label.font_size);
2503
2504 let align_offset = match label.anchor_align {
2506 crate::renderer::types::LabelAnchor::Leading => 6.0,
2507 crate::renderer::types::LabelAnchor::Center => -layout.total_width * 0.5,
2508 crate::renderer::types::LabelAnchor::Trailing => -layout.total_width - 6.0,
2509 };
2510
2511 let text_x = anchor_px[0] + align_offset + label.offset[0];
2513 let text_y = anchor_px[1] - layout.height * 0.5 + label.offset[1];
2514
2515 if label.background {
2517 let pad = label.padding;
2518 let bx0 = text_x - pad;
2519 let by0 = text_y - pad;
2520 let bx1 = text_x + layout.total_width + pad;
2521 let by1 = text_y + layout.height + pad;
2522 let bg_color = apply_opacity(label.background_color, opacity);
2523 if label.border_radius > 0.0 {
2524 emit_rounded_quad(
2525 &mut verts,
2526 bx0, by0, bx1, by1,
2527 label.border_radius,
2528 bg_color,
2529 vp_w, vp_h,
2530 );
2531 } else {
2532 emit_solid_quad(
2533 &mut verts,
2534 bx0, by0, bx1, by1,
2535 bg_color,
2536 vp_w, vp_h,
2537 );
2538 }
2539 }
2540
2541 if label.leader_line {
2543 if let Some(wa) = label.world_anchor {
2544 let world_px = project_to_screen(wa, view, proj, vp_w, vp_h);
2545 if let Some(wp) = world_px {
2546 emit_line_quad(
2547 &mut verts,
2548 wp[0], wp[1],
2549 text_x, text_y + layout.height * 0.5,
2550 1.5,
2551 apply_opacity(label.leader_color, opacity),
2552 vp_w, vp_h,
2553 );
2554 }
2555 }
2556 }
2557
2558 let text_color = apply_opacity(label.color, opacity);
2560 for gq in &layout.quads {
2561 let gx = text_x + gq.pos[0];
2562 let gy = text_y + ascent + gq.pos[1];
2563 emit_textured_quad(
2564 &mut verts,
2565 gx, gy,
2566 gx + gq.size[0], gy + gq.size[1],
2567 gq.uv_min, gq.uv_max,
2568 text_color,
2569 vp_w, vp_h,
2570 );
2571 }
2572 }
2573
2574 self.resources.glyph_atlas.upload_if_dirty(queue);
2576
2577 if !verts.is_empty() {
2578 let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2579 label: Some("overlay_label_vbuf"),
2580 contents: bytemuck::cast_slice(&verts),
2581 usage: wgpu::BufferUsages::VERTEX,
2582 });
2583 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2584 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2585 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2586 label: Some("overlay_label_bg"),
2587 layout: bgl,
2588 entries: &[
2589 wgpu::BindGroupEntry {
2590 binding: 0,
2591 resource: wgpu::BindingResource::TextureView(
2592 &self.resources.glyph_atlas.view,
2593 ),
2594 },
2595 wgpu::BindGroupEntry {
2596 binding: 1,
2597 resource: wgpu::BindingResource::Sampler(sampler),
2598 },
2599 ],
2600 });
2601 self.label_gpu_data = Some(crate::resources::LabelGpuData {
2602 vertex_buf,
2603 vertex_count: verts.len() as u32,
2604 bind_group,
2605 });
2606 }
2607 }
2608 }
2609
2610 self.scalar_bar_gpu_data = None;
2614 if !frame.overlays.scalar_bars.is_empty() {
2615 self.resources.ensure_overlay_text_pipeline(device);
2616 let vp_w = frame.camera.viewport_size[0];
2617 let vp_h = frame.camera.viewport_size[1];
2618 if vp_w > 0.0 && vp_h > 0.0 {
2619 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2620
2621 for bar in &frame.overlays.scalar_bars {
2622 let Some(lut) = self.resources.get_colormap_rgba(bar.colormap_id).map(|l| l.to_vec()) else {
2625 continue;
2626 };
2627
2628 let is_vertical = matches!(
2629 bar.orientation,
2630 crate::renderer::types::ScalarBarOrientation::Vertical
2631 );
2632 let reversed = bar.ticks_reversed;
2633
2634 let tick_fs = bar.font_size;
2636 let title_fs = bar.title_font_size.unwrap_or(bar.font_size);
2637 let font_index = bar.font.map_or(0, |h| h.0);
2638
2639 let (strip_w, strip_h) = if is_vertical {
2641 (bar.bar_width_px, bar.bar_length_px)
2642 } else {
2643 (bar.bar_length_px, bar.bar_width_px)
2644 };
2645
2646 let tick_count = bar.tick_count.max(2);
2649 let mut tick_data: Vec<(String, f32, f32)> = Vec::new(); let mut max_tick_w = 0.0f32;
2651 let mut tick_h = 0.0f32;
2652 for i in 0..tick_count {
2653 let t = i as f32 / (tick_count - 1) as f32;
2654 let value = bar.scalar_min + t * (bar.scalar_max - bar.scalar_min);
2655 let text = format!("{value:.2}");
2656 let layout = self.resources.glyph_atlas.layout_text(
2657 &text, tick_fs, bar.font, device,
2658 );
2659 max_tick_w = max_tick_w.max(layout.total_width);
2660 tick_h = layout.height;
2661 tick_data.push((text, layout.total_width, layout.height));
2662 }
2663
2664 let half_tick = tick_h / 2.0;
2669 let title_h = if bar.title.is_some() {
2670 title_fs + 4.0 + half_tick
2672 } else {
2673 half_tick
2675 };
2676
2677 let title_w = if let Some(ref t) = bar.title {
2680 self.resources.glyph_atlas.layout_text(t, title_fs, bar.font, device).total_width
2681 } else {
2682 0.0
2683 };
2684
2685 let bg_pad = 4.0;
2691 let (inset_left, inset_right) = if is_vertical {
2692 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
2693 let right_extent = 4.0 + max_tick_w + bg_pad; (title_oh + bg_pad, right_extent)
2695 } else {
2696 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
2697 let tick_oh = max_tick_w / 2.0;
2698 let side = title_oh.max(tick_oh) + bg_pad;
2699 (side, side)
2700 };
2701
2702 let bottom_overhang = if is_vertical { half_tick } else { 3.0 + tick_h };
2707
2708 let (bar_x, bar_y) = match bar.anchor {
2714 crate::renderer::types::ScalarBarAnchor::TopLeft => (
2715 bar.margin_px + inset_left,
2716 bar.margin_px + title_h + bg_pad,
2717 ),
2718 crate::renderer::types::ScalarBarAnchor::TopRight => (
2719 vp_w - bar.margin_px - strip_w - inset_right,
2720 bar.margin_px + title_h + bg_pad,
2721 ),
2722 crate::renderer::types::ScalarBarAnchor::BottomLeft => (
2723 bar.margin_px + inset_left,
2724 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
2725 ),
2726 crate::renderer::types::ScalarBarAnchor::BottomRight => (
2727 vp_w - bar.margin_px - strip_w - inset_right,
2728 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
2729 ),
2730 };
2731
2732 let (bg_x0, bg_y0, bg_x1, bg_y1) = if is_vertical {
2734 let title_right = bar_x + (strip_w + title_w) / 2.0;
2735 let ticks_right = bar_x + strip_w + 4.0 + max_tick_w;
2736 (
2737 bar_x - bg_pad - ((title_w - strip_w) / 2.0).max(0.0),
2738 bar_y - title_h - bg_pad,
2739 ticks_right.max(title_right) + bg_pad,
2740 bar_y + strip_h + half_tick + bg_pad,
2741 )
2742 } else {
2743 let title_overhang = ((title_w - strip_w) / 2.0).max(0.0);
2744 let tick_overhang = max_tick_w / 2.0;
2745 let side_pad = title_overhang.max(tick_overhang);
2746 let bottom = bar_y + strip_h + 3.0 + tick_h + bg_pad;
2747 (
2748 bar_x - bg_pad - side_pad,
2749 bar_y - title_h - bg_pad,
2750 bar_x + strip_w + bg_pad + side_pad,
2751 bottom,
2752 )
2753 };
2754 emit_rounded_quad(
2755 &mut verts,
2756 bg_x0, bg_y0, bg_x1, bg_y1,
2757 3.0,
2758 bar.background_color,
2759 vp_w, vp_h,
2760 );
2761
2762 let steps: usize = 64;
2764 for s in 0..steps {
2765 let (qx0, qy0, qx1, qy1, t) = if is_vertical {
2766 let t = if reversed {
2768 s as f32 / (steps - 1) as f32
2769 } else {
2770 1.0 - s as f32 / (steps - 1) as f32
2771 };
2772 let step_h = strip_h / steps as f32;
2773 let sy = bar_y + s as f32 * step_h;
2774 (bar_x, sy, bar_x + strip_w, sy + step_h + 0.5, t)
2775 } else {
2776 let t = if reversed {
2778 1.0 - s as f32 / (steps - 1) as f32
2779 } else {
2780 s as f32 / (steps - 1) as f32
2781 };
2782 let step_w = strip_w / steps as f32;
2783 let sx = bar_x + s as f32 * step_w;
2784 (sx, bar_y, sx + step_w + 0.5, bar_y + strip_h, t)
2785 };
2786 let lut_idx = (t * 255.0).clamp(0.0, 255.0) as usize;
2787 let [r, g, b, a] = lut[lut_idx];
2788 let color = [
2789 r as f32 / 255.0,
2790 g as f32 / 255.0,
2791 b as f32 / 255.0,
2792 a as f32 / 255.0,
2793 ];
2794 emit_solid_quad(&mut verts, qx0, qy0, qx1, qy1, color, vp_w, vp_h);
2795 }
2796
2797 let ascent = self.resources.glyph_atlas.font_ascent(font_index, tick_fs);
2799 for (i, (text, tw, th)) in tick_data.iter().enumerate() {
2800 let t = i as f32 / (tick_count - 1) as f32;
2801 let layout = self.resources.glyph_atlas.layout_text(
2802 text, tick_fs, bar.font, device,
2803 );
2804
2805 let (lx, ly) = if is_vertical {
2806 let progress = if reversed { t } else { 1.0 - t };
2811 let tick_y = bar_y + progress * strip_h;
2812 (bar_x + strip_w + 4.0, tick_y - th * 0.5)
2813 } else {
2814 let frac = if reversed { 1.0 - t } else { t };
2818 let tick_x = bar_x + frac * strip_w;
2819 (tick_x - tw * 0.5, bar_y + strip_h + 3.0)
2820 };
2821 let _ = (tw, th); for gq in &layout.quads {
2824 let gx = lx + gq.pos[0];
2825 let gy = ly + ascent + gq.pos[1];
2826 emit_textured_quad(
2827 &mut verts,
2828 gx, gy,
2829 gx + gq.size[0], gy + gq.size[1],
2830 gq.uv_min, gq.uv_max,
2831 bar.label_color,
2832 vp_w, vp_h,
2833 );
2834 }
2835 }
2836
2837 if let Some(ref title_text) = bar.title {
2839 let layout = self.resources.glyph_atlas.layout_text(
2840 title_text, title_fs, bar.font, device,
2841 );
2842 let title_ascent = self.resources.glyph_atlas.font_ascent(font_index, title_fs);
2843 let tx = bar_x + (strip_w - layout.total_width) * 0.5;
2845 let ty = bar_y - title_h;
2846 for gq in &layout.quads {
2847 let gx = tx + gq.pos[0];
2848 let gy = ty + title_ascent + gq.pos[1];
2849 emit_textured_quad(
2850 &mut verts,
2851 gx, gy,
2852 gx + gq.size[0], gy + gq.size[1],
2853 gq.uv_min, gq.uv_max,
2854 bar.label_color,
2855 vp_w, vp_h,
2856 );
2857 }
2858 }
2859 }
2860
2861 self.resources.glyph_atlas.upload_if_dirty(queue);
2863
2864 if !verts.is_empty() {
2865 let vertex_buf =
2866 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2867 label: Some("overlay_scalar_bar_vbuf"),
2868 contents: bytemuck::cast_slice(&verts),
2869 usage: wgpu::BufferUsages::VERTEX,
2870 });
2871 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2872 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2873 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2874 label: Some("overlay_scalar_bar_bg"),
2875 layout: bgl,
2876 entries: &[
2877 wgpu::BindGroupEntry {
2878 binding: 0,
2879 resource: wgpu::BindingResource::TextureView(
2880 &self.resources.glyph_atlas.view,
2881 ),
2882 },
2883 wgpu::BindGroupEntry {
2884 binding: 1,
2885 resource: wgpu::BindingResource::Sampler(sampler),
2886 },
2887 ],
2888 });
2889 self.scalar_bar_gpu_data = Some(crate::resources::LabelGpuData {
2890 vertex_buf,
2891 vertex_count: verts.len() as u32,
2892 bind_group,
2893 });
2894 }
2895 }
2896 }
2897
2898 self.ruler_gpu_data = None;
2902 if !frame.overlays.rulers.is_empty() {
2903 self.resources.ensure_overlay_text_pipeline(device);
2904 let vp_w = frame.camera.viewport_size[0];
2905 let vp_h = frame.camera.viewport_size[1];
2906 if vp_w > 0.0 && vp_h > 0.0 {
2907 let view = &frame.camera.render_camera.view;
2908 let proj = &frame.camera.render_camera.projection;
2909
2910 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2911
2912 for ruler in &frame.overlays.rulers {
2913 let start_ndc = project_to_ndc(ruler.start, view, proj);
2915 let end_ndc = project_to_ndc(ruler.end, view, proj);
2916
2917 let (Some(sndc), Some(endc)) = (start_ndc, end_ndc) else { continue };
2919
2920 let Some((csndc, cendc)) = clip_line_ndc(sndc, endc) else { continue };
2923
2924 let [sx, sy] = ndc_to_screen_px(csndc, vp_w, vp_h);
2925 let [ex, ey] = ndc_to_screen_px(cendc, vp_w, vp_h);
2926
2927 let start_on_screen = ndc_in_viewport(sndc);
2929 let end_on_screen = ndc_in_viewport(endc);
2930
2931 emit_line_quad(
2933 &mut verts,
2934 sx, sy, ex, ey,
2935 ruler.line_width_px,
2936 ruler.color,
2937 vp_w, vp_h,
2938 );
2939
2940 if ruler.end_caps {
2942 let dx = ex - sx;
2943 let dy = ey - sy;
2944 let len = (dx * dx + dy * dy).sqrt().max(0.001);
2945 let cap_half = 5.0;
2946 let px = -dy / len * cap_half;
2947 let py = dx / len * cap_half;
2948
2949 if start_on_screen {
2950 emit_line_quad(
2951 &mut verts,
2952 sx - px, sy - py,
2953 sx + px, sy + py,
2954 ruler.line_width_px,
2955 ruler.color,
2956 vp_w, vp_h,
2957 );
2958 }
2959 if end_on_screen {
2960 emit_line_quad(
2961 &mut verts,
2962 ex - px, ey - py,
2963 ex + px, ey + py,
2964 ruler.line_width_px,
2965 ruler.color,
2966 vp_w, vp_h,
2967 );
2968 }
2969 }
2970
2971 let start_world = glam::Vec3::from(ruler.start);
2974 let end_world = glam::Vec3::from(ruler.end);
2975 let distance = (end_world - start_world).length();
2976 let text = format_ruler_distance(distance, ruler.label_format.as_deref());
2977
2978 let mid_x = (sx + ex) * 0.5;
2979 let mid_y = (sy + ey) * 0.5;
2980
2981 let layout = self.resources.glyph_atlas.layout_text(
2982 &text,
2983 ruler.font_size,
2984 ruler.font,
2985 device,
2986 );
2987 let font_index = ruler.font.map_or(0, |h| h.0);
2988 let ascent = self.resources.glyph_atlas.font_ascent(font_index, ruler.font_size);
2989
2990 let lx = mid_x - layout.total_width * 0.5;
2992 let ly = mid_y - layout.height - 6.0;
2993
2994 let pad = 3.0;
2996 emit_solid_quad(
2997 &mut verts,
2998 lx - pad, ly - pad,
2999 lx + layout.total_width + pad, ly + layout.height + pad,
3000 [0.0, 0.0, 0.0, 0.55],
3001 vp_w, vp_h,
3002 );
3003
3004 for gq in &layout.quads {
3006 let gx = lx + gq.pos[0];
3007 let gy = ly + ascent + gq.pos[1];
3008 emit_textured_quad(
3009 &mut verts,
3010 gx, gy,
3011 gx + gq.size[0], gy + gq.size[1],
3012 gq.uv_min, gq.uv_max,
3013 ruler.label_color,
3014 vp_w, vp_h,
3015 );
3016 }
3017 }
3018
3019 self.resources.glyph_atlas.upload_if_dirty(queue);
3021
3022 if !verts.is_empty() {
3023 let vertex_buf =
3024 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3025 label: Some("overlay_ruler_vbuf"),
3026 contents: bytemuck::cast_slice(&verts),
3027 usage: wgpu::BufferUsages::VERTEX,
3028 });
3029 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3030 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3031 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3032 label: Some("overlay_ruler_bg"),
3033 layout: bgl,
3034 entries: &[
3035 wgpu::BindGroupEntry {
3036 binding: 0,
3037 resource: wgpu::BindingResource::TextureView(
3038 &self.resources.glyph_atlas.view,
3039 ),
3040 },
3041 wgpu::BindGroupEntry {
3042 binding: 1,
3043 resource: wgpu::BindingResource::Sampler(sampler),
3044 },
3045 ],
3046 });
3047 self.ruler_gpu_data = Some(crate::resources::LabelGpuData {
3048 vertex_buf,
3049 vertex_count: verts.len() as u32,
3050 bind_group,
3051 });
3052 }
3053 }
3054 }
3055
3056 self.loading_bar_gpu_data = None;
3060 if !frame.overlays.loading_bars.is_empty() {
3061 self.resources.ensure_overlay_text_pipeline(device);
3062 let vp_w = frame.camera.viewport_size[0];
3063 let vp_h = frame.camera.viewport_size[1];
3064 if vp_w > 0.0 && vp_h > 0.0 {
3065 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
3066
3067 for bar in &frame.overlays.loading_bars {
3068 let bar_x = vp_w * 0.5 - bar.width_px * 0.5;
3070 let bar_y = match bar.anchor {
3071 crate::renderer::types::LoadingBarAnchor::TopCenter => bar.margin_px,
3072 crate::renderer::types::LoadingBarAnchor::Center => {
3073 vp_h * 0.5 - bar.height_px * 0.5
3074 }
3075 crate::renderer::types::LoadingBarAnchor::BottomCenter => {
3076 vp_h - bar.margin_px - bar.height_px
3077 }
3078 };
3079
3080 if let Some(ref text) = bar.label {
3082 let layout = self.resources.glyph_atlas.layout_text(
3083 text,
3084 bar.font_size,
3085 None,
3086 device,
3087 );
3088 let ascent =
3089 self.resources.glyph_atlas.font_ascent(0, bar.font_size);
3090 let label_gap = 5.0;
3091 let lx = bar_x + bar.width_px * 0.5 - layout.total_width * 0.5;
3092 let ly = match bar.anchor {
3093 crate::renderer::types::LoadingBarAnchor::TopCenter => {
3094 bar_y + bar.height_px + label_gap
3095 }
3096 _ => bar_y - layout.height - label_gap,
3097 };
3098 for gq in &layout.quads {
3099 let gx = lx + gq.pos[0];
3100 let gy = ly + ascent + gq.pos[1];
3101 emit_textured_quad(
3102 &mut verts,
3103 gx, gy,
3104 gx + gq.size[0], gy + gq.size[1],
3105 gq.uv_min, gq.uv_max,
3106 bar.label_color,
3107 vp_w, vp_h,
3108 );
3109 }
3110 }
3111
3112 emit_rounded_quad(
3114 &mut verts,
3115 bar_x, bar_y,
3116 bar_x + bar.width_px, bar_y + bar.height_px,
3117 bar.corner_radius,
3118 bar.background_color,
3119 vp_w, vp_h,
3120 );
3121
3122 let fill_w = bar.width_px * bar.progress.clamp(0.0, 1.0);
3124 if fill_w > 0.5 {
3125 emit_rounded_quad(
3126 &mut verts,
3127 bar_x, bar_y,
3128 bar_x + fill_w, bar_y + bar.height_px,
3129 bar.corner_radius,
3130 bar.fill_color,
3131 vp_w, vp_h,
3132 );
3133 }
3134 }
3135
3136 self.resources.glyph_atlas.upload_if_dirty(queue);
3137
3138 if !verts.is_empty() {
3139 let vertex_buf =
3140 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3141 label: Some("loading_bar_vbuf"),
3142 contents: bytemuck::cast_slice(&verts),
3143 usage: wgpu::BufferUsages::VERTEX,
3144 });
3145 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
3146 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
3147 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3148 label: Some("loading_bar_bg"),
3149 layout: bgl,
3150 entries: &[
3151 wgpu::BindGroupEntry {
3152 binding: 0,
3153 resource: wgpu::BindingResource::TextureView(
3154 &self.resources.glyph_atlas.view,
3155 ),
3156 },
3157 wgpu::BindGroupEntry {
3158 binding: 1,
3159 resource: wgpu::BindingResource::Sampler(sampler),
3160 },
3161 ],
3162 });
3163 self.loading_bar_gpu_data = Some(crate::resources::LabelGpuData {
3164 vertex_buf,
3165 vertex_count: verts.len() as u32,
3166 bind_group,
3167 });
3168 }
3169 }
3170 }
3171
3172 self.gaussian_splat_draw_data.clear();
3176 if !frame.scene.gaussian_splats.is_empty() {
3177 self.resources.ensure_gaussian_splat_pipelines(device);
3178 let vp_idx = frame.camera.viewport_index;
3179 let eye = frame.camera.render_camera.eye_position;
3180 let vp_w = frame.camera.viewport_size[0].max(1.0);
3181 let vp_h = frame.camera.viewport_size[1].max(1.0);
3182 for item in &frame.scene.gaussian_splats {
3183 let store_index = item.id.0;
3184 if self.resources.gaussian_splat_store.get(store_index).is_none() {
3185 continue;
3186 }
3187 let sh_degree = self
3188 .resources
3189 .gaussian_splat_store
3190 .get(store_index)
3191 .unwrap()
3192 .sh_degree;
3193 let count = self
3194 .resources
3195 .gaussian_splat_store
3196 .get(store_index)
3197 .unwrap()
3198 .count;
3199 self.resources.run_gaussian_splat_sort(
3200 device,
3201 queue,
3202 store_index,
3203 vp_idx,
3204 eye,
3205 item.model,
3206 vp_w,
3207 vp_h,
3208 sh_degree,
3209 );
3210 self.gaussian_splat_draw_data.push(
3211 crate::resources::GaussianSplatDrawData {
3212 store_index,
3213 viewport_index: vp_idx,
3214 model: item.model,
3215 count,
3216 },
3217 );
3218 }
3219 }
3220 }
3221
3222 pub fn prepare(
3227 &mut self,
3228 device: &wgpu::Device,
3229 queue: &wgpu::Queue,
3230 frame: &FrameData,
3231 ) -> crate::renderer::stats::FrameStats {
3232 let prepare_start = std::time::Instant::now();
3233
3234 if self.ts_needs_readback {
3238 if let Some(ref stg_buf) = self.ts_staging_buf {
3239 let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3240 stg_buf.slice(..).map_async(wgpu::MapMode::Read, move |r| {
3241 let _ = tx.send(r);
3242 });
3243 device
3246 .poll(wgpu::PollType::Wait {
3247 submission_index: None,
3248 timeout: Some(std::time::Duration::from_millis(100)),
3249 })
3250 .ok();
3251 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
3252 let data = stg_buf.slice(..).get_mapped_range();
3253 let t0 = u64::from_le_bytes(data[0..8].try_into().unwrap());
3254 let t1 = u64::from_le_bytes(data[8..16].try_into().unwrap());
3255 drop(data);
3256 let gpu_ms = t1.saturating_sub(t0) as f32 * self.ts_period / 1_000_000.0;
3258 self.last_stats.gpu_frame_ms = Some(gpu_ms);
3259 }
3260 stg_buf.unmap();
3261 }
3262 self.ts_needs_readback = false;
3263 }
3264
3265 if self.indirect_readback_pending {
3269 if let Some(ref stg_buf) = self.indirect_readback_buf {
3270 let bytes = self.indirect_readback_batch_count as u64 * 20;
3271 if bytes > 0 {
3272 let (tx, rx) =
3273 std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
3274 stg_buf.slice(..bytes).map_async(wgpu::MapMode::Read, move |r| {
3275 let _ = tx.send(r);
3276 });
3277 device
3278 .poll(wgpu::PollType::Wait {
3279 submission_index: None,
3280 timeout: Some(std::time::Duration::from_millis(100)),
3281 })
3282 .ok();
3283 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
3284 let data = stg_buf.slice(..bytes).get_mapped_range();
3285 let mut visible: u32 = 0;
3286 for i in 0..self.indirect_readback_batch_count as usize {
3287 let off = i * 20 + 4;
3290 let n = u32::from_le_bytes(data[off..off + 4].try_into().unwrap());
3291 visible = visible.saturating_add(n);
3292 }
3293 drop(data);
3294 self.last_stats.gpu_visible_instances = Some(visible);
3295 }
3296 stg_buf.unmap();
3297 }
3298 }
3299 self.indirect_readback_pending = false;
3300 }
3301
3302 let total_frame_ms = self
3304 .last_prepare_instant
3305 .map(|t| t.elapsed().as_secs_f32() * 1000.0)
3306 .unwrap_or(0.0);
3307
3308 let upload_bytes = self.resources.frame_upload_bytes;
3310 self.resources.frame_upload_bytes = 0;
3311
3312 let policy = self.performance_policy;
3316 let (eff_min_scale, eff_max_scale, eff_allow_shadows, eff_allow_volumes, eff_allow_effects) =
3317 match policy.preset {
3318 Some(crate::renderer::stats::QualityPreset::High) => {
3319 (1.0_f32, 1.0_f32, false, false, false)
3320 }
3321 Some(crate::renderer::stats::QualityPreset::Medium) => {
3322 (0.75_f32, 1.0_f32, true, false, true)
3323 }
3324 Some(crate::renderer::stats::QualityPreset::Low) => {
3325 (0.5_f32, 0.75_f32, true, true, true)
3326 }
3327 None => (
3328 policy.min_render_scale,
3329 policy.max_render_scale,
3330 policy.allow_shadow_reduction,
3331 policy.allow_volume_quality_reduction,
3332 policy.allow_effect_throttling,
3333 ),
3334 };
3335
3336 let in_capture = self.runtime_mode == crate::renderer::stats::RuntimeMode::Capture;
3339 if in_capture {
3340 self.current_render_scale = eff_max_scale;
3341 }
3342
3343 let hdr_active = frame.effects.post_process.enabled;
3349
3350 if !in_capture && !hdr_active && policy.preset.is_some() {
3355 self.current_render_scale =
3356 self.current_render_scale.clamp(eff_min_scale, eff_max_scale);
3357 }
3358
3359 let missed_prev = self.last_stats.missed_budget;
3368 let under_prev = !self.last_stats.missed_budget
3369 && policy
3370 .target_fps
3371 .map(|fps| {
3372 let budget = 1000.0 / fps;
3373 let sig = self
3374 .last_stats
3375 .gpu_frame_ms
3376 .unwrap_or(self.last_stats.total_frame_ms);
3377 sig < budget * 0.8
3378 })
3379 .unwrap_or(true);
3380 if in_capture {
3381 self.degradation_tier = 0;
3382 } else if !hdr_active {
3383 let at_min = !policy.allow_dynamic_resolution
3384 || self.current_render_scale <= eff_min_scale + 0.001;
3385 if missed_prev && at_min {
3386 self.degradation_tier = (self.degradation_tier + 1).min(3);
3387 } else if under_prev {
3388 self.degradation_tier = self.degradation_tier.saturating_sub(1);
3389 }
3390 }
3391
3392 self.degradation_shadows_skipped =
3395 !in_capture && self.degradation_tier >= 1 && eff_allow_shadows;
3396 self.degradation_volume_quality_reduced =
3397 !in_capture && self.degradation_tier >= 2 && eff_allow_volumes;
3398 self.degradation_effects_throttled =
3399 !in_capture && self.degradation_tier >= 3 && eff_allow_effects;
3400
3401 let (scene_fx, viewport_fx) = frame.effects.split();
3402 self.prepare_scene_internal(device, queue, frame, &scene_fx);
3403 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
3404
3405 let cpu_prepare_ms = prepare_start.elapsed().as_secs_f32() * 1000.0;
3406
3407 let budget_ms = policy.target_fps.map(|fps| 1000.0 / fps);
3408
3409 let controller_ms = self
3415 .last_stats
3416 .gpu_frame_ms
3417 .unwrap_or(total_frame_ms);
3418
3419 let missed_budget = !in_capture
3421 && budget_ms.map(|b| controller_ms > b).unwrap_or(false);
3422
3423 if policy.allow_dynamic_resolution && !in_capture && !hdr_active {
3428 if let Some(budget) = budget_ms {
3429 if controller_ms > budget {
3430 self.current_render_scale =
3432 (self.current_render_scale - 0.1).max(eff_min_scale);
3433 } else if controller_ms < budget * 0.8 {
3434 self.current_render_scale =
3436 (self.current_render_scale + 0.05).min(eff_max_scale);
3437 }
3438 }
3439 }
3440
3441 self.last_prepare_instant = Some(prepare_start);
3442 self.frame_counter = self.frame_counter.wrapping_add(1);
3443
3444 let reported_render_scale = if hdr_active { 1.0 } else { self.current_render_scale };
3447
3448 let stats = crate::renderer::stats::FrameStats {
3449 cpu_prepare_ms,
3450 gpu_frame_ms: self.last_stats.gpu_frame_ms,
3453 total_frame_ms,
3454 render_scale: reported_render_scale,
3455 missed_budget,
3456 upload_bytes,
3457 shadows_skipped: self.degradation_shadows_skipped,
3458 volume_quality_reduced: self.degradation_volume_quality_reduced,
3459 effects_throttled: self.degradation_effects_throttled,
3463 ..self.last_stats
3464 };
3465 self.last_stats = stats;
3466 stats
3467 }
3468}
3469
3470fn clip_box_outline(
3476 center: [f32; 3],
3477 half: [f32; 3],
3478 orientation: [[f32; 3]; 3],
3479 color: [f32; 4],
3480) -> PolylineItem {
3481 let ax = glam::Vec3::from(orientation[0]) * half[0];
3482 let ay = glam::Vec3::from(orientation[1]) * half[1];
3483 let az = glam::Vec3::from(orientation[2]) * half[2];
3484 let c = glam::Vec3::from(center);
3485
3486 let corners = [
3487 c - ax - ay - az,
3488 c + ax - ay - az,
3489 c + ax + ay - az,
3490 c - ax + ay - az,
3491 c - ax - ay + az,
3492 c + ax - ay + az,
3493 c + ax + ay + az,
3494 c - ax + ay + az,
3495 ];
3496 let edges: [(usize, usize); 12] = [
3497 (0, 1),
3498 (1, 2),
3499 (2, 3),
3500 (3, 0), (4, 5),
3502 (5, 6),
3503 (6, 7),
3504 (7, 4), (0, 4),
3506 (1, 5),
3507 (2, 6),
3508 (3, 7), ];
3510
3511 let mut positions = Vec::with_capacity(24);
3512 let mut strip_lengths = Vec::with_capacity(12);
3513 for (a, b) in edges {
3514 positions.push(corners[a].to_array());
3515 positions.push(corners[b].to_array());
3516 strip_lengths.push(2u32);
3517 }
3518
3519 let mut item = PolylineItem::default();
3520 item.positions = positions;
3521 item.strip_lengths = strip_lengths;
3522 item.default_color = color;
3523 item.line_width = 2.0;
3524 item
3525}
3526
3527fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
3529 let c = glam::Vec3::from(center);
3530 let segs = 64usize;
3531 let mut positions = Vec::with_capacity((segs + 1) * 3);
3532 let mut strip_lengths = Vec::with_capacity(3);
3533
3534 for axis in 0..3usize {
3535 let start = positions.len();
3536 for i in 0..=segs {
3537 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
3538 let (s, cs) = t.sin_cos();
3539 let p = c + match axis {
3540 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
3541 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
3542 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
3543 };
3544 positions.push(p.to_array());
3545 }
3546 strip_lengths.push((positions.len() - start) as u32);
3547 }
3548
3549 let mut item = PolylineItem::default();
3550 item.positions = positions;
3551 item.strip_lengths = strip_lengths;
3552 item.default_color = color;
3553 item.line_width = 2.0;
3554 item
3555}
3556
3557fn clip_cylinder_outline(
3559 center: [f32; 3],
3560 axis: [f32; 3],
3561 radius: f32,
3562 half_length: f32,
3563 color: [f32; 4],
3564) -> PolylineItem {
3565 let c = glam::Vec3::from(center);
3566 let ax = glam::Vec3::from(axis).normalize();
3567
3568 let ref_v = if ax.y.abs() < 0.99 {
3570 glam::Vec3::Y
3571 } else {
3572 glam::Vec3::X
3573 };
3574 let perp_u = ref_v.cross(ax).normalize();
3575 let perp_v = ax.cross(perp_u);
3576
3577 let segs = 32usize;
3578 let long_lines = 8usize;
3579 let cap_verts = segs + 1;
3580 let total_cap = cap_verts * 2 + long_lines * 2;
3581 let mut positions = Vec::with_capacity(total_cap);
3582 let mut strip_lengths = Vec::with_capacity(2 + long_lines);
3583
3584 for sign in [-1.0f32, 1.0] {
3586 let cap_center = c + ax * (sign * half_length);
3587 let start = positions.len();
3588 for i in 0..=segs {
3589 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
3590 let (s, cs) = t.sin_cos();
3591 let p = cap_center + perp_u * (cs * radius) + perp_v * (s * radius);
3592 positions.push(p.to_array());
3593 }
3594 strip_lengths.push((positions.len() - start) as u32);
3595 }
3596
3597 for i in 0..long_lines {
3599 let t = i as f32 / long_lines as f32 * std::f32::consts::TAU;
3600 let (s, cs) = t.sin_cos();
3601 let offset = perp_u * (cs * radius) + perp_v * (s * radius);
3602 positions.push((c + ax * (-half_length) + offset).to_array());
3603 positions.push((c + ax * half_length + offset).to_array());
3604 strip_lengths.push(2);
3605 }
3606
3607 let mut item = PolylineItem::default();
3608 item.positions = positions;
3609 item.strip_lengths = strip_lengths;
3610 item.default_color = color;
3611 item.line_width = 2.0;
3612 item
3613}
3614
3615fn project_to_ndc(
3623 pos: [f32; 3],
3624 view: &glam::Mat4,
3625 proj: &glam::Mat4,
3626) -> Option<[f32; 2]> {
3627 let clip = *proj * *view * glam::Vec3::from(pos).extend(1.0);
3628 if clip.w <= 0.0 { return None; }
3629 Some([clip.x / clip.w, clip.y / clip.w])
3630}
3631
3632fn ndc_to_screen_px(ndc: [f32; 2], vp_w: f32, vp_h: f32) -> [f32; 2] {
3634 [
3635 (ndc[0] * 0.5 + 0.5) * vp_w,
3636 (1.0 - (ndc[1] * 0.5 + 0.5)) * vp_h,
3637 ]
3638}
3639
3640fn ndc_in_viewport(ndc: [f32; 2]) -> bool {
3642 ndc[0] >= -1.0 && ndc[0] <= 1.0 && ndc[1] >= -1.0 && ndc[1] <= 1.0
3643}
3644
3645fn clip_line_ndc(a: [f32; 2], b: [f32; 2]) -> Option<([f32; 2], [f32; 2])> {
3649 let dx = b[0] - a[0];
3650 let dy = b[1] - a[1];
3651 let mut t0 = 0.0f32;
3652 let mut t1 = 1.0f32;
3653
3654 for (p, q) in [
3656 (-dx, a[0] + 1.0),
3657 ( dx, 1.0 - a[0]),
3658 (-dy, a[1] + 1.0),
3659 ( dy, 1.0 - a[1]),
3660 ] {
3661 if p == 0.0 {
3662 if q < 0.0 { return None; }
3663 } else {
3664 let r = q / p;
3665 if p < 0.0 { t0 = t0.max(r); } else { t1 = t1.min(r); }
3666 }
3667 }
3668
3669 if t0 > t1 { return None; }
3670 Some((
3671 [a[0] + t0 * dx, a[1] + t0 * dy],
3672 [a[0] + t1 * dx, a[1] + t1 * dy],
3673 ))
3674}
3675
3676fn project_to_screen(
3679 pos: [f32; 3],
3680 view: &glam::Mat4,
3681 proj: &glam::Mat4,
3682 vp_w: f32,
3683 vp_h: f32,
3684) -> Option<[f32; 2]> {
3685 let p = glam::Vec3::from(pos);
3686 let clip = *proj * *view * p.extend(1.0);
3687 if clip.w <= 0.0 {
3688 return None;
3689 }
3690 let ndc_x = clip.x / clip.w;
3691 let ndc_y = clip.y / clip.w;
3692 if ndc_x < -1.0 || ndc_x > 1.0 || ndc_y < -1.0 || ndc_y > 1.0 {
3693 return None;
3694 }
3695 let x = (ndc_x * 0.5 + 0.5) * vp_w;
3696 let y = (1.0 - (ndc_y * 0.5 + 0.5)) * vp_h;
3697 Some([x, y])
3698}
3699
3700#[inline]
3702fn px_to_ndc(px_x: f32, px_y: f32, vp_w: f32, vp_h: f32) -> [f32; 2] {
3703 [
3704 px_x / vp_w * 2.0 - 1.0,
3705 1.0 - px_y / vp_h * 2.0,
3706 ]
3707}
3708
3709fn emit_solid_quad(
3711 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3712 x0: f32, y0: f32,
3713 x1: f32, y1: f32,
3714 color: [f32; 4],
3715 vp_w: f32, vp_h: f32,
3716) {
3717 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
3718 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
3719 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
3720 let br = px_to_ndc(x1, y1, vp_w, vp_h);
3721 let uv = [0.0, 0.0];
3722 let tex = 0.0;
3723 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
3724 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3725 };
3726 verts.extend_from_slice(&[v(tl), v(bl), v(tr), v(tr), v(bl), v(br)]);
3727}
3728
3729fn emit_textured_quad(
3731 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3732 x0: f32, y0: f32,
3733 x1: f32, y1: f32,
3734 uv_min: [f32; 2],
3735 uv_max: [f32; 2],
3736 color: [f32; 4],
3737 vp_w: f32, vp_h: f32,
3738) {
3739 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
3740 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
3741 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
3742 let br = px_to_ndc(x1, y1, vp_w, vp_h);
3743 let tex = 1.0;
3744 let v = |pos: [f32; 2], uv: [f32; 2]| crate::resources::OverlayTextVertex {
3745 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3746 };
3747 verts.extend_from_slice(&[
3749 v(tl, uv_min),
3750 v(bl, [uv_min[0], uv_max[1]]),
3751 v(tr, [uv_max[0], uv_min[1]]),
3752 v(tr, [uv_max[0], uv_min[1]]),
3753 v(bl, [uv_min[0], uv_max[1]]),
3754 v(br, uv_max),
3755 ]);
3756}
3757
3758fn emit_line_quad(
3760 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3761 x0: f32, y0: f32,
3762 x1: f32, y1: f32,
3763 thickness: f32,
3764 color: [f32; 4],
3765 vp_w: f32, vp_h: f32,
3766) {
3767 let dx = x1 - x0;
3768 let dy = y1 - y0;
3769 let len = (dx * dx + dy * dy).sqrt();
3770 if len < 0.001 {
3771 return;
3772 }
3773 let half = thickness * 0.5;
3774 let nx = -dy / len * half;
3775 let ny = dx / len * half;
3776
3777 let p0 = px_to_ndc(x0 + nx, y0 + ny, vp_w, vp_h);
3778 let p1 = px_to_ndc(x0 - nx, y0 - ny, vp_w, vp_h);
3779 let p2 = px_to_ndc(x1 + nx, y1 + ny, vp_w, vp_h);
3780 let p3 = px_to_ndc(x1 - nx, y1 - ny, vp_w, vp_h);
3781 let uv = [0.0, 0.0];
3782 let tex = 0.0;
3783 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
3784 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3785 };
3786 verts.extend_from_slice(&[v(p0), v(p1), v(p2), v(p2), v(p1), v(p3)]);
3787}
3788
3789#[inline]
3791fn apply_opacity(color: [f32; 4], opacity: f32) -> [f32; 4] {
3792 [color[0], color[1], color[2], color[3] * opacity]
3793}
3794
3795fn emit_rounded_quad(
3799 verts: &mut Vec<crate::resources::OverlayTextVertex>,
3800 x0: f32, y0: f32,
3801 x1: f32, y1: f32,
3802 radius: f32,
3803 color: [f32; 4],
3804 vp_w: f32, vp_h: f32,
3805) {
3806 let w = x1 - x0;
3807 let h = y1 - y0;
3808 let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
3809
3810 if r < 0.5 {
3811 emit_solid_quad(verts, x0, y0, x1, y1, color, vp_w, vp_h);
3812 return;
3813 }
3814
3815 emit_solid_quad(verts, x0, y0 + r, x1, y1 - r, color, vp_w, vp_h);
3818 emit_solid_quad(verts, x0 + r, y0, x1 - r, y0 + r, color, vp_w, vp_h);
3820 emit_solid_quad(verts, x0 + r, y1 - r, x1 - r, y1, color, vp_w, vp_h);
3822
3823 let corners = [
3825 (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), ];
3830 let segments = 6;
3831 let uv = [0.0, 0.0];
3832 let tex = 0.0;
3833 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
3834 position: pos, uv, color, use_texture: tex, _pad: 0.0,
3835 };
3836 for (cx, cy, start, end) in corners {
3837 let center = px_to_ndc(cx, cy, vp_w, vp_h);
3838 for i in 0..segments {
3839 let a0 = start + (end - start) * i as f32 / segments as f32;
3840 let a1 = start + (end - start) * (i + 1) as f32 / segments as f32;
3841 let p0 = px_to_ndc(cx + a0.cos() * r, cy + a0.sin() * r, vp_w, vp_h);
3842 let p1 = px_to_ndc(cx + a1.cos() * r, cy + a1.sin() * r, vp_w, vp_h);
3843 verts.extend_from_slice(&[v(center), v(p0), v(p1)]);
3844 }
3845 }
3846}
3847
3848fn format_ruler_distance(distance: f32, fmt: Option<&str>) -> String {
3859 let pattern = fmt.unwrap_or("{:.3}");
3860 if let Some(open) = pattern.find('{') {
3862 if let Some(close_rel) = pattern[open..].find('}') {
3863 let close = open + close_rel;
3864 let spec = &pattern[open + 1..close]; let prefix = &pattern[..open];
3866 let suffix = &pattern[close + 1..];
3867 let formatted = if let Some(prec_str) = spec.strip_prefix(":.") {
3868 let prec_str = prec_str.trim_end_matches('f');
3870 if let Ok(prec) = prec_str.parse::<usize>() {
3871 format!("{distance:.prec$}")
3872 } else {
3873 format!("{distance:.3}")
3874 }
3875 } else if spec.is_empty() || spec == ":" {
3876 format!("{distance}")
3877 } else {
3878 format!("{distance:.3}")
3879 };
3880 return format!("{prefix}{formatted}{suffix}");
3881 }
3882 }
3883 format!("{distance:.3}")
3884}