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
32 let resources = &mut self.resources;
33 let lighting = scene_fx.lighting;
34
35 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
37 SurfaceSubmission::Flat(items) => items,
38 };
39
40 let (shadow_center, shadow_extent) = if let Some(extent) = lighting.shadow_extent_override {
42 (glam::Vec3::ZERO, extent)
43 } else {
44 (glam::Vec3::ZERO, 20.0)
45 };
46
47 fn compute_shadow_matrix(
49 kind: &LightKind,
50 shadow_center: glam::Vec3,
51 shadow_extent: f32,
52 ) -> glam::Mat4 {
53 match kind {
54 LightKind::Directional { direction } => {
55 let dir = glam::Vec3::from(*direction).normalize();
56 let light_up = if dir.z.abs() > 0.99 {
57 glam::Vec3::Y
58 } else {
59 glam::Vec3::Z
60 };
61 let light_pos = shadow_center + dir * shadow_extent * 2.0;
62 let light_view = glam::Mat4::look_at_rh(light_pos, shadow_center, light_up);
63 let light_proj = glam::Mat4::orthographic_rh(
64 -shadow_extent,
65 shadow_extent,
66 -shadow_extent,
67 shadow_extent,
68 0.01,
69 shadow_extent * 5.0,
70 );
71 light_proj * light_view
72 }
73 LightKind::Point { position, range } => {
74 let pos = glam::Vec3::from(*position);
75 let to_center = (shadow_center - pos).normalize();
76 let light_up = if to_center.z.abs() > 0.99 {
77 glam::Vec3::Y
78 } else {
79 glam::Vec3::Z
80 };
81 let light_view = glam::Mat4::look_at_rh(pos, shadow_center, light_up);
82 let light_proj =
83 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
84 light_proj * light_view
85 }
86 LightKind::Spot {
87 position,
88 direction,
89 range,
90 ..
91 } => {
92 let pos = glam::Vec3::from(*position);
93 let dir = glam::Vec3::from(*direction).normalize();
94 let look_target = pos + dir;
95 let up = if dir.z.abs() > 0.99 {
96 glam::Vec3::Y
97 } else {
98 glam::Vec3::Z
99 };
100 let light_view = glam::Mat4::look_at_rh(pos, look_target, up);
101 let light_proj =
102 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
103 light_proj * light_view
104 }
105 }
106 }
107
108 fn build_single_light_uniform(
110 src: &LightSource,
111 shadow_center: glam::Vec3,
112 shadow_extent: f32,
113 compute_shadow: bool,
114 ) -> SingleLightUniform {
115 let shadow_mat = if compute_shadow {
116 compute_shadow_matrix(&src.kind, shadow_center, shadow_extent)
117 } else {
118 glam::Mat4::IDENTITY
119 };
120
121 match &src.kind {
122 LightKind::Directional { direction } => SingleLightUniform {
123 light_view_proj: shadow_mat.to_cols_array_2d(),
124 pos_or_dir: *direction,
125 light_type: 0,
126 color: src.color,
127 intensity: src.intensity,
128 range: 0.0,
129 inner_angle: 0.0,
130 outer_angle: 0.0,
131 _pad_align: 0,
132 spot_direction: [0.0, -1.0, 0.0],
133 _pad: [0.0; 5],
134 },
135 LightKind::Point { position, range } => SingleLightUniform {
136 light_view_proj: shadow_mat.to_cols_array_2d(),
137 pos_or_dir: *position,
138 light_type: 1,
139 color: src.color,
140 intensity: src.intensity,
141 range: *range,
142 inner_angle: 0.0,
143 outer_angle: 0.0,
144 _pad_align: 0,
145 spot_direction: [0.0, -1.0, 0.0],
146 _pad: [0.0; 5],
147 },
148 LightKind::Spot {
149 position,
150 direction,
151 range,
152 inner_angle,
153 outer_angle,
154 } => SingleLightUniform {
155 light_view_proj: shadow_mat.to_cols_array_2d(),
156 pos_or_dir: *position,
157 light_type: 2,
158 color: src.color,
159 intensity: src.intensity,
160 range: *range,
161 inner_angle: *inner_angle,
162 outer_angle: *outer_angle,
163 _pad_align: 0,
164 spot_direction: *direction,
165 _pad: [0.0; 5],
166 },
167 }
168 }
169
170 let light_count = lighting.lights.len().min(8) as u32;
172 let mut lights_arr = [SingleLightUniform {
173 light_view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
174 pos_or_dir: [0.0; 3],
175 light_type: 0,
176 color: [1.0; 3],
177 intensity: 1.0,
178 range: 0.0,
179 inner_angle: 0.0,
180 outer_angle: 0.0,
181 _pad_align: 0,
182 spot_direction: [0.0, -1.0, 0.0],
183 _pad: [0.0; 5],
184 }; 8];
185
186 for (i, src) in lighting.lights.iter().take(8).enumerate() {
187 lights_arr[i] = build_single_light_uniform(src, shadow_center, shadow_extent, i == 0);
188 }
189
190 let cascade_count = lighting.shadow_cascade_count.clamp(1, 4) as usize;
195 let atlas_res = lighting.shadow_atlas_resolution.max(64);
196 let tile_size = atlas_res / 2;
197
198 let cascade_splits = compute_cascade_splits(
199 frame.camera.render_camera.near.max(0.01),
200 frame.camera.render_camera.far.max(1.0),
201 cascade_count as u32,
202 lighting.cascade_split_lambda,
203 );
204
205 let light_dir_for_csm = if light_count > 0 {
206 match &lighting.lights[0].kind {
207 LightKind::Directional { direction } => glam::Vec3::from(*direction).normalize(),
208 LightKind::Point { position, .. } => {
209 (glam::Vec3::from(*position) - shadow_center).normalize()
210 }
211 LightKind::Spot {
212 position,
213 direction,
214 ..
215 } => {
216 let _ = position;
217 glam::Vec3::from(*direction).normalize()
218 }
219 }
220 } else {
221 glam::Vec3::new(0.3, 1.0, 0.5).normalize()
222 };
223
224 let mut cascade_view_projs = [glam::Mat4::IDENTITY; 4];
225 let mut cascade_split_distances = [0.0f32; 4];
227
228 let use_csm = light_count > 0
230 && matches!(lighting.lights[0].kind, LightKind::Directional { .. })
231 && frame.camera.render_camera.view != glam::Mat4::IDENTITY;
232
233 if use_csm {
234 for i in 0..cascade_count {
235 let split_near = if i == 0 {
236 frame.camera.render_camera.near.max(0.01)
237 } else {
238 cascade_splits[i - 1]
239 };
240 let split_far = cascade_splits[i];
241 cascade_view_projs[i] = compute_cascade_matrix(
242 light_dir_for_csm,
243 frame.camera.render_camera.view,
244 frame.camera.render_camera.fov,
245 frame.camera.render_camera.aspect,
246 split_near,
247 split_far,
248 tile_size as f32,
249 );
250 cascade_split_distances[i] = split_far;
251 }
252 } else {
253 let primary_shadow_mat = if light_count > 0 {
255 compute_shadow_matrix(&lighting.lights[0].kind, shadow_center, shadow_extent)
256 } else {
257 glam::Mat4::IDENTITY
258 };
259 cascade_view_projs[0] = primary_shadow_mat;
260 cascade_split_distances[0] = frame.camera.render_camera.far;
261 }
262 let effective_cascade_count = if use_csm { cascade_count } else { 1 };
263
264 let atlas_rects: [[f32; 4]; 8] = [
267 [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],
272 [0.0; 4],
273 [0.0; 4],
274 [0.0; 4], ];
276
277 {
279 let mut vp_data = [[0.0f32; 4]; 16]; for c in 0..4 {
281 let cols = cascade_view_projs[c].to_cols_array_2d();
282 for row in 0..4 {
283 vp_data[c * 4 + row] = cols[row];
284 }
285 }
286 let shadow_atlas_uniform = ShadowAtlasUniform {
287 cascade_view_proj: vp_data,
288 cascade_splits: cascade_split_distances,
289 cascade_count: effective_cascade_count as u32,
290 atlas_size: atlas_res as f32,
291 shadow_filter: match lighting.shadow_filter {
292 ShadowFilter::Pcf => 0,
293 ShadowFilter::Pcss => 1,
294 },
295 pcss_light_radius: lighting.pcss_light_radius,
296 atlas_rects,
297 };
298 queue.write_buffer(
299 &resources.shadow_info_buf,
300 0,
301 bytemuck::cast_slice(&[shadow_atlas_uniform]),
302 );
303 for slot in &self.viewport_slots {
306 queue.write_buffer(
307 &slot.shadow_info_buf,
308 0,
309 bytemuck::cast_slice(&[shadow_atlas_uniform]),
310 );
311 }
312 }
313
314 let _primary_shadow_mat = cascade_view_projs[0];
317
318 let (ibl_enabled, ibl_intensity, ibl_rotation, show_skybox) =
321 if let Some(env) = scene_fx.environment {
322 if resources.ibl_irradiance_view.is_some() {
323 (
324 1u32,
325 env.intensity,
326 env.rotation,
327 if env.show_skybox { 1u32 } else { 0 },
328 )
329 } else {
330 (0, 0.0, 0.0, 0)
331 }
332 } else {
333 (0, 0.0, 0.0, 0)
334 };
335
336 let lights_uniform = LightsUniform {
337 count: light_count,
338 shadow_bias: lighting.shadow_bias,
339 shadows_enabled: if lighting.shadows_enabled { 1 } else { 0 },
340 _pad: 0,
341 sky_color: lighting.sky_color,
342 hemisphere_intensity: lighting.hemisphere_intensity,
343 ground_color: lighting.ground_color,
344 _pad2: 0.0,
345 lights: lights_arr,
346 ibl_enabled,
347 ibl_intensity,
348 ibl_rotation,
349 show_skybox,
350 };
351 queue.write_buffer(
352 &resources.light_uniform_buf,
353 0,
354 bytemuck::cast_slice(&[lights_uniform]),
355 );
356
357 const SHADOW_SLOT_STRIDE: u64 = 256;
361 for c in 0..4usize {
362 queue.write_buffer(
363 &resources.shadow_uniform_buf,
364 c as u64 * SHADOW_SLOT_STRIDE,
365 bytemuck::cast_slice(&cascade_view_projs[c].to_cols_array_2d()),
366 );
367 }
368
369 let visible_count = scene_items.iter().filter(|i| i.visible).count();
372 let prev_use_instancing = self.use_instancing;
373 self.use_instancing = visible_count > INSTANCING_THRESHOLD;
374
375 if self.use_instancing != prev_use_instancing {
378 self.instanced_batches.clear();
379 self.last_scene_generation = u64::MAX;
380 self.last_scene_items_count = usize::MAX;
381 }
382
383 let has_scalar_items = scene_items.iter().any(|i| i.active_attribute.is_some());
387 let has_two_sided_items = scene_items.iter().any(|i| i.two_sided);
388 if !self.use_instancing
389 || frame.viewport.wireframe_mode
390 || has_scalar_items
391 || has_two_sided_items
392 {
393 for item in scene_items {
394 if resources
395 .mesh_store
396 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
397 .is_none()
398 {
399 tracing::warn!(
400 mesh_index = item.mesh_index,
401 "scene item mesh_index invalid, skipping"
402 );
403 continue;
404 };
405 let m = &item.material;
406 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
408 let range = item
409 .scalar_range
410 .or_else(|| {
411 resources
412 .mesh_store
413 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
414 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
415 })
416 .unwrap_or((0.0, 1.0));
417 (1u32, range.0, range.1)
418 } else {
419 (0u32, 0.0, 1.0)
420 };
421 let obj_uniform = ObjectUniform {
422 model: item.model,
423 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
424 selected: if item.selected { 1 } else { 0 },
425 wireframe: if frame.viewport.wireframe_mode { 1 } else { 0 },
426 ambient: m.ambient,
427 diffuse: m.diffuse,
428 specular: m.specular,
429 shininess: m.shininess,
430 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
431 use_pbr: if m.use_pbr { 1 } else { 0 },
432 metallic: m.metallic,
433 roughness: m.roughness,
434 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
435 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
436 has_attribute: has_attr,
437 scalar_min: s_min,
438 scalar_max: s_max,
439 _pad_scalar: 0,
440 nan_color: item.nan_color.unwrap_or([0.0; 4]),
441 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
442 _pad_nan: [0; 3],
443 };
444
445 let normal_obj_uniform = ObjectUniform {
446 model: item.model,
447 color: [1.0, 1.0, 1.0, 1.0],
448 selected: 0,
449 wireframe: 0,
450 ambient: 0.15,
451 diffuse: 0.75,
452 specular: 0.4,
453 shininess: 32.0,
454 has_texture: 0,
455 use_pbr: 0,
456 metallic: 0.0,
457 roughness: 0.5,
458 has_normal_map: 0,
459 has_ao_map: 0,
460 has_attribute: 0,
461 scalar_min: 0.0,
462 scalar_max: 1.0,
463 _pad_scalar: 0,
464 nan_color: [0.0; 4],
465 use_nan_color: 0,
466 _pad_nan: [0; 3],
467 };
468
469 {
471 let mesh = resources
472 .mesh_store
473 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
474 .unwrap();
475 queue.write_buffer(
476 &mesh.object_uniform_buf,
477 0,
478 bytemuck::cast_slice(&[obj_uniform]),
479 );
480 queue.write_buffer(
481 &mesh.normal_uniform_buf,
482 0,
483 bytemuck::cast_slice(&[normal_obj_uniform]),
484 );
485 } resources.update_mesh_texture_bind_group(
489 device,
490 item.mesh_index,
491 item.material.texture_id,
492 item.material.normal_map_id,
493 item.material.ao_map_id,
494 item.colormap_id,
495 item.active_attribute.as_ref().map(|a| a.name.as_str()),
496 );
497 }
498 }
499
500 if self.use_instancing {
501 resources.ensure_instanced_pipelines(device);
502
503 let cache_valid = frame.scene.generation == self.last_scene_generation
508 && frame.interaction.selection_generation == self.last_selection_generation
509 && scene_items.len() == self.last_scene_items_count;
510
511 if !cache_valid {
512 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
514 .iter()
515 .filter(|item| {
516 item.visible
517 && item.active_attribute.is_none()
518 && !item.two_sided
519 && resources
520 .mesh_store
521 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
522 .is_some()
523 })
524 .collect();
525
526 sorted_items.sort_unstable_by_key(|item| {
527 (
528 item.mesh_index,
529 item.material.texture_id,
530 item.material.normal_map_id,
531 item.material.ao_map_id,
532 )
533 });
534
535 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
536 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
537
538 if !sorted_items.is_empty() {
539 let mut batch_start = 0usize;
540 for i in 1..=sorted_items.len() {
541 let at_end = i == sorted_items.len();
542 let key_changed = !at_end && {
543 let a = sorted_items[batch_start];
544 let b = sorted_items[i];
545 a.mesh_index != b.mesh_index
546 || a.material.texture_id != b.material.texture_id
547 || a.material.normal_map_id != b.material.normal_map_id
548 || a.material.ao_map_id != b.material.ao_map_id
549 };
550
551 if at_end || key_changed {
552 let batch_items = &sorted_items[batch_start..i];
553 let rep = batch_items[0];
554 let instance_offset = all_instances.len() as u32;
555 let is_transparent = rep.material.opacity < 1.0;
556
557 for item in batch_items {
558 let m = &item.material;
559 all_instances.push(InstanceData {
560 model: item.model,
561 color: [
562 m.base_color[0],
563 m.base_color[1],
564 m.base_color[2],
565 m.opacity,
566 ],
567 selected: if item.selected { 1 } else { 0 },
568 wireframe: 0, ambient: m.ambient,
570 diffuse: m.diffuse,
571 specular: m.specular,
572 shininess: m.shininess,
573 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
574 use_pbr: if m.use_pbr { 1 } else { 0 },
575 metallic: m.metallic,
576 roughness: m.roughness,
577 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
578 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
579 });
580 }
581
582 instanced_batches.push(InstancedBatch {
583 mesh_index: rep.mesh_index,
584 texture_id: rep.material.texture_id,
585 normal_map_id: rep.material.normal_map_id,
586 ao_map_id: rep.material.ao_map_id,
587 instance_offset,
588 instance_count: batch_items.len() as u32,
589 is_transparent,
590 });
591
592 batch_start = i;
593 }
594 }
595 }
596
597 self.cached_instance_data = all_instances;
598 self.cached_instanced_batches = instanced_batches;
599
600 resources.upload_instance_data(device, queue, &self.cached_instance_data);
601
602 self.instanced_batches = self.cached_instanced_batches.clone();
603
604 self.last_scene_generation = frame.scene.generation;
605 self.last_selection_generation = frame.interaction.selection_generation;
606 self.last_scene_items_count = scene_items.len();
607
608 for batch in &self.instanced_batches {
609 resources.get_instance_bind_group(
610 device,
611 batch.texture_id,
612 batch.normal_map_id,
613 batch.ao_map_id,
614 );
615 }
616 } else {
617 for batch in &self.instanced_batches {
618 resources.get_instance_bind_group(
619 device,
620 batch.texture_id,
621 batch.normal_map_id,
622 batch.ao_map_id,
623 );
624 }
625 }
626 }
627
628 self.point_cloud_gpu_data.clear();
632 if !frame.scene.point_clouds.is_empty() {
633 resources.ensure_point_cloud_pipeline(device);
634 for item in &frame.scene.point_clouds {
635 if item.positions.is_empty() {
636 continue;
637 }
638 let gpu_data = resources.upload_point_cloud(device, queue, item);
639 self.point_cloud_gpu_data.push(gpu_data);
640 }
641 }
642
643 self.glyph_gpu_data.clear();
644 if !frame.scene.glyphs.is_empty() {
645 resources.ensure_glyph_pipeline(device);
646 for item in &frame.scene.glyphs {
647 if item.positions.is_empty() || item.vectors.is_empty() {
648 continue;
649 }
650 let gpu_data = resources.upload_glyph_set(device, queue, item);
651 self.glyph_gpu_data.push(gpu_data);
652 }
653 }
654
655 self.polyline_gpu_data.clear();
659 if !frame.scene.polylines.is_empty() {
660 resources.ensure_polyline_pipeline(device);
661 for item in &frame.scene.polylines {
662 if item.positions.is_empty() {
663 continue;
664 }
665 let gpu_data = resources.upload_polyline(device, queue, item);
666 self.polyline_gpu_data.push(gpu_data);
667 }
668 }
669
670 if !frame.scene.isolines.is_empty() {
674 resources.ensure_polyline_pipeline(device);
675 for item in &frame.scene.isolines {
676 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
677 continue;
678 }
679 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
680 if positions.is_empty() {
681 continue;
682 }
683 let polyline = PolylineItem {
684 positions,
685 scalars: Vec::new(),
686 strip_lengths,
687 scalar_range: None,
688 colormap_id: None,
689 default_color: item.color,
690 line_width: item.line_width,
691 id: 0,
692 };
693 let gpu_data = resources.upload_polyline(device, queue, &polyline);
694 self.polyline_gpu_data.push(gpu_data);
695 }
696 }
697
698 self.streamtube_gpu_data.clear();
702 if !frame.scene.streamtube_items.is_empty() {
703 resources.ensure_streamtube_pipeline(device);
704 for item in &frame.scene.streamtube_items {
705 if item.positions.is_empty() || item.strip_lengths.is_empty() {
706 continue;
707 }
708 let gpu_data = resources.upload_streamtube(device, queue, item);
709 if gpu_data.instance_count > 0 {
710 self.streamtube_gpu_data.push(gpu_data);
711 }
712 }
713 }
714
715 self.volume_gpu_data.clear();
721 if !frame.scene.volumes.is_empty() {
722 resources.ensure_volume_pipeline(device);
723 for item in &frame.scene.volumes {
724 let gpu =
725 resources.upload_volume_frame(device, queue, item, &frame.effects.clip_planes);
726 self.volume_gpu_data.push(gpu);
727 }
728 }
729
730 {
732 let total = scene_items.len() as u32;
733 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
734 let mut draw_calls = 0u32;
735 let mut triangles = 0u64;
736 let instanced_batch_count = if self.use_instancing {
737 self.instanced_batches.len() as u32
738 } else {
739 0
740 };
741
742 if self.use_instancing {
743 for batch in &self.instanced_batches {
744 if let Some(mesh) = resources
745 .mesh_store
746 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
747 {
748 draw_calls += 1;
749 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
750 }
751 }
752 } else {
753 for item in scene_items {
754 if !item.visible {
755 continue;
756 }
757 if let Some(mesh) = resources
758 .mesh_store
759 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
760 {
761 draw_calls += 1;
762 triangles += (mesh.index_count / 3) as u64;
763 }
764 }
765 }
766
767 self.last_stats = crate::renderer::stats::FrameStats {
768 total_objects: total,
769 visible_objects: visible,
770 culled_objects: total.saturating_sub(visible),
771 draw_calls,
772 instanced_batches: instanced_batch_count,
773 triangles_submitted: triangles,
774 shadow_draw_calls: 0, };
776 }
777
778 if lighting.shadows_enabled && !scene_items.is_empty() {
782 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
783 label: Some("shadow_pass_encoder"),
784 });
785 {
786 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
787 label: Some("shadow_pass"),
788 color_attachments: &[],
789 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
790 view: &resources.shadow_map_view,
791 depth_ops: Some(wgpu::Operations {
792 load: wgpu::LoadOp::Clear(1.0),
793 store: wgpu::StoreOp::Store,
794 }),
795 stencil_ops: None,
796 }),
797 timestamp_writes: None,
798 occlusion_query_set: None,
799 });
800
801 let mut shadow_draws = 0u32;
802 let tile_px = tile_size as f32;
803
804 if self.use_instancing {
805 if let (Some(pipeline), Some(instance_bg)) = (
806 &resources.shadow_instanced_pipeline,
807 self.instanced_batches.first().and_then(|b| {
808 resources.instance_bind_groups.get(&(
809 b.texture_id.unwrap_or(u64::MAX),
810 b.normal_map_id.unwrap_or(u64::MAX),
811 b.ao_map_id.unwrap_or(u64::MAX),
812 ))
813 }),
814 ) {
815 for cascade in 0..effective_cascade_count {
816 let tile_col = (cascade % 2) as f32;
817 let tile_row = (cascade / 2) as f32;
818 shadow_pass.set_viewport(
819 tile_col * tile_px,
820 tile_row * tile_px,
821 tile_px,
822 tile_px,
823 0.0,
824 1.0,
825 );
826 shadow_pass.set_scissor_rect(
827 (tile_col * tile_px) as u32,
828 (tile_row * tile_px) as u32,
829 tile_size,
830 tile_size,
831 );
832
833 shadow_pass.set_pipeline(pipeline);
834
835 queue.write_buffer(
836 resources.shadow_instanced_cascade_bufs[cascade]
837 .as_ref()
838 .expect("shadow_instanced_cascade_bufs not allocated"),
839 0,
840 bytemuck::cast_slice(
841 &cascade_view_projs[cascade].to_cols_array_2d(),
842 ),
843 );
844
845 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
846 .as_ref()
847 .expect("shadow_instanced_cascade_bgs not allocated");
848 shadow_pass.set_bind_group(0, cascade_bg, &[]);
849 shadow_pass.set_bind_group(1, instance_bg, &[]);
850
851 for batch in &self.instanced_batches {
852 if batch.is_transparent {
853 continue;
854 }
855 let Some(mesh) = resources
856 .mesh_store
857 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
858 else {
859 continue;
860 };
861 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
862 shadow_pass.set_index_buffer(
863 mesh.index_buffer.slice(..),
864 wgpu::IndexFormat::Uint32,
865 );
866 shadow_pass.draw_indexed(
867 0..mesh.index_count,
868 0,
869 batch.instance_offset
870 ..batch.instance_offset + batch.instance_count,
871 );
872 shadow_draws += 1;
873 }
874 }
875 }
876 } else {
877 for cascade in 0..effective_cascade_count {
878 let tile_col = (cascade % 2) as f32;
879 let tile_row = (cascade / 2) as f32;
880 shadow_pass.set_viewport(
881 tile_col * tile_px,
882 tile_row * tile_px,
883 tile_px,
884 tile_px,
885 0.0,
886 1.0,
887 );
888 shadow_pass.set_scissor_rect(
889 (tile_col * tile_px) as u32,
890 (tile_row * tile_px) as u32,
891 tile_size,
892 tile_size,
893 );
894
895 shadow_pass.set_pipeline(&resources.shadow_pipeline);
896 shadow_pass.set_bind_group(
897 0,
898 &resources.shadow_bind_group,
899 &[cascade as u32 * 256],
900 );
901
902 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
903 &cascade_view_projs[cascade],
904 );
905
906 for item in scene_items.iter() {
907 if !item.visible {
908 continue;
909 }
910 if item.material.opacity < 1.0 {
911 continue;
912 }
913 let Some(mesh) = resources
914 .mesh_store
915 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
916 else {
917 continue;
918 };
919
920 let world_aabb = mesh
921 .aabb
922 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
923 if cascade_frustum.cull_aabb(&world_aabb) {
924 continue;
925 }
926
927 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
928 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
929 shadow_pass.set_index_buffer(
930 mesh.index_buffer.slice(..),
931 wgpu::IndexFormat::Uint32,
932 );
933 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
934 shadow_draws += 1;
935 }
936 }
937 }
938 drop(shadow_pass);
939 self.last_stats.shadow_draw_calls = shadow_draws;
940 }
941 queue.submit(std::iter::once(encoder.finish()));
942 }
943 }
944
945 pub(super) fn prepare_viewport_internal(
950 &mut self,
951 device: &wgpu::Device,
952 queue: &wgpu::Queue,
953 frame: &FrameData,
954 viewport_fx: &ViewportEffects<'_>,
955 ) {
956 self.ensure_viewport_slot(device, frame.camera.viewport_index);
959
960 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
961 SurfaceSubmission::Flat(items) => items,
962 };
963
964 {
965 let resources = &mut self.resources;
966
967 {
969 let mut planes = [[0.0f32; 4]; 6];
970 let mut count = 0u32;
971 for plane in viewport_fx.clip_planes.iter().filter(|p| p.enabled).take(6) {
972 planes[count as usize] = [
973 plane.normal[0],
974 plane.normal[1],
975 plane.normal[2],
976 plane.distance,
977 ];
978 count += 1;
979 }
980 let clip_uniform = ClipPlanesUniform {
981 planes,
982 count,
983 _pad0: 0,
984 viewport_width: frame.camera.viewport_size[0].max(1.0),
985 viewport_height: frame.camera.viewport_size[1].max(1.0),
986 };
987 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
989 queue.write_buffer(
990 &slot.clip_planes_buf,
991 0,
992 bytemuck::cast_slice(&[clip_uniform]),
993 );
994 }
995 queue.write_buffer(
997 &resources.clip_planes_uniform_buf,
998 0,
999 bytemuck::cast_slice(&[clip_uniform]),
1000 );
1001 }
1002
1003 {
1005 let clip_vol_uniform = ClipVolumeUniform::from_clip_volume(viewport_fx.clip_volume);
1006 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1007 queue.write_buffer(
1008 &slot.clip_volume_buf,
1009 0,
1010 bytemuck::cast_slice(&[clip_vol_uniform]),
1011 );
1012 }
1013 queue.write_buffer(
1014 &resources.clip_volume_uniform_buf,
1015 0,
1016 bytemuck::cast_slice(&[clip_vol_uniform]),
1017 );
1018 }
1019
1020 let camera_uniform = frame.camera.render_camera.camera_uniform();
1022 queue.write_buffer(
1024 &resources.camera_uniform_buf,
1025 0,
1026 bytemuck::cast_slice(&[camera_uniform]),
1027 );
1028 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1030 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1031 }
1032
1033 if frame.viewport.show_grid {
1035 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1036 if !eye.is_finite() {
1037 tracing::warn!(
1038 eye_x = eye.x,
1039 eye_y = eye.y,
1040 eye_z = eye.z,
1041 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1042 );
1043 } else {
1044 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1045
1046 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1047 (frame.viewport.grid_cell_size, 1.0_f32)
1048 } else {
1049 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1050 let world_per_pixel =
1051 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1052 / frame.camera.viewport_size[1].max(1.0);
1053 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1054 let mut s = 1.0_f32;
1055 let mut iters = 0u32;
1056 while s < target {
1057 s *= 10.0;
1058 iters += 1;
1059 }
1060 let ratio = (target / s).clamp(0.0, 1.0);
1061 let fade = if ratio < 0.5 {
1062 1.0_f32
1063 } else {
1064 let t = (ratio - 0.5) * 2.0;
1065 1.0 - t * t * (3.0 - 2.0 * t)
1066 };
1067 tracing::debug!(
1068 eye_z = eye.z,
1069 vertical_depth,
1070 world_per_pixel,
1071 target,
1072 spacing = s,
1073 lod_iters = iters,
1074 ratio,
1075 minor_fade = fade,
1076 "grid LOD"
1077 );
1078 (s, fade)
1079 };
1080
1081 let spacing_major = spacing * 10.0;
1082 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1083 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1084 tracing::debug!(
1085 spacing_minor = spacing,
1086 spacing_major,
1087 snap_x,
1088 snap_y,
1089 eye_x = eye.x,
1090 eye_y = eye.y,
1091 eye_z = eye.z,
1092 "grid snap"
1093 );
1094
1095 let orient = frame.camera.render_camera.orientation;
1096 let right = orient * glam::Vec3::X;
1097 let up = orient * glam::Vec3::Y;
1098 let back = orient * glam::Vec3::Z;
1099 let cam_to_world = [
1100 [right.x, right.y, right.z, 0.0_f32],
1101 [up.x, up.y, up.z, 0.0_f32],
1102 [back.x, back.y, back.z, 0.0_f32],
1103 ];
1104 let aspect =
1105 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1106 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1107
1108 let uniform = GridUniform {
1109 view_proj: view_proj_mat,
1110 cam_to_world,
1111 tan_half_fov,
1112 aspect,
1113 _pad_ivp: [0.0; 2],
1114 eye_pos: frame.camera.render_camera.eye_position,
1115 grid_z: frame.viewport.grid_z,
1116 spacing_minor: spacing,
1117 spacing_major,
1118 snap_origin: [snap_x, snap_y],
1119 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1120 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1121 };
1122 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1124 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1125 }
1126 queue.write_buffer(
1128 &resources.grid_uniform_buf,
1129 0,
1130 bytemuck::cast_slice(&[uniform]),
1131 );
1132 }
1133 }
1134 } let vp_idx = frame.camera.viewport_index;
1143
1144 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1146 if frame.interaction.outline_selected {
1147 let resources = &self.resources;
1148 for item in scene_items {
1149 if !item.visible || !item.selected {
1150 continue;
1151 }
1152 let m = &item.material;
1153 let stencil_uniform = ObjectUniform {
1154 model: item.model,
1155 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
1156 selected: 1,
1157 wireframe: 0,
1158 ambient: m.ambient,
1159 diffuse: m.diffuse,
1160 specular: m.specular,
1161 shininess: m.shininess,
1162 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
1163 use_pbr: if m.use_pbr { 1 } else { 0 },
1164 metallic: m.metallic,
1165 roughness: m.roughness,
1166 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
1167 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
1168 has_attribute: 0,
1169 scalar_min: 0.0,
1170 scalar_max: 1.0,
1171 _pad_scalar: 0,
1172 nan_color: [0.0; 4],
1173 use_nan_color: 0,
1174 _pad_nan: [0; 3],
1175 };
1176 let stencil_buf = device.create_buffer(&wgpu::BufferDescriptor {
1177 label: Some("outline_stencil_object_uniform_buf"),
1178 size: std::mem::size_of::<ObjectUniform>() as u64,
1179 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1180 mapped_at_creation: false,
1181 });
1182 queue.write_buffer(&stencil_buf, 0, bytemuck::cast_slice(&[stencil_uniform]));
1183
1184 let albedo_view = match m.texture_id {
1185 Some(id) if (id as usize) < resources.textures.len() => {
1186 &resources.textures[id as usize].view
1187 }
1188 _ => &resources.fallback_texture.view,
1189 };
1190 let normal_view = match m.normal_map_id {
1191 Some(id) if (id as usize) < resources.textures.len() => {
1192 &resources.textures[id as usize].view
1193 }
1194 _ => &resources.fallback_normal_map_view,
1195 };
1196 let ao_view = match m.ao_map_id {
1197 Some(id) if (id as usize) < resources.textures.len() => {
1198 &resources.textures[id as usize].view
1199 }
1200 _ => &resources.fallback_ao_map_view,
1201 };
1202 let stencil_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1203 label: Some("outline_stencil_object_bg"),
1204 layout: &resources.object_bind_group_layout,
1205 entries: &[
1206 wgpu::BindGroupEntry {
1207 binding: 0,
1208 resource: stencil_buf.as_entire_binding(),
1209 },
1210 wgpu::BindGroupEntry {
1211 binding: 1,
1212 resource: wgpu::BindingResource::TextureView(albedo_view),
1213 },
1214 wgpu::BindGroupEntry {
1215 binding: 2,
1216 resource: wgpu::BindingResource::Sampler(&resources.material_sampler),
1217 },
1218 wgpu::BindGroupEntry {
1219 binding: 3,
1220 resource: wgpu::BindingResource::TextureView(normal_view),
1221 },
1222 wgpu::BindGroupEntry {
1223 binding: 4,
1224 resource: wgpu::BindingResource::TextureView(ao_view),
1225 },
1226 wgpu::BindGroupEntry {
1227 binding: 5,
1228 resource: wgpu::BindingResource::TextureView(
1229 &resources.fallback_lut_view,
1230 ),
1231 },
1232 wgpu::BindGroupEntry {
1233 binding: 6,
1234 resource: resources.fallback_scalar_buf.as_entire_binding(),
1235 },
1236 ],
1237 });
1238
1239 let uniform = OutlineUniform {
1240 model: item.model,
1241 color: frame.interaction.outline_color,
1242 pixel_offset: frame.interaction.outline_width_px,
1243 _pad: [0.0; 3],
1244 };
1245 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1246 label: Some("outline_uniform_buf"),
1247 size: std::mem::size_of::<OutlineUniform>() as u64,
1248 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1249 mapped_at_creation: false,
1250 });
1251 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1252 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1253 label: Some("outline_object_bg"),
1254 layout: &resources.outline_bind_group_layout,
1255 entries: &[wgpu::BindGroupEntry {
1256 binding: 0,
1257 resource: buf.as_entire_binding(),
1258 }],
1259 });
1260 outline_object_buffers.push(OutlineObjectBuffers {
1261 mesh_index: item.mesh_index,
1262 two_sided: item.two_sided,
1263 _stencil_uniform_buf: stencil_buf,
1264 stencil_bind_group: stencil_bg,
1265 _outline_uniform_buf: buf,
1266 outline_bind_group: bg,
1267 });
1268 }
1269 }
1270
1271 let mut xray_object_buffers: Vec<(usize, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1273 if frame.interaction.xray_selected {
1274 let resources = &self.resources;
1275 for item in scene_items {
1276 if !item.visible || !item.selected {
1277 continue;
1278 }
1279 let uniform = OutlineUniform {
1280 model: item.model,
1281 color: frame.interaction.xray_color,
1282 pixel_offset: 0.0,
1283 _pad: [0.0; 3],
1284 };
1285 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1286 label: Some("xray_uniform_buf"),
1287 size: std::mem::size_of::<OutlineUniform>() as u64,
1288 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1289 mapped_at_creation: false,
1290 });
1291 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1292 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1293 label: Some("xray_object_bg"),
1294 layout: &resources.outline_bind_group_layout,
1295 entries: &[wgpu::BindGroupEntry {
1296 binding: 0,
1297 resource: buf.as_entire_binding(),
1298 }],
1299 });
1300 xray_object_buffers.push((item.mesh_index, buf, bg));
1301 }
1302 }
1303
1304 let mut constraint_line_buffers = Vec::new();
1306 for overlay in &frame.interaction.constraint_overlays {
1307 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1308 }
1309
1310 let mut clip_plane_fill_buffers = Vec::new();
1312 let mut clip_plane_line_buffers = Vec::new();
1313 for overlay in &frame.interaction.clip_plane_overlays {
1314 clip_plane_fill_buffers.push(
1315 self.resources
1316 .create_clip_plane_fill_overlay(device, overlay),
1317 );
1318 clip_plane_line_buffers.push(
1319 self.resources
1320 .create_clip_plane_line_overlay(device, overlay),
1321 );
1322 }
1323
1324 let mut cap_buffers = Vec::new();
1326 if viewport_fx.cap_fill_enabled {
1327 let active_planes: Vec<_> = viewport_fx
1328 .clip_planes
1329 .iter()
1330 .filter(|p| p.enabled)
1331 .collect();
1332 for plane in &active_planes {
1333 let plane_n = glam::Vec3::from(plane.normal);
1334 for item in scene_items.iter().filter(|i| i.visible) {
1335 let Some(mesh) = self
1336 .resources
1337 .mesh_store
1338 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
1339 else {
1340 continue;
1341 };
1342 let model = glam::Mat4::from_cols_array_2d(&item.model);
1343 let world_aabb = mesh.aabb.transformed(&model);
1344 if !world_aabb.intersects_plane(plane_n, plane.distance) {
1345 continue;
1346 }
1347 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices) else {
1348 continue;
1349 };
1350 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
1351 pos,
1352 idx,
1353 &model,
1354 plane_n,
1355 plane.distance,
1356 ) {
1357 let bc = item.material.base_color;
1358 let color = plane.cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
1359 let buf = self.resources.upload_cap_geometry(device, &cap, color);
1360 cap_buffers.push(buf);
1361 }
1362 }
1363 }
1364 }
1365
1366 let axes_verts = if frame.viewport.show_axes_indicator
1368 && frame.camera.viewport_size[0] > 0.0
1369 && frame.camera.viewport_size[1] > 0.0
1370 {
1371 let verts = crate::widgets::axes_indicator::build_axes_geometry(
1372 frame.camera.viewport_size[0],
1373 frame.camera.viewport_size[1],
1374 frame.camera.render_camera.orientation,
1375 );
1376 if verts.is_empty() { None } else { Some(verts) }
1377 } else {
1378 None
1379 };
1380
1381 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
1383 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
1384 frame.interaction.gizmo_mode,
1385 frame.interaction.gizmo_hovered,
1386 frame.interaction.gizmo_space_orientation,
1387 );
1388 (verts, indices, model)
1389 });
1390
1391 {
1395 let slot = &mut self.viewport_slots[vp_idx];
1396 slot.outline_object_buffers = outline_object_buffers;
1397 slot.xray_object_buffers = xray_object_buffers;
1398 slot.constraint_line_buffers = constraint_line_buffers;
1399 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
1400 slot.clip_plane_line_buffers = clip_plane_line_buffers;
1401 slot.cap_buffers = cap_buffers;
1402
1403 if let Some(verts) = axes_verts {
1405 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
1406 if byte_size > slot.axes_vertex_buffer.size() {
1407 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1408 label: Some("vp_axes_vertex_buf"),
1409 size: byte_size,
1410 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1411 mapped_at_creation: false,
1412 });
1413 }
1414 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
1415 slot.axes_vertex_count = verts.len() as u32;
1416 } else {
1417 slot.axes_vertex_count = 0;
1418 }
1419
1420 if let Some((verts, indices, model)) = gizmo_update {
1422 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1423 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1424 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
1425 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1426 label: Some("vp_gizmo_vertex_buf"),
1427 size: vert_bytes.len() as u64,
1428 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1429 mapped_at_creation: false,
1430 });
1431 }
1432 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
1433 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1434 label: Some("vp_gizmo_index_buf"),
1435 size: idx_bytes.len() as u64,
1436 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1437 mapped_at_creation: false,
1438 });
1439 }
1440 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
1441 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
1442 slot.gizmo_index_count = indices.len() as u32;
1443 let uniform = crate::interaction::gizmo::GizmoUniform {
1444 model: model.to_cols_array_2d(),
1445 };
1446 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
1447 }
1448 }
1449
1450 if frame.interaction.outline_selected
1457 && !self.viewport_slots[vp_idx]
1458 .outline_object_buffers
1459 .is_empty()
1460 {
1461 let w = frame.camera.viewport_size[0] as u32;
1462 let h = frame.camera.viewport_size[1] as u32;
1463
1464 self.ensure_viewport_hdr(device, queue, vp_idx, w.max(1), h.max(1));
1466
1467 let slot_ref = &self.viewport_slots[vp_idx];
1471 let outlines_ptr = &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
1472 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
1473 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
1474 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
1475 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
1476 let (outlines, camera_bg, color_view, depth_view) = unsafe {
1479 (
1480 &*outlines_ptr,
1481 &*camera_bg_ptr,
1482 &*color_view_ptr,
1483 &*depth_view_ptr,
1484 )
1485 };
1486
1487 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1488 label: Some("outline_offscreen_encoder"),
1489 });
1490 {
1491 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1492 label: Some("outline_offscreen_pass"),
1493 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1494 view: color_view,
1495 resolve_target: None,
1496 ops: wgpu::Operations {
1497 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1498 store: wgpu::StoreOp::Store,
1499 },
1500 depth_slice: None,
1501 })],
1502 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1503 view: depth_view,
1504 depth_ops: Some(wgpu::Operations {
1505 load: wgpu::LoadOp::Clear(1.0),
1506 store: wgpu::StoreOp::Discard,
1507 }),
1508 stencil_ops: Some(wgpu::Operations {
1509 load: wgpu::LoadOp::Clear(0),
1510 store: wgpu::StoreOp::Discard,
1511 }),
1512 }),
1513 timestamp_writes: None,
1514 occlusion_query_set: None,
1515 });
1516
1517 pass.set_stencil_reference(1);
1519 pass.set_bind_group(0, camera_bg, &[]);
1520 for outlined in outlines {
1521 let Some(mesh) = self
1522 .resources
1523 .mesh_store
1524 .get(crate::resources::mesh_store::MeshId(outlined.mesh_index))
1525 else {
1526 continue;
1527 };
1528 let pipeline = if outlined.two_sided {
1529 &self.resources.stencil_write_two_sided_pipeline
1530 } else {
1531 &self.resources.stencil_write_pipeline
1532 };
1533 pass.set_pipeline(pipeline);
1534 pass.set_bind_group(1, &outlined.stencil_bind_group, &[]);
1535 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1536 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1537 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1538 }
1539
1540 pass.set_pipeline(&self.resources.outline_pipeline);
1542 pass.set_stencil_reference(1);
1543 for outlined in outlines {
1544 let Some(mesh) = self
1545 .resources
1546 .mesh_store
1547 .get(crate::resources::mesh_store::MeshId(outlined.mesh_index))
1548 else {
1549 continue;
1550 };
1551 pass.set_bind_group(1, &outlined.outline_bind_group, &[]);
1552 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1553 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1554 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1555 }
1556 }
1557 queue.submit(std::iter::once(encoder.finish()));
1558 }
1559 }
1560
1561 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, frame: &FrameData) {
1564 let (scene_fx, viewport_fx) = frame.effects.split();
1565 self.prepare_scene_internal(device, queue, frame, &scene_fx);
1566 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
1567 }
1568}