1use super::types::{ClipShape, SceneEffects, ViewportEffects};
2use super::*;
3
4impl ViewportRenderer {
5 pub(super) fn prepare_scene_internal(
13 &mut self,
14 device: &wgpu::Device,
15 queue: &wgpu::Queue,
16 frame: &FrameData,
17 scene_fx: &SceneEffects<'_>,
18 ) {
19 if !scene_fx.compute_filter_items.is_empty() {
22 self.compute_filter_results =
23 self.resources
24 .run_compute_filters(device, queue, scene_fx.compute_filter_items);
25 } else {
26 self.compute_filter_results.clear();
27 }
28
29 self.resources.ensure_colormaps_initialized(device, queue);
31 self.resources.ensure_matcaps_initialized(device, queue);
32
33 let resources = &mut self.resources;
34 let lighting = scene_fx.lighting;
35
36 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
38 SurfaceSubmission::Flat(items) => items,
39 };
40
41 let (shadow_center, shadow_extent) = if let Some(extent) = lighting.shadow_extent_override {
43 (glam::Vec3::ZERO, extent)
44 } else {
45 (glam::Vec3::ZERO, 20.0)
46 };
47
48 fn compute_shadow_matrix(
50 kind: &LightKind,
51 shadow_center: glam::Vec3,
52 shadow_extent: f32,
53 ) -> glam::Mat4 {
54 match kind {
55 LightKind::Directional { direction } => {
56 let dir = glam::Vec3::from(*direction).normalize();
57 let light_up = if dir.z.abs() > 0.99 {
58 glam::Vec3::Y
59 } else {
60 glam::Vec3::Z
61 };
62 let light_pos = shadow_center + dir * shadow_extent * 2.0;
63 let light_view = glam::Mat4::look_at_rh(light_pos, shadow_center, light_up);
64 let light_proj = glam::Mat4::orthographic_rh(
65 -shadow_extent,
66 shadow_extent,
67 -shadow_extent,
68 shadow_extent,
69 0.01,
70 shadow_extent * 5.0,
71 );
72 light_proj * light_view
73 }
74 LightKind::Point { position, range } => {
75 let pos = glam::Vec3::from(*position);
76 let to_center = (shadow_center - pos).normalize();
77 let light_up = if to_center.z.abs() > 0.99 {
78 glam::Vec3::Y
79 } else {
80 glam::Vec3::Z
81 };
82 let light_view = glam::Mat4::look_at_rh(pos, shadow_center, light_up);
83 let light_proj =
84 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
85 light_proj * light_view
86 }
87 LightKind::Spot {
88 position,
89 direction,
90 range,
91 ..
92 } => {
93 let pos = glam::Vec3::from(*position);
94 let dir = glam::Vec3::from(*direction).normalize();
95 let look_target = pos + dir;
96 let up = if dir.z.abs() > 0.99 {
97 glam::Vec3::Y
98 } else {
99 glam::Vec3::Z
100 };
101 let light_view = glam::Mat4::look_at_rh(pos, look_target, up);
102 let light_proj =
103 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
104 light_proj * light_view
105 }
106 }
107 }
108
109 fn build_single_light_uniform(
111 src: &LightSource,
112 shadow_center: glam::Vec3,
113 shadow_extent: f32,
114 compute_shadow: bool,
115 ) -> SingleLightUniform {
116 let shadow_mat = if compute_shadow {
117 compute_shadow_matrix(&src.kind, shadow_center, shadow_extent)
118 } else {
119 glam::Mat4::IDENTITY
120 };
121
122 match &src.kind {
123 LightKind::Directional { direction } => SingleLightUniform {
124 light_view_proj: shadow_mat.to_cols_array_2d(),
125 pos_or_dir: *direction,
126 light_type: 0,
127 color: src.color,
128 intensity: src.intensity,
129 range: 0.0,
130 inner_angle: 0.0,
131 outer_angle: 0.0,
132 _pad_align: 0,
133 spot_direction: [0.0, -1.0, 0.0],
134 _pad: [0.0; 5],
135 },
136 LightKind::Point { position, range } => SingleLightUniform {
137 light_view_proj: shadow_mat.to_cols_array_2d(),
138 pos_or_dir: *position,
139 light_type: 1,
140 color: src.color,
141 intensity: src.intensity,
142 range: *range,
143 inner_angle: 0.0,
144 outer_angle: 0.0,
145 _pad_align: 0,
146 spot_direction: [0.0, -1.0, 0.0],
147 _pad: [0.0; 5],
148 },
149 LightKind::Spot {
150 position,
151 direction,
152 range,
153 inner_angle,
154 outer_angle,
155 } => SingleLightUniform {
156 light_view_proj: shadow_mat.to_cols_array_2d(),
157 pos_or_dir: *position,
158 light_type: 2,
159 color: src.color,
160 intensity: src.intensity,
161 range: *range,
162 inner_angle: *inner_angle,
163 outer_angle: *outer_angle,
164 _pad_align: 0,
165 spot_direction: *direction,
166 _pad: [0.0; 5],
167 },
168 }
169 }
170
171 let light_count = lighting.lights.len().min(8) as u32;
173 let mut lights_arr = [SingleLightUniform {
174 light_view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
175 pos_or_dir: [0.0; 3],
176 light_type: 0,
177 color: [1.0; 3],
178 intensity: 1.0,
179 range: 0.0,
180 inner_angle: 0.0,
181 outer_angle: 0.0,
182 _pad_align: 0,
183 spot_direction: [0.0, -1.0, 0.0],
184 _pad: [0.0; 5],
185 }; 8];
186
187 for (i, src) in lighting.lights.iter().take(8).enumerate() {
188 lights_arr[i] = build_single_light_uniform(src, shadow_center, shadow_extent, i == 0);
189 }
190
191 let cascade_count = lighting.shadow_cascade_count.clamp(1, 4) as usize;
196 let atlas_res = lighting.shadow_atlas_resolution.max(64);
197 let tile_size = atlas_res / 2;
198
199 let cascade_splits = compute_cascade_splits(
200 frame.camera.render_camera.near.max(0.01),
201 frame.camera.render_camera.far.max(1.0),
202 cascade_count as u32,
203 lighting.cascade_split_lambda,
204 );
205
206 let light_dir_for_csm = if light_count > 0 {
207 match &lighting.lights[0].kind {
208 LightKind::Directional { direction } => glam::Vec3::from(*direction).normalize(),
209 LightKind::Point { position, .. } => {
210 (glam::Vec3::from(*position) - shadow_center).normalize()
211 }
212 LightKind::Spot {
213 position,
214 direction,
215 ..
216 } => {
217 let _ = position;
218 glam::Vec3::from(*direction).normalize()
219 }
220 }
221 } else {
222 glam::Vec3::new(0.3, 1.0, 0.5).normalize()
223 };
224
225 let mut cascade_view_projs = [glam::Mat4::IDENTITY; 4];
226 let mut cascade_split_distances = [0.0f32; 4];
228
229 let use_csm = light_count > 0
231 && matches!(lighting.lights[0].kind, LightKind::Directional { .. })
232 && frame.camera.render_camera.view != glam::Mat4::IDENTITY;
233
234 if use_csm {
235 for i in 0..cascade_count {
236 let split_near = if i == 0 {
237 frame.camera.render_camera.near.max(0.01)
238 } else {
239 cascade_splits[i - 1]
240 };
241 let split_far = cascade_splits[i];
242 cascade_view_projs[i] = compute_cascade_matrix(
243 light_dir_for_csm,
244 frame.camera.render_camera.view,
245 frame.camera.render_camera.fov,
246 frame.camera.render_camera.aspect,
247 split_near,
248 split_far,
249 tile_size as f32,
250 );
251 cascade_split_distances[i] = split_far;
252 }
253 } else {
254 let primary_shadow_mat = if light_count > 0 {
256 compute_shadow_matrix(&lighting.lights[0].kind, shadow_center, shadow_extent)
257 } else {
258 glam::Mat4::IDENTITY
259 };
260 cascade_view_projs[0] = primary_shadow_mat;
261 cascade_split_distances[0] = frame.camera.render_camera.far;
262 }
263 let effective_cascade_count = if use_csm { cascade_count } else { 1 };
264
265 let atlas_rects: [[f32; 4]; 8] = [
268 [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],
273 [0.0; 4],
274 [0.0; 4],
275 [0.0; 4], ];
277
278 {
280 let mut vp_data = [[0.0f32; 4]; 16]; for c in 0..4 {
282 let cols = cascade_view_projs[c].to_cols_array_2d();
283 for row in 0..4 {
284 vp_data[c * 4 + row] = cols[row];
285 }
286 }
287 let shadow_atlas_uniform = ShadowAtlasUniform {
288 cascade_view_proj: vp_data,
289 cascade_splits: cascade_split_distances,
290 cascade_count: effective_cascade_count as u32,
291 atlas_size: atlas_res as f32,
292 shadow_filter: match lighting.shadow_filter {
293 ShadowFilter::Pcf => 0,
294 ShadowFilter::Pcss => 1,
295 },
296 pcss_light_radius: lighting.pcss_light_radius,
297 atlas_rects,
298 };
299 queue.write_buffer(
300 &resources.shadow_info_buf,
301 0,
302 bytemuck::cast_slice(&[shadow_atlas_uniform]),
303 );
304 for slot in &self.viewport_slots {
307 queue.write_buffer(
308 &slot.shadow_info_buf,
309 0,
310 bytemuck::cast_slice(&[shadow_atlas_uniform]),
311 );
312 }
313 }
314
315 let _primary_shadow_mat = cascade_view_projs[0];
318 self.last_cascade0_shadow_mat = cascade_view_projs[0];
320
321 let (ibl_enabled, ibl_intensity, ibl_rotation, show_skybox) =
324 if let Some(env) = scene_fx.environment {
325 if resources.ibl_irradiance_view.is_some() {
326 (
327 1u32,
328 env.intensity,
329 env.rotation,
330 if env.show_skybox { 1u32 } else { 0 },
331 )
332 } else {
333 (0, 0.0, 0.0, 0)
334 }
335 } else {
336 (0, 0.0, 0.0, 0)
337 };
338
339 let lights_uniform = LightsUniform {
340 count: light_count,
341 shadow_bias: lighting.shadow_bias,
342 shadows_enabled: if lighting.shadows_enabled { 1 } else { 0 },
343 _pad: 0,
344 sky_color: lighting.sky_color,
345 hemisphere_intensity: lighting.hemisphere_intensity,
346 ground_color: lighting.ground_color,
347 _pad2: 0.0,
348 lights: lights_arr,
349 ibl_enabled,
350 ibl_intensity,
351 ibl_rotation,
352 show_skybox,
353 };
354 queue.write_buffer(
355 &resources.light_uniform_buf,
356 0,
357 bytemuck::cast_slice(&[lights_uniform]),
358 );
359
360 const SHADOW_SLOT_STRIDE: u64 = 256;
364 for c in 0..4usize {
365 queue.write_buffer(
366 &resources.shadow_uniform_buf,
367 c as u64 * SHADOW_SLOT_STRIDE,
368 bytemuck::cast_slice(&cascade_view_projs[c].to_cols_array_2d()),
369 );
370 }
371
372 let visible_count = scene_items.iter().filter(|i| i.visible).count();
375 let prev_use_instancing = self.use_instancing;
376 self.use_instancing = visible_count > INSTANCING_THRESHOLD;
377
378 if self.use_instancing != prev_use_instancing {
381 self.instanced_batches.clear();
382 self.last_scene_generation = u64::MAX;
383 self.last_scene_items_count = usize::MAX;
384 }
385
386 let has_scalar_items = scene_items.iter().any(|i| i.active_attribute.is_some());
390 let has_two_sided_items = scene_items
391 .iter()
392 .any(|i| i.material.is_two_sided());
393 let has_matcap_items = scene_items.iter().any(|i| i.material.matcap_id.is_some());
394 let has_param_vis_items = scene_items.iter().any(|i| i.material.param_vis.is_some());
395 if !self.use_instancing
396 || frame.viewport.wireframe_mode
397 || has_scalar_items
398 || has_two_sided_items
399 || has_matcap_items
400 || has_param_vis_items
401 {
402 for item in scene_items {
403 if resources
404 .mesh_store
405 .get(item.mesh_id)
406 .is_none()
407 {
408 tracing::warn!(
409 mesh_index = item.mesh_id.index(),
410 "scene item mesh_index invalid, skipping"
411 );
412 continue;
413 };
414 let m = &item.material;
415 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
417 let range = item
418 .scalar_range
419 .or_else(|| {
420 resources
421 .mesh_store
422 .get(item.mesh_id)
423 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
424 })
425 .unwrap_or((0.0, 1.0));
426 (1u32, range.0, range.1)
427 } else {
428 (0u32, 0.0, 1.0)
429 };
430 let obj_uniform = ObjectUniform {
431 model: item.model,
432 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
433 selected: if item.selected { 1 } else { 0 },
434 wireframe: if frame.viewport.wireframe_mode { 1 } else { 0 },
435 ambient: m.ambient,
436 diffuse: m.diffuse,
437 specular: m.specular,
438 shininess: m.shininess,
439 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
440 use_pbr: if m.use_pbr { 1 } else { 0 },
441 metallic: m.metallic,
442 roughness: m.roughness,
443 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
444 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
445 has_attribute: has_attr,
446 scalar_min: s_min,
447 scalar_max: s_max,
448 _pad_scalar: 0,
449 nan_color: item.nan_color.unwrap_or([0.0; 4]),
450 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
451 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
452 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
453 _pad2: 0,
454 use_face_color: u32::from(item.active_attribute.as_ref().map_or(false, |a| {
455 a.kind == crate::resources::AttributeKind::FaceColor
456 })),
457 uv_vis_mode: m.param_vis.map_or(0, |pv| pv.mode as u32),
458 uv_vis_scale: m.param_vis.map_or(8.0, |pv| pv.scale),
459 backface_policy: match m.backface_policy {
460 crate::scene::material::BackfacePolicy::Cull => 0,
461 crate::scene::material::BackfacePolicy::Identical => 1,
462 crate::scene::material::BackfacePolicy::DifferentColor(_) => 2,
463 crate::scene::material::BackfacePolicy::Tint(_) => 3,
464 crate::scene::material::BackfacePolicy::Pattern { pattern, .. } => {
465 4 + pattern as u32
466 }
467 },
468 backface_color: match m.backface_policy {
469 crate::scene::material::BackfacePolicy::DifferentColor(c) => {
470 [c[0], c[1], c[2], 1.0]
471 }
472 crate::scene::material::BackfacePolicy::Tint(factor) => {
473 [factor, 0.0, 0.0, 1.0]
474 }
475 crate::scene::material::BackfacePolicy::Pattern { color, .. } => {
476 [color[0], color[1], color[2], 1.0]
477 }
478 _ => [0.0; 4],
479 },
480 };
481
482 let normal_obj_uniform = ObjectUniform {
483 model: item.model,
484 color: [1.0, 1.0, 1.0, 1.0],
485 selected: 0,
486 wireframe: 0,
487 ambient: 0.15,
488 diffuse: 0.75,
489 specular: 0.4,
490 shininess: 32.0,
491 has_texture: 0,
492 use_pbr: 0,
493 metallic: 0.0,
494 roughness: 0.5,
495 has_normal_map: 0,
496 has_ao_map: 0,
497 has_attribute: 0,
498 scalar_min: 0.0,
499 scalar_max: 1.0,
500 _pad_scalar: 0,
501 nan_color: [0.0; 4],
502 use_nan_color: 0,
503 use_matcap: 0,
504 matcap_blendable: 0,
505 _pad2: 0,
506 use_face_color: 0,
507 uv_vis_mode: 0,
508 uv_vis_scale: 8.0,
509 backface_policy: 0,
510 backface_color: [0.0; 4],
511 };
512
513 {
515 let mesh = resources
516 .mesh_store
517 .get(item.mesh_id)
518 .unwrap();
519 queue.write_buffer(
520 &mesh.object_uniform_buf,
521 0,
522 bytemuck::cast_slice(&[obj_uniform]),
523 );
524 queue.write_buffer(
525 &mesh.normal_uniform_buf,
526 0,
527 bytemuck::cast_slice(&[normal_obj_uniform]),
528 );
529 } resources.update_mesh_texture_bind_group(
533 device,
534 item.mesh_id,
535 item.material.texture_id,
536 item.material.normal_map_id,
537 item.material.ao_map_id,
538 item.colormap_id,
539 item.active_attribute.as_ref().map(|a| a.name.as_str()),
540 item.material.matcap_id,
541 );
542 }
543 }
544
545 if self.use_instancing {
546 resources.ensure_instanced_pipelines(device);
547
548 let cache_valid = frame.scene.generation == self.last_scene_generation
553 && frame.interaction.selection_generation == self.last_selection_generation
554 && scene_items.len() == self.last_scene_items_count;
555
556 if !cache_valid {
557 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
559 .iter()
560 .filter(|item| {
561 item.visible
562 && item.active_attribute.is_none()
563 && !item.material.is_two_sided()
564 && item.material.matcap_id.is_none()
565 && item.material.param_vis.is_none()
566 && resources
567 .mesh_store
568 .get(item.mesh_id)
569 .is_some()
570 })
571 .collect();
572
573 sorted_items.sort_unstable_by_key(|item| {
574 (
575 item.mesh_id.index(),
576 item.material.texture_id,
577 item.material.normal_map_id,
578 item.material.ao_map_id,
579 )
580 });
581
582 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
583 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
584
585 if !sorted_items.is_empty() {
586 let mut batch_start = 0usize;
587 for i in 1..=sorted_items.len() {
588 let at_end = i == sorted_items.len();
589 let key_changed = !at_end && {
590 let a = sorted_items[batch_start];
591 let b = sorted_items[i];
592 a.mesh_id != b.mesh_id
593 || a.material.texture_id != b.material.texture_id
594 || a.material.normal_map_id != b.material.normal_map_id
595 || a.material.ao_map_id != b.material.ao_map_id
596 };
597
598 if at_end || key_changed {
599 let batch_items = &sorted_items[batch_start..i];
600 let rep = batch_items[0];
601 let instance_offset = all_instances.len() as u32;
602 let is_transparent = rep.material.opacity < 1.0;
603
604 for item in batch_items {
605 let m = &item.material;
606 all_instances.push(InstanceData {
607 model: item.model,
608 color: [
609 m.base_color[0],
610 m.base_color[1],
611 m.base_color[2],
612 m.opacity,
613 ],
614 selected: if item.selected { 1 } else { 0 },
615 wireframe: 0, ambient: m.ambient,
617 diffuse: m.diffuse,
618 specular: m.specular,
619 shininess: m.shininess,
620 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
621 use_pbr: if m.use_pbr { 1 } else { 0 },
622 metallic: m.metallic,
623 roughness: m.roughness,
624 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
625 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
626 });
627 }
628
629 instanced_batches.push(InstancedBatch {
630 mesh_id: rep.mesh_id,
631 texture_id: rep.material.texture_id,
632 normal_map_id: rep.material.normal_map_id,
633 ao_map_id: rep.material.ao_map_id,
634 instance_offset,
635 instance_count: batch_items.len() as u32,
636 is_transparent,
637 });
638
639 batch_start = i;
640 }
641 }
642 }
643
644 self.cached_instance_data = all_instances;
645 self.cached_instanced_batches = instanced_batches;
646
647 resources.upload_instance_data(device, queue, &self.cached_instance_data);
648
649 self.instanced_batches = self.cached_instanced_batches.clone();
650
651 self.last_scene_generation = frame.scene.generation;
652 self.last_selection_generation = frame.interaction.selection_generation;
653 self.last_scene_items_count = scene_items.len();
654
655 for batch in &self.instanced_batches {
656 resources.get_instance_bind_group(
657 device,
658 batch.texture_id,
659 batch.normal_map_id,
660 batch.ao_map_id,
661 );
662 }
663 } else {
664 for batch in &self.instanced_batches {
665 resources.get_instance_bind_group(
666 device,
667 batch.texture_id,
668 batch.normal_map_id,
669 batch.ao_map_id,
670 );
671 }
672 }
673 }
674
675 self.point_cloud_gpu_data.clear();
679 if !frame.scene.point_clouds.is_empty() {
680 resources.ensure_point_cloud_pipeline(device);
681 for item in &frame.scene.point_clouds {
682 if item.positions.is_empty() {
683 continue;
684 }
685 let gpu_data = resources.upload_point_cloud(device, queue, item);
686 self.point_cloud_gpu_data.push(gpu_data);
687 }
688 }
689
690 self.glyph_gpu_data.clear();
691 if !frame.scene.glyphs.is_empty() {
692 resources.ensure_glyph_pipeline(device);
693 for item in &frame.scene.glyphs {
694 if item.positions.is_empty() || item.vectors.is_empty() {
695 continue;
696 }
697 let gpu_data = resources.upload_glyph_set(device, queue, item);
698 self.glyph_gpu_data.push(gpu_data);
699 }
700 }
701
702 self.polyline_gpu_data.clear();
706 let vp_size = frame.camera.viewport_size;
707 if !frame.scene.polylines.is_empty() {
708 resources.ensure_polyline_pipeline(device);
709 for item in &frame.scene.polylines {
710 if item.positions.is_empty() {
711 continue;
712 }
713 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
714 self.polyline_gpu_data.push(gpu_data);
715
716 if !item.node_vectors.is_empty() {
718 resources.ensure_glyph_pipeline(device);
719 let g = crate::quantities::polyline_node_vectors_to_glyphs(item);
720 if !g.positions.is_empty() {
721 let gd = resources.upload_glyph_set(device, queue, &g);
722 self.glyph_gpu_data.push(gd);
723 }
724 }
725 if !item.edge_vectors.is_empty() {
726 resources.ensure_glyph_pipeline(device);
727 let g = crate::quantities::polyline_edge_vectors_to_glyphs(item);
728 if !g.positions.is_empty() {
729 let gd = resources.upload_glyph_set(device, queue, &g);
730 self.glyph_gpu_data.push(gd);
731 }
732 }
733 }
734 }
735
736 if !frame.scene.isolines.is_empty() {
740 resources.ensure_polyline_pipeline(device);
741 for item in &frame.scene.isolines {
742 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
743 continue;
744 }
745 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
746 if positions.is_empty() {
747 continue;
748 }
749 let polyline = PolylineItem {
750 positions,
751 scalars: Vec::new(),
752 strip_lengths,
753 scalar_range: None,
754 colormap_id: None,
755 default_color: item.color,
756 line_width: item.line_width,
757 id: 0,
758 ..Default::default()
759 };
760 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
761 self.polyline_gpu_data.push(gpu_data);
762 }
763 }
764
765 if !frame.scene.camera_frustums.is_empty() {
769 resources.ensure_polyline_pipeline(device);
770 for item in &frame.scene.camera_frustums {
771 let polyline = item.to_polyline();
772 if !polyline.positions.is_empty() {
773 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
774 self.polyline_gpu_data.push(gpu_data);
775 }
776 }
777 }
778
779 self.screen_image_gpu_data.clear();
783 if !frame.scene.screen_images.is_empty() {
784 resources.ensure_screen_image_pipeline(device);
785 if frame.scene.screen_images.iter().any(|i| i.depth.is_some()) {
787 resources.ensure_screen_image_dc_pipeline(device);
788 }
789 let vp_w = vp_size[0];
790 let vp_h = vp_size[1];
791 for item in &frame.scene.screen_images {
792 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
793 continue;
794 }
795 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
796 self.screen_image_gpu_data.push(gpu);
797 }
798 }
799
800 self.streamtube_gpu_data.clear();
804 if !frame.scene.streamtube_items.is_empty() {
805 resources.ensure_streamtube_pipeline(device);
806 for item in &frame.scene.streamtube_items {
807 if item.positions.is_empty() || item.strip_lengths.is_empty() {
808 continue;
809 }
810 let gpu_data = resources.upload_streamtube(device, queue, item);
811 if gpu_data.index_count > 0 {
812 self.streamtube_gpu_data.push(gpu_data);
813 }
814 }
815 }
816
817 self.volume_gpu_data.clear();
823 if !frame.scene.volumes.is_empty() {
824 resources.ensure_volume_pipeline(device);
825 let clip_planes_for_vol: Vec<crate::renderer::types::ClipPlane> = frame
827 .effects
828 .clip_objects
829 .iter()
830 .filter(|o| o.enabled)
831 .filter_map(|o| {
832 if let ClipShape::Plane {
833 normal,
834 distance,
835 cap_color,
836 } = o.shape
837 {
838 Some(crate::renderer::types::ClipPlane {
839 normal,
840 distance,
841 enabled: true,
842 cap_color,
843 })
844 } else {
845 None
846 }
847 })
848 .collect();
849 for item in &frame.scene.volumes {
850 let gpu = resources.upload_volume_frame(device, queue, item, &clip_planes_for_vol);
851 self.volume_gpu_data.push(gpu);
852 }
853 }
854
855 {
857 let total = scene_items.len() as u32;
858 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
859 let mut draw_calls = 0u32;
860 let mut triangles = 0u64;
861 let instanced_batch_count = if self.use_instancing {
862 self.instanced_batches.len() as u32
863 } else {
864 0
865 };
866
867 if self.use_instancing {
868 for batch in &self.instanced_batches {
869 if let Some(mesh) = resources
870 .mesh_store
871 .get(batch.mesh_id)
872 {
873 draw_calls += 1;
874 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
875 }
876 }
877 } else {
878 for item in scene_items {
879 if !item.visible {
880 continue;
881 }
882 if let Some(mesh) = resources
883 .mesh_store
884 .get(item.mesh_id)
885 {
886 draw_calls += 1;
887 triangles += (mesh.index_count / 3) as u64;
888 }
889 }
890 }
891
892 self.last_stats = crate::renderer::stats::FrameStats {
893 total_objects: total,
894 visible_objects: visible,
895 culled_objects: total.saturating_sub(visible),
896 draw_calls,
897 instanced_batches: instanced_batch_count,
898 triangles_submitted: triangles,
899 shadow_draw_calls: 0, };
901 }
902
903 if lighting.shadows_enabled && !scene_items.is_empty() {
907 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
908 label: Some("shadow_pass_encoder"),
909 });
910 {
911 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
912 label: Some("shadow_pass"),
913 color_attachments: &[],
914 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
915 view: &resources.shadow_map_view,
916 depth_ops: Some(wgpu::Operations {
917 load: wgpu::LoadOp::Clear(1.0),
918 store: wgpu::StoreOp::Store,
919 }),
920 stencil_ops: None,
921 }),
922 timestamp_writes: None,
923 occlusion_query_set: None,
924 });
925
926 let mut shadow_draws = 0u32;
927 let tile_px = tile_size as f32;
928
929 if self.use_instancing {
930 if let (Some(pipeline), Some(instance_bg)) = (
931 &resources.shadow_instanced_pipeline,
932 self.instanced_batches.first().and_then(|b| {
933 resources.instance_bind_groups.get(&(
934 b.texture_id.unwrap_or(u64::MAX),
935 b.normal_map_id.unwrap_or(u64::MAX),
936 b.ao_map_id.unwrap_or(u64::MAX),
937 ))
938 }),
939 ) {
940 for cascade in 0..effective_cascade_count {
941 let tile_col = (cascade % 2) as f32;
942 let tile_row = (cascade / 2) as f32;
943 shadow_pass.set_viewport(
944 tile_col * tile_px,
945 tile_row * tile_px,
946 tile_px,
947 tile_px,
948 0.0,
949 1.0,
950 );
951 shadow_pass.set_scissor_rect(
952 (tile_col * tile_px) as u32,
953 (tile_row * tile_px) as u32,
954 tile_size,
955 tile_size,
956 );
957
958 shadow_pass.set_pipeline(pipeline);
959
960 queue.write_buffer(
961 resources.shadow_instanced_cascade_bufs[cascade]
962 .as_ref()
963 .expect("shadow_instanced_cascade_bufs not allocated"),
964 0,
965 bytemuck::cast_slice(
966 &cascade_view_projs[cascade].to_cols_array_2d(),
967 ),
968 );
969
970 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
971 .as_ref()
972 .expect("shadow_instanced_cascade_bgs not allocated");
973 shadow_pass.set_bind_group(0, cascade_bg, &[]);
974 shadow_pass.set_bind_group(1, instance_bg, &[]);
975
976 for batch in &self.instanced_batches {
977 if batch.is_transparent {
978 continue;
979 }
980 let Some(mesh) = resources
981 .mesh_store
982 .get(batch.mesh_id)
983 else {
984 continue;
985 };
986 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
987 shadow_pass.set_index_buffer(
988 mesh.index_buffer.slice(..),
989 wgpu::IndexFormat::Uint32,
990 );
991 shadow_pass.draw_indexed(
992 0..mesh.index_count,
993 0,
994 batch.instance_offset
995 ..batch.instance_offset + batch.instance_count,
996 );
997 shadow_draws += 1;
998 }
999 }
1000 }
1001 } else {
1002 for cascade in 0..effective_cascade_count {
1003 let tile_col = (cascade % 2) as f32;
1004 let tile_row = (cascade / 2) as f32;
1005 shadow_pass.set_viewport(
1006 tile_col * tile_px,
1007 tile_row * tile_px,
1008 tile_px,
1009 tile_px,
1010 0.0,
1011 1.0,
1012 );
1013 shadow_pass.set_scissor_rect(
1014 (tile_col * tile_px) as u32,
1015 (tile_row * tile_px) as u32,
1016 tile_size,
1017 tile_size,
1018 );
1019
1020 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1021 shadow_pass.set_bind_group(
1022 0,
1023 &resources.shadow_bind_group,
1024 &[cascade as u32 * 256],
1025 );
1026
1027 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1028 &cascade_view_projs[cascade],
1029 );
1030
1031 for item in scene_items.iter() {
1032 if !item.visible {
1033 continue;
1034 }
1035 if item.material.opacity < 1.0 {
1036 continue;
1037 }
1038 let Some(mesh) = resources
1039 .mesh_store
1040 .get(item.mesh_id)
1041 else {
1042 continue;
1043 };
1044
1045 let world_aabb = mesh
1046 .aabb
1047 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1048 if cascade_frustum.cull_aabb(&world_aabb) {
1049 continue;
1050 }
1051
1052 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1053 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1054 shadow_pass.set_index_buffer(
1055 mesh.index_buffer.slice(..),
1056 wgpu::IndexFormat::Uint32,
1057 );
1058 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1059 shadow_draws += 1;
1060 }
1061 }
1062 }
1063 drop(shadow_pass);
1064 self.last_stats.shadow_draw_calls = shadow_draws;
1065 }
1066 queue.submit(std::iter::once(encoder.finish()));
1067 }
1068 }
1069
1070 pub(super) fn prepare_viewport_internal(
1075 &mut self,
1076 device: &wgpu::Device,
1077 queue: &wgpu::Queue,
1078 frame: &FrameData,
1079 viewport_fx: &ViewportEffects<'_>,
1080 ) {
1081 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1084
1085 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1086 SurfaceSubmission::Flat(items) => items,
1087 };
1088
1089 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1091
1092 {
1093 let resources = &mut self.resources;
1094
1095 {
1097 let mut planes = [[0.0f32; 4]; 6];
1098 let mut count = 0u32;
1099 let mut clip_vol_uniform: ClipVolumeUniform = bytemuck::Zeroable::zeroed(); for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1102 match obj.shape {
1103 ClipShape::Plane {
1104 normal, distance, ..
1105 } if count < 6 => {
1106 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1107 count += 1;
1108 }
1109 ClipShape::Box {
1110 center,
1111 half_extents,
1112 orientation,
1113 } if clip_vol_uniform.volume_type == 0 => {
1114 clip_vol_uniform.volume_type = 2;
1115 clip_vol_uniform.box_center = center;
1116 clip_vol_uniform.box_half_extents = half_extents;
1117 clip_vol_uniform.box_col0 = orientation[0];
1118 clip_vol_uniform.box_col1 = orientation[1];
1119 clip_vol_uniform.box_col2 = orientation[2];
1120 }
1121 ClipShape::Sphere { center, radius }
1122 if clip_vol_uniform.volume_type == 0 =>
1123 {
1124 clip_vol_uniform.volume_type = 3;
1125 clip_vol_uniform.sphere_center = center;
1126 clip_vol_uniform.sphere_radius = radius;
1127 }
1128 _ => {}
1129 }
1130 }
1131
1132 let clip_uniform = ClipPlanesUniform {
1133 planes,
1134 count,
1135 _pad0: 0,
1136 viewport_width: frame.camera.viewport_size[0].max(1.0),
1137 viewport_height: frame.camera.viewport_size[1].max(1.0),
1138 };
1139 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1141 queue.write_buffer(
1142 &slot.clip_planes_buf,
1143 0,
1144 bytemuck::cast_slice(&[clip_uniform]),
1145 );
1146 queue.write_buffer(
1147 &slot.clip_volume_buf,
1148 0,
1149 bytemuck::cast_slice(&[clip_vol_uniform]),
1150 );
1151 }
1152 queue.write_buffer(
1154 &resources.clip_planes_uniform_buf,
1155 0,
1156 bytemuck::cast_slice(&[clip_uniform]),
1157 );
1158 queue.write_buffer(
1159 &resources.clip_volume_uniform_buf,
1160 0,
1161 bytemuck::cast_slice(&[clip_vol_uniform]),
1162 );
1163 }
1164
1165 let camera_uniform = frame.camera.render_camera.camera_uniform();
1167 queue.write_buffer(
1169 &resources.camera_uniform_buf,
1170 0,
1171 bytemuck::cast_slice(&[camera_uniform]),
1172 );
1173 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1175 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1176 }
1177
1178 if frame.viewport.show_grid {
1180 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1181 if !eye.is_finite() {
1182 tracing::warn!(
1183 eye_x = eye.x,
1184 eye_y = eye.y,
1185 eye_z = eye.z,
1186 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1187 );
1188 } else {
1189 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1190
1191 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1192 (frame.viewport.grid_cell_size, 1.0_f32)
1193 } else {
1194 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1195 let world_per_pixel =
1196 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1197 / frame.camera.viewport_size[1].max(1.0);
1198 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1199 let mut s = 1.0_f32;
1200 let mut iters = 0u32;
1201 while s < target {
1202 s *= 10.0;
1203 iters += 1;
1204 }
1205 let ratio = (target / s).clamp(0.0, 1.0);
1206 let fade = if ratio < 0.5 {
1207 1.0_f32
1208 } else {
1209 let t = (ratio - 0.5) * 2.0;
1210 1.0 - t * t * (3.0 - 2.0 * t)
1211 };
1212 tracing::debug!(
1213 eye_z = eye.z,
1214 vertical_depth,
1215 world_per_pixel,
1216 target,
1217 spacing = s,
1218 lod_iters = iters,
1219 ratio,
1220 minor_fade = fade,
1221 "grid LOD"
1222 );
1223 (s, fade)
1224 };
1225
1226 let spacing_major = spacing * 10.0;
1227 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1228 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1229 tracing::debug!(
1230 spacing_minor = spacing,
1231 spacing_major,
1232 snap_x,
1233 snap_y,
1234 eye_x = eye.x,
1235 eye_y = eye.y,
1236 eye_z = eye.z,
1237 "grid snap"
1238 );
1239
1240 let orient = frame.camera.render_camera.orientation;
1241 let right = orient * glam::Vec3::X;
1242 let up = orient * glam::Vec3::Y;
1243 let back = orient * glam::Vec3::Z;
1244 let cam_to_world = [
1245 [right.x, right.y, right.z, 0.0_f32],
1246 [up.x, up.y, up.z, 0.0_f32],
1247 [back.x, back.y, back.z, 0.0_f32],
1248 ];
1249 let aspect =
1250 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1251 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1252
1253 let uniform = GridUniform {
1254 view_proj: view_proj_mat,
1255 cam_to_world,
1256 tan_half_fov,
1257 aspect,
1258 _pad_ivp: [0.0; 2],
1259 eye_pos: frame.camera.render_camera.eye_position,
1260 grid_z: frame.viewport.grid_z,
1261 spacing_minor: spacing,
1262 spacing_major,
1263 snap_origin: [snap_x, snap_y],
1264 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1265 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1266 };
1267 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1269 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1270 }
1271 queue.write_buffer(
1273 &resources.grid_uniform_buf,
1274 0,
1275 bytemuck::cast_slice(&[uniform]),
1276 );
1277 }
1278 }
1279 {
1283 let gp = &viewport_fx.ground_plane;
1284 let mode_u32: u32 = match gp.mode {
1285 crate::renderer::types::GroundPlaneMode::None => 0,
1286 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1287 crate::renderer::types::GroundPlaneMode::Tile => 2,
1288 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1289 };
1290 let orient = frame.camera.render_camera.orientation;
1291 let right = orient * glam::Vec3::X;
1292 let up = orient * glam::Vec3::Y;
1293 let back = orient * glam::Vec3::Z;
1294 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1295 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1296 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1297 let gp_uniform = crate::resources::GroundPlaneUniform {
1298 view_proj: vp,
1299 cam_right: [right.x, right.y, right.z, 0.0],
1300 cam_up: [up.x, up.y, up.z, 0.0],
1301 cam_back: [back.x, back.y, back.z, 0.0],
1302 eye_pos: frame.camera.render_camera.eye_position,
1303 height: gp.height,
1304 color: gp.color,
1305 shadow_color: gp.shadow_color,
1306 light_vp: gp_cascade0_mat,
1307 tan_half_fov,
1308 aspect,
1309 tile_size: gp.tile_size,
1310 shadow_bias: 0.002,
1311 mode: mode_u32,
1312 shadow_opacity: gp.shadow_opacity,
1313 _pad: [0.0; 2],
1314 };
1315 queue.write_buffer(
1316 &resources.ground_plane_uniform_buf,
1317 0,
1318 bytemuck::cast_slice(&[gp_uniform]),
1319 );
1320 }
1321 } let vp_idx = frame.camera.viewport_index;
1330
1331 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1333 if frame.interaction.outline_selected {
1334 let resources = &self.resources;
1335 for item in scene_items {
1336 if !item.visible || !item.selected {
1337 continue;
1338 }
1339 let uniform = OutlineUniform {
1340 model: item.model,
1341 color: [0.0; 4], pixel_offset: 0.0,
1343 _pad: [0.0; 3],
1344 };
1345 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1346 label: Some("outline_mask_uniform_buf"),
1347 size: std::mem::size_of::<OutlineUniform>() as u64,
1348 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1349 mapped_at_creation: false,
1350 });
1351 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1352 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1353 label: Some("outline_mask_object_bg"),
1354 layout: &resources.outline_bind_group_layout,
1355 entries: &[wgpu::BindGroupEntry {
1356 binding: 0,
1357 resource: buf.as_entire_binding(),
1358 }],
1359 });
1360 outline_object_buffers.push(OutlineObjectBuffers {
1361 mesh_id: item.mesh_id,
1362 two_sided: item.material.is_two_sided(),
1363 _mask_uniform_buf: buf,
1364 mask_bind_group: bg,
1365 });
1366 }
1367 }
1368
1369 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1371 if frame.interaction.xray_selected {
1372 let resources = &self.resources;
1373 for item in scene_items {
1374 if !item.visible || !item.selected {
1375 continue;
1376 }
1377 let uniform = OutlineUniform {
1378 model: item.model,
1379 color: frame.interaction.xray_color,
1380 pixel_offset: 0.0,
1381 _pad: [0.0; 3],
1382 };
1383 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1384 label: Some("xray_uniform_buf"),
1385 size: std::mem::size_of::<OutlineUniform>() as u64,
1386 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1387 mapped_at_creation: false,
1388 });
1389 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1390 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1391 label: Some("xray_object_bg"),
1392 layout: &resources.outline_bind_group_layout,
1393 entries: &[wgpu::BindGroupEntry {
1394 binding: 0,
1395 resource: buf.as_entire_binding(),
1396 }],
1397 });
1398 xray_object_buffers.push((item.mesh_id, buf, bg));
1399 }
1400 }
1401
1402 let mut constraint_line_buffers = Vec::new();
1404 for overlay in &frame.interaction.constraint_overlays {
1405 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1406 }
1407
1408 let mut clip_plane_fill_buffers = Vec::new();
1410 let mut clip_plane_line_buffers = Vec::new();
1411 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1412 let Some(base_color) = obj.color else {
1413 continue;
1414 };
1415 if let ClipShape::Plane {
1416 normal, distance, ..
1417 } = obj.shape
1418 {
1419 let n = glam::Vec3::from(normal);
1420 let center = n * (-distance);
1423 let active = obj.active;
1424 let hovered = obj.hovered || active;
1425
1426 let fill_color = if active {
1427 [
1428 base_color[0] * 0.5,
1429 base_color[1] * 0.5,
1430 base_color[2] * 0.5,
1431 base_color[3] * 0.5,
1432 ]
1433 } else if hovered {
1434 [
1435 base_color[0] * 0.8,
1436 base_color[1] * 0.8,
1437 base_color[2] * 0.8,
1438 base_color[3] * 0.6,
1439 ]
1440 } else {
1441 [
1442 base_color[0] * 0.5,
1443 base_color[1] * 0.5,
1444 base_color[2] * 0.5,
1445 base_color[3] * 0.3,
1446 ]
1447 };
1448 let border_color = if active {
1449 [base_color[0], base_color[1], base_color[2], 0.9]
1450 } else if hovered {
1451 [base_color[0], base_color[1], base_color[2], 0.8]
1452 } else {
1453 [
1454 base_color[0] * 0.9,
1455 base_color[1] * 0.9,
1456 base_color[2] * 0.9,
1457 0.6,
1458 ]
1459 };
1460
1461 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
1462 center,
1463 normal: n,
1464 extent: obj.extent,
1465 fill_color,
1466 border_color,
1467 hovered,
1468 active,
1469 };
1470 clip_plane_fill_buffers.push(
1471 self.resources
1472 .create_clip_plane_fill_overlay(device, &overlay),
1473 );
1474 clip_plane_line_buffers.push(
1475 self.resources
1476 .create_clip_plane_line_overlay(device, &overlay),
1477 );
1478 } else {
1479 self.resources.ensure_polyline_pipeline(device);
1483 match obj.shape {
1484 ClipShape::Box {
1485 center,
1486 half_extents,
1487 orientation,
1488 } => {
1489 let polyline =
1490 clip_box_outline(center, half_extents, orientation, base_color);
1491 let vp_size = frame.camera.viewport_size;
1492 let gpu = self
1493 .resources
1494 .upload_polyline(device, queue, &polyline, vp_size);
1495 self.polyline_gpu_data.push(gpu);
1496 }
1497 ClipShape::Sphere { center, radius } => {
1498 let polyline = clip_sphere_outline(center, radius, base_color);
1499 let vp_size = frame.camera.viewport_size;
1500 let gpu = self
1501 .resources
1502 .upload_polyline(device, queue, &polyline, vp_size);
1503 self.polyline_gpu_data.push(gpu);
1504 }
1505 _ => {}
1506 }
1507 }
1508 }
1509
1510 let mut cap_buffers = Vec::new();
1512 if viewport_fx.cap_fill_enabled {
1513 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1514 if let ClipShape::Plane {
1515 normal,
1516 distance,
1517 cap_color,
1518 } = obj.shape
1519 {
1520 let plane_n = glam::Vec3::from(normal);
1521 for item in scene_items.iter().filter(|i| i.visible) {
1522 let Some(mesh) = self
1523 .resources
1524 .mesh_store
1525 .get(item.mesh_id)
1526 else {
1527 continue;
1528 };
1529 let model = glam::Mat4::from_cols_array_2d(&item.model);
1530 let world_aabb = mesh.aabb.transformed(&model);
1531 if !world_aabb.intersects_plane(plane_n, distance) {
1532 continue;
1533 }
1534 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1535 else {
1536 continue;
1537 };
1538 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
1539 pos, idx, &model, plane_n, distance,
1540 ) {
1541 let bc = item.material.base_color;
1542 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
1543 let buf = self.resources.upload_cap_geometry(device, &cap, color);
1544 cap_buffers.push(buf);
1545 }
1546 }
1547 }
1548 }
1549 }
1550
1551 let axes_verts = if frame.viewport.show_axes_indicator
1553 && frame.camera.viewport_size[0] > 0.0
1554 && frame.camera.viewport_size[1] > 0.0
1555 {
1556 let verts = crate::widgets::axes_indicator::build_axes_geometry(
1557 frame.camera.viewport_size[0],
1558 frame.camera.viewport_size[1],
1559 frame.camera.render_camera.orientation,
1560 );
1561 if verts.is_empty() { None } else { Some(verts) }
1562 } else {
1563 None
1564 };
1565
1566 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
1568 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
1569 frame.interaction.gizmo_mode,
1570 frame.interaction.gizmo_hovered,
1571 frame.interaction.gizmo_space_orientation,
1572 );
1573 (verts, indices, model)
1574 });
1575
1576 {
1580 let slot = &mut self.viewport_slots[vp_idx];
1581 slot.outline_object_buffers = outline_object_buffers;
1582 slot.xray_object_buffers = xray_object_buffers;
1583 slot.constraint_line_buffers = constraint_line_buffers;
1584 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
1585 slot.clip_plane_line_buffers = clip_plane_line_buffers;
1586 slot.cap_buffers = cap_buffers;
1587
1588 if let Some(verts) = axes_verts {
1590 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
1591 if byte_size > slot.axes_vertex_buffer.size() {
1592 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1593 label: Some("vp_axes_vertex_buf"),
1594 size: byte_size,
1595 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1596 mapped_at_creation: false,
1597 });
1598 }
1599 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
1600 slot.axes_vertex_count = verts.len() as u32;
1601 } else {
1602 slot.axes_vertex_count = 0;
1603 }
1604
1605 if let Some((verts, indices, model)) = gizmo_update {
1607 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1608 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1609 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
1610 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1611 label: Some("vp_gizmo_vertex_buf"),
1612 size: vert_bytes.len() as u64,
1613 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1614 mapped_at_creation: false,
1615 });
1616 }
1617 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
1618 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1619 label: Some("vp_gizmo_index_buf"),
1620 size: idx_bytes.len() as u64,
1621 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1622 mapped_at_creation: false,
1623 });
1624 }
1625 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
1626 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
1627 slot.gizmo_index_count = indices.len() as u32;
1628 let uniform = crate::interaction::gizmo::GizmoUniform {
1629 model: model.to_cols_array_2d(),
1630 };
1631 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
1632 }
1633 }
1634
1635 if frame.interaction.outline_selected
1646 && !self.viewport_slots[vp_idx]
1647 .outline_object_buffers
1648 .is_empty()
1649 {
1650 let w = frame.camera.viewport_size[0] as u32;
1651 let h = frame.camera.viewport_size[1] as u32;
1652
1653 self.ensure_viewport_hdr(
1655 device,
1656 queue,
1657 vp_idx,
1658 w.max(1),
1659 h.max(1),
1660 frame.effects.post_process.ssaa_factor.max(1),
1661 );
1662
1663 {
1665 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1666 let edge_uniform = OutlineEdgeUniform {
1667 color: frame.interaction.outline_color,
1668 radius: frame.interaction.outline_width_px,
1669 viewport_w: w as f32,
1670 viewport_h: h as f32,
1671 _pad: 0.0,
1672 };
1673 queue.write_buffer(
1674 &slot_hdr.outline_edge_uniform_buf,
1675 0,
1676 bytemuck::cast_slice(&[edge_uniform]),
1677 );
1678 }
1679
1680 let slot_ref = &self.viewport_slots[vp_idx];
1683 let outlines_ptr =
1684 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
1685 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
1686 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
1687 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
1688 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
1689 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
1690 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
1691 let (outlines, camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
1694 (
1695 &*outlines_ptr,
1696 &*camera_bg_ptr,
1697 &*mask_view_ptr,
1698 &*color_view_ptr,
1699 &*depth_view_ptr,
1700 &*edge_bg_ptr,
1701 )
1702 };
1703
1704 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1705 label: Some("outline_offscreen_encoder"),
1706 });
1707
1708 {
1710 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1711 label: Some("outline_mask_pass"),
1712 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1713 view: mask_view,
1714 resolve_target: None,
1715 ops: wgpu::Operations {
1716 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1717 store: wgpu::StoreOp::Store,
1718 },
1719 depth_slice: None,
1720 })],
1721 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1722 view: depth_view,
1723 depth_ops: Some(wgpu::Operations {
1724 load: wgpu::LoadOp::Clear(1.0),
1725 store: wgpu::StoreOp::Discard,
1726 }),
1727 stencil_ops: None,
1728 }),
1729 timestamp_writes: None,
1730 occlusion_query_set: None,
1731 });
1732
1733 pass.set_bind_group(0, camera_bg, &[]);
1734 for outlined in outlines {
1735 let Some(mesh) = self
1736 .resources
1737 .mesh_store
1738 .get(outlined.mesh_id)
1739 else {
1740 continue;
1741 };
1742 let pipeline = if outlined.two_sided {
1743 &self.resources.outline_mask_two_sided_pipeline
1744 } else {
1745 &self.resources.outline_mask_pipeline
1746 };
1747 pass.set_pipeline(pipeline);
1748 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
1749 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1750 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1751 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1752 }
1753 }
1754
1755 {
1757 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1758 label: Some("outline_edge_pass"),
1759 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1760 view: color_view,
1761 resolve_target: None,
1762 ops: wgpu::Operations {
1763 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1764 store: wgpu::StoreOp::Store,
1765 },
1766 depth_slice: None,
1767 })],
1768 depth_stencil_attachment: None,
1769 timestamp_writes: None,
1770 occlusion_query_set: None,
1771 });
1772 pass.set_pipeline(&self.resources.outline_edge_pipeline);
1773 pass.set_bind_group(0, edge_bg, &[]);
1774 pass.draw(0..3, 0..1);
1775 }
1776
1777 queue.submit(std::iter::once(encoder.finish()));
1778 }
1779 }
1780
1781 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, frame: &FrameData) {
1784 let (scene_fx, viewport_fx) = frame.effects.split();
1785 self.prepare_scene_internal(device, queue, frame, &scene_fx);
1786 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1787 }
1788}
1789
1790fn clip_box_outline(
1796 center: [f32; 3],
1797 half: [f32; 3],
1798 orientation: [[f32; 3]; 3],
1799 color: [f32; 4],
1800) -> PolylineItem {
1801 let ax = glam::Vec3::from(orientation[0]) * half[0];
1802 let ay = glam::Vec3::from(orientation[1]) * half[1];
1803 let az = glam::Vec3::from(orientation[2]) * half[2];
1804 let c = glam::Vec3::from(center);
1805
1806 let corners = [
1807 c - ax - ay - az,
1808 c + ax - ay - az,
1809 c + ax + ay - az,
1810 c - ax + ay - az,
1811 c - ax - ay + az,
1812 c + ax - ay + az,
1813 c + ax + ay + az,
1814 c - ax + ay + az,
1815 ];
1816 let edges: [(usize, usize); 12] = [
1817 (0, 1),
1818 (1, 2),
1819 (2, 3),
1820 (3, 0), (4, 5),
1822 (5, 6),
1823 (6, 7),
1824 (7, 4), (0, 4),
1826 (1, 5),
1827 (2, 6),
1828 (3, 7), ];
1830
1831 let mut positions = Vec::with_capacity(24);
1832 let mut strip_lengths = Vec::with_capacity(12);
1833 for (a, b) in edges {
1834 positions.push(corners[a].to_array());
1835 positions.push(corners[b].to_array());
1836 strip_lengths.push(2u32);
1837 }
1838
1839 let mut item = PolylineItem::default();
1840 item.positions = positions;
1841 item.strip_lengths = strip_lengths;
1842 item.default_color = color;
1843 item.line_width = 2.0;
1844 item
1845}
1846
1847fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
1849 let c = glam::Vec3::from(center);
1850 let segs = 64usize;
1851 let mut positions = Vec::with_capacity((segs + 1) * 3);
1852 let mut strip_lengths = Vec::with_capacity(3);
1853
1854 for axis in 0..3usize {
1855 let start = positions.len();
1856 for i in 0..=segs {
1857 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
1858 let (s, cs) = t.sin_cos();
1859 let p = c + match axis {
1860 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
1861 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
1862 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
1863 };
1864 positions.push(p.to_array());
1865 }
1866 strip_lengths.push((positions.len() - start) as u32);
1867 }
1868
1869 let mut item = PolylineItem::default();
1870 item.positions = positions;
1871 item.strip_lengths = strip_lengths;
1872 item.default_color = color;
1873 item.line_width = 2.0;
1874 item
1875}