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