blade_render/model/
mod.rs

1use std::{
2    borrow::Cow,
3    collections::hash_map::{Entry, HashMap},
4    fmt, hash, mem,
5    ops::Range,
6    ptr, str,
7    sync::{Arc, Mutex},
8};
9
10const PRELOAD_TEXTURES: bool = false;
11
12const META_BASE_COLOR: crate::texture::Meta = crate::texture::Meta {
13    format: blade_graphics::TextureFormat::Bc1UnormSrgb,
14    generate_mips: true,
15    y_flip: false,
16};
17const META_NORMAL: crate::texture::Meta = crate::texture::Meta {
18    //Note: "texpresso" doesn't know how to produce signed normalized
19    format: blade_graphics::TextureFormat::Bc5Unorm,
20    generate_mips: false,
21    y_flip: false,
22};
23
24fn pack4x8snorm(v: [f32; 4]) -> u32 {
25    v.iter().rev().fold(0u32, |u, f| {
26        (u << 8) | (f.clamp(-1.0, 1.0) * 127.0 + 0.5) as i8 as u8 as u32
27    })
28}
29
30fn encode_normal(v: [f32; 3]) -> u32 {
31    pack4x8snorm([v[0], v[1], v[2], 0.0])
32}
33
34pub struct Geometry {
35    pub name: String,
36    pub vertex_range: Range<u32>,
37    pub index_offset: u64,
38    pub index_type: Option<blade_graphics::IndexType>,
39    pub triangle_count: u32,
40    pub transform: blade_graphics::Transform,
41    pub material_index: usize,
42}
43
44//TODO: move out into a separate asset type
45pub struct Material {
46    pub base_color_texture: Option<blade_asset::Handle<crate::Texture>>,
47    pub base_color_factor: [f32; 4],
48    pub normal_texture: Option<blade_asset::Handle<crate::Texture>>,
49    pub normal_scale: f32,
50    pub transparent: bool,
51}
52
53pub struct Model {
54    pub name: String,
55    pub winding: f32,
56    pub geometries: Vec<Geometry>,
57    pub materials: Vec<Material>,
58    pub vertex_buffer: blade_graphics::Buffer,
59    pub index_buffer: blade_graphics::Buffer,
60    pub transform_buffer: blade_graphics::Buffer,
61    pub acceleration_structure: blade_graphics::AccelerationStructure,
62}
63
64#[derive(blade_macros::Flat, Default)]
65struct TextureReference<'a> {
66    path: Cow<'a, [u8]>,
67    embedded_data: Cow<'a, [u8]>,
68    //Note: this isn't used for anything during deserialization
69    source_index: usize,
70}
71
72#[derive(blade_macros::Flat)]
73struct CookedMaterial<'a> {
74    base_color: TextureReference<'a>,
75    base_color_factor: [f32; 4],
76    normal: TextureReference<'a>,
77    normal_scale: f32,
78    transparent: bool,
79}
80
81#[derive(blade_macros::Flat)]
82struct CookedGeometry<'a> {
83    name: Cow<'a, [u8]>,
84    vertices: Cow<'a, [crate::Vertex]>,
85    indices: Cow<'a, [u32]>,
86    transform: [f32; 12],
87    material_index: u32,
88}
89
90#[derive(Clone, PartialEq)]
91struct GltfVertex {
92    position: [f32; 3],
93    normal: [f32; 3],
94    tangent: [f32; 4],
95    tex_coords: [f32; 2],
96}
97impl Default for GltfVertex {
98    fn default() -> Self {
99        Self {
100            position: [0.0; 3],
101            normal: [0.0, 1.0, 0.0],
102            tangent: [1.0, 0.0, 0.0, 0.0],
103            tex_coords: [0.0; 2],
104        }
105    }
106}
107impl Eq for GltfVertex {}
108impl hash::Hash for GltfVertex {
109    fn hash<H: hash::Hasher>(&self, state: &mut H) {
110        for f in self.position.iter() {
111            f.to_bits().hash(state);
112        }
113        for f in self.normal.iter() {
114            f.to_bits().hash(state);
115        }
116        for f in self.tangent.iter() {
117            f.to_bits().hash(state);
118        }
119        for f in self.tex_coords.iter() {
120            f.to_bits().hash(state);
121        }
122    }
123}
124
125#[cfg(feature = "asset")]
126struct FlattenedGeometry(Box<[GltfVertex]>);
127#[cfg(feature = "asset")]
128impl mikktspace::Geometry for FlattenedGeometry {
129    fn num_faces(&self) -> usize {
130        self.0.len() / 3
131    }
132    fn num_vertices_of_face(&self, _face: usize) -> usize {
133        3
134    }
135    fn position(&self, face: usize, vert: usize) -> [f32; 3] {
136        self.0[face * 3 + vert].position
137    }
138    fn normal(&self, face: usize, vert: usize) -> [f32; 3] {
139        self.0[face * 3 + vert].normal
140    }
141    fn tex_coord(&self, face: usize, vert: usize) -> [f32; 2] {
142        self.0[face * 3 + vert].tex_coords
143    }
144    fn set_tangent_encoded(&mut self, tangent: [f32; 4], face: usize, vert: usize) {
145        self.0[face * 3 + vert].tangent = tangent;
146    }
147}
148#[cfg(feature = "asset")]
149impl FlattenedGeometry {
150    #[profiling::function]
151    fn reconstruct_indices(self) -> (Vec<u32>, Vec<crate::Vertex>) {
152        let mut indices = Vec::with_capacity(self.0.len());
153        let mut vertices = Vec::new();
154        let mut cache = HashMap::new();
155        for v in self.0.iter() {
156            let i = match cache.entry(v.clone()) {
157                Entry::Occupied(e) => *e.get(),
158                Entry::Vacant(e) => {
159                    let i = vertices.len() as u32;
160                    let t = &v.tangent;
161                    vertices.push(crate::Vertex {
162                        position: v.position,
163                        bitangent_sign: t[3],
164                        tex_coords: v.tex_coords,
165                        normal: encode_normal(v.normal),
166                        tangent: encode_normal([t[0], t[1], t[2]]),
167                    });
168                    *e.insert(i)
169                }
170            };
171            indices.push(i);
172        }
173        log::debug!("Compacted {}->{}", self.0.len(), vertices.len());
174        (indices, vertices)
175    }
176}
177
178#[derive(blade_macros::Flat)]
179pub struct CookedModel<'a> {
180    name: &'a [u8],
181    winding: f32,
182    materials: Vec<CookedMaterial<'a>>,
183    geometries: Vec<CookedGeometry<'a>>,
184}
185
186#[cfg(feature = "asset")]
187impl CookedModel<'_> {
188    fn populate_gltf(
189        &mut self,
190        g_node: gltf::Node,
191        parent_transform: glam::Mat4,
192        data_buffers: &[Vec<u8>],
193        flattened_geos: &mut Vec<FlattenedGeometry>,
194    ) {
195        let local_transform = glam::Mat4::from_cols_array_2d(&g_node.transform().matrix());
196        let global_transform = parent_transform * local_transform;
197
198        if let Some(g_mesh) = g_node.mesh() {
199            let name = g_node.name().unwrap_or("");
200            let col_matrix = mint::ColumnMatrix3x4 {
201                x: global_transform.x_axis.truncate().into(),
202                y: global_transform.y_axis.truncate().into(),
203                z: global_transform.z_axis.truncate().into(),
204                w: global_transform.w_axis.truncate().into(),
205            };
206            let transform = mint::RowMatrix3x4::from(col_matrix).into();
207
208            for (prim_index, g_primitive) in g_mesh.primitives().enumerate() {
209                if g_primitive.mode() != gltf::mesh::Mode::Triangles {
210                    log::warn!(
211                        "Skipping primitive '{}'[{}] for having mesh mode {:?}",
212                        name,
213                        prim_index,
214                        g_primitive.mode()
215                    );
216                    continue;
217                }
218                let material_index = match g_primitive.material().index() {
219                    Some(index) => index as u32,
220                    None => {
221                        log::warn!(
222                            "Skipping primitive '{}'[{}] for having default material",
223                            name,
224                            prim_index
225                        );
226                        continue;
227                    }
228                };
229
230                let reader = g_primitive.reader(|buffer| Some(&data_buffers[buffer.index()]));
231                let vertex_count = g_primitive.get(&gltf::Semantic::Positions).unwrap().count();
232
233                // Read the vertices into memory
234                flattened_geos.push({
235                    profiling::scope!("Read data");
236                    let mut pre_vertices = vec![GltfVertex::default(); vertex_count];
237
238                    for (v, pos) in pre_vertices
239                        .iter_mut()
240                        .zip(reader.read_positions().unwrap())
241                    {
242                        for component in pos {
243                            assert!(component.is_finite());
244                        }
245                        v.position = pos;
246                    }
247                    if let Some(iter) = reader.read_tex_coords(0) {
248                        for (v, tc) in pre_vertices.iter_mut().zip(iter.into_f32()) {
249                            v.tex_coords = tc;
250                        }
251                    } else {
252                        log::warn!("No tex coords in {name}");
253                    }
254                    if let Some(iter) = reader.read_normals() {
255                        assert_eq!(
256                            pre_vertices.len(),
257                            iter.len(),
258                            "geometry {name} doesn't have enough normals"
259                        );
260                        for (v, normal) in pre_vertices.iter_mut().zip(iter) {
261                            v.normal = normal;
262                            assert_ne!(encode_normal(normal), 0);
263                        }
264                    } else {
265                        log::warn!("No normals in {name}");
266                    }
267
268                    // Untangle from the index buffer
269                    match reader.read_indices() {
270                        Some(read) => FlattenedGeometry(
271                            read.into_u32()
272                                .map(|i| pre_vertices[i as usize].clone())
273                                .collect(),
274                        ),
275                        None => FlattenedGeometry(pre_vertices.into_boxed_slice()),
276                    }
277                });
278
279                self.geometries.push(CookedGeometry {
280                    name: Cow::Owned(name.as_bytes().to_owned()),
281                    vertices: Cow::Borrowed(&[]),
282                    indices: Cow::Borrowed(&[]),
283                    transform,
284                    material_index,
285                });
286            }
287        }
288
289        for child in g_node.children() {
290            self.populate_gltf(child, global_transform, data_buffers, flattened_geos);
291        }
292    }
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
296pub enum FrontFace {
297    Clockwise,
298    CounterClockwise,
299}
300impl Default for FrontFace {
301    fn default() -> Self {
302        Self::CounterClockwise
303    }
304}
305
306#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
307pub struct Meta {
308    pub generate_tangents: bool,
309    pub front_face: FrontFace,
310}
311
312impl fmt::Display for Meta {
313    fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
314        Ok(()) //TODO
315    }
316}
317
318#[derive(Debug)]
319struct Transfer {
320    stage: blade_graphics::Buffer,
321    dst: blade_graphics::Buffer,
322    size: u64,
323}
324
325#[derive(Debug)]
326struct BlasConstruct {
327    meshes: Vec<blade_graphics::AccelerationStructureMesh>,
328    scratch: blade_graphics::Buffer,
329    dst: blade_graphics::AccelerationStructure,
330}
331
332#[derive(Default)]
333struct PendingOperations {
334    transfers: Vec<Transfer>,
335    blas_constructs: Vec<BlasConstruct>,
336}
337
338enum TextureSource {
339    Path(String),
340    Embedded(
341        Option<choir::IdleTask>,
342        Arc<blade_asset::Cooker<super::texture::Baker>>,
343    ),
344}
345
346#[cfg(feature = "asset")]
347impl TextureReference<'_> {
348    fn complete(&mut self, sources: &slab::Slab<TextureSource>) {
349        match sources.get(self.source_index) {
350            Some(&TextureSource::Embedded(ref _task, ref sub_cooker)) => {
351                self.embedded_data = Cow::Owned(sub_cooker.extract_embedded());
352            }
353            Some(&TextureSource::Path(ref full)) => {
354                self.path = Cow::Owned(full.as_bytes().to_owned());
355            }
356            None => {}
357        }
358    }
359}
360
361pub struct Baker {
362    gpu_context: Arc<blade_graphics::Context>,
363    pending_operations: Mutex<PendingOperations>,
364    //TODO: change to asset materials
365    asset_textures: Arc<blade_asset::AssetManager<crate::texture::Baker>>,
366}
367
368impl Baker {
369    pub fn new(
370        gpu_context: &Arc<blade_graphics::Context>,
371        asset_textures: &Arc<blade_asset::AssetManager<crate::texture::Baker>>,
372    ) -> Self {
373        Self {
374            gpu_context: Arc::clone(gpu_context),
375            pending_operations: Mutex::new(PendingOperations::default()),
376            asset_textures: Arc::clone(asset_textures),
377        }
378    }
379
380    pub fn flush(
381        &self,
382        encoder: &mut blade_graphics::CommandEncoder,
383        temp_buffers: &mut Vec<blade_graphics::Buffer>,
384    ) {
385        let mut pending_ops = self.pending_operations.lock().unwrap();
386        if !pending_ops.transfers.is_empty() {
387            let mut pass = encoder.transfer("init models");
388            for transfer in pending_ops.transfers.drain(..) {
389                pass.copy_buffer_to_buffer(
390                    transfer.stage.into(),
391                    transfer.dst.into(),
392                    transfer.size,
393                );
394                temp_buffers.push(transfer.stage);
395            }
396        }
397        if !pending_ops.blas_constructs.is_empty() {
398            let mut pass = encoder.acceleration_structure("BLAS");
399            for construct in pending_ops.blas_constructs.drain(..) {
400                pass.build_bottom_level(construct.dst, &construct.meshes, construct.scratch.into());
401                temp_buffers.push(construct.scratch);
402            }
403        }
404    }
405
406    #[cfg(feature = "asset")]
407    fn cook_texture(
408        &self,
409        texture: gltf::texture::Texture,
410        meta: super::texture::Meta,
411        parent_cooker: &blade_asset::Cooker<Baker>,
412        data_buffers: &[Vec<u8>],
413    ) -> TextureSource {
414        match texture.source().source() {
415            gltf::image::Source::View { view, mime_type } => {
416                let sub_cooker = Arc::new(blade_asset::Cooker::new_embedded());
417                let cooker = Arc::clone(&sub_cooker);
418                let baker = Arc::clone(&self.asset_textures.baker);
419                let buffer = &data_buffers[view.buffer().index()];
420                let data = buffer[view.offset()..view.offset() + view.length()].to_vec();
421                let extension = mime_type.split_once('/').unwrap().1.to_string();
422                let task =
423                    self.asset_textures
424                        .choir
425                        .spawn("embedded cook")
426                        .init(move |exe_ontext| {
427                            blade_asset::Baker::cook(
428                                baker.as_ref(),
429                                &data,
430                                &extension,
431                                meta,
432                                cooker,
433                                &exe_ontext,
434                            );
435                        });
436                TextureSource::Embedded(Some(task), sub_cooker)
437            }
438            gltf::image::Source::Uri { uri, mime_type: _ } => {
439                let relative = if let Some(_rest) = uri.strip_prefix("data:") {
440                    panic!("Data URL isn't supported for textures yet");
441                } else if let Some(rest) = uri.strip_prefix("file://") {
442                    rest
443                } else if let Some(rest) = uri.strip_prefix("file:") {
444                    rest
445                } else {
446                    uri
447                };
448                let full = parent_cooker.base_path().join(relative);
449                if PRELOAD_TEXTURES {
450                    self.asset_textures.load(&full, meta);
451                }
452                TextureSource::Path(full.to_str().unwrap().to_string())
453            }
454        }
455    }
456
457    fn serve_texture(
458        &self,
459        texture_ref: &TextureReference,
460        meta: super::texture::Meta,
461        exe_context: &choir::ExecutionContext,
462    ) -> Option<blade_asset::Handle<super::texture::Texture>> {
463        if !texture_ref.path.is_empty() {
464            let path_str = str::from_utf8(&texture_ref.path).unwrap();
465            let (handle, task) = self.asset_textures.load(path_str, meta);
466            exe_context.add_fork(&task);
467            Some(handle)
468        } else if !texture_ref.embedded_data.is_empty() {
469            let cooked = unsafe {
470                <super::texture::CookedImage<'_> as blade_asset::Flat>::read(
471                    texture_ref.embedded_data.as_ptr(),
472                )
473            };
474            Some(
475                self.asset_textures
476                    .load_cooked_inside_task(cooked, exe_context),
477            )
478        } else {
479            None
480        }
481    }
482}
483
484impl blade_asset::Baker for Baker {
485    type Meta = Meta;
486    type Data<'a> = CookedModel<'a>;
487    type Output = Model;
488
489    fn cook(
490        &self,
491        source: &[u8],
492        extension: &str,
493        meta: Meta,
494        cooker: Arc<blade_asset::Cooker<Self>>,
495        exe_context: &choir::ExecutionContext,
496    ) {
497        match extension {
498            #[cfg(feature = "asset")]
499            "gltf" | "glb" => {
500                use base64::engine::{general_purpose::URL_SAFE as ENCODING_ENGINE, Engine as _};
501
502                let gltf::Gltf { document, mut blob } = gltf::Gltf::from_slice(source).unwrap();
503                // extract buffers
504                let mut buffers = Vec::new();
505                for buffer in document.buffers() {
506                    let mut data = match buffer.source() {
507                        gltf::buffer::Source::Uri(uri) => {
508                            if let Some(rest) = uri.strip_prefix("data:") {
509                                let (_before, after) = rest.split_once(";base64,").unwrap();
510                                ENCODING_ENGINE.decode(after).unwrap()
511                            } else if let Some(rest) = uri.strip_prefix("file://") {
512                                cooker.add_dependency(rest.as_ref())
513                            } else if let Some(rest) = uri.strip_prefix("file:") {
514                                cooker.add_dependency(rest.as_ref())
515                            } else {
516                                cooker.add_dependency(uri.as_ref())
517                            }
518                        }
519                        gltf::buffer::Source::Bin => blob.take().unwrap(),
520                    };
521                    assert!(data.len() >= buffer.length());
522                    while data.len() % 4 != 0 {
523                        data.push(0);
524                    }
525                    buffers.push(data);
526                }
527
528                let mut sources = slab::Slab::new();
529                let mut model = CookedModel {
530                    name: &[],
531                    winding: match meta.front_face {
532                        FrontFace::Clockwise => -1.0,
533                        FrontFace::CounterClockwise => 1.0,
534                    },
535                    materials: Vec::new(),
536                    geometries: Vec::new(),
537                };
538                for g_material in document.materials() {
539                    let pbr = g_material.pbr_metallic_roughness();
540                    model.materials.push(CookedMaterial {
541                        base_color: TextureReference {
542                            source_index: match pbr.base_color_texture() {
543                                Some(info) => sources.insert(self.cook_texture(
544                                    info.texture(),
545                                    META_BASE_COLOR,
546                                    &cooker,
547                                    &buffers,
548                                )),
549                                None => !0,
550                            },
551                            ..Default::default()
552                        },
553                        base_color_factor: pbr.base_color_factor(),
554                        normal: TextureReference {
555                            source_index: match g_material.normal_texture() {
556                                Some(info) => sources.insert(self.cook_texture(
557                                    info.texture(),
558                                    META_NORMAL,
559                                    &cooker,
560                                    &buffers,
561                                )),
562                                None => !0,
563                            },
564                            ..Default::default()
565                        },
566                        normal_scale: g_material.normal_texture().map_or(0.0, |info| info.scale()),
567                        transparent: g_material.alpha_mode() != gltf::material::AlphaMode::Opaque,
568                    });
569                }
570
571                let mut flattened_geos = Vec::new();
572                for g_scene in document.scenes() {
573                    for g_node in g_scene.nodes() {
574                        model.populate_gltf(
575                            g_node,
576                            glam::Mat4::IDENTITY,
577                            &buffers,
578                            &mut flattened_geos,
579                        );
580                    }
581                }
582
583                assert!(
584                    !model.geometries.is_empty(),
585                    "Empty models are not supported yet"
586                );
587                let model_shared = Arc::new(Mutex::new(model));
588                let model_clone = Arc::clone(&model_shared);
589                let gen_tangents = exe_context.choir().spawn("generate tangents").init_iter(
590                    flattened_geos.into_iter().enumerate(),
591                    move |_, (index, mut fg)| {
592                        if meta.generate_tangents {
593                            let ok = mikktspace::generate_tangents(&mut fg);
594                            assert!(ok, "MikkTSpace failed");
595                        }
596                        let (indices, vertices) = fg.reconstruct_indices();
597                        let mut model = model_clone.lock().unwrap();
598                        let geo = &mut model.geometries[index];
599                        geo.vertices = Cow::Owned(vertices);
600                        geo.indices = Cow::Owned(indices);
601                    },
602                );
603
604                let mut dependencies = vec![gen_tangents];
605                for (_, source) in sources.iter_mut() {
606                    if let TextureSource::Embedded(ref mut task, _) = *source {
607                        dependencies.push(task.take().unwrap())
608                    }
609                }
610
611                let mut finish = exe_context.fork("finish").init(move |_| {
612                    let mut model = Arc::into_inner(model_shared).unwrap().into_inner().unwrap();
613                    for material in model.materials.iter_mut() {
614                        material.base_color.complete(&sources);
615                        material.normal.complete(&sources);
616                    }
617                    cooker.finish(model);
618                });
619                for dependency in dependencies {
620                    finish.depend_on(&dependency);
621                }
622            }
623            other => panic!("Unknown model extension: {}", other),
624        }
625    }
626
627    fn serve(&self, model: CookedModel<'_>, exe_context: &choir::ExecutionContext) -> Self::Output {
628        let mut materials = Vec::with_capacity(model.materials.len());
629        for material in model.materials.iter() {
630            materials.push(Material {
631                base_color_texture: self.serve_texture(
632                    &material.base_color,
633                    META_BASE_COLOR,
634                    exe_context,
635                ),
636                base_color_factor: material.base_color_factor,
637                normal_texture: self.serve_texture(&material.normal, META_NORMAL, exe_context),
638                normal_scale: material.normal_scale,
639                transparent: material.transparent,
640            });
641        }
642
643        let total_vertices = model
644            .geometries
645            .iter()
646            .map(|geo| geo.vertices.len())
647            .sum::<usize>();
648        let total_vertex_size = (total_vertices * mem::size_of::<crate::Vertex>()) as u64;
649        let vertex_buffer = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
650            name: "vertex",
651            size: total_vertex_size,
652            memory: blade_graphics::Memory::Device,
653        });
654        let vertex_stage = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
655            name: "vertex stage",
656            size: total_vertex_size,
657            memory: blade_graphics::Memory::Upload,
658        });
659
660        let total_indices = model
661            .geometries
662            .iter()
663            .map(|geo| geo.indices.len())
664            .sum::<usize>();
665        let total_index_size = total_indices as u64 * 4;
666        let index_buffer = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
667            name: "index",
668            size: total_index_size,
669            memory: blade_graphics::Memory::Device,
670        });
671        let index_stage = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
672            name: "index stage",
673            size: total_index_size,
674            memory: blade_graphics::Memory::Upload,
675        });
676
677        let total_transform_size =
678            (model.geometries.len() * mem::size_of::<blade_graphics::Transform>()) as u64;
679        let transform_buffer = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
680            name: "transform",
681            size: total_transform_size,
682            memory: blade_graphics::Memory::Device,
683        });
684        let transform_stage = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
685            name: "transform stage",
686            size: total_transform_size,
687            memory: blade_graphics::Memory::Upload,
688        });
689
690        let mut meshes = Vec::with_capacity(model.geometries.len());
691        let vertex_stride = mem::size_of::<super::Vertex>() as u32;
692        let mut start_vertex = 0;
693        let mut index_offset = 0;
694        let mut transform_offset = 0;
695        let mut geometries = Vec::with_capacity(model.geometries.len());
696        for geometry in model.geometries.iter() {
697            let material = &model.materials[geometry.material_index as usize];
698            unsafe {
699                ptr::copy_nonoverlapping(
700                    geometry.vertices.as_ptr(),
701                    (vertex_stage.data() as *mut crate::Vertex).add(start_vertex as usize),
702                    geometry.vertices.len(),
703                );
704                ptr::copy_nonoverlapping(
705                    geometry.indices.as_ptr(),
706                    index_stage.data().add(index_offset as usize) as *mut u32,
707                    geometry.indices.len(),
708                );
709                ptr::copy_nonoverlapping(
710                    geometry.transform.as_ptr() as *const u8,
711                    transform_stage.data().add(transform_offset as usize),
712                    mem::size_of::<blade_graphics::Transform>(),
713                );
714            }
715            let index_type = if geometry.indices.is_empty() {
716                None
717            } else {
718                Some(blade_graphics::IndexType::U32)
719            };
720            let triangle_count = if geometry.indices.is_empty() {
721                geometry.vertices.len() as u32 / 3
722            } else {
723                geometry.indices.len() as u32 / 3
724            };
725            meshes.push(blade_graphics::AccelerationStructureMesh {
726                vertex_data: vertex_buffer.at(start_vertex as u64 * vertex_stride as u64),
727                vertex_format: blade_graphics::VertexFormat::F32Vec3,
728                vertex_stride,
729                vertex_count: geometry.vertices.len() as u32,
730                index_data: index_buffer.at(index_offset),
731                index_type,
732                triangle_count,
733                transform_data: transform_buffer.at(transform_offset), //TODO
734                is_opaque: !material.transparent,
735            });
736            geometries.push(Geometry {
737                name: String::from_utf8_lossy(geometry.name.as_ref()).into_owned(),
738                vertex_range: start_vertex..start_vertex + geometry.vertices.len() as u32,
739                index_offset,
740                index_type,
741                triangle_count,
742                transform: geometry.transform.into(),
743                material_index: geometry.material_index as usize,
744            });
745            start_vertex += geometry.vertices.len() as u32;
746            index_offset += geometry.indices.len() as u64 * 4;
747            transform_offset += mem::size_of::<blade_graphics::Transform>() as u64;
748        }
749        assert_eq!(start_vertex as usize, total_vertices);
750        assert_eq!(index_offset, total_index_size);
751        assert_eq!(transform_offset, total_transform_size);
752
753        let sizes = self
754            .gpu_context
755            .get_bottom_level_acceleration_structure_sizes(&meshes);
756        let acceleration_structure = self.gpu_context.create_acceleration_structure(
757            blade_graphics::AccelerationStructureDesc {
758                name: str::from_utf8(model.name).unwrap(),
759                ty: blade_graphics::AccelerationStructureType::BottomLevel,
760                size: sizes.data,
761            },
762        );
763        let scratch = self.gpu_context.create_buffer(blade_graphics::BufferDesc {
764            name: "BLAS scratch",
765            size: sizes.scratch,
766            memory: blade_graphics::Memory::Device,
767        });
768
769        let mut pending_ops = self.pending_operations.lock().unwrap();
770        pending_ops.transfers.push(Transfer {
771            stage: vertex_stage,
772            dst: vertex_buffer,
773            size: total_vertex_size,
774        });
775        pending_ops.transfers.push(Transfer {
776            stage: index_stage,
777            dst: index_buffer,
778            size: total_index_size,
779        });
780        pending_ops.transfers.push(Transfer {
781            stage: transform_stage,
782            dst: transform_buffer,
783            size: total_transform_size,
784        });
785        pending_ops.blas_constructs.push(BlasConstruct {
786            meshes,
787            scratch,
788            dst: acceleration_structure,
789        });
790
791        Model {
792            name: String::from_utf8_lossy(model.name).into_owned(),
793            winding: model.winding,
794            geometries,
795            materials,
796            vertex_buffer,
797            index_buffer,
798            transform_buffer,
799            acceleration_structure,
800        }
801    }
802
803    fn delete(&self, model: Self::Output) {
804        self.gpu_context
805            .destroy_acceleration_structure(model.acceleration_structure);
806        self.gpu_context.destroy_buffer(model.vertex_buffer);
807        self.gpu_context.destroy_buffer(model.index_buffer);
808        self.gpu_context.destroy_buffer(model.transform_buffer);
809    }
810}