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