1use super::*;
2
3impl ViewportRenderer {
4 pub fn paint(&self, render_pass: &mut wgpu::RenderPass<'static>, frame: &FrameData) {
10 let vp_idx = frame.camera.viewport_index;
11 let camera_bg = self.viewport_camera_bind_group(vp_idx);
12 let grid_bg = self.viewport_grid_bind_group(vp_idx);
13 let vp_slot = self.viewport_slots.get(vp_idx);
14 emit_draw_calls!(
15 &self.resources,
16 &mut *render_pass,
17 frame,
18 self.use_instancing,
19 &self.instanced_batches,
20 camera_bg,
21 grid_bg,
22 &self.compute_filter_results,
23 vp_slot
24 );
25 emit_scivis_draw_calls!(
26 &self.resources,
27 render_pass,
28 &self.point_cloud_gpu_data,
29 &self.glyph_gpu_data,
30 &self.polyline_gpu_data,
31 &self.volume_gpu_data,
32 &self.streamtube_gpu_data,
33 camera_bg
34 );
35 }
36
37 pub fn paint_to<'rp>(&'rp self, render_pass: &mut wgpu::RenderPass<'rp>, frame: &FrameData) {
43 let vp_idx = frame.camera.viewport_index;
44 let camera_bg = self.viewport_camera_bind_group(vp_idx);
45 let grid_bg = self.viewport_grid_bind_group(vp_idx);
46 let vp_slot = self.viewport_slots.get(vp_idx);
47 emit_draw_calls!(
48 &self.resources,
49 &mut *render_pass,
50 frame,
51 self.use_instancing,
52 &self.instanced_batches,
53 camera_bg,
54 grid_bg,
55 &self.compute_filter_results,
56 vp_slot
57 );
58 emit_scivis_draw_calls!(
59 &self.resources,
60 render_pass,
61 &self.point_cloud_gpu_data,
62 &self.glyph_gpu_data,
63 &self.polyline_gpu_data,
64 &self.volume_gpu_data,
65 &self.streamtube_gpu_data,
66 camera_bg
67 );
68 }
69
70 pub fn render_viewport(
84 &mut self,
85 device: &wgpu::Device,
86 queue: &wgpu::Queue,
87 output_view: &wgpu::TextureView,
88 id: ViewportId,
89 frame: &FrameData,
90 ) -> wgpu::CommandBuffer {
91 self.render_frame_internal(device, queue, output_view, id.0, frame)
92 }
93
94 pub fn render(
102 &mut self,
103 device: &wgpu::Device,
104 queue: &wgpu::Queue,
105 output_view: &wgpu::TextureView,
106 frame: &FrameData,
107 ) -> wgpu::CommandBuffer {
108 self.prepare(device, queue, frame);
110 self.render_frame_internal(
111 device,
112 queue,
113 output_view,
114 frame.camera.viewport_index,
115 frame,
116 )
117 }
118
119 fn render_frame_internal(
124 &mut self,
125 device: &wgpu::Device,
126 queue: &wgpu::Queue,
127 output_view: &wgpu::TextureView,
128 vp_idx: usize,
129 frame: &FrameData,
130 ) -> wgpu::CommandBuffer {
131 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
133 SurfaceSubmission::Flat(items) => items,
134 };
135
136 let bg_color = frame.viewport.background_color.unwrap_or([
137 65.0 / 255.0,
138 65.0 / 255.0,
139 65.0 / 255.0,
140 1.0,
141 ]);
142 let w = frame.camera.viewport_size[0] as u32;
143 let h = frame.camera.viewport_size[1] as u32;
144
145 self.ensure_viewport_hdr(device, queue, vp_idx, w.max(1), h.max(1));
147
148 if !frame.effects.post_process.enabled {
149 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
151 label: Some("ldr_encoder"),
152 });
153 {
154 let slot = &self.viewport_slots[vp_idx];
155 let slot_hdr = slot.hdr.as_ref().unwrap();
156 let camera_bg = &slot.camera_bind_group;
157 let grid_bg = &slot.grid_bind_group;
158 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
159 label: Some("ldr_render_pass"),
160 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
161 view: output_view,
162 resolve_target: None,
163 ops: wgpu::Operations {
164 load: wgpu::LoadOp::Clear(wgpu::Color {
165 r: bg_color[0] as f64,
166 g: bg_color[1] as f64,
167 b: bg_color[2] as f64,
168 a: bg_color[3] as f64,
169 }),
170 store: wgpu::StoreOp::Store,
171 },
172 depth_slice: None,
173 })],
174 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
175 view: &slot_hdr.outline_depth_view,
176 depth_ops: Some(wgpu::Operations {
177 load: wgpu::LoadOp::Clear(1.0),
178 store: wgpu::StoreOp::Discard,
179 }),
180 stencil_ops: None,
181 }),
182 timestamp_writes: None,
183 occlusion_query_set: None,
184 });
185 emit_draw_calls!(
186 &self.resources,
187 &mut render_pass,
188 frame,
189 self.use_instancing,
190 &self.instanced_batches,
191 camera_bg,
192 grid_bg,
193 &self.compute_filter_results,
194 Some(slot)
195 );
196 emit_scivis_draw_calls!(
197 &self.resources,
198 &mut render_pass,
199 &self.point_cloud_gpu_data,
200 &self.glyph_gpu_data,
201 &self.polyline_gpu_data,
202 &self.volume_gpu_data,
203 &self.streamtube_gpu_data,
204 camera_bg
205 );
206 }
207 return encoder.finish();
208 }
209
210 let pp = &frame.effects.post_process;
212
213 let hdr_clear_rgb = [
214 bg_color[0].powf(2.2),
215 bg_color[1].powf(2.2),
216 bg_color[2].powf(2.2),
217 ];
218
219 let mode = match pp.tone_mapping {
221 crate::renderer::ToneMapping::Reinhard => 0u32,
222 crate::renderer::ToneMapping::Aces => 1u32,
223 crate::renderer::ToneMapping::KhronosNeutral => 2u32,
224 };
225 let tm_uniform = crate::resources::ToneMapUniform {
226 exposure: pp.exposure,
227 mode,
228 bloom_enabled: if pp.bloom { 1 } else { 0 },
229 ssao_enabled: if pp.ssao { 1 } else { 0 },
230 contact_shadows_enabled: if pp.contact_shadows { 1 } else { 0 },
231 _pad_tm: [0; 3],
232 background_color: bg_color,
233 };
234 {
235 let hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
236 queue.write_buffer(
237 &hdr.tone_map_uniform_buf,
238 0,
239 bytemuck::cast_slice(&[tm_uniform]),
240 );
241
242 if pp.ssao {
244 let proj = frame.camera.render_camera.projection;
245 let inv_proj = proj.inverse();
246 let ssao_uniform = crate::resources::SsaoUniform {
247 inv_proj: inv_proj.to_cols_array_2d(),
248 proj: proj.to_cols_array_2d(),
249 radius: 0.5,
250 bias: 0.025,
251 _pad: [0.0; 2],
252 };
253 queue.write_buffer(
254 &hdr.ssao_uniform_buf,
255 0,
256 bytemuck::cast_slice(&[ssao_uniform]),
257 );
258 }
259
260 if pp.contact_shadows {
262 let proj = frame.camera.render_camera.projection;
263 let inv_proj = proj.inverse();
264 let light_dir_world: glam::Vec3 =
265 if let Some(l) = frame.effects.lighting.lights.first() {
266 match l.kind {
267 LightKind::Directional { direction } => {
268 glam::Vec3::from(direction).normalize()
269 }
270 LightKind::Spot { direction, .. } => {
271 glam::Vec3::from(direction).normalize()
272 }
273 _ => glam::Vec3::new(0.0, -1.0, 0.0),
274 }
275 } else {
276 glam::Vec3::new(0.0, -1.0, 0.0)
277 };
278 let view = frame.camera.render_camera.view;
279 let light_dir_view = view.transform_vector3(light_dir_world).normalize();
280 let world_up_view = view.transform_vector3(glam::Vec3::Z).normalize();
281 let cs_uniform = crate::resources::ContactShadowUniform {
282 inv_proj: inv_proj.to_cols_array_2d(),
283 proj: proj.to_cols_array_2d(),
284 light_dir_view: [light_dir_view.x, light_dir_view.y, light_dir_view.z, 0.0],
285 world_up_view: [world_up_view.x, world_up_view.y, world_up_view.z, 0.0],
286 params: [
287 pp.contact_shadow_max_distance,
288 pp.contact_shadow_steps as f32,
289 pp.contact_shadow_thickness,
290 0.0,
291 ],
292 };
293 queue.write_buffer(
294 &hdr.contact_shadow_uniform_buf,
295 0,
296 bytemuck::cast_slice(&[cs_uniform]),
297 );
298 }
299
300 if pp.bloom {
302 let bloom_u = crate::resources::BloomUniform {
303 threshold: pp.bloom_threshold,
304 intensity: pp.bloom_intensity,
305 horizontal: 0,
306 _pad: 0,
307 };
308 queue.write_buffer(&hdr.bloom_uniform_buf, 0, bytemuck::cast_slice(&[bloom_u]));
309 }
310 }
311
312 {
314 let hdr = self.viewport_slots[vp_idx].hdr.as_mut().unwrap();
315 self.resources.rebuild_tone_map_bind_group(
316 device,
317 hdr,
318 pp.bloom,
319 pp.ssao,
320 pp.contact_shadows,
321 );
322 }
323
324 {
329 let needs_oit = if self.use_instancing && !self.instanced_batches.is_empty() {
330 self.instanced_batches.iter().any(|b| b.is_transparent)
331 } else {
332 scene_items
333 .iter()
334 .any(|i| i.visible && i.material.opacity < 1.0)
335 };
336 if needs_oit {
337 let hdr = self.viewport_slots[vp_idx].hdr.as_mut().unwrap();
338 self.resources
339 .ensure_viewport_oit(device, hdr, w.max(1), h.max(1));
340 }
341 }
342
343 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
347 label: Some("hdr_encoder"),
348 });
349
350 let slot = &self.viewport_slots[vp_idx];
352 let camera_bg = &slot.camera_bind_group;
353 let slot_hdr = slot.hdr.as_ref().unwrap();
354
355 {
359 let hdr_view = &slot_hdr.hdr_view;
360 let hdr_depth_view = &slot_hdr.hdr_depth_view;
361
362 let clear_wgpu = wgpu::Color {
363 r: hdr_clear_rgb[0] as f64,
364 g: hdr_clear_rgb[1] as f64,
365 b: hdr_clear_rgb[2] as f64,
366 a: bg_color[3] as f64,
367 };
368
369 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
370 label: Some("hdr_scene_pass"),
371 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
372 view: hdr_view,
373 resolve_target: None,
374 ops: wgpu::Operations {
375 load: wgpu::LoadOp::Clear(clear_wgpu),
376 store: wgpu::StoreOp::Store,
377 },
378 depth_slice: None,
379 })],
380 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
381 view: hdr_depth_view,
382 depth_ops: Some(wgpu::Operations {
383 load: wgpu::LoadOp::Clear(1.0),
384 store: wgpu::StoreOp::Store,
385 }),
386 stencil_ops: Some(wgpu::Operations {
387 load: wgpu::LoadOp::Clear(0),
388 store: wgpu::StoreOp::Store,
389 }),
390 }),
391 timestamp_writes: None,
392 occlusion_query_set: None,
393 });
394
395 let resources = &self.resources;
396 render_pass.set_bind_group(0, camera_bg, &[]);
397
398 let show_skybox = frame
400 .effects
401 .environment
402 .as_ref()
403 .is_some_and(|e| e.show_skybox)
404 && resources.ibl_skybox_view.is_some();
405
406 let use_instancing = self.use_instancing;
407 let batches = &self.instanced_batches;
408
409 if !scene_items.is_empty() {
410 if use_instancing && !batches.is_empty() {
411 let excluded_items: Vec<&SceneRenderItem> = scene_items
412 .iter()
413 .filter(|item| {
414 item.visible
415 && (item.active_attribute.is_some()
416 || item.two_sided
417 || item.material.matcap_id.is_some())
418 && resources
419 .mesh_store
420 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
421 .is_some()
422 })
423 .collect();
424
425 let mut opaque_batches: Vec<&InstancedBatch> = Vec::new();
427 let mut transparent_batches: Vec<&InstancedBatch> = Vec::new();
428 for batch in batches {
429 if batch.is_transparent {
430 transparent_batches.push(batch);
431 } else {
432 opaque_batches.push(batch);
433 }
434 }
435
436 if !opaque_batches.is_empty() && !frame.viewport.wireframe_mode {
437 if let Some(ref pipeline) = resources.hdr_solid_instanced_pipeline {
438 render_pass.set_pipeline(pipeline);
439 for batch in &opaque_batches {
440 let Some(mesh) = resources
441 .mesh_store
442 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
443 else {
444 continue;
445 };
446 let mat_key = (
447 batch.texture_id.unwrap_or(u64::MAX),
448 batch.normal_map_id.unwrap_or(u64::MAX),
449 batch.ao_map_id.unwrap_or(u64::MAX),
450 );
451 let Some(inst_tex_bg) =
452 resources.instance_bind_groups.get(&mat_key)
453 else {
454 continue;
455 };
456 render_pass.set_bind_group(1, inst_tex_bg, &[]);
457 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
458 render_pass.set_index_buffer(
459 mesh.index_buffer.slice(..),
460 wgpu::IndexFormat::Uint32,
461 );
462 render_pass.draw_indexed(
463 0..mesh.index_count,
464 0,
465 batch.instance_offset
466 ..batch.instance_offset + batch.instance_count,
467 );
468 }
469 }
470 }
471
472 let _ = &transparent_batches; if frame.viewport.wireframe_mode {
477 if let Some(ref hdr_wf) = resources.hdr_wireframe_pipeline {
478 render_pass.set_pipeline(hdr_wf);
479 for item in scene_items {
480 if !item.visible {
481 continue;
482 }
483 let Some(mesh) = resources
484 .mesh_store
485 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
486 else {
487 continue;
488 };
489 render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
490 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
491 render_pass.set_index_buffer(
492 mesh.edge_index_buffer.slice(..),
493 wgpu::IndexFormat::Uint32,
494 );
495 render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
496 }
497 }
498 } else if let (Some(hdr_solid), Some(hdr_solid_two_sided)) = (
499 &resources.hdr_solid_pipeline,
500 &resources.hdr_solid_two_sided_pipeline,
501 ) {
502 for item in excluded_items
503 .into_iter()
504 .filter(|item| item.material.opacity >= 1.0)
505 {
506 let Some(mesh) = resources
507 .mesh_store
508 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
509 else {
510 continue;
511 };
512 let pipeline = if item.two_sided {
513 hdr_solid_two_sided
514 } else {
515 hdr_solid
516 };
517 render_pass.set_pipeline(pipeline);
518 render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
519 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
520 render_pass.set_index_buffer(
521 mesh.index_buffer.slice(..),
522 wgpu::IndexFormat::Uint32,
523 );
524 render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
525 }
526 }
527 } else {
528 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
530 let dist_from_eye = |item: &&SceneRenderItem| -> f32 {
531 let pos =
532 glam::Vec3::new(item.model[3][0], item.model[3][1], item.model[3][2]);
533 (pos - eye).length()
534 };
535
536 let mut opaque: Vec<&SceneRenderItem> = Vec::new();
537 let mut transparent: Vec<&SceneRenderItem> = Vec::new();
538 for item in scene_items {
539 if !item.visible
540 || resources
541 .mesh_store
542 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
543 .is_none()
544 {
545 continue;
546 }
547 if item.material.opacity < 1.0 {
548 transparent.push(item);
549 } else {
550 opaque.push(item);
551 }
552 }
553 opaque.sort_by(|a, b| {
554 dist_from_eye(a)
555 .partial_cmp(&dist_from_eye(b))
556 .unwrap_or(std::cmp::Ordering::Equal)
557 });
558 transparent.sort_by(|a, b| {
559 dist_from_eye(b)
560 .partial_cmp(&dist_from_eye(a))
561 .unwrap_or(std::cmp::Ordering::Equal)
562 });
563
564 let draw_item_hdr =
565 |render_pass: &mut wgpu::RenderPass<'_>,
566 item: &SceneRenderItem,
567 solid_pl: &wgpu::RenderPipeline,
568 trans_pl: &wgpu::RenderPipeline,
569 wf_pl: &wgpu::RenderPipeline| {
570 let mesh = resources
571 .mesh_store
572 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
573 .unwrap();
574 render_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
577 let is_face_attr =
578 item.active_attribute.as_ref().map_or(false, |a| {
579 matches!(
580 a.kind,
581 crate::resources::AttributeKind::Face
582 | crate::resources::AttributeKind::FaceColor
583 )
584 });
585 if frame.viewport.wireframe_mode {
586 render_pass.set_pipeline(wf_pl);
587 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
588 render_pass.set_index_buffer(
589 mesh.edge_index_buffer.slice(..),
590 wgpu::IndexFormat::Uint32,
591 );
592 render_pass.draw_indexed(0..mesh.edge_index_count, 0, 0..1);
593 } else if is_face_attr {
594 if let Some(ref fvb) = mesh.face_vertex_buffer {
595 let pl = if item.material.opacity < 1.0 {
596 trans_pl
597 } else {
598 solid_pl
599 };
600 render_pass.set_pipeline(pl);
601 render_pass.set_vertex_buffer(0, fvb.slice(..));
602 render_pass.draw(0..mesh.index_count, 0..1);
603 }
604 } else if item.material.opacity < 1.0 {
605 render_pass.set_pipeline(trans_pl);
606 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
607 render_pass.set_index_buffer(
608 mesh.index_buffer.slice(..),
609 wgpu::IndexFormat::Uint32,
610 );
611 render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
612 } else {
613 render_pass.set_pipeline(solid_pl);
614 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
615 render_pass.set_index_buffer(
616 mesh.index_buffer.slice(..),
617 wgpu::IndexFormat::Uint32,
618 );
619 render_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
620 }
621 };
622
623 let _ = &transparent; if let (
627 Some(hdr_solid),
628 Some(hdr_solid_two_sided),
629 Some(hdr_trans),
630 Some(hdr_wf),
631 ) = (
632 &resources.hdr_solid_pipeline,
633 &resources.hdr_solid_two_sided_pipeline,
634 &resources.hdr_transparent_pipeline,
635 &resources.hdr_wireframe_pipeline,
636 ) {
637 for item in &opaque {
638 let solid_pl = if item.two_sided {
639 hdr_solid_two_sided
640 } else {
641 hdr_solid
642 };
643 draw_item_hdr(&mut render_pass, item, solid_pl, hdr_trans, hdr_wf);
644 }
645 }
646 }
647 }
648
649 if !slot.cap_buffers.is_empty() {
651 if let Some(ref hdr_overlay) = resources.hdr_overlay_pipeline {
652 render_pass.set_pipeline(hdr_overlay);
653 render_pass.set_bind_group(0, camera_bg, &[]);
654 for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.cap_buffers {
655 render_pass.set_bind_group(1, bg, &[]);
656 render_pass.set_vertex_buffer(0, vbuf.slice(..));
657 render_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
658 render_pass.draw_indexed(0..*idx_count, 0, 0..1);
659 }
660 }
661 }
662
663 emit_scivis_draw_calls!(
665 &self.resources,
666 &mut render_pass,
667 &self.point_cloud_gpu_data,
668 &self.glyph_gpu_data,
669 &self.polyline_gpu_data,
670 &self.volume_gpu_data,
671 &self.streamtube_gpu_data,
672 camera_bg
673 );
674
675 if show_skybox {
677 render_pass.set_bind_group(0, camera_bg, &[]);
678 render_pass.set_pipeline(&resources.skybox_pipeline);
679 render_pass.draw(0..3, 0..1);
680 }
681 }
682
683 let has_transparent = if self.use_instancing && !self.instanced_batches.is_empty() {
688 self.instanced_batches.iter().any(|b| b.is_transparent)
689 } else {
690 scene_items
691 .iter()
692 .any(|i| i.visible && i.material.opacity < 1.0)
693 };
694
695 if has_transparent {
696 if let (Some(accum_view), Some(reveal_view)) = (
698 slot_hdr.oit_accum_view.as_ref(),
699 slot_hdr.oit_reveal_view.as_ref(),
700 ) {
701 let hdr_depth_view = &slot_hdr.hdr_depth_view;
702 let mut oit_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
704 label: Some("oit_pass"),
705 color_attachments: &[
706 Some(wgpu::RenderPassColorAttachment {
707 view: accum_view,
708 resolve_target: None,
709 ops: wgpu::Operations {
710 load: wgpu::LoadOp::Clear(wgpu::Color {
711 r: 0.0,
712 g: 0.0,
713 b: 0.0,
714 a: 0.0,
715 }),
716 store: wgpu::StoreOp::Store,
717 },
718 depth_slice: None,
719 }),
720 Some(wgpu::RenderPassColorAttachment {
721 view: reveal_view,
722 resolve_target: None,
723 ops: wgpu::Operations {
724 load: wgpu::LoadOp::Clear(wgpu::Color {
725 r: 1.0,
726 g: 1.0,
727 b: 1.0,
728 a: 1.0,
729 }),
730 store: wgpu::StoreOp::Store,
731 },
732 depth_slice: None,
733 }),
734 ],
735 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
736 view: hdr_depth_view,
737 depth_ops: Some(wgpu::Operations {
738 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
740 }),
741 stencil_ops: None,
742 }),
743 timestamp_writes: None,
744 occlusion_query_set: None,
745 });
746
747 oit_pass.set_bind_group(0, camera_bg, &[]);
748
749 if self.use_instancing && !self.instanced_batches.is_empty() {
750 if let Some(ref pipeline) = self.resources.oit_instanced_pipeline {
751 oit_pass.set_pipeline(pipeline);
752 for batch in &self.instanced_batches {
753 if !batch.is_transparent {
754 continue;
755 }
756 let Some(mesh) = self
757 .resources
758 .mesh_store
759 .get(crate::resources::mesh_store::MeshId(batch.mesh_index))
760 else {
761 continue;
762 };
763 let mat_key = (
764 batch.texture_id.unwrap_or(u64::MAX),
765 batch.normal_map_id.unwrap_or(u64::MAX),
766 batch.ao_map_id.unwrap_or(u64::MAX),
767 );
768 let Some(inst_tex_bg) =
769 self.resources.instance_bind_groups.get(&mat_key)
770 else {
771 continue;
772 };
773 oit_pass.set_bind_group(1, inst_tex_bg, &[]);
774 oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
775 oit_pass.set_index_buffer(
776 mesh.index_buffer.slice(..),
777 wgpu::IndexFormat::Uint32,
778 );
779 oit_pass.draw_indexed(
780 0..mesh.index_count,
781 0,
782 batch.instance_offset..batch.instance_offset + batch.instance_count,
783 );
784 }
785 }
786 } else if let Some(ref pipeline) = self.resources.oit_pipeline {
787 oit_pass.set_pipeline(pipeline);
788 for item in scene_items {
789 if !item.visible || item.material.opacity >= 1.0 {
790 continue;
791 }
792 let Some(mesh) = self
793 .resources
794 .mesh_store
795 .get(crate::resources::mesh_store::MeshId(item.mesh_index))
796 else {
797 continue;
798 };
799 oit_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
800 oit_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
801 oit_pass.set_index_buffer(
802 mesh.index_buffer.slice(..),
803 wgpu::IndexFormat::Uint32,
804 );
805 oit_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
806 }
807 }
808 }
809 }
810
811 if has_transparent {
816 if let (Some(pipeline), Some(bg)) = (
817 self.resources.oit_composite_pipeline.as_ref(),
818 slot_hdr.oit_composite_bind_group.as_ref(),
819 ) {
820 let hdr_view = &slot_hdr.hdr_view;
821 let mut composite_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
822 label: Some("oit_composite_pass"),
823 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
824 view: hdr_view,
825 resolve_target: None,
826 ops: wgpu::Operations {
827 load: wgpu::LoadOp::Load,
828 store: wgpu::StoreOp::Store,
829 },
830 depth_slice: None,
831 })],
832 depth_stencil_attachment: None,
833 timestamp_writes: None,
834 occlusion_query_set: None,
835 });
836 composite_pass.set_pipeline(pipeline);
837 composite_pass.set_bind_group(0, bg, &[]);
838 composite_pass.draw(0..3, 0..1);
839 }
840 }
841
842 if !slot.outline_object_buffers.is_empty() {
848 let hdr_pipeline = self
850 .resources
851 .outline_composite_pipeline_hdr
852 .as_ref()
853 .or(self.resources.outline_composite_pipeline_single.as_ref());
854 if let Some(pipeline) = hdr_pipeline {
855 let bg = &slot_hdr.outline_composite_bind_group;
856 let hdr_view = &slot_hdr.hdr_view;
857 let hdr_depth_view = &slot_hdr.hdr_depth_view;
858 let mut outline_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
859 label: Some("hdr_outline_composite_pass"),
860 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
861 view: hdr_view,
862 resolve_target: None,
863 ops: wgpu::Operations {
864 load: wgpu::LoadOp::Load,
865 store: wgpu::StoreOp::Store,
866 },
867 depth_slice: None,
868 })],
869 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
870 view: hdr_depth_view,
871 depth_ops: Some(wgpu::Operations {
872 load: wgpu::LoadOp::Load,
873 store: wgpu::StoreOp::Store,
874 }),
875 stencil_ops: None,
876 }),
877 timestamp_writes: None,
878 occlusion_query_set: None,
879 });
880 outline_pass.set_pipeline(pipeline);
881 outline_pass.set_bind_group(0, bg, &[]);
882 outline_pass.draw(0..3, 0..1);
883 }
884 }
885
886 if pp.ssao {
890 if let Some(ssao_pipeline) = &self.resources.ssao_pipeline {
891 {
892 let mut ssao_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
893 label: Some("ssao_pass"),
894 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
895 view: &slot_hdr.ssao_view,
896 resolve_target: None,
897 ops: wgpu::Operations {
898 load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
899 store: wgpu::StoreOp::Store,
900 },
901 depth_slice: None,
902 })],
903 depth_stencil_attachment: None,
904 timestamp_writes: None,
905 occlusion_query_set: None,
906 });
907 ssao_pass.set_pipeline(ssao_pipeline);
908 ssao_pass.set_bind_group(0, &slot_hdr.ssao_bg, &[]);
909 ssao_pass.draw(0..3, 0..1);
910 }
911
912 if let Some(ssao_blur_pipeline) = &self.resources.ssao_blur_pipeline {
914 let mut ssao_blur_pass =
915 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
916 label: Some("ssao_blur_pass"),
917 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
918 view: &slot_hdr.ssao_blur_view,
919 resolve_target: None,
920 ops: wgpu::Operations {
921 load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
922 store: wgpu::StoreOp::Store,
923 },
924 depth_slice: None,
925 })],
926 depth_stencil_attachment: None,
927 timestamp_writes: None,
928 occlusion_query_set: None,
929 });
930 ssao_blur_pass.set_pipeline(ssao_blur_pipeline);
931 ssao_blur_pass.set_bind_group(0, &slot_hdr.ssao_blur_bg, &[]);
932 ssao_blur_pass.draw(0..3, 0..1);
933 }
934 }
935 }
936
937 if pp.contact_shadows {
941 if let Some(cs_pipeline) = &self.resources.contact_shadow_pipeline {
942 let mut cs_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
943 label: Some("contact_shadow_pass"),
944 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
945 view: &slot_hdr.contact_shadow_view,
946 resolve_target: None,
947 depth_slice: None,
948 ops: wgpu::Operations {
949 load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
950 store: wgpu::StoreOp::Store,
951 },
952 })],
953 depth_stencil_attachment: None,
954 timestamp_writes: None,
955 occlusion_query_set: None,
956 });
957 cs_pass.set_pipeline(cs_pipeline);
958 cs_pass.set_bind_group(0, &slot_hdr.contact_shadow_bg, &[]);
959 cs_pass.draw(0..3, 0..1);
960 }
961 }
962
963 if pp.bloom {
967 if let Some(bloom_threshold_pipeline) = &self.resources.bloom_threshold_pipeline {
969 {
970 let mut threshold_pass =
971 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
972 label: Some("bloom_threshold_pass"),
973 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
974 view: &slot_hdr.bloom_threshold_view,
975 resolve_target: None,
976 ops: wgpu::Operations {
977 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
978 store: wgpu::StoreOp::Store,
979 },
980 depth_slice: None,
981 })],
982 depth_stencil_attachment: None,
983 timestamp_writes: None,
984 occlusion_query_set: None,
985 });
986 threshold_pass.set_pipeline(bloom_threshold_pipeline);
987 threshold_pass.set_bind_group(0, &slot_hdr.bloom_threshold_bg, &[]);
988 threshold_pass.draw(0..3, 0..1);
989 }
990
991 if let Some(blur_pipeline) = &self.resources.bloom_blur_pipeline {
994 let blur_h_bg = &slot_hdr.bloom_blur_h_bg;
995 let blur_h_pong_bg = &slot_hdr.bloom_blur_h_pong_bg;
996 let blur_v_bg = &slot_hdr.bloom_blur_v_bg;
997 let bloom_ping_view = &slot_hdr.bloom_ping_view;
998 let bloom_pong_view = &slot_hdr.bloom_pong_view;
999 const BLUR_ITERATIONS: usize = 4;
1000 for i in 0..BLUR_ITERATIONS {
1001 let h_bg = if i == 0 { blur_h_bg } else { blur_h_pong_bg };
1003 {
1004 let mut h_pass =
1005 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1006 label: Some("bloom_blur_h_pass"),
1007 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1008 view: bloom_ping_view,
1009 resolve_target: None,
1010 ops: wgpu::Operations {
1011 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1012 store: wgpu::StoreOp::Store,
1013 },
1014 depth_slice: None,
1015 })],
1016 depth_stencil_attachment: None,
1017 timestamp_writes: None,
1018 occlusion_query_set: None,
1019 });
1020 h_pass.set_pipeline(blur_pipeline);
1021 h_pass.set_bind_group(0, h_bg, &[]);
1022 h_pass.draw(0..3, 0..1);
1023 }
1024 {
1026 let mut v_pass =
1027 encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1028 label: Some("bloom_blur_v_pass"),
1029 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1030 view: bloom_pong_view,
1031 resolve_target: None,
1032 ops: wgpu::Operations {
1033 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1034 store: wgpu::StoreOp::Store,
1035 },
1036 depth_slice: None,
1037 })],
1038 depth_stencil_attachment: None,
1039 timestamp_writes: None,
1040 occlusion_query_set: None,
1041 });
1042 v_pass.set_pipeline(blur_pipeline);
1043 v_pass.set_bind_group(0, blur_v_bg, &[]);
1044 v_pass.draw(0..3, 0..1);
1045 }
1046 }
1047 }
1048 }
1049 }
1050
1051 let use_fxaa = pp.fxaa;
1055 if let Some(tone_map_pipeline) = &self.resources.tone_map_pipeline {
1056 let tone_target: &wgpu::TextureView = if use_fxaa {
1057 &slot_hdr.fxaa_view
1058 } else {
1059 output_view
1060 };
1061 let mut tone_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1062 label: Some("tone_map_pass"),
1063 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1064 view: tone_target,
1065 resolve_target: None,
1066 ops: wgpu::Operations {
1067 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1068 store: wgpu::StoreOp::Store,
1069 },
1070 depth_slice: None,
1071 })],
1072 depth_stencil_attachment: None,
1073 timestamp_writes: None,
1074 occlusion_query_set: None,
1075 });
1076 tone_pass.set_pipeline(tone_map_pipeline);
1077 tone_pass.set_bind_group(0, &slot_hdr.tone_map_bind_group, &[]);
1078 tone_pass.draw(0..3, 0..1);
1079 }
1080
1081 if use_fxaa {
1085 if let Some(fxaa_pipeline) = &self.resources.fxaa_pipeline {
1086 let mut fxaa_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1087 label: Some("fxaa_pass"),
1088 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1089 view: output_view,
1090 resolve_target: None,
1091 ops: wgpu::Operations {
1092 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1093 store: wgpu::StoreOp::Store,
1094 },
1095 depth_slice: None,
1096 })],
1097 depth_stencil_attachment: None,
1098 timestamp_writes: None,
1099 occlusion_query_set: None,
1100 });
1101 fxaa_pass.set_pipeline(fxaa_pipeline);
1102 fxaa_pass.set_bind_group(0, &slot_hdr.fxaa_bind_group, &[]);
1103 fxaa_pass.draw(0..3, 0..1);
1104 }
1105 }
1106
1107 if frame.viewport.show_grid {
1111 let slot = &self.viewport_slots[vp_idx];
1112 let slot_hdr = slot.hdr.as_ref().unwrap();
1113 let grid_bg = &slot.grid_bind_group;
1114 let mut grid_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1115 label: Some("hdr_grid_pass"),
1116 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1117 view: output_view,
1118 resolve_target: None,
1119 ops: wgpu::Operations {
1120 load: wgpu::LoadOp::Load,
1121 store: wgpu::StoreOp::Store,
1122 },
1123 depth_slice: None,
1124 })],
1125 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1126 view: &slot_hdr.hdr_depth_view,
1127 depth_ops: Some(wgpu::Operations {
1128 load: wgpu::LoadOp::Load,
1129 store: wgpu::StoreOp::Store,
1130 }),
1131 stencil_ops: None,
1132 }),
1133 timestamp_writes: None,
1134 occlusion_query_set: None,
1135 });
1136 grid_pass.set_pipeline(&self.resources.grid_pipeline);
1137 grid_pass.set_bind_group(0, grid_bg, &[]);
1138 grid_pass.draw(0..3, 0..1);
1139 }
1140
1141 {
1145 let slot = &self.viewport_slots[vp_idx];
1146 let slot_hdr = slot.hdr.as_ref().unwrap();
1147 let has_editor_overlays =
1148 (frame.interaction.gizmo_model.is_some() && slot.gizmo_index_count > 0)
1149 || !slot.constraint_line_buffers.is_empty()
1150 || !slot.clip_plane_fill_buffers.is_empty()
1151 || !slot.clip_plane_line_buffers.is_empty()
1152 || !slot.xray_object_buffers.is_empty();
1153 if has_editor_overlays {
1154 let camera_bg = &slot.camera_bind_group;
1155 let mut overlay_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1156 label: Some("hdr_editor_overlay_pass"),
1157 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1158 view: output_view,
1159 resolve_target: None,
1160 ops: wgpu::Operations {
1161 load: wgpu::LoadOp::Load,
1162 store: wgpu::StoreOp::Store,
1163 },
1164 depth_slice: None,
1165 })],
1166 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1167 view: &slot_hdr.hdr_depth_view,
1168 depth_ops: Some(wgpu::Operations {
1169 load: wgpu::LoadOp::Load,
1170 store: wgpu::StoreOp::Discard,
1171 }),
1172 stencil_ops: None,
1173 }),
1174 timestamp_writes: None,
1175 occlusion_query_set: None,
1176 });
1177
1178 if frame.interaction.gizmo_model.is_some() && slot.gizmo_index_count > 0 {
1179 overlay_pass.set_pipeline(&self.resources.gizmo_pipeline);
1180 overlay_pass.set_bind_group(0, camera_bg, &[]);
1181 overlay_pass.set_bind_group(1, &slot.gizmo_bind_group, &[]);
1182 overlay_pass.set_vertex_buffer(0, slot.gizmo_vertex_buffer.slice(..));
1183 overlay_pass.set_index_buffer(
1184 slot.gizmo_index_buffer.slice(..),
1185 wgpu::IndexFormat::Uint32,
1186 );
1187 overlay_pass.draw_indexed(0..slot.gizmo_index_count, 0, 0..1);
1188 }
1189
1190 if !slot.constraint_line_buffers.is_empty() {
1191 overlay_pass.set_pipeline(&self.resources.overlay_line_pipeline);
1192 overlay_pass.set_bind_group(0, camera_bg, &[]);
1193 for (vbuf, ibuf, index_count, _ubuf, bg) in &slot.constraint_line_buffers {
1194 overlay_pass.set_bind_group(1, bg, &[]);
1195 overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
1196 overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
1197 overlay_pass.draw_indexed(0..*index_count, 0, 0..1);
1198 }
1199 }
1200
1201 if !slot.clip_plane_fill_buffers.is_empty() {
1202 overlay_pass.set_pipeline(&self.resources.overlay_pipeline);
1203 overlay_pass.set_bind_group(0, camera_bg, &[]);
1204 for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.clip_plane_fill_buffers {
1205 overlay_pass.set_bind_group(1, bg, &[]);
1206 overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
1207 overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
1208 overlay_pass.draw_indexed(0..*idx_count, 0, 0..1);
1209 }
1210 }
1211
1212 if !slot.clip_plane_line_buffers.is_empty() {
1213 overlay_pass.set_pipeline(&self.resources.overlay_line_pipeline);
1214 overlay_pass.set_bind_group(0, camera_bg, &[]);
1215 for (vbuf, ibuf, idx_count, _ubuf, bg) in &slot.clip_plane_line_buffers {
1216 overlay_pass.set_bind_group(1, bg, &[]);
1217 overlay_pass.set_vertex_buffer(0, vbuf.slice(..));
1218 overlay_pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32);
1219 overlay_pass.draw_indexed(0..*idx_count, 0, 0..1);
1220 }
1221 }
1222
1223 if !slot.xray_object_buffers.is_empty() {
1224 overlay_pass.set_pipeline(&self.resources.xray_pipeline);
1225 overlay_pass.set_bind_group(0, camera_bg, &[]);
1226 for (mesh_idx, _buf, bg) in &slot.xray_object_buffers {
1227 let Some(mesh) = self
1228 .resources
1229 .mesh_store
1230 .get(crate::resources::mesh_store::MeshId(*mesh_idx))
1231 else {
1232 continue;
1233 };
1234 overlay_pass.set_bind_group(1, bg, &[]);
1235 overlay_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1236 overlay_pass.set_index_buffer(
1237 mesh.index_buffer.slice(..),
1238 wgpu::IndexFormat::Uint32,
1239 );
1240 overlay_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1241 }
1242 }
1243 }
1244 }
1245
1246 if frame.viewport.show_axes_indicator {
1249 let slot = &self.viewport_slots[vp_idx];
1250 if slot.axes_vertex_count > 0 {
1251 let slot_hdr = slot.hdr.as_ref().unwrap();
1252 let mut axes_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1253 label: Some("hdr_axes_pass"),
1254 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1255 view: output_view,
1256 resolve_target: None,
1257 ops: wgpu::Operations {
1258 load: wgpu::LoadOp::Load,
1259 store: wgpu::StoreOp::Store,
1260 },
1261 depth_slice: None,
1262 })],
1263 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1264 view: &slot_hdr.hdr_depth_view,
1265 depth_ops: Some(wgpu::Operations {
1266 load: wgpu::LoadOp::Load,
1267 store: wgpu::StoreOp::Discard,
1268 }),
1269 stencil_ops: None,
1270 }),
1271 timestamp_writes: None,
1272 occlusion_query_set: None,
1273 });
1274 axes_pass.set_pipeline(&self.resources.axes_pipeline);
1275 axes_pass.set_vertex_buffer(0, slot.axes_vertex_buffer.slice(..));
1276 axes_pass.draw(0..slot.axes_vertex_count, 0..1);
1277 }
1278 }
1279
1280 encoder.finish()
1281 }
1282
1283 pub fn render_offscreen(
1297 &mut self,
1298 device: &wgpu::Device,
1299 queue: &wgpu::Queue,
1300 frame: &FrameData,
1301 width: u32,
1302 height: u32,
1303 ) -> Vec<u8> {
1304 let target_format = self.resources.target_format;
1306 let offscreen_texture = device.create_texture(&wgpu::TextureDescriptor {
1307 label: Some("offscreen_target"),
1308 size: wgpu::Extent3d {
1309 width: width.max(1),
1310 height: height.max(1),
1311 depth_or_array_layers: 1,
1312 },
1313 mip_level_count: 1,
1314 sample_count: 1,
1315 dimension: wgpu::TextureDimension::D2,
1316 format: target_format,
1317 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1318 view_formats: &[],
1319 });
1320
1321 let output_view = offscreen_texture.create_view(&wgpu::TextureViewDescriptor::default());
1323
1324 let cmd_buf = self.render(device, queue, &output_view, frame);
1332 queue.submit(std::iter::once(cmd_buf));
1333
1334 let bytes_per_pixel = 4u32;
1336 let unpadded_row = width * bytes_per_pixel;
1337 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1338 let padded_row = (unpadded_row + align - 1) & !(align - 1);
1339 let buffer_size = (padded_row * height.max(1)) as u64;
1340
1341 let staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
1342 label: Some("offscreen_staging"),
1343 size: buffer_size,
1344 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1345 mapped_at_creation: false,
1346 });
1347
1348 let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1349 label: Some("offscreen_copy_encoder"),
1350 });
1351 copy_encoder.copy_texture_to_buffer(
1352 wgpu::TexelCopyTextureInfo {
1353 texture: &offscreen_texture,
1354 mip_level: 0,
1355 origin: wgpu::Origin3d::ZERO,
1356 aspect: wgpu::TextureAspect::All,
1357 },
1358 wgpu::TexelCopyBufferInfo {
1359 buffer: &staging_buf,
1360 layout: wgpu::TexelCopyBufferLayout {
1361 offset: 0,
1362 bytes_per_row: Some(padded_row),
1363 rows_per_image: Some(height.max(1)),
1364 },
1365 },
1366 wgpu::Extent3d {
1367 width: width.max(1),
1368 height: height.max(1),
1369 depth_or_array_layers: 1,
1370 },
1371 );
1372 queue.submit(std::iter::once(copy_encoder.finish()));
1373
1374 let (tx, rx) = std::sync::mpsc::channel();
1376 staging_buf
1377 .slice(..)
1378 .map_async(wgpu::MapMode::Read, move |result| {
1379 let _ = tx.send(result);
1380 });
1381 device
1382 .poll(wgpu::PollType::Wait {
1383 submission_index: None,
1384 timeout: Some(std::time::Duration::from_secs(5)),
1385 })
1386 .unwrap();
1387 let _ = rx.recv().unwrap_or(Err(wgpu::BufferAsyncError));
1388
1389 let mut pixels: Vec<u8> = Vec::with_capacity((width * height * 4) as usize);
1390 {
1391 let mapped = staging_buf.slice(..).get_mapped_range();
1392 let data: &[u8] = &mapped;
1393 if padded_row == unpadded_row {
1394 pixels.extend_from_slice(data);
1396 } else {
1397 for row in 0..height as usize {
1399 let start = row * padded_row as usize;
1400 let end = start + unpadded_row as usize;
1401 pixels.extend_from_slice(&data[start..end]);
1402 }
1403 }
1404 }
1405 staging_buf.unmap();
1406
1407 let is_bgra = matches!(
1409 target_format,
1410 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
1411 );
1412 if is_bgra {
1413 for pixel in pixels.chunks_exact_mut(4) {
1414 pixel.swap(0, 2); }
1416 }
1417
1418 pixels
1419 }
1420}