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 let has_wireframe_items = scene_items.iter().any(|i| i.render_as_wireframe);
396 if !self.use_instancing
397 || frame.viewport.wireframe_mode
398 || has_scalar_items
399 || has_two_sided_items
400 || has_matcap_items
401 || has_param_vis_items
402 || has_wireframe_items
403 {
404 for item in scene_items {
405 if resources
406 .mesh_store
407 .get(item.mesh_id)
408 .is_none()
409 {
410 tracing::warn!(
411 mesh_index = item.mesh_id.index(),
412 "scene item mesh_index invalid, skipping"
413 );
414 continue;
415 };
416 let m = &item.material;
417 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
419 let range = item
420 .scalar_range
421 .or_else(|| {
422 resources
423 .mesh_store
424 .get(item.mesh_id)
425 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
426 })
427 .unwrap_or((0.0, 1.0));
428 (1u32, range.0, range.1)
429 } else {
430 (0u32, 0.0, 1.0)
431 };
432 let obj_uniform = ObjectUniform {
433 model: item.model,
434 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
435 selected: if item.selected { 1 } else { 0 },
436 wireframe: if frame.viewport.wireframe_mode || item.render_as_wireframe { 1 } else { 0 },
437 ambient: m.ambient,
438 diffuse: m.diffuse,
439 specular: m.specular,
440 shininess: m.shininess,
441 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
442 use_pbr: if m.use_pbr { 1 } else { 0 },
443 metallic: m.metallic,
444 roughness: m.roughness,
445 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
446 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
447 has_attribute: has_attr,
448 scalar_min: s_min,
449 scalar_max: s_max,
450 _pad_scalar: 0,
451 nan_color: item.nan_color.unwrap_or([0.0; 4]),
452 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
453 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
454 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
455 _pad2: 0,
456 use_face_color: u32::from(item.active_attribute.as_ref().map_or(false, |a| {
457 a.kind == crate::resources::AttributeKind::FaceColor
458 })),
459 uv_vis_mode: m.param_vis.map_or(0, |pv| pv.mode as u32),
460 uv_vis_scale: m.param_vis.map_or(8.0, |pv| pv.scale),
461 backface_policy: match m.backface_policy {
462 crate::scene::material::BackfacePolicy::Cull => 0,
463 crate::scene::material::BackfacePolicy::Identical => 1,
464 crate::scene::material::BackfacePolicy::DifferentColor(_) => 2,
465 crate::scene::material::BackfacePolicy::Tint(_) => 3,
466 crate::scene::material::BackfacePolicy::Pattern { pattern, .. } => {
467 4 + pattern as u32
468 }
469 },
470 backface_color: match m.backface_policy {
471 crate::scene::material::BackfacePolicy::DifferentColor(c) => {
472 [c[0], c[1], c[2], 1.0]
473 }
474 crate::scene::material::BackfacePolicy::Tint(factor) => {
475 [factor, 0.0, 0.0, 1.0]
476 }
477 crate::scene::material::BackfacePolicy::Pattern { color, .. } => {
478 [color[0], color[1], color[2], 1.0]
479 }
480 _ => [0.0; 4],
481 },
482 };
483
484 let normal_obj_uniform = ObjectUniform {
485 model: item.model,
486 color: [1.0, 1.0, 1.0, 1.0],
487 selected: 0,
488 wireframe: 0,
489 ambient: 0.15,
490 diffuse: 0.75,
491 specular: 0.4,
492 shininess: 32.0,
493 has_texture: 0,
494 use_pbr: 0,
495 metallic: 0.0,
496 roughness: 0.5,
497 has_normal_map: 0,
498 has_ao_map: 0,
499 has_attribute: 0,
500 scalar_min: 0.0,
501 scalar_max: 1.0,
502 _pad_scalar: 0,
503 nan_color: [0.0; 4],
504 use_nan_color: 0,
505 use_matcap: 0,
506 matcap_blendable: 0,
507 _pad2: 0,
508 use_face_color: 0,
509 uv_vis_mode: 0,
510 uv_vis_scale: 8.0,
511 backface_policy: 0,
512 backface_color: [0.0; 4],
513 };
514
515 {
517 let mesh = resources
518 .mesh_store
519 .get(item.mesh_id)
520 .unwrap();
521 queue.write_buffer(
522 &mesh.object_uniform_buf,
523 0,
524 bytemuck::cast_slice(&[obj_uniform]),
525 );
526 queue.write_buffer(
527 &mesh.normal_uniform_buf,
528 0,
529 bytemuck::cast_slice(&[normal_obj_uniform]),
530 );
531 } resources.update_mesh_texture_bind_group(
535 device,
536 item.mesh_id,
537 item.material.texture_id,
538 item.material.normal_map_id,
539 item.material.ao_map_id,
540 item.colormap_id,
541 item.active_attribute.as_ref().map(|a| a.name.as_str()),
542 item.material.matcap_id,
543 );
544 }
545 }
546
547 if self.use_instancing {
548 resources.ensure_instanced_pipelines(device);
549
550 let cache_valid = frame.scene.generation == self.last_scene_generation
555 && frame.interaction.selection_generation == self.last_selection_generation
556 && scene_items.len() == self.last_scene_items_count;
557
558 if !cache_valid {
559 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
561 .iter()
562 .filter(|item| {
563 item.visible
564 && item.active_attribute.is_none()
565 && !item.material.is_two_sided()
566 && item.material.matcap_id.is_none()
567 && item.material.param_vis.is_none()
568 && resources
569 .mesh_store
570 .get(item.mesh_id)
571 .is_some()
572 })
573 .collect();
574
575 sorted_items.sort_unstable_by_key(|item| {
576 (
577 item.mesh_id.index(),
578 item.material.texture_id,
579 item.material.normal_map_id,
580 item.material.ao_map_id,
581 )
582 });
583
584 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
585 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
586
587 if !sorted_items.is_empty() {
588 let mut batch_start = 0usize;
589 for i in 1..=sorted_items.len() {
590 let at_end = i == sorted_items.len();
591 let key_changed = !at_end && {
592 let a = sorted_items[batch_start];
593 let b = sorted_items[i];
594 a.mesh_id != b.mesh_id
595 || a.material.texture_id != b.material.texture_id
596 || a.material.normal_map_id != b.material.normal_map_id
597 || a.material.ao_map_id != b.material.ao_map_id
598 };
599
600 if at_end || key_changed {
601 let batch_items = &sorted_items[batch_start..i];
602 let rep = batch_items[0];
603 let instance_offset = all_instances.len() as u32;
604 let is_transparent = rep.material.opacity < 1.0;
605
606 for item in batch_items {
607 let m = &item.material;
608 all_instances.push(InstanceData {
609 model: item.model,
610 color: [
611 m.base_color[0],
612 m.base_color[1],
613 m.base_color[2],
614 m.opacity,
615 ],
616 selected: if item.selected { 1 } else { 0 },
617 wireframe: 0, ambient: m.ambient,
619 diffuse: m.diffuse,
620 specular: m.specular,
621 shininess: m.shininess,
622 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
623 use_pbr: if m.use_pbr { 1 } else { 0 },
624 metallic: m.metallic,
625 roughness: m.roughness,
626 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
627 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
628 });
629 }
630
631 instanced_batches.push(InstancedBatch {
632 mesh_id: rep.mesh_id,
633 texture_id: rep.material.texture_id,
634 normal_map_id: rep.material.normal_map_id,
635 ao_map_id: rep.material.ao_map_id,
636 instance_offset,
637 instance_count: batch_items.len() as u32,
638 is_transparent,
639 });
640
641 batch_start = i;
642 }
643 }
644 }
645
646 self.cached_instance_data = all_instances;
647 self.cached_instanced_batches = instanced_batches;
648
649 resources.upload_instance_data(device, queue, &self.cached_instance_data);
650
651 self.instanced_batches = self.cached_instanced_batches.clone();
652
653 self.last_scene_generation = frame.scene.generation;
654 self.last_selection_generation = frame.interaction.selection_generation;
655 self.last_scene_items_count = scene_items.len();
656
657 for batch in &self.instanced_batches {
658 resources.get_instance_bind_group(
659 device,
660 batch.texture_id,
661 batch.normal_map_id,
662 batch.ao_map_id,
663 );
664 }
665 } else {
666 for batch in &self.instanced_batches {
667 resources.get_instance_bind_group(
668 device,
669 batch.texture_id,
670 batch.normal_map_id,
671 batch.ao_map_id,
672 );
673 }
674 }
675 }
676
677 self.point_cloud_gpu_data.clear();
681 if !frame.scene.point_clouds.is_empty() {
682 resources.ensure_point_cloud_pipeline(device);
683 for item in &frame.scene.point_clouds {
684 if item.positions.is_empty() {
685 continue;
686 }
687 let gpu_data = resources.upload_point_cloud(device, queue, item);
688 self.point_cloud_gpu_data.push(gpu_data);
689 }
690 }
691
692 self.glyph_gpu_data.clear();
693 if !frame.scene.glyphs.is_empty() {
694 resources.ensure_glyph_pipeline(device);
695 for item in &frame.scene.glyphs {
696 if item.positions.is_empty() || item.vectors.is_empty() {
697 continue;
698 }
699 let gpu_data = resources.upload_glyph_set(device, queue, item);
700 self.glyph_gpu_data.push(gpu_data);
701 }
702 }
703
704 self.polyline_gpu_data.clear();
708 let vp_size = frame.camera.viewport_size;
709 if !frame.scene.polylines.is_empty() {
710 resources.ensure_polyline_pipeline(device);
711 for item in &frame.scene.polylines {
712 if item.positions.is_empty() {
713 continue;
714 }
715 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
716 self.polyline_gpu_data.push(gpu_data);
717
718 if !item.node_vectors.is_empty() {
720 resources.ensure_glyph_pipeline(device);
721 let g = crate::quantities::polyline_node_vectors_to_glyphs(item);
722 if !g.positions.is_empty() {
723 let gd = resources.upload_glyph_set(device, queue, &g);
724 self.glyph_gpu_data.push(gd);
725 }
726 }
727 if !item.edge_vectors.is_empty() {
728 resources.ensure_glyph_pipeline(device);
729 let g = crate::quantities::polyline_edge_vectors_to_glyphs(item);
730 if !g.positions.is_empty() {
731 let gd = resources.upload_glyph_set(device, queue, &g);
732 self.glyph_gpu_data.push(gd);
733 }
734 }
735 }
736 }
737
738 if !frame.scene.isolines.is_empty() {
742 resources.ensure_polyline_pipeline(device);
743 for item in &frame.scene.isolines {
744 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
745 continue;
746 }
747 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
748 if positions.is_empty() {
749 continue;
750 }
751 let polyline = PolylineItem {
752 positions,
753 scalars: Vec::new(),
754 strip_lengths,
755 scalar_range: None,
756 colormap_id: None,
757 default_color: item.color,
758 line_width: item.line_width,
759 id: 0,
760 ..Default::default()
761 };
762 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
763 self.polyline_gpu_data.push(gpu_data);
764 }
765 }
766
767 if !frame.scene.camera_frustums.is_empty() {
771 resources.ensure_polyline_pipeline(device);
772 for item in &frame.scene.camera_frustums {
773 let polyline = item.to_polyline();
774 if !polyline.positions.is_empty() {
775 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
776 self.polyline_gpu_data.push(gpu_data);
777 }
778 }
779 }
780
781 self.implicit_gpu_data.clear();
785 if !frame.scene.gpu_implicit.is_empty() {
786 resources.ensure_implicit_pipeline(device);
787 for item in &frame.scene.gpu_implicit {
788 if item.primitives.is_empty() {
789 continue;
790 }
791 let gpu = resources.upload_implicit_item(device, item);
792 self.implicit_gpu_data.push(gpu);
793 }
794 }
795
796 self.mc_gpu_data.clear();
800 if !frame.scene.gpu_mc_jobs.is_empty() {
801 resources.ensure_mc_pipelines(device);
802 self.mc_gpu_data =
803 resources.run_mc_jobs(device, queue, &frame.scene.gpu_mc_jobs);
804 }
805
806 self.screen_image_gpu_data.clear();
810 if !frame.scene.screen_images.is_empty() {
811 resources.ensure_screen_image_pipeline(device);
812 if frame.scene.screen_images.iter().any(|i| i.depth.is_some()) {
814 resources.ensure_screen_image_dc_pipeline(device);
815 }
816 let vp_w = vp_size[0];
817 let vp_h = vp_size[1];
818 for item in &frame.scene.screen_images {
819 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
820 continue;
821 }
822 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
823 self.screen_image_gpu_data.push(gpu);
824 }
825 }
826
827 self.streamtube_gpu_data.clear();
831 if !frame.scene.streamtube_items.is_empty() {
832 resources.ensure_streamtube_pipeline(device);
833 for item in &frame.scene.streamtube_items {
834 if item.positions.is_empty() || item.strip_lengths.is_empty() {
835 continue;
836 }
837 let gpu_data = resources.upload_streamtube(device, queue, item);
838 if gpu_data.index_count > 0 {
839 self.streamtube_gpu_data.push(gpu_data);
840 }
841 }
842 }
843
844 self.volume_gpu_data.clear();
850 if !frame.scene.volumes.is_empty() {
851 resources.ensure_volume_pipeline(device);
852 let clip_planes_for_vol: Vec<crate::renderer::types::ClipPlane> = frame
854 .effects
855 .clip_objects
856 .iter()
857 .filter(|o| o.enabled)
858 .filter_map(|o| {
859 if let ClipShape::Plane {
860 normal,
861 distance,
862 cap_color,
863 } = o.shape
864 {
865 Some(crate::renderer::types::ClipPlane {
866 normal,
867 distance,
868 enabled: true,
869 cap_color,
870 })
871 } else {
872 None
873 }
874 })
875 .collect();
876 for item in &frame.scene.volumes {
877 let gpu = resources.upload_volume_frame(device, queue, item, &clip_planes_for_vol);
878 self.volume_gpu_data.push(gpu);
879 }
880 }
881
882 {
884 let total = scene_items.len() as u32;
885 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
886 let mut draw_calls = 0u32;
887 let mut triangles = 0u64;
888 let instanced_batch_count = if self.use_instancing {
889 self.instanced_batches.len() as u32
890 } else {
891 0
892 };
893
894 if self.use_instancing {
895 for batch in &self.instanced_batches {
896 if let Some(mesh) = resources
897 .mesh_store
898 .get(batch.mesh_id)
899 {
900 draw_calls += 1;
901 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
902 }
903 }
904 } else {
905 for item in scene_items {
906 if !item.visible {
907 continue;
908 }
909 if let Some(mesh) = resources
910 .mesh_store
911 .get(item.mesh_id)
912 {
913 draw_calls += 1;
914 triangles += (mesh.index_count / 3) as u64;
915 }
916 }
917 }
918
919 self.last_stats = crate::renderer::stats::FrameStats {
920 total_objects: total,
921 visible_objects: visible,
922 culled_objects: total.saturating_sub(visible),
923 draw_calls,
924 instanced_batches: instanced_batch_count,
925 triangles_submitted: triangles,
926 shadow_draw_calls: 0, };
928 }
929
930 if lighting.shadows_enabled && !scene_items.is_empty() {
934 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
935 label: Some("shadow_pass_encoder"),
936 });
937 {
938 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
939 label: Some("shadow_pass"),
940 color_attachments: &[],
941 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
942 view: &resources.shadow_map_view,
943 depth_ops: Some(wgpu::Operations {
944 load: wgpu::LoadOp::Clear(1.0),
945 store: wgpu::StoreOp::Store,
946 }),
947 stencil_ops: None,
948 }),
949 timestamp_writes: None,
950 occlusion_query_set: None,
951 });
952
953 let mut shadow_draws = 0u32;
954 let tile_px = tile_size as f32;
955
956 if self.use_instancing {
957 if let (Some(pipeline), Some(instance_bg)) = (
958 &resources.shadow_instanced_pipeline,
959 self.instanced_batches.first().and_then(|b| {
960 resources.instance_bind_groups.get(&(
961 b.texture_id.unwrap_or(u64::MAX),
962 b.normal_map_id.unwrap_or(u64::MAX),
963 b.ao_map_id.unwrap_or(u64::MAX),
964 ))
965 }),
966 ) {
967 for cascade in 0..effective_cascade_count {
968 let tile_col = (cascade % 2) as f32;
969 let tile_row = (cascade / 2) as f32;
970 shadow_pass.set_viewport(
971 tile_col * tile_px,
972 tile_row * tile_px,
973 tile_px,
974 tile_px,
975 0.0,
976 1.0,
977 );
978 shadow_pass.set_scissor_rect(
979 (tile_col * tile_px) as u32,
980 (tile_row * tile_px) as u32,
981 tile_size,
982 tile_size,
983 );
984
985 shadow_pass.set_pipeline(pipeline);
986
987 queue.write_buffer(
988 resources.shadow_instanced_cascade_bufs[cascade]
989 .as_ref()
990 .expect("shadow_instanced_cascade_bufs not allocated"),
991 0,
992 bytemuck::cast_slice(
993 &cascade_view_projs[cascade].to_cols_array_2d(),
994 ),
995 );
996
997 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
998 .as_ref()
999 .expect("shadow_instanced_cascade_bgs not allocated");
1000 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1001 shadow_pass.set_bind_group(1, instance_bg, &[]);
1002
1003 for batch in &self.instanced_batches {
1004 if batch.is_transparent {
1005 continue;
1006 }
1007 let Some(mesh) = resources
1008 .mesh_store
1009 .get(batch.mesh_id)
1010 else {
1011 continue;
1012 };
1013 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1014 shadow_pass.set_index_buffer(
1015 mesh.index_buffer.slice(..),
1016 wgpu::IndexFormat::Uint32,
1017 );
1018 shadow_pass.draw_indexed(
1019 0..mesh.index_count,
1020 0,
1021 batch.instance_offset
1022 ..batch.instance_offset + batch.instance_count,
1023 );
1024 shadow_draws += 1;
1025 }
1026 }
1027 }
1028 } else {
1029 for cascade in 0..effective_cascade_count {
1030 let tile_col = (cascade % 2) as f32;
1031 let tile_row = (cascade / 2) as f32;
1032 shadow_pass.set_viewport(
1033 tile_col * tile_px,
1034 tile_row * tile_px,
1035 tile_px,
1036 tile_px,
1037 0.0,
1038 1.0,
1039 );
1040 shadow_pass.set_scissor_rect(
1041 (tile_col * tile_px) as u32,
1042 (tile_row * tile_px) as u32,
1043 tile_size,
1044 tile_size,
1045 );
1046
1047 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1048 shadow_pass.set_bind_group(
1049 0,
1050 &resources.shadow_bind_group,
1051 &[cascade as u32 * 256],
1052 );
1053
1054 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1055 &cascade_view_projs[cascade],
1056 );
1057
1058 for item in scene_items.iter() {
1059 if !item.visible {
1060 continue;
1061 }
1062 if item.material.opacity < 1.0 {
1063 continue;
1064 }
1065 let Some(mesh) = resources
1066 .mesh_store
1067 .get(item.mesh_id)
1068 else {
1069 continue;
1070 };
1071
1072 let world_aabb = mesh
1073 .aabb
1074 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1075 if cascade_frustum.cull_aabb(&world_aabb) {
1076 continue;
1077 }
1078
1079 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1080 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1081 shadow_pass.set_index_buffer(
1082 mesh.index_buffer.slice(..),
1083 wgpu::IndexFormat::Uint32,
1084 );
1085 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1086 shadow_draws += 1;
1087 }
1088 }
1089 }
1090 drop(shadow_pass);
1091 self.last_stats.shadow_draw_calls = shadow_draws;
1092 }
1093 queue.submit(std::iter::once(encoder.finish()));
1094 }
1095 }
1096
1097 pub(super) fn prepare_viewport_internal(
1102 &mut self,
1103 device: &wgpu::Device,
1104 queue: &wgpu::Queue,
1105 frame: &FrameData,
1106 viewport_fx: &ViewportEffects<'_>,
1107 ) {
1108 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1111
1112 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1113 SurfaceSubmission::Flat(items) => items,
1114 };
1115
1116 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1118
1119 {
1120 let resources = &mut self.resources;
1121
1122 {
1124 let mut planes = [[0.0f32; 4]; 6];
1125 let mut count = 0u32;
1126 let mut clip_vol_uniform: ClipVolumeUniform = bytemuck::Zeroable::zeroed(); for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1129 match obj.shape {
1130 ClipShape::Plane {
1131 normal, distance, ..
1132 } if count < 6 => {
1133 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1134 count += 1;
1135 }
1136 ClipShape::Box {
1137 center,
1138 half_extents,
1139 orientation,
1140 } if clip_vol_uniform.volume_type == 0 => {
1141 clip_vol_uniform.volume_type = 2;
1142 clip_vol_uniform.box_center = center;
1143 clip_vol_uniform.box_half_extents = half_extents;
1144 clip_vol_uniform.box_col0 = orientation[0];
1145 clip_vol_uniform.box_col1 = orientation[1];
1146 clip_vol_uniform.box_col2 = orientation[2];
1147 }
1148 ClipShape::Sphere { center, radius }
1149 if clip_vol_uniform.volume_type == 0 =>
1150 {
1151 clip_vol_uniform.volume_type = 3;
1152 clip_vol_uniform.sphere_center = center;
1153 clip_vol_uniform.sphere_radius = radius;
1154 }
1155 _ => {}
1156 }
1157 }
1158
1159 let clip_uniform = ClipPlanesUniform {
1160 planes,
1161 count,
1162 _pad0: 0,
1163 viewport_width: frame.camera.viewport_size[0].max(1.0),
1164 viewport_height: frame.camera.viewport_size[1].max(1.0),
1165 };
1166 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1168 queue.write_buffer(
1169 &slot.clip_planes_buf,
1170 0,
1171 bytemuck::cast_slice(&[clip_uniform]),
1172 );
1173 queue.write_buffer(
1174 &slot.clip_volume_buf,
1175 0,
1176 bytemuck::cast_slice(&[clip_vol_uniform]),
1177 );
1178 }
1179 queue.write_buffer(
1181 &resources.clip_planes_uniform_buf,
1182 0,
1183 bytemuck::cast_slice(&[clip_uniform]),
1184 );
1185 queue.write_buffer(
1186 &resources.clip_volume_uniform_buf,
1187 0,
1188 bytemuck::cast_slice(&[clip_vol_uniform]),
1189 );
1190 }
1191
1192 let camera_uniform = frame.camera.render_camera.camera_uniform();
1194 queue.write_buffer(
1196 &resources.camera_uniform_buf,
1197 0,
1198 bytemuck::cast_slice(&[camera_uniform]),
1199 );
1200 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1202 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1203 }
1204
1205 if frame.viewport.show_grid {
1207 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1208 if !eye.is_finite() {
1209 tracing::warn!(
1210 eye_x = eye.x,
1211 eye_y = eye.y,
1212 eye_z = eye.z,
1213 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1214 );
1215 } else {
1216 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1217
1218 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1219 (frame.viewport.grid_cell_size, 1.0_f32)
1220 } else {
1221 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1222 let world_per_pixel =
1223 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1224 / frame.camera.viewport_size[1].max(1.0);
1225 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1226 let mut s = 1.0_f32;
1227 let mut iters = 0u32;
1228 while s < target {
1229 s *= 10.0;
1230 iters += 1;
1231 }
1232 let ratio = (target / s).clamp(0.0, 1.0);
1233 let fade = if ratio < 0.5 {
1234 1.0_f32
1235 } else {
1236 let t = (ratio - 0.5) * 2.0;
1237 1.0 - t * t * (3.0 - 2.0 * t)
1238 };
1239 tracing::debug!(
1240 eye_z = eye.z,
1241 vertical_depth,
1242 world_per_pixel,
1243 target,
1244 spacing = s,
1245 lod_iters = iters,
1246 ratio,
1247 minor_fade = fade,
1248 "grid LOD"
1249 );
1250 (s, fade)
1251 };
1252
1253 let spacing_major = spacing * 10.0;
1254 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1255 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1256 tracing::debug!(
1257 spacing_minor = spacing,
1258 spacing_major,
1259 snap_x,
1260 snap_y,
1261 eye_x = eye.x,
1262 eye_y = eye.y,
1263 eye_z = eye.z,
1264 "grid snap"
1265 );
1266
1267 let orient = frame.camera.render_camera.orientation;
1268 let right = orient * glam::Vec3::X;
1269 let up = orient * glam::Vec3::Y;
1270 let back = orient * glam::Vec3::Z;
1271 let cam_to_world = [
1272 [right.x, right.y, right.z, 0.0_f32],
1273 [up.x, up.y, up.z, 0.0_f32],
1274 [back.x, back.y, back.z, 0.0_f32],
1275 ];
1276 let aspect =
1277 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1278 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1279
1280 let uniform = GridUniform {
1281 view_proj: view_proj_mat,
1282 cam_to_world,
1283 tan_half_fov,
1284 aspect,
1285 _pad_ivp: [0.0; 2],
1286 eye_pos: frame.camera.render_camera.eye_position,
1287 grid_z: frame.viewport.grid_z,
1288 spacing_minor: spacing,
1289 spacing_major,
1290 snap_origin: [snap_x, snap_y],
1291 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1292 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1293 };
1294 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1296 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1297 }
1298 queue.write_buffer(
1300 &resources.grid_uniform_buf,
1301 0,
1302 bytemuck::cast_slice(&[uniform]),
1303 );
1304 }
1305 }
1306 {
1310 let gp = &viewport_fx.ground_plane;
1311 let mode_u32: u32 = match gp.mode {
1312 crate::renderer::types::GroundPlaneMode::None => 0,
1313 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1314 crate::renderer::types::GroundPlaneMode::Tile => 2,
1315 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1316 };
1317 let orient = frame.camera.render_camera.orientation;
1318 let right = orient * glam::Vec3::X;
1319 let up = orient * glam::Vec3::Y;
1320 let back = orient * glam::Vec3::Z;
1321 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1322 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1323 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1324 let gp_uniform = crate::resources::GroundPlaneUniform {
1325 view_proj: vp,
1326 cam_right: [right.x, right.y, right.z, 0.0],
1327 cam_up: [up.x, up.y, up.z, 0.0],
1328 cam_back: [back.x, back.y, back.z, 0.0],
1329 eye_pos: frame.camera.render_camera.eye_position,
1330 height: gp.height,
1331 color: gp.color,
1332 shadow_color: gp.shadow_color,
1333 light_vp: gp_cascade0_mat,
1334 tan_half_fov,
1335 aspect,
1336 tile_size: gp.tile_size,
1337 shadow_bias: 0.002,
1338 mode: mode_u32,
1339 shadow_opacity: gp.shadow_opacity,
1340 _pad: [0.0; 2],
1341 };
1342 queue.write_buffer(
1343 &resources.ground_plane_uniform_buf,
1344 0,
1345 bytemuck::cast_slice(&[gp_uniform]),
1346 );
1347 }
1348 } let vp_idx = frame.camera.viewport_index;
1357
1358 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1360 if frame.interaction.outline_selected {
1361 let resources = &self.resources;
1362 for item in scene_items {
1363 if !item.visible || !item.selected {
1364 continue;
1365 }
1366 let uniform = OutlineUniform {
1367 model: item.model,
1368 color: [0.0; 4], pixel_offset: 0.0,
1370 _pad: [0.0; 3],
1371 };
1372 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1373 label: Some("outline_mask_uniform_buf"),
1374 size: std::mem::size_of::<OutlineUniform>() as u64,
1375 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1376 mapped_at_creation: false,
1377 });
1378 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1379 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1380 label: Some("outline_mask_object_bg"),
1381 layout: &resources.outline_bind_group_layout,
1382 entries: &[wgpu::BindGroupEntry {
1383 binding: 0,
1384 resource: buf.as_entire_binding(),
1385 }],
1386 });
1387 outline_object_buffers.push(OutlineObjectBuffers {
1388 mesh_id: item.mesh_id,
1389 two_sided: item.material.is_two_sided(),
1390 _mask_uniform_buf: buf,
1391 mask_bind_group: bg,
1392 });
1393 }
1394 }
1395
1396 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1398 if frame.interaction.xray_selected {
1399 let resources = &self.resources;
1400 for item in scene_items {
1401 if !item.visible || !item.selected {
1402 continue;
1403 }
1404 let uniform = OutlineUniform {
1405 model: item.model,
1406 color: frame.interaction.xray_color,
1407 pixel_offset: 0.0,
1408 _pad: [0.0; 3],
1409 };
1410 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1411 label: Some("xray_uniform_buf"),
1412 size: std::mem::size_of::<OutlineUniform>() as u64,
1413 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1414 mapped_at_creation: false,
1415 });
1416 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1417 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1418 label: Some("xray_object_bg"),
1419 layout: &resources.outline_bind_group_layout,
1420 entries: &[wgpu::BindGroupEntry {
1421 binding: 0,
1422 resource: buf.as_entire_binding(),
1423 }],
1424 });
1425 xray_object_buffers.push((item.mesh_id, buf, bg));
1426 }
1427 }
1428
1429 let mut constraint_line_buffers = Vec::new();
1431 for overlay in &frame.interaction.constraint_overlays {
1432 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1433 }
1434
1435 let mut clip_plane_fill_buffers = Vec::new();
1437 let mut clip_plane_line_buffers = Vec::new();
1438 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1439 let Some(base_color) = obj.color else {
1440 continue;
1441 };
1442 if let ClipShape::Plane {
1443 normal, distance, ..
1444 } = obj.shape
1445 {
1446 let n = glam::Vec3::from(normal);
1447 let center = n * (-distance);
1450 let active = obj.active;
1451 let hovered = obj.hovered || active;
1452
1453 let fill_color = if active {
1454 [
1455 base_color[0] * 0.5,
1456 base_color[1] * 0.5,
1457 base_color[2] * 0.5,
1458 base_color[3] * 0.5,
1459 ]
1460 } else if hovered {
1461 [
1462 base_color[0] * 0.8,
1463 base_color[1] * 0.8,
1464 base_color[2] * 0.8,
1465 base_color[3] * 0.6,
1466 ]
1467 } else {
1468 [
1469 base_color[0] * 0.5,
1470 base_color[1] * 0.5,
1471 base_color[2] * 0.5,
1472 base_color[3] * 0.3,
1473 ]
1474 };
1475 let border_color = if active {
1476 [base_color[0], base_color[1], base_color[2], 0.9]
1477 } else if hovered {
1478 [base_color[0], base_color[1], base_color[2], 0.8]
1479 } else {
1480 [
1481 base_color[0] * 0.9,
1482 base_color[1] * 0.9,
1483 base_color[2] * 0.9,
1484 0.6,
1485 ]
1486 };
1487
1488 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
1489 center,
1490 normal: n,
1491 extent: obj.extent,
1492 fill_color,
1493 border_color,
1494 hovered,
1495 active,
1496 };
1497 clip_plane_fill_buffers.push(
1498 self.resources
1499 .create_clip_plane_fill_overlay(device, &overlay),
1500 );
1501 clip_plane_line_buffers.push(
1502 self.resources
1503 .create_clip_plane_line_overlay(device, &overlay),
1504 );
1505 } else {
1506 self.resources.ensure_polyline_pipeline(device);
1510 match obj.shape {
1511 ClipShape::Box {
1512 center,
1513 half_extents,
1514 orientation,
1515 } => {
1516 let polyline =
1517 clip_box_outline(center, half_extents, orientation, base_color);
1518 let vp_size = frame.camera.viewport_size;
1519 let gpu = self
1520 .resources
1521 .upload_polyline(device, queue, &polyline, vp_size);
1522 self.polyline_gpu_data.push(gpu);
1523 }
1524 ClipShape::Sphere { center, radius } => {
1525 let polyline = clip_sphere_outline(center, radius, base_color);
1526 let vp_size = frame.camera.viewport_size;
1527 let gpu = self
1528 .resources
1529 .upload_polyline(device, queue, &polyline, vp_size);
1530 self.polyline_gpu_data.push(gpu);
1531 }
1532 _ => {}
1533 }
1534 }
1535 }
1536
1537 let mut cap_buffers = Vec::new();
1539 if viewport_fx.cap_fill_enabled {
1540 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1541 if let ClipShape::Plane {
1542 normal,
1543 distance,
1544 cap_color,
1545 } = obj.shape
1546 {
1547 let plane_n = glam::Vec3::from(normal);
1548 for item in scene_items.iter().filter(|i| i.visible) {
1549 let Some(mesh) = self
1550 .resources
1551 .mesh_store
1552 .get(item.mesh_id)
1553 else {
1554 continue;
1555 };
1556 let model = glam::Mat4::from_cols_array_2d(&item.model);
1557 let world_aabb = mesh.aabb.transformed(&model);
1558 if !world_aabb.intersects_plane(plane_n, distance) {
1559 continue;
1560 }
1561 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1562 else {
1563 continue;
1564 };
1565 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
1566 pos, idx, &model, plane_n, distance,
1567 ) {
1568 let bc = item.material.base_color;
1569 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
1570 let buf = self.resources.upload_cap_geometry(device, &cap, color);
1571 cap_buffers.push(buf);
1572 }
1573 }
1574 }
1575 }
1576 }
1577
1578 let axes_verts = if frame.viewport.show_axes_indicator
1580 && frame.camera.viewport_size[0] > 0.0
1581 && frame.camera.viewport_size[1] > 0.0
1582 {
1583 let verts = crate::widgets::axes_indicator::build_axes_geometry(
1584 frame.camera.viewport_size[0],
1585 frame.camera.viewport_size[1],
1586 frame.camera.render_camera.orientation,
1587 );
1588 if verts.is_empty() { None } else { Some(verts) }
1589 } else {
1590 None
1591 };
1592
1593 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
1595 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
1596 frame.interaction.gizmo_mode,
1597 frame.interaction.gizmo_hovered,
1598 frame.interaction.gizmo_space_orientation,
1599 );
1600 (verts, indices, model)
1601 });
1602
1603 {
1607 let slot = &mut self.viewport_slots[vp_idx];
1608 slot.outline_object_buffers = outline_object_buffers;
1609 slot.xray_object_buffers = xray_object_buffers;
1610 slot.constraint_line_buffers = constraint_line_buffers;
1611 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
1612 slot.clip_plane_line_buffers = clip_plane_line_buffers;
1613 slot.cap_buffers = cap_buffers;
1614
1615 if let Some(verts) = axes_verts {
1617 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
1618 if byte_size > slot.axes_vertex_buffer.size() {
1619 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1620 label: Some("vp_axes_vertex_buf"),
1621 size: byte_size,
1622 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1623 mapped_at_creation: false,
1624 });
1625 }
1626 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
1627 slot.axes_vertex_count = verts.len() as u32;
1628 } else {
1629 slot.axes_vertex_count = 0;
1630 }
1631
1632 if let Some((verts, indices, model)) = gizmo_update {
1634 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1635 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1636 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
1637 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1638 label: Some("vp_gizmo_vertex_buf"),
1639 size: vert_bytes.len() as u64,
1640 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1641 mapped_at_creation: false,
1642 });
1643 }
1644 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
1645 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1646 label: Some("vp_gizmo_index_buf"),
1647 size: idx_bytes.len() as u64,
1648 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1649 mapped_at_creation: false,
1650 });
1651 }
1652 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
1653 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
1654 slot.gizmo_index_count = indices.len() as u32;
1655 let uniform = crate::interaction::gizmo::GizmoUniform {
1656 model: model.to_cols_array_2d(),
1657 };
1658 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
1659 }
1660 }
1661
1662 if frame.interaction.outline_selected
1673 && !self.viewport_slots[vp_idx]
1674 .outline_object_buffers
1675 .is_empty()
1676 {
1677 let w = frame.camera.viewport_size[0] as u32;
1678 let h = frame.camera.viewport_size[1] as u32;
1679
1680 self.ensure_viewport_hdr(
1682 device,
1683 queue,
1684 vp_idx,
1685 w.max(1),
1686 h.max(1),
1687 frame.effects.post_process.ssaa_factor.max(1),
1688 );
1689
1690 {
1692 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1693 let edge_uniform = OutlineEdgeUniform {
1694 color: frame.interaction.outline_color,
1695 radius: frame.interaction.outline_width_px,
1696 viewport_w: w as f32,
1697 viewport_h: h as f32,
1698 _pad: 0.0,
1699 };
1700 queue.write_buffer(
1701 &slot_hdr.outline_edge_uniform_buf,
1702 0,
1703 bytemuck::cast_slice(&[edge_uniform]),
1704 );
1705 }
1706
1707 let slot_ref = &self.viewport_slots[vp_idx];
1710 let outlines_ptr =
1711 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
1712 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
1713 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
1714 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
1715 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
1716 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
1717 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
1718 let (outlines, camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
1721 (
1722 &*outlines_ptr,
1723 &*camera_bg_ptr,
1724 &*mask_view_ptr,
1725 &*color_view_ptr,
1726 &*depth_view_ptr,
1727 &*edge_bg_ptr,
1728 )
1729 };
1730
1731 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1732 label: Some("outline_offscreen_encoder"),
1733 });
1734
1735 {
1737 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1738 label: Some("outline_mask_pass"),
1739 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1740 view: mask_view,
1741 resolve_target: None,
1742 ops: wgpu::Operations {
1743 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1744 store: wgpu::StoreOp::Store,
1745 },
1746 depth_slice: None,
1747 })],
1748 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1749 view: depth_view,
1750 depth_ops: Some(wgpu::Operations {
1751 load: wgpu::LoadOp::Clear(1.0),
1752 store: wgpu::StoreOp::Discard,
1753 }),
1754 stencil_ops: None,
1755 }),
1756 timestamp_writes: None,
1757 occlusion_query_set: None,
1758 });
1759
1760 pass.set_bind_group(0, camera_bg, &[]);
1761 for outlined in outlines {
1762 let Some(mesh) = self
1763 .resources
1764 .mesh_store
1765 .get(outlined.mesh_id)
1766 else {
1767 continue;
1768 };
1769 let pipeline = if outlined.two_sided {
1770 &self.resources.outline_mask_two_sided_pipeline
1771 } else {
1772 &self.resources.outline_mask_pipeline
1773 };
1774 pass.set_pipeline(pipeline);
1775 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
1776 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1777 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1778 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1779 }
1780 }
1781
1782 {
1784 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1785 label: Some("outline_edge_pass"),
1786 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1787 view: color_view,
1788 resolve_target: None,
1789 ops: wgpu::Operations {
1790 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1791 store: wgpu::StoreOp::Store,
1792 },
1793 depth_slice: None,
1794 })],
1795 depth_stencil_attachment: None,
1796 timestamp_writes: None,
1797 occlusion_query_set: None,
1798 });
1799 pass.set_pipeline(&self.resources.outline_edge_pipeline);
1800 pass.set_bind_group(0, edge_bg, &[]);
1801 pass.draw(0..3, 0..1);
1802 }
1803
1804 queue.submit(std::iter::once(encoder.finish()));
1805 }
1806
1807 {
1812 let w = frame.camera.viewport_size[0];
1813 let h = frame.camera.viewport_size[1];
1814 if let Some(sel_ref) = &frame.interaction.sub_selection {
1815 let needs_rebuild = {
1816 let slot = &self.viewport_slots[vp_idx];
1817 slot.sub_highlight_generation != sel_ref.version
1818 || slot.sub_highlight.is_none()
1819 };
1820 if needs_rebuild {
1821 self.resources.ensure_sub_highlight_pipelines(device);
1822 let data = self.resources.build_sub_highlight(
1823 device,
1824 queue,
1825 sel_ref,
1826 frame.interaction.sub_highlight_face_fill_color,
1827 frame.interaction.sub_highlight_edge_color,
1828 frame.interaction.sub_highlight_edge_width_px,
1829 frame.interaction.sub_highlight_vertex_size_px,
1830 w,
1831 h,
1832 );
1833 let slot = &mut self.viewport_slots[vp_idx];
1834 slot.sub_highlight = Some(data);
1835 slot.sub_highlight_generation = sel_ref.version;
1836 }
1837 } else {
1838 let slot = &mut self.viewport_slots[vp_idx];
1839 slot.sub_highlight = None;
1840 slot.sub_highlight_generation = u64::MAX;
1841 }
1842 }
1843 }
1844
1845 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, frame: &FrameData) {
1848 let (scene_fx, viewport_fx) = frame.effects.split();
1849 self.prepare_scene_internal(device, queue, frame, &scene_fx);
1850 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1851 }
1852}
1853
1854fn clip_box_outline(
1860 center: [f32; 3],
1861 half: [f32; 3],
1862 orientation: [[f32; 3]; 3],
1863 color: [f32; 4],
1864) -> PolylineItem {
1865 let ax = glam::Vec3::from(orientation[0]) * half[0];
1866 let ay = glam::Vec3::from(orientation[1]) * half[1];
1867 let az = glam::Vec3::from(orientation[2]) * half[2];
1868 let c = glam::Vec3::from(center);
1869
1870 let corners = [
1871 c - ax - ay - az,
1872 c + ax - ay - az,
1873 c + ax + ay - az,
1874 c - ax + ay - az,
1875 c - ax - ay + az,
1876 c + ax - ay + az,
1877 c + ax + ay + az,
1878 c - ax + ay + az,
1879 ];
1880 let edges: [(usize, usize); 12] = [
1881 (0, 1),
1882 (1, 2),
1883 (2, 3),
1884 (3, 0), (4, 5),
1886 (5, 6),
1887 (6, 7),
1888 (7, 4), (0, 4),
1890 (1, 5),
1891 (2, 6),
1892 (3, 7), ];
1894
1895 let mut positions = Vec::with_capacity(24);
1896 let mut strip_lengths = Vec::with_capacity(12);
1897 for (a, b) in edges {
1898 positions.push(corners[a].to_array());
1899 positions.push(corners[b].to_array());
1900 strip_lengths.push(2u32);
1901 }
1902
1903 let mut item = PolylineItem::default();
1904 item.positions = positions;
1905 item.strip_lengths = strip_lengths;
1906 item.default_color = color;
1907 item.line_width = 2.0;
1908 item
1909}
1910
1911fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
1913 let c = glam::Vec3::from(center);
1914 let segs = 64usize;
1915 let mut positions = Vec::with_capacity((segs + 1) * 3);
1916 let mut strip_lengths = Vec::with_capacity(3);
1917
1918 for axis in 0..3usize {
1919 let start = positions.len();
1920 for i in 0..=segs {
1921 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
1922 let (s, cs) = t.sin_cos();
1923 let p = c + match axis {
1924 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
1925 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
1926 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
1927 };
1928 positions.push(p.to_array());
1929 }
1930 strip_lengths.push((positions.len() - start) as u32);
1931 }
1932
1933 let mut item = PolylineItem::default();
1934 item.positions = positions;
1935 item.strip_lengths = strip_lengths;
1936 item.default_color = color;
1937 item.line_width = 2.0;
1938 item
1939}