use crate::base::{Error, Result};
use crate::gfx::bitmap::Bitmap;
use crate::three::animation::NodePose;
use crate::three::doc::Doc;
use crate::three::extract::{extract_primitive, MeshData};
use crate::three::glb;
use crate::three::math::{Mat4, Vec3};
use crate::three::scene::Camera;
#[path = "load_rig.rs"]
mod rig;
use rig::sanitize_skin_vertices;
pub use rig::{Pose, Rig, RigNode, SkinData};
#[path = "load_texture.rs"]
mod texture_decode;
use texture_decode::{decode_texture, TextureOutcome};
#[derive(Debug, Clone)]
pub struct MeshInstance {
pub data: MeshData,
pub world: Mat4,
pub source_node: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct MaterialData {
pub base_color: [f32; 4],
pub texture: Option<Bitmap>,
pub mips: Vec<Bitmap>,
pub emissive: [f32; 3],
}
impl Default for MaterialData {
fn default() -> Self {
MaterialData {
base_color: [1.0; 4],
texture: None,
mips: Vec::new(),
emissive: [0.0; 3],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LoadStats {
pub total: std::time::Duration,
pub texture_decode: std::time::Duration,
pub mip_build: std::time::Duration,
pub textures_decoded: usize,
pub triangles: usize,
}
#[derive(Debug, Default)]
pub struct Model {
pub instances: Vec<MeshInstance>,
pub materials: Vec<MaterialData>,
pub rig: Option<Rig>,
pub warnings: Vec<String>,
}
const MAX_NODE_DEPTH: usize = 256;
pub const MAX_TRIANGLES: usize = 2_000_000;
impl Model {
pub fn load(bytes: &[u8]) -> Result<Model> {
Model::load_with_stats(bytes).map(|(m, _)| m)
}
pub fn load_with_stats(bytes: &[u8]) -> Result<(Model, LoadStats)> {
let t_start = std::time::Instant::now();
let mut stats = LoadStats::default();
let chunks = glb::split(bytes)?;
let doc = Doc::parse(chunks.json)?;
let bin = chunks.bin;
let mut warnings = Vec::new();
let mut materials = Vec::with_capacity(doc.materials.len());
for (mi, m) in doc.materials.iter().enumerate() {
let mut out = MaterialData {
base_color: m.base_color,
texture: None,
mips: Vec::new(),
emissive: m.emissive,
};
if m.has_normal_texture {
warnings.push(format!(
"#FALLBACK material {mi}: normal map ignored (no tangent pipeline)"
));
}
if let Some(tex_idx) = m.base_color_texture {
let t0 = std::time::Instant::now();
match decode_texture(&doc, tex_idx, bin)? {
TextureOutcome::Decoded(bmp) => {
stats.texture_decode += t0.elapsed();
stats.textures_decoded += 1;
let t1 = std::time::Instant::now();
out.mips = bmp.mip_chain();
stats.mip_build += t1.elapsed();
out.texture = Some(bmp);
}
TextureOutcome::Skipped(w) => {
warnings.push(format!("#FALLBACK material {mi}: {w}"))
}
}
}
materials.push(out);
}
let mut placements: Vec<(usize, Mat4, Option<usize>, Option<usize>)> = Vec::new();
if !doc.scene_roots.is_empty() {
let mut visited = vec![false; doc.nodes.len()];
let mut stack: Vec<(usize, Mat4, usize)> = doc
.scene_roots
.iter()
.rev()
.map(|&r| (r, Mat4::IDENTITY, 0))
.collect();
while let Some((ni, parent, depth)) = stack.pop() {
if depth > MAX_NODE_DEPTH {
return Err(Error::Parse(format!(
"gltf: node hierarchy deeper than {MAX_NODE_DEPTH} (cycle?)"
)));
}
let node = doc
.nodes
.get(ni)
.ok_or_else(|| Error::Parse(format!("gltf: node index {ni} out of range")))?;
if visited[ni] {
return Err(Error::Parse(format!(
"gltf: node {ni} reachable twice (cycle or shared child)"
)));
}
visited[ni] = true;
let local = match node.matrix {
Some(m) => Mat4::from_cols_array(m),
None => Mat4::from_trs(
Vec3::new(
node.translation[0],
node.translation[1],
node.translation[2],
),
(
node.rotation[0],
node.rotation[1],
node.rotation[2],
node.rotation[3],
),
Vec3::new(node.scale[0], node.scale[1], node.scale[2]),
),
};
let world = parent.mul(&local);
if let Some(mesh) = node.mesh {
placements.push((mesh, world, Some(ni), node.skin));
}
for &child in node.children.iter().rev() {
stack.push((child, world, depth + 1));
}
}
} else if !doc.meshes.is_empty() {
warnings.push("#FALLBACK no scene/nodes; placing all meshes at identity".to_string());
for mi in 0..doc.meshes.len() {
placements.push((mi, Mat4::IDENTITY, None, None));
}
}
let mut instances = Vec::new();
let mut instance_skins: Vec<Option<usize>> = Vec::new();
let mut triangles = 0usize;
for (mesh_idx, world, source_node, node_skin) in placements {
let mesh = doc
.meshes
.get(mesh_idx)
.ok_or_else(|| Error::Parse(format!("gltf: mesh index {mesh_idx} out of range")))?;
for prim in &mesh.primitives {
let declared = prim
.indices
.or(prim.position)
.and_then(|a| doc.accessors.get(a))
.map(|a| a.count / 3)
.unwrap_or(0);
triangles = triangles.saturating_add(declared);
if triangles > MAX_TRIANGLES {
return Err(Error::Parse(format!(
"gltf: triangle count exceeds the {MAX_TRIANGLES} budget \
(pathological input for a terminal renderer)"
)));
}
let mut data = extract_primitive(&doc, prim, bin)?;
if let Some(mat) = data.material {
if mat >= materials.len() {
return Err(Error::Parse(format!(
"gltf: material index {mat} out of range ({})",
materials.len()
)));
}
}
let skin = match (node_skin, &data.joints) {
(Some(s), Some(_)) => {
let joint_count = doc.skins[s].joints.len(); sanitize_skin_vertices(&mut data, joint_count, &mut warnings)?;
Some(s)
}
_ => None,
};
instance_skins.push(skin);
instances.push(MeshInstance {
data,
world,
source_node,
});
}
}
if instances.is_empty() {
return Err(Error::Parse("gltf: no drawable triangle primitives".into()));
}
stats.triangles = triangles;
let rig = if doc.animations.is_empty() && doc.skins.is_empty() {
None
} else {
let (animations, anim_warnings) = crate::three::animation::build_animations(&doc, bin)?;
warnings.extend(anim_warnings);
let mut skins = Vec::with_capacity(doc.skins.len());
for (si, s) in doc.skins.iter().enumerate() {
let inverse_bind = match s.inverse_bind_matrices {
None => vec![Mat4::IDENTITY; s.joints.len()],
Some(acc) => {
let what = format!("skin {si} inverseBindMatrices");
let mats = crate::three::extract::read_mat4_f32(&doc, acc, bin, &what)?;
if mats.len() < s.joints.len() {
return Err(Error::Parse(format!(
"gltf: skin {si} has {} joints but {} inverse bind matrices",
s.joints.len(),
mats.len()
)));
}
mats.into_iter()
.take(s.joints.len())
.map(Mat4::from_cols_array)
.collect()
}
};
skins.push(SkinData {
joints: s.joints.clone(),
inverse_bind,
});
}
let nodes = doc
.nodes
.iter()
.map(|n| RigNode {
rest: NodePose {
translation: Vec3::new(
n.translation[0],
n.translation[1],
n.translation[2],
),
rotation: n.rotation,
scale: Vec3::new(n.scale[0], n.scale[1], n.scale[2]),
},
matrix: n.matrix.map(Mat4::from_cols_array),
children: n.children.clone(),
})
.collect();
Some(Rig {
nodes,
roots: doc.scene_roots.clone(),
animations,
skins,
instance_skins,
})
};
stats.total = t_start.elapsed();
Ok((
Model {
instances,
materials,
rig,
warnings,
},
stats,
))
}
pub fn center(&self) -> Option<Vec3> {
self.bounds().map(|(min, max)| (min + max) * 0.5)
}
pub fn fit_camera(&self, yaw: f32, pitch: f32) -> Camera {
match self.bounds() {
Some((min, max)) => Camera::framing(min, max, yaw, pitch),
None => Camera::orbit(Vec3::ZERO, 1.0, yaw, pitch),
}
}
pub fn ensure_smooth_normals(&mut self) {
for inst in &mut self.instances {
inst.data.compute_smooth_normals();
}
}
pub fn triangle_count(&self) -> usize {
self.instances.iter().map(|i| i.data.triangle_count()).sum()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
let mut min = Vec3::splat(f32::INFINITY);
let mut max = Vec3::splat(f32::NEG_INFINITY);
let mut any = false;
for inst in &self.instances {
for p in &inst.data.positions {
let w = inst.world.transform_point(Vec3::new(p[0], p[1], p[2]));
if !(w.x.is_finite() && w.y.is_finite() && w.z.is_finite()) {
continue;
}
any = true;
min = Vec3::new(min.x.min(w.x), min.y.min(w.y), min.z.min(w.z));
max = Vec3::new(max.x.max(w.x), max.y.max(w.y), max.z.max(w.z));
}
}
any.then_some((min, max))
}
}
pub fn load_glb(path: impl AsRef<std::path::Path>) -> Result<Model> {
load_glb_with_stats(path).map(|(m, _)| m)
}
pub fn load_glb_with_stats(path: impl AsRef<std::path::Path>) -> Result<(Model, LoadStats)> {
let path = path.as_ref();
let bytes = std::fs::read(path)
.map_err(|e| Error::Parse(format!("glb: cannot read {}: {e}", path.display())))?;
Model::load_with_stats(&bytes)
}
#[cfg(test)]
#[path = "load_tests.rs"]
mod tests;