1use crate::engine::ecs::component::{
2 ColorComponent, EmissiveComponent, GLTFComponent, MeshComponent, RenderableComponent,
3 SerializeComponent, SkinnedMeshComponent, TextureComponent, TransformComponent,
4};
5use crate::engine::ecs::{ComponentId, SignalEmitter, World};
6use crate::engine::graphics::mesh::{CpuMesh, CpuVertex};
7use crate::engine::graphics::primitives::TransformMatrix;
8use crate::engine::graphics::primitives::{CpuMeshHandle, MaterialHandle, Renderable};
9use crate::engine::graphics::{RenderAssets, RenderUploader, SkinId, VisualWorld};
10use crate::engine::user_input::InputState;
11use std::collections::{HashMap, HashSet};
12use std::env;
13use std::path::Path;
14
15#[derive(Debug, Default)]
16pub struct GLTFSystem {
17 tracked_components: HashSet<ComponentId>,
18 spawned_components: HashSet<ComponentId>,
19 resources_by_uri: HashMap<String, LoadedGltf>,
20}
21
22#[derive(Debug)]
23struct LoadedGltf {
24 gltf_name: String,
25 meshes: Vec<ImportedMesh>,
26 texture_keys: Vec<String>,
27 textures: Vec<ImportedTexture>,
28 skins: Vec<ImportedSkin>,
29 meshes_registered: bool,
30 textures_uploaded: bool,
31}
32
33#[derive(Debug)]
34struct ImportedMesh {
35 key: String,
36 mesh: Option<CpuMesh>,
37}
38
39#[derive(Debug)]
40struct ImportedTexture {
41 rgba: Option<Vec<u8>>,
42 width: u32,
43 height: u32,
44}
45
46#[derive(Debug, Clone)]
47struct ImportedSkin {
48 joints: Vec<usize>,
49 inverse_bind_matrices: Vec<TransformMatrix>,
50 skeleton_root: Option<usize>,
51}
52
53impl GLTFSystem {
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn register_component(&mut self, component_id: ComponentId) {
59 self.tracked_components.insert(component_id);
60 }
61
62 pub fn tracked_components(&self) -> impl Iterator<Item = ComponentId> + '_ {
63 self.tracked_components.iter().copied()
64 }
65
66 pub fn tracked_component_count(&self) -> usize {
67 self.tracked_components.len()
68 }
69
70 pub fn cached_resource_count(&self) -> usize {
71 self.resources_by_uri.len()
72 }
73
74 pub fn cached_mesh_count(&self) -> usize {
75 self.resources_by_uri
76 .values()
77 .map(|loaded| loaded.meshes.len())
78 .sum()
79 }
80
81 pub fn cached_texture_count(&self) -> usize {
82 self.resources_by_uri
83 .values()
84 .map(|loaded| loaded.texture_keys.len())
85 .sum()
86 }
87
88 pub fn cached_cpu_bytes(&self) -> usize {
89 self.resources_by_uri
90 .values()
91 .map(LoadedGltf::approximate_heap_bytes)
92 .sum()
93 }
94
95 fn debug_enabled() -> bool {
96 match env::var("LITTLE_CAT_GLTF_DEBUG") {
97 Ok(v) => {
98 let v = v.trim().to_ascii_lowercase();
99 !(v.is_empty() || v == "0" || v == "false" || v == "off")
100 }
101 Err(_) => false,
102 }
103 }
104
105 fn import_audit_enabled() -> bool {
106 match env::var("CAT_DEBUG_GLTF_IMPORT_AUDIT") {
107 Ok(v) => {
108 let v = v.trim().to_ascii_lowercase();
109 v == "1" || v == "true" || v == "on" || v == "yes"
110 }
111 Err(_) => false,
112 }
113 }
114
115 fn debug_indent(n: usize) -> String {
116 let mut s = String::new();
117 for _ in 0..n {
118 s.push_str(" ");
119 }
120 s
121 }
122
123 fn debug_dump_document(uri: &str, doc: &gltf::Document, loaded: &LoadedGltf) {
124 println!("[GLTFSystem][debug] ===== GLTF dump: '{}' =====", uri);
125 println!(
126 "[GLTFSystem][debug] scenes={} meshes={} materials={} images={}",
127 doc.scenes().len(),
128 doc.meshes().len(),
129 doc.materials().len(),
130 doc.images().len()
131 );
132
133 if let Some(scene) = doc.default_scene() {
134 println!(
135 "[GLTFSystem][debug] default_scene index={} name={:?}",
136 scene.index(),
137 scene.name()
138 );
139 } else {
140 println!("[GLTFSystem][debug] default_scene <none>");
141 }
142
143 for (i, img) in doc.images().enumerate() {
144 println!(
145 "[GLTFSystem][debug] image[{}] name={:?} source={:?}",
146 i,
147 img.name(),
148 img.source()
149 );
150 }
151
152 for (i, mat) in doc.materials().enumerate() {
153 let pbr = mat.pbr_metallic_roughness();
154 let base_color_factor = pbr.base_color_factor();
155 let base_color_tex = pbr
156 .base_color_texture()
157 .map(|t| (t.texture().index(), t.texture().source().index()));
158
159 println!(
160 "[GLTFSystem][debug] material[{}] name={:?} double_sided={} alpha_mode={:?} base_color_factor={:?} base_color_tex={:?}",
161 i,
162 mat.name(),
163 mat.double_sided(),
164 mat.alpha_mode(),
165 base_color_factor,
166 base_color_tex
167 );
168 }
169
170 for scene in doc.scenes() {
171 println!(
172 "[GLTFSystem][debug] scene index={} name={:?} root_nodes={} ",
173 scene.index(),
174 scene.name(),
175 scene.nodes().len()
176 );
177 for node in scene.nodes() {
178 Self::debug_dump_node(node, 1, loaded);
179 }
180 }
181 }
182
183 fn debug_dump_node(node: gltf::Node, depth: usize, loaded: &LoadedGltf) {
184 let indent = Self::debug_indent(depth);
185 let name = node.name().unwrap_or("<unnamed>");
186 let mesh_info = node.mesh().map(|m| {
187 let mesh_name = m.name().unwrap_or("<unnamed>");
188 format!("mesh#{} name='{}'", m.index(), mesh_name)
189 });
190
191 println!(
192 "[GLTFSystem][debug] {}node index={} name='{}' mesh={:?} children={}",
193 indent,
194 node.index(),
195 name,
196 mesh_info,
197 node.children().len()
198 );
199
200 if let Some(mesh) = node.mesh() {
201 let mesh_name_or_index = mesh
202 .name()
203 .map(Self::sanitize_key_part)
204 .filter(|s| !s.is_empty())
205 .unwrap_or_else(|| format!("mesh{}", mesh.index()));
206
207 for (prim_index, prim) in mesh.primitives().enumerate() {
208 let mode = prim.mode();
209 let mat = prim.material();
210 let pbr = mat.pbr_metallic_roughness();
211 let base_color_factor = pbr.base_color_factor();
212 let base_color_tex = pbr
213 .base_color_texture()
214 .map(|t| (t.texture().index(), t.texture().source().index()));
215
216 let mesh_key = format!(
217 "{}:{}:prim{}",
218 loaded.gltf_name, mesh_name_or_index, prim_index
219 );
220
221 println!(
222 "[GLTFSystem][debug] {} prim{} mode={:?} mesh_key='{}' material name={:?} base_color_factor={:?} base_color_tex={:?}",
223 indent,
224 prim_index,
225 mode,
226 mesh_key,
227 mat.name(),
228 base_color_factor,
229 base_color_tex
230 );
231 }
232 }
233
234 for ch in node.children() {
235 Self::debug_dump_node(ch, depth + 1, loaded);
236 }
237 }
238
239 fn candidate_paths(uri: &str) -> Vec<String> {
240 let mut paths = vec![uri.to_string()];
241 let uri_path = Path::new(uri);
242 if !uri_path.is_absolute() {
243 let manifest_join = Path::new(env!("CARGO_MANIFEST_DIR")).join(uri);
244 let manifest_join = manifest_join.to_string_lossy().to_string();
245 if manifest_join != uri {
246 paths.push(manifest_join);
247 }
248 }
249 paths
250 }
251
252 fn import_with_fallback(
253 uri: &str,
254 ) -> Result<
255 (
256 gltf::Document,
257 Vec<gltf::buffer::Data>,
258 Vec<gltf::image::Data>,
259 ),
260 String,
261 > {
262 let mut last_err: Option<(String, String)> = None;
263 for candidate in Self::candidate_paths(uri) {
264 match gltf::import(&candidate) {
265 Ok(ok) => {
266 if candidate != uri {
267 println!("[GLTFSystem] resolved '{}' -> '{}'", uri, candidate);
268 }
269 return Ok(ok);
270 }
271 Err(err) => {
272 last_err = Some((candidate, err.to_string()));
273 }
274 }
275 }
276
277 if let Some((candidate, err)) = last_err {
278 Err(format!(
279 "{} (tried '{}'; cwd may differ from project root)",
280 err, candidate
281 ))
282 } else {
283 Err("unknown error".to_string())
284 }
285 }
286
287 pub fn tick_with_queue(
292 &mut self,
293 world: &mut World,
294 visuals: &mut VisualWorld,
295 skinned_mesh: &mut crate::engine::ecs::system::SkinnedMeshSystem,
296 emit: &mut dyn SignalEmitter,
297 _dt_sec: f32,
298 ) {
299 self.tracked_components
300 .retain(|component_id| world.get_component_record(*component_id).is_some());
301 self.spawned_components
302 .retain(|component_id| world.get_component_record(*component_id).is_some());
303
304 let gltf_components: Vec<ComponentId> = self.tracked_components.iter().copied().collect();
305
306 for cid in gltf_components {
307 if self.spawned_components.contains(&cid) {
308 continue;
309 }
310
311 let serialize_spawned_nodes = world
312 .children_of(cid)
313 .iter()
314 .find_map(|&child| {
315 world
316 .get_component_by_id_as::<SerializeComponent>(child)
317 .map(|serialize| serialize.enabled)
318 })
319 .unwrap_or(false);
320
321 let Some(uri) = world
322 .get_component_by_id_as::<GLTFComponent>(cid)
323 .map(|c| c.uri.clone())
324 else {
325 continue;
326 };
327
328 if !self.resources_by_uri.contains_key(&uri) {
330 match Self::load_gltf_resources(&uri) {
331 Ok(r) => {
332 self.resources_by_uri.insert(uri.clone(), r);
333 }
334 Err(err) => {
335 println!("[GLTFSystem] failed to load '{}': {}", uri, err);
336 self.spawned_components.insert(cid);
338 continue;
339 }
340 }
341 }
342
343 let Some(anchor_transform) = Self::nearest_transform_ancestor(world, cid) else {
344 println!(
345 "[GLTFSystem] gltf component has no Transform ancestor (cid={:?})",
346 cid
347 );
348 self.spawned_components.insert(cid);
349 continue;
350 };
351
352 let Some(loaded) = self.resources_by_uri.get(&uri) else {
353 self.spawned_components.insert(cid);
354 continue;
355 };
356
357 let Ok((doc, buffers, _images)) = Self::import_with_fallback(&uri) else {
360 self.spawned_components.insert(cid);
361 continue;
362 };
363 let scene = doc.default_scene().or_else(|| doc.scenes().next());
364 let Some(scene) = scene else {
365 self.spawned_components.insert(cid);
366 continue;
367 };
368
369 if Self::debug_enabled() {
370 Self::debug_dump_document(&uri, &doc, loaded);
371 }
372
373 let mut node_index_to_component: HashMap<usize, ComponentId> = HashMap::new();
376 let mut pending_skin_components: Vec<(ComponentId, usize)> = Vec::new();
377 let joint_node_indices: HashSet<usize> = loaded
378 .skins
379 .iter()
380 .flat_map(|skin| skin.joints.iter().copied())
381 .collect();
382
383 for node in scene.nodes() {
384 let root = self.spawn_node_recursive(
385 world,
386 anchor_transform,
387 &buffers,
388 loaded,
389 node,
390 &mut node_index_to_component,
391 &mut pending_skin_components,
392 serialize_spawned_nodes,
393 );
394 if let Some(root) = root {
395 world.init_component_tree(root, emit);
396 }
397 }
398
399 if Self::import_audit_enabled() {
400 println!(
401 "[GLTFSystem][audit] spawned uri='{}' component={cid:?} nodes={} joints={}",
402 uri,
403 node_index_to_component.len(),
404 joint_node_indices.len()
405 );
406 }
407
408 let mut skin_id_by_index: HashMap<usize, SkinId> = HashMap::new();
413 for (skin_index, skin) in loaded.skins.iter().enumerate() {
414 let skin_id = visuals.upsert_skin(
415 &uri,
416 skin_index,
417 skin.joints.clone(),
418 skin.inverse_bind_matrices.clone(),
419 );
420 skin_id_by_index.insert(skin_index, skin_id);
421
422 let mut joints_resolved: Vec<Option<ComponentId>> =
423 Vec::with_capacity(skin.joints.len());
424 for &node_index in &skin.joints {
425 joints_resolved.push(node_index_to_component.get(&node_index).copied());
426 }
427
428 let debug_joint_order = std::env::var("CAT_DEBUG_SKIN_JOINT_ORDER")
429 .ok()
430 .map(|s| {
431 let s = s.trim().to_ascii_lowercase();
432 s == "1" || s == "true" || s == "on" || s == "yes"
433 })
434 .unwrap_or(false);
435 if debug_joint_order {
436 println!(
437 "[GLTFSystem] skin joint order: uri='{}' skin_index={} joints={} (showing 0..16 and 74)",
438 uri,
439 skin_index,
440 skin.joints.len()
441 );
442
443 let mut to_show: Vec<usize> = (0..skin.joints.len().min(16)).collect();
444 if skin.joints.len() > 74 {
445 to_show.push(74);
446 }
447
448 for joint_i in to_show {
449 let node_i = skin.joints[joint_i];
450 let name = joints_resolved[joint_i]
451 .and_then(|cid| world.get_component_record(cid).map(|n| n.name.clone()))
452 .unwrap_or_else(|| "<missing>".to_string());
453 println!(
454 " joint_index={joint_i:03} gltf_node_index={node_i:03} name={name}",
455 );
456 }
457 }
458
459 skinned_mesh.register_skin_instance_joints(cid, skin_id, joints_resolved);
460 }
461
462 for (skinned_cid, skin_index) in pending_skin_components {
463 let Some(skin_id) = skin_id_by_index.get(&skin_index).copied() else {
464 continue;
465 };
466 let Some(sm) =
467 world.get_component_by_id_as_mut::<SkinnedMeshComponent>(skinned_cid)
468 else {
469 continue;
470 };
471 sm.skin_id = Some(skin_id);
472 }
473
474 self.spawned_components.insert(cid);
476 if let Some(c) = world.get_component_by_id_as_mut::<GLTFComponent>(cid) {
477 c.spawned = true;
478 c.spawned_node_transforms = node_index_to_component.values().copied().collect();
479 c.armature_joint_transforms = joint_node_indices
480 .iter()
481 .filter_map(|node_index| node_index_to_component.get(node_index).copied())
482 .collect();
483 }
484 }
485 }
486
487 pub fn flush_imports(
488 &mut self,
489 render_assets: &mut RenderAssets,
490 texture_system: &mut crate::engine::ecs::system::TextureSystem,
491 uploader: &mut dyn RenderUploader,
492 ) {
493 for loaded in self.resources_by_uri.values_mut() {
494 if !loaded.meshes_registered {
495 for m in &loaded.meshes {
496 let Some(mesh) = &m.mesh else {
497 continue;
498 };
499 if Self::import_audit_enabled() {
500 println!(
501 "[GLTFSystem][audit] imported mesh key='{}' first_registration=true verts={} indices={}",
502 m.key,
503 mesh.vertices.len(),
504 mesh.indices_u32.len()
505 );
506 }
507 let _h = render_assets.register_imported_mesh(m.key.clone(), mesh.clone());
508 }
509 for mesh in &mut loaded.meshes {
510 mesh.mesh = None;
511 }
512 loaded.meshes_registered = true;
513 }
514
515 if !loaded.textures_uploaded {
516 for (index, t) in loaded.textures.iter_mut().enumerate() {
517 let Some(rgba) = t.rgba.as_deref() else {
518 continue;
519 };
520 let key = loaded
521 .texture_keys
522 .get(index)
523 .cloned()
524 .unwrap_or_else(|| format!("{}:{}", loaded.gltf_name, index));
525 match uploader.upload_texture_rgba8(rgba, t.width, t.height) {
526 Ok(handle) => {
527 texture_system.register_cached_texture(key, handle);
528 }
529 Err(err) => {
530 println!(
531 "[GLTFSystem] texture upload failed for key='{}': {:?}",
532 key, err
533 );
534 }
535 }
536 t.rgba = None;
537 }
538 loaded.textures_uploaded = true;
539 }
540 }
541 }
542
543 pub fn flush_mesh_imports_only(&mut self, render_assets: &mut RenderAssets) {
548 for loaded in self.resources_by_uri.values_mut() {
549 if loaded.meshes_registered {
550 continue;
551 }
552 for m in &loaded.meshes {
553 let Some(mesh) = &m.mesh else {
554 continue;
555 };
556 if Self::import_audit_enabled() {
557 println!(
558 "[GLTFSystem][audit] imported mesh key='{}' first_registration=true verts={} indices={}",
559 m.key,
560 mesh.vertices.len(),
561 mesh.indices_u32.len()
562 );
563 }
564 let _h = render_assets.register_imported_mesh(m.key.clone(), mesh.clone());
565 }
566 for mesh in &mut loaded.meshes {
567 mesh.mesh = None;
568 }
569 loaded.meshes_registered = true;
570 }
571 }
572
573 fn nearest_transform_ancestor(world: &World, mut cid: ComponentId) -> Option<ComponentId> {
574 while let Some(parent) = world.parent_of(cid) {
575 if world
576 .get_component_by_id_as::<TransformComponent>(parent)
577 .is_some()
578 {
579 return Some(parent);
580 }
581 cid = parent;
582 }
583 None
584 }
585
586 fn sanitize_key_part(s: &str) -> String {
587 s.chars()
589 .map(|c| match c {
590 ' ' | '\t' | '\n' | '\r' => '_',
591 '{' | '}' => '_',
592 _ => c,
593 })
594 .collect()
595 }
596
597 fn is_black_rgb(rgb: [f32; 3]) -> bool {
598 let eps = 1e-4_f32;
599 rgb[0].abs() <= eps && rgb[1].abs() <= eps && rgb[2].abs() <= eps
600 }
601
602 fn load_gltf_resources(uri: &str) -> Result<LoadedGltf, String> {
603 let (doc, buffers, images) = Self::import_with_fallback(uri)?;
604
605 let gltf_name = Path::new(uri)
606 .file_stem()
607 .and_then(|s| s.to_str())
608 .map(Self::sanitize_key_part)
609 .filter(|s| !s.is_empty())
610 .unwrap_or_else(|| "gltf".to_string());
611
612 let mut meshes: Vec<ImportedMesh> = Vec::new();
614 for (mesh_index, mesh) in doc.meshes().enumerate() {
615 let mesh_name_or_index = mesh
616 .name()
617 .map(Self::sanitize_key_part)
618 .filter(|s| !s.is_empty())
619 .unwrap_or_else(|| format!("mesh{}", mesh_index));
620
621 for (prim_index, prim) in mesh.primitives().enumerate() {
622 if prim.mode() != gltf::mesh::Mode::Triangles {
624 continue;
625 }
626
627 let reader = prim.reader(|b| Some(&buffers[b.index()].0));
628
629 let Some(positions_iter) = reader.read_positions() else {
630 continue;
631 };
632 let positions: Vec<[f32; 3]> = positions_iter.collect();
633
634 let uvs: Vec<[f32; 2]> = reader
635 .read_tex_coords(0)
636 .map(|t| t.into_f32().collect())
637 .unwrap_or_default();
638
639 let indices_u32: Vec<u32> = match reader.read_indices() {
640 Some(read) => read.into_u32().collect(),
641 None => (0..positions.len() as u32).collect(),
642 };
643
644 let joints0: Option<Vec<[u16; 4]>> = reader
646 .read_joints(0)
647 .map(|j| j.into_u16().collect::<Vec<[u16; 4]>>());
648 let weights0: Option<Vec<[f32; 4]>> = reader
649 .read_weights(0)
650 .map(|w| w.into_f32().collect::<Vec<[f32; 4]>>());
651
652 let mut normals: Vec<[f32; 3]> = reader
654 .read_normals()
655 .map(|it| it.collect())
656 .unwrap_or_default();
657
658 if normals.len() != positions.len() {
659 normals.clear();
660 }
661
662 if normals.is_empty() {
663 let mut acc = vec![[0.0f32; 3]; positions.len()];
664
665 let cross = |a: [f32; 3], b: [f32; 3]| -> [f32; 3] {
666 [
667 a[1] * b[2] - a[2] * b[1],
668 a[2] * b[0] - a[0] * b[2],
669 a[0] * b[1] - a[1] * b[0],
670 ]
671 };
672
673 let sub = |a: [f32; 3], b: [f32; 3]| -> [f32; 3] {
674 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
675 };
676
677 for tri in indices_u32.chunks_exact(3) {
678 let i0 = tri[0] as usize;
679 let i1 = tri[1] as usize;
680 let i2 = tri[2] as usize;
681 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
682 continue;
683 }
684
685 let p0 = positions[i0];
686 let p1 = positions[i1];
687 let p2 = positions[i2];
688
689 let e1 = sub(p1, p0);
690 let e2 = sub(p2, p0);
691 let n = cross(e1, e2);
692
693 for &idx in &[i0, i1, i2] {
694 acc[idx][0] += n[0];
695 acc[idx][1] += n[1];
696 acc[idx][2] += n[2];
697 }
698 }
699
700 normals = acc
701 .into_iter()
702 .map(|n| {
703 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
704 if len > 1e-8 {
705 [n[0] / len, n[1] / len, n[2] / len]
706 } else {
707 [0.0, 0.0, 1.0]
708 }
709 })
710 .collect();
711 }
712
713 let mut vertices: Vec<CpuVertex> = Vec::with_capacity(positions.len());
714 for (i, p) in positions.iter().copied().enumerate() {
715 let uv = uvs.get(i).copied().unwrap_or([0.0, 0.0]);
716 let normal = normals.get(i).copied().unwrap_or([0.0, 0.0, 1.0]);
717 vertices.push(CpuVertex { pos: p, uv, normal });
718 }
719
720 let (joints0, weights0) = match (joints0, weights0) {
721 (Some(j), Some(w))
722 if j.len() == positions.len() && w.len() == positions.len() =>
723 {
724 (Some(j), Some(w))
725 }
726 _ => (None, None),
727 };
728
729 let key = format!("{}:{}:prim{}", gltf_name, mesh_name_or_index, prim_index);
730 meshes.push(ImportedMesh {
731 key,
732 mesh: Some({
733 let mesh = CpuMesh::new(vertices, indices_u32);
734 if let (Some(j), Some(w)) = (joints0, weights0) {
735 mesh.with_skinning(j, w)
736 } else {
737 mesh
738 }
739 }),
740 });
741 }
742 }
743
744 let mut texture_keys: Vec<String> = Vec::new();
746 let mut textures: Vec<ImportedTexture> = Vec::new();
747 for (i, img) in images.into_iter().enumerate() {
748 let name_or_index = doc
749 .images()
750 .nth(i)
751 .and_then(|im| im.name())
752 .map(Self::sanitize_key_part)
753 .filter(|s| !s.is_empty())
754 .unwrap_or_else(|| format!("{}", i));
755
756 let key = format!("{}:{}", gltf_name, name_or_index);
757 texture_keys.push(key);
758
759 let (rgba, width, height) = match img.format {
760 gltf::image::Format::R8G8B8A8 => (img.pixels, img.width, img.height),
761 gltf::image::Format::R8G8B8 => {
762 let mut out = Vec::with_capacity((img.width * img.height * 4) as usize);
763 for chunk in img.pixels.chunks_exact(3) {
764 out.extend_from_slice(chunk);
765 out.push(255);
766 }
767 (out, img.width, img.height)
768 }
769 gltf::image::Format::R8 => {
770 let mut out = Vec::with_capacity((img.width * img.height * 4) as usize);
771 for &v in &img.pixels {
772 out.extend_from_slice(&[v, v, v, 255]);
773 }
774 (out, img.width, img.height)
775 }
776 gltf::image::Format::R8G8 => {
777 let mut out = Vec::with_capacity((img.width * img.height * 4) as usize);
778 for chunk in img.pixels.chunks_exact(2) {
779 out.push(chunk[0]);
780 out.push(chunk[0]);
781 out.push(chunk[0]);
782 out.push(chunk[1]);
783 }
784 (out, img.width, img.height)
785 }
786 other => {
787 return Err(format!("unsupported glTF image format: {:?}", other));
788 }
789 };
790
791 textures.push(ImportedTexture {
792 rgba: Some(rgba),
793 width,
794 height,
795 });
796 }
797
798 fn mat4_identity() -> TransformMatrix {
799 [
800 [1.0, 0.0, 0.0, 0.0],
801 [0.0, 1.0, 0.0, 0.0],
802 [0.0, 0.0, 1.0, 0.0],
803 [0.0, 0.0, 0.0, 1.0],
804 ]
805 }
806
807 fn read_accessor_matrices4x4_f32(
808 acc: gltf::Accessor,
809 buffers: &[gltf::buffer::Data],
810 ) -> Vec<TransformMatrix> {
811 use gltf::accessor::{DataType, Dimensions};
812
813 if acc.data_type() != DataType::F32 {
814 return Vec::new();
815 }
816 if acc.dimensions() != Dimensions::Mat4 {
817 return Vec::new();
818 }
819
820 let Some(view) = acc.view() else {
821 return Vec::new();
822 };
823
824 let buffer_index = view.buffer().index();
825 let Some(buf) = buffers.get(buffer_index) else {
826 return Vec::new();
827 };
828
829 let stride_bytes: usize = view.stride().unwrap_or(16 * 4);
830 let start = view.offset() + acc.offset();
831 let count = acc.count();
832
833 let bytes = &buf.0;
834 let mut out: Vec<TransformMatrix> = Vec::with_capacity(count);
835 for i in 0..count {
836 let base = start + i * stride_bytes;
837 if base + 16 * 4 > bytes.len() {
838 break;
839 }
840
841 let mut m = [[0.0f32; 4]; 4];
843 for col in 0..4 {
844 for row in 0..4 {
845 let j = col * 4 + row;
846 let bi = base + j * 4;
847 let Some(chunk) = bytes.get(bi..bi + 4) else {
848 return out;
849 };
850 m[col][row] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
851 }
852 }
853
854 out.push(m);
855 }
856
857 out
858 }
859
860 let mut skins: Vec<ImportedSkin> = Vec::new();
862 for skin in doc.skins() {
863 let joints: Vec<usize> = skin.joints().map(|n| n.index()).collect();
864
865 let mut inverse_bind_matrices: Vec<TransformMatrix> = Vec::new();
866 if let Some(acc) = skin.inverse_bind_matrices() {
867 inverse_bind_matrices = read_accessor_matrices4x4_f32(acc, &buffers);
868 }
869
870 if inverse_bind_matrices.len() != joints.len() {
871 inverse_bind_matrices = vec![mat4_identity(); joints.len()];
872 }
873
874 skins.push(ImportedSkin {
875 joints,
876 inverse_bind_matrices,
877 skeleton_root: skin.skeleton().map(|n| n.index()),
878 });
879 }
880
881 Ok(LoadedGltf {
882 gltf_name,
883 meshes,
884 texture_keys,
885 textures,
886 skins,
887 meshes_registered: false,
888 textures_uploaded: false,
889 })
890 }
891
892 fn spawn_node_recursive(
893 &self,
894 world: &mut World,
895 parent_transform: ComponentId,
896 buffers: &[gltf::buffer::Data],
897 loaded: &LoadedGltf,
898 node: gltf::Node,
899 node_index_to_component: &mut HashMap<usize, ComponentId>,
900 pending_skin_components: &mut Vec<(ComponentId, usize)>,
901 serialize_spawned_nodes: bool,
902 ) -> Option<ComponentId> {
903 let node_display_name = node
904 .name()
905 .map(Self::sanitize_key_part)
906 .filter(|s| !s.is_empty())
907 .unwrap_or_else(|| format!("node{}", node.index()));
908
909 let (t, r, s) = node.transform().decomposed();
910 let mut tc = TransformComponent::new();
911 tc.transform.translation = t;
912 tc.transform.rotation = r;
913 tc.transform.scale = s;
914 tc.transform.recompute_model();
915
916 let this_transform =
917 world.add_component_boxed_named(node_display_name.clone(), Box::new(tc));
918 let _ = world.add_child(parent_transform, this_transform);
919 if !serialize_spawned_nodes {
920 let serialize_off = world.add_component(SerializeComponent::off());
921 let _ = world.add_child(this_transform, serialize_off);
922 }
923
924 let rest_pose = crate::engine::ecs::component::BoneRestPoseComponent::new(t, r, s);
929 let rest_id = world.add_component(rest_pose);
930 let _ = world.add_child(this_transform, rest_id);
931
932 node_index_to_component.insert(node.index(), this_transform);
933
934 let node_skin_index = node.skin().map(|s| s.index());
937
938 if let Some(mesh) = node.mesh() {
939 for (prim_index, prim) in mesh.primitives().enumerate() {
940 if prim.mode() != gltf::mesh::Mode::Triangles {
941 continue;
942 }
943
944 let mesh_name_or_index = mesh
945 .name()
946 .map(Self::sanitize_key_part)
947 .filter(|s| !s.is_empty())
948 .unwrap_or_else(|| format!("mesh{}", mesh.index()));
949 let mesh_key = format!(
950 "{}:{}:prim{}",
951 loaded.gltf_name, mesh_name_or_index, prim_index
952 );
953
954 let material_handle = if node_skin_index.is_some() {
957 MaterialHandle::SKINNED_TOON_MESH
958 } else {
959 MaterialHandle::TOON_MESH
960 };
961 let renderable = world.add_component(RenderableComponent::new(Renderable::new(
962 CpuMeshHandle(0),
963 material_handle,
964 )));
965 let mesh_ref = world.add_component(MeshComponent::new(mesh_key));
966
967 let _ = world.add_child(this_transform, renderable);
968 let _ = world.add_child(renderable, mesh_ref);
969
970 if let Some(skin_index) = node_skin_index {
971 if loaded.skins.get(skin_index).is_some() {
972 let skin_comp = world.add_component(SkinnedMeshComponent::new(skin_index));
973 let _ = world.add_child(renderable, skin_comp);
974 pending_skin_components.push((skin_comp, skin_index));
975 } else {
976 println!(
977 "[GLTFSystem] warning: node refers to missing skin index={} (uri='{}')",
978 skin_index, loaded.gltf_name
979 );
980 }
981 }
982
983 let material = prim.material();
984
985 let base_color_tex = material
987 .pbr_metallic_roughness()
988 .base_color_texture()
989 .map(|t| t.texture().source().index());
990
991 if let Some(image_index) = base_color_tex {
992 let image_name_or_index = loaded
993 .texture_keys
994 .get(image_index)
995 .cloned()
996 .unwrap_or_else(|| format!("{}:{}", loaded.gltf_name, image_index));
997
998 let tex_comp = world.add_component(TextureComponent::new(image_name_or_index));
999 let _ = world.add_child(renderable, tex_comp);
1000 }
1001
1002 let base_color_factor = material.pbr_metallic_roughness().base_color_factor();
1006 let base_rgb = [
1007 base_color_factor[0],
1008 base_color_factor[1],
1009 base_color_factor[2],
1010 ];
1011 let emissive_factor = material.emissive_factor();
1012 let emissive_rgb = [emissive_factor[0], emissive_factor[1], emissive_factor[2]];
1013
1014 let has_base_tex = base_color_tex.is_some();
1015 let mut wants_emissive = false;
1016
1017 let color_rgba = if !has_base_tex
1018 && Self::is_black_rgb(base_rgb)
1019 && !Self::is_black_rgb(emissive_rgb)
1020 {
1021 wants_emissive = true;
1022 [
1023 emissive_rgb[0],
1024 emissive_rgb[1],
1025 emissive_rgb[2],
1026 base_color_factor[3],
1027 ]
1028 } else {
1029 base_color_factor
1030 };
1031
1032 let should_attach_color = if has_base_tex {
1035 (color_rgba[0] - 1.0).abs() > 1e-4
1036 || (color_rgba[1] - 1.0).abs() > 1e-4
1037 || (color_rgba[2] - 1.0).abs() > 1e-4
1038 || (color_rgba[3] - 1.0).abs() > 1e-4
1039 } else {
1040 true
1041 };
1042
1043 if should_attach_color {
1044 let color_comp = world.add_component(ColorComponent { rgba: color_rgba });
1045 let _ = world.add_child(renderable, color_comp);
1046 }
1047
1048 if wants_emissive {
1049 let emissive_comp = world.add_component(EmissiveComponent::on());
1050 let _ = world.add_child(renderable, emissive_comp);
1051 }
1052
1053 let _ = buffers;
1055 }
1056 }
1057
1058 for ch in node.children() {
1060 let _ = self.spawn_node_recursive(
1061 world,
1062 this_transform,
1063 buffers,
1064 loaded,
1065 ch,
1066 node_index_to_component,
1067 pending_skin_components,
1068 serialize_spawned_nodes,
1069 );
1070 }
1071
1072 Some(this_transform)
1073 }
1074}
1075
1076impl LoadedGltf {
1077 fn approximate_heap_bytes(&self) -> usize {
1078 let mesh_bytes: usize = self
1079 .meshes
1080 .iter()
1081 .map(|mesh| {
1082 mesh.key.capacity()
1083 + mesh
1084 .mesh
1085 .as_ref()
1086 .map(CpuMesh::approximate_heap_bytes)
1087 .unwrap_or(0)
1088 })
1089 .sum();
1090 let texture_key_bytes: usize = self.texture_keys.iter().map(String::capacity).sum();
1091 let texture_bytes: usize = self
1092 .textures
1093 .iter()
1094 .map(|texture| texture.rgba.as_ref().map(Vec::capacity).unwrap_or(0))
1095 .sum();
1096 let skin_bytes: usize = self
1097 .skins
1098 .iter()
1099 .map(|skin| {
1100 skin.joints.capacity() * std::mem::size_of::<usize>()
1101 + skin.inverse_bind_matrices.capacity() * std::mem::size_of::<TransformMatrix>()
1102 })
1103 .sum();
1104 self.gltf_name.capacity() + mesh_bytes + texture_key_bytes + texture_bytes + skin_bytes
1105 }
1106}
1107
1108impl crate::engine::ecs::system::System for GLTFSystem {
1110 fn tick(
1111 &mut self,
1112 _world: &mut World,
1113 _visuals: &mut crate::engine::graphics::VisualWorld,
1114 _input: &InputState,
1115 _dt_sec: f32,
1116 ) {
1117 }
1118}