dotrix_core 0.5.2

Dotrix 3D game engine core
Documentation
use std::{
    path::Path,
    sync::{ Arc, mpsc, Mutex },
};

use gltf::{
    Gltf,
    buffer::Source,
    animation::util::ReadOutputs,
};

use log::info;

use crate::transform::Transform;
use dotrix_math::{ Mat4, Vec3, Quat };

use super::{
    animation::{ Animation, Interpolation },
    loader::{ Asset, ImportError, Response, load_image },
    mesh::Mesh,
    skin::{ Skin, JointId, Joint, JointIndex },
};

pub fn load_gltf(
    sender: &Arc<Mutex<mpsc::Sender<Response>>>,
    name: String,
    data: Vec<u8>,
    path: &Path,
) -> Result<(), ImportError>{

    let gltf = Gltf::from_slice(&data)?;
    let buffers = load_buffers(&gltf, path)?;

    for scene in gltf.scenes() {
        for node in scene.nodes() {
            load_node(sender, &name, &node, None, &buffers)?;
        }
    }

    for animation in gltf.animations() {
        load_animation(sender, &name, &animation, &buffers);
    }

    Ok(())
}

fn load_buffers(gltf: &Gltf, path: &Path) -> Result<Vec<Vec<u8>>, ImportError> {
    const URI_BASE64: &str = "data:application/octet-stream;base64,";
    let mut buffers = Vec::new();

    for buffer in gltf.buffers() {
        match buffer.source() {
            Source::Bin => {
                if let Some(blob) = gltf.blob.as_deref() {
                    buffers.push(blob.into());
                } else {
                    return Err(ImportError::Corruption("blob buffer not found"));
                }
            },
            Source::Uri(uri) => {
                buffers.push(
                    if let Some(stripped) = uri.strip_prefix(URI_BASE64) {
                        base64::decode(stripped)?
                    } else {
                        std::fs::read(path.parent().unwrap().join(uri))?
                    }
                );
            }
        }
    }

    Ok(buffers)
}

fn load_joints(
    joints: &mut Vec<Joint>,
    node: &gltf::Node,
    parent_id: Option<JointId>,
) {
    let local_transform = Transform::from(node.transform());
    let id = node.index();
    joints.push(Joint::new(
        id,
        parent_id,
        node.name().map(String::from),
        local_transform,
    ));

    for child in node.children() {
        load_joints(joints, &child, Some(id));
    }
}

fn load_node(
    sender: &Arc<Mutex<mpsc::Sender<Response>>>,
    name: &str,
    node: &gltf::Node,
    root: Option<&gltf::Node>,
    buffers: &[Vec<u8>],
) -> Result <(), ImportError> {

    if let Some(skin) = node.skin() {
        let reader = skin.reader(|buffer| Some(&buffers[buffer.index()]));
        let inverse_bind_matrices = reader
            .read_inverse_bind_matrices()
            .map(|v| v.map(Mat4::from).collect());

        let asset_name = [name, "skin"].join("::");
        let index = skin.joints().map(|j| JointIndex { id: j.index(), inverse_bind_matrix: None}).collect::<Vec<_>>();
        let mut joints: Vec<Joint> = Vec::new();
        load_joints(&mut joints, skin.skeleton().as_ref().or(root).unwrap(), None);

        info!("importing skin as `{}`", asset_name);
        sender.lock().unwrap().send(Response::Skin(
            Asset {
                name: asset_name,
                asset: Box::new(Skin::new(joints, index, inverse_bind_matrices)),
            }
        )).unwrap();
    }

    if let Some(mesh) = node.mesh() {
        for primitive in mesh.primitives() {
            load_mesh(sender, name, &primitive, buffers)?;
            let material = primitive.material();
            if let Some(texture) = material.pbr_metallic_roughness().base_color_texture() {
                load_texture(sender, name, &texture, buffers)?;
            }
        }
    }

    let root = root.or(Some(node));
    for child in node.children() {
        let child_name = if let Some(child_name) = child.name() {
            [name, child_name].join("::")
        } else {
            format!("{}.node[{}]", name, child.index())
        };


        load_node(sender, &child_name, &child, root, buffers)?;
    }

    Ok(())
}

