use std::collections::HashMap;
use serde_json::{Value, json};
use super::{AlphaMode, Mesh, Vertex, Wrap};
use crate::Result;
const FLOAT: u32 = 5126;
const UNSIGNED_BYTE: u32 = 5121;
const UNSIGNED_SHORT: u32 = 5123;
const UNSIGNED_INT: u32 = 5125;
const ARRAY_BUFFER: u32 = 34962;
const ELEMENT_ARRAY_BUFFER: u32 = 34963;
const NEAREST: u32 = 9728;
const WHITE: [u8; 4] = [255; 4];
pub(super) fn glb(mesh: &Mesh) -> Result<Vec<u8>> {
let mut buffer = Buffer::default();
let mut primitives = Vec::new();
for primitive in &mesh.primitives {
let triangles = primitive.vertices.len() / 3 * 3;
if triangles == 0 {
continue;
}
let (vertices, indices) = merge(&primitive.vertices[..triangles]);
let material = primitive.material.filter(|&m| m < mesh.materials.len());
let textured = material.is_some_and(|m| mesh.materials[m].texture.is_some());
let positions: Vec<[f32; 3]> = vertices.iter().map(|v| v.position).collect();
let (min, max) = positions.iter().fold(
([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]),
|(min, max), p| {
(
[0, 1, 2].map(|i| min[i].min(p[i])),
[0, 1, 2].map(|i| max[i].max(p[i])),
)
},
);
let mut attributes = json!({
"POSITION": buffer.accessor(&floats(&positions), FLOAT, "VEC3", vertices.len(), ARRAY_BUFFER)
});
let position = &mut buffer.accessors.last_mut().expect("just added");
position["min"] = json!(min);
position["max"] = json!(max);
if vertices.iter().all(|v| v.normal != [0.0; 3]) {
let normals: Vec<[f32; 3]> = vertices.iter().map(|v| normalize(v.normal)).collect();
attributes["NORMAL"] = buffer.accessor(
&floats(&normals),
FLOAT,
"VEC3",
normals.len(),
ARRAY_BUFFER,
);
}
if textured {
let uvs: Vec<[f32; 2]> = vertices.iter().map(|v| v.uv).collect();
attributes["TEXCOORD_0"] =
buffer.accessor(&floats(&uvs), FLOAT, "VEC2", uvs.len(), ARRAY_BUFFER);
}
if vertices.iter().any(|v| v.color != WHITE) {
let colors: Vec<u8> = vertices.iter().flat_map(|v| v.color).collect();
attributes["COLOR_0"] =
buffer.accessor(&colors, UNSIGNED_BYTE, "VEC4", vertices.len(), ARRAY_BUFFER);
buffer.accessors.last_mut().expect("just added")["normalized"] = json!(true);
}
let indices = match vertices.len() <= u16::MAX as usize {
true => {
let bytes: Vec<u8> = indices
.iter()
.flat_map(|&i| (i as u16).to_le_bytes())
.collect();
buffer.accessor(
&bytes,
UNSIGNED_SHORT,
"SCALAR",
indices.len(),
ELEMENT_ARRAY_BUFFER,
)
}
false => {
let bytes: Vec<u8> = indices.iter().flat_map(|&i| i.to_le_bytes()).collect();
buffer.accessor(
&bytes,
UNSIGNED_INT,
"SCALAR",
indices.len(),
ELEMENT_ARRAY_BUFFER,
)
}
};
let mut primitive = json!({ "attributes": attributes, "indices": indices });
if let Some(material) = material {
primitive["material"] = json!(material);
}
primitives.push(primitive);
}
let mut images = Vec::new();
let mut samplers = Vec::new();
let mut textures = Vec::new();
for (index, texture) in mesh.textures.iter().enumerate() {
let view = buffer.view(&texture.image.to_png()?, None);
images.push(json!({ "bufferView": view, "mimeType": "image/png" }));
let [s, t] = texture.wrap.map(|wrap| match wrap {
Wrap::Clamp => 33071,
Wrap::Repeat => 10497,
Wrap::Mirror => 33648,
});
samplers
.push(json!({ "magFilter": NEAREST, "minFilter": NEAREST, "wrapS": s, "wrapT": t }));
textures.push(json!({ "source": index, "sampler": index }));
}
let materials: Vec<Value> = mesh
.materials
.iter()
.map(|material| {
let mut pbr = json!({
"baseColorFactor": material.color.map(|c| c as f32 / 255.0),
"metallicFactor": 0.0,
"roughnessFactor": 1.0,
});
if let Some(texture) = material.texture.filter(|&t| t < mesh.textures.len()) {
pbr["baseColorTexture"] = json!({ "index": texture });
}
let mut value = json!({
"name": material.name,
"pbrMetallicRoughness": pbr,
"alphaMode": match material.alpha {
AlphaMode::Opaque => "OPAQUE",
AlphaMode::Mask => "MASK",
AlphaMode::Blend => "BLEND",
},
"doubleSided": material.double_sided,
});
if material.unlit {
value["extensions"] = json!({ "KHR_materials_unlit": {} });
}
value
})
.collect();
let mut node = json!({ "name": mesh.name });
let mut document = json!({
"asset": { "version": "2.0", "generator": concat!("romkit ", env!("CARGO_PKG_VERSION")) },
"scene": 0,
"scenes": [{ "nodes": [0] }],
});
if !primitives.is_empty() {
node["mesh"] = json!(0);
document["meshes"] = json!([{ "name": mesh.name, "primitives": primitives }]);
}
document["nodes"] = json!([node]);
for (key, list) in [
("materials", materials),
("textures", textures),
("samplers", samplers),
("images", images),
] {
if !list.is_empty() {
document[key] = Value::Array(list);
}
}
if mesh.materials.iter().any(|material| material.unlit) {
document["extensionsUsed"] = json!(["KHR_materials_unlit"]);
}
if !buffer.bytes.is_empty() {
document["buffers"] = json!([{ "byteLength": buffer.bytes.len() }]);
document["bufferViews"] = Value::Array(buffer.views);
document["accessors"] = Value::Array(buffer.accessors);
}
let mut json = serde_json::to_vec(&document)?;
pad(&mut json, b' ');
let mut bin = buffer.bytes;
pad(&mut bin, 0);
let chunks = 8 + json.len() + if bin.is_empty() { 0 } else { 8 + bin.len() };
let mut glb = Vec::with_capacity(12 + chunks);
glb.extend(b"glTF");
glb.extend(2u32.to_le_bytes());
glb.extend((12 + chunks as u32).to_le_bytes());
for (kind, chunk) in [(b"JSON", &json), (b"BIN\0", &bin)] {
if !chunk.is_empty() {
glb.extend((chunk.len() as u32).to_le_bytes());
glb.extend(kind);
glb.extend(chunk);
}
}
Ok(glb)
}
#[derive(Default)]
struct Buffer {
bytes: Vec<u8>,
views: Vec<Value>,
accessors: Vec<Value>,
}
impl Buffer {
fn view(&mut self, bytes: &[u8], target: Option<u32>) -> usize {
pad(&mut self.bytes, 0);
let mut view =
json!({ "buffer": 0, "byteOffset": self.bytes.len(), "byteLength": bytes.len() });
if let Some(target) = target {
view["target"] = json!(target);
}
self.bytes.extend(bytes);
self.views.push(view);
self.views.len() - 1
}
fn accessor(
&mut self,
bytes: &[u8],
component: u32,
kind: &str,
count: usize,
target: u32,
) -> Value {
let view = self.view(bytes, Some(target));
self.accessors.push(json!({
"bufferView": view,
"componentType": component,
"type": kind,
"count": count,
}));
json!(self.accessors.len() - 1)
}
}
fn merge(triangles: &[Vertex]) -> (Vec<Vertex>, Vec<u32>) {
let mut seen = HashMap::new();
let mut vertices = Vec::new();
let indices = triangles
.iter()
.map(|vertex| {
let bits = (
vertex.position.map(f32::to_bits),
vertex.normal.map(f32::to_bits),
vertex.uv.map(f32::to_bits),
vertex.color,
);
*seen.entry(bits).or_insert_with(|| {
vertices.push(*vertex);
vertices.len() as u32 - 1
})
})
.collect();
(vertices, indices)
}
fn floats<const N: usize>(values: &[[f32; N]]) -> Vec<u8> {
values
.iter()
.flatten()
.flat_map(|value| value.to_le_bytes())
.collect()
}
fn normalize(v: [f32; 3]) -> [f32; 3] {
let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
v.map(|c| c / length)
}
fn pad(bytes: &mut Vec<u8>, with: u8) {
bytes.resize(bytes.len().next_multiple_of(4), with);
}
#[cfg(test)]
mod tests {
use super::super::{Image, Material, Primitive, Texture};
use super::*;
fn vertex(position: [f32; 3], color: [u8; 4]) -> Vertex {
Vertex {
position,
normal: [0.0, 0.0, 1.0],
uv: [position[0], position[1]],
color,
}
}
fn chunk(glb: &[u8], at: usize) -> (&[u8], &[u8]) {
let len = u32::from_le_bytes(glb[at..at + 4].try_into().unwrap()) as usize;
(&glb[at + 4..at + 8], &glb[at + 8..at + 8 + len])
}
#[test]
fn writes_a_textured_quad_with_shared_vertices() {
let [a, b, c, d] = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
]
.map(|p| vertex(p, WHITE));
let mesh = Mesh {
name: "quad".into(),
primitives: vec![Primitive {
material: Some(0),
vertices: vec![a, b, c, a, c, d],
}],
materials: vec![Material {
name: "wall".into(),
color: WHITE,
texture: Some(0),
alpha: AlphaMode::Mask,
double_sided: false,
unlit: true,
}],
textures: vec![Texture {
image: Image::new(2, 2),
wrap: [Wrap::Repeat, Wrap::Mirror],
}],
};
let glb = mesh.to_glb().unwrap();
assert_eq!(&glb[..4], b"glTF");
assert_eq!(
u32::from_le_bytes(glb[8..12].try_into().unwrap()) as usize,
glb.len()
);
let (kind, json) = chunk(&glb, 12);
assert_eq!(kind, b"JSON");
let document: Value = serde_json::from_slice(json).unwrap();
let (kind, bin) = chunk(&glb, 20 + json.len());
assert_eq!(kind, b"BIN\0");
assert_eq!(
document["buffers"][0]["byteLength"].as_u64().unwrap() as usize,
bin.len() - 1
);
let primitive = &document["meshes"][0]["primitives"][0];
let accessor = |name: &str| {
&document["accessors"][primitive["attributes"][name].as_u64().unwrap() as usize]
};
assert_eq!(accessor("POSITION")["count"], 4);
assert_eq!(accessor("POSITION")["max"], json!([1.0, 1.0, 0.0]));
assert!(primitive["attributes"].get("COLOR_0").is_none());
assert_eq!(
document["accessors"][primitive["indices"].as_u64().unwrap() as usize]["count"],
6
);
assert_eq!(document["samplers"][0]["wrapT"], 33648);
assert_eq!(document["materials"][0]["alphaMode"], "MASK");
assert_eq!(document["extensionsUsed"], json!(["KHR_materials_unlit"]));
let image = &document["bufferViews"]
[document["images"][0]["bufferView"].as_u64().unwrap() as usize];
let start = image["byteOffset"].as_u64().unwrap() as usize;
assert_eq!(&bin[start..start + 4], b"\x89PNG");
}
#[test]
fn writes_an_empty_mesh_as_a_bare_node() {
let glb = Mesh::default().to_glb().unwrap();
let (_, json) = chunk(&glb, 12);
let document: Value = serde_json::from_slice(json).unwrap();
assert!(document.get("meshes").is_none() && document.get("buffers").is_none());
assert_eq!(glb.len(), 20 + json.len());
}
}