1use super::types::{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
319 let (ibl_enabled, ibl_intensity, ibl_rotation, show_skybox) =
322 if let Some(env) = scene_fx.environment {
323 if resources.ibl_irradiance_view.is_some() {
324 (
325 1u32,
326 env.intensity,
327 env.rotation,
328 if env.show_skybox { 1u32 } else { 0 },
329 )
330 } else {
331 (0, 0.0, 0.0, 0)
332 }
333 } else {
334 (0, 0.0, 0.0, 0)
335 };
336
337 let lights_uniform = LightsUniform {
338 count: light_count,
339 shadow_bias: lighting.shadow_bias,
340 shadows_enabled: if lighting.shadows_enabled { 1 } else { 0 },
341 _pad: 0,
342 sky_color: lighting.sky_color,
343 hemisphere_intensity: lighting.hemisphere_intensity,
344 ground_color: lighting.ground_color,
345 _pad2: 0.0,
346 lights: lights_arr,
347 ibl_enabled,
348 ibl_intensity,
349 ibl_rotation,
350 show_skybox,
351 };
352 queue.write_buffer(
353 &resources.light_uniform_buf,
354 0,
355 bytemuck::cast_slice(&[lights_uniform]),
356 );
357
358 const SHADOW_SLOT_STRIDE: u64 = 256;
362 for c in 0..4usize {
363 queue.write_buffer(
364 &resources.shadow_uniform_buf,
365 c as u64 * SHADOW_SLOT_STRIDE,
366 bytemuck::cast_slice(&cascade_view_projs[c].to_cols_array_2d()),
367 );
368 }
369
370 let visible_count = scene_items.iter().filter(|i| i.visible).count();
373 let prev_use_instancing = self.use_instancing;
374 self.use_instancing = visible_count > INSTANCING_THRESHOLD;
375
376 if self.use_instancing != prev_use_instancing {
379 self.instanced_batches.clear();
380 self.last_scene_generation = u64::MAX;
381 self.last_scene_items_count = usize::MAX;
382 }
383
384 let has_scalar_items = scene_items.iter().any(|i| i.active_attribute.is_some());
388 let has_two_sided_items = scene_items.iter().any(|i| i.two_sided);
389 let has_matcap_items = scene_items.iter().any(|i| i.material.matcap_id.is_some());
390 if !self.use_instancing
391 || frame.viewport.wireframe_mode
392 || has_scalar_items
393 || has_two_sided_items
394 || has_matcap_items
395 {
396 for item in scene_items {
397 if resources
398 .mesh_store
399 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
400 .is_none()
401 {
402 tracing::warn!(
403 mesh_index = item.mesh_index,
404 "scene item mesh_index invalid, skipping"
405 );
406 continue;
407 };
408 let m = &item.material;
409 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
411 let range = item
412 .scalar_range
413 .or_else(|| {
414 resources
415 .mesh_store
416 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
417 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
418 })
419 .unwrap_or((0.0, 1.0));
420 (1u32, range.0, range.1)
421 } else {
422 (0u32, 0.0, 1.0)
423 };
424 let obj_uniform = ObjectUniform {
425 model: item.model,
426 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
427 selected: if item.selected { 1 } else { 0 },
428 wireframe: if frame.viewport.wireframe_mode { 1 } else { 0 },
429 ambient: m.ambient,
430 diffuse: m.diffuse,
431 specular: m.specular,
432 shininess: m.shininess,
433 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
434 use_pbr: if m.use_pbr { 1 } else { 0 },
435 metallic: m.metallic,
436 roughness: m.roughness,
437 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
438 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
439 has_attribute: has_attr,
440 scalar_min: s_min,
441 scalar_max: s_max,
442 _pad_scalar: 0,
443 nan_color: item.nan_color.unwrap_or([0.0; 4]),
444 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
445 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
446 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
447 _pad2: 0,
448 use_face_color: u32::from(
449 item.active_attribute.as_ref()
450 .map_or(false, |a| a.kind == crate::resources::AttributeKind::FaceColor)
451 ),
452 _pad3: [0; 3],
453 };
454
455 let normal_obj_uniform = ObjectUniform {
456 model: item.model,
457 color: [1.0, 1.0, 1.0, 1.0],
458 selected: 0,
459 wireframe: 0,
460 ambient: 0.15,
461 diffuse: 0.75,
462 specular: 0.4,
463 shininess: 32.0,
464 has_texture: 0,
465 use_pbr: 0,
466 metallic: 0.0,
467 roughness: 0.5,
468 has_normal_map: 0,
469 has_ao_map: 0,
470 has_attribute: 0,
471 scalar_min: 0.0,
472 scalar_max: 1.0,
473 _pad_scalar: 0,
474 nan_color: [0.0; 4],
475 use_nan_color: 0,
476 use_matcap: 0,
477 matcap_blendable: 0,
478 _pad2: 0,
479 use_face_color: 0,
480 _pad3: [0; 3],
481 };
482
483 {
485 let mesh = resources
486 .mesh_store
487 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
488 .unwrap();
489 queue.write_buffer(
490 &mesh.object_uniform_buf,
491 0,
492 bytemuck::cast_slice(&[obj_uniform]),
493 );
494 queue.write_buffer(
495 &mesh.normal_uniform_buf,
496 0,
497 bytemuck::cast_slice(&[normal_obj_uniform]),
498 );
499 } resources.update_mesh_texture_bind_group(
503 device,
504 item.mesh_index,
505 item.material.texture_id,
506 item.material.normal_map_id,
507 item.material.ao_map_id,
508 item.colormap_id,
509 item.active_attribute.as_ref().map(|a| a.name.as_str()),
510 item.material.matcap_id,
511 );
512 }
513 }
514
515 if self.use_instancing {
516 resources.ensure_instanced_pipelines(device);
517
518 let cache_valid = frame.scene.generation == self.last_scene_generation
523 && frame.interaction.selection_generation == self.last_selection_generation
524 && scene_items.len() == self.last_scene_items_count;
525
526 if !cache_valid {
527 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
529 .iter()
530 .filter(|item| {
531 item.visible
532 && item.active_attribute.is_none()
533 && !item.two_sided
534 && item.material.matcap_id.is_none()
535 && resources
536 .mesh_store
537 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
538 .is_some()
539 })
540 .collect();
541
542 sorted_items.sort_unstable_by_key(|item| {
543 (
544 item.mesh_index,
545 item.material.texture_id,
546 item.material.normal_map_id,
547 item.material.ao_map_id,
548 )
549 });
550
551 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
552 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
553
554 if !sorted_items.is_empty() {
555 let mut batch_start = 0usize;
556 for i in 1..=sorted_items.len() {
557 let at_end = i == sorted_items.len();
558 let key_changed = !at_end && {
559 let a = sorted_items[batch_start];
560 let b = sorted_items[i];
561 a.mesh_index != b.mesh_index
562 || a.material.texture_id != b.material.texture_id
563 || a.material.normal_map_id != b.material.normal_map_id
564 || a.material.ao_map_id != b.material.ao_map_id
565 };
566
567 if at_end || key_changed {
568 let batch_items = &sorted_items[batch_start..i];
569 let rep = batch_items[0];
570 let instance_offset = all_instances.len() as u32;
571 let is_transparent = rep.material.opacity < 1.0;
572
573 for item in batch_items {
574 let m = &item.material;
575 all_instances.push(InstanceData {
576 model: item.model,
577 color: [
578 m.base_color[0],
579 m.base_color[1],
580 m.base_color[2],
581 m.opacity,
582 ],
583 selected: if item.selected { 1 } else { 0 },
584 wireframe: 0, ambient: m.ambient,
586 diffuse: m.diffuse,
587 specular: m.specular,
588 shininess: m.shininess,
589 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
590 use_pbr: if m.use_pbr { 1 } else { 0 },
591 metallic: m.metallic,
592 roughness: m.roughness,
593 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
594 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
595 });
596 }
597
598 instanced_batches.push(InstancedBatch {
599 mesh_index: rep.mesh_index,
600 texture_id: rep.material.texture_id,
601 normal_map_id: rep.material.normal_map_id,
602 ao_map_id: rep.material.ao_map_id,
603 instance_offset,
604 instance_count: batch_items.len() as u32,
605 is_transparent,
606 });
607
608 batch_start = i;
609 }
610 }
611 }
612
613 self.cached_instance_data = all_instances;
614 self.cached_instanced_batches = instanced_batches;
615
616 resources.upload_instance_data(device, queue, &self.cached_instance_data);
617
618 self.instanced_batches = self.cached_instanced_batches.clone();
619
620 self.last_scene_generation = frame.scene.generation;
621 self.last_selection_generation = frame.interaction.selection_generation;
622 self.last_scene_items_count = scene_items.len();
623
624 for batch in &self.instanced_batches {
625 resources.get_instance_bind_group(
626 device,
627 batch.texture_id,
628 batch.normal_map_id,
629 batch.ao_map_id,
630 );
631 }
632 } else {
633 for batch in &self.instanced_batches {
634 resources.get_instance_bind_group(
635 device,
636 batch.texture_id,
637 batch.normal_map_id,
638 batch.ao_map_id,
639 );
640 }
641 }
642 }
643
644 self.point_cloud_gpu_data.clear();
648 if !frame.scene.point_clouds.is_empty() {
649 resources.ensure_point_cloud_pipeline(device);
650 for item in &frame.scene.point_clouds {
651 if item.positions.is_empty() {
652 continue;
653 }
654 let gpu_data = resources.upload_point_cloud(device, queue, item);
655 self.point_cloud_gpu_data.push(gpu_data);
656 }
657 }
658
659 self.glyph_gpu_data.clear();
660 if !frame.scene.glyphs.is_empty() {
661 resources.ensure_glyph_pipeline(device);
662 for item in &frame.scene.glyphs {
663 if item.positions.is_empty() || item.vectors.is_empty() {
664 continue;
665 }
666 let gpu_data = resources.upload_glyph_set(device, queue, item);
667 self.glyph_gpu_data.push(gpu_data);
668 }
669 }
670
671 self.polyline_gpu_data.clear();
675 let vp_size = frame.camera.viewport_size;
676 if !frame.scene.polylines.is_empty() {
677 resources.ensure_polyline_pipeline(device);
678 for item in &frame.scene.polylines {
679 if item.positions.is_empty() {
680 continue;
681 }
682 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
683 self.polyline_gpu_data.push(gpu_data);
684 }
685 }
686
687 if !frame.scene.isolines.is_empty() {
691 resources.ensure_polyline_pipeline(device);
692 for item in &frame.scene.isolines {
693 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
694 continue;
695 }
696 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
697 if positions.is_empty() {
698 continue;
699 }
700 let polyline = PolylineItem {
701 positions,
702 scalars: Vec::new(),
703 strip_lengths,
704 scalar_range: None,
705 colormap_id: None,
706 default_color: item.color,
707 line_width: item.line_width,
708 id: 0,
709 };
710 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
711 self.polyline_gpu_data.push(gpu_data);
712 }
713 }
714
715 self.streamtube_gpu_data.clear();
719 if !frame.scene.streamtube_items.is_empty() {
720 resources.ensure_streamtube_pipeline(device);
721 for item in &frame.scene.streamtube_items {
722 if item.positions.is_empty() || item.strip_lengths.is_empty() {
723 continue;
724 }
725 let gpu_data = resources.upload_streamtube(device, queue, item);
726 if gpu_data.index_count > 0 {
727 self.streamtube_gpu_data.push(gpu_data);
728 }
729 }
730 }
731
732 self.volume_gpu_data.clear();
738 if !frame.scene.volumes.is_empty() {
739 resources.ensure_volume_pipeline(device);
740 for item in &frame.scene.volumes {
741 let gpu =
742 resources.upload_volume_frame(device, queue, item, &frame.effects.clip_planes);
743 self.volume_gpu_data.push(gpu);
744 }
745 }
746
747 {
749 let total = scene_items.len() as u32;
750 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
751 let mut draw_calls = 0u32;
752 let mut triangles = 0u64;
753 let instanced_batch_count = if self.use_instancing {
754 self.instanced_batches.len() as u32
755 } else {
756 0
757 };
758
759 if self.use_instancing {
760 for batch in &self.instanced_batches {
761 if let Some(mesh) = resources
762 .mesh_store
763 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
764 {
765 draw_calls += 1;
766 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
767 }
768 }
769 } else {
770 for item in scene_items {
771 if !item.visible {
772 continue;
773 }
774 if let Some(mesh) = resources
775 .mesh_store
776 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
777 {
778 draw_calls += 1;
779 triangles += (mesh.index_count / 3) as u64;
780 }
781 }
782 }
783
784 self.last_stats = crate::renderer::stats::FrameStats {
785 total_objects: total,
786 visible_objects: visible,
787 culled_objects: total.saturating_sub(visible),
788 draw_calls,
789 instanced_batches: instanced_batch_count,
790 triangles_submitted: triangles,
791 shadow_draw_calls: 0, };
793 }
794
795 if lighting.shadows_enabled && !scene_items.is_empty() {
799 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
800 label: Some("shadow_pass_encoder"),
801 });
802 {
803 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
804 label: Some("shadow_pass"),
805 color_attachments: &[],
806 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
807 view: &resources.shadow_map_view,
808 depth_ops: Some(wgpu::Operations {
809 load: wgpu::LoadOp::Clear(1.0),
810 store: wgpu::StoreOp::Store,
811 }),
812 stencil_ops: None,
813 }),
814 timestamp_writes: None,
815 occlusion_query_set: None,
816 });
817
818 let mut shadow_draws = 0u32;
819 let tile_px = tile_size as f32;
820
821 if self.use_instancing {
822 if let (Some(pipeline), Some(instance_bg)) = (
823 &resources.shadow_instanced_pipeline,
824 self.instanced_batches.first().and_then(|b| {
825 resources.instance_bind_groups.get(&(
826 b.texture_id.unwrap_or(u64::MAX),
827 b.normal_map_id.unwrap_or(u64::MAX),
828 b.ao_map_id.unwrap_or(u64::MAX),
829 ))
830 }),
831 ) {
832 for cascade in 0..effective_cascade_count {
833 let tile_col = (cascade % 2) as f32;
834 let tile_row = (cascade / 2) as f32;
835 shadow_pass.set_viewport(
836 tile_col * tile_px,
837 tile_row * tile_px,
838 tile_px,
839 tile_px,
840 0.0,
841 1.0,
842 );
843 shadow_pass.set_scissor_rect(
844 (tile_col * tile_px) as u32,
845 (tile_row * tile_px) as u32,
846 tile_size,
847 tile_size,
848 );
849
850 shadow_pass.set_pipeline(pipeline);
851
852 queue.write_buffer(
853 resources.shadow_instanced_cascade_bufs[cascade]
854 .as_ref()
855 .expect("shadow_instanced_cascade_bufs not allocated"),
856 0,
857 bytemuck::cast_slice(
858 &cascade_view_projs[cascade].to_cols_array_2d(),
859 ),
860 );
861
862 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
863 .as_ref()
864 .expect("shadow_instanced_cascade_bgs not allocated");
865 shadow_pass.set_bind_group(0, cascade_bg, &[]);
866 shadow_pass.set_bind_group(1, instance_bg, &[]);
867
868 for batch in &self.instanced_batches {
869 if batch.is_transparent {
870 continue;
871 }
872 let Some(mesh) = resources
873 .mesh_store
874 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
875 else {
876 continue;
877 };
878 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
879 shadow_pass.set_index_buffer(
880 mesh.index_buffer.slice(..),
881 wgpu::IndexFormat::Uint32,
882 );
883 shadow_pass.draw_indexed(
884 0..mesh.index_count,
885 0,
886 batch.instance_offset
887 ..batch.instance_offset + batch.instance_count,
888 );
889 shadow_draws += 1;
890 }
891 }
892 }
893 } else {
894 for cascade in 0..effective_cascade_count {
895 let tile_col = (cascade % 2) as f32;
896 let tile_row = (cascade / 2) as f32;
897 shadow_pass.set_viewport(
898 tile_col * tile_px,
899 tile_row * tile_px,
900 tile_px,
901 tile_px,
902 0.0,
903 1.0,
904 );
905 shadow_pass.set_scissor_rect(
906 (tile_col * tile_px) as u32,
907 (tile_row * tile_px) as u32,
908 tile_size,
909 tile_size,
910 );
911
912 shadow_pass.set_pipeline(&resources.shadow_pipeline);
913 shadow_pass.set_bind_group(
914 0,
915 &resources.shadow_bind_group,
916 &[cascade as u32 * 256],
917 );
918
919 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
920 &cascade_view_projs[cascade],
921 );
922
923 for item in scene_items.iter() {
924 if !item.visible {
925 continue;
926 }
927 if item.material.opacity < 1.0 {
928 continue;
929 }
930 let Some(mesh) = resources
931 .mesh_store
932 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
933 else {
934 continue;
935 };
936
937 let world_aabb = mesh
938 .aabb
939 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
940 if cascade_frustum.cull_aabb(&world_aabb) {
941 continue;
942 }
943
944 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
945 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
946 shadow_pass.set_index_buffer(
947 mesh.index_buffer.slice(..),
948 wgpu::IndexFormat::Uint32,
949 );
950 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
951 shadow_draws += 1;
952 }
953 }
954 }
955 drop(shadow_pass);
956 self.last_stats.shadow_draw_calls = shadow_draws;
957 }
958 queue.submit(std::iter::once(encoder.finish()));
959 }
960 }
961
962 pub(super) fn prepare_viewport_internal(
967 &mut self,
968 device: &wgpu::Device,
969 queue: &wgpu::Queue,
970 frame: &FrameData,
971 viewport_fx: &ViewportEffects<'_>,
972 ) {
973 self.ensure_viewport_slot(device, frame.camera.viewport_index);
976
977 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
978 SurfaceSubmission::Flat(items) => items,
979 };
980
981 {
982 let resources = &mut self.resources;
983
984 {
986 let mut planes = [[0.0f32; 4]; 6];
987 let mut count = 0u32;
988 for plane in viewport_fx.clip_planes.iter().filter(|p| p.enabled).take(6) {
989 planes[count as usize] = [
990 plane.normal[0],
991 plane.normal[1],
992 plane.normal[2],
993 plane.distance,
994 ];
995 count += 1;
996 }
997 let clip_uniform = ClipPlanesUniform {
998 planes,
999 count,
1000 _pad0: 0,
1001 viewport_width: frame.camera.viewport_size[0].max(1.0),
1002 viewport_height: frame.camera.viewport_size[1].max(1.0),
1003 };
1004 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1006 queue.write_buffer(
1007 &slot.clip_planes_buf,
1008 0,
1009 bytemuck::cast_slice(&[clip_uniform]),
1010 );
1011 }
1012 queue.write_buffer(
1014 &resources.clip_planes_uniform_buf,
1015 0,
1016 bytemuck::cast_slice(&[clip_uniform]),
1017 );
1018 }
1019
1020 {
1022 let clip_vol_uniform = ClipVolumeUniform::from_clip_volume(viewport_fx.clip_volume);
1023 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1024 queue.write_buffer(
1025 &slot.clip_volume_buf,
1026 0,
1027 bytemuck::cast_slice(&[clip_vol_uniform]),
1028 );
1029 }
1030 queue.write_buffer(
1031 &resources.clip_volume_uniform_buf,
1032 0,
1033 bytemuck::cast_slice(&[clip_vol_uniform]),
1034 );
1035 }
1036
1037 let camera_uniform = frame.camera.render_camera.camera_uniform();
1039 queue.write_buffer(
1041 &resources.camera_uniform_buf,
1042 0,
1043 bytemuck::cast_slice(&[camera_uniform]),
1044 );
1045 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1047 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1048 }
1049
1050 if frame.viewport.show_grid {
1052 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1053 if !eye.is_finite() {
1054 tracing::warn!(
1055 eye_x = eye.x,
1056 eye_y = eye.y,
1057 eye_z = eye.z,
1058 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1059 );
1060 } else {
1061 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1062
1063 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1064 (frame.viewport.grid_cell_size, 1.0_f32)
1065 } else {
1066 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1067 let world_per_pixel =
1068 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1069 / frame.camera.viewport_size[1].max(1.0);
1070 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1071 let mut s = 1.0_f32;
1072 let mut iters = 0u32;
1073 while s < target {
1074 s *= 10.0;
1075 iters += 1;
1076 }
1077 let ratio = (target / s).clamp(0.0, 1.0);
1078 let fade = if ratio < 0.5 {
1079 1.0_f32
1080 } else {
1081 let t = (ratio - 0.5) * 2.0;
1082 1.0 - t * t * (3.0 - 2.0 * t)
1083 };
1084 tracing::debug!(
1085 eye_z = eye.z,
1086 vertical_depth,
1087 world_per_pixel,
1088 target,
1089 spacing = s,
1090 lod_iters = iters,
1091 ratio,
1092 minor_fade = fade,
1093 "grid LOD"
1094 );
1095 (s, fade)
1096 };
1097
1098 let spacing_major = spacing * 10.0;
1099 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1100 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1101 tracing::debug!(
1102 spacing_minor = spacing,
1103 spacing_major,
1104 snap_x,
1105 snap_y,
1106 eye_x = eye.x,
1107 eye_y = eye.y,
1108 eye_z = eye.z,
1109 "grid snap"
1110 );
1111
1112 let orient = frame.camera.render_camera.orientation;
1113 let right = orient * glam::Vec3::X;
1114 let up = orient * glam::Vec3::Y;
1115 let back = orient * glam::Vec3::Z;
1116 let cam_to_world = [
1117 [right.x, right.y, right.z, 0.0_f32],
1118 [up.x, up.y, up.z, 0.0_f32],
1119 [back.x, back.y, back.z, 0.0_f32],
1120 ];
1121 let aspect =
1122 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1123 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1124
1125 let uniform = GridUniform {
1126 view_proj: view_proj_mat,
1127 cam_to_world,
1128 tan_half_fov,
1129 aspect,
1130 _pad_ivp: [0.0; 2],
1131 eye_pos: frame.camera.render_camera.eye_position,
1132 grid_z: frame.viewport.grid_z,
1133 spacing_minor: spacing,
1134 spacing_major,
1135 snap_origin: [snap_x, snap_y],
1136 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1137 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1138 };
1139 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1141 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1142 }
1143 queue.write_buffer(
1145 &resources.grid_uniform_buf,
1146 0,
1147 bytemuck::cast_slice(&[uniform]),
1148 );
1149 }
1150 }
1151 } let vp_idx = frame.camera.viewport_index;
1160
1161 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1163 if frame.interaction.outline_selected {
1164 let resources = &self.resources;
1165 for item in scene_items {
1166 if !item.visible || !item.selected {
1167 continue;
1168 }
1169 let m = &item.material;
1170 let stencil_uniform = ObjectUniform {
1171 model: item.model,
1172 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
1173 selected: 1,
1174 wireframe: 0,
1175 ambient: m.ambient,
1176 diffuse: m.diffuse,
1177 specular: m.specular,
1178 shininess: m.shininess,
1179 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
1180 use_pbr: if m.use_pbr { 1 } else { 0 },
1181 metallic: m.metallic,
1182 roughness: m.roughness,
1183 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
1184 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
1185 has_attribute: 0,
1186 scalar_min: 0.0,
1187 scalar_max: 1.0,
1188 _pad_scalar: 0,
1189 nan_color: [0.0; 4],
1190 use_nan_color: 0,
1191 use_matcap: 0, matcap_blendable: 0, _pad2: 0,
1192 use_face_color: 0, _pad3: [0; 3],
1193 };
1194 let stencil_buf = device.create_buffer(&wgpu::BufferDescriptor {
1195 label: Some("outline_stencil_object_uniform_buf"),
1196 size: std::mem::size_of::<ObjectUniform>() as u64,
1197 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1198 mapped_at_creation: false,
1199 });
1200 queue.write_buffer(&stencil_buf, 0, bytemuck::cast_slice(&[stencil_uniform]));
1201
1202 let albedo_view = match m.texture_id {
1203 Some(id) if (id as usize) < resources.textures.len() => {
1204 &resources.textures[id as usize].view
1205 }
1206 _ => &resources.fallback_texture.view,
1207 };
1208 let normal_view = match m.normal_map_id {
1209 Some(id) if (id as usize) < resources.textures.len() => {
1210 &resources.textures[id as usize].view
1211 }
1212 _ => &resources.fallback_normal_map_view,
1213 };
1214 let ao_view = match m.ao_map_id {
1215 Some(id) if (id as usize) < resources.textures.len() => {
1216 &resources.textures[id as usize].view
1217 }
1218 _ => &resources.fallback_ao_map_view,
1219 };
1220 let stencil_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1221 label: Some("outline_stencil_object_bg"),
1222 layout: &resources.object_bind_group_layout,
1223 entries: &[
1224 wgpu::BindGroupEntry {
1225 binding: 0,
1226 resource: stencil_buf.as_entire_binding(),
1227 },
1228 wgpu::BindGroupEntry {
1229 binding: 1,
1230 resource: wgpu::BindingResource::TextureView(albedo_view),
1231 },
1232 wgpu::BindGroupEntry {
1233 binding: 2,
1234 resource: wgpu::BindingResource::Sampler(&resources.material_sampler),
1235 },
1236 wgpu::BindGroupEntry {
1237 binding: 3,
1238 resource: wgpu::BindingResource::TextureView(normal_view),
1239 },
1240 wgpu::BindGroupEntry {
1241 binding: 4,
1242 resource: wgpu::BindingResource::TextureView(ao_view),
1243 },
1244 wgpu::BindGroupEntry {
1245 binding: 5,
1246 resource: wgpu::BindingResource::TextureView(
1247 &resources.fallback_lut_view,
1248 ),
1249 },
1250 wgpu::BindGroupEntry {
1251 binding: 6,
1252 resource: resources.fallback_scalar_buf.as_entire_binding(),
1253 },
1254 wgpu::BindGroupEntry {
1255 binding: 7,
1256 resource: wgpu::BindingResource::TextureView(
1257 &resources.fallback_texture.view,
1258 ),
1259 },
1260 wgpu::BindGroupEntry {
1261 binding: 8,
1262 resource: resources.fallback_face_color_buf.as_entire_binding(),
1263 },
1264 ],
1265 });
1266
1267 let uniform = OutlineUniform {
1268 model: item.model,
1269 color: frame.interaction.outline_color,
1270 pixel_offset: frame.interaction.outline_width_px,
1271 _pad: [0.0; 3],
1272 };
1273 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1274 label: Some("outline_uniform_buf"),
1275 size: std::mem::size_of::<OutlineUniform>() as u64,
1276 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1277 mapped_at_creation: false,
1278 });
1279 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1280 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1281 label: Some("outline_object_bg"),
1282 layout: &resources.outline_bind_group_layout,
1283 entries: &[wgpu::BindGroupEntry {
1284 binding: 0,
1285 resource: buf.as_entire_binding(),
1286 }],
1287 });
1288 outline_object_buffers.push(OutlineObjectBuffers {
1289 mesh_index: item.mesh_index,
1290 two_sided: item.two_sided,
1291 _stencil_uniform_buf: stencil_buf,
1292 stencil_bind_group: stencil_bg,
1293 _outline_uniform_buf: buf,
1294 outline_bind_group: bg,
1295 });
1296 }
1297 }
1298
1299 let mut xray_object_buffers: Vec<(usize, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1301 if frame.interaction.xray_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: frame.interaction.xray_color,
1310 pixel_offset: 0.0,
1311 _pad: [0.0; 3],
1312 };
1313 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1314 label: Some("xray_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("xray_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 xray_object_buffers.push((item.mesh_index, buf, bg));
1329 }
1330 }
1331
1332 let mut constraint_line_buffers = Vec::new();
1334 for overlay in &frame.interaction.constraint_overlays {
1335 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1336 }
1337
1338 let mut clip_plane_fill_buffers = Vec::new();
1340 let mut clip_plane_line_buffers = Vec::new();
1341 for overlay in &frame.interaction.clip_plane_overlays {
1342 clip_plane_fill_buffers.push(
1343 self.resources
1344 .create_clip_plane_fill_overlay(device, overlay),
1345 );
1346 clip_plane_line_buffers.push(
1347 self.resources
1348 .create_clip_plane_line_overlay(device, overlay),
1349 );
1350 }
1351
1352 let mut cap_buffers = Vec::new();
1354 if viewport_fx.cap_fill_enabled {
1355 let active_planes: Vec<_> = viewport_fx
1356 .clip_planes
1357 .iter()
1358 .filter(|p| p.enabled)
1359 .collect();
1360 for plane in &active_planes {
1361 let plane_n = glam::Vec3::from(plane.normal);
1362 for item in scene_items.iter().filter(|i| i.visible) {
1363 let Some(mesh) = self
1364 .resources
1365 .mesh_store
1366 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
1367 else {
1368 continue;
1369 };
1370 let model = glam::Mat4::from_cols_array_2d(&item.model);
1371 let world_aabb = mesh.aabb.transformed(&model);
1372 if !world_aabb.intersects_plane(plane_n, plane.distance) {
1373 continue;
1374 }
1375 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices) else {
1376 continue;
1377 };
1378 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
1379 pos,
1380 idx,
1381 &model,
1382 plane_n,
1383 plane.distance,
1384 ) {
1385 let bc = item.material.base_color;
1386 let color = plane.cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
1387 let buf = self.resources.upload_cap_geometry(device, &cap, color);
1388 cap_buffers.push(buf);
1389 }
1390 }
1391 }
1392 }
1393
1394 let axes_verts = if frame.viewport.show_axes_indicator
1396 && frame.camera.viewport_size[0] > 0.0
1397 && frame.camera.viewport_size[1] > 0.0
1398 {
1399 let verts = crate::widgets::axes_indicator::build_axes_geometry(
1400 frame.camera.viewport_size[0],
1401 frame.camera.viewport_size[1],
1402 frame.camera.render_camera.orientation,
1403 );
1404 if verts.is_empty() { None } else { Some(verts) }
1405 } else {
1406 None
1407 };
1408
1409 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
1411 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
1412 frame.interaction.gizmo_mode,
1413 frame.interaction.gizmo_hovered,
1414 frame.interaction.gizmo_space_orientation,
1415 );
1416 (verts, indices, model)
1417 });
1418
1419 {
1423 let slot = &mut self.viewport_slots[vp_idx];
1424 slot.outline_object_buffers = outline_object_buffers;
1425 slot.xray_object_buffers = xray_object_buffers;
1426 slot.constraint_line_buffers = constraint_line_buffers;
1427 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
1428 slot.clip_plane_line_buffers = clip_plane_line_buffers;
1429 slot.cap_buffers = cap_buffers;
1430
1431 if let Some(verts) = axes_verts {
1433 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
1434 if byte_size > slot.axes_vertex_buffer.size() {
1435 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1436 label: Some("vp_axes_vertex_buf"),
1437 size: byte_size,
1438 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1439 mapped_at_creation: false,
1440 });
1441 }
1442 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
1443 slot.axes_vertex_count = verts.len() as u32;
1444 } else {
1445 slot.axes_vertex_count = 0;
1446 }
1447
1448 if let Some((verts, indices, model)) = gizmo_update {
1450 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1451 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1452 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
1453 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1454 label: Some("vp_gizmo_vertex_buf"),
1455 size: vert_bytes.len() as u64,
1456 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1457 mapped_at_creation: false,
1458 });
1459 }
1460 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
1461 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1462 label: Some("vp_gizmo_index_buf"),
1463 size: idx_bytes.len() as u64,
1464 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1465 mapped_at_creation: false,
1466 });
1467 }
1468 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
1469 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
1470 slot.gizmo_index_count = indices.len() as u32;
1471 let uniform = crate::interaction::gizmo::GizmoUniform {
1472 model: model.to_cols_array_2d(),
1473 };
1474 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
1475 }
1476 }
1477
1478 if frame.interaction.outline_selected
1485 && !self.viewport_slots[vp_idx]
1486 .outline_object_buffers
1487 .is_empty()
1488 {
1489 let w = frame.camera.viewport_size[0] as u32;
1490 let h = frame.camera.viewport_size[1] as u32;
1491
1492 self.ensure_viewport_hdr(device, queue, vp_idx, w.max(1), h.max(1));
1494
1495 let slot_ref = &self.viewport_slots[vp_idx];
1499 let outlines_ptr = &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
1500 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
1501 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
1502 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
1503 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
1504 let (outlines, camera_bg, color_view, depth_view) = unsafe {
1507 (
1508 &*outlines_ptr,
1509 &*camera_bg_ptr,
1510 &*color_view_ptr,
1511 &*depth_view_ptr,
1512 )
1513 };
1514
1515 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1516 label: Some("outline_offscreen_encoder"),
1517 });
1518 {
1519 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1520 label: Some("outline_offscreen_pass"),
1521 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1522 view: color_view,
1523 resolve_target: None,
1524 ops: wgpu::Operations {
1525 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1526 store: wgpu::StoreOp::Store,
1527 },
1528 depth_slice: None,
1529 })],
1530 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1531 view: depth_view,
1532 depth_ops: Some(wgpu::Operations {
1533 load: wgpu::LoadOp::Clear(1.0),
1534 store: wgpu::StoreOp::Discard,
1535 }),
1536 stencil_ops: Some(wgpu::Operations {
1537 load: wgpu::LoadOp::Clear(0),
1538 store: wgpu::StoreOp::Discard,
1539 }),
1540 }),
1541 timestamp_writes: None,
1542 occlusion_query_set: None,
1543 });
1544
1545 pass.set_stencil_reference(1);
1547 pass.set_bind_group(0, camera_bg, &[]);
1548 for outlined in outlines {
1549 let Some(mesh) = self
1550 .resources
1551 .mesh_store
1552 .get(crate::resources::mesh_store::MeshId(outlined.mesh_index))
1553 else {
1554 continue;
1555 };
1556 let pipeline = if outlined.two_sided {
1557 &self.resources.stencil_write_two_sided_pipeline
1558 } else {
1559 &self.resources.stencil_write_pipeline
1560 };
1561 pass.set_pipeline(pipeline);
1562 pass.set_bind_group(1, &outlined.stencil_bind_group, &[]);
1563 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1564 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1565 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1566 }
1567
1568 pass.set_pipeline(&self.resources.outline_pipeline);
1570 pass.set_stencil_reference(1);
1571 for outlined in outlines {
1572 let Some(mesh) = self
1573 .resources
1574 .mesh_store
1575 .get(crate::resources::mesh_store::MeshId(outlined.mesh_index))
1576 else {
1577 continue;
1578 };
1579 pass.set_bind_group(1, &outlined.outline_bind_group, &[]);
1580 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1581 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1582 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1583 }
1584 }
1585 queue.submit(std::iter::once(encoder.finish()));
1586 }
1587 }
1588
1589 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, frame: &FrameData) {
1592 let (scene_fx, viewport_fx) = frame.effects.split();
1593 self.prepare_scene_internal(device, queue, frame, &scene_fx);
1594 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1595 }
1596}