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