fn load_mesh(
    sender: &Arc<Mutex<mpsc::Sender<Response>>>,
    name: &str,
    primitive: &gltf::Primitive,
    buffers: &[Vec<u8>],
) -> Result <(), ImportError> {

    let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
    let mode = primitive.mode();

    if mode != gltf::mesh::Mode::Triangles {
        return Err(ImportError::NotImplemented("primitive mode", Some(mode_to_string(mode))));
    }

    let mut mesh = Mesh::default();

    let indices = reader.read_indices().map(|i| i.into_u32().collect::<Vec<u32>>());

    if let Some(indices_ref) = indices.as_ref() {
        mesh.with_indices(indices_ref);
    }

    let positions = reader.read_positions()
        .map(|p| p.collect::<Vec<[f32; 3]>>())
        .expect("Mesh must contain some vertices positions");

    mesh.with_vertices(&positions);

    let normals = reader.read_normals()
        .map(|n| n.collect::<Vec<[f32; 3]>>())
        .unwrap_or_else(
            || Mesh::calculate_normals(&positions, indices.as_deref())
        );

    mesh.with_vertices(&normals);

    if let Some(uvs) = reader.read_tex_coords(0) {
        mesh.with_vertices(uvs.into_f32().collect::<Vec<_>>().as_slice());
    }

    if let Some(weights) = reader.read_weights(0) {
        mesh.with_vertices(weights.into_f32().collect::<Vec<[f32; 4]>>().as_slice());
    }

    if let Some(joints) = reader.read_joints(0) {
        mesh.with_vertices(joints.into_u16().collect::<Vec<[u16; 4]>>().as_slice());
    }

    let name = [name, "mesh"].join("::");

    info!("import mesh as `{}`", name);

    sender.lock().unwrap().send(Response::Mesh(
        Asset {
            name,
            asset: Box::new(mesh),
        }
    )).unwrap();

    Ok(())
}

fn load_texture(
    sender: &Arc<Mutex<mpsc::Sender<Response>>>,
    name: &str,
    texture: &gltf::texture::Info,
    buffers: &[Vec<u8>],
) -> Result <(), ImportError> {

    let source = texture.texture().source().source();
    let name = [name, "texture"].join("::");
    info!("importing texture as `{}`", name);

    let (data, format) = match source {
        gltf::image::Source::Uri { uri, mime_type } => {
            const URI_IMAGE_PNG: &str = "data:image/png;base64,";

            if !uri.starts_with(URI_IMAGE_PNG) {
                return Err(ImportError::NotImplemented("mime type",
                        mime_type.map(String::from)));
            }

            let data = base64::decode(&uri[URI_IMAGE_PNG.len()..])?;
            (data, image::ImageFormat::Png)
        },

        gltf::image::Source::View { view, mime_type } => {
            if mime_type != "image/png" {
                return Err(ImportError::NotImplemented("mime type",
                        Some(String::from(mime_type))));
            }

            let index = view.buffer().index();
            let offset = view.offset();
            let tail = offset + view.length();
            let data = &buffers[index][offset..tail];

            (data.to_vec(), image::ImageFormat::Png)
        }
    };

    load_image(sender, name, data, format)?;

    Ok(())
}

fn load_animation(
    sender: &Arc<Mutex<mpsc::Sender<Response>>>,
    name: &str,
    gltf_animation: &gltf::Animation,
    buffers: &[Vec<u8>],
) {
    let name = if let Some(animation_name) = gltf_animation.name() {
        [name, animation_name].join("::")
    } else {
        format!("{}.animation[{}]", name, gltf_animation.index())
    };

    info!("importing animation as `{}`", name);

    let mut animation = Animation::new();

    for channel in gltf_animation.channels() {

        let sampler = channel.sampler();
        let interpolation = Interpolation::from(sampler.interpolation());
        let index = channel.target().node().index();
        let reader = channel.reader(|buffer| Some(&buffers[buffer.index()]));
        let outputs = reader.read_outputs();
        let timestamps = reader.read_inputs().unwrap().collect::<Vec<f32>>();

        match outputs.unwrap() {
            ReadOutputs::Translations(output) => animation.add_translation_channel(
                index, interpolation, timestamps, output.map(Vec3::from).collect(),
            ),
            ReadOutputs::Rotations(output) => animation.add_rotation_channel(
                index, interpolation, timestamps, output.into_f32()
                    .map(|q| Quat::new(q[3], q[0], q[1], q[2])).collect()
            ),
            ReadOutputs::Scales(output) => animation.add_scale_channel(
                index, interpolation, timestamps, output.map(Vec3::from).collect()
            ),
            ReadOutputs::MorphTargetWeights(ref _weights) => (),
        };
    }

    sender.lock().unwrap().send(Response::Animation(
        Asset {
            name,
            asset: Box::new(animation),
        }
    )).unwrap();
}

fn mode_to_string(mode: gltf::mesh::Mode) -> String {
    let result = match mode {
        gltf::mesh::Mode::Points => "Points",
        gltf::mesh::Mode::Lines => "Lines",
        gltf::mesh::Mode::LineLoop => "Line Loop",
        gltf::mesh::Mode::LineStrip => "Line Strip",
        gltf::mesh::Mode::Triangles => "Triangles",
        gltf::mesh::Mode::TriangleStrip => "Triangle Strip",
        gltf::mesh::Mode::TriangleFan => "Triangle Fan",
    };
    String::from(result)
}