use std::collections::BTreeMap;
use std::sync::Arc;
use ::gltf::Gltf;
use ::gltf::buffer::Source as BufferSource;
use crate::diagnostics::AssetError;
pub use self::anchors::SceneAssetAnchor;
use self::anchors::parse_node_anchors;
use self::animation::parse_gltf_clips;
pub use self::connectors::SceneAssetConnector;
use self::connectors::parse_node_connectors;
pub use self::extensions::{GltfDecoderPolicy, GltfExtensionDiagnostic, GltfExtensionStatus};
use self::extensions::{collect_extension_diagnostics, is_v1_required_gltf_extension};
use self::external::{external_buffer_paths, external_image_paths, resolve_relative_path};
use self::lights::parse_punctual_lights;
pub use self::material_variants::MaterialVariantBinding;
use self::materials::parse_materials;
use self::meshes::parse_meshes;
use self::scene_asset::SceneAssetData;
pub use self::scene_asset::{
ASSET_GEOMETRY_SUMMARY_SCHEMA_V1, SceneAsset, SceneAssetClip, SceneAssetGeometrySummary,
SceneAssetLight, SceneAssetMesh, SceneAssetNode,
};
pub use self::skins::SceneAssetSkin;
use self::skins::parse_skins;
use self::textures::parse_textures;
use self::transform::from_gltf_transform;
use super::{AssetPath, AssetProvenance, AssetStorage};
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
fn gltf_now_ms() -> f64 {
js_sys::Date::now()
}
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
fn log_gltf_step(label: &str, start_ms: f64) -> f64 {
let now = gltf_now_ms();
if crate::diagnostics::browser_timing_enabled() {
web_sys::console::log_1(
&format!("[scena-demo] glTF {label}: {:.1}ms", now - start_ms).into(),
);
}
now
}
mod anchors;
mod animation;
mod buffers;
mod connectors;
mod extensions;
mod external;
mod instancing;
mod lights;
mod material_extensions;
mod material_variants;
mod materials;
mod meshes;
mod meshopt;
mod scene_asset;
mod skins;
mod textures;
mod transform;
impl SceneAsset {
pub(super) fn from_gltf_bytes(
path: AssetPath,
bytes: &[u8],
storage: &mut AssetStorage,
) -> Result<Self, AssetError> {
Self::from_gltf_bytes_with_external_resources(
path,
bytes,
&BTreeMap::new(),
&BTreeMap::new(),
storage,
)
}
pub(super) fn from_gltf_bytes_with_external_resources(
path: AssetPath,
bytes: &[u8],
external_buffers: &BTreeMap<usize, Vec<u8>>,
external_images: &BTreeMap<AssetPath, Vec<u8>>,
storage: &mut AssetStorage,
) -> Result<Self, AssetError> {
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
let total_start = gltf_now_ms();
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
let mut step_start = total_start;
let gltf = open_gltf_with_massage(&path, bytes)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("open_gltf_with_massage", step_start);
}
let blob = gltf.blob.clone();
let provenance = AssetProvenance::from_source_bytes(path.clone(), bytes);
let scene = Self::from_gltf_document(
&path,
&gltf,
blob.as_deref(),
external_buffers,
external_images,
provenance,
storage,
)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
log_gltf_step("from_gltf_document wrapper", step_start);
log_gltf_step("from_gltf_bytes_with_external_resources total", total_start);
}
Ok(scene)
}
pub(super) fn external_buffer_paths(
path: &AssetPath,
bytes: &[u8],
) -> Result<Vec<(usize, AssetPath, usize)>, AssetError> {
external_buffer_paths(path, bytes)
}
pub(super) fn external_image_paths(
path: &AssetPath,
bytes: &[u8],
) -> Result<Vec<AssetPath>, AssetError> {
external_image_paths(path, bytes)
}
fn from_gltf_document(
path: &AssetPath,
gltf: &Gltf,
binary_chunk: Option<&[u8]>,
external_buffers: &BTreeMap<usize, Vec<u8>>,
external_images: &BTreeMap<AssetPath, Vec<u8>>,
provenance: AssetProvenance,
storage: &mut AssetStorage,
) -> Result<Self, AssetError> {
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
let total_start = gltf_now_ms();
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
let mut step_start = total_start;
validate_gltf_version(path, gltf)?;
let extensions_used: Vec<String> = gltf
.document
.extensions_used()
.map(str::to_string)
.collect();
let extensions_required: Vec<String> = gltf
.document
.extensions_required()
.map(str::to_string)
.collect();
for extension in &extensions_required {
if !is_v1_required_gltf_extension(extension) {
return Err(AssetError::UnsupportedRequiredExtension {
path: path.as_str().to_string(),
extension: extension.clone(),
});
}
}
let extension_diagnostics = collect_extension_diagnostics(&extensions_used);
let material_variants = material_variants::parse_material_variant_names(&gltf.document);
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("metadata + extensions", step_start);
}
let mut buffers = buffers::ResolvedGltfBuffers::new(resolve_buffers(
path,
gltf,
binary_chunk,
external_buffers,
)?);
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("resolve_buffers", step_start);
}
meshopt::decode_meshopt_buffer_views(path, &gltf.document, &mut buffers)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("decode_meshopt_buffer_views", step_start);
}
let textures = parse_textures(path, &gltf.document, &buffers, external_images, storage);
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("parse_textures", step_start);
}
let materials = parse_materials(path, &gltf.document, storage, &textures)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("parse_materials", step_start);
}
let meshes = parse_meshes(path, &gltf.document, &buffers, &materials, storage)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("parse_meshes", step_start);
}
let skins = parse_skins(path, &gltf.document, &buffers)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("parse_skins", step_start);
}
let lights = parse_punctual_lights(&gltf.document);
let nodes = parse_gltf_nodes(path, &gltf.document, &buffers, &meshes, &lights)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
step_start = log_gltf_step("parse_lights_nodes", step_start);
}
let clips = parse_gltf_clips(path, &gltf.document, &buffers)?;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
{
log_gltf_step("parse_clips", step_start);
log_gltf_step("from_gltf_document total", total_start);
}
let node_count = nodes.len();
let mesh_count = meshes.iter().map(Vec::len).sum();
Ok(Self {
inner: Arc::new(SceneAssetData {
path: path.clone(),
node_count,
mesh_count,
nodes,
skins,
clips,
extensions_used,
extensions_required,
extension_diagnostics,
material_variants,
provenance,
retained_source_bytes: None,
}),
})
}
}
fn validate_gltf_version(path: &AssetPath, gltf: &Gltf) -> Result<(), AssetError> {
if gltf.document.as_json().asset.version == "2.0" {
Ok(())
} else {
Err(AssetError::Parse {
path: path.as_str().to_string(),
reason: "expected glTF asset.version \"2.0\"".to_string(),
})
}
}
fn resolve_buffers(
path: &AssetPath,
gltf: &Gltf,
binary_chunk: Option<&[u8]>,
external_buffers: &BTreeMap<usize, Vec<u8>>,
) -> Result<Vec<Vec<u8>>, AssetError> {
gltf.document
.buffers()
.map(|buffer| {
let byte_length = buffer.length();
match buffer.source() {
BufferSource::Bin => {
let bin = binary_chunk.ok_or_else(|| AssetError::Parse {
path: path.as_str().to_string(),
reason: "glTF buffer without uri requires a GLB binary chunk".to_string(),
})?;
Ok(bin
.get(..byte_length)
.ok_or_else(|| AssetError::Parse {
path: path.as_str().to_string(),
reason: "GLB binary chunk is shorter than buffer byteLength".to_string(),
})?
.to_vec())
}
BufferSource::Uri(uri) => {
if uri.starts_with("data:") {
let (_, encoded) =
uri.split_once(";base64,").ok_or_else(|| AssetError::Parse {
path: path.as_str().to_string(),
reason:
"only embedded base64 glTF buffers are supported in this loader slice"
.to_string(),
})?;
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(encoded)
.map_err(|error| AssetError::Parse {
path: path.as_str().to_string(),
reason: format!("invalid embedded buffer base64: {error}"),
})
} else {
let _resolved = resolve_relative_path(path, uri);
let bytes = external_buffers
.get(&buffer.index())
.ok_or_else(|| AssetError::Parse {
path: path.as_str().to_string(),
reason: "external glTF buffer was not fetched".to_string(),
})?;
bytes
.get(..byte_length)
.map(<[u8]>::to_vec)
.ok_or_else(|| AssetError::Parse {
path: path.as_str().to_string(),
reason: "external glTF buffer is shorter than byteLength"
.to_string(),
})
}
}
}
})
.collect()
}
fn parse_gltf_nodes(
path: &AssetPath,
document: &::gltf::Document,
buffers: &buffers::ResolvedGltfBuffers,
meshes: &[Vec<SceneAssetMesh>],
lights: &[SceneAssetLight],
) -> Result<Vec<SceneAssetNode>, AssetError> {
document
.nodes()
.map(|node| {
Ok(SceneAssetNode {
name: node.name().map(str::to_string),
children: node.children().map(|child| child.index()).collect(),
transform: from_gltf_transform(node.transform()),
meshes: node
.mesh()
.and_then(|mesh| meshes.get(mesh.index()))
.cloned()
.unwrap_or_default(),
instance_transforms: instancing::parse_node_instance_transforms(
path, document, buffers, &node,
)?,
skin: node.skin().map(|skin| skin.index()),
light: node
.light()
.and_then(|light| lights.get(light.index()).copied()),
anchors: parse_node_anchors(&node),
connectors: parse_node_connectors(&node),
})
})
.collect()
}
pub(super) fn extras_to_value(extras: &::gltf::json::Extras) -> Option<serde_json::Value> {
let raw = extras.as_ref()?;
serde_json::from_str(raw.get()).ok()
}
pub(super) fn has_glb_magic(bytes: &[u8]) -> bool {
bytes.starts_with(&0x4654_6C67_u32.to_le_bytes())
}
pub(super) fn open_gltf_with_massage(path: &AssetPath, bytes: &[u8]) -> Result<Gltf, AssetError> {
let parse = |slice: &[u8]| {
Gltf::from_slice_without_validation(slice).map_err(|error| AssetError::Parse {
path: path.as_str().to_string(),
reason: error.to_string(),
})
};
if has_glb_magic(bytes) {
return parse(bytes);
}
if let Some(massaged) = massage_json_for_gltf_crate(bytes) {
return parse(&massaged);
}
parse(bytes)
}
pub(super) fn massage_json_for_gltf_crate(bytes: &[u8]) -> Option<Vec<u8>> {
let mut value: serde_json::Value = serde_json::from_slice(bytes).ok()?;
let mut changed = false;
if let Some(animations) = value.get_mut("animations").and_then(|v| v.as_array_mut()) {
for animation in animations.iter_mut() {
let Some(object) = animation.as_object_mut() else {
continue;
};
if !object.contains_key("channels") {
object.insert("channels".to_string(), serde_json::Value::Array(Vec::new()));
changed = true;
}
if !object.contains_key("samplers") {
object.insert("samplers".to_string(), serde_json::Value::Array(Vec::new()));
changed = true;
}
}
}
if let Some(ext) = value
.get_mut("extensions")
.and_then(|v| v.get_mut("KHR_materials_variants"))
.and_then(|v| v.as_object_mut())
&& !ext.contains_key("variants")
{
ext.insert("variants".to_string(), serde_json::Value::Array(Vec::new()));
changed = true;
}
if changed {
serde_json::to_vec(&value).ok()
} else {
None
}
}