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