#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct GltfExportConfig {
pub generator: String,
pub version: String,
pub pretty: bool,
}
impl Default for GltfExportConfig {
fn default() -> Self {
Self {
generator: "OxiHuman".to_string(),
version: "2.0".to_string(),
pretty: false,
}
}
}
#[derive(Debug, Clone)]
pub struct GltfNode {
pub name: String,
pub mesh: Option<usize>,
pub translation: [f32; 3],
pub rotation: [f32; 4],
pub scale: [f32; 3],
}
#[derive(Debug, Clone)]
pub struct GltfMesh {
pub name: String,
pub attributes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct GltfMaterial {
pub name: String,
pub base_color_factor: [f32; 4],
pub metallic_factor: f32,
pub roughness_factor: f32,
pub double_sided: bool,
}
pub type GltfValidationResult = Result<(), String>;
#[allow(dead_code)]
pub fn default_gltf_config() -> GltfExportConfig {
GltfExportConfig::default()
}
#[allow(dead_code)]
pub fn new_gltf_node(name: &str) -> GltfNode {
GltfNode {
name: name.to_string(),
mesh: None,
translation: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
}
}
#[allow(dead_code)]
pub fn add_gltf_node(nodes: &mut Vec<GltfNode>, node: GltfNode) -> usize {
nodes.push(node);
nodes.len()
}
#[allow(dead_code)]
pub fn add_gltf_mesh(meshes: &mut Vec<GltfMesh>, mesh: GltfMesh) -> usize {
meshes.push(mesh);
meshes.len()
}
#[allow(dead_code)]
pub fn add_gltf_material(materials: &mut Vec<GltfMaterial>, material: GltfMaterial) -> usize {
materials.push(material);
materials.len()
}
#[allow(dead_code)]
pub fn node_count(nodes: &[GltfNode]) -> usize {
nodes.len()
}
#[allow(dead_code)]
pub fn mesh_count(meshes: &[GltfMesh]) -> usize {
meshes.len()
}
#[allow(dead_code)]
pub fn material_count_gltf(materials: &[GltfMaterial]) -> usize {
materials.len()
}
#[allow(dead_code)]
pub fn default_gltf_material(name: &str) -> GltfMaterial {
GltfMaterial {
name: name.to_string(),
base_color_factor: [1.0, 1.0, 1.0, 1.0],
metallic_factor: 0.0,
roughness_factor: 0.5,
double_sided: false,
}
}
#[allow(dead_code)]
pub fn validate_gltf(
nodes: &[GltfNode],
meshes: &[GltfMesh],
_materials: &[GltfMaterial],
) -> GltfValidationResult {
for node in nodes {
if let Some(mi) = node.mesh {
if mi >= meshes.len() {
return Err(format!(
"node '{}' references mesh index {} but only {} meshes exist",
node.name,
mi,
meshes.len()
));
}
}
}
Ok(())
}
#[allow(dead_code)]
pub fn gltf_file_size_estimate(
nodes: &[GltfNode],
meshes: &[GltfMesh],
materials: &[GltfMaterial],
) -> usize {
let base = 200usize;
let node_bytes: usize = nodes.iter().map(|n| 80 + n.name.len()).sum();
let mesh_bytes: usize = meshes.iter().map(|m| 60 + m.name.len() + m.attributes.len() * 15).sum();
let mat_bytes: usize = materials.iter().map(|m| 100 + m.name.len()).sum();
base + node_bytes + mesh_bytes + mat_bytes
}
#[allow(dead_code)]
pub fn gltf_to_json(
nodes: &[GltfNode],
meshes: &[GltfMesh],
materials: &[GltfMaterial],
cfg: &GltfExportConfig,
) -> String {
let sep = if cfg.pretty { "\n " } else { "" };
let nl = if cfg.pretty { "\n" } else { "" };
let asset = format!(
r#"{{"generator":"{}","version":"{}"}}"#,
cfg.generator, cfg.version
);
let nodes_json: Vec<String> = nodes
.iter()
.map(|n| {
let mesh_part = match n.mesh {
Some(i) => format!(r#","mesh":{}"#, i),
None => String::new(),
};
format!(
r#"{{"name":"{}","translation":[{},{},{}],"rotation":[{},{},{},{}],"scale":[{},{},{}]{}}}"#,
n.name,
n.translation[0], n.translation[1], n.translation[2],
n.rotation[0], n.rotation[1], n.rotation[2], n.rotation[3],
n.scale[0], n.scale[1], n.scale[2],
mesh_part
)
})
.collect();
let meshes_json: Vec<String> = meshes
.iter()
.map(|m| {
let attrs: Vec<String> = m
.attributes
.iter()
.enumerate()
.map(|(i, a)| format!(r#""{}":{}"#, a, i))
.collect();
format!(
r#"{{"name":"{}","primitives":[{{"attributes":{{{}}}}}]}}"#,
m.name,
attrs.join(",")
)
})
.collect();
let mats_json: Vec<String> = materials
.iter()
.map(|mat| {
let cf = mat.base_color_factor;
format!(
r#"{{"name":"{}","pbrMetallicRoughness":{{"baseColorFactor":[{},{},{},{}],"metallicFactor":{},"roughnessFactor":{}}},"doubleSided":{}}}"#,
mat.name,
cf[0], cf[1], cf[2], cf[3],
mat.metallic_factor,
mat.roughness_factor,
mat.double_sided
)
})
.collect();
let node_indices: Vec<String> = (0..nodes.len()).map(|i| i.to_string()).collect();
format!(
r#"{{{sep}"asset":{asset},{sep}"scene":0,{sep}"scenes":[{{"nodes":[{nodes_idx}]}}],{sep}"nodes":[{nodes_arr}],{sep}"meshes":[{meshes_arr}],{sep}"materials":[{mats_arr}]{nl}}}"#,
sep = sep,
nl = nl,
asset = asset,
nodes_idx = node_indices.join(","),
nodes_arr = nodes_json.join(","),
meshes_arr = meshes_json.join(","),
mats_arr = mats_json.join(","),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_node() -> GltfNode {
let mut n = new_gltf_node("RootNode");
n.mesh = Some(0);
n
}
fn sample_mesh() -> GltfMesh {
GltfMesh {
name: "Body".to_string(),
attributes: vec!["POSITION".to_string(), "NORMAL".to_string()],
}
}
fn sample_material() -> GltfMaterial {
default_gltf_material("Skin")
}
#[test]
fn test_default_gltf_config() {
let cfg = default_gltf_config();
assert_eq!(cfg.version, "2.0");
assert_eq!(cfg.generator, "OxiHuman");
assert!(!cfg.pretty);
}
#[test]
fn test_new_gltf_node_defaults() {
let n = new_gltf_node("Pelvis");
assert_eq!(n.name, "Pelvis");
assert_eq!(n.mesh, None);
assert_eq!(n.scale, [1.0, 1.0, 1.0]);
assert_eq!(n.rotation[3], 1.0);
}
#[test]
fn test_add_gltf_node_count() {
let mut nodes: Vec<GltfNode> = Vec::new();
assert_eq!(add_gltf_node(&mut nodes, sample_node()), 1);
assert_eq!(add_gltf_node(&mut nodes, new_gltf_node("Head")), 2);
}
#[test]
fn test_add_gltf_mesh_count() {
let mut meshes: Vec<GltfMesh> = Vec::new();
assert_eq!(add_gltf_mesh(&mut meshes, sample_mesh()), 1);
}
#[test]
fn test_add_gltf_material_count() {
let mut mats: Vec<GltfMaterial> = Vec::new();
assert_eq!(add_gltf_material(&mut mats, sample_material()), 1);
}
#[test]
fn test_node_count() {
let nodes = vec![sample_node(), new_gltf_node("B")];
assert_eq!(node_count(&nodes), 2);
}
#[test]
fn test_mesh_count() {
let meshes = vec![sample_mesh()];
assert_eq!(mesh_count(&meshes), 1);
}
#[test]
fn test_material_count_gltf() {
let mats = vec![sample_material(), default_gltf_material("Hair")];
assert_eq!(material_count_gltf(&mats), 2);
}
#[test]
fn test_default_gltf_material_fields() {
let mat = default_gltf_material("Eyes");
assert_eq!(mat.name, "Eyes");
assert_eq!(mat.base_color_factor, [1.0, 1.0, 1.0, 1.0]);
assert_eq!(mat.metallic_factor, 0.0);
assert!(!mat.double_sided);
}
#[test]
fn test_validate_gltf_ok() {
let nodes = vec![sample_node()];
let meshes = vec![sample_mesh()];
let mats = vec![sample_material()];
assert!(validate_gltf(&nodes, &meshes, &mats).is_ok());
}
#[test]
fn test_validate_gltf_bad_mesh_ref() {
let mut node = new_gltf_node("Bad");
node.mesh = Some(99);
let meshes: Vec<GltfMesh> = Vec::new();
let mats: Vec<GltfMaterial> = Vec::new();
assert!(validate_gltf(&[node], &meshes, &mats).is_err());
}
#[test]
fn test_gltf_file_size_estimate_positive() {
let nodes = vec![sample_node()];
let meshes = vec![sample_mesh()];
let mats = vec![sample_material()];
assert!(gltf_file_size_estimate(&nodes, &meshes, &mats) > 0);
}
#[test]
fn test_gltf_to_json_contains_asset() {
let cfg = default_gltf_config();
let json = gltf_to_json(&[sample_node()], &[sample_mesh()], &[sample_material()], &cfg);
assert!(json.contains("\"asset\""));
assert!(json.contains("\"2.0\""));
assert!(json.contains("OxiHuman"));
}
#[test]
fn test_gltf_to_json_contains_node_name() {
let cfg = default_gltf_config();
let json = gltf_to_json(&[sample_node()], &[sample_mesh()], &[sample_material()], &cfg);
assert!(json.contains("RootNode"));
}
#[test]
fn test_gltf_to_json_contains_mesh_name() {
let cfg = default_gltf_config();
let json = gltf_to_json(&[sample_node()], &[sample_mesh()], &[sample_material()], &cfg);
assert!(json.contains("Body"));
}
#[test]
fn test_gltf_to_json_contains_material_name() {
let cfg = default_gltf_config();
let json = gltf_to_json(&[sample_node()], &[sample_mesh()], &[sample_material()], &cfg);
assert!(json.contains("Skin"));
}
#[test]
fn test_gltf_to_json_empty() {
let cfg = default_gltf_config();
let json = gltf_to_json(&[], &[], &[], &cfg);
assert!(json.contains("\"asset\""));
assert!(json.contains("\"scenes\""));
}
}