Files
craft-libs/crates/mesh/src/gltf.rs
T
Dylan M. Taylor ceb78f63af craft-mesh: preserve glTF tangents
* gltf: test preserving authored tangents

* gltf: expose authored tangent attributes
2026-11-12 06:26:09 -05:00

1101 lines
50 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! glTF 2.0, read (`.gltf` and `.glb`) and written (`.glb`), from the Khronos glTF 2.0
//! specification.
//!
//! Read: the default scene's node hierarchy (matrix or translation/rotation/scale), triangle
//! primitives (`TRIANGLES`, `TRIANGLE_STRIP`, `TRIANGLE_FAN`, indexed or not) with POSITION,
//! NORMAL, TANGENT, every TEXCOORD_n, COLOR_0, JOINTS_0 and WEIGHTS_0 of any component type the
//! specification or `KHR_mesh_quantization` allows, strided buffer views, sparse accessors,
//! metallic-roughness materials (factors, the five texture slots with their UV set and wrap
//! modes, alpha modes, double-sided, `KHR_materials_emissive_strength`, `KHR_materials_unlit`;
//! `KHR_materials_pbrSpecularGlossiness` as its diffuse colour and texture with roughness from
//! the glossiness), textures whose image is only in `EXT_texture_webp`, images (embedded
//! in a buffer view or a `data:` URI, or named), skins, animations of translation, rotation
//! and scale, cameras and `KHR_lights_punctual` lights. A glTF mesh becomes one [`Mesh`] whose
//! pools hold its primitives' vertices one after another (once for primitives that name the
//! same accessors), and each primitive a [`Part`].
//!
//! Not read: required extensions that change how geometry is stored (Draco, meshopt) are
//! [`Error::Unsupported`]; points, lines, morph targets and
//! other extensions are passed over (skipped primitives are counted in a warning).
//!
//! Buffers are needed to read anything, so an external buffer is asked of the resolver (by its
//! percent-decoded URI) and is [`Error::Missing`] when declined. External images are only named
//! ([`Image::uri`]) and never asked for. Both are listed in [`Scene::external`]. The names come
//! from the file: a resolver must not trust them as paths (they may be absolute or hold `..`).
//!
//! A triangle that names a vertex that is not there, or one whose position is not finite, is
//! left out and counted in a warning.
//!
//! Written ([`write_glb`]): nodes, meshes (positions, normals, every UV set, colours),
//! materials and images. Skins, animations, cameras and lights are not written.
use std::cell::Cell;
use serde_json::{Map, Value, json};
use crate::{
AlphaMode, Animation, Camera, Channel, ChannelPath, Corner, Error, IDENTITY, Image, Interpolation, Light, LightKind, Limits, Material, Mesh,
NONE, Node, Part, Projection, Resolver, Result, Scene, Skin, TextureRef, Trs, Wrap, count_warning, malformed, too_large,
};
const KIND: &str = "glTF";
const CHUNK_JSON: u32 = 0x4E4F_534A;
const CHUNK_BIN: u32 = 0x004E_4942;
/// Most UV sets read from one primitive.
pub const MAX_UV_SETS: usize = 8;
/// Most components that accessors without a buffer view (all zeros) may have, together:
/// nothing in the file backs their counts, so they are capped rather than checked against a
/// buffer.
pub const MAX_ZERO_COMPONENTS: usize = 1 << 24;
/// Does `bytes` start like a GLB container?
pub fn is_glb(bytes: &[u8]) -> bool {
bytes.starts_with(b"glTF")
}
/// Split a GLB container into its JSON chunk and its binary chunk.
pub fn split_glb(bytes: &[u8]) -> Result<(&[u8], Option<&[u8]>)> {
let bad = |m: &str| malformed("GLB", m);
let u32_at = |at: usize| bytes.get(at..at.checked_add(4)?).and_then(|b| b.try_into().ok()).map(u32::from_le_bytes);
if !is_glb(bytes) {
return Err(bad("not a GLB file"));
}
let version = u32_at(4).ok_or_else(|| bad("truncated header"))?;
if version != 2 {
return Err(Error::Unsupported(format!("GLB: version {version} is not glTF 2.0")));
}
let length = u32_at(8).ok_or_else(|| bad("truncated header"))? as usize;
if length > bytes.len() {
return Err(bad(&format!("truncated: the header says {length} bytes, the file has {}", bytes.len())));
}
let mut at = 12usize;
let (mut json, mut bin) = (None, None);
while at.saturating_add(8) <= length {
let (Some(len), Some(kind)) = (u32_at(at), u32_at(at + 4)) else { break };
let start = at + 8;
let end = start.checked_add(len as usize).filter(|e| *e <= length).ok_or_else(|| bad("a chunk runs past the end of the file"))?;
let data = bytes.get(start..end).ok_or_else(|| bad("a chunk runs past the end of the file"))?;
match kind {
CHUNK_JSON if json.is_none() => json = Some(data),
CHUNK_BIN if json.is_some() && bin.is_none() => bin = Some(data),
CHUNK_JSON | CHUNK_BIN => return Err(bad("chunks out of order")),
// Chunks of unknown types are skipped, as the specification asks.
_ => {}
}
at = end;
}
Ok((json.ok_or_else(|| bad("no JSON chunk"))?, bin))
}
/// A GLB container of `json` and an optional binary chunk (each padded to four bytes).
pub fn glb_container(json: &[u8], bin: &[u8]) -> Result<Vec<u8>> {
let pad = |n: usize| (4 - n % 4) % 4;
let json_len = json.len() + pad(json.len());
let bin_len = bin.len() + pad(bin.len());
let total = 12 + 8 + json_len + if bin.is_empty() { 0 } else { 8 + bin_len };
let size = |n: usize| u32::try_from(n).map_err(|_| too_large("GLB", "more than the format's 32-bit lengths hold"));
let mut out = Vec::with_capacity(total);
out.extend_from_slice(b"glTF");
out.extend_from_slice(&2u32.to_le_bytes());
out.extend_from_slice(&size(total)?.to_le_bytes());
out.extend_from_slice(&size(json_len)?.to_le_bytes());
out.extend_from_slice(b"JSON");
out.extend_from_slice(json);
out.extend(std::iter::repeat_n(b' ', pad(json.len())));
if !bin.is_empty() {
out.extend_from_slice(&size(bin_len)?.to_le_bytes());
out.extend_from_slice(b"BIN\0");
out.extend_from_slice(bin);
out.extend(std::iter::repeat_n(0u8, pad(bin.len())));
}
Ok(out)
}
pub use crate::base64_decode;
/// Percent-decode a URI. A `%` that two hex digits do not follow stays as it is.
pub fn uri_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out = Vec::with_capacity(b.len());
let mut i = 0;
while let Some(&c) = b.get(i) {
let hex = |k: usize| b.get(k).and_then(|c| (*c as char).to_digit(16));
if let (b'%', Some(h), Some(l)) = (c, hex(i + 1), hex(i + 2)) {
out.push((h * 16 + l) as u8);
i += 3;
} else {
out.push(c);
i += 1;
}
}
String::from_utf8_lossy(&out).into_owned()
}
/// The media type and bytes of a `data:` URI.
fn data_uri(uri: &str) -> Option<Result<(String, Vec<u8>)>> {
let rest = uri.strip_prefix("data:")?;
Some((|| {
let (meta, data) = rest.split_once(',').ok_or_else(|| malformed(KIND, "a data URI without a comma"))?;
let mime = meta.split(';').next().unwrap_or("").to_string();
let bytes = if meta.ends_with(";base64") {
base64_decode(data).ok_or_else(|| malformed(KIND, "bad base64 in a data URI"))?
} else {
uri_decode(data).into_bytes()
};
Ok((mime, bytes))
})())
}
fn arr<'v>(v: &'v Value, k: &str) -> &'v [Value] {
v.get(k).and_then(Value::as_array).map(Vec::as_slice).unwrap_or(&[])
}
/// A non-negative integer field; one too large for `usize` counts as absent.
fn idx(v: &Value, k: &str) -> Option<usize> {
v.get(k).and_then(Value::as_u64).and_then(|x| usize::try_from(x).ok())
}
/// The indices in an array field that are below `len`.
fn indices(v: &Value, k: &str, len: usize) -> Vec<usize> {
arr(v, k).iter().filter_map(Value::as_u64).filter_map(|c| usize::try_from(c).ok()).filter(|c| *c < len).collect()
}
fn num(v: &Value, k: &str, d: f64) -> f64 {
v.get(k).and_then(Value::as_f64).filter(|x| x.is_finite()).unwrap_or(d)
}
fn nums<const N: usize>(v: &Value, k: &str, d: [f64; N]) -> [f64; N] {
let mut out = d;
if let Some(a) = v.get(k).and_then(Value::as_array) {
for (slot, x) in out.iter_mut().zip(a) {
*slot = x.as_f64().filter(|x| x.is_finite()).unwrap_or(*slot);
}
}
out
}
fn name(v: &Value) -> String {
v.get("name").and_then(Value::as_str).unwrap_or("").to_string()
}
/// One component of type `ct` at the start of `s`, mapped to 0..1 / −1..1 when `norm`.
fn scalar(ct: usize, s: &[u8], norm: bool) -> Option<f64> {
let b2 = || s.get(..2).and_then(|b| <[u8; 2]>::try_from(b).ok());
let b4 = || s.get(..4).and_then(|b| <[u8; 4]>::try_from(b).ok());
Some(match (ct, norm) {
(5126, _) => f64::from(f32::from_le_bytes(b4()?)),
(5125, _) => f64::from(u32::from_le_bytes(b4()?)),
(5123, false) => f64::from(u16::from_le_bytes(b2()?)),
(5123, true) => f64::from(u16::from_le_bytes(b2()?)) / 65_535.0,
(5122, false) => f64::from(i16::from_le_bytes(b2()?)),
(5122, true) => (f64::from(i16::from_le_bytes(b2()?)) / 32_767.0).max(-1.0),
(5121, false) => f64::from(*s.first()?),
(5121, true) => f64::from(*s.first()?) / 255.0,
(5120, false) => f64::from(*s.first()? as i8),
(5120, true) => (f64::from(*s.first()? as i8) / 127.0).max(-1.0),
_ => return None,
})
}
struct Doc<'a> {
j: &'a Value,
buffers: Vec<Vec<u8>>,
vertices_left: Cell<usize>,
triangles_left: Cell<usize>,
parts_left: Cell<usize>,
zero_left: Cell<usize>,
/// Values skins and animations may still take, and bytes images may still copy.
data_left: Cell<usize>,
}
fn charge(cell: &Cell<usize>, n: usize, what: &str) -> Result<()> {
cell.set(cell.get().checked_sub(n).ok_or_else(|| too_large(KIND, format!("more {what} than the limit allows")))?);
Ok(())
}
impl Doc<'_> {
fn accessor(&self, a: usize) -> Result<&Value> {
arr(self.j, "accessors").get(a).ok_or_else(|| malformed(KIND, format!("no accessor {a}")))
}
/// Elements the accessor declares.
fn count(&self, a: usize) -> Option<usize> {
idx(self.accessor(a).ok()?, "count")
}
/// The bytes of a buffer view.
fn view(&self, bv: usize) -> Result<&[u8]> {
let view = arr(self.j, "bufferViews").get(bv).ok_or_else(|| malformed(KIND, format!("no bufferView {bv}")))?;
let buf =
self.buffers.get(idx(view, "buffer").unwrap_or(0)).ok_or_else(|| malformed(KIND, "a bufferView names a buffer that does not exist"))?;
let start = idx(view, "byteOffset").unwrap_or(0);
let end = match view.get("byteLength") {
Some(_) => start.checked_add(idx(view, "byteLength").unwrap_or(usize::MAX)),
None => Some(buf.len()),
};
end.and_then(|end| buf.get(start..end)).ok_or_else(|| malformed(KIND, "a bufferView runs past its buffer"))
}
/// The accessor's values, `n` per element, at most `max` elements: every length is checked
/// against the bytes that are there before anything is allocated from a count.
fn rows(&self, a: usize, max: usize, what: &str) -> Result<(Vec<f64>, usize)> {
let acc = self.accessor(a)?;
let n = match acc.get("type").and_then(Value::as_str).unwrap_or("") {
"SCALAR" => 1,
"VEC2" => 2,
"VEC3" => 3,
"VEC4" | "MAT2" => 4,
"MAT3" => 9,
"MAT4" => 16,
t => return Err(malformed(KIND, format!("accessor type `{}`", t.chars().take(16).collect::<String>()))),
};
let ct = idx(acc, "componentType").unwrap_or(5126);
let csize = match ct {
5120 | 5121 => 1,
5122 | 5123 => 2,
5125 | 5126 => 4,
_ => return Err(malformed(KIND, format!("componentType {ct}"))),
};
let norm = acc.get("normalized").and_then(Value::as_bool).unwrap_or(false);
let count = match acc.get("count") {
None => 0,
Some(c) => c.as_u64().and_then(|c| usize::try_from(c).ok()).ok_or_else(|| malformed(KIND, "an accessor count is not a valid size"))?,
};
if count > max {
return Err(too_large(KIND, format!("{count} {what}, more than the limit allows")));
}
let total = count.checked_mul(n).ok_or_else(|| too_large(KIND, "an accessor count is too large"))?;
let mut out = match idx(acc, "bufferView") {
// No buffer view: all zeros.
None => {
charge(&self.zero_left, total, "values without a buffer")?;
vec![0.0; total]
}
Some(bv) => self.dense(acc, bv, (ct, csize, norm), n, count)?,
};
if let Some(sparse) = acc.get("sparse") {
self.sparse(sparse, (ct, csize, norm), n, &mut out)?;
}
Ok((out, n))
}
/// The `count` elements of `n` components an accessor reads from buffer view `bv`.
fn dense(&self, acc: &Value, bv: usize, (ct, csize, norm): (usize, usize, bool), n: usize, count: usize) -> Result<Vec<f64>> {
let total = count * n;
let stride = arr(self.j, "bufferViews").get(bv).and_then(|v| idx(v, "byteStride")).filter(|s| *s > 0).unwrap_or(csize * n);
if stride < csize * n {
return Err(malformed(KIND, format!("bufferView byteStride {stride} is smaller than its {}-byte elements", csize * n)));
}
let past = || malformed(KIND, "an accessor reads past its buffer");
let data = self.view(bv)?.get(idx(acc, "byteOffset").unwrap_or(0)..).ok_or_else(past)?;
if count > 0 {
let end = (count - 1).checked_mul(stride).and_then(|o| o.checked_add(csize * n)).ok_or_else(past)?;
if end > data.len() {
return Err(past());
}
}
// Matrices of 1- and 2-byte components pad their columns to 4 bytes; not handled (rare).
let mut out = vec![0.0; total];
for (i, row) in out.chunks_exact_mut(n).enumerate() {
for (c, v) in row.iter_mut().enumerate() {
*v = i.checked_mul(stride).and_then(|o| data.get(o.checked_add(c * csize)?..)).and_then(|s| scalar(ct, s, norm)).ok_or_else(past)?;
}
}
Ok(out)
}
/// Put a sparse accessor's values over the elements it names. Its indices and values are
/// tightly packed in their buffer views; an index past the accessor is an error.
fn sparse(&self, sparse: &Value, (ct, csize, norm): (usize, usize, bool), n: usize, out: &mut [f64]) -> Result<()> {
let bad = || malformed(KIND, "a sparse accessor reads past its buffer or names an element that is not there");
let count = idx(sparse, "count").filter(|k| *k <= out.len() / n).ok_or_else(bad)?;
let part = |key: &str| -> Result<(&Value, &[u8])> {
let v = sparse.get(key).ok_or_else(bad)?;
Ok((v, self.view(idx(v, "bufferView").ok_or_else(bad)?)?.get(idx(v, "byteOffset").unwrap_or(0)..).ok_or_else(bad)?))
};
let ((indices, index_data), (_, value_data)) = (part("indices")?, part("values")?);
let index_type = idx(indices, "componentType").unwrap_or(5125);
let index_size = match index_type {
5121 => 1,
5123 => 2,
5125 => 4,
t => return Err(malformed(KIND, format!("sparse index componentType {t}"))),
};
for e in 0..count {
let target = index_data.get(e * index_size..).and_then(|s| scalar(index_type, s, false)).ok_or_else(bad)? as usize;
let row = target.checked_mul(n).and_then(|at| out.get_mut(at..at.checked_add(n)?)).ok_or_else(bad)?;
for (c, v) in row.iter_mut().enumerate() {
*v = value_data.get((e * n + c) * csize..).and_then(|s| scalar(ct, s, norm)).ok_or_else(bad)?;
}
}
Ok(())
}
/// A vertex attribute of `nv` vertices with `n` components in `want`; `None` when the
/// primitive has none or it does not fit the positions.
fn attribute(&self, attrs: &Value, key: &str, nv: usize, want: &[usize]) -> Result<Option<(Vec<f64>, usize)>> {
let Some(a) = idx(attrs, key).filter(|a| self.count(*a) == Some(nv)) else { return Ok(None) };
let (v, n) = self.rows(a, nv, key)?;
Ok(want.contains(&n).then_some((v, n)))
}
}
/// What was passed over, for the warnings.
#[derive(Default)]
struct Skipped {
triangles: usize,
primitives: usize,
}
fn rows_of<const N: usize>(v: &[f64]) -> impl Iterator<Item = [f64; N]> + '_ {
v.as_chunks::<N>().0.iter().copied()
}
/// Append the primitive's vertex attributes to the mesh's pools; the bases its corners count
/// from (`NONE` for a pool the primitive does not fill).
fn attributes(d: &Doc, attrs: &Value, pos: &[f64], mesh: &mut Mesh) -> Result<[u32; 4]> {
let nv = pos.len() / 3;
let base_of = |len: usize| {
u32::try_from(len)
.ok()
.filter(|b| (*b as usize).saturating_add(nv) < NONE as usize)
.ok_or_else(|| too_large(KIND, "more vertices in a mesh than a 32-bit index holds"))
};
let base = base_of(mesh.positions.len())?;
let mut normal_base = NONE;
if let Some((v, _)) = d.attribute(attrs, "NORMAL", nv, &[3])? {
normal_base = base_of(mesh.normals.len())?;
mesh.normals.extend(rows_of::<3>(&v));
}
let mut tangent_base = NONE;
match d.attribute(attrs, "TANGENT", nv, &[4])? {
Some((v, _)) => {
if mesh.tangents.is_empty() {
mesh.tangents.resize(mesh.positions.len(), [0.0; 4]);
}
tangent_base = base_of(mesh.tangents.len())?;
mesh.tangents.extend(rows_of::<4>(&v));
}
None if !mesh.tangents.is_empty() => mesh.tangents.resize(mesh.positions.len() + nv, [0.0; 4]),
None => {}
}
let mut sets = Vec::new();
while sets.len() < MAX_UV_SETS {
let Some((v, _)) = d.attribute(attrs, &format!("TEXCOORD_{}", sets.len()), nv, &[2])? else { break };
sets.push(v);
}
let mut uv_base = NONE;
if !sets.is_empty() {
let len = mesh.uv_sets.first().map_or(0, Vec::len);
uv_base = base_of(len)?;
// Every set of the mesh has one length: a set this primitive lacks is zeros.
while mesh.uv_sets.len() < sets.len() {
mesh.uv_sets.push(vec![[0.0; 2]; len]);
}
for (k, set) in mesh.uv_sets.iter_mut().enumerate() {
match sets.get(k) {
Some(v) => set.extend(rows_of::<2>(v).map(|t| t.map(|x| x as f32))),
None => set.resize(len + nv, [0.0; 2]),
}
}
}
let total = mesh.positions.len() + nv;
match d.attribute(attrs, "COLOR_0", nv, &[3, 4])? {
Some((v, n)) => {
// Vertices of earlier primitives without a colour are white.
mesh.colors.resize(mesh.positions.len(), [1.0; 4]);
mesh.colors.extend(v.chunks_exact(n).map(|c| [0, 1, 2, 3].map(|k| c.get(k).map_or(1.0, |x| *x as f32))));
}
None if !mesh.colors.is_empty() => mesh.colors.resize(total, [1.0; 4]),
None => {}
}
match (d.attribute(attrs, "JOINTS_0", nv, &[4])?, d.attribute(attrs, "WEIGHTS_0", nv, &[4])?) {
(Some((j, _)), Some((w, _))) => {
mesh.joints.resize(mesh.positions.len(), [0; 4]);
mesh.weights.resize(mesh.positions.len(), [0.0; 4]);
mesh.joints.extend(rows_of::<4>(&j).map(|r| r.map(|x| x as u16)));
mesh.weights.extend(rows_of::<4>(&w).map(|r| r.map(|x| x as f32)));
}
_ if !mesh.joints.is_empty() => {
mesh.joints.resize(total, [0; 4]);
mesh.weights.resize(total, [0.0; 4]);
}
_ => {}
}
mesh.positions.extend(rows_of::<3>(pos));
Ok([base, normal_base, uv_base, tangent_base])
}
/// Where the vertices of one set of attribute accessors start in the mesh's pools (positions,
/// normals, UVs), and how many there are. Primitives of a mesh that name the same accessors
/// (one vertex buffer, several materials) share the vertices.
type Shared = std::collections::HashMap<String, ([u32; 4], usize)>;
fn primitive(d: &Doc, p: &Value, mesh: &mut Mesh, shared: &mut Shared, materials: usize, skipped: &mut Skipped) -> Result<()> {
let mode = idx(p, "mode").unwrap_or(4);
let position = p.get("attributes").and_then(|a| idx(a, "POSITION"));
let (Some(attrs), Some(pa), 4..=6) = (p.get("attributes"), position, mode) else {
skipped.primitives += 1;
return Ok(());
};
charge(&d.parts_left, 1, "primitives")?;
let key = attrs.to_string();
let ([base, normal_base, uv_base, tangent_base], nv) = match shared.get(&key) {
Some(known) => *known,
None => {
let (pos, n) = d.rows(pa, d.vertices_left.get(), "vertices")?;
if n != 3 {
return Err(malformed(KIND, "POSITION is not VEC3"));
}
charge(&d.vertices_left, pos.len() / 3, "vertices")?;
let placed = (attributes(d, attrs, &pos, mesh)?, pos.len() / 3);
shared.insert(key, placed);
placed
}
};
let left = d.triangles_left.get();
// A strip or fan of k indices makes k − 2 triangles; a list, k / 3.
let cap = if mode == 4 { left.saturating_mul(3).saturating_add(2) } else { left.saturating_add(2) };
let raw: Vec<u32> = match idx(p, "indices") {
Some(a) => {
let (v, n) = d.rows(a, cap, "indices")?;
if n != 1 {
return Err(malformed(KIND, "indices are not SCALAR"));
}
v.iter().map(|x| *x as u32).collect()
}
None if nv > cap => return Err(too_large(KIND, "more triangles than the limit allows")),
None => (0..nv as u32).collect(),
};
let at = |k: usize| raw.get(k).copied();
let triangle = |k: usize| match mode {
4 => Some([at(3 * k)?, at(3 * k + 1)?, at(3 * k + 2)?]),
5 if k.is_multiple_of(2) => Some([at(k)?, at(k + 1)?, at(k + 2)?]),
5 => Some([at(k + 1)?, at(k)?, at(k + 2)?]),
_ => Some([at(0)?, at(k + 1)?, at(k + 2)?]),
};
let finite = |i: u32| (i as usize) < nv && mesh.positions.get(base as usize + i as usize).is_some_and(|p| p.iter().all(|x| x.is_finite()));
let offset = |b: u32, i: u32| if b == NONE { NONE } else { b + i };
let mut part = Part { material: idx(p, "material").filter(|m| *m < materials), has_tangents: tangent_base != NONE, ..Part::default() };
let count = if mode == 4 { raw.len() / 3 } else { raw.len().saturating_sub(2) };
for t in (0..count).filter_map(triangle) {
if t.iter().all(|i| finite(*i)) {
part.corners.extend(t.map(|i| Corner { position: base + i, normal: offset(normal_base, i), uv: offset(uv_base, i) }));
} else {
skipped.triangles += 1;
}
}
skipped.triangles += usize::from(mode == 4 && !raw.len().is_multiple_of(3));
charge(&d.triangles_left, part.corners.len() / 3, "triangles")?;
mesh.parts.push(part);
Ok(())
}
fn buffers(j: &Value, bin: Option<&[u8]>, limits: &Limits, scene: &mut Scene, resolve: Resolver) -> Result<Vec<Vec<u8>>> {
let list = arr(j, "buffers");
if list.len() > limits.max_extras {
return Err(too_large(KIND, format!("more than {} buffers", limits.max_extras)));
}
let mut out = Vec::new();
let mut total = 0usize;
for (i, b) in list.iter().enumerate() {
let data = match b.get("uri").and_then(Value::as_str) {
Some(uri) => match data_uri(uri) {
Some(data) => data?.1,
None => {
let name = uri_decode(uri);
scene.external.push(name.clone());
resolve(&name).ok_or(Error::Missing(name))?
}
},
// Without a binary chunk the buffer is empty, and whatever reads it runs past it.
None if i == 0 => bin.map(<[u8]>::to_vec).unwrap_or_default(),
None => vec![],
};
total = total.saturating_add(data.len());
if total > limits.max_data_bytes {
return Err(too_large(KIND, format!("buffers of more than {} bytes", limits.max_data_bytes)));
}
out.push(data);
}
Ok(out)
}
fn images(d: &Doc, limits: &Limits, scene: &mut Scene) -> Result<()> {
let list = arr(d.j, "images");
if list.len() > limits.max_materials {
return Err(too_large(KIND, format!("more than {} images", limits.max_materials)));
}
for im in list {
let mut image = Image { name: name(im), mime_type: im.get("mimeType").and_then(Value::as_str).map(str::to_string), ..Image::default() };
if let Some(uri) = im.get("uri").and_then(Value::as_str) {
match data_uri(uri) {
// An image that cannot be decoded is left without data, not an error.
Some(data) => {
if let Ok((mime, bytes)) = data {
image.mime_type = image.mime_type.or(Some(mime).filter(|m| !m.is_empty()));
image.data = Some(bytes);
}
}
None => {
let name = uri_decode(uri);
scene.external.push(name.clone());
image.uri = Some(name);
}
}
} else if let Some(bytes) = idx(im, "bufferView").and_then(|bv| d.view(bv).ok()) {
charge(&d.data_left, bytes.len(), "image bytes")?;
image.data = Some(bytes.to_vec());
}
scene.images.push(image);
}
Ok(())
}
fn materials(j: &Value, limits: &Limits, scene: &mut Scene) -> Result<()> {
let list = arr(j, "materials");
if list.len() > limits.max_materials {
return Err(too_large(KIND, format!("more than {} materials", limits.max_materials)));
}
let wrap = |s: Option<&Value>, k: &str| match s.and_then(|s| s.get(k)).and_then(Value::as_u64) {
Some(33071) => Wrap::Clamp,
Some(33648) => Wrap::Mirror,
_ => Wrap::Repeat,
};
let images = scene.images.len();
let tref = |v: Option<&Value>| -> Option<TextureRef> {
let v = v?;
let texture = arr(j, "textures").get(idx(v, "index")?)?;
let sampler = idx(texture, "sampler").and_then(|s| arr(j, "samplers").get(s));
// `EXT_texture_webp`: the image when the texture has no fallback `source`.
let webp = || texture.get("extensions").and_then(|e| e.get("EXT_texture_webp")).and_then(|e| idx(e, "source"));
let image = idx(texture, "source").or_else(webp).filter(|i| *i < images)?;
Some(TextureRef { image, uv_set: idx(v, "texCoord").unwrap_or(0) as u32, wrap: [wrap(sampler, "wrapS"), wrap(sampler, "wrapT")] })
};
for m in list {
let null = Value::Null;
let pbr = m.get("pbrMetallicRoughness").unwrap_or(&null);
let ext = m.get("extensions");
let strength = ext.and_then(|e| e.get("KHR_materials_emissive_strength")).map_or(1.0, |e| num(e, "emissiveStrength", 1.0));
scene.materials.push(Material {
name: name(m),
base_color: nums(pbr, "baseColorFactor", [1.0; 4]).map(|v| v as f32),
metallic: num(pbr, "metallicFactor", 1.0) as f32,
roughness: num(pbr, "roughnessFactor", 1.0) as f32,
emissive: nums(m, "emissiveFactor", [0.0; 3]).map(|v| (v * strength) as f32),
alpha_mode: match m.get("alphaMode").and_then(Value::as_str) {
Some("MASK") => AlphaMode::Mask(num(m, "alphaCutoff", 0.5) as f32),
Some("BLEND") => AlphaMode::Blend,
_ => AlphaMode::Opaque,
},
double_sided: m.get("doubleSided").and_then(Value::as_bool).unwrap_or(false),
unlit: ext.and_then(|e| e.get("KHR_materials_unlit")).is_some(),
base_color_texture: tref(pbr.get("baseColorTexture")),
metallic_roughness_texture: tref(pbr.get("metallicRoughnessTexture")),
normal_texture: tref(m.get("normalTexture")),
normal_scale: m.get("normalTexture").map_or(1.0, |n| num(n, "scale", 1.0)) as f32,
occlusion_texture: tref(m.get("occlusionTexture")),
occlusion_strength: m.get("occlusionTexture").map_or(1.0, |n| num(n, "strength", 1.0)) as f32,
emissive_texture: tref(m.get("emissiveTexture")),
});
// The older specular-glossiness model, as the nearest metallic-roughness material: its
// diffuse colour and texture, no metal, and roughness from the glossiness.
if let (Some(sg), Some(out)) = (ext.and_then(|e| e.get("KHR_materials_pbrSpecularGlossiness")), scene.materials.last_mut()) {
out.base_color = nums(sg, "diffuseFactor", [1.0; 4]).map(|v| v as f32);
out.base_color_texture = tref(sg.get("diffuseTexture"));
out.metallic = 0.0;
out.roughness = (1.0 - num(sg, "glossinessFactor", 1.0)).clamp(0.0, 1.0) as f32;
out.metallic_roughness_texture = None;
}
}
Ok(())
}
fn nodes(j: &Value, limits: &Limits, scene: &mut Scene) -> Result<()> {
let list = arr(j, "nodes");
if list.len() > limits.max_nodes {
return Err(too_large(KIND, format!("more than {} nodes", limits.max_nodes)));
}
let skins = arr(j, "skins").len().min(limits.max_extras);
for n in list {
let matrix = n.get("matrix").and_then(Value::as_array).filter(|a| a.len() == 16).map(|_| nums(n, "matrix", IDENTITY));
let trs = Trs {
translation: nums(n, "translation", [0.0; 3]),
rotation: nums(n, "rotation", [0.0, 0.0, 0.0, 1.0]),
scale: nums(n, "scale", [1.0; 3]),
};
scene.nodes.push(Node {
name: name(n),
transform: matrix.unwrap_or_else(|| trs.matrix()),
trs: matrix.is_none().then_some(trs),
mesh: idx(n, "mesh").filter(|m| *m < scene.meshes.len()),
skin: idx(n, "skin").filter(|s| *s < skins),
camera: idx(n, "camera").filter(|c| *c < scene.cameras.len()),
light: n.get("extensions").and_then(|e| e.get("KHR_lights_punctual")).and_then(|e| idx(e, "light")).filter(|l| *l < scene.lights.len()),
children: indices(n, "children", list.len()),
});
}
scene.roots = match arr(j, "scenes").get(idx(j, "scene").unwrap_or(0)).or_else(|| arr(j, "scenes").first()) {
Some(s) => indices(s, "nodes", list.len()),
None => {
// No scene: every node that is nobody's child.
let mut child = vec![false; list.len()];
for c in scene.nodes.iter().flat_map(|n| &n.children) {
if let Some(slot) = child.get_mut(*c) {
*slot = true;
}
}
child.iter().enumerate().filter(|(_, c)| !**c).map(|(i, _)| i).collect()
}
};
Ok(())
}
fn skins(d: &Doc, limits: &Limits, scene: &mut Scene) -> Result<()> {
let list = arr(d.j, "skins");
if list.len() > limits.max_extras {
return Err(too_large(KIND, format!("more than {} skins", limits.max_extras)));
}
for s in list {
let joints = indices(s, "joints", scene.nodes.len());
let inverse_bind = match idx(s, "inverseBindMatrices") {
Some(a) => {
let (v, n) = d.rows(a, joints.len(), "inverse bind matrices")?;
charge(&d.data_left, v.len(), "skin and animation values")?;
if n == 16 { rows_of::<16>(&v).collect() } else { vec![] }
}
None => vec![],
};
scene.skins.push(Skin { name: name(s), joints, inverse_bind });
}
Ok(())
}
fn animations(d: &Doc, limits: &Limits, scene: &mut Scene) -> Result<()> {
let list = arr(d.j, "animations");
if list.len() > limits.max_extras {
return Err(too_large(KIND, format!("more than {} animations", limits.max_extras)));
}
for a in list {
let samplers = arr(a, "samplers");
let mut anim = Animation { name: name(a), channels: vec![] };
if arr(a, "channels").len() > limits.max_extras {
return Err(too_large(KIND, format!("more than {} channels in an animation", limits.max_extras)));
}
for c in arr(a, "channels") {
let Some(t) = c.get("target") else { continue };
let Some(node) = idx(t, "node").filter(|n| *n < scene.nodes.len()) else { continue };
let path = match t.get("path").and_then(Value::as_str) {
Some("translation") => ChannelPath::Translation,
Some("rotation") => ChannelPath::Rotation,
Some("scale") => ChannelPath::Scale,
_ => continue,
};
let Some(s) = idx(c, "sampler").and_then(|s| samplers.get(s)) else { continue };
let interpolation = match s.get("interpolation").and_then(Value::as_str) {
Some("STEP") => Interpolation::Step,
Some("CUBICSPLINE") => Interpolation::CubicSpline,
_ => Interpolation::Linear,
};
let (Some(input), Some(output)) = (idx(s, "input"), idx(s, "output")) else { continue };
let floats = |a: usize| -> Result<Vec<f32>> {
let (v, _) = d.rows(a, d.data_left.get(), "animation keys")?;
charge(&d.data_left, v.len(), "skin and animation values")?;
Ok(v.iter().map(|x| *x as f32).collect())
};
anim.channels.push(Channel { node, path, interpolation, times: floats(input)?, values: floats(output)? });
}
scene.animations.push(anim);
}
Ok(())
}
fn cameras_and_lights(j: &Value, limits: &Limits, scene: &mut Scene) -> Result<()> {
let lights = j.get("extensions").and_then(|e| e.get("KHR_lights_punctual")).map_or(&[][..], |e| arr(e, "lights"));
if arr(j, "cameras").len() > limits.max_extras || lights.len() > limits.max_extras {
return Err(too_large(KIND, format!("more than {} cameras or lights", limits.max_extras)));
}
let null = Value::Null;
for c in arr(j, "cameras") {
let projection = match (c.get("type").and_then(Value::as_str), c.get("orthographic")) {
(Some("orthographic"), Some(o)) => Projection::Orthographic {
xmag: num(o, "xmag", 1.0),
ymag: num(o, "ymag", 1.0),
znear: num(o, "znear", 0.01),
zfar: num(o, "zfar", 1000.0),
},
_ => {
let p = c.get("perspective").unwrap_or(&null);
Projection::Perspective {
yfov: num(p, "yfov", 0.8).clamp(1e-3, 3.1),
aspect: p.get("aspectRatio").and_then(Value::as_f64).filter(|a| a.is_finite() && *a > 0.0),
znear: num(p, "znear", 0.01),
zfar: p.get("zfar").and_then(Value::as_f64).filter(|z| z.is_finite()),
}
}
};
scene.cameras.push(Camera { name: name(c), projection });
}
for l in lights {
let kind = match l.get("type").and_then(Value::as_str) {
Some("directional") => LightKind::Directional,
Some("spot") => {
let s = l.get("spot").unwrap_or(&null);
let outer = num(s, "outerConeAngle", std::f64::consts::FRAC_PI_4).clamp(1e-3, std::f64::consts::FRAC_PI_2);
LightKind::Spot { inner: num(s, "innerConeAngle", 0.0).clamp(0.0, outer), outer }
}
_ => LightKind::Point,
};
let range = l.get("range").and_then(Value::as_f64).filter(|r| r.is_finite());
scene.lights.push(Light { name: name(l), kind, color: nums(l, "color", [1.0; 3]), intensity: num(l, "intensity", 1.0).max(0.0), range });
}
Ok(())
}
/// Required extensions that only change how a file looks (textures, material models) or that
/// the reader handles: the geometry reads without the others being understood.
fn check_extensions(j: &Value, scene: &mut Scene) -> Result<()> {
const LOOKS: [&str; 4] = ["KHR_texture_transform", "KHR_texture_basisu", "MSFT_texture_dds", "KHR_lights_punctual"];
const READ: [&str; 5] = [
"KHR_mesh_quantization",
"KHR_materials_unlit",
"KHR_materials_emissive_strength",
"KHR_materials_pbrSpecularGlossiness",
"EXT_texture_webp",
];
for e in arr(j, "extensionsRequired").iter().filter_map(Value::as_str) {
let short: String = e.chars().take(64).collect();
if READ.contains(&e) {
continue;
}
if !LOOKS.contains(&e) && !e.starts_with("KHR_materials_") {
let hint = if e.contains("draco") || e.contains("meshopt") { " (compressed meshes: save the file uncompressed)" } else { "" };
return Err(Error::Unsupported(format!("glTF: required extension {short}{hint}")));
}
if scene.warnings.len() < 16 {
scene.warnings.push(format!("required extension {short} is not read"));
}
}
Ok(())
}
/// Read a `.gltf` (JSON) or `.glb` file. External buffers are asked of `resolve`; see the
/// module documentation.
pub fn read(bytes: &[u8], limits: &Limits, resolve: Resolver) -> Result<Scene> {
let (json, bin) = if is_glb(bytes) { split_glb(bytes)? } else { (bytes, None) };
let j: Value = serde_json::from_slice(json).map_err(|e| malformed(KIND, format!("JSON: {e}")))?;
let version = j.get("asset").and_then(|a| a.get("version")).and_then(Value::as_str).unwrap_or("");
if version.split('.').next() != Some("2") {
return Err(Error::Unsupported(format!("glTF: version `{}` (2.x is read)", version.chars().take(16).collect::<String>())));
}
let mut scene = Scene::default();
check_extensions(&j, &mut scene)?;
let d = Doc {
j: &j,
buffers: buffers(&j, bin, limits, &mut scene, resolve)?,
vertices_left: Cell::new(limits.max_vertices),
triangles_left: Cell::new(limits.max_triangles),
parts_left: Cell::new(limits.max_parts),
zero_left: Cell::new(MAX_ZERO_COMPONENTS),
data_left: Cell::new(limits.max_data_bytes),
};
images(&d, limits, &mut scene)?;
materials(&j, limits, &mut scene)?;
if arr(&j, "meshes").len() > limits.max_meshes {
return Err(too_large(KIND, format!("more than {} meshes", limits.max_meshes)));
}
let mut skipped = Skipped::default();
for m in arr(&j, "meshes") {
let mut mesh = Mesh { name: name(m), ..Mesh::default() };
let mut shared = Shared::new();
for p in arr(m, "primitives") {
primitive(&d, p, &mut mesh, &mut shared, scene.materials.len(), &mut skipped)?;
}
scene.meshes.push(mesh);
}
cameras_and_lights(&j, limits, &mut scene)?;
nodes(&j, limits, &mut scene)?;
skins(&d, limits, &mut scene)?;
animations(&d, limits, &mut scene)?;
count_warning(&mut scene.warnings, skipped.primitives, "primitive(s) are points, lines or have no positions and were left out");
count_warning(&mut scene.warnings, skipped.triangles, "triangle(s) could not be read and were left out");
// A hierarchy with a cycle, or one too deep to walk, is refused here rather than by every caller.
scene.instances(limits)?;
Ok(scene)
}
/// The binary chunk and the buffer views and accessors over it, as they are written.
#[derive(Default)]
struct Writer {
bin: Vec<u8>,
views: Vec<Value>,
accessors: Vec<Value>,
}
impl Writer {
fn view(&mut self, bytes: &[u8], target: Option<u32>) -> usize {
self.bin.resize(self.bin.len().next_multiple_of(4), 0);
let mut view = json!({ "buffer": 0, "byteOffset": self.bin.len(), "byteLength": bytes.len() });
if let (Some(t), Some(v)) = (target, view.as_object_mut()) {
v.insert("target".into(), json!(t));
}
self.bin.extend_from_slice(bytes);
self.views.push(view);
self.views.len() - 1
}
/// A float accessor of `N` components per row; with the bounds glTF wants for positions.
fn floats<const N: usize>(&mut self, rows: impl Iterator<Item = [f32; N]>, kind: &str, bounds: bool) -> usize {
let mut bytes = Vec::new();
let (mut lo, mut hi, mut count) = ([f32::INFINITY; N], [f32::NEG_INFINITY; N], 0usize);
for row in rows {
count += 1;
for (k, x) in row.iter().enumerate() {
bytes.extend_from_slice(&x.to_le_bytes());
if let (Some(l), Some(h)) = (lo.get_mut(k), hi.get_mut(k)) {
*l = l.min(*x);
*h = h.max(*x);
}
}
}
let view = self.view(&bytes, Some(34962));
let mut acc = json!({ "bufferView": view, "componentType": 5126, "count": count, "type": kind });
if let (true, Some(a)) = (bounds && count > 0, acc.as_object_mut()) {
a.insert("min".into(), json!(lo.to_vec()));
a.insert("max".into(), json!(hi.to_vec()));
}
self.accessors.push(acc);
self.accessors.len() - 1
}
fn indices(&mut self, triangles: &[[u32; 3]]) -> usize {
let bytes: Vec<u8> = triangles.iter().flatten().flat_map(|i| i.to_le_bytes()).collect();
let view = self.view(&bytes, Some(34963));
self.accessors.push(json!({ "bufferView": view, "componentType": 5125, "count": triangles.len() * 3, "type": "SCALAR" }));
self.accessors.len() - 1
}
/// The glTF mesh of `mesh`, or `None` when it has no triangle.
fn mesh(&mut self, mesh: &Mesh) -> Option<Value> {
let u = mesh.unified();
if u.parts.iter().all(Vec::is_empty) {
return None;
}
let mut attrs = Map::new();
attrs.insert("POSITION".into(), json!(self.floats(u.positions.iter().map(|p| p.map(|x| x as f32)), "VEC3", true)));
if !u.normals.is_empty() {
attrs.insert("NORMAL".into(), json!(self.floats(u.normals.iter().map(|n| n.map(|x| x as f32)), "VEC3", false)));
}
if !u.uvs.is_empty() {
for (k, set) in mesh.uv_sets.iter().enumerate() {
let rows = u.corners.iter().map(|c| set.get(c.uv as usize).copied().unwrap_or_default());
attrs.insert(format!("TEXCOORD_{k}"), json!(self.floats(rows, "VEC2", false)));
}
}
if !u.colors.is_empty() {
attrs.insert("COLOR_0".into(), json!(self.floats(u.colors.iter().copied(), "VEC4", false)));
}
let mut primitives = Vec::new();
for (part, triangles) in mesh.parts.iter().zip(&u.parts).filter(|(_, t)| !t.is_empty()) {
let mut p = json!({ "attributes": attrs, "indices": self.indices(triangles) });
if let (Some(m), Some(o)) = (part.material, p.as_object_mut()) {
o.insert("material".into(), json!(m));
}
primitives.push(p);
}
let mut out = json!({ "primitives": primitives });
if let (false, Some(o)) = (mesh.name.is_empty(), out.as_object_mut()) {
o.insert("name".into(), json!(mesh.name));
}
Some(out)
}
}
/// The media type of encoded image bytes, for the formats glTF allows.
fn sniff(bytes: &[u8]) -> &'static str {
if bytes.starts_with(&[0xFF, 0xD8]) { "image/jpeg" } else { "image/png" }
}
/// Images, samplers and textures as written: one texture per image and wrap pair in use.
#[derive(Default)]
struct Textures {
images: Vec<Value>,
/// The written index of each image of the scene (`None`: no bytes and no name to write).
image_map: Vec<Option<usize>>,
samplers: Vec<[Wrap; 2]>,
textures: Vec<(usize, usize)>,
}
impl Textures {
fn slot(&mut self, t: Option<TextureRef>) -> Option<Value> {
let t = t?;
let image = (*self.image_map.get(t.image)?)?;
let sampler = self.samplers.iter().position(|s| *s == t.wrap).unwrap_or_else(|| {
self.samplers.push(t.wrap);
self.samplers.len() - 1
});
let index = self.textures.iter().position(|x| *x == (image, sampler)).unwrap_or_else(|| {
self.textures.push((image, sampler));
self.textures.len() - 1
});
Some(json!({ "index": index, "texCoord": t.uv_set }))
}
}
fn material_json(m: &Material, textures: &mut Textures, extensions: &mut Vec<&'static str>) -> Value {
let mut pbr = json!({ "baseColorFactor": m.base_color, "metallicFactor": m.metallic, "roughnessFactor": m.roughness });
let mut out = json!({ "name": m.name, "doubleSided": m.double_sided });
let mut ext = Map::new();
// glTF's emissive factor stops at 1; the rest is a strength.
let peak = m.emissive.iter().copied().fold(1.0f32, f32::max);
if peak > 1.0 {
ext.insert("KHR_materials_emissive_strength".into(), json!({ "emissiveStrength": peak }));
}
if m.unlit {
ext.insert("KHR_materials_unlit".into(), json!({}));
}
for name in ["KHR_materials_emissive_strength", "KHR_materials_unlit"] {
if ext.contains_key(name) && !extensions.contains(&name) {
extensions.push(name);
}
}
let set = |o: &mut Value, key: &str, v: Option<Value>| {
if let (Some(v), Some(o)) = (v, o.as_object_mut()) {
o.insert(key.into(), v);
}
};
set(&mut pbr, "baseColorTexture", textures.slot(m.base_color_texture));
set(&mut pbr, "metallicRoughnessTexture", textures.slot(m.metallic_roughness_texture));
let with = |slot: Option<Value>, key: &str, v: f32| {
slot.map(|mut s| {
if let Some(o) = s.as_object_mut() {
o.insert(key.into(), json!(v));
}
s
})
};
set(&mut out, "normalTexture", with(textures.slot(m.normal_texture), "scale", m.normal_scale));
set(&mut out, "occlusionTexture", with(textures.slot(m.occlusion_texture), "strength", m.occlusion_strength));
set(&mut out, "emissiveTexture", textures.slot(m.emissive_texture));
set(&mut out, "emissiveFactor", Some(json!(m.emissive.map(|e| e / peak))));
set(&mut out, "pbrMetallicRoughness", Some(pbr));
match m.alpha_mode {
AlphaMode::Opaque => {}
AlphaMode::Blend => set(&mut out, "alphaMode", Some(json!("BLEND"))),
AlphaMode::Mask(cutoff) => {
set(&mut out, "alphaMode", Some(json!("MASK")));
set(&mut out, "alphaCutoff", Some(json!(cutoff)));
}
}
set(&mut out, "extensions", (!ext.is_empty()).then_some(Value::Object(ext)));
out
}
/// JSON has no number for these, and a file with them is not glTF.
fn check_finite(scene: &Scene) -> Result<()> {
let positions = scene.meshes.iter().flat_map(|m| m.positions.iter().flatten()).all(|x| (*x as f32).is_finite());
let nodes = scene.nodes.iter().flat_map(|n| n.transform.iter()).all(|x| x.is_finite());
let materials = scene.materials.iter().all(|m| {
let values = [m.metallic, m.roughness, m.normal_scale, m.occlusion_strength];
let cutoff = if let AlphaMode::Mask(c) = m.alpha_mode { c } else { 0.0 };
m.base_color.iter().chain(&m.emissive).chain(&values).all(|x| x.is_finite()) && cutoff.is_finite()
});
if positions && nodes && materials { Ok(()) } else { Err(malformed(KIND, "a position, transform or material value is not finite")) }
}
/// The scene as a binary glTF file: its nodes, meshes (each part a primitive over the mesh's
/// [`Mesh::unified`] vertices: positions, normals, UV sets and colours) and materials, with
/// images that have bytes embedded and the others named by their URI. The binary chunk is
/// limited to `limits.max_data_bytes`.
pub fn write_glb(scene: &Scene, limits: &Limits) -> Result<Vec<u8>> {
scene.validate()?;
scene.instances(limits)?;
check_finite(scene)?;
let mut w = Writer::default();
let mut meshes = Vec::new();
let mesh_map: Vec<Option<usize>> = scene
.meshes
.iter()
.map(|m| {
w.mesh(m).map(|v| {
meshes.push(v);
meshes.len() - 1
})
})
.collect();
let mut textures = Textures::default();
for image in &scene.images {
let mut out = Map::new();
if !image.name.is_empty() {
out.insert("name".into(), json!(image.name));
}
match (&image.data, &image.uri) {
(Some(bytes), _) => {
out.insert("bufferView".into(), json!(w.view(bytes, None)));
out.insert("mimeType".into(), json!(image.mime_type.clone().unwrap_or_else(|| sniff(bytes).into())));
}
(None, Some(uri)) => drop(out.insert("uri".into(), json!(uri))),
(None, None) => {
textures.image_map.push(None);
continue;
}
}
textures.image_map.push(Some(textures.images.len()));
textures.images.push(Value::Object(out));
}
let mut extensions = Vec::new();
let materials: Vec<Value> = scene.materials.iter().map(|m| material_json(m, &mut textures, &mut extensions)).collect();
let nodes: Vec<Value> = scene
.nodes
.iter()
.map(|n| {
let mut out = Map::new();
if !n.name.is_empty() {
out.insert("name".into(), json!(n.name));
}
if n.transform != IDENTITY {
out.insert("matrix".into(), json!(n.transform));
}
if let Some(m) = n.mesh.and_then(|m| mesh_map.get(m).copied().flatten()) {
out.insert("mesh".into(), json!(m));
}
if !n.children.is_empty() {
out.insert("children".into(), json!(n.children));
}
Value::Object(out)
})
.collect();
if w.bin.len() > limits.max_data_bytes {
return Err(too_large("GLB", format!("more than {} bytes of data", limits.max_data_bytes)));
}
let wrap = |w: Wrap| match w {
Wrap::Repeat => 10497,
Wrap::Clamp => 33071,
Wrap::Mirror => 33648,
};
let mut root = Map::new();
root.insert("asset".into(), json!({ "version": "2.0", "generator": "craft-mesh" }));
root.insert("scene".into(), json!(0));
root.insert("scenes".into(), json!([{ "nodes": scene.roots }]));
// glTF does not allow empty arrays.
let mut list = |key: &str, values: Vec<Value>| {
if !values.is_empty() {
root.insert(key.into(), Value::Array(values));
}
};
list("nodes", nodes);
list("meshes", meshes);
list("materials", materials);
list("textures", textures.textures.iter().map(|(image, sampler)| json!({ "source": image, "sampler": sampler })).collect());
list("samplers", textures.samplers.iter().map(|s| json!({ "wrapS": wrap(s[0]), "wrapT": wrap(s[1]) })).collect());
list("images", textures.images);
list("accessors", w.accessors);
list("bufferViews", w.views);
list("extensionsUsed", extensions.iter().map(|e| json!(e)).collect());
if !w.bin.is_empty() {
root.insert("buffers".into(), json!([{ "byteLength": w.bin.len() }]));
}
let json = serde_json::to_vec(&Value::Object(root)).map_err(|e| malformed(KIND, e))?;
glb_container(&json, &w.bin)
}