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 let vp_w = vp_size[0];
786 let vp_h = vp_size[1];
787 for item in &frame.scene.screen_images {
788 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
789 continue;
790 }
791 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
792 self.screen_image_gpu_data.push(gpu);
793 }
794 }
795
796 self.streamtube_gpu_data.clear();
800 if !frame.scene.streamtube_items.is_empty() {
801 resources.ensure_streamtube_pipeline(device);
802 for item in &frame.scene.streamtube_items {
803 if item.positions.is_empty() || item.strip_lengths.is_empty() {
804 continue;
805 }
806 let gpu_data = resources.upload_streamtube(device, queue, item);
807 if gpu_data.index_count > 0 {
808 self.streamtube_gpu_data.push(gpu_data);
809 }
810 }
811 }
812
813 self.volume_gpu_data.clear();
819 if !frame.scene.volumes.is_empty() {
820 resources.ensure_volume_pipeline(device);
821 let clip_planes_for_vol: Vec<crate::renderer::types::ClipPlane> = frame
823 .effects
824 .clip_objects
825 .iter()
826 .filter(|o| o.enabled)
827 .filter_map(|o| {
828 if let ClipShape::Plane {
829 normal,
830 distance,
831 cap_color,
832 } = o.shape
833 {
834 Some(crate::renderer::types::ClipPlane {
835 normal,
836 distance,
837 enabled: true,
838 cap_color,
839 })
840 } else {
841 None
842 }
843 })
844 .collect();
845 for item in &frame.scene.volumes {
846 let gpu = resources.upload_volume_frame(device, queue, item, &clip_planes_for_vol);
847 self.volume_gpu_data.push(gpu);
848 }
849 }
850
851 {
853 let total = scene_items.len() as u32;
854 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
855 let mut draw_calls = 0u32;
856 let mut triangles = 0u64;
857 let instanced_batch_count = if self.use_instancing {
858 self.instanced_batches.len() as u32
859 } else {
860 0
861 };
862
863 if self.use_instancing {
864 for batch in &self.instanced_batches {
865 if let Some(mesh) = resources
866 .mesh_store
867 .get(batch.mesh_id)
868 {
869 draw_calls += 1;
870 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
871 }
872 }
873 } else {
874 for item in scene_items {
875 if !item.visible {
876 continue;
877 }
878 if let Some(mesh) = resources
879 .mesh_store
880 .get(item.mesh_id)
881 {
882 draw_calls += 1;
883 triangles += (mesh.index_count / 3) as u64;
884 }
885 }
886 }
887
888 self.last_stats = crate::renderer::stats::FrameStats {
889 total_objects: total,
890 visible_objects: visible,
891 culled_objects: total.saturating_sub(visible),
892 draw_calls,
893 instanced_batches: instanced_batch_count,
894 triangles_submitted: triangles,
895 shadow_draw_calls: 0, };
897 }
898
899 if lighting.shadows_enabled && !scene_items.is_empty() {
903 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
904 label: Some("shadow_pass_encoder"),
905 });
906 {
907 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
908 label: Some("shadow_pass"),
909 color_attachments: &[],
910 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
911 view: &resources.shadow_map_view,
912 depth_ops: Some(wgpu::Operations {
913 load: wgpu::LoadOp::Clear(1.0),
914 store: wgpu::StoreOp::Store,
915 }),
916 stencil_ops: None,
917 }),
918 timestamp_writes: None,
919 occlusion_query_set: None,
920 });
921
922 let mut shadow_draws = 0u32;
923 let tile_px = tile_size as f32;
924
925 if self.use_instancing {
926 if let (Some(pipeline), Some(instance_bg)) = (
927 &resources.shadow_instanced_pipeline,
928 self.instanced_batches.first().and_then(|b| {
929 resources.instance_bind_groups.get(&(
930 b.texture_id.unwrap_or(u64::MAX),
931 b.normal_map_id.unwrap_or(u64::MAX),
932 b.ao_map_id.unwrap_or(u64::MAX),
933 ))
934 }),
935 ) {
936 for cascade in 0..effective_cascade_count {
937 let tile_col = (cascade % 2) as f32;
938 let tile_row = (cascade / 2) as f32;
939 shadow_pass.set_viewport(
940 tile_col * tile_px,
941 tile_row * tile_px,
942 tile_px,
943 tile_px,
944 0.0,
945 1.0,
946 );
947 shadow_pass.set_scissor_rect(
948 (tile_col * tile_px) as u32,
949 (tile_row * tile_px) as u32,
950 tile_size,
951 tile_size,
952 );
953
954 shadow_pass.set_pipeline(pipeline);
955
956 queue.write_buffer(
957 resources.shadow_instanced_cascade_bufs[cascade]
958 .as_ref()
959 .expect("shadow_instanced_cascade_bufs not allocated"),
960 0,
961 bytemuck::cast_slice(
962 &cascade_view_projs[cascade].to_cols_array_2d(),
963 ),
964 );
965
966 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
967 .as_ref()
968 .expect("shadow_instanced_cascade_bgs not allocated");
969 shadow_pass.set_bind_group(0, cascade_bg, &[]);
970 shadow_pass.set_bind_group(1, instance_bg, &[]);
971
972 for batch in &self.instanced_batches {
973 if batch.is_transparent {
974 continue;
975 }
976 let Some(mesh) = resources
977 .mesh_store
978 .get(batch.mesh_id)
979 else {
980 continue;
981 };
982 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
983 shadow_pass.set_index_buffer(
984 mesh.index_buffer.slice(..),
985 wgpu::IndexFormat::Uint32,
986 );
987 shadow_pass.draw_indexed(
988 0..mesh.index_count,
989 0,
990 batch.instance_offset
991 ..batch.instance_offset + batch.instance_count,
992 );
993 shadow_draws += 1;
994 }
995 }
996 }
997 } else {
998 for cascade in 0..effective_cascade_count {
999 let tile_col = (cascade % 2) as f32;
1000 let tile_row = (cascade / 2) as f32;
1001 shadow_pass.set_viewport(
1002 tile_col * tile_px,
1003 tile_row * tile_px,
1004 tile_px,
1005 tile_px,
1006 0.0,
1007 1.0,
1008 );
1009 shadow_pass.set_scissor_rect(
1010 (tile_col * tile_px) as u32,
1011 (tile_row * tile_px) as u32,
1012 tile_size,
1013 tile_size,
1014 );
1015
1016 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1017 shadow_pass.set_bind_group(
1018 0,
1019 &resources.shadow_bind_group,
1020 &[cascade as u32 * 256],
1021 );
1022
1023 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1024 &cascade_view_projs[cascade],
1025 );
1026
1027 for item in scene_items.iter() {
1028 if !item.visible {
1029 continue;
1030 }
1031 if item.material.opacity < 1.0 {
1032 continue;
1033 }
1034 let Some(mesh) = resources
1035 .mesh_store
1036 .get(item.mesh_id)
1037 else {
1038 continue;
1039 };
1040
1041 let world_aabb = mesh
1042 .aabb
1043 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1044 if cascade_frustum.cull_aabb(&world_aabb) {
1045 continue;
1046 }
1047
1048 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1049 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1050 shadow_pass.set_index_buffer(
1051 mesh.index_buffer.slice(..),
1052 wgpu::IndexFormat::Uint32,
1053 );
1054 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1055 shadow_draws += 1;
1056 }
1057 }
1058 }
1059 drop(shadow_pass);
1060 self.last_stats.shadow_draw_calls = shadow_draws;
1061 }
1062 queue.submit(std::iter::once(encoder.finish()));
1063 }
1064 }
1065
1066 pub(super) fn prepare_viewport_internal(
1071 &mut self,
1072 device: &wgpu::Device,
1073 queue: &wgpu::Queue,
1074 frame: &FrameData,
1075 viewport_fx: &ViewportEffects<'_>,
1076 ) {
1077 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1080
1081 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1082 SurfaceSubmission::Flat(items) => items,
1083 };
1084
1085 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1087
1088 {
1089 let resources = &mut self.resources;
1090
1091 {
1093 let mut planes = [[0.0f32; 4]; 6];
1094 let mut count = 0u32;
1095 let mut clip_vol_uniform: ClipVolumeUniform = bytemuck::Zeroable::zeroed(); for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1098 match obj.shape {
1099 ClipShape::Plane {
1100 normal, distance, ..
1101 } if count < 6 => {
1102 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1103 count += 1;
1104 }
1105 ClipShape::Box {
1106 center,
1107 half_extents,
1108 orientation,
1109 } if clip_vol_uniform.volume_type == 0 => {
1110 clip_vol_uniform.volume_type = 2;
1111 clip_vol_uniform.box_center = center;
1112 clip_vol_uniform.box_half_extents = half_extents;
1113 clip_vol_uniform.box_col0 = orientation[0];
1114 clip_vol_uniform.box_col1 = orientation[1];
1115 clip_vol_uniform.box_col2 = orientation[2];
1116 }
1117 ClipShape::Sphere { center, radius }
1118 if clip_vol_uniform.volume_type == 0 =>
1119 {
1120 clip_vol_uniform.volume_type = 3;
1121 clip_vol_uniform.sphere_center = center;
1122 clip_vol_uniform.sphere_radius = radius;
1123 }
1124 _ => {}
1125 }
1126 }
1127
1128 let clip_uniform = ClipPlanesUniform {
1129 planes,
1130 count,
1131 _pad0: 0,
1132 viewport_width: frame.camera.viewport_size[0].max(1.0),
1133 viewport_height: frame.camera.viewport_size[1].max(1.0),
1134 };
1135 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1137 queue.write_buffer(
1138 &slot.clip_planes_buf,
1139 0,
1140 bytemuck::cast_slice(&[clip_uniform]),
1141 );
1142 queue.write_buffer(
1143 &slot.clip_volume_buf,
1144 0,
1145 bytemuck::cast_slice(&[clip_vol_uniform]),
1146 );
1147 }
1148 queue.write_buffer(
1150 &resources.clip_planes_uniform_buf,
1151 0,
1152 bytemuck::cast_slice(&[clip_uniform]),
1153 );
1154 queue.write_buffer(
1155 &resources.clip_volume_uniform_buf,
1156 0,
1157 bytemuck::cast_slice(&[clip_vol_uniform]),
1158 );
1159 }
1160
1161 let camera_uniform = frame.camera.render_camera.camera_uniform();
1163 queue.write_buffer(
1165 &resources.camera_uniform_buf,
1166 0,
1167 bytemuck::cast_slice(&[camera_uniform]),
1168 );
1169 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1171 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1172 }
1173
1174 if frame.viewport.show_grid {
1176 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1177 if !eye.is_finite() {
1178 tracing::warn!(
1179 eye_x = eye.x,
1180 eye_y = eye.y,
1181 eye_z = eye.z,
1182 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1183 );
1184 } else {
1185 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1186
1187 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1188 (frame.viewport.grid_cell_size, 1.0_f32)
1189 } else {
1190 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1191 let world_per_pixel =
1192 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1193 / frame.camera.viewport_size[1].max(1.0);
1194 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1195 let mut s = 1.0_f32;
1196 let mut iters = 0u32;
1197 while s < target {
1198 s *= 10.0;
1199 iters += 1;
1200 }
1201 let ratio = (target / s).clamp(0.0, 1.0);
1202 let fade = if ratio < 0.5 {
1203 1.0_f32
1204 } else {
1205 let t = (ratio - 0.5) * 2.0;
1206 1.0 - t * t * (3.0 - 2.0 * t)
1207 };
1208 tracing::debug!(
1209 eye_z = eye.z,
1210 vertical_depth,
1211 world_per_pixel,
1212 target,
1213 spacing = s,
1214 lod_iters = iters,
1215 ratio,
1216 minor_fade = fade,
1217 "grid LOD"
1218 );
1219 (s, fade)
1220 };
1221
1222 let spacing_major = spacing * 10.0;
1223 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1224 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1225 tracing::debug!(
1226 spacing_minor = spacing,
1227 spacing_major,
1228 snap_x,
1229 snap_y,
1230 eye_x = eye.x,
1231 eye_y = eye.y,
1232 eye_z = eye.z,
1233 "grid snap"
1234 );
1235
1236 let orient = frame.camera.render_camera.orientation;
1237 let right = orient * glam::Vec3::X;
1238 let up = orient * glam::Vec3::Y;
1239 let back = orient * glam::Vec3::Z;
1240 let cam_to_world = [
1241 [right.x, right.y, right.z, 0.0_f32],
1242 [up.x, up.y, up.z, 0.0_f32],
1243 [back.x, back.y, back.z, 0.0_f32],
1244 ];
1245 let aspect =
1246 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1247 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1248
1249 let uniform = GridUniform {
1250 view_proj: view_proj_mat,
1251 cam_to_world,
1252 tan_half_fov,
1253 aspect,
1254 _pad_ivp: [0.0; 2],
1255 eye_pos: frame.camera.render_camera.eye_position,
1256 grid_z: frame.viewport.grid_z,
1257 spacing_minor: spacing,
1258 spacing_major,
1259 snap_origin: [snap_x, snap_y],
1260 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1261 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1262 };
1263 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1265 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1266 }
1267 queue.write_buffer(
1269 &resources.grid_uniform_buf,
1270 0,
1271 bytemuck::cast_slice(&[uniform]),
1272 );
1273 }
1274 }
1275 {
1279 let gp = &viewport_fx.ground_plane;
1280 let mode_u32: u32 = match gp.mode {
1281 crate::renderer::types::GroundPlaneMode::None => 0,
1282 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1283 crate::renderer::types::GroundPlaneMode::Tile => 2,
1284 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1285 };
1286 let orient = frame.camera.render_camera.orientation;
1287 let right = orient * glam::Vec3::X;
1288 let up = orient * glam::Vec3::Y;
1289 let back = orient * glam::Vec3::Z;
1290 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1291 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1292 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1293 let gp_uniform = crate::resources::GroundPlaneUniform {
1294 view_proj: vp,
1295 cam_right: [right.x, right.y, right.z, 0.0],
1296 cam_up: [up.x, up.y, up.z, 0.0],
1297 cam_back: [back.x, back.y, back.z, 0.0],
1298 eye_pos: frame.camera.render_camera.eye_position,
1299 height: gp.height,
1300 color: gp.color,
1301 shadow_color: gp.shadow_color,
1302 light_vp: gp_cascade0_mat,
1303 tan_half_fov,
1304 aspect,
1305 tile_size: gp.tile_size,
1306 shadow_bias: 0.002,
1307 mode: mode_u32,
1308 shadow_opacity: gp.shadow_opacity,
1309 _pad: [0.0; 2],
1310 };
1311 queue.write_buffer(
1312 &resources.ground_plane_uniform_buf,
1313 0,
1314 bytemuck::cast_slice(&[gp_uniform]),
1315 );
1316 }
1317 } let vp_idx = frame.camera.viewport_index;
1326
1327 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1329 if frame.interaction.outline_selected {
1330 let resources = &self.resources;
1331 for item in scene_items {
1332 if !item.visible || !item.selected {
1333 continue;
1334 }
1335 let uniform = OutlineUniform {
1336 model: item.model,
1337 color: [0.0; 4], pixel_offset: 0.0,
1339 _pad: [0.0; 3],
1340 };
1341 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1342 label: Some("outline_mask_uniform_buf"),
1343 size: std::mem::size_of::<OutlineUniform>() as u64,
1344 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1345 mapped_at_creation: false,
1346 });
1347 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1348 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1349 label: Some("outline_mask_object_bg"),
1350 layout: &resources.outline_bind_group_layout,
1351 entries: &[wgpu::BindGroupEntry {
1352 binding: 0,
1353 resource: buf.as_entire_binding(),
1354 }],
1355 });
1356 outline_object_buffers.push(OutlineObjectBuffers {
1357 mesh_id: item.mesh_id,
1358 two_sided: item.material.is_two_sided(),
1359 _mask_uniform_buf: buf,
1360 mask_bind_group: bg,
1361 });
1362 }
1363 }
1364
1365 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1367 if frame.interaction.xray_selected {
1368 let resources = &self.resources;
1369 for item in scene_items {
1370 if !item.visible || !item.selected {
1371 continue;
1372 }
1373 let uniform = OutlineUniform {
1374 model: item.model,
1375 color: frame.interaction.xray_color,
1376 pixel_offset: 0.0,
1377 _pad: [0.0; 3],
1378 };
1379 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1380 label: Some("xray_uniform_buf"),
1381 size: std::mem::size_of::<OutlineUniform>() as u64,
1382 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1383 mapped_at_creation: false,
1384 });
1385 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1386 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1387 label: Some("xray_object_bg"),
1388 layout: &resources.outline_bind_group_layout,
1389 entries: &[wgpu::BindGroupEntry {
1390 binding: 0,
1391 resource: buf.as_entire_binding(),
1392 }],
1393 });
1394 xray_object_buffers.push((item.mesh_id, buf, bg));
1395 }
1396 }
1397
1398 let mut constraint_line_buffers = Vec::new();
1400 for overlay in &frame.interaction.constraint_overlays {
1401 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1402 }
1403
1404 let mut clip_plane_fill_buffers = Vec::new();
1406 let mut clip_plane_line_buffers = Vec::new();
1407 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1408 let Some(base_color) = obj.color else {
1409 continue;
1410 };
1411 if let ClipShape::Plane {
1412 normal, distance, ..
1413 } = obj.shape
1414 {
1415 let n = glam::Vec3::from(normal);
1416 let center = n * (-distance);
1419 let active = obj.active;
1420 let hovered = obj.hovered || active;
1421
1422 let fill_color = if active {
1423 [
1424 base_color[0] * 0.5,
1425 base_color[1] * 0.5,
1426 base_color[2] * 0.5,
1427 base_color[3] * 0.5,
1428 ]
1429 } else if hovered {
1430 [
1431 base_color[0] * 0.8,
1432 base_color[1] * 0.8,
1433 base_color[2] * 0.8,
1434 base_color[3] * 0.6,
1435 ]
1436 } else {
1437 [
1438 base_color[0] * 0.5,
1439 base_color[1] * 0.5,
1440 base_color[2] * 0.5,
1441 base_color[3] * 0.3,
1442 ]
1443 };
1444 let border_color = if active {
1445 [base_color[0], base_color[1], base_color[2], 0.9]
1446 } else if hovered {
1447 [base_color[0], base_color[1], base_color[2], 0.8]
1448 } else {
1449 [
1450 base_color[0] * 0.9,
1451 base_color[1] * 0.9,
1452 base_color[2] * 0.9,
1453 0.6,
1454 ]
1455 };
1456
1457 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
1458 center,
1459 normal: n,
1460 extent: obj.extent,
1461 fill_color,
1462 border_color,
1463 hovered,
1464 active,
1465 };
1466 clip_plane_fill_buffers.push(
1467 self.resources
1468 .create_clip_plane_fill_overlay(device, &overlay),
1469 );
1470 clip_plane_line_buffers.push(
1471 self.resources
1472 .create_clip_plane_line_overlay(device, &overlay),
1473 );
1474 } else {
1475 self.resources.ensure_polyline_pipeline(device);
1479 match obj.shape {
1480 ClipShape::Box {
1481 center,
1482 half_extents,
1483 orientation,
1484 } => {
1485 let polyline =
1486 clip_box_outline(center, half_extents, orientation, base_color);
1487 let vp_size = frame.camera.viewport_size;
1488 let gpu = self
1489 .resources
1490 .upload_polyline(device, queue, &polyline, vp_size);
1491 self.polyline_gpu_data.push(gpu);
1492 }
1493 ClipShape::Sphere { center, radius } => {
1494 let polyline = clip_sphere_outline(center, radius, base_color);
1495 let vp_size = frame.camera.viewport_size;
1496 let gpu = self
1497 .resources
1498 .upload_polyline(device, queue, &polyline, vp_size);
1499 self.polyline_gpu_data.push(gpu);
1500 }
1501 _ => {}
1502 }
1503 }
1504 }
1505
1506 let mut cap_buffers = Vec::new();
1508 if viewport_fx.cap_fill_enabled {
1509 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1510 if let ClipShape::Plane {
1511 normal,
1512 distance,
1513 cap_color,
1514 } = obj.shape
1515 {
1516 let plane_n = glam::Vec3::from(normal);
1517 for item in scene_items.iter().filter(|i| i.visible) {
1518 let Some(mesh) = self
1519 .resources
1520 .mesh_store
1521 .get(item.mesh_id)
1522 else {
1523 continue;
1524 };
1525 let model = glam::Mat4::from_cols_array_2d(&item.model);
1526 let world_aabb = mesh.aabb.transformed(&model);
1527 if !world_aabb.intersects_plane(plane_n, distance) {
1528 continue;
1529 }
1530 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1531 else {
1532 continue;
1533 };
1534 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
1535 pos, idx, &model, plane_n, distance,
1536 ) {
1537 let bc = item.material.base_color;
1538 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
1539 let buf = self.resources.upload_cap_geometry(device, &cap, color);
1540 cap_buffers.push(buf);
1541 }
1542 }
1543 }
1544 }
1545 }
1546
1547 let axes_verts = if frame.viewport.show_axes_indicator
1549 && frame.camera.viewport_size[0] > 0.0
1550 && frame.camera.viewport_size[1] > 0.0
1551 {
1552 let verts = crate::widgets::axes_indicator::build_axes_geometry(
1553 frame.camera.viewport_size[0],
1554 frame.camera.viewport_size[1],
1555 frame.camera.render_camera.orientation,
1556 );
1557 if verts.is_empty() { None } else { Some(verts) }
1558 } else {
1559 None
1560 };
1561
1562 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
1564 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
1565 frame.interaction.gizmo_mode,
1566 frame.interaction.gizmo_hovered,
1567 frame.interaction.gizmo_space_orientation,
1568 );
1569 (verts, indices, model)
1570 });
1571
1572 {
1576 let slot = &mut self.viewport_slots[vp_idx];
1577 slot.outline_object_buffers = outline_object_buffers;
1578 slot.xray_object_buffers = xray_object_buffers;
1579 slot.constraint_line_buffers = constraint_line_buffers;
1580 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
1581 slot.clip_plane_line_buffers = clip_plane_line_buffers;
1582 slot.cap_buffers = cap_buffers;
1583
1584 if let Some(verts) = axes_verts {
1586 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
1587 if byte_size > slot.axes_vertex_buffer.size() {
1588 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1589 label: Some("vp_axes_vertex_buf"),
1590 size: byte_size,
1591 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1592 mapped_at_creation: false,
1593 });
1594 }
1595 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
1596 slot.axes_vertex_count = verts.len() as u32;
1597 } else {
1598 slot.axes_vertex_count = 0;
1599 }
1600
1601 if let Some((verts, indices, model)) = gizmo_update {
1603 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1604 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1605 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
1606 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1607 label: Some("vp_gizmo_vertex_buf"),
1608 size: vert_bytes.len() as u64,
1609 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1610 mapped_at_creation: false,
1611 });
1612 }
1613 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
1614 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1615 label: Some("vp_gizmo_index_buf"),
1616 size: idx_bytes.len() as u64,
1617 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1618 mapped_at_creation: false,
1619 });
1620 }
1621 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
1622 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
1623 slot.gizmo_index_count = indices.len() as u32;
1624 let uniform = crate::interaction::gizmo::GizmoUniform {
1625 model: model.to_cols_array_2d(),
1626 };
1627 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
1628 }
1629 }
1630
1631 if frame.interaction.outline_selected
1642 && !self.viewport_slots[vp_idx]
1643 .outline_object_buffers
1644 .is_empty()
1645 {
1646 let w = frame.camera.viewport_size[0] as u32;
1647 let h = frame.camera.viewport_size[1] as u32;
1648
1649 self.ensure_viewport_hdr(
1651 device,
1652 queue,
1653 vp_idx,
1654 w.max(1),
1655 h.max(1),
1656 frame.effects.post_process.ssaa_factor.max(1),
1657 );
1658
1659 {
1661 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1662 let edge_uniform = OutlineEdgeUniform {
1663 color: frame.interaction.outline_color,
1664 radius: frame.interaction.outline_width_px,
1665 viewport_w: w as f32,
1666 viewport_h: h as f32,
1667 _pad: 0.0,
1668 };
1669 queue.write_buffer(
1670 &slot_hdr.outline_edge_uniform_buf,
1671 0,
1672 bytemuck::cast_slice(&[edge_uniform]),
1673 );
1674 }
1675
1676 let slot_ref = &self.viewport_slots[vp_idx];
1679 let outlines_ptr =
1680 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
1681 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
1682 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
1683 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
1684 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
1685 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
1686 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
1687 let (outlines, camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
1690 (
1691 &*outlines_ptr,
1692 &*camera_bg_ptr,
1693 &*mask_view_ptr,
1694 &*color_view_ptr,
1695 &*depth_view_ptr,
1696 &*edge_bg_ptr,
1697 )
1698 };
1699
1700 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1701 label: Some("outline_offscreen_encoder"),
1702 });
1703
1704 {
1706 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1707 label: Some("outline_mask_pass"),
1708 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1709 view: mask_view,
1710 resolve_target: None,
1711 ops: wgpu::Operations {
1712 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1713 store: wgpu::StoreOp::Store,
1714 },
1715 depth_slice: None,
1716 })],
1717 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1718 view: depth_view,
1719 depth_ops: Some(wgpu::Operations {
1720 load: wgpu::LoadOp::Clear(1.0),
1721 store: wgpu::StoreOp::Discard,
1722 }),
1723 stencil_ops: None,
1724 }),
1725 timestamp_writes: None,
1726 occlusion_query_set: None,
1727 });
1728
1729 pass.set_bind_group(0, camera_bg, &[]);
1730 for outlined in outlines {
1731 let Some(mesh) = self
1732 .resources
1733 .mesh_store
1734 .get(outlined.mesh_id)
1735 else {
1736 continue;
1737 };
1738 let pipeline = if outlined.two_sided {
1739 &self.resources.outline_mask_two_sided_pipeline
1740 } else {
1741 &self.resources.outline_mask_pipeline
1742 };
1743 pass.set_pipeline(pipeline);
1744 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
1745 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1746 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1747 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1748 }
1749 }
1750
1751 {
1753 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1754 label: Some("outline_edge_pass"),
1755 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1756 view: color_view,
1757 resolve_target: None,
1758 ops: wgpu::Operations {
1759 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1760 store: wgpu::StoreOp::Store,
1761 },
1762 depth_slice: None,
1763 })],
1764 depth_stencil_attachment: None,
1765 timestamp_writes: None,
1766 occlusion_query_set: None,
1767 });
1768 pass.set_pipeline(&self.resources.outline_edge_pipeline);
1769 pass.set_bind_group(0, edge_bg, &[]);
1770 pass.draw(0..3, 0..1);
1771 }
1772
1773 queue.submit(std::iter::once(encoder.finish()));
1774 }
1775 }
1776
1777 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, frame: &FrameData) {
1780 let (scene_fx, viewport_fx) = frame.effects.split();
1781 self.prepare_scene_internal(device, queue, frame, &scene_fx);
1782 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1783 }
1784}
1785
1786fn clip_box_outline(
1792 center: [f32; 3],
1793 half: [f32; 3],
1794 orientation: [[f32; 3]; 3],
1795 color: [f32; 4],
1796) -> PolylineItem {
1797 let ax = glam::Vec3::from(orientation[0]) * half[0];
1798 let ay = glam::Vec3::from(orientation[1]) * half[1];
1799 let az = glam::Vec3::from(orientation[2]) * half[2];
1800 let c = glam::Vec3::from(center);
1801
1802 let corners = [
1803 c - ax - ay - az,
1804 c + ax - ay - az,
1805 c + ax + ay - az,
1806 c - ax + ay - az,
1807 c - ax - ay + az,
1808 c + ax - ay + az,
1809 c + ax + ay + az,
1810 c - ax + ay + az,
1811 ];
1812 let edges: [(usize, usize); 12] = [
1813 (0, 1),
1814 (1, 2),
1815 (2, 3),
1816 (3, 0), (4, 5),
1818 (5, 6),
1819 (6, 7),
1820 (7, 4), (0, 4),
1822 (1, 5),
1823 (2, 6),
1824 (3, 7), ];
1826
1827 let mut positions = Vec::with_capacity(24);
1828 let mut strip_lengths = Vec::with_capacity(12);
1829 for (a, b) in edges {
1830 positions.push(corners[a].to_array());
1831 positions.push(corners[b].to_array());
1832 strip_lengths.push(2u32);
1833 }
1834
1835 let mut item = PolylineItem::default();
1836 item.positions = positions;
1837 item.strip_lengths = strip_lengths;
1838 item.default_color = color;
1839 item.line_width = 2.0;
1840 item
1841}
1842
1843fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
1845 let c = glam::Vec3::from(center);
1846 let segs = 64usize;
1847 let mut positions = Vec::with_capacity((segs + 1) * 3);
1848 let mut strip_lengths = Vec::with_capacity(3);
1849
1850 for axis in 0..3usize {
1851 let start = positions.len();
1852 for i in 0..=segs {
1853 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
1854 let (s, cs) = t.sin_cos();
1855 let p = c + match axis {
1856 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
1857 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
1858 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
1859 };
1860 positions.push(p.to_array());
1861 }
1862 strip_lengths.push((positions.len() - start) as u32);
1863 }
1864
1865 let mut item = PolylineItem::default();
1866 item.positions = positions;
1867 item.strip_lengths = strip_lengths;
1868 item.default_color = color;
1869 item.line_width = 2.0;
1870 item
1871}