use crate::{
LoadError, build_document, capture_dependency_closure, extract_source_skeleton,
has_extension_object, project_extension_facts, project_resource_facts, resolve_buffers,
source_facts_builder, topology, validate_animations, validate_document, validate_glb_framing,
};
use animsmith_core::{Document, LoadedSource, SourceFactsViewV1, SourceSetCoverageStateV1};
use serde::Serialize;
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
const GLB_MAGIC: &[u8; 4] = b"glTF";
const GLB_JSON_CHUNK: u32 = 0x4e4f_534a;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GltfContainerKind {
Gltf,
Glb,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GltfBufferSourceKind {
BinaryChunk,
DataUri,
External,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfBufferCapability {
pub buffer_index: usize,
pub source_kind: GltfBufferSourceKind,
pub declared_byte_length: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum GltfNodeRestKind {
Trs,
Matrix,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfNodeCapability {
pub node_index: usize,
pub rest_kind: GltfNodeRestKind,
pub mesh_index: Option<usize>,
pub skin_index: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfAnimationChannelCapability {
pub animation_index: usize,
pub channel_index: usize,
pub target_node_index: usize,
pub target_path: String,
pub interpolation: String,
pub input_accessor_index: usize,
pub output_accessor_index: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfAttributeCapability {
pub semantic: String,
pub accessor_index: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfPrimitiveCapability {
pub mesh_index: usize,
pub primitive_index: usize,
pub mode: u64,
pub attributes: Vec<GltfAttributeCapability>,
pub morph_target_count: usize,
pub morph_position_accessors: Vec<usize>,
#[serde(skip)]
pub unsupported_morph_locations: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfInstancingCapability {
pub node_index: usize,
pub attributes: Vec<GltfAttributeCapability>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfAccessorCapability {
pub accessor_index: usize,
pub buffer_view_index: Option<usize>,
pub byte_offset: u64,
pub component_type: u64,
pub accessor_type: String,
pub count: u64,
pub normalized: bool,
pub sparse: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfBufferViewCapability {
pub buffer_view_index: usize,
pub buffer_index: usize,
pub byte_offset: u64,
pub byte_length: u64,
pub byte_stride: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfSkinCapability {
pub skin_index: usize,
pub joint_count: usize,
pub inverse_bind_accessor_index: Option<usize>,
pub inverse_bind_count: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct GltfCapabilityManifest {
pub container: GltfContainerKind,
pub buffers: Vec<GltfBufferCapability>,
pub buffer_views: Vec<GltfBufferViewCapability>,
pub accessors: Vec<GltfAccessorCapability>,
pub nodes: Vec<GltfNodeCapability>,
pub animation_channels: Vec<GltfAnimationChannelCapability>,
pub primitives: Vec<GltfPrimitiveCapability>,
pub morph_weight_locations: Vec<String>,
pub instancing: Vec<GltfInstancingCapability>,
pub skins: Vec<GltfSkinCapability>,
pub camera_count: usize,
pub extensions: Vec<String>,
pub extension_locations: Vec<String>,
pub external_resource_locations: Vec<String>,
pub extras_locations: Vec<String>,
pub unknown_member_locations: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum GltfCapabilityViolationKind {
ExternalResource,
MorphTarget,
MorphWeights,
Camera,
Light,
Instancing,
ExtensionDeclaration,
ExtensionPayload,
Extras,
UnknownJsonMember,
NonTrianglePrimitive,
UnsupportedVertexAttribute,
SecondarySkinInfluences,
MissingInverseBinds,
EmptyInverseBindAccessor,
InverseBindCountMismatch,
UnreadableInverseBinds,
UnsafeAccessorLayout,
ConflictingAccessorUse,
OverlappingAccessorRanges,
ConflictingNodeTransform,
NonAffineNodeMatrix,
AnimatedMatrixNode,
ImagePayloadOverlap,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct GltfCapabilityViolation {
pub location: String,
pub kind: GltfCapabilityViolationKind,
}
#[derive(Debug)]
pub struct GltfScaleSource {
loaded_source: LoadedSource,
#[cfg(test)]
document_override: Option<Document>,
manifest: GltfCapabilityManifest,
source_bytes: Vec<u8>,
raw_json: Value,
resolved_buffers: Vec<Vec<u8>>,
clip_track_projection_required: bool,
}
impl GltfScaleSource {
pub fn document(&self) -> &Document {
#[cfg(test)]
if let Some(document) = self.document_override.as_ref() {
return document;
}
self.loaded_source.document()
}
pub fn source_facts(&self) -> SourceFactsViewV1<'_> {
self.loaded_source.source_facts()
}
pub fn manifest(&self) -> &GltfCapabilityManifest {
&self.manifest
}
pub fn source_bytes(&self) -> &[u8] {
&self.source_bytes
}
pub fn raw_json(&self) -> &Value {
&self.raw_json
}
pub fn resolved_buffers(&self) -> &[Vec<u8>] {
&self.resolved_buffers
}
pub const fn requires_clip_track_projection(&self) -> bool {
self.clip_track_projection_required
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum GltfScalePreflightError {
#[error(transparent)]
Load(#[from] LoadError),
#[error("glTF scale preflight rejected {count} unsupported source domain(s)")]
Unsupported {
manifest: Box<GltfCapabilityManifest>,
violations: Vec<GltfCapabilityViolation>,
count: usize,
},
}
pub fn preflight_scale_source(path: &Path) -> Result<GltfScaleSource, GltfScalePreflightError> {
let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
path: path.display().to_string(),
source,
})?;
preflight_scale_source_bytes(path, &bytes)
}
pub fn preflight_scale_source_bytes(
path: &Path,
bytes: &[u8],
) -> Result<GltfScaleSource, GltfScalePreflightError> {
capture_scale_source(path, bytes, GatePolicy::Enforce)
}
pub fn preflight_clip_track_source(
path: &Path,
) -> Result<GltfScaleSource, GltfScalePreflightError> {
let bytes = std::fs::read(path).map_err(|source| LoadError::Io {
path: path.display().to_string(),
source,
})?;
preflight_clip_track_source_bytes(path, &bytes)
}
pub fn preflight_clip_track_source_bytes(
path: &Path,
bytes: &[u8],
) -> Result<GltfScaleSource, GltfScalePreflightError> {
capture_source(path, bytes, CapturePolicy::ClipTracks)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GatePolicy {
Enforce,
#[cfg(test)]
Bypass,
}
fn capture_scale_source(
path: &Path,
bytes: &[u8],
policy: GatePolicy,
) -> Result<GltfScaleSource, GltfScalePreflightError> {
capture_source(path, bytes, CapturePolicy::Scale(policy))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CapturePolicy {
Scale(GatePolicy),
ClipTracks,
}
fn capture_source(
path: &Path,
bytes: &[u8],
policy: CapturePolicy,
) -> Result<GltfScaleSource, GltfScalePreflightError> {
validate_glb_framing(bytes)?;
let (container, json_bytes) = raw_json_bytes(bytes)?;
let raw_json: Value = serde_json::from_slice(json_bytes)
.map_err(|error| LoadError::Malformed(format!("invalid top-level JSON: {error}")))?;
if !raw_json.is_object() {
return Err(LoadError::Malformed("top-level glTF JSON is not an object".into()).into());
}
let gltf = gltf::Gltf::from_slice_without_validation(bytes).map_err(LoadError::Gltf)?;
let mut violations = Vec::new();
let manifest = inventory(&raw_json, container, &mut violations);
let accessor_uses = inspect_accessor_uses(&raw_json, &mut violations);
match validate_document(&gltf.document) {
Ok(()) => {}
Err(error) => return Err(LoadError::Gltf(error).into()),
}
validate_animations(&gltf.document)?;
let topology = topology(&gltf.document)?;
let can_resolve_buffers = !manifest
.buffers
.iter()
.any(|buffer| buffer.source_kind == GltfBufferSourceKind::External);
let resolved_buffers = if can_resolve_buffers {
resolve_buffers(&gltf, path.parent())?
} else {
Vec::new()
};
if can_resolve_buffers {
inspect_accessor_layouts(
&raw_json,
&resolved_buffers,
&accessor_uses,
&mut violations,
);
}
let (clip_track_projection_required, refuse) = match policy {
CapturePolicy::Scale(policy) => {
violations.sort();
violations.dedup();
(
false,
match policy {
GatePolicy::Enforce => !violations.is_empty(),
#[cfg(test)]
GatePolicy::Bypass => false,
},
)
}
CapturePolicy::ClipTracks => {
let animation_accessors = animation_accessor_indices(&raw_json);
let projection_required = violations
.iter()
.any(|violation| clip_track_projects_away(violation, &animation_accessors));
violations
.retain(|violation| !clip_track_projects_away(violation, &animation_accessors));
violations.sort();
violations.dedup();
(projection_required, !violations.is_empty())
}
};
if refuse {
let count = violations.len();
return Err(GltfScalePreflightError::Unsupported {
manifest: Box::new(manifest),
violations,
count,
});
}
if !clip_track_projection_required {
let loaded_source = crate::load_source_bytes(path, bytes)?;
if matches!(policy, CapturePolicy::ClipTracks)
&& !clip_track_source_facts_complete(loaded_source.source_facts())
{
return Err(LoadError::Malformed(
"clip-track raw source facts coverage is incomplete".into(),
)
.into());
}
return Ok(captured_scale_source(
loaded_source,
bytes,
manifest,
raw_json,
resolved_buffers,
false,
));
}
let mut facts = source_facts_builder(bytes).map_err(LoadError::from)?;
project_extension_facts(&gltf.document, &mut facts);
project_resource_facts(&gltf.document, &mut facts);
let has_unmodeled_extension_domain = has_extension_object(bytes)
|| gltf.document.extensions_used().next().is_some()
|| gltf.document.extensions_required().next().is_some();
let (dependency_closure, _) = capture_dependency_closure(
&facts,
None,
has_unmodeled_extension_domain,
&mut crate::read_external_file,
)?;
let source_skeleton = extract_source_skeleton(&gltf.document, &resolved_buffers, &topology);
let mut document = build_document(&gltf, &resolved_buffers, path, &topology, &mut facts)?;
document.assets.source_skeleton = source_skeleton;
let loaded_source = facts
.finish_with_dependency_closure(document, dependency_closure)
.map_err(LoadError::from)?;
if !clip_track_source_facts_complete(loaded_source.source_facts()) {
return Err(LoadError::Malformed(
"clip-track raw source facts coverage is incomplete".into(),
)
.into());
}
Ok(captured_scale_source(
loaded_source,
bytes,
manifest,
raw_json,
resolved_buffers,
clip_track_projection_required,
))
}
fn captured_scale_source(
loaded_source: LoadedSource,
source_bytes: &[u8],
manifest: GltfCapabilityManifest,
raw_json: Value,
resolved_buffers: Vec<Vec<u8>>,
clip_track_projection_required: bool,
) -> GltfScaleSource {
GltfScaleSource {
loaded_source,
#[cfg(test)]
document_override: None,
manifest,
source_bytes: source_bytes.to_vec(),
raw_json,
resolved_buffers,
clip_track_projection_required,
}
}
fn clip_track_projects_away(
violation: &GltfCapabilityViolation,
animation_accessors: &BTreeSet<usize>,
) -> bool {
use GltfCapabilityViolationKind as Kind;
match violation.kind {
Kind::MorphTarget
| Kind::MorphWeights
| Kind::NonTrianglePrimitive
| Kind::UnsupportedVertexAttribute
| Kind::SecondarySkinInfluences
| Kind::MissingInverseBinds
| Kind::EmptyInverseBindAccessor
| Kind::InverseBindCountMismatch
| Kind::UnreadableInverseBinds => true,
Kind::UnsafeAccessorLayout
| Kind::ConflictingAccessorUse
| Kind::OverlappingAccessorRanges => accessor_index_at(&violation.location)
.is_some_and(|accessor| !animation_accessors.contains(&accessor)),
Kind::ImagePayloadOverlap => true,
Kind::ExternalResource
| Kind::Camera
| Kind::Light
| Kind::Instancing
| Kind::ExtensionDeclaration
| Kind::ExtensionPayload
| Kind::Extras
| Kind::UnknownJsonMember
| Kind::ConflictingNodeTransform
| Kind::NonAffineNodeMatrix
| Kind::AnimatedMatrixNode => false,
}
}
fn animation_accessor_indices(root: &Value) -> BTreeSet<usize> {
let Some(animations) = root.get("animations").and_then(Value::as_array) else {
return BTreeSet::new();
};
animations
.iter()
.flat_map(|animation| {
animation
.get("samplers")
.and_then(Value::as_array)
.into_iter()
.flatten()
})
.flat_map(|sampler| {
[
as_index(sampler.get("input")),
as_index(sampler.get("output")),
]
})
.flatten()
.collect()
}
fn accessor_index_at(location: &str) -> Option<usize> {
location
.strip_prefix("/accessors/")?
.split('/')
.next()?
.parse()
.ok()
}
fn clip_track_source_facts_complete(source: SourceFactsViewV1<'_>) -> bool {
[
source.clips().coverage().state(),
source.constructs().coverage().state(),
source.resources().coverage().state(),
]
.into_iter()
.all(|state| state == SourceSetCoverageStateV1::Complete)
}
#[cfg(test)]
pub(crate) fn scale_source_past_the_gate(
path: &Path,
bytes: &[u8],
) -> Result<GltfScaleSource, GltfScalePreflightError> {
capture_scale_source(path, bytes, GatePolicy::Bypass)
}
#[cfg(test)]
pub(crate) fn scale_source_with_document(
mut source: GltfScaleSource,
document: Document,
) -> GltfScaleSource {
source.document_override = Some(document);
source
}
pub(crate) fn raw_json_bytes(bytes: &[u8]) -> Result<(GltfContainerKind, &[u8]), LoadError> {
if !bytes.starts_with(GLB_MAGIC) {
return Ok((GltfContainerKind::Gltf, bytes));
}
let chunk_length = bytes
.get(12..16)
.and_then(|slice| slice.try_into().ok())
.map(u32::from_le_bytes)
.ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?
as usize;
let chunk_type = bytes
.get(16..20)
.and_then(|slice| slice.try_into().ok())
.map(u32::from_le_bytes)
.ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk header".into()))?;
if chunk_type != GLB_JSON_CHUNK {
return Err(LoadError::Buffer(
"GLB first chunk is not a JSON chunk".into(),
));
}
let end = 20usize
.checked_add(chunk_length)
.ok_or_else(|| LoadError::Buffer("GLB JSON chunk range overflow".into()))?;
let json = bytes
.get(20..end)
.ok_or_else(|| LoadError::Buffer("malformed GLB JSON chunk length".into()))?;
Ok((GltfContainerKind::Glb, json))
}
fn violation(
violations: &mut Vec<GltfCapabilityViolation>,
kind: GltfCapabilityViolationKind,
location: impl Into<String>,
) {
violations.push(GltfCapabilityViolation {
kind,
location: location.into(),
});
}
fn as_index(value: Option<&Value>) -> Option<usize> {
value?.as_u64()?.try_into().ok()
}
fn inventory(
root: &Value,
container: GltfContainerKind,
violations: &mut Vec<GltfCapabilityViolation>,
) -> GltfCapabilityManifest {
let Some(object) = root.as_object() else {
return GltfCapabilityManifest {
container,
buffers: Vec::new(),
buffer_views: Vec::new(),
accessors: Vec::new(),
nodes: Vec::new(),
animation_channels: Vec::new(),
primitives: Vec::new(),
morph_weight_locations: Vec::new(),
instancing: Vec::new(),
skins: Vec::new(),
camera_count: 0,
extensions: Vec::new(),
extension_locations: Vec::new(),
external_resource_locations: Vec::new(),
extras_locations: Vec::new(),
unknown_member_locations: Vec::new(),
};
};
let mut manifest = GltfCapabilityManifest {
container,
buffers: Vec::new(),
buffer_views: Vec::new(),
accessors: Vec::new(),
nodes: Vec::new(),
animation_channels: Vec::new(),
primitives: Vec::new(),
morph_weight_locations: Vec::new(),
instancing: Vec::new(),
skins: Vec::new(),
camera_count: object
.get("cameras")
.and_then(Value::as_array)
.map_or(0, Vec::len),
extensions: Vec::new(),
extension_locations: Vec::new(),
external_resource_locations: Vec::new(),
extras_locations: Vec::new(),
unknown_member_locations: Vec::new(),
};
inspect_schema_members(root, "", &mut manifest, violations);
inventory_extensions(object, &mut manifest, violations);
inventory_buffers(object, container, &mut manifest, violations);
inventory_buffer_views_and_accessors(object, &mut manifest);
inventory_nodes(object, &mut manifest, violations);
inventory_animations(object, &mut manifest, violations);
inventory_meshes(object, &mut manifest, violations);
inventory_skins(object, &mut manifest, violations);
if manifest.camera_count > 0 {
violation(violations, GltfCapabilityViolationKind::Camera, "/cameras");
}
manifest.extensions.sort();
manifest.extensions.dedup();
manifest.extension_locations.sort();
manifest.extension_locations.dedup();
manifest.external_resource_locations.sort();
manifest.external_resource_locations.dedup();
manifest.extras_locations.sort();
manifest.extras_locations.dedup();
manifest.unknown_member_locations.sort();
manifest.unknown_member_locations.dedup();
manifest.morph_weight_locations.sort();
manifest.morph_weight_locations.dedup();
manifest
}
fn inventory_extensions(
root: &Map<String, Value>,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
for key in ["extensionsUsed", "extensionsRequired"] {
let Some(values) = root.get(key).and_then(Value::as_array) else {
continue;
};
for (index, value) in values.iter().enumerate() {
let Some(name) = value.as_str() else { continue };
manifest.extensions.push(name.to_owned());
let kind = match name {
"KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
"EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
_ => GltfCapabilityViolationKind::ExtensionDeclaration,
};
violation(violations, kind, format!("/{key}/{index}"));
}
}
}
fn inventory_buffers(
root: &Map<String, Value>,
container: GltfContainerKind,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
let Some(buffers) = root.get("buffers").and_then(Value::as_array) else {
return;
};
for (buffer_index, buffer) in buffers.iter().enumerate() {
let Some(buffer) = buffer.as_object() else {
continue;
};
let uri = buffer.get("uri").and_then(Value::as_str);
let source_kind = match uri {
Some(uri) if uri.starts_with("data:") => GltfBufferSourceKind::DataUri,
Some(_) => GltfBufferSourceKind::External,
None if container == GltfContainerKind::Glb => GltfBufferSourceKind::BinaryChunk,
None => GltfBufferSourceKind::External,
};
if source_kind == GltfBufferSourceKind::External {
manifest
.external_resource_locations
.push(format!("/buffers/{buffer_index}/uri"));
violation(
violations,
GltfCapabilityViolationKind::ExternalResource,
format!("/buffers/{buffer_index}/uri"),
);
}
manifest.buffers.push(GltfBufferCapability {
buffer_index,
source_kind,
declared_byte_length: buffer
.get("byteLength")
.and_then(Value::as_u64)
.unwrap_or(0),
});
}
if let Some(images) = root.get("images").and_then(Value::as_array) {
for (image_index, image) in images.iter().enumerate() {
if image
.get("uri")
.and_then(Value::as_str)
.is_some_and(|uri| !uri.starts_with("data:"))
{
manifest
.external_resource_locations
.push(format!("/images/{image_index}/uri"));
violation(
violations,
GltfCapabilityViolationKind::ExternalResource,
format!("/images/{image_index}/uri"),
);
}
}
}
}
fn inventory_buffer_views_and_accessors(
root: &Map<String, Value>,
manifest: &mut GltfCapabilityManifest,
) {
if let Some(buffer_views) = root.get("bufferViews").and_then(Value::as_array) {
for (buffer_view_index, view) in buffer_views.iter().enumerate() {
let Some(view) = view.as_object() else {
continue;
};
manifest.buffer_views.push(GltfBufferViewCapability {
buffer_view_index,
buffer_index: as_index(view.get("buffer")).unwrap_or(usize::MAX),
byte_offset: view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0),
byte_length: view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
byte_stride: view.get("byteStride").and_then(Value::as_u64),
});
}
}
if let Some(accessors) = root.get("accessors").and_then(Value::as_array) {
for (accessor_index, accessor) in accessors.iter().enumerate() {
let Some(accessor) = accessor.as_object() else {
continue;
};
manifest.accessors.push(GltfAccessorCapability {
accessor_index,
buffer_view_index: as_index(accessor.get("bufferView")),
byte_offset: accessor
.get("byteOffset")
.and_then(Value::as_u64)
.unwrap_or(0),
component_type: accessor
.get("componentType")
.and_then(Value::as_u64)
.unwrap_or(0),
accessor_type: accessor
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned(),
count: accessor.get("count").and_then(Value::as_u64).unwrap_or(0),
normalized: accessor
.get("normalized")
.and_then(Value::as_bool)
.unwrap_or(false),
sparse: accessor.contains_key("sparse"),
});
}
}
}
pub(crate) const AFFINE_LAST_ROW: [(usize, f64); 4] = [(3, 0.0), (7, 0.0), (11, 0.0), (15, 1.0)];
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum NodeTransformFault {
TrsBesideMatrix {
node_index: usize,
member: &'static str,
},
ProjectiveMatrixEntry {
node_index: usize,
component: usize,
value: f64,
expected: f64,
},
UnreadableMatrixEntry {
node_index: usize,
component: usize,
},
}
impl NodeTransformFault {
pub(crate) fn location(self) -> String {
match self {
Self::TrsBesideMatrix { node_index, member } => format!("/nodes/{node_index}/{member}"),
Self::ProjectiveMatrixEntry {
node_index,
component,
..
}
| Self::UnreadableMatrixEntry {
node_index,
component,
} => format!("/nodes/{node_index}/matrix/{component}"),
}
}
fn kind(self) -> GltfCapabilityViolationKind {
match self {
Self::TrsBesideMatrix { .. } => GltfCapabilityViolationKind::ConflictingNodeTransform,
Self::ProjectiveMatrixEntry { .. } | Self::UnreadableMatrixEntry { .. } => {
GltfCapabilityViolationKind::NonAffineNodeMatrix
}
}
}
}
pub(crate) fn declared<'a>(object: &'a Value, member: &str) -> Option<&'a Value> {
object.get(member).filter(|value| !value.is_null())
}
pub(crate) fn node_transform_faults(nodes: &[Value]) -> Vec<NodeTransformFault> {
let mut faults = Vec::new();
for (node_index, node) in nodes.iter().enumerate() {
let Some(matrix) = declared(node, "matrix") else {
continue;
};
for member in ["translation", "rotation", "scale"] {
if declared(node, member).is_some() {
faults.push(NodeTransformFault::TrsBesideMatrix { node_index, member });
}
}
let Some(values) = matrix.as_array().filter(|values| values.len() == 16) else {
continue;
};
for (component, expected) in AFFINE_LAST_ROW {
match values[component].as_f64() {
None => faults.push(NodeTransformFault::UnreadableMatrixEntry {
node_index,
component,
}),
Some(value) if value != expected => {
faults.push(NodeTransformFault::ProjectiveMatrixEntry {
node_index,
component,
value,
expected,
});
}
Some(_) => {}
}
}
}
faults
}
fn inventory_nodes(
root: &Map<String, Value>,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
let Some(nodes) = root.get("nodes").and_then(Value::as_array) else {
return;
};
for fault in node_transform_faults(nodes) {
violation(violations, fault.kind(), fault.location());
}
for (node_index, node) in nodes.iter().enumerate() {
if !node.is_object() {
continue;
}
if node.get("weights").is_some() {
manifest
.morph_weight_locations
.push(format!("/nodes/{node_index}/weights"));
}
if node.get("camera").is_some() {
violation(
violations,
GltfCapabilityViolationKind::Camera,
format!("/nodes/{node_index}/camera"),
);
}
if let Some(attributes) = node
.get("extensions")
.and_then(|extensions| extensions.get("EXT_mesh_gpu_instancing"))
.and_then(|extension| extension.get("attributes"))
.and_then(Value::as_object)
{
let mut attributes = attributes
.iter()
.map(|(semantic, accessor)| GltfAttributeCapability {
semantic: semantic.clone(),
accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
})
.collect::<Vec<_>>();
attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
manifest.instancing.push(GltfInstancingCapability {
node_index,
attributes,
});
}
manifest.nodes.push(GltfNodeCapability {
node_index,
rest_kind: if declared(node, "matrix").is_some() {
GltfNodeRestKind::Matrix
} else {
GltfNodeRestKind::Trs
},
mesh_index: as_index(node.get("mesh")),
skin_index: as_index(node.get("skin")),
});
}
}
fn inventory_animations(
root: &Map<String, Value>,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
let Some(animations) = root.get("animations").and_then(Value::as_array) else {
return;
};
for (animation_index, animation) in animations.iter().enumerate() {
let Some(animation) = animation.as_object() else {
continue;
};
let samplers = animation
.get("samplers")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let channels = animation
.get("channels")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for (channel_index, channel) in channels.iter().enumerate() {
let Some(channel) = channel.as_object() else {
continue;
};
let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
let Some(sampler) = samplers.get(sampler_index).and_then(Value::as_object) else {
continue;
};
let Some(target) = channel.get("target").and_then(Value::as_object) else {
continue;
};
let target_path = target
.get("path")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
if target_path == "weights" {
manifest.morph_weight_locations.push(format!(
"/animations/{animation_index}/channels/{channel_index}/target/path"
));
}
let target_node_index = as_index(target.get("node")).unwrap_or(usize::MAX);
if manifest
.nodes
.get(target_node_index)
.is_some_and(|node| node.rest_kind == GltfNodeRestKind::Matrix)
{
violation(
violations,
GltfCapabilityViolationKind::AnimatedMatrixNode,
format!("/animations/{animation_index}/channels/{channel_index}/target"),
);
}
manifest
.animation_channels
.push(GltfAnimationChannelCapability {
animation_index,
channel_index,
target_node_index,
target_path,
interpolation: sampler
.get("interpolation")
.and_then(Value::as_str)
.unwrap_or("LINEAR")
.to_owned(),
input_accessor_index: as_index(sampler.get("input")).unwrap_or(usize::MAX),
output_accessor_index: as_index(sampler.get("output")).unwrap_or(usize::MAX),
});
}
}
}
fn inventory_meshes(
root: &Map<String, Value>,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
let Some(meshes) = root.get("meshes").and_then(Value::as_array) else {
return;
};
for (mesh_index, mesh) in meshes.iter().enumerate() {
let Some(mesh) = mesh.as_object() else {
continue;
};
if mesh.contains_key("weights") {
manifest
.morph_weight_locations
.push(format!("/meshes/{mesh_index}/weights"));
}
let primitives = mesh
.get("primitives")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for (primitive_index, primitive) in primitives.iter().enumerate() {
let Some(primitive) = primitive.as_object() else {
continue;
};
let mode = primitive.get("mode").and_then(Value::as_u64).unwrap_or(4);
if mode != 4 {
violation(
violations,
GltfCapabilityViolationKind::NonTrianglePrimitive,
format!("/meshes/{mesh_index}/primitives/{primitive_index}/mode"),
);
}
let mut attributes = primitive
.get("attributes")
.and_then(Value::as_object)
.map(|attributes| {
attributes
.iter()
.map(|(semantic, accessor)| GltfAttributeCapability {
semantic: semantic.clone(),
accessor_index: as_index(Some(accessor)).unwrap_or(usize::MAX),
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
attributes.sort_by(|left, right| left.semantic.cmp(&right.semantic));
for attribute in &attributes {
let semantic = &attribute.semantic;
let semantic_pointer = json_pointer_token(semantic);
let location = format!(
"/meshes/{mesh_index}/primitives/{primitive_index}/attributes/{semantic_pointer}"
);
if is_secondary_influence(semantic) {
violation(
violations,
GltfCapabilityViolationKind::SecondarySkinInfluences,
location,
);
} else if !matches!(
semantic.as_str(),
"POSITION" | "NORMAL" | "TEXCOORD_0" | "JOINTS_0" | "WEIGHTS_0"
) {
violation(
violations,
GltfCapabilityViolationKind::UnsupportedVertexAttribute,
location,
);
}
}
let morph_target_count = primitive
.get("targets")
.and_then(Value::as_array)
.map_or(0, Vec::len);
let mut morph_position_accessors = Vec::new();
let mut unsupported_morph_locations = Vec::new();
for (target_index, target) in primitive
.get("targets")
.and_then(Value::as_array)
.into_iter()
.flatten()
.enumerate()
{
let Some(target) = target.as_object() else {
continue;
};
for (semantic, accessor) in target {
let location = format!(
"/meshes/{mesh_index}/primitives/{primitive_index}/targets/{target_index}/{}",
json_pointer_token(semantic)
);
if semantic == "POSITION" {
if let Some(accessor_index) = as_index(Some(accessor)) {
morph_position_accessors.push(accessor_index);
}
} else {
violation(
violations,
GltfCapabilityViolationKind::MorphTarget,
location.clone(),
);
unsupported_morph_locations.push(location);
}
}
}
manifest.primitives.push(GltfPrimitiveCapability {
mesh_index,
primitive_index,
mode,
attributes,
morph_target_count,
morph_position_accessors,
unsupported_morph_locations,
});
}
}
}
fn is_secondary_influence(semantic: &str) -> bool {
semantic
.strip_prefix("JOINTS_")
.or_else(|| semantic.strip_prefix("WEIGHTS_"))
.and_then(|index| index.parse::<u32>().ok())
.is_some_and(|index| index >= 1)
}
fn inventory_skins(
root: &Map<String, Value>,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
let accessors = root
.get("accessors")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let Some(skins) = root.get("skins").and_then(Value::as_array) else {
return;
};
for (skin_index, skin) in skins.iter().enumerate() {
let Some(skin) = skin.as_object() else {
continue;
};
let joint_count = skin
.get("joints")
.and_then(Value::as_array)
.map_or(0, Vec::len);
let inverse_bind_accessor_index = as_index(skin.get("inverseBindMatrices"));
let inverse_bind_count = inverse_bind_accessor_index
.and_then(|index| accessors.get(index))
.and_then(|accessor| accessor.get("count"))
.and_then(Value::as_u64);
let inverse_bind_readable = inverse_bind_accessor_index
.and_then(|index| accessors.get(index))
.and_then(Value::as_object)
.is_some_and(|accessor| {
accessor.get("bufferView").and_then(Value::as_u64).is_some()
&& accessor.get("componentType").and_then(Value::as_u64) == Some(5126)
&& accessor.get("type").and_then(Value::as_str) == Some("MAT4")
&& !accessor.contains_key("sparse")
});
match (inverse_bind_accessor_index, inverse_bind_count) {
(None, _) => violation(
violations,
GltfCapabilityViolationKind::MissingInverseBinds,
format!("/skins/{skin_index}/inverseBindMatrices"),
),
(Some(_), Some(0)) => violation(
violations,
GltfCapabilityViolationKind::EmptyInverseBindAccessor,
format!("/skins/{skin_index}/inverseBindMatrices"),
),
(Some(_), Some(count)) if count != joint_count as u64 => violation(
violations,
GltfCapabilityViolationKind::InverseBindCountMismatch,
format!("/skins/{skin_index}/inverseBindMatrices"),
),
(Some(_), _) if !inverse_bind_readable => violation(
violations,
GltfCapabilityViolationKind::UnreadableInverseBinds,
format!("/skins/{skin_index}/inverseBindMatrices"),
),
_ => {}
}
manifest.skins.push(GltfSkinCapability {
skin_index,
joint_count,
inverse_bind_accessor_index,
inverse_bind_count,
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum AccessorUse {
ScaleBearing,
Dimensionless,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum RangeOwner {
Accessor(usize),
SparseIndices(usize),
SparseValues(usize),
ImagePayload(usize),
}
impl RangeOwner {
fn location(self) -> String {
match self {
Self::Accessor(index) => format!("/accessors/{index}"),
Self::SparseIndices(index) => {
format!("/accessors/{index}/sparse/indices/bufferView")
}
Self::SparseValues(index) => {
format!("/accessors/{index}/sparse/values/bufferView")
}
Self::ImagePayload(index) => format!("/images/{index}/bufferView"),
}
}
fn overlap_kind(self) -> GltfCapabilityViolationKind {
match self {
Self::Accessor(_) | Self::SparseIndices(_) | Self::SparseValues(_) => {
GltfCapabilityViolationKind::OverlappingAccessorRanges
}
Self::ImagePayload(_) => GltfCapabilityViolationKind::ImagePayloadOverlap,
}
}
}
type OwnedRange = (usize, usize, usize, RangeOwner, bool);
fn inspect_accessor_layouts(
root: &Value,
buffers: &[Vec<u8>],
uses: &BTreeMap<usize, BTreeSet<AccessorUse>>,
violations: &mut Vec<GltfCapabilityViolation>,
) {
let Some(root) = root.as_object() else { return };
let mut ranges: Vec<OwnedRange> = Vec::new();
let accessors = root
.get("accessors")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
for accessor_index in 0..accessors.len() {
let accessor_uses = uses.get(&accessor_index);
let scale_bearing =
accessor_uses.is_some_and(|uses| uses.contains(&AccessorUse::ScaleBearing));
let accessor_ranges = if scale_bearing {
dense_f32_accessor_range(root, buffers, accessor_index).map(|range| {
vec![(
range.0,
range.1,
range.2,
RangeOwner::Accessor(accessor_index),
true,
)]
})
} else if accessor_uses.is_some() {
accessor_range(root, buffers, accessor_index).map(|range| {
vec![(
range.buffer,
range.start,
range.end,
RangeOwner::Accessor(accessor_index),
false,
)]
})
} else {
preserved_accessor_ranges(root, buffers, accessor_index)
};
match accessor_ranges {
Some(accessor_ranges) => ranges.extend(accessor_ranges),
None => violation(
violations,
GltfCapabilityViolationKind::UnsafeAccessorLayout,
format!("/accessors/{accessor_index}"),
),
}
}
ranges.extend(image_payload_ranges(root));
ranges.sort_unstable();
let mut overlapping = BTreeSet::new();
let mut prior_scale: Option<(usize, usize, RangeOwner)> = None;
for &(buffer, start, end, owner, scale_bearing) in &ranges {
if let Some((left_buffer, left_end, left_owner)) = prior_scale
&& left_buffer == buffer
&& start < left_end
{
overlapping.insert(left_owner);
overlapping.insert(owner);
}
if scale_bearing
&& prior_scale
.is_none_or(|(left_buffer, left_end, _)| left_buffer != buffer || end > left_end)
{
prior_scale = Some((buffer, end, owner));
}
}
let mut later_scale: Option<(usize, usize, RangeOwner)> = None;
for &(buffer, start, end, owner, scale_bearing) in ranges.iter().rev() {
if let Some((right_buffer, right_start, right_owner)) = later_scale
&& right_buffer == buffer
&& right_start < end
{
overlapping.insert(owner);
overlapping.insert(right_owner);
}
if scale_bearing
&& later_scale.is_none_or(|(right_buffer, right_start, _)| {
right_buffer != buffer || start < right_start
})
{
later_scale = Some((buffer, start, owner));
}
}
for owner in overlapping {
violation(violations, owner.overlap_kind(), owner.location());
}
}
fn preserved_accessor_ranges(
root: &Map<String, Value>,
buffers: &[Vec<u8>],
accessor_index: usize,
) -> Option<Vec<OwnedRange>> {
let accessor = root
.get("accessors")?
.as_array()?
.get(accessor_index)?
.as_object()?;
let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
if count == 0 {
return Some(Vec::new());
}
let Some(sparse) = accessor.get("sparse") else {
let range = accessor_range(root, buffers, accessor_index)?;
return Some(vec![(
range.buffer,
range.start,
range.end,
RangeOwner::Accessor(accessor_index),
false,
)]);
};
let mut ranges = Vec::with_capacity(3);
if accessor.get("bufferView").is_some() {
let range = dense_accessor_range(root, buffers, accessor_index)?;
ranges.push((
range.buffer,
range.start,
range.end,
RangeOwner::Accessor(accessor_index),
false,
));
}
let sparse = sparse.as_object()?;
let sparse_count: usize = sparse.get("count")?.as_u64()?.try_into().ok()?;
if sparse_count == 0 {
return Some(ranges);
}
let indices = sparse.get("indices")?.as_object()?;
let index_size = match indices.get("componentType")?.as_u64()? {
5121 => 1,
5123 => 2,
5125 => 4,
_ => return None,
};
let indices_range = packed_view_range(
root,
buffers,
as_index(indices.get("bufferView"))?,
indices
.get("byteOffset")
.and_then(Value::as_u64)
.unwrap_or(0),
sparse_count,
index_size,
index_size,
)?;
ranges.push((
indices_range.0,
indices_range.1,
indices_range.2,
RangeOwner::SparseIndices(accessor_index),
false,
));
let values = sparse.get("values")?.as_object()?;
let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
let values_range = packed_view_range(
root,
buffers,
as_index(values.get("bufferView"))?,
values
.get("byteOffset")
.and_then(Value::as_u64)
.unwrap_or(0),
sparse_count,
element_layout.stride,
element_layout.terminal_size,
)?;
ranges.push((
values_range.0,
values_range.1,
values_range.2,
RangeOwner::SparseValues(accessor_index),
false,
));
Some(ranges)
}
fn packed_view_range(
root: &Map<String, Value>,
buffers: &[Vec<u8>],
view_index: usize,
relative_offset: u64,
count: usize,
element_stride: usize,
terminal_size: usize,
) -> Option<(usize, usize, usize)> {
let view = root
.get("bufferViews")?
.as_array()?
.get(view_index)?
.as_object()?;
let buffer_index = as_index(view.get("buffer"))?;
let buffer = buffers.get(buffer_index)?;
let view_offset: usize = view
.get("byteOffset")
.and_then(Value::as_u64)
.unwrap_or(0)
.try_into()
.ok()?;
let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
if view_offset.checked_add(view_length)? > buffer.len() {
return None;
}
let relative_offset: usize = relative_offset.try_into().ok()?;
let relative_end = relative_offset
.checked_add(count.checked_sub(1)?.checked_mul(element_stride)?)?
.checked_add(terminal_size)?;
if relative_end > view_length {
return None;
}
let start = view_offset.checked_add(relative_offset)?;
let end = view_offset.checked_add(relative_end)?;
Some((buffer_index, start, end))
}
fn image_payload_ranges(root: &Map<String, Value>) -> Vec<OwnedRange> {
let Some(images) = root.get("images").and_then(Value::as_array) else {
return Vec::new();
};
let buffer_views = root
.get("bufferViews")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let mut out = Vec::new();
for (image_index, image) in images.iter().enumerate() {
let Some(view_index) = as_index(image.get("bufferView")) else {
continue;
};
let Some(view) = buffer_views.get(view_index).and_then(Value::as_object) else {
continue;
};
let Some(buffer) = as_index(view.get("buffer")) else {
continue;
};
let start = clamped_usize(view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0));
let end = start.saturating_add(clamped_usize(
view.get("byteLength").and_then(Value::as_u64).unwrap_or(0),
));
if start < end {
out.push((
buffer,
start,
end,
RangeOwner::ImagePayload(image_index),
false,
));
}
}
out
}
fn clamped_usize(value: u64) -> usize {
usize::try_from(value).unwrap_or(usize::MAX)
}
fn inspect_accessor_uses(
root: &Value,
violations: &mut Vec<GltfCapabilityViolation>,
) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
let Some(root) = root.as_object() else {
return BTreeMap::new();
};
let uses = collect_accessor_uses(root);
for (accessor_index, accessor_uses) in &uses {
if accessor_uses.len() > 1 {
violation(
violations,
GltfCapabilityViolationKind::ConflictingAccessorUse,
format!("/accessors/{accessor_index}"),
);
}
}
uses
}
fn collect_accessor_uses(root: &Map<String, Value>) -> BTreeMap<usize, BTreeSet<AccessorUse>> {
let mut uses: BTreeMap<usize, BTreeSet<AccessorUse>> = BTreeMap::new();
let mut add = |index: Option<usize>, kind| {
if let Some(index) = index {
uses.entry(index).or_default().insert(kind);
}
};
if let Some(meshes) = root.get("meshes").and_then(Value::as_array) {
for mesh in meshes {
let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
continue;
};
for primitive in primitives {
if let Some(attributes) = primitive.get("attributes").and_then(Value::as_object) {
for (semantic, index) in attributes {
add(
as_index(Some(index)),
if semantic == "POSITION" {
AccessorUse::ScaleBearing
} else {
AccessorUse::Dimensionless
},
);
}
}
add(
as_index(primitive.get("indices")),
AccessorUse::Dimensionless,
);
if let Some(targets) = primitive.get("targets").and_then(Value::as_array) {
for target in targets {
if let Some(target) = target.as_object() {
for (semantic, index) in target {
add(
as_index(Some(index)),
if semantic == "POSITION" {
AccessorUse::ScaleBearing
} else {
AccessorUse::Dimensionless
},
);
}
}
}
}
}
}
}
if let Some(skins) = root.get("skins").and_then(Value::as_array) {
for skin in skins {
add(
as_index(skin.get("inverseBindMatrices")),
AccessorUse::ScaleBearing,
);
}
}
if let Some(animations) = root.get("animations").and_then(Value::as_array) {
for animation in animations {
let samplers = animation
.get("samplers")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let channels = animation
.get("channels")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default();
let referenced: BTreeSet<usize> = channels
.iter()
.filter_map(|channel| as_index(channel.get("sampler")))
.collect();
for (sampler_index, sampler) in samplers.iter().enumerate() {
add(as_index(sampler.get("input")), AccessorUse::Dimensionless);
if !referenced.contains(&sampler_index) {
add(as_index(sampler.get("output")), AccessorUse::Dimensionless);
}
}
for channel in channels {
let sampler_index = as_index(channel.get("sampler")).unwrap_or(usize::MAX);
let Some(sampler) = samplers.get(sampler_index) else {
continue;
};
let path = channel
.get("target")
.and_then(|target| target.get("path"))
.and_then(Value::as_str);
add(
as_index(sampler.get("output")),
if path == Some("translation") {
AccessorUse::ScaleBearing
} else {
AccessorUse::Dimensionless
},
);
}
}
}
uses
}
pub(crate) fn dense_f32_accessor_range(
root: &Map<String, Value>,
buffers: &[Vec<u8>],
accessor_index: usize,
) -> Option<(usize, usize, usize)> {
let accessors = root.get("accessors")?.as_array()?;
let accessor = accessors.get(accessor_index)?.as_object()?;
if accessor.get("componentType")?.as_u64()? != 5126
|| accessor.get("normalized").and_then(Value::as_bool) == Some(true)
|| accessor.contains_key("sparse")
{
return None;
}
let range = accessor_range(root, buffers, accessor_index)?;
if range.stride != range.element_stride || !range.start.is_multiple_of(4) {
return None;
}
Some((range.buffer, range.start, range.end))
}
pub(crate) fn resolved_accessor_range(
root: &Map<String, Value>,
buffers: &[Vec<u8>],
accessor_index: usize,
) -> Option<(usize, usize, usize)> {
accessor_range(root, buffers, accessor_index)
.map(|range| (range.buffer, range.start, range.end))
}
#[derive(Debug, Clone, Copy)]
struct AccessorRange {
buffer: usize,
start: usize,
end: usize,
stride: usize,
element_stride: usize,
}
fn accessor_range(
root: &Map<String, Value>,
buffers: &[Vec<u8>],
accessor_index: usize,
) -> Option<AccessorRange> {
let accessor = root
.get("accessors")?
.as_array()?
.get(accessor_index)?
.as_object()?;
if accessor.contains_key("sparse") {
return None;
}
dense_accessor_range(root, buffers, accessor_index)
}
fn dense_accessor_range(
root: &Map<String, Value>,
buffers: &[Vec<u8>],
accessor_index: usize,
) -> Option<AccessorRange> {
let accessors = root.get("accessors")?.as_array()?;
let buffer_views = root.get("bufferViews")?.as_array()?;
let accessor = accessors.get(accessor_index)?.as_object()?;
let component_size = component_size(accessor.get("componentType")?.as_u64()?)?;
let element_layout = accessor_element_layout(accessor.get("type")?.as_str()?, component_size)?;
let count: usize = accessor.get("count")?.as_u64()?.try_into().ok()?;
if count == 0 {
return None;
}
let view_index = as_index(accessor.get("bufferView"))?;
let view = buffer_views.get(view_index)?.as_object()?;
let buffer_index = as_index(view.get("buffer"))?;
let buffer = buffers.get(buffer_index)?;
let view_offset: usize = view
.get("byteOffset")
.and_then(Value::as_u64)
.unwrap_or(0)
.try_into()
.ok()?;
let view_length: usize = view.get("byteLength")?.as_u64()?.try_into().ok()?;
if view_offset.checked_add(view_length)? > buffer.len() {
return None;
}
let accessor_offset: usize = accessor
.get("byteOffset")
.and_then(Value::as_u64)
.unwrap_or(0)
.try_into()
.ok()?;
let stride: usize = view
.get("byteStride")
.and_then(Value::as_u64)
.unwrap_or(element_layout.stride as u64)
.try_into()
.ok()?;
if stride < element_layout.stride {
return None;
}
let relative_end = accessor_offset
.checked_add(count.checked_sub(1)?.checked_mul(stride)?)?
.checked_add(element_layout.terminal_size)?;
if relative_end > view_length {
return None;
}
let start = view_offset.checked_add(accessor_offset)?;
let end = view_offset.checked_add(relative_end)?;
(end <= buffer.len()).then_some(AccessorRange {
buffer: buffer_index,
start,
end,
stride,
element_stride: element_layout.stride,
})
}
fn component_size(component_type: u64) -> Option<usize> {
match component_type {
5120 | 5121 => Some(1),
5122 | 5123 => Some(2),
5125 | 5126 => Some(4),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
struct AccessorElementLayout {
stride: usize,
terminal_size: usize,
}
fn accessor_element_layout(
accessor_type: &str,
component_size: usize,
) -> Option<AccessorElementLayout> {
let (columns, rows, matrix) = match accessor_type {
"SCALAR" => (1usize, 1usize, false),
"VEC2" => (1, 2, false),
"VEC3" => (1, 3, false),
"VEC4" => (1, 4, false),
"MAT2" => (2, 2, true),
"MAT3" => (3, 3, true),
"MAT4" => (4, 4, true),
_ => return None,
};
let column_size = rows.checked_mul(component_size)?;
let stored_column_size = if matrix {
column_size.checked_add(3)? & !3
} else {
column_size
};
let stride = columns.checked_mul(stored_column_size)?;
let terminal_size = columns
.checked_sub(1)?
.checked_mul(stored_column_size)?
.checked_add(column_size)?;
Some(AccessorElementLayout {
stride,
terminal_size,
})
}
fn inspect_schema_members(
value: &Value,
pointer: &str,
manifest: &mut GltfCapabilityManifest,
violations: &mut Vec<GltfCapabilityViolation>,
) {
match value {
Value::Object(object) => {
if object.get("extras").is_some_and(|value| !value.is_null()) {
let location = format!("{pointer}/extras");
manifest.extras_locations.push(location.clone());
violation(violations, GltfCapabilityViolationKind::Extras, location);
}
if let Some(extensions) = object.get("extensions").and_then(Value::as_object) {
for name in extensions.keys() {
let location = json_pointer_child(&format!("{pointer}/extensions"), name);
manifest.extensions.push(name.clone());
manifest.extension_locations.push(location.clone());
violation(
violations,
match name.as_str() {
"KHR_lights_punctual" => GltfCapabilityViolationKind::Light,
"EXT_mesh_gpu_instancing" => GltfCapabilityViolationKind::Instancing,
_ => GltfCapabilityViolationKind::ExtensionPayload,
},
location,
);
}
}
if let Some(allowed) = allowed_members(pointer) {
for key in object.keys() {
if !allowed.contains(&key.as_str()) {
let location = json_pointer_child(pointer, key);
manifest.unknown_member_locations.push(location.clone());
violation(
violations,
GltfCapabilityViolationKind::UnknownJsonMember,
location,
);
}
}
}
for (key, child) in object {
if key == "extras" || key == "extensions" {
continue;
}
inspect_schema_members(
child,
&json_pointer_child(pointer, key),
manifest,
violations,
);
}
}
Value::Array(values) => {
for (index, child) in values.iter().enumerate() {
inspect_schema_members(child, &format!("{pointer}/{index}"), manifest, violations);
}
}
_ => {}
}
}
fn json_pointer_child(pointer: &str, token: &str) -> String {
format!("{pointer}/{}", json_pointer_token(token))
}
fn json_pointer_token(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
fn allowed_members(pointer: &str) -> Option<&'static [&'static str]> {
const ROOT: &[&str] = &[
"accessors",
"animations",
"asset",
"buffers",
"bufferViews",
"cameras",
"extensions",
"extensionsRequired",
"extensionsUsed",
"extras",
"images",
"materials",
"meshes",
"nodes",
"samplers",
"scene",
"scenes",
"skins",
"textures",
];
const ASSET: &[&str] = &[
"copyright",
"extensions",
"extras",
"generator",
"minVersion",
"version",
];
const ACCESSOR: &[&str] = &[
"bufferView",
"byteOffset",
"componentType",
"count",
"extensions",
"extras",
"max",
"min",
"name",
"normalized",
"sparse",
"type",
];
const BUFFER: &[&str] = &["byteLength", "extensions", "extras", "name", "uri"];
const VIEW: &[&str] = &[
"buffer",
"byteLength",
"byteOffset",
"byteStride",
"extensions",
"extras",
"name",
"target",
];
const NODE: &[&str] = &[
"camera",
"children",
"extensions",
"extras",
"matrix",
"mesh",
"name",
"rotation",
"scale",
"skin",
"translation",
"weights",
];
const MESH: &[&str] = &["extensions", "extras", "name", "primitives", "weights"];
const PRIMITIVE: &[&str] = &[
"attributes",
"extensions",
"extras",
"indices",
"material",
"mode",
"targets",
];
const ANIMATION: &[&str] = &["channels", "extensions", "extras", "name", "samplers"];
const CHANNEL: &[&str] = &["extensions", "extras", "sampler", "target"];
const TARGET: &[&str] = &["extensions", "extras", "node", "path"];
const ANIM_SAMPLER: &[&str] = &["extensions", "extras", "input", "interpolation", "output"];
const SKIN: &[&str] = &[
"extensions",
"extras",
"inverseBindMatrices",
"joints",
"name",
"skeleton",
];
const SCENE: &[&str] = &["extensions", "extras", "name", "nodes"];
const IMAGE: &[&str] = &[
"bufferView",
"extensions",
"extras",
"mimeType",
"name",
"uri",
];
const TEXTURE: &[&str] = &["extensions", "extras", "name", "sampler", "source"];
const SAMPLER: &[&str] = &[
"extensions",
"extras",
"magFilter",
"minFilter",
"name",
"wrapS",
"wrapT",
];
const CAMERA: &[&str] = &[
"extensions",
"extras",
"name",
"orthographic",
"perspective",
"type",
];
const MATERIAL: &[&str] = &[
"alphaCutoff",
"alphaMode",
"doubleSided",
"emissiveFactor",
"emissiveTexture",
"extensions",
"extras",
"name",
"normalTexture",
"occlusionTexture",
"pbrMetallicRoughness",
];
const PBR: &[&str] = &[
"baseColorFactor",
"baseColorTexture",
"extensions",
"extras",
"metallicFactor",
"metallicRoughnessTexture",
"roughnessFactor",
];
const TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "texCoord"];
const NORMAL_TEXTURE_INFO: &[&str] = &["extensions", "extras", "index", "scale", "texCoord"];
const OCCLUSION_TEXTURE_INFO: &[&str] =
&["extensions", "extras", "index", "strength", "texCoord"];
const PERSPECTIVE: &[&str] = &[
"aspectRatio",
"extensions",
"extras",
"yfov",
"zfar",
"znear",
];
const ORTHOGRAPHIC: &[&str] = &["extensions", "extras", "xmag", "ymag", "zfar", "znear"];
const SPARSE: &[&str] = &["count", "extensions", "extras", "indices", "values"];
const SPARSE_INDICES: &[&str] = &[
"bufferView",
"byteOffset",
"componentType",
"extensions",
"extras",
];
const SPARSE_VALUES: &[&str] = &["bufferView", "byteOffset", "extensions", "extras"];
if pointer.is_empty() {
Some(ROOT)
} else if pointer == "/asset" {
Some(ASSET)
} else if indexed_member(pointer, "/accessors/") {
Some(ACCESSOR)
} else if indexed_member(pointer, "/buffers/") {
Some(BUFFER)
} else if indexed_member(pointer, "/bufferViews/") {
Some(VIEW)
} else if indexed_member(pointer, "/nodes/") {
Some(NODE)
} else if indexed_member(pointer, "/meshes/") {
Some(MESH)
} else if indexed_nested_member(pointer, "/meshes/", "/primitives/") {
Some(PRIMITIVE)
} else if indexed_member(pointer, "/animations/") {
Some(ANIMATION)
} else if indexed_nested_member(pointer, "/animations/", "/channels/") {
Some(CHANNEL)
} else if pointer.contains("/animations/") && pointer.ends_with("/target") {
Some(TARGET)
} else if indexed_nested_member(pointer, "/animations/", "/samplers/") {
Some(ANIM_SAMPLER)
} else if indexed_member(pointer, "/skins/") {
Some(SKIN)
} else if indexed_member(pointer, "/scenes/") {
Some(SCENE)
} else if indexed_member(pointer, "/images/") {
Some(IMAGE)
} else if indexed_member(pointer, "/textures/") {
Some(TEXTURE)
} else if indexed_member(pointer, "/samplers/") {
Some(SAMPLER)
} else if indexed_member(pointer, "/cameras/") {
Some(CAMERA)
} else if indexed_member(pointer, "/materials/") {
Some(MATERIAL)
} else if pointer.contains("/materials/") && pointer.ends_with("/pbrMetallicRoughness") {
Some(PBR)
} else if pointer.contains("/materials/")
&& (pointer.ends_with("/baseColorTexture")
|| pointer.ends_with("/metallicRoughnessTexture")
|| pointer.ends_with("/emissiveTexture"))
{
Some(TEXTURE_INFO)
} else if pointer.contains("/materials/") && pointer.ends_with("/normalTexture") {
Some(NORMAL_TEXTURE_INFO)
} else if pointer.contains("/materials/") && pointer.ends_with("/occlusionTexture") {
Some(OCCLUSION_TEXTURE_INFO)
} else if pointer.contains("/cameras/") && pointer.ends_with("/perspective") {
Some(PERSPECTIVE)
} else if pointer.contains("/cameras/") && pointer.ends_with("/orthographic") {
Some(ORTHOGRAPHIC)
} else if pointer.contains("/accessors/") && pointer.ends_with("/sparse") {
Some(SPARSE)
} else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/indices") {
Some(SPARSE_INDICES)
} else if pointer.contains("/accessors/") && pointer.ends_with("/sparse/values") {
Some(SPARSE_VALUES)
} else {
None
}
}
fn indexed_member(pointer: &str, prefix: &str) -> bool {
pointer
.strip_prefix(prefix)
.is_some_and(|suffix| !suffix.is_empty() && !suffix.contains('/'))
}
fn indexed_nested_member(pointer: &str, prefix: &str, nested: &str) -> bool {
let Some(suffix) = pointer.strip_prefix(prefix) else {
return false;
};
let Some((outer, inner)) = suffix.split_once(nested) else {
return false;
};
!outer.is_empty() && !outer.contains('/') && !inner.is_empty() && !inner.contains('/')
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn accessor_use_inventory_claims_orphan_sampler_fields_without_self_conflicting_channels() {
let root = json!({
"animations": [{
"samplers": [
{ "input": 1, "output": 2 },
{ "input": 3, "output": 4 }
],
"channels": [{
"sampler": 0,
"target": { "node": 0, "path": "translation" }
}]
}]
});
let uses = collect_accessor_uses(root.as_object().expect("root"));
assert_eq!(uses[&1], BTreeSet::from([AccessorUse::Dimensionless]));
assert_eq!(uses[&2], BTreeSet::from([AccessorUse::ScaleBearing]));
assert_eq!(uses[&3], BTreeSet::from([AccessorUse::Dimensionless]));
assert_eq!(uses[&4], BTreeSet::from([AccessorUse::Dimensionless]));
}
}