use std::path::Path;
use crate::components::{
GpuMaterial, GpuMesh, MorphBinding, MorphWeights, SkinBinding, SkinPose, Transform,
ZSortedDrawing,
};
use crate::mesh::MorphDeltas;
use crate::mesh_source::MeshSource;
use crate::unlit::{
InstanceColor, InstanceCutoff, MeshSourceUnlitExt, UnlitMeshDesc, UnlitPipeline,
UnlitPipelineKey,
};
use gltf::animation::util::ReadOutputs;
use gltf::animation::{Interpolation, Property};
use unlit_ecs::bundle;
use unlit_ecs::{DynamicBundle, Scope, ScopeMut, prelude::Entity};
use unlit_wgpu::pipeline::{BaseColorChannels, UnlitOptions};
use unlit_wgpu::resources::{ResHandle, TextureExt, TextureView};
const DEFAULT_ALPHA_CUTOFF: f32 = 0.5;
pub struct UnlitGltf {
document: gltf::Document,
buffers: Vec<gltf::buffer::Data>,
images: Vec<gltf::image::Data>,
node_transforms: Vec<Transform>,
node_matrices: Vec<glam::Mat4>,
parents: Vec<Option<usize>>,
locals: Vec<Transform>,
}
#[derive(Clone, Debug)]
pub struct GltfImage {
pub image: usize,
pub texture: ResHandle<wgpu::Texture>,
pub view: ResHandle<TextureView>,
pub filtering: bool,
}
#[derive(Clone, Debug)]
pub struct GltfMaterial {
pub material: usize,
pub bind_group: GpuMaterial,
pub view: ResHandle<TextureView>,
pub sampler: ResHandle<wgpu::Sampler>,
}
#[derive(Clone, Debug)]
pub struct GltfMesh {
pub mesh_index: usize,
pub primitive_index: usize,
pub key: UnlitPipelineKey,
pub mesh: GpuMesh,
}
#[derive(Clone, Debug)]
pub struct GltfNode {
pub node: usize,
pub entities: Vec<Entity>,
pub pose: Option<Entity>,
pub weights: Option<Entity>,
}
#[derive(Clone, Debug, Default)]
pub struct GltfResources {
pub images: Vec<GltfImage>,
pub materials: Vec<Option<GltfMaterial>>,
pub meshes: Vec<GltfMesh>,
}
impl UnlitGltf {
pub fn load(path: impl AsRef<Path>) -> Result<Self, gltf::Error> {
let (document, buffers, images) = gltf::import(path)?;
Ok(Self::from_parts(document, buffers, images))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, gltf::Error> {
let (document, buffers, images) = gltf::import_slice(bytes)?;
Ok(Self::from_parts(document, buffers, images))
}
fn from_parts(
document: gltf::Document,
buffers: Vec<gltf::buffer::Data>,
images: Vec<gltf::image::Data>,
) -> Self {
let parents = node_parents(&document);
let locals = node_locals(&document);
let node_matrices = node_world_matrices(&document, &parents);
let node_transforms = node_matrices.iter().map(|&m| decompose(m)).collect();
Self {
document,
buffers,
images,
node_transforms,
node_matrices,
parents,
locals,
}
}
pub fn pipeline_key(
&self,
device: &wgpu::Device,
mesh: usize,
primitive: usize,
) -> UnlitPipelineKey {
let primitive = self.primitive(mesh, primitive);
let material = primitive.material();
let mut options = UnlitOptions::standard(device);
let texture = material.pbr_metallic_roughness().base_color_texture();
if samples_base_color_texture(&primitive)
&& let Some(info) = &texture
{
let image = info.texture().source().index();
options.texture_filtering = self.image_filtering(image, device);
options.base_color = self.image_luminance(image);
}
if material.alpha_mode() == gltf::material::AlphaMode::Blend {
options.color_target.blend = Some(wgpu::BlendState::ALPHA_BLENDING);
}
if alpha_cutoff(&material).is_some() {
options.alpha_cutoff = true;
}
if material.double_sided() {
options.primitive.cull_mode = None;
}
UnlitPipelineKey::new(options)
}
pub fn insert_image(
&self,
source: &mut MeshSource,
world: &Scope<'_>,
image: usize,
) -> GltfImage {
let data = &self.images[image];
let device = source.device(world);
let format = image_format(data, &device);
let (width, height) = (data.width, data.height);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: format.format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let bytes_per_row = align_up(
width * format.texel_bytes(),
wgpu::COPY_BYTES_PER_ROW_ALIGNMENT,
);
let rows = encode_rows(data, format.format);
source.queue(world).write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&rows,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let (texture, view) = source.register_texture_and_default_view(world, texture);
GltfImage {
image,
texture,
view,
filtering: format.filtering,
}
}
pub fn insert_images(&self, source: &mut MeshSource, world: &Scope<'_>) -> Vec<GltfImage> {
(0..self.document.images().len())
.map(|image| self.insert_image(source, world, image))
.collect()
}
pub fn insert_material(
&self,
source: &mut MeshSource,
world: &Scope<'_>,
material: usize,
images: &[GltfImage],
) -> Option<GltfMaterial> {
let data = self.material(material);
let info = data.pbr_metallic_roughness().base_color_texture()?;
let image = images
.iter()
.find(|image| image.image == info.texture().source().index())
.expect("the base-color texture's image must be inserted before the material");
let key = textured_key(&source.device(world), image.filtering);
let descriptor = sampler_descriptor(&info.texture().sampler());
let descriptor = if image.filtering {
descriptor
} else {
wgpu::SamplerDescriptor {
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..descriptor
}
};
let sampler = source.register_sampler(world, Some(descriptor));
let view = {
let texture = {
let graph = source.context().graph(world);
graph
.get(&image.texture)
.expect("the image's texture is in the graph")
.clone()
};
let view = TextureExt::create_view(&texture, &wgpu::TextureViewDescriptor::default());
let mut graph = source.context().graph(world);
let view = graph.insert(view, None);
graph.add_dependency(&view, &image.texture);
view
};
let bind_group = source.allocate_unlit_material(world, &key, view.clone(), sampler.clone());
Some(GltfMaterial {
material,
bind_group,
view,
sampler,
})
}
pub fn insert_materials(
&self,
source: &mut MeshSource,
world: &Scope<'_>,
images: &[GltfImage],
) -> Vec<Option<GltfMaterial>> {
(0..self.document.materials().len())
.map(|material| self.insert_material(source, world, material, images))
.collect()
}
pub fn insert_mesh(
&self,
source: &mut MeshSource,
world: &Scope<'_>,
mesh: usize,
primitive: usize,
) -> GltfMesh {
let key = self.pipeline_key(&source.device(world), mesh, primitive);
let primitive = self.primitive(mesh, primitive);
let reader = primitive.reader(|buffer| Some(&self.buffers[buffer.index()]));
let positions: Vec<[f32; 3]> = reader
.read_positions()
.expect("a glTF primitive carries its POSITION attribute")
.collect();
let uvs: Option<Vec<[f32; 2]>> = samples_base_color_texture(&primitive).then(|| {
reader
.read_tex_coords(0)
.expect("the primitive samples a texture, so it has TEXCOORD_0")
.into_f32()
.collect()
});
let colors: Option<Vec<[u8; 4]>> = primitive
.get(&gltf::Semantic::Colors(0))
.is_some()
.then(|| {
reader
.read_colors(0)
.expect("the primitive has COLOR_0")
.into_rgba_u8()
.collect()
});
let indices: Option<Vec<u32>> = reader.read_indices().map(|i| i.into_u32().collect());
let skinned = primitive.get(&gltf::Semantic::Joints(0)).is_some()
&& primitive.get(&gltf::Semantic::Weights(0)).is_some();
let joints: Option<Vec<[u16; 4]>> = skinned.then(|| {
reader
.read_joints(0)
.expect("the primitive has JOINTS_0")
.into_u16()
.collect()
});
let weights: Option<Vec<[f32; 4]>> = skinned.then(|| {
reader
.read_weights(0)
.expect("the primitive has WEIGHTS_0")
.into_f32()
.collect()
});
let morph_deltas = (morph_target_count(&primitive) > 0).then(|| {
let target_count = morph_target_count(&primitive);
let mut deltas = vec![0.0; positions.len() * target_count * 3];
let mut target = 0;
for (displacements, _, _) in reader.read_morph_targets() {
let Some(displacements) = displacements else {
continue;
};
for (vertex, displacement) in displacements.enumerate() {
let start = (vertex * target_count + target) * 3;
deltas[start..start + 3].copy_from_slice(&displacement);
}
target += 1;
}
MorphDeltas {
deltas,
target_count: target_count as u32,
}
});
let gpu_mesh = source.allocate_unlit_mesh(
world,
&key,
UnlitMeshDesc {
positions: &positions,
uvs: uvs.as_deref(),
colors: colors.as_deref(),
indices: indices.as_deref(),
joints: joints.as_deref(),
weights: weights.as_deref(),
morph_deltas,
},
);
GltfMesh {
mesh_index: mesh,
primitive_index: primitive.index(),
key,
mesh: gpu_mesh,
}
}
pub fn insert_meshes(&self, source: &mut MeshSource, world: &Scope<'_>) -> Vec<GltfMesh> {
let count: usize = self
.document
.meshes()
.map(|mesh| mesh.primitives().len())
.sum();
let mut meshes = Vec::with_capacity(count);
for mesh in 0..self.document.meshes().len() {
for primitive in 0..self.mesh(mesh).primitives().count() {
meshes.push(self.insert_mesh(source, world, mesh, primitive));
}
}
meshes
}
pub fn insert_resources(&self, source: &mut MeshSource, world: &Scope<'_>) -> GltfResources {
let images = self.insert_images(source, world);
let materials = self.insert_materials(source, world, &images);
let meshes = self.insert_meshes(source, world);
GltfResources {
images,
materials,
meshes,
}
}
pub fn skin_pose(&self, node: usize) -> SkinPose {
self.skin_pose_with_matrices(node, &self.node_matrices)
}
fn skin_pose_with_matrices(&self, node: usize, matrices: &[glam::Mat4]) -> SkinPose {
let node_data = self.node(node);
let Some(skin) = node_data.skin() else {
return SkinPose::default();
};
let reader = skin.reader(|buffer| Some(&self.buffers[buffer.index()]));
let inverse_bind: Option<Vec<glam::Mat4>> = reader.read_inverse_bind_matrices().map(|m| {
m.map(|matrix| glam::Mat4::from_cols_array_2d(&matrix))
.collect()
});
let world_to_mesh = matrices[node].inverse();
let poses: Vec<glam::Mat4> = skin
.joints()
.enumerate()
.map(|(joint, joint_node)| {
let joint_world = matrices[joint_node.index()];
let bind = inverse_bind.as_ref().map_or(glam::Mat4::IDENTITY, |ibm| {
*ibm.get(joint).expect("one inverse bind matrix per joint")
});
world_to_mesh * joint_world * bind
})
.collect();
SkinPose { matrices: poses }
}
pub fn skin_joint_count(&self, node: usize) -> Option<usize> {
Some(self.node(node).skin()?.joints().count())
}
pub fn morph_weights(&self, node: usize) -> Vec<f32> {
let Some(mesh) = self.node(node).mesh() else {
return Vec::new();
};
let node_weights = self.node(node).weights();
let mut weights = node_weights
.or_else(|| mesh.weights())
.map(<[f32]>::to_vec)
.unwrap_or_default();
let count = mesh
.primitives()
.map(|primitive| morph_target_count(&primitive))
.max()
.unwrap_or(0);
weights.resize(count, 0.0);
weights
}
pub fn animation_count(&self) -> usize {
self.document.animations().count()
}
pub fn animation_name(&self, animation: usize) -> Option<&str> {
self.animation(animation).name()
}
pub fn animation_duration(&self, animation: usize) -> f32 {
let animation = self.animation(animation);
let mut duration: f32 = 0.0;
for channel in animation.channels() {
let reader = channel.reader(|buffer| Some(&self.buffers[buffer.index()]));
if let Some(inputs) = reader.read_inputs()
&& let Some(last) = inputs.last()
{
duration = duration.max(last);
}
}
duration
}
pub fn apply_animation(
&self,
world: &Scope<'_>,
animation: usize,
time: f32,
nodes: &[GltfNode],
) {
let animation = self.animation(animation);
let count = self.parents.len();
let mut locals = self.locals.clone();
let mut animated = vec![false; count];
let mut weights: Vec<Option<Vec<f32>>> = vec![None; count];
for channel in animation.channels() {
let node = channel.target().node().index();
let property = channel.target().property();
let interpolation = channel.sampler().interpolation();
let reader = channel.reader(|buffer| Some(&self.buffers[buffer.index()]));
let Some(inputs) = reader.read_inputs() else {
continue;
};
let times: Vec<f32> = inputs.collect();
if times.len() < 2 {
continue;
}
let Some(outputs) = reader.read_outputs() else {
continue;
};
let required = times.len() * interpolation_stride(interpolation);
match (property, outputs) {
(Property::Translation, ReadOutputs::Translations(values)) => {
let values: Vec<[f32; 3]> = values.collect();
if values.len() < required {
continue;
}
locals[node].translation =
sample_vec3(interpolation, ×, &values, time).into();
animated[node] = true;
}
(Property::Scale, ReadOutputs::Scales(values)) => {
let values: Vec<[f32; 3]> = values.collect();
if values.len() < required {
continue;
}
locals[node].scale = sample_vec3(interpolation, ×, &values, time).into();
animated[node] = true;
}
(Property::Rotation, ReadOutputs::Rotations(rotations)) => {
let values: Vec<[f32; 4]> = rotations.into_f32().collect();
if values.len() < required {
continue;
}
locals[node].rotation = sample_rotation(interpolation, ×, &values, time);
animated[node] = true;
}
(Property::MorphTargetWeights, ReadOutputs::MorphTargetWeights(outputs)) => {
let values: Vec<f32> = outputs.into_f32().collect();
let targets = self.morph_weights(node).len();
let needed = times.len() * targets * interpolation_stride(interpolation);
if targets == 0 || values.len() < needed {
continue;
}
weights[node] = Some(sample_weights(
interpolation,
×,
&values,
targets,
time,
));
}
_ => {}
}
}
if !animated.iter().any(|&moved| moved) && weights.iter().all(Option::is_none) {
return;
}
let mut children: Vec<Vec<usize>> = vec![Vec::new(); count];
for (node, parent) in self.parents.iter().enumerate() {
if let Some(parent) = parent {
children[*parent].push(node);
}
}
let mut dirty = animated.clone();
let mut stack: Vec<usize> = (0..count).filter(|&node| dirty[node]).collect();
while let Some(node) = stack.pop() {
for &child in &children[node] {
if !dirty[child] {
dirty[child] = true;
stack.push(child);
}
}
}
let mut matrices = self.node_matrices.clone();
for &node in &node_order(&self.document, &self.parents) {
if !dirty[node] {
continue;
}
let local = glam::Mat4::from(locals[node].compute_matrix());
matrices[node] = match self.parents[node] {
Some(parent) => matrices[parent] * local,
None => local,
};
}
for handle in nodes {
let node = handle.node;
if dirty[node] {
let transform = decompose(matrices[node]);
for &entity in &handle.entities {
let _ = world
.query_one::<&mut Transform>(entity)
.map(|mut current| {
*current = transform.clone();
});
}
}
if let Some(entity) = handle.pose {
let pose = self.skin_pose_with_matrices(node, &matrices);
let _ = world
.query_one::<&mut SkinPose>(entity)
.map(|mut current| *current = pose.clone());
}
if let (Some(entity), Some(sampled)) = (handle.weights, weights[node].as_ref()) {
let _ = world
.query_one::<&mut MorphWeights>(entity)
.map(|mut current| {
current.weights = sampled.clone();
});
}
}
}
fn animation(&self, animation: usize) -> gltf::animation::Animation<'_> {
self.document
.animations()
.nth(animation)
.expect("glTF animation index in bounds")
}
pub fn spawn_node(
&self,
world: &mut ScopeMut<'_>,
node: usize,
resources: &GltfResources,
) -> GltfNode {
let index = node;
let transform = self.node_transforms[node].clone();
let node = self.node(node);
let mut entities = Vec::new();
let mut pose: Option<Entity> = None;
let mut morphs: Option<Entity> = None;
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let mesh_handle = resources
.meshes
.iter()
.find(|handle| {
handle.mesh_index == mesh.index()
&& handle.primitive_index == primitive.index()
})
.expect("a document mesh must be inserted before the node that draws it");
let tint: glam::Vec4 = primitive
.material()
.pbr_metallic_roughness()
.base_color_factor()
.into();
let cutoff = alpha_cutoff(&primitive.material());
let material = samples_base_color_texture(&primitive)
.then(|| self.primitive_material(primitive, &resources.materials));
let z_sorted = mesh_handle.key.options.color_target.blend.is_some();
let skinned = mesh_handle.mesh.skinned;
let binding = skinned.then(|| {
let pose = *pose.get_or_insert_with(|| {
world.spawn_named("gltf skin pose", bundle![self.skin_pose(node.index())])
});
SkinBinding { pose }
});
let morphed = mesh_handle.mesh.morph_targets > 0;
let morph_binding = morphed.then(|| {
let weights = *morphs.get_or_insert_with(|| {
world.spawn_named(
"gltf morph weights",
bundle![MorphWeights {
weights: self.morph_weights(node.index())
}],
)
});
MorphBinding { weights }
});
let mut bundle = DynamicBundle::new().named("gltf primitive");
bundle.push_component(transform.clone());
bundle.push_component(mesh_handle.mesh.clone());
bundle.push_component(UnlitPipeline::new(mesh_handle.key.clone()));
bundle.push_component(InstanceColor::new(tint));
if let Some(cutoff) = cutoff {
bundle.push_component(InstanceCutoff::new(cutoff));
}
if let Some(material) = material {
bundle.push_component(material.bind_group.clone());
}
if let Some(binding) = binding {
bundle.push_component(binding);
}
if let Some(binding) = morph_binding {
bundle.push_component(binding);
}
if z_sorted {
bundle.push_component(ZSortedDrawing);
}
entities.push(world.spawn(bundle));
}
}
GltfNode {
node: index,
entities,
pose,
weights: morphs,
}
}
pub fn spawn_default_scene(
&self,
world: &mut ScopeMut<'_>,
resources: &GltfResources,
) -> Vec<GltfNode> {
let scene = self
.document
.default_scene()
.expect("the glTF document declares a default scene");
let mut nodes = Vec::new();
let mut stack: Vec<usize> = scene.nodes().map(|node| node.index()).collect();
while let Some(node) = stack.pop() {
nodes.push(self.spawn_node(world, node, resources));
stack.extend(self.node(node).children().map(|child| child.index()));
}
nodes
}
fn primitive_material<'m>(
&self,
primitive: gltf::Primitive<'_>,
materials: &'m [Option<GltfMaterial>],
) -> &'m GltfMaterial {
match primitive.material().index() {
Some(index) => materials
.get(index)
.and_then(Option::as_ref)
.expect("every material a spawned mesh reads must have been inserted"),
None => panic!("a primitive that samples a texture must name a material"),
}
}
fn node(&self, node: usize) -> gltf::scene::Node<'_> {
self.document
.nodes()
.nth(node)
.expect("glTF node index in bounds")
}
fn mesh(&self, mesh: usize) -> gltf::Mesh<'_> {
self.document
.meshes()
.nth(mesh)
.expect("glTF mesh index in bounds")
}
fn primitive(&self, mesh: usize, primitive: usize) -> gltf::Primitive<'_> {
self.mesh(mesh)
.primitives()
.nth(primitive)
.expect("glTF primitive index in bounds")
}
fn image_filtering(&self, image: usize, device: &wgpu::Device) -> bool {
image_format(&self.images[image], device).filtering
}
fn image_luminance(&self, image: usize) -> BaseColorChannels {
match self.images[image].format {
gltf::image::Format::R8 | gltf::image::Format::R16 => BaseColorChannels::Luminance,
gltf::image::Format::R8G8 | gltf::image::Format::R16G16 => {
BaseColorChannels::LuminanceAlpha
}
_ => BaseColorChannels::Rgba,
}
}
fn material(&self, material: usize) -> gltf::Material<'_> {
self.document
.materials()
.nth(material)
.expect("glTF material index in bounds")
}
}
fn align_up(value: u32, alignment: u32) -> u32 {
value.div_ceil(alignment) * alignment
}
#[derive(Clone, Copy, Debug)]
struct ImageFormat {
format: wgpu::TextureFormat,
filtering: bool,
}
impl ImageFormat {
fn texel_bytes(self) -> u32 {
texel_bytes(self.format)
}
}
fn texel_bytes(format: wgpu::TextureFormat) -> u32 {
format
.block_copy_size(Some(wgpu::TextureAspect::All))
.expect("a sampled color format has a texel block size")
}
fn image_format(data: &gltf::image::Data, device: &wgpu::Device) -> ImageFormat {
use gltf::image::Format as F;
use wgpu::TextureFormat as T;
let format = match data.format {
F::R8 => T::R8Unorm,
F::R8G8 => T::Rg8Unorm,
F::R8G8B8 => T::Rgba8UnormSrgb,
F::R8G8B8A8 => T::Rgba8UnormSrgb,
F::R16 => T::R16Float,
F::R16G16 => T::Rg16Float,
F::R16G16B16 => T::Rgba16Float,
F::R16G16B16A16 => T::Rgba16Float,
F::R32G32B32FLOAT => T::Rgba32Float,
F::R32G32B32A32FLOAT => T::Rgba32Float,
};
let filtering = matches!(
format.sample_type(Some(wgpu::TextureAspect::All), Some(device.features())),
Some(wgpu::TextureSampleType::Float { filterable: true })
);
ImageFormat { format, filtering }
}
fn encode_rows(data: &gltf::image::Data, format: wgpu::TextureFormat) -> Vec<u8> {
let (width, height) = (data.width as usize, data.height as usize);
let channels = source_channels(data.format);
let texel = texel_bytes(format) as usize;
let bytes_per_row = align_up(
width as u32 * texel as u32,
wgpu::COPY_BYTES_PER_ROW_ALIGNMENT,
) as usize;
let mut rows = vec![0u8; bytes_per_row * height];
let mut scratch = [0u8; 16];
for (y, out) in rows.chunks_exact_mut(bytes_per_row).enumerate() {
for x in 0..width {
let pixel = &data.pixels[(y * width + x) * channels..];
let bytes = encode_texel(data.format, format, pixel, &mut scratch);
out[x * texel..][..texel].copy_from_slice(bytes);
}
}
rows
}
fn source_channel_bytes(format: gltf::image::Format) -> usize {
match format {
gltf::image::Format::R8
| gltf::image::Format::R8G8
| gltf::image::Format::R8G8B8
| gltf::image::Format::R8G8B8A8 => 1,
gltf::image::Format::R16
| gltf::image::Format::R16G16
| gltf::image::Format::R16G16B16
| gltf::image::Format::R16G16B16A16 => 2,
gltf::image::Format::R32G32B32FLOAT | gltf::image::Format::R32G32B32A32FLOAT => 4,
}
}
fn source_channels(format: gltf::image::Format) -> usize {
match format {
gltf::image::Format::R8 | gltf::image::Format::R16 => 1,
gltf::image::Format::R8G8 | gltf::image::Format::R16G16 => 2,
gltf::image::Format::R8G8B8 | gltf::image::Format::R16G16B16 => 3,
gltf::image::Format::R8G8B8A8 | gltf::image::Format::R16G16B16A16 => 4,
gltf::image::Format::R32G32B32FLOAT => 3,
gltf::image::Format::R32G32B32A32FLOAT => 4,
}
}
fn format_channels(format: wgpu::TextureFormat) -> usize {
match format {
wgpu::TextureFormat::R8Unorm | wgpu::TextureFormat::R16Float => 1,
wgpu::TextureFormat::Rg8Unorm | wgpu::TextureFormat::Rg16Float => 2,
wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Rgba16Float
| wgpu::TextureFormat::Rgba32Float => 4,
_ => unreachable!("a glTF image uploads in one of the formats above"),
}
}
fn encode_texel<'a>(
source: gltf::image::Format,
format: wgpu::TextureFormat,
pixel: &[u8],
scratch: &'a mut [u8; 16],
) -> &'a [u8] {
let channel = source_channel_bytes(source);
let wanted = format_channels(format);
let available = source_channels(source);
for slot in 0..wanted {
let value = if slot < available {
read_channel(source, &pixel[slot * channel..])
} else {
1.0
};
write_channel(format, slot, value, scratch);
}
&scratch[..texel_bytes(format) as usize]
}
fn read_channel(format: gltf::image::Format, bytes: &[u8]) -> f32 {
match format {
gltf::image::Format::R8
| gltf::image::Format::R8G8
| gltf::image::Format::R8G8B8
| gltf::image::Format::R8G8B8A8 => bytes[0] as f32 / 255.0,
gltf::image::Format::R16
| gltf::image::Format::R16G16
| gltf::image::Format::R16G16B16
| gltf::image::Format::R16G16B16A16 => {
u16::from_le_bytes([bytes[0], bytes[1]]) as f32 / 65535.0
}
gltf::image::Format::R32G32B32FLOAT | gltf::image::Format::R32G32B32A32FLOAT => {
f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
}
}
fn write_channel(format: wgpu::TextureFormat, slot: usize, value: f32, scratch: &mut [u8; 16]) {
let clamped = if value.is_nan() {
0.0
} else {
value.clamp(0.0, 1.0)
};
match format {
wgpu::TextureFormat::R8Unorm
| wgpu::TextureFormat::Rg8Unorm
| wgpu::TextureFormat::Rgba8UnormSrgb => {
scratch[slot] = (clamped * 255.0).round() as u8;
}
wgpu::TextureFormat::R16Float
| wgpu::TextureFormat::Rg16Float
| wgpu::TextureFormat::Rgba16Float => {
let bits = half::f16::from_f32(clamped).to_bits();
scratch[slot * 2..][..2].copy_from_slice(&bits.to_le_bytes());
}
wgpu::TextureFormat::Rgba32Float => {
scratch[slot * 4..][..4].copy_from_slice(&clamped.to_le_bytes());
}
_ => unreachable!("a glTF image uploads in one of the formats above"),
}
}
fn textured_key(device: &wgpu::Device, filtering: bool) -> UnlitPipelineKey {
UnlitPipelineKey::new(UnlitOptions {
texture_filtering: filtering,
..UnlitOptions::standard(device)
})
}
fn samples_base_color_texture(primitive: &gltf::Primitive<'_>) -> bool {
primitive.get(&gltf::Semantic::TexCoords(0)).is_some()
&& primitive
.material()
.pbr_metallic_roughness()
.base_color_texture()
.is_some()
}
fn morph_target_count(primitive: &gltf::Primitive<'_>) -> usize {
primitive
.morph_targets()
.filter(|target| target.positions().is_some())
.count()
}
fn alpha_cutoff(material: &gltf::Material<'_>) -> Option<f32> {
(material.alpha_mode() == gltf::material::AlphaMode::Mask)
.then(|| material.alpha_cutoff().unwrap_or(DEFAULT_ALPHA_CUTOFF))
}
fn sampler_descriptor(sampler: &gltf::texture::Sampler<'_>) -> wgpu::SamplerDescriptor<'static> {
use gltf::texture::{MagFilter, MinFilter};
let (min_filter, mipmap_filter) = match sampler.min_filter() {
Some(MinFilter::Nearest) | Some(MinFilter::NearestMipmapNearest) => {
(wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Nearest)
}
Some(MinFilter::NearestMipmapLinear) => {
(wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Linear)
}
Some(MinFilter::LinearMipmapNearest) => {
(wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Nearest)
}
Some(MinFilter::Linear) | Some(MinFilter::LinearMipmapLinear) | None => {
(wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Linear)
}
};
let mag_filter = match sampler.mag_filter() {
Some(MagFilter::Nearest) => wgpu::FilterMode::Nearest,
Some(MagFilter::Linear) | None => wgpu::FilterMode::Linear,
};
wgpu::SamplerDescriptor {
label: None,
address_mode_u: address_mode(sampler.wrap_s()),
address_mode_v: address_mode(sampler.wrap_t()),
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter,
min_filter,
mipmap_filter,
..wgpu::SamplerDescriptor::default()
}
}
fn address_mode(mode: gltf::texture::WrappingMode) -> wgpu::AddressMode {
use gltf::texture::WrappingMode;
match mode {
WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
WrappingMode::Repeat => wgpu::AddressMode::Repeat,
}
}
fn node_parents(document: &gltf::Document) -> Vec<Option<usize>> {
let mut parents = vec![None; document.nodes().len()];
for node in document.nodes() {
for child in node.children() {
parents[child.index()] = Some(node.index());
}
}
parents
}
fn node_locals(document: &gltf::Document) -> Vec<Transform> {
document
.nodes()
.map(|node| decompose(glam::Mat4::from_cols_array_2d(&node.transform().matrix())))
.collect()
}
fn node_world_matrices(document: &gltf::Document, parents: &[Option<usize>]) -> Vec<glam::Mat4> {
let mut world = vec![glam::Mat4::IDENTITY; document.nodes().len()];
for &root in &node_order(document, parents) {
let node_data = document.nodes().nth(root).expect("node index in bounds");
let local = glam::Mat4::from_cols_array_2d(&node_data.transform().matrix());
world[root] = match parents[root] {
Some(parent) => world[parent] * local,
None => local,
};
}
world
}
fn node_order(document: &gltf::Document, parents: &[Option<usize>]) -> Vec<usize> {
let roots: Vec<usize> = (0..document.nodes().len())
.filter(|&index| parents[index].is_none())
.collect();
let mut order = Vec::with_capacity(document.nodes().len());
let mut stack = roots;
while let Some(node) = stack.pop() {
order.push(node);
stack.extend(
document
.nodes()
.nth(node)
.expect("node index in bounds")
.children()
.map(|child| child.index()),
);
}
order
}
fn decompose(matrix: glam::Mat4) -> Transform {
let translation = matrix.w_axis.truncate();
let x = matrix.x_axis.truncate();
let y = matrix.y_axis.truncate();
let z = matrix.z_axis.truncate();
let (sx, sy, sz) = (x.length(), y.length(), z.length());
let rotation = glam::Quat::from_mat3(&glam::Mat3::from_cols(
axis_or_identity(x, sx),
axis_or_identity(y, sy),
axis_or_identity(z, sz),
));
Transform {
translation,
rotation,
scale: glam::Vec3::new(sx, sy, sz),
}
}
fn axis_or_identity(axis: glam::Vec3, length: f32) -> glam::Vec3 {
if length > f32::EPSILON {
axis / length
} else {
glam::Vec3::X }
}
fn interpolation_stride(interpolation: Interpolation) -> usize {
match interpolation {
Interpolation::CubicSpline => 3,
Interpolation::Step | Interpolation::Linear => 1,
}
}
fn keyframe(times: &[f32], time: f32) -> (usize, f32) {
let last = times.len() - 1;
if time <= times[0] {
return (0, 0.0);
}
if time >= times[last] {
return (last - 1, 1.0);
}
let frame = times
.partition_point(|&key| key <= time)
.saturating_sub(1)
.min(last - 1);
let span = times[frame + 1] - times[frame];
let t = if span > 0.0 {
(time - times[frame]) / span
} else {
0.0
};
(frame, t)
}
fn step_frame(times: &[f32], time: f32) -> usize {
times.partition_point(|&key| key <= time).saturating_sub(1)
}
fn sample_vec3(
interpolation: Interpolation,
times: &[f32],
values: &[[f32; 3]],
time: f32,
) -> [f32; 3] {
let stride = interpolation_stride(interpolation);
match interpolation {
Interpolation::Step => values[step_frame(times, time) * stride],
Interpolation::Linear => {
let (frame, t) = keyframe(times, time);
glam::Vec3::from(values[frame])
.lerp(glam::Vec3::from(values[frame + 1]), t)
.to_array()
}
Interpolation::CubicSpline => {
let (frame, t) = keyframe(times, time);
cubic_vec3(
glam::Vec3::from(values[frame * 3 + 1]),
glam::Vec3::from(values[frame * 3 + 2]),
glam::Vec3::from(values[(frame + 1) * 3 + 1]),
glam::Vec3::from(values[(frame + 1) * 3]),
t,
times[frame + 1] - times[frame],
)
.to_array()
}
}
}
fn sample_rotation(
interpolation: Interpolation,
times: &[f32],
values: &[[f32; 4]],
time: f32,
) -> glam::Quat {
let stride = interpolation_stride(interpolation);
match interpolation {
Interpolation::Step => quat(values[step_frame(times, time) * stride]),
Interpolation::Linear => {
let (frame, t) = keyframe(times, time);
quat(values[frame]).slerp(quat(values[frame + 1]), t)
}
Interpolation::CubicSpline => {
let (frame, t) = keyframe(times, time);
let value = cubic_vec4(
glam::Vec4::from(values[frame * 3 + 1]),
glam::Vec4::from(values[frame * 3 + 2]),
glam::Vec4::from(values[(frame + 1) * 3 + 1]),
glam::Vec4::from(values[(frame + 1) * 3]),
t,
times[frame + 1] - times[frame],
);
quat(value.to_array())
}
}
}
fn sample_weights(
interpolation: Interpolation,
times: &[f32],
values: &[f32],
targets: usize,
time: f32,
) -> Vec<f32> {
match interpolation {
Interpolation::Step => {
let frame = step_frame(times, time);
values[frame * targets..(frame + 1) * targets].to_vec()
}
Interpolation::Linear => {
let (frame, t) = keyframe(times, time);
let a = &values[frame * targets..(frame + 1) * targets];
let b = &values[(frame + 1) * targets..(frame + 2) * targets];
a.iter().zip(b).map(|(&a, &b)| a + (b - a) * t).collect()
}
Interpolation::CubicSpline => {
let (frame, t) = keyframe(times, time);
let span = times[frame + 1] - times[frame];
let base = frame * 3 * targets;
let next = (frame + 1) * 3 * targets;
(0..targets)
.map(|target| {
cubic_f32(
values[base + targets + target],
values[base + 2 * targets + target],
values[next + targets + target],
values[next + target],
t,
span,
)
})
.collect()
}
}
}
fn cubic_f32(v0: f32, b0: f32, v1: f32, a1: f32, t: f32, span: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
(2.0 * t3 - 3.0 * t2 + 1.0) * v0
+ span * (t3 - 2.0 * t2 + t) * b0
+ (-2.0 * t3 + 3.0 * t2) * v1
+ span * (t3 - t2) * a1
}
fn cubic_vec3(
v0: glam::Vec3,
b0: glam::Vec3,
v1: glam::Vec3,
a1: glam::Vec3,
t: f32,
span: f32,
) -> glam::Vec3 {
let t2 = t * t;
let t3 = t2 * t;
(2.0 * t3 - 3.0 * t2 + 1.0) * v0
+ span * (t3 - 2.0 * t2 + t) * b0
+ (-2.0 * t3 + 3.0 * t2) * v1
+ span * (t3 - t2) * a1
}
fn cubic_vec4(
v0: glam::Vec4,
b0: glam::Vec4,
v1: glam::Vec4,
a1: glam::Vec4,
t: f32,
span: f32,
) -> glam::Vec4 {
let t2 = t * t;
let t3 = t2 * t;
(2.0 * t3 - 3.0 * t2 + 1.0) * v0
+ span * (t3 - 2.0 * t2 + t) * b0
+ (-2.0 * t3 + 3.0 * t2) * v1
+ span * (t3 - t2) * a1
}
fn quat(value: [f32; 4]) -> glam::Quat {
let quat = glam::Quat::from_xyzw(value[0], value[1], value[2], value[3]);
let length = quat.length();
if length > f32::EPSILON {
quat / length
} else {
glam::Quat::IDENTITY
}
}
#[cfg(test)]
mod tests {
use super::*;
use unlit_ecs::World;
fn image(format: gltf::image::Format, pixels: Vec<u8>) -> gltf::image::Data {
gltf::image::Data {
pixels,
format,
width: 1,
height: 1,
}
}
fn encode(format: wgpu::TextureFormat, data: &gltf::image::Data) -> Vec<u8> {
let texel = texel_bytes(format) as usize;
let mut out = Vec::new();
let mut scratch = [0u8; 16];
out.extend_from_slice(encode_texel(
data.format,
format,
&data.pixels,
&mut scratch,
));
assert_eq!(out.len(), texel);
out
}
#[test]
fn a_luma_alpha_image_keeps_luma_in_every_channel_slot_it_has() {
use gltf::image::Format as F;
let data = image(F::R8G8, vec![0x40, 0x80]);
assert_eq!(
encode(wgpu::TextureFormat::Rg8Unorm, &data),
vec![0x40, 0x80]
);
}
#[test]
fn a_three_channel_image_gains_an_opaque_alpha() {
use gltf::image::Format as F;
let data = image(F::R8G8B8, vec![1, 2, 3]);
assert_eq!(
encode(wgpu::TextureFormat::Rgba8UnormSrgb, &data),
vec![1, 2, 3, 255]
);
}
#[test]
fn a_sixteen_bit_image_narrows_to_half_float() {
use gltf::image::Format as F;
let data = image(F::R16G16B16A16, vec![0x00, 0x80, 0x00, 0x40, 0, 0, 0, 0]);
let encoded = encode(wgpu::TextureFormat::Rgba16Float, &data);
let channels = encoded
.as_chunks::<2>()
.0
.iter()
.map(|bytes| half::f16::from_bits(u16::from_le_bytes(*bytes)).to_f32())
.collect::<Vec<_>>();
assert!((channels[0] - 0.5).abs() < 1e-3, "{channels:?}");
assert!((channels[1] - 0.25).abs() < 1e-3, "{channels:?}");
assert_eq!(&channels[2..], &[0.0, 0.0]);
}
#[test]
fn a_float_image_out_of_range_clamps() {
use gltf::image::Format as F;
let data = image(F::R32G32B32A32FLOAT, {
let mut pixels = Vec::new();
pixels.extend_from_slice(&(-1.0f32).to_le_bytes());
pixels.extend_from_slice(&0.5f32.to_le_bytes());
pixels.extend_from_slice(&2.0f32.to_le_bytes());
pixels.extend_from_slice(&f32::NAN.to_le_bytes());
pixels
});
let encoded = encode(wgpu::TextureFormat::Rgba32Float, &data);
let channels = encoded
.as_chunks::<4>()
.0
.iter()
.map(|bytes| f32::from_le_bytes(*bytes))
.collect::<Vec<_>>();
assert_eq!(channels, vec![0.0, 0.5, 1.0, 0.0]);
}
fn skinned_document(translation: [f32; 3]) -> Vec<u8> {
let mut bin = Vec::new();
let mut views: Vec<String> = Vec::new();
let mut add = |bin: &mut Vec<u8>, bytes: &[u8], target: Option<u32>| {
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let target = target.map_or(String::new(), |target| format!(r#", "target": {target}"#));
views.push(format!(
r#"{{ "buffer": 0, "byteOffset": {}, "byteLength": {}{target} }}"#,
bin.len(),
bytes.len()
));
bin.extend_from_slice(bytes);
views.len() - 1
};
let positions: Vec<u8> = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let position_view = add(&mut bin, &positions, Some(34962));
let joints: Vec<u8> = (0..3)
.flat_map(|_| [0u16, 0, 0, 0])
.flat_map(u16::to_le_bytes)
.collect();
let joint_view = add(&mut bin, &joints, Some(34962));
let weights: Vec<u8> = (0..3)
.flat_map(|_| [1.0f32, 0.0, 0.0, 0.0])
.flat_map(f32::to_le_bytes)
.collect();
let weight_view = add(&mut bin, &weights, Some(34962));
let identity: Vec<u8> = [
1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let bind_view = add(&mut bin, &identity, None);
let indices: Vec<u8> = [0u16, 1, 2].iter().flat_map(|i| i.to_le_bytes()).collect();
let index_view = add(&mut bin, &indices, Some(34963));
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let json = format!(
r#"{{"asset":{{"version":"2.0"}},
"scene":0,
"scenes":[{{"nodes":[0]}}],
"nodes":[{{"mesh":0,"skin":0}},
{{"name":"joint","translation":[{x},{y},{z}]}}],
"skins":[{{"joints":[1],"inverseBindMatrices":3}}],
"meshes":[{{"primitives":[{{"attributes":{{"POSITION":0,"JOINTS_0":1,"WEIGHTS_0":2}},
"indices":4}}]}}],
"accessors":[{{"bufferView":{position_view},"componentType":5126,"count":3,
"type":"VEC3","min":[0.0,0.0,0.0],"max":[1.0,1.0,0.0]}},
{{"bufferView":{joint_view},"componentType":5123,"count":3,"type":"VEC4"}},
{{"bufferView":{weight_view},"componentType":5126,"count":3,"type":"VEC4"}},
{{"bufferView":{bind_view},"componentType":5126,"count":1,"type":"MAT4"}},
{{"bufferView":{index_view},"componentType":5123,"count":3,"type":"SCALAR"}}],
"bufferViews":[{}],
"buffers":[{{"byteLength":{}}}]}}"#,
views.join(", "),
bin.len(),
x = translation[0],
y = translation[1],
z = translation[2],
);
let mut json = json.into_bytes();
while !json.len().is_multiple_of(4) {
json.push(b' ');
}
let mut glb = Vec::new();
glb.extend_from_slice(&0x4654_6C67u32.to_le_bytes());
glb.extend_from_slice(&2u32.to_le_bytes());
glb.extend_from_slice(&((12 + 8 + json.len() + 8 + bin.len()) as u32).to_le_bytes());
glb.extend_from_slice(&(json.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x4E4F_534Au32.to_le_bytes());
glb.extend_from_slice(&json);
glb.extend_from_slice(&(bin.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x004E_4942u32.to_le_bytes());
glb.extend_from_slice(&bin);
glb
}
fn morphed_document(
mesh_weights: Option<&[f32]>,
node_weights: Option<&[f32]>,
targets: &[Option<[f32; 3]>],
) -> Vec<u8> {
let mut bin = Vec::new();
let mut views: Vec<String> = Vec::new();
let mut add = |bin: &mut Vec<u8>, bytes: &[u8], target: Option<u32>| {
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let target = target.map_or(String::new(), |target| format!(r#", "target": {target}"#));
views.push(format!(
r#"{{ "buffer": 0, "byteOffset": {}, "byteLength": {}{target} }}"#,
bin.len(),
bytes.len()
));
bin.extend_from_slice(bytes);
views.len() - 1
};
let positions: Vec<u8> = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let mut accessors = vec![format!(
r#"{{ "bufferView": {}, "componentType": 5126, "count": 3, "type": "VEC3",
"min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 0.0] }}"#,
add(&mut bin, &positions, Some(34962))
)];
let mut displacement_accessors = Vec::new();
for target in targets {
let Some(delta) = target else {
continue;
};
let mut deltas = [0.0f32; 9];
deltas[3] = delta[0];
deltas[4] = delta[1];
deltas[5] = delta[2];
let bytes: Vec<u8> = deltas.iter().flat_map(|f| f.to_le_bytes()).collect();
let view = add(&mut bin, &bytes, None);
displacement_accessors.push(accessors.len());
accessors.push(format!(
r#"{{ "bufferView": {view}, "componentType": 5126, "count": 3, "type": "VEC3" }}"#
));
}
let indices: Vec<u8> = [0u16, 1, 2].iter().flat_map(|i| i.to_le_bytes()).collect();
let index_accessor = accessors.len();
accessors.push(format!(
r#"{{ "bufferView": {}, "componentType": 5123, "count": 3, "type": "SCALAR" }}"#,
add(&mut bin, &indices, Some(34963))
));
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let target_json: Vec<String> = targets
.iter()
.enumerate()
.map(|(index, target)| match target {
Some(_) => format!(r#"{{"POSITION":{}}}"#, displacement_accessors[index]),
None => "{}".to_string(),
})
.collect();
let mesh_weights = mesh_weights
.map(|weights| {
let list: Vec<String> = weights.iter().map(|w| w.to_string()).collect();
format!(r#","weights":[{}]"#, list.join(","))
})
.unwrap_or_default();
let node_weights = node_weights
.map(|weights| {
let list: Vec<String> = weights.iter().map(|w| w.to_string()).collect();
format!(r#","weights":[{}]"#, list.join(","))
})
.unwrap_or_default();
let json = format!(
r#"{{"asset":{{"version":"2.0"}},
"scene":0,
"scenes":[{{"nodes":[0]}}],
"nodes":[{{"mesh":0{node_weights}}}],
"meshes":[{{"primitives":[{{"attributes":{{"POSITION":0}},
"indices":{index_accessor},
"targets":[{}]}}]{mesh_weights}}}],
"accessors":[{}],
"bufferViews":[{}],
"buffers":[{{"byteLength":{}}}]}}"#,
target_json.join(", "),
accessors.join(", "),
views.join(", "),
bin.len(),
);
let mut json = json.into_bytes();
while !json.len().is_multiple_of(4) {
json.push(b' ');
}
let mut glb = Vec::new();
glb.extend_from_slice(&0x4654_6C67u32.to_le_bytes());
glb.extend_from_slice(&2u32.to_le_bytes());
glb.extend_from_slice(&((12 + 8 + json.len() + 8 + bin.len()) as u32).to_le_bytes());
glb.extend_from_slice(&(json.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x4E4F_534Au32.to_le_bytes());
glb.extend_from_slice(&json);
glb.extend_from_slice(&(bin.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x004E_4942u32.to_le_bytes());
glb.extend_from_slice(&bin);
glb
}
fn sided_document(double_sided: bool) -> Vec<u8> {
let mut bin = Vec::new();
let mut views: Vec<String> = Vec::new();
let mut add = |bin: &mut Vec<u8>, bytes: &[u8], target: Option<u32>| {
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let target = target.map_or(String::new(), |target| format!(r#", "target": {target}"#));
views.push(format!(
r#"{{ "buffer": 0, "byteOffset": {}, "byteLength": {}{target} }}"#,
bin.len(),
bytes.len()
));
bin.extend_from_slice(bytes);
views.len() - 1
};
let positions: Vec<u8> = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let position_view = add(&mut bin, &positions, Some(34962));
let indices: Vec<u8> = [0u16, 1, 2].iter().flat_map(|i| i.to_le_bytes()).collect();
let index_view = add(&mut bin, &indices, Some(34963));
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let sided = if double_sided {
r#","doubleSided":true"#
} else {
""
};
let json = format!(
r#"{{"asset":{{"version":"2.0"}},
"scene":0,
"scenes":[{{"nodes":[0]}}],
"nodes":[{{"mesh":0}}],
"meshes":[{{"primitives":[{{"attributes":{{"POSITION":0}},
"indices":1,
"material":0}}]}}],
"materials":[{{"pbrMetallicRoughness":{{"baseColorFactor":[1.0,1.0,1.0,1.0]}}{sided}}}],
"accessors":[
{{"bufferView":{position_view},"componentType":5126,"count":3,"type":"VEC3",
"min":[0.0,0.0,0.0],"max":[1.0,1.0,0.0]}},
{{"bufferView":{index_view},"componentType":5123,"count":3,"type":"SCALAR"}}],
"bufferViews":[{}],
"buffers":[{{"byteLength":{}}}]}}"#,
views.join(", "),
bin.len(),
);
let mut json = json.into_bytes();
while !json.len().is_multiple_of(4) {
json.push(b' ');
}
let mut glb = Vec::new();
glb.extend_from_slice(&0x4654_6C67u32.to_le_bytes());
glb.extend_from_slice(&2u32.to_le_bytes());
glb.extend_from_slice(&((12 + 8 + json.len() + 8 + bin.len()) as u32).to_le_bytes());
glb.extend_from_slice(&(json.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x4E4F_534Au32.to_le_bytes());
glb.extend_from_slice(&json);
glb.extend_from_slice(&(bin.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x004E_4942u32.to_le_bytes());
glb.extend_from_slice(&bin);
glb
}
#[test]
fn a_double_sided_material_culls_nothing() {
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
let single = UnlitGltf::from_bytes(&sided_document(false))
.expect("the single-sided document parses");
assert_eq!(
single
.pipeline_key(&device, 0, 0)
.options
.primitive
.cull_mode,
Some(wgpu::Face::Back),
"a material that is not double-sided culls back faces"
);
let double =
UnlitGltf::from_bytes(&sided_document(true)).expect("the sided document parses");
assert_eq!(
double
.pipeline_key(&device, 0, 0)
.options
.primitive
.cull_mode,
None,
"a double-sided material culls nothing"
);
}
#[test]
fn culling_alone_distinguishes_the_two_sided_variants() {
let (device, _queue) = wgpu::Device::noop(&wgpu::DeviceDescriptor::default());
let single = UnlitGltf::from_bytes(&sided_document(false))
.expect("the single-sided document parses")
.pipeline_key(&device, 0, 0);
let double = UnlitGltf::from_bytes(&sided_document(true))
.expect("the double-sided document parses")
.pipeline_key(&device, 0, 0);
assert_ne!(
single, double,
"the two sides must be separate variants so each keeps its own pipeline"
);
assert_eq!(
single.options.primitive.cull_mode,
Some(wgpu::Face::Back),
"a single-sided primitive culls its back faces"
);
assert_eq!(
double.options.primitive.cull_mode, None,
"a double-sided material culls nothing"
);
let mut normalized = single.options.clone();
normalized
.update(|options| options.primitive.cull_mode = double.options.primitive.cull_mode);
assert_eq!(normalized, double.options);
}
#[test]
fn a_morph_target_makes_the_mesh_morph() {
let gltf = UnlitGltf::from_bytes(&morphed_document(None, None, &[Some([0.0, 1.0, 0.0])]))
.expect("the morphed document parses");
let primitive = gltf.primitive(0, 0);
assert_eq!(morph_target_count(&primitive), 1);
}
#[test]
fn a_target_without_positions_does_not_make_the_mesh_morph() {
let gltf = UnlitGltf::from_bytes(&morphed_document(None, None, &[None]))
.expect("the morphed document parses");
let primitive = gltf.primitive(0, 0);
assert_eq!(morph_target_count(&primitive), 0);
assert!(gltf.morph_weights(0).is_empty());
}
#[test]
fn a_node_overrides_the_mesh_weights() {
let gltf = UnlitGltf::from_bytes(&morphed_document(
Some(&[0.25]),
Some(&[0.75]),
&[Some([0.0, 1.0, 0.0])],
))
.expect("the morphed document parses");
assert_eq!(gltf.morph_weights(0), vec![0.75]);
let gltf = UnlitGltf::from_bytes(&morphed_document(
Some(&[0.25]),
None,
&[Some([1.0, 0.0, 0.0])],
))
.expect("the morphed document parses");
assert_eq!(gltf.morph_weights(0), vec![0.25]);
}
#[test]
fn morph_weights_are_padded_to_the_target_count() {
let gltf = UnlitGltf::from_bytes(&morphed_document(
Some(&[0.5]),
None,
&[Some([0.0, 1.0, 0.0]), Some([1.0, 0.0, 0.0])],
))
.expect("the morphed document parses");
assert_eq!(gltf.morph_weights(0), vec![0.5, 0.0]);
let gltf = UnlitGltf::from_bytes(&morphed_document(
Some(&[0.5, 1.0]),
None,
&[Some([0.0, 1.0, 0.0])],
))
.expect("the morphed document parses");
assert_eq!(gltf.morph_weights(0), vec![0.5]);
}
#[test]
fn a_mesh_without_targets_starts_undeformed() {
let gltf = UnlitGltf::from_bytes(&morphed_document(None, None, &[]))
.expect("the morphed document parses");
assert!(gltf.morph_weights(0).is_empty());
assert_eq!(morph_target_count(&gltf.primitive(0, 0)), 0);
}
#[test]
fn a_rest_skin_pose_deforms_nothing() {
let gltf = UnlitGltf::from_bytes(&skinned_document([0.0, 0.0, 0.0]))
.expect("the skinned document parses");
let pose = gltf.skin_pose(0);
assert_eq!(pose.matrices.len(), 1, "the skin has one joint");
assert!(
pose.matrices[0].abs_diff_eq(glam::Mat4::IDENTITY, 1e-6),
"{}",
pose.matrices[0]
);
}
#[test]
fn a_joint_away_from_its_bind_pose_moves_the_mesh() {
let gltf = UnlitGltf::from_bytes(&skinned_document([3.0, -2.0, 1.0]))
.expect("the skinned document parses");
let pose = gltf.skin_pose(0);
assert!(
pose.matrices[0].abs_diff_eq(
glam::Mat4::from_translation(glam::Vec3::new(3.0, -2.0, 1.0)),
1e-6
),
"{}",
pose.matrices[0]
);
}
#[test]
fn a_skinned_primitive_declares_its_joint_stream() {
let gltf = UnlitGltf::from_bytes(&skinned_document([0.0, 0.0, 0.0]))
.expect("the skinned document parses");
let primitive = gltf.primitive(0, 0);
assert!(primitive.get(&gltf::Semantic::Joints(0)).is_some());
assert!(primitive.get(&gltf::Semantic::Weights(0)).is_some());
assert!(!samples_base_color_texture(&primitive));
}
#[test]
fn a_node_without_a_skin_has_no_pose() {
let gltf = UnlitGltf::from_bytes(&skinned_document([0.0, 0.0, 0.0]))
.expect("the skinned document parses");
assert_eq!(gltf.skin_joint_count(0), Some(1));
assert_eq!(gltf.skin_joint_count(1), None);
assert!(gltf.skin_pose(1).matrices.is_empty());
}
#[test]
fn rows_are_padded_to_the_copy_stride() {
use gltf::image::Format as F;
let data = gltf::image::Data {
pixels: vec![7; 2 * 4],
format: F::R8G8B8A8,
width: 2,
height: 1,
};
let rows = encode_rows(&data, wgpu::TextureFormat::Rgba8UnormSrgb);
assert_eq!(rows.len(), wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize);
assert_eq!(&rows[..8], &[7; 8]);
assert_eq!(&rows[8..], &vec![0; 256 - 8][..]);
}
fn assert_keyframe(actual: (usize, f32), expected: (usize, f32)) {
assert_eq!(actual.0, expected.0, "{actual:?} vs {expected:?}");
assert!(
(actual.1 - expected.1).abs() < 1e-6,
"{actual:?} vs {expected:?}"
);
}
fn add_view(
bin: &mut Vec<u8>,
views: &mut Vec<String>,
bytes: &[u8],
target: Option<u32>,
) -> usize {
while !bin.len().is_multiple_of(4) {
bin.push(0);
}
let target = target.map_or(String::new(), |target| format!(r#", "target": {target}"#));
views.push(format!(
r#"{{ "buffer": 0, "byteOffset": {}, "byteLength": {}{target} }}"#,
bin.len(),
bytes.len()
));
bin.extend_from_slice(bytes);
views.len() - 1
}
fn glb(json: String, bin: &[u8]) -> Vec<u8> {
let mut json = json.into_bytes();
while !json.len().is_multiple_of(4) {
json.push(b' ');
}
let mut glb = Vec::new();
glb.extend_from_slice(&0x4654_6C67u32.to_le_bytes());
glb.extend_from_slice(&2u32.to_le_bytes());
glb.extend_from_slice(&((12 + 8 + json.len() + 8 + bin.len()) as u32).to_le_bytes());
glb.extend_from_slice(&(json.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x4E4F_534Au32.to_le_bytes());
glb.extend_from_slice(&json);
glb.extend_from_slice(&(bin.len() as u32).to_le_bytes());
glb.extend_from_slice(&0x004E_4942u32.to_le_bytes());
glb.extend_from_slice(bin);
glb
}
fn bounds(times: &[f32]) -> String {
let min = times.first().copied().unwrap_or(0.0);
let max = times.last().copied().unwrap_or(0.0);
format!(r#", "min": [{min}], "max": [{max}]"#)
}
fn add_triangle(
bin: &mut Vec<u8>,
views: &mut Vec<String>,
accessors: &mut Vec<String>,
) -> usize {
let positions: Vec<u8> = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let position = add_view(bin, views, &positions, Some(34962));
accessors.push(format!(
r#"{{ "bufferView": {position}, "componentType": 5126, "count": 3, "type": "VEC3", "min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 0.0] }}"#
));
let index_accessor = accessors.len();
let indices: Vec<u8> = [0u16, 1, 2].iter().flat_map(|i| i.to_le_bytes()).collect();
let index = add_view(bin, views, &indices, Some(34963));
accessors.push(format!(
r#"{{ "bufferView": {index}, "componentType": 5123, "count": 3, "type": "SCALAR" }}"#
));
index_accessor
}
fn parented_animated_document(
interpolation: &str,
times: &[f32],
translations: &[[f32; 3]],
rotations: &[[f32; 4]],
) -> Vec<u8> {
let mut bin = Vec::new();
let mut views: Vec<String> = Vec::new();
let mut accessors: Vec<String> = Vec::new();
let input_bytes: Vec<u8> = times.iter().flat_map(|t| t.to_le_bytes()).collect();
let input = add_view(&mut bin, &mut views, &input_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {input}, "componentType": 5126, "count": {}, "type": "SCALAR"{} }}"#,
times.len(),
bounds(times),
));
let translation_bytes: Vec<u8> = translations
.iter()
.flatten()
.flat_map(|f| f.to_le_bytes())
.collect();
let translation = add_view(&mut bin, &mut views, &translation_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {translation}, "componentType": 5126, "count": {}, "type": "VEC3" }}"#,
translations.len()
));
let rotation_bytes: Vec<u8> = rotations
.iter()
.flatten()
.flat_map(|f| f.to_le_bytes())
.collect();
let rotation = add_view(&mut bin, &mut views, &rotation_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {rotation}, "componentType": 5126, "count": {}, "type": "VEC4" }}"#,
rotations.len()
));
let index_accessor = add_triangle(&mut bin, &mut views, &mut accessors);
let json = format!(
r#"{{"asset":{{"version":"2.0"}},
"scene":0,
"scenes":[{{"nodes":[0]}}],
"nodes":[{{"children":[1]}},{{"mesh":0}}],
"meshes":[{{"primitives":[{{"attributes":{{"POSITION":3}},"indices":{index_accessor}}}]}}],
"accessors":[{}],
"bufferViews":[{}],
"buffers":[{{"byteLength":{}}}],
"animations":[{{"name":"walk",
"channels":[{{"sampler":0,"target":{{"node":0,"path":"translation"}}}},
{{"sampler":1,"target":{{"node":1,"path":"rotation"}}}}],
"samplers":[{{"input":0,"interpolation":"{interpolation}","output":1}},
{{"input":0,"interpolation":"{interpolation}","output":2}}]}}]}}"#,
accessors.join(", "),
views.join(", "),
bin.len(),
);
glb(json, &bin)
}
fn animated_skinned_document(times: &[f32], translations: &[[f32; 3]]) -> Vec<u8> {
let mut bin = Vec::new();
let mut views: Vec<String> = Vec::new();
let mut accessors: Vec<String> = Vec::new();
let input_bytes: Vec<u8> = times.iter().flat_map(|t| t.to_le_bytes()).collect();
let input = add_view(&mut bin, &mut views, &input_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {input}, "componentType": 5126, "count": {}, "type": "SCALAR"{} }}"#,
times.len(),
bounds(times),
));
let translation_bytes: Vec<u8> = translations
.iter()
.flatten()
.flat_map(|f| f.to_le_bytes())
.collect();
let translation = add_view(&mut bin, &mut views, &translation_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {translation}, "componentType": 5126, "count": {}, "type": "VEC3" }}"#,
translations.len()
));
let positions: Vec<u8> = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let position = add_view(&mut bin, &mut views, &positions, Some(34962));
accessors.push(format!(
r#"{{ "bufferView": {position}, "componentType": 5126, "count": 3, "type": "VEC3", "min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 0.0] }}"#
));
let joints: Vec<u8> = (0..3)
.flat_map(|_| [0u16, 0, 0, 0])
.flat_map(u16::to_le_bytes)
.collect();
let joint = add_view(&mut bin, &mut views, &joints, Some(34962));
accessors.push(format!(
r#"{{ "bufferView": {joint}, "componentType": 5123, "count": 3, "type": "VEC4" }}"#
));
let weights: Vec<u8> = (0..3)
.flat_map(|_| [1.0f32, 0.0, 0.0, 0.0])
.flat_map(f32::to_le_bytes)
.collect();
let weight = add_view(&mut bin, &mut views, &weights, Some(34962));
accessors.push(format!(
r#"{{ "bufferView": {weight}, "componentType": 5126, "count": 3, "type": "VEC4" }}"#
));
let identity: Vec<u8> = [
1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let bind = add_view(&mut bin, &mut views, &identity, None);
accessors.push(format!(
r#"{{ "bufferView": {bind}, "componentType": 5126, "count": 1, "type": "MAT4" }}"#
));
let index_accessor = accessors.len();
let indices: Vec<u8> = [0u16, 1, 2].iter().flat_map(|i| i.to_le_bytes()).collect();
let index = add_view(&mut bin, &mut views, &indices, Some(34963));
accessors.push(format!(
r#"{{ "bufferView": {index}, "componentType": 5123, "count": 3, "type": "SCALAR" }}"#
));
let json = format!(
r#"{{"asset":{{"version":"2.0"}},
"scene":0,
"scenes":[{{"nodes":[0]}}],
"nodes":[{{"mesh":0,"skin":0}},{{"name":"joint"}}],
"skins":[{{"joints":[1],"inverseBindMatrices":5}}],
"meshes":[{{"primitives":[{{"attributes":{{"POSITION":2,"JOINTS_0":3,"WEIGHTS_0":4}},
"indices":{index_accessor}}}]}}],
"accessors":[{}],
"bufferViews":[{}],
"buffers":[{{"byteLength":{}}}],
"animations":[{{"name":"walk",
"channels":[{{"sampler":0,"target":{{"node":1,"path":"translation"}}}}],
"samplers":[{{"input":0,"interpolation":"LINEAR","output":1}}]}}]}}"#,
accessors.join(", "),
views.join(", "),
bin.len(),
);
glb(json, &bin)
}
fn animated_morphed_document(times: &[f32], weights: &[f32]) -> Vec<u8> {
let mut bin = Vec::new();
let mut views: Vec<String> = Vec::new();
let mut accessors: Vec<String> = Vec::new();
let input_bytes: Vec<u8> = times.iter().flat_map(|t| t.to_le_bytes()).collect();
let input = add_view(&mut bin, &mut views, &input_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {input}, "componentType": 5126, "count": {}, "type": "SCALAR"{} }}"#,
times.len(),
bounds(times),
));
let weight_bytes: Vec<u8> = weights.iter().flat_map(|w| w.to_le_bytes()).collect();
let weight = add_view(&mut bin, &mut views, &weight_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {weight}, "componentType": 5126, "count": {}, "type": "SCALAR" }}"#,
weights.len()
));
let positions: Vec<u8> = [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
let position = add_view(&mut bin, &mut views, &positions, Some(34962));
accessors.push(format!(
r#"{{ "bufferView": {position}, "componentType": 5126, "count": 3, "type": "VEC3", "min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 0.0] }}"#
));
let mut deltas = [0.0f32; 9];
deltas[4] = 1.0;
let delta_bytes: Vec<u8> = deltas.iter().flat_map(|f| f.to_le_bytes()).collect();
let delta = add_view(&mut bin, &mut views, &delta_bytes, None);
accessors.push(format!(
r#"{{ "bufferView": {delta}, "componentType": 5126, "count": 3, "type": "VEC3" }}"#
));
let index_accessor = accessors.len();
let indices: Vec<u8> = [0u16, 1, 2].iter().flat_map(|i| i.to_le_bytes()).collect();
let index = add_view(&mut bin, &mut views, &indices, Some(34963));
accessors.push(format!(
r#"{{ "bufferView": {index}, "componentType": 5123, "count": 3, "type": "SCALAR" }}"#
));
let json = format!(
r#"{{"asset":{{"version":"2.0"}},
"scene":0,
"scenes":[{{"nodes":[0]}}],
"nodes":[{{"mesh":0}}],
"meshes":[{{"primitives":[{{"attributes":{{"POSITION":2}},"indices":{index_accessor},
"targets":[{{"POSITION":3}}]}}]}}],
"accessors":[{}],
"bufferViews":[{}],
"buffers":[{{"byteLength":{}}}],
"animations":[{{"name":"walk",
"channels":[{{"sampler":0,"target":{{"node":0,"path":"weights"}}}}],
"samplers":[{{"input":0,"interpolation":"LINEAR","output":1}}]}}]}}"#,
accessors.join(", "),
views.join(", "),
bin.len(),
);
glb(json, &bin)
}
#[test]
fn a_keyframe_clamps_past_either_end_and_splits_the_span() {
let times = [0.0, 1.0, 3.0];
assert_keyframe(keyframe(×, -1.0), (0, 0.0));
assert_keyframe(keyframe(×, 0.0), (0, 0.0));
assert_keyframe(keyframe(×, 0.5), (0, 0.5));
assert_keyframe(keyframe(×, 2.0), (1, 0.5));
assert_keyframe(keyframe(×, 3.0), (1, 1.0));
assert_keyframe(keyframe(×, 4.0), (1, 1.0));
}
#[test]
fn a_step_holds_the_keyframe_before_the_time() {
let times = [0.0, 1.0, 3.0];
assert_eq!(step_frame(×, -1.0), 0);
assert_eq!(step_frame(×, 0.0), 0);
assert_eq!(step_frame(×, 0.9), 0);
assert_eq!(step_frame(×, 1.0), 1);
assert_eq!(step_frame(×, 2.9), 1);
assert_eq!(step_frame(×, 3.0), 2);
assert_eq!(step_frame(×, 9.0), 2);
}
#[test]
fn a_linear_translation_blends_between_its_keyframes() {
let times = [0.0, 2.0];
let values = [[0.0, 0.0, 0.0], [4.0, 2.0, 0.0]];
let sample = |time| sample_vec3(Interpolation::Linear, ×, &values, time);
assert_eq!(sample(1.0), [2.0, 1.0, 0.0]);
assert_eq!(sample(-5.0), [0.0, 0.0, 0.0]);
assert_eq!(sample(99.0), [4.0, 2.0, 0.0]);
}
#[test]
fn a_step_translation_jumps_at_the_next_keyframe() {
let times = [0.0, 1.0];
let values = [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]];
assert_eq!(
sample_vec3(Interpolation::Step, ×, &values, 0.999),
[0.0, 0.0, 0.0]
);
assert_eq!(
sample_vec3(Interpolation::Step, ×, &values, 1.0),
[5.0, 0.0, 0.0]
);
}
#[test]
fn a_cubic_translation_follows_its_tangents() {
let times = [0.0, 1.0];
let values = [
[9.0, 9.0, 9.0],
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
[9.0, 9.0, 9.0],
];
let sample = |time| sample_vec3(Interpolation::CubicSpline, ×, &values, time);
assert_eq!(sample(0.0), [0.0, 0.0, 0.0]);
assert_eq!(sample(1.0), [4.0, 0.0, 0.0]);
assert_eq!(sample(0.5), [1.875, 0.0, 0.0]);
}
#[test]
fn a_linear_rotation_slerps_toward_its_keyframe() {
let quarter = glam::Quat::from_rotation_z(std::f32::consts::FRAC_PI_2);
let values = [
[0.0, 0.0, 0.0, 1.0],
[quarter.x, quarter.y, quarter.z, quarter.w],
];
let half = sample_rotation(Interpolation::Linear, &[0.0, 1.0], &values, 0.5);
let eighth = glam::Quat::from_rotation_z(std::f32::consts::FRAC_PI_4);
assert!(half.dot(eighth).abs() > 1.0 - 1e-6, "{half:?}");
}
#[test]
fn a_linear_rotation_takes_the_short_way_around() {
let a = glam::Quat::from_rotation_z(170f32.to_radians());
let b = glam::Quat::from_rotation_z((-170f32).to_radians());
let values = [[a.x, a.y, a.z, a.w], [b.x, b.y, b.z, b.w]];
let mid = sample_rotation(Interpolation::Linear, &[0.0, 1.0], &values, 0.5);
let expected = glam::Quat::from_rotation_z(180f32.to_radians());
assert!(mid.dot(expected).abs() > 1.0 - 1e-5, "{mid:?}");
}
#[test]
fn a_cubic_rotation_stays_a_unit_quaternion() {
let times = [0.0, 1.0];
let values = [
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
[0.0, 0.0, 0.5, 0.0],
[0.0, 0.0, -0.5, 0.0],
[
0.0,
0.0,
std::f32::consts::FRAC_1_SQRT_2,
std::f32::consts::FRAC_1_SQRT_2,
],
[0.0, 0.0, 0.0, 0.0],
];
let value = sample_rotation(Interpolation::CubicSpline, ×, &values, 0.5);
assert!((value.length() - 1.0).abs() < 1e-5, "{value:?}");
}
#[test]
fn a_linear_weight_blend_is_per_target() {
let times = [0.0, 2.0];
let values = [0.0, 10.0, 2.0, 20.0];
assert_eq!(
sample_weights(Interpolation::Linear, ×, &values, 2, 1.0),
vec![1.0, 15.0]
);
}
#[test]
fn a_cubic_weight_blend_reads_its_kind_first_layout() {
let times = [0.0, 1.0];
let values = [0.0, 1.0, 2.0, 3.0, 9.0, 4.0];
assert_eq!(
sample_weights(Interpolation::CubicSpline, ×, &values, 1, 0.5),
vec![4.875]
);
}
#[test]
fn an_animation_reports_its_name_and_duration() {
let gltf = UnlitGltf::from_bytes(&parented_animated_document(
"LINEAR",
&[0.0, 1.5, 4.0],
&[[0.0; 3]; 3],
&[[0.0, 0.0, 0.0, 1.0]; 3],
))
.expect("the animated document parses");
assert_eq!(gltf.animation_count(), 1);
assert_eq!(gltf.animation_name(0), Some("walk"));
assert!((gltf.animation_duration(0) - 4.0).abs() < 1e-6);
}
#[test]
fn apply_animation_moves_a_node_and_carries_its_child() {
let gltf = UnlitGltf::from_bytes(&parented_animated_document(
"LINEAR",
&[0.0, 2.0],
&[[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]],
&[
[0.0, 0.0, 0.0, 1.0],
[
0.0,
0.0,
std::f32::consts::FRAC_1_SQRT_2,
std::f32::consts::FRAC_1_SQRT_2,
],
],
))
.expect("the animated document parses");
let mut world = World::new();
let parent = world.root_scope_mut().spawn(bundle![Transform::default()]);
let child = world.root_scope_mut().spawn(bundle![Transform::default()]);
let nodes = [
GltfNode {
node: 0,
entities: vec![parent],
pose: None,
weights: None,
},
GltfNode {
node: 1,
entities: vec![child],
pose: None,
weights: None,
},
];
gltf.apply_animation(&world.root_scope(), 0, 1.0, &nodes);
assert_eq!(
world
.root_scope()
.query_one::<&Transform>(parent)
.expect("the parent carries a transform")
.translation,
glam::Vec3::new(2.0, 0.0, 0.0)
);
let child = world
.root_scope()
.query_one::<&Transform>(child)
.expect("the child carries a transform");
assert_eq!(child.translation, glam::Vec3::new(2.0, 0.0, 0.0));
let quarter = glam::Quat::from_rotation_z(std::f32::consts::FRAC_PI_4);
assert!(child.rotation.dot(quarter).abs() > 1.0 - 1e-5, "{child:?}");
}
#[test]
fn apply_animation_holds_a_step_keyframe() {
let gltf = UnlitGltf::from_bytes(&parented_animated_document(
"STEP",
&[0.0, 2.0],
&[[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]],
&[[0.0, 0.0, 0.0, 1.0]; 2],
))
.expect("the animated document parses");
let mut world = World::new();
let parent = world.root_scope_mut().spawn(bundle![Transform::default()]);
let nodes = [GltfNode {
node: 0,
entities: vec![parent],
pose: None,
weights: None,
}];
gltf.apply_animation(&world.root_scope(), 0, 1.999, &nodes);
assert_eq!(
world
.root_scope()
.query_one::<&Transform>(parent)
.expect("the parent carries a transform")
.translation,
glam::Vec3::ZERO
);
}
#[test]
fn apply_animation_rebuilds_a_skin_pose() {
let gltf = UnlitGltf::from_bytes(&animated_skinned_document(
&[0.0, 2.0],
&[[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]],
))
.expect("the animated skinned document parses");
let mut world = World::new();
let mesh = world.root_scope_mut().spawn(bundle![Transform::default()]);
let pose = world.root_scope_mut().spawn(bundle![SkinPose::default()]);
let nodes = [GltfNode {
node: 0,
entities: vec![mesh],
pose: Some(pose),
weights: None,
}];
gltf.apply_animation(&world.root_scope(), 0, 1.0, &nodes);
let pose = world
.root_scope()
.query_one::<&SkinPose>(pose)
.expect("the pose entity carries a pose");
assert!(
pose.matrices[0].abs_diff_eq(
glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
1e-6
),
"{}",
pose.matrices[0]
);
}
#[test]
fn apply_animation_writes_morph_weights() {
let gltf = UnlitGltf::from_bytes(&animated_morphed_document(&[0.0, 2.0], &[0.0, 2.0]))
.expect("the animated morphed document parses");
let mut world = World::new();
let mesh = world.root_scope_mut().spawn(bundle![Transform::default()]);
let weights = world
.root_scope_mut()
.spawn(bundle![MorphWeights { weights: vec![0.0] }]);
let nodes = [GltfNode {
node: 0,
entities: vec![mesh],
pose: None,
weights: Some(weights),
}];
gltf.apply_animation(&world.root_scope(), 0, 1.0, &nodes);
let weights = world
.root_scope()
.query_one::<&MorphWeights>(weights)
.expect("the weights entity carries weights");
assert_eq!(weights.weights, vec![1.0]);
}
}