scena 1.7.0

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use ::gltf::accessor::{DataType, Dimensions, Iter as AccessorIter};
use ::gltf::{Accessor, Document, Node};
use serde_json::Value;

use crate::diagnostics::AssetError;
use crate::scene::{Quat, Transform, Vec3};

use super::AssetPath;
use super::buffers::ResolvedGltfBuffers;

const EXTENSION: &str = "EXT_mesh_gpu_instancing";

pub(super) fn parse_node_instance_transforms(
    path: &AssetPath,
    document: &Document,
    buffers: &ResolvedGltfBuffers,
    node: &Node<'_>,
) -> Result<Vec<Transform>, AssetError> {
    let Some(extension) = node.extension_value(EXTENSION) else {
        return Ok(Vec::new());
    };
    if node.mesh().is_none() {
        return Err(parse_error(
            path,
            format!(
                "{EXTENSION} appears on node {} without a mesh",
                node_label(node)
            ),
        ));
    }
    let attributes = extension
        .get("attributes")
        .and_then(Value::as_object)
        .ok_or_else(|| {
            parse_error(
                path,
                format!(
                    "{EXTENSION} on node {} must contain an attributes object",
                    node_label(node)
                ),
            )
        })?;
    let translations =
        read_optional_vec3_attribute(path, document, buffers, attributes, "TRANSLATION")?;
    let rotations = read_optional_rotation_attribute(path, document, buffers, attributes)?;
    let scales = read_optional_vec3_attribute(path, document, buffers, attributes, "SCALE")?;
    let count = translations
        .as_ref()
        .map(Vec::len)
        .or_else(|| rotations.as_ref().map(Vec::len))
        .or_else(|| scales.as_ref().map(Vec::len))
        .ok_or_else(|| {
            parse_error(
                path,
                format!(
                    "{EXTENSION} on node {} must provide TRANSLATION, ROTATION, or SCALE",
                    node_label(node)
                ),
            )
        })?;
    validate_attribute_count(path, node, "TRANSLATION", translations.as_ref(), count)?;
    validate_attribute_count(path, node, "ROTATION", rotations.as_ref(), count)?;
    validate_attribute_count(path, node, "SCALE", scales.as_ref(), count)?;

    Ok((0..count)
        .map(|index| Transform {
            translation: translations
                .as_ref()
                .map_or(Vec3::ZERO, |values| values[index]),
            rotation: rotations
                .as_ref()
                .map_or(Quat::IDENTITY, |values| values[index]),
            scale: scales.as_ref().map_or(Vec3::ONE, |values| values[index]),
        })
        .collect())
}

fn read_optional_vec3_attribute(
    path: &AssetPath,
    document: &Document,
    buffers: &ResolvedGltfBuffers,
    attributes: &serde_json::Map<String, Value>,
    semantic: &'static str,
) -> Result<Option<Vec<Vec3>>, AssetError> {
    let Some(index) = optional_accessor_index(path, attributes, semantic)? else {
        return Ok(None);
    };
    let accessor = accessor_by_index(path, document, index, semantic)?;
    if accessor.dimensions() != Dimensions::Vec3 || accessor.data_type() != DataType::F32 {
        return Err(parse_error(
            path,
            format!("{EXTENSION} {semantic} accessor must be FLOAT VEC3"),
        ));
    }
    let get_buffer = |buffer: ::gltf::Buffer<'_>| buffers.reader_buffer(buffer.index());
    let values = AccessorIter::<[f32; 3]>::new(accessor, get_buffer)
        .ok_or_else(|| {
            parse_error(
                path,
                format!("{EXTENSION} {semantic} accessor is unreadable"),
            )
        })?
        .map(Vec3::from_array)
        .collect();
    Ok(Some(values))
}

fn read_optional_rotation_attribute(
    path: &AssetPath,
    document: &Document,
    buffers: &ResolvedGltfBuffers,
    attributes: &serde_json::Map<String, Value>,
) -> Result<Option<Vec<Quat>>, AssetError> {
    let Some(index) = optional_accessor_index(path, attributes, "ROTATION")? else {
        return Ok(None);
    };
    let accessor = accessor_by_index(path, document, index, "ROTATION")?;
    if accessor.dimensions() != Dimensions::Vec4 {
        return Err(parse_error(
            path,
            format!("{EXTENSION} ROTATION accessor must be VEC4"),
        ));
    }
    let get_buffer = |buffer: ::gltf::Buffer<'_>| buffers.reader_buffer(buffer.index());
    let rotations = match (accessor.data_type(), accessor.normalized()) {
        (DataType::F32, _) => AccessorIter::<[f32; 4]>::new(accessor, get_buffer)
            .ok_or_else(|| {
                parse_error(path, format!("{EXTENSION} ROTATION accessor is unreadable"))
            })?
            .map(|values| quat_from_xyzw(path, values))
            .collect::<Result<Vec<_>, _>>()?,
        (DataType::I8, true) => AccessorIter::<[i8; 4]>::new(accessor, get_buffer)
            .ok_or_else(|| {
                parse_error(path, format!("{EXTENSION} ROTATION accessor is unreadable"))
            })?
            .map(|values| {
                quat_from_xyzw(
                    path,
                    [
                        normalize_i8(values[0]),
                        normalize_i8(values[1]),
                        normalize_i8(values[2]),
                        normalize_i8(values[3]),
                    ],
                )
            })
            .collect::<Result<Vec<_>, _>>()?,
        (DataType::I16, true) => AccessorIter::<[i16; 4]>::new(accessor, get_buffer)
            .ok_or_else(|| {
                parse_error(path, format!("{EXTENSION} ROTATION accessor is unreadable"))
            })?
            .map(|values| {
                quat_from_xyzw(
                    path,
                    [
                        normalize_i16(values[0]),
                        normalize_i16(values[1]),
                        normalize_i16(values[2]),
                        normalize_i16(values[3]),
                    ],
                )
            })
            .collect::<Result<Vec<_>, _>>()?,
        _ => {
            return Err(parse_error(
                path,
                format!(
                    "{EXTENSION} ROTATION accessor must be FLOAT, normalized BYTE, or normalized SHORT VEC4"
                ),
            ));
        }
    };
    Ok(Some(rotations))
}

fn optional_accessor_index(
    path: &AssetPath,
    attributes: &serde_json::Map<String, Value>,
    semantic: &'static str,
) -> Result<Option<usize>, AssetError> {
    let Some(value) = attributes.get(semantic) else {
        return Ok(None);
    };
    let Some(index) = value.as_u64() else {
        return Err(parse_error(
            path,
            format!("{EXTENSION} {semantic} attribute must be an accessor index"),
        ));
    };
    usize::try_from(index).map(Some).map_err(|_| {
        parse_error(
            path,
            format!("{EXTENSION} {semantic} accessor index is too large"),
        )
    })
}

fn accessor_by_index<'a>(
    path: &AssetPath,
    document: &'a Document,
    index: usize,
    semantic: &'static str,
) -> Result<Accessor<'a>, AssetError> {
    document.accessors().nth(index).ok_or_else(|| {
        parse_error(
            path,
            format!("{EXTENSION} {semantic} references missing accessor {index}"),
        )
    })
}

fn validate_attribute_count<T>(
    path: &AssetPath,
    node: &Node<'_>,
    semantic: &'static str,
    values: Option<&Vec<T>>,
    expected: usize,
) -> Result<(), AssetError> {
    let Some(values) = values else {
        return Ok(());
    };
    if values.len() == expected {
        return Ok(());
    }
    Err(parse_error(
        path,
        format!(
            "{EXTENSION} {semantic} accessor on node {} has count {}, expected {expected}",
            node_label(node),
            values.len()
        ),
    ))
}

fn quat_from_xyzw(path: &AssetPath, values: [f32; 4]) -> Result<Quat, AssetError> {
    let rotation = Quat::from_xyzw(values[0], values[1], values[2], values[3]);
    let length_squared = rotation.length_squared();
    if length_squared <= f32::EPSILON || !length_squared.is_finite() {
        return Err(parse_error(
            path,
            format!("{EXTENSION} ROTATION accessor contains an invalid quaternion"),
        ));
    }
    Ok(rotation.normalize())
}

fn normalize_i8(value: i8) -> f32 {
    (value as f32 / 127.0).max(-1.0)
}

fn normalize_i16(value: i16) -> f32 {
    (value as f32 / 32767.0).max(-1.0)
}

fn node_label(node: &Node<'_>) -> String {
    node.name()
        .map(str::to_string)
        .unwrap_or_else(|| format!("#{}", node.index()))
}

fn parse_error(path: &AssetPath, reason: String) -> AssetError {
    AssetError::Parse {
        path: path.as_str().to_string(),
        reason,
    }
}