use std::collections::HashMap;
use std::sync::Arc;
use base64::Engine;
use glam::{Mat4, Quat, Vec2, Vec3, Vec4};
use gltf::animation::util::ReadOutputs;
use crate::animation::{AnimationClip, Channel, Interpolation, Keyframes};
use crate::geometry::{MorphTarget, SkinWeights};
use crate::math::Aabb;
use crate::scene::Skin;
use crate::{Geometry, Light, Material, Node, NodeId, Rgb, Scene, Shading, Side, Texture};
#[derive(Debug)]
pub enum ModelError {
Gltf(gltf::Error),
MissingFile(String),
Image(image::ImageError),
}
impl std::fmt::Display for ModelError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModelError::Gltf(e) => write!(f, "invalid glTF: {e}"),
ModelError::MissingFile(uri) => {
write!(f, "glTF refers to a file that was not provided: {uri}")
}
ModelError::Image(e) => write!(f, "cannot decode a glTF texture: {e}"),
}
}
}
impl std::error::Error for ModelError {}
impl From<gltf::Error> for ModelError {
fn from(e: gltf::Error) -> Self {
ModelError::Gltf(e)
}
}
impl From<image::ImageError> for ModelError {
fn from(e: image::ImageError) -> Self {
ModelError::Image(e)
}
}
#[derive(Debug, Clone)]
pub struct Model {
pub scene: Scene,
pub animations: Vec<AnimationClip>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelInstance {
pub root: NodeId,
pub offset: u32,
pub len: u32,
}
impl ModelInstance {
pub fn node(&self, model_node: NodeId) -> NodeId {
NodeId(model_node.0 + self.offset)
}
pub fn find(&self, scene: &Scene, name: &str) -> Option<NodeId> {
(self.offset..self.offset + self.len)
.map(NodeId)
.find(|id| scene.get(*id).and_then(|n| n.name.as_deref()) == Some(name))
}
pub fn play(&self, scene: &mut Scene, clip: &AnimationClip, time: f32) {
self.blend(scene, clip, time, 1.0);
}
pub fn blend(&self, scene: &mut Scene, clip: &AnimationClip, time: f32, weight: f32) {
clip.apply_looped(scene, self.offset, time, weight);
}
}
impl Model {
pub fn from_gltf(bytes: &[u8]) -> Result<Self, ModelError> {
Self::from_gltf_with(bytes, |_| None)
}
pub fn from_gltf_with(
bytes: &[u8],
resolve: impl Fn(&str) -> Option<Vec<u8>>,
) -> Result<Self, ModelError> {
let gltf = gltf::Gltf::from_slice(bytes)?;
let load_uri = |uri: &str| {
let missing = || ModelError::MissingFile(uri.to_owned());
match uri.strip_prefix("data:") {
Some(data) => {
let base64 = data.split_once(";base64,").map_or("", |(_, d)| d);
base64::engine::general_purpose::STANDARD
.decode(base64)
.map_err(|_| missing())
}
None => resolve(uri).ok_or_else(missing),
}
};
let buffers = gltf
.buffers()
.map(|buffer| match buffer.source() {
gltf::buffer::Source::Bin => Ok(gltf.blob.clone().unwrap_or_default()),
gltf::buffer::Source::Uri(uri) => load_uri(uri),
})
.collect::<Result<Vec<_>, _>>()?;
let decode = |bytes: &[u8]| -> Result<_, ModelError> {
Ok(Arc::new(image::load_from_memory(bytes)?.to_rgba8()))
};
let images = gltf
.images()
.map(|image| match image.source() {
gltf::image::Source::View { view, .. } => buffers[view.buffer().index()]
.get(view.offset()..view.offset() + view.length())
.map(decode)
.transpose(),
gltf::image::Source::Uri { uri, .. } => {
load_uri(uri).ok().map(|bytes| decode(&bytes)).transpose()
}
})
.collect::<Result<Vec<_>, _>>()?;
let mut loader = Loader {
buffers: &buffers,
images: &images,
textures: HashMap::new(),
scene: Scene::new(),
node_ids: HashMap::new(),
mesh_nodes: Vec::new(),
};
let roots: Vec<gltf::Node> =
if let Some(scene) = gltf.default_scene().or_else(|| gltf.scenes().next()) {
scene.nodes().collect()
} else {
let children: std::collections::HashSet<usize> = gltf
.nodes()
.flat_map(|n| n.children().map(|c| c.index()))
.collect();
gltf.nodes()
.filter(|n| !children.contains(&n.index()))
.collect()
};
for root in roots {
loader.add_node(&root, None);
}
loader.add_skins();
let animations = gltf.animations().map(|a| loader.animation(&a)).collect();
Ok(Model {
scene: loader.scene,
animations,
})
}
pub fn animation(&self, name: &str) -> Option<&AnimationClip> {
self.animations.iter().find(|a| a.name == name)
}
pub fn add_to(&self, scene: &mut Scene, parent: Option<NodeId>) -> ModelInstance {
let root = scene.insert(parent, Node::group());
let offset = scene.append(Some(root), &self.scene);
ModelInstance {
root,
offset,
len: self.scene.len() as u32,
}
}
pub fn bounds(&self) -> Aabb {
self.scene
.nodes()
.filter(|(_, node)| node.parent().is_none())
.fold(Aabb::EMPTY, |aabb, (id, _)| {
aabb.union(self.scene.bounds(id))
})
}
}
struct Loader<'a, 'g> {
buffers: &'a [Vec<u8>],
images: &'a [Option<Arc<image::RgbaImage>>],
textures: HashMap<(usize, bool), Arc<Texture>>,
scene: Scene,
node_ids: HashMap<usize, NodeId>,
mesh_nodes: Vec<(NodeId, gltf::Node<'g>)>,
}
impl<'g> Loader<'_, 'g> {
fn add_node(&mut self, gltf_node: &gltf::Node<'g>, parent: Option<NodeId>) {
if self.node_ids.contains_key(&gltf_node.index()) {
return;
}
let (t, r, s) = gltf_node.transform().decomposed();
let meshes: Vec<Node> = gltf_node.mesh().map_or_else(Vec::new, |mesh| {
let weights = gltf_node.weights().or(mesh.weights()).unwrap_or_default();
mesh.primitives()
.filter(|p| p.mode() == gltf::mesh::Mode::Triangles)
.map(|primitive| {
let mut node = self.mesh_node(&primitive);
if let Some(mesh) = node.mesh_mut() {
mesh.morph_weights = weights.to_vec();
}
node
})
.collect()
});
let (base, meshes) = match <[Node; 1]>::try_from(meshes) {
Ok([mesh]) => (mesh, Vec::new()),
Err(meshes) => (Node::group(), meshes),
};
let node = Node {
name: gltf_node.name().map(Into::into),
position: Vec3::from(t),
rotation: Quat::from_array(r).normalize(),
scale: Vec3::from(s),
..base
};
let id = self.scene.insert(parent, node);
self.node_ids.insert(gltf_node.index(), id);
if let Some(light) = gltf_node.light() {
self.scene.add_to(id, Node::light(convert_light(&light)));
}
for mesh in meshes {
self.scene.add_to(id, mesh);
}
if gltf_node.mesh().is_some() {
self.mesh_nodes.push((id, gltf_node.clone()));
}
for child in gltf_node.children() {
self.add_node(&child, Some(id));
}
}
fn add_skins(&mut self) {
let buffers = self.buffers;
for (id, gltf_node) in std::mem::take(&mut self.mesh_nodes) {
let Some(skin) = gltf_node.skin() else {
continue;
};
let reader = skin.reader(|b| buffers.get(b.index()).map(Vec::as_slice));
let inverse_bind_matrices = reader
.read_inverse_bind_matrices()
.map(|m| m.map(|m| Mat4::from_cols_array_2d(&m)).collect())
.unwrap_or_default();
let joints = skin
.joints()
.map(|j| self.node_ids.get(&j.index()).copied().unwrap_or(NodeId(0)))
.collect();
let skin = Skin {
joints,
inverse_bind_matrices: Arc::new(inverse_bind_matrices),
};
for target in self.scene.descendants(id) {
if let Some(mesh) = self.scene[target].mesh_mut()
&& mesh.geometry.skin.is_some()
{
mesh.skin = Some(skin.clone());
}
}
}
}
fn animation(&self, animation: &gltf::Animation) -> AnimationClip {
let buffers = self.buffers;
let mut channels = Vec::new();
let mut duration: f32 = 0.0;
for channel in animation.channels() {
let Some(&node) = self.node_ids.get(&channel.target().node().index()) else {
continue;
};
let reader = channel.reader(|b| buffers.get(b.index()).map(Vec::as_slice));
let Some(times) = reader.read_inputs().map(Iterator::collect::<Vec<f32>>) else {
continue;
};
duration = duration.max(times.last().copied().unwrap_or(0.0));
let Some(outputs) = reader.read_outputs() else {
continue;
};
let interpolation = match channel.sampler().interpolation() {
gltf::animation::Interpolation::Step => Interpolation::Step,
gltf::animation::Interpolation::Linear => Interpolation::Linear,
gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline,
};
let keyframes = match outputs {
ReadOutputs::Translations(t) => Keyframes::Translation(t.map(Vec3::from).collect()),
ReadOutputs::Scales(s) => Keyframes::Scale(s.map(Vec3::from).collect()),
ReadOutputs::Rotations(r) => {
Keyframes::Rotation(r.into_f32().map(Quat::from_array).collect())
}
ReadOutputs::MorphTargetWeights(w) => {
let values: Vec<f32> = w.into_f32().collect();
let per_keyframe = values.len().checked_div(times.len()).unwrap_or(0);
let targets = match interpolation {
Interpolation::CubicSpline => per_keyframe / 3,
_ => per_keyframe,
};
Keyframes::Weights { targets, values }
}
};
channels.push(Channel {
node,
interpolation,
times,
keyframes,
});
}
AnimationClip {
name: animation.name().unwrap_or_default().to_owned(),
duration,
channels,
}
}
fn mesh_node(&mut self, primitive: &gltf::Primitive) -> Node {
let geometry = self.geometry(primitive);
let mut material = self.material(&primitive.material());
material.flat_shading |= primitive.get(&gltf::Semantic::Normals).is_none();
Node::mesh(geometry, material)
}
fn geometry(&self, primitive: &gltf::Primitive) -> Geometry {
let reader = primitive.reader(|b| self.buffers.get(b.index()).map(Vec::as_slice));
let positions = vec3s(reader.read_positions());
let indices: Vec<u32> = reader.read_indices().map_or_else(
|| (0..positions.len() as u32).collect(),
|i| i.into_u32().collect(),
);
let normals = vec3s(reader.read_normals());
let uvs: Vec<Vec2> = reader
.read_tex_coords(0)
.map(|uv| uv.into_f32().map(Vec2::from).collect())
.unwrap_or_default();
let mut geometry = Geometry::new(positions, normals, uvs, indices);
if let Some(colors) = reader.read_colors(0) {
geometry.colors = colors.into_rgba_f32().map(Vec4::from).collect();
}
if let (Some(joints), Some(weights)) = (reader.read_joints(0), reader.read_weights(0)) {
let normalize = |w: [f32; 4]| {
let sum: f32 = w.iter().sum();
if sum > 0.0 {
w.map(|x| x / sum)
} else {
[1.0, 0.0, 0.0, 0.0]
}
};
geometry.skin = Some(SkinWeights {
joints: joints.into_u16().collect(),
weights: weights.into_f32().map(normalize).collect(),
});
}
geometry
.morph_targets
.extend(
reader
.read_morph_targets()
.map(|(positions, normals, _)| MorphTarget {
name: None,
positions: vec3s(positions),
normals: vec3s(normals),
}),
);
geometry
}
fn texture(&mut self, texture: &gltf::Texture, srgb: bool) -> Option<Arc<Texture>> {
let index = texture.source().index();
if let Some(texture) = self.textures.get(&(index, srgb)) {
return Some(Arc::clone(texture));
}
let image = self.images.get(index)?.as_ref()?;
let texture = Arc::new(Texture::from_rgba8(
image.width(),
image.height(),
image.as_raw(),
srgb,
));
self.textures.insert((index, srgb), Arc::clone(&texture));
Some(texture)
}
fn material(&mut self, material: &gltf::Material) -> Material {
use gltf::material::AlphaMode;
let pbr = material.pbr_metallic_roughness();
let [r, g, b, a] = pbr.base_color_factor();
let [er, eg, eb] = material.emissive_factor();
let alpha_mode = material.alpha_mode();
let normal = material.normal_texture();
Material {
shading: if material.unlit() {
Shading::Unlit
} else {
Shading::Standard
},
color: Rgb::linear(r, g, b),
opacity: if alpha_mode == AlphaMode::Opaque {
1.0
} else {
a
},
metalness: pbr.metallic_factor(),
roughness: pbr.roughness_factor(),
emissive: Rgb::linear(er, eg, eb),
emissive_intensity: material.emissive_strength().unwrap_or(1.0),
side: if material.double_sided() {
Side::Double
} else {
Side::Front
},
transparent: alpha_mode != AlphaMode::Opaque,
depth_write: alpha_mode == AlphaMode::Mask,
map: pbr
.base_color_texture()
.and_then(|info| self.texture(&info.texture(), true)),
metal_rough_map: pbr
.metallic_roughness_texture()
.and_then(|info| self.texture(&info.texture(), false)),
emissive_map: material
.emissive_texture()
.and_then(|info| self.texture(&info.texture(), true)),
normal_scale: normal
.as_ref()
.map_or(1.0, gltf::material::NormalTexture::scale),
normal_map: normal.and_then(|info| self.texture(&info.texture(), false)),
ao_map: material
.occlusion_texture()
.and_then(|info| self.texture(&info.texture(), false)),
..Default::default()
}
}
}
fn vec3s(values: Option<impl Iterator<Item = [f32; 3]>>) -> Vec<Vec3> {
values
.map(|v| v.map(Vec3::from).collect())
.unwrap_or_default()
}
fn convert_light(light: &gltf::khr_lights_punctual::Light) -> Light {
let color = Rgb::from(light.color());
let intensity = light.intensity();
let light_range = light.range();
match light.kind() {
gltf::khr_lights_punctual::Kind::Directional => {
Light::directional(color, intensity, Vec3::Z)
}
gltf::khr_lights_punctual::Kind::Point => {
Light::point(color, intensity, light.range().unwrap_or(0.0))
}
gltf::khr_lights_punctual::Kind::Spot {
outer_cone_angle,
inner_cone_angle,
} => {
let penumbra = if outer_cone_angle > 0.0 {
1.0 - inner_cone_angle / outer_cone_angle
} else {
0.0
};
let mut spot = Light::spot(color, intensity, outer_cone_angle, penumbra);
if let Light::Spot(light) = &mut spot {
light.distance = light_range.unwrap_or(0.0);
}
spot.pointing(Vec3::NEG_Z)
}
}
}