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.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.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.cache_hints.scene_generation == self.last_scene_generation
525 && frame.cache_hints.selection_generation == self.last_selection_generation
526 && frame.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.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.cache_hints.scene_generation;
635 self.last_selection_generation = frame.cache_hints.selection_generation;
636 self.last_wireframe_mode = frame.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 && !frame.viewport.is_2d {
839 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
840 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
841
842 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
848 (frame.viewport.grid_cell_size, 1.0_f32)
849 } else {
850 let vertical_depth = (eye.y - frame.viewport.grid_y).abs().max(1.0);
851 let world_per_pixel = 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
852 / frame.camera.viewport_size[1].max(1.0);
853 let target = (world_per_pixel * 60.0).max(1e-9_f32);
854 let mut s = 1.0_f32;
855 while s < target {
856 s *= 10.0;
857 }
858 let ratio = (target / s).clamp(0.0, 1.0);
862 let fade = if ratio < 0.5 {
863 1.0_f32
864 } else {
865 let t = (ratio - 0.5) * 2.0; 1.0 - t * t * (3.0 - 2.0 * t) };
868 (s, fade)
869 };
870
871 let spacing_major = spacing * 10.0;
875 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
876 let snap_z = (eye.z / spacing_major).floor() * spacing_major;
877
878 let orient = frame.camera.render_camera.orientation;
882 let right = orient * glam::Vec3::X;
883 let up = orient * glam::Vec3::Y;
884 let back = orient * glam::Vec3::Z;
885 let cam_to_world = [
886 [right.x, right.y, right.z, 0.0_f32],
887 [up.x, up.y, up.z, 0.0_f32],
888 [back.x, back.y, back.z, 0.0_f32],
889 ];
890 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
891 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
892
893 let uniform = GridUniform {
894 view_proj: view_proj_mat,
895 cam_to_world,
896 tan_half_fov,
897 aspect,
898 _pad_ivp: [0.0; 2],
899 eye_pos: frame.camera.render_camera.eye_position,
900 grid_y: frame.viewport.grid_y,
901 spacing_minor: spacing,
902 spacing_major,
903 snap_origin: [snap_x, snap_z],
904 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
909 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
910 };
911 queue.write_buffer(
912 &resources.grid_uniform_buf,
913 0,
914 bytemuck::cast_slice(&[uniform]),
915 );
916 }
917
918 resources.bc_quad_buffers.clear();
920 for quad in &frame.viewport.overlay_quads {
921 let buf = resources.create_overlay_quad(device, &quad.corners, quad.color);
922 resources.bc_quad_buffers.push(buf);
923 }
924
925 resources.constraint_line_buffers.clear();
926 for overlay in &frame.interaction.constraint_overlays {
927 let buf = resources.create_constraint_overlay(device, overlay);
928 resources.constraint_line_buffers.push(buf);
929 }
930
931 resources.cap_buffers.clear();
933 if frame.effects.cap_fill_enabled {
934 let active_planes: Vec<_> = frame.effects.clip_planes.iter().filter(|p| p.enabled).collect();
935 for plane in &active_planes {
936 let plane_n = glam::Vec3::from(plane.normal);
937 for item in scene_items.iter().filter(|i| i.visible) {
938 let Some(mesh) = resources
939 .mesh_store
940 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
941 else {
942 continue;
943 };
944 let model = glam::Mat4::from_cols_array_2d(&item.model);
945 let world_aabb = mesh.aabb.transformed(&model);
946 if !world_aabb.intersects_plane(plane_n, plane.distance) {
947 continue;
948 }
949 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices) else {
950 continue;
951 };
952 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
953 pos,
954 idx,
955 &model,
956 plane_n,
957 plane.distance,
958 ) {
959 let bc = item.material.base_color;
960 let color = plane.cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
961 let buf = resources.upload_cap_geometry(device, &cap, color);
962 resources.cap_buffers.push(buf);
963 }
964 }
965 }
966 }
967
968 if frame.viewport.show_axes_indicator && frame.camera.viewport_size[0] > 0.0 && frame.camera.viewport_size[1] > 0.0
970 {
971 let verts = crate::widgets::axes_indicator::build_axes_geometry(
972 frame.camera.viewport_size[0],
973 frame.camera.viewport_size[1],
974 frame.camera.render_camera.orientation,
975 );
976 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
977 if byte_size > resources.axes_vertex_buffer.size() {
978 resources.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
980 label: Some("axes_vertex_buf"),
981 size: byte_size,
982 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
983 mapped_at_creation: false,
984 });
985 }
986 if !verts.is_empty() {
987 queue.write_buffer(
988 &resources.axes_vertex_buffer,
989 0,
990 bytemuck::cast_slice(&verts),
991 );
992 }
993 resources.axes_vertex_count = verts.len() as u32;
994 } else {
995 resources.axes_vertex_count = 0;
996 }
997
998 self.point_cloud_gpu_data.clear();
1003 if !frame.scene.point_clouds.is_empty() {
1004 resources.ensure_point_cloud_pipeline(device);
1005 for item in &frame.scene.point_clouds {
1006 if item.positions.is_empty() {
1007 continue;
1008 }
1009 let gpu_data = resources.upload_point_cloud(device, queue, item);
1010 self.point_cloud_gpu_data.push(gpu_data);
1011 }
1012 }
1013
1014 self.glyph_gpu_data.clear();
1015 if !frame.scene.glyphs.is_empty() {
1016 resources.ensure_glyph_pipeline(device);
1017 for item in &frame.scene.glyphs {
1018 if item.positions.is_empty() || item.vectors.is_empty() {
1019 continue;
1020 }
1021 let gpu_data = resources.upload_glyph_set(device, queue, item);
1022 self.glyph_gpu_data.push(gpu_data);
1023 }
1024 }
1025
1026 self.polyline_gpu_data.clear();
1031 if !frame.scene.polylines.is_empty() {
1032 resources.ensure_polyline_pipeline(device);
1033 for item in &frame.scene.polylines {
1034 if item.positions.is_empty() {
1035 continue;
1036 }
1037 let gpu_data = resources.upload_polyline(device, queue, item);
1038 self.polyline_gpu_data.push(gpu_data);
1039 }
1040 }
1041
1042 if !frame.scene.isolines.is_empty() {
1047 resources.ensure_polyline_pipeline(device);
1048 for item in &frame.scene.isolines {
1049 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
1050 continue;
1051 }
1052 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
1053 if positions.is_empty() {
1054 continue;
1055 }
1056 let polyline = PolylineItem {
1057 positions,
1058 scalars: Vec::new(), strip_lengths,
1060 scalar_range: None,
1061 colormap_id: None,
1062 default_color: item.color,
1063 line_width: item.line_width,
1064 id: 0, };
1066 let gpu_data = resources.upload_polyline(device, queue, &polyline);
1067 self.polyline_gpu_data.push(gpu_data);
1068 }
1069 }
1070
1071 self.streamtube_gpu_data.clear();
1076 if !frame.scene.streamtube_items.is_empty() {
1077 resources.ensure_streamtube_pipeline(device);
1078 for item in &frame.scene.streamtube_items {
1079 if item.positions.is_empty() || item.strip_lengths.is_empty() {
1080 continue;
1081 }
1082 let gpu_data = resources.upload_streamtube(device, queue, item);
1083 if gpu_data.instance_count > 0 {
1084 self.streamtube_gpu_data.push(gpu_data);
1085 }
1086 }
1087 }
1088
1089 self.volume_gpu_data.clear();
1094 if !frame.scene.volumes.is_empty() {
1095 resources.ensure_volume_pipeline(device);
1096 for item in &frame.scene.volumes {
1097 let gpu = resources.upload_volume_frame(device, queue, item, &frame.effects.clip_planes);
1098 self.volume_gpu_data.push(gpu);
1099 }
1100 }
1101
1102 {
1104 let total = scene_items.len() as u32;
1105 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
1106 let mut draw_calls = 0u32;
1107 let mut triangles = 0u64;
1108 let instanced_batch_count = if self.use_instancing {
1109 self.instanced_batches.len() as u32
1110 } else {
1111 0
1112 };
1113
1114 if self.use_instancing {
1115 for batch in &self.instanced_batches {
1116 if let Some(mesh) = resources
1117 .mesh_store
1118 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
1119 {
1120 draw_calls += 1;
1121 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
1122 }
1123 }
1124 } else {
1125 for item in scene_items {
1126 if !item.visible {
1127 continue;
1128 }
1129 if let Some(mesh) = resources
1130 .mesh_store
1131 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
1132 {
1133 draw_calls += 1;
1134 triangles += (mesh.index_count / 3) as u64;
1135 }
1136 }
1137 }
1138
1139 self.last_stats = crate::renderer::stats::FrameStats {
1140 total_objects: total,
1141 visible_objects: visible,
1142 culled_objects: total.saturating_sub(visible),
1143 draw_calls,
1144 instanced_batches: instanced_batch_count,
1145 triangles_submitted: triangles,
1146 shadow_draw_calls: 0, };
1148 }
1149
1150 if frame.effects.lighting.shadows_enabled && !scene_items.is_empty() {
1156 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1157 label: Some("shadow_pass_encoder"),
1158 });
1159 {
1160 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1161 label: Some("shadow_pass"),
1162 color_attachments: &[],
1163 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1164 view: &resources.shadow_map_view,
1165 depth_ops: Some(wgpu::Operations {
1166 load: wgpu::LoadOp::Clear(1.0),
1167 store: wgpu::StoreOp::Store,
1168 }),
1169 stencil_ops: None,
1170 }),
1171 timestamp_writes: None,
1172 occlusion_query_set: None,
1173 });
1174
1175 let mut shadow_draws = 0u32;
1176 let tile_px = tile_size as f32;
1177
1178 if self.use_instancing {
1179 if let (Some(pipeline), Some(instance_bg)) = (
1182 &resources.shadow_instanced_pipeline,
1183 self.instanced_batches.first().and_then(|b| {
1184 resources.instance_bind_groups.get(&(
1185 b.texture_id.unwrap_or(u64::MAX),
1186 b.normal_map_id.unwrap_or(u64::MAX),
1187 b.ao_map_id.unwrap_or(u64::MAX),
1188 ))
1189 }),
1190 ) {
1191 for cascade in 0..effective_cascade_count {
1192 let tile_col = (cascade % 2) as f32;
1193 let tile_row = (cascade / 2) as f32;
1194 shadow_pass.set_viewport(
1195 tile_col * tile_px,
1196 tile_row * tile_px,
1197 tile_px,
1198 tile_px,
1199 0.0,
1200 1.0,
1201 );
1202 shadow_pass.set_scissor_rect(
1203 (tile_col * tile_px) as u32,
1204 (tile_row * tile_px) as u32,
1205 tile_size,
1206 tile_size,
1207 );
1208
1209 shadow_pass.set_pipeline(pipeline);
1210
1211 queue.write_buffer(
1213 resources.shadow_instanced_cascade_bufs[cascade]
1214 .as_ref()
1215 .expect("shadow_instanced_cascade_bufs not allocated"),
1216 0,
1217 bytemuck::cast_slice(
1218 &cascade_view_projs[cascade].to_cols_array_2d(),
1219 ),
1220 );
1221
1222 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
1223 .as_ref()
1224 .expect("shadow_instanced_cascade_bgs not allocated");
1225 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1226 shadow_pass.set_bind_group(1, instance_bg, &[]);
1227
1228 for batch in &self.instanced_batches {
1229 if batch.is_transparent {
1231 continue;
1232 }
1233 let Some(mesh) = resources
1234 .mesh_store
1235 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
1236 else {
1237 continue;
1238 };
1239 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1240 shadow_pass.set_index_buffer(
1241 mesh.index_buffer.slice(..),
1242 wgpu::IndexFormat::Uint32,
1243 );
1244 shadow_pass.draw_indexed(
1245 0..mesh.index_count,
1246 0,
1247 batch.instance_offset
1248 ..batch.instance_offset + batch.instance_count,
1249 );
1250 shadow_draws += 1;
1251 }
1252 }
1253 }
1254 } else {
1255 for cascade in 0..effective_cascade_count {
1257 let tile_col = (cascade % 2) as f32;
1259 let tile_row = (cascade / 2) as f32;
1260 shadow_pass.set_viewport(
1261 tile_col * tile_px,
1262 tile_row * tile_px,
1263 tile_px,
1264 tile_px,
1265 0.0,
1266 1.0,
1267 );
1268 shadow_pass.set_scissor_rect(
1269 (tile_col * tile_px) as u32,
1270 (tile_row * tile_px) as u32,
1271 tile_size,
1272 tile_size,
1273 );
1274
1275 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1276 shadow_pass.set_bind_group(
1278 0,
1279 &resources.shadow_bind_group,
1280 &[cascade as u32 * 256],
1281 );
1282
1283 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1285 &cascade_view_projs[cascade],
1286 );
1287
1288 for item in scene_items.iter() {
1289 if !item.visible {
1290 continue;
1291 }
1292 if item.material.opacity < 1.0 {
1294 continue;
1295 }
1296 let Some(mesh) = resources
1297 .mesh_store
1298 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
1299 else {
1300 continue;
1301 };
1302
1303 let world_aabb = mesh
1304 .aabb
1305 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1306 if cascade_frustum.cull_aabb(&world_aabb) {
1307 continue;
1308 }
1309
1310 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1313 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1314 shadow_pass.set_index_buffer(
1315 mesh.index_buffer.slice(..),
1316 wgpu::IndexFormat::Uint32,
1317 );
1318 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1319 shadow_draws += 1;
1320 }
1321 }
1322 }
1323 drop(shadow_pass);
1324 self.last_stats.shadow_draw_calls = shadow_draws;
1325 }
1326 queue.submit(std::iter::once(encoder.finish()));
1327 }
1328
1329 if frame.interaction.outline_selected && !resources.outline_object_buffers.is_empty() {
1335 let w = frame.camera.viewport_size[0] as u32;
1336 let h = frame.camera.viewport_size[1] as u32;
1337 resources.ensure_outline_target(device, w, h);
1338
1339 if let (Some(color_view), Some(depth_view)) =
1340 (&resources.outline_color_view, &resources.outline_depth_view)
1341 {
1342 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1343 label: Some("outline_offscreen_encoder"),
1344 });
1345 {
1346 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1347 label: Some("outline_offscreen_pass"),
1348 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1349 view: color_view,
1350 resolve_target: None,
1351 ops: wgpu::Operations {
1352 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1353 store: wgpu::StoreOp::Store,
1354 },
1355 depth_slice: None,
1356 })],
1357 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1358 view: depth_view,
1359 depth_ops: Some(wgpu::Operations {
1360 load: wgpu::LoadOp::Clear(1.0),
1361 store: wgpu::StoreOp::Discard,
1362 }),
1363 stencil_ops: Some(wgpu::Operations {
1364 load: wgpu::LoadOp::Clear(0),
1365 store: wgpu::StoreOp::Discard,
1366 }),
1367 }),
1368 timestamp_writes: None,
1369 occlusion_query_set: None,
1370 });
1371
1372 pass.set_pipeline(&resources.stencil_write_pipeline);
1376 pass.set_stencil_reference(1);
1377 pass.set_bind_group(0, &resources.camera_bind_group, &[]);
1378 for outlined in &resources.outline_object_buffers {
1379 let Some(mesh) = resources
1380 .mesh_store
1381 .get(crate::resources::mesh_store::MeshId(outlined.mesh_index))
1382 else {
1383 continue;
1384 };
1385 pass.set_bind_group(1, &outlined.stencil_bind_group, &[]);
1386 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1387 pass.set_index_buffer(
1388 mesh.index_buffer.slice(..),
1389 wgpu::IndexFormat::Uint32,
1390 );
1391 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1392 }
1393
1394 pass.set_pipeline(&resources.outline_pipeline);
1396 pass.set_stencil_reference(1);
1397 for outlined in &resources.outline_object_buffers {
1398 let Some(mesh) = resources
1399 .mesh_store
1400 .get(crate::resources::mesh_store::MeshId(outlined.mesh_index))
1401 else {
1402 continue;
1403 };
1404 pass.set_bind_group(1, &outlined.outline_bind_group, &[]);
1405 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1406 pass.set_index_buffer(
1407 mesh.index_buffer.slice(..),
1408 wgpu::IndexFormat::Uint32,
1409 );
1410 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1411 }
1412 }
1413 queue.submit(std::iter::once(encoder.finish()));
1414 }
1415 }
1416 }
1417}