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