From 15b61fe269ff757fb9036282ed7442ff9282b186 Mon Sep 17 00:00:00 2001 From: b-guild Date: Tue, 2 Apr 2024 21:46:09 -0700 Subject: [PATCH] Adding the first version of the glTF loader. --- fyrox-animation/src/lib.rs | 4 +- fyrox-animation/src/value.rs | 13 +- fyrox-dylib/Cargo.toml | 5 + fyrox-impl/Cargo.toml | 4 + fyrox-impl/src/engine/mod.rs | 9 + fyrox-impl/src/resource/gltf/animation.rs | 479 ++++++++++++ .../src/resource/gltf/gltf_standard.shader | 632 +++++++++++++++ fyrox-impl/src/resource/gltf/iter.rs | 118 +++ fyrox-impl/src/resource/gltf/material.rs | 433 ++++++++++ fyrox-impl/src/resource/gltf/mod.rs | 740 ++++++++++++++++++ fyrox-impl/src/resource/gltf/node_names.rs | 192 +++++ fyrox-impl/src/resource/gltf/surface.rs | 554 +++++++++++++ fyrox-impl/src/resource/gltf/uri.rs | 182 +++++ fyrox-impl/src/resource/mod.rs | 2 + fyrox-impl/src/utils/astar.rs | 4 +- fyrox/Cargo.toml | 3 + 16 files changed, 3368 insertions(+), 6 deletions(-) create mode 100644 fyrox-impl/src/resource/gltf/animation.rs create mode 100644 fyrox-impl/src/resource/gltf/gltf_standard.shader create mode 100644 fyrox-impl/src/resource/gltf/iter.rs create mode 100644 fyrox-impl/src/resource/gltf/material.rs create mode 100644 fyrox-impl/src/resource/gltf/mod.rs create mode 100644 fyrox-impl/src/resource/gltf/node_names.rs create mode 100644 fyrox-impl/src/resource/gltf/surface.rs create mode 100644 fyrox-impl/src/resource/gltf/uri.rs diff --git a/fyrox-animation/src/lib.rs b/fyrox-animation/src/lib.rs index 84a71f824..d4ac459a9 100644 --- a/fyrox-animation/src/lib.rs +++ b/fyrox-animation/src/lib.rs @@ -28,7 +28,7 @@ use fyrox_core::uuid::uuid; pub use pose::{AnimationPose, NodePose}; pub use signal::{AnimationEvent, AnimationSignal}; -use value::{TrackValue, ValueBinding}; +use value::{nlerp, TrackValue, ValueBinding}; pub mod container; pub mod machine; @@ -243,7 +243,7 @@ impl RootMotion { /// Blend this motion with some other using `weight` as a proportion. pub fn blend_with(&mut self, other: &RootMotion, weight: f32) { self.delta_position = self.delta_position.lerp(&other.delta_position, weight); - self.delta_rotation = self.delta_rotation.nlerp(&other.delta_rotation, weight); + self.delta_rotation = nlerp(self.delta_rotation, &other.delta_rotation, weight); } } diff --git a/fyrox-animation/src/value.rs b/fyrox-animation/src/value.rs index 21125a991..d69a0bed1 100644 --- a/fyrox-animation/src/value.rs +++ b/fyrox-animation/src/value.rs @@ -2,7 +2,7 @@ //! for more info. use crate::core::{ - algebra::{UnitQuaternion, Vector2, Vector3, Vector4}, + algebra::{Unit, UnitQuaternion, Vector2, Vector3, Vector4}, math::lerpf, num_traits::AsPrimitive, reflect::prelude::*, @@ -148,7 +148,7 @@ impl TrackValue { (Self::Vector2(a), Self::Vector2(b)) => *a = a.lerp(b, weight), (Self::Vector3(a), Self::Vector3(b)) => *a = a.lerp(b, weight), (Self::Vector4(a), Self::Vector4(b)) => *a = a.lerp(b, weight), - (Self::UnitQuaternion(a), Self::UnitQuaternion(b)) => *a = a.nlerp(b, weight), + (Self::UnitQuaternion(a), Self::UnitQuaternion(b)) => *a = nlerp(*a, &b, weight), _ => (), } } @@ -357,3 +357,12 @@ impl BoundValueCollection { } } } + +/// Interpolates from `a` to `b` using nlerp, including an additional check to ensure +/// that the a.dot(b) is positive to prevent the interpolation from going around the long way. +pub fn nlerp(mut a: UnitQuaternion, b: &UnitQuaternion, w: f32) -> UnitQuaternion { + if a.dot(b) < 0.0 { + a = Unit::new_unchecked(-a.as_ref()); + } + a.nlerp(b, w) +} diff --git a/fyrox-dylib/Cargo.toml b/fyrox-dylib/Cargo.toml index c26e492cb..c9c765c42 100644 --- a/fyrox-dylib/Cargo.toml +++ b/fyrox-dylib/Cargo.toml @@ -6,5 +6,10 @@ edition = "2021" [lib] crate-type = ["dylib"] +[features] +gltf = ["fyrox-impl/gltf"] +mesh_analysis = ["fyrox-impl/mesh_analysis"] +gltf_blend_shapes = ["fyrox-impl/gltf_blend_shapes"] + [dependencies] fyrox-impl = { path = "../fyrox-impl", version = "0.33.1" } \ No newline at end of file diff --git a/fyrox-impl/Cargo.toml b/fyrox-impl/Cargo.toml index 7f70d405c..33dbf22c8 100644 --- a/fyrox-impl/Cargo.toml +++ b/fyrox-impl/Cargo.toml @@ -46,6 +46,8 @@ base64 = "0.21.0" uvgen = "0.1.0" lightmap = "0.1.1" libloading = "0.8.1" +serde_json = { version="1.0", optional=true } +gltf = { version = "1.4.0", optional = true, default-features = false, features = ["names", "utils"] } # This dependency isn't actually used by the engine, but it is needed to prevent cargo from rebuilding # the engine lib on different packages. @@ -53,6 +55,8 @@ hashbrown = { version = "0.14.3", features = ["raw"] } [features] enable_profiler = ["fyrox-core/enable_profiler"] +gltf_blend_shapes = ["gltf", "gltf/extras", "dep:serde_json"] +mesh_analysis = [] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] glutin = "0.31" diff --git a/fyrox-impl/src/engine/mod.rs b/fyrox-impl/src/engine/mod.rs index 861a1622c..263bad0ae 100644 --- a/fyrox-impl/src/engine/mod.rs +++ b/fyrox-impl/src/engine/mod.rs @@ -1148,6 +1148,15 @@ pub(crate) fn initialize_resource_manager_loaders( let mut state = resource_manager.state(); + #[cfg(feature = "gltf")] + { + let gltf_loader = super::resource::gltf::GltfLoader { + resource_manager: resource_manager.clone(), + default_import_options: Default::default(), + }; + state.loaders.set(gltf_loader); + } + for shader in ShaderResource::standard_shaders() { state .built_in_resources diff --git a/fyrox-impl/src/resource/gltf/animation.rs b/fyrox-impl/src/resource/gltf/animation.rs new file mode 100644 index 000000000..57c6dbfce --- /dev/null +++ b/fyrox-impl/src/resource/gltf/animation.rs @@ -0,0 +1,479 @@ +use crate::core::algebra::{Quaternion, Unit, UnitQuaternion, Vector3}; +use crate::core::log::Log; +use crate::core::math::curve::{Curve, CurveKey, CurveKeyKind}; +use crate::core::pool::Handle; +use crate::generic_animation::container::{TrackDataContainer, TrackValueKind}; +use crate::generic_animation::track::Track; +use crate::generic_animation::value::{ValueBinding, ValueType}; +use crate::scene::animation::Animation; +use crate::scene::node::Node; +use gltf::animation::util::ReadOutputs; +use gltf::animation::Channel; +use gltf::animation::{Interpolation, Property}; +use gltf::Buffer; + +type Result = std::result::Result; + +use super::iter::*; + +/// Extract a list of Animations from the give glTF document, if that document contains any. +/// The resulting list of animations is not guaranteed to be the same length as the list of animations +/// in the document. If any error is encountered in translating an animation from glTF to Fyrox, +/// that animation will be excluded from the resulting list and an error message will be logged. +/// +/// * `doc`: The document in which to find the animations. +/// +/// * `node_handles`: A slice containing a [Handle] for every node defined in the document, in that order, so that +/// a handle can be looked up using the index of a node within the document. Animations in glTF specify their target +/// nodes by their index within the node list of the document, and these indices need to be translated into handles. +/// +/// * `buffers`: A slice containing a list of byte-vectors, one for each buffer in the glTF document. +/// Animations in glTF make reference to data stored in the document's list of buffers by index. +/// This slcie allows an index into the document's list of buffers to be translated into actual bytes of data. +pub fn import_animations( + doc: &gltf::Document, + node_handles: &[Handle], + buffers: &[Vec], +) -> Vec { + let mut result: Vec = Vec::with_capacity(doc.animations().len()); + for animation in doc.animations() { + if let Ok(animation) = import_animation(&animation, node_handles, buffers) { + result.push(animation); + } else { + Log::err(format!( + "Failed to import animation: {}", + animation.name().unwrap_or("[Unnamed]") + )); + } + } + result +} + +fn import_animation( + animation: &gltf::Animation, + node_handles: &[Handle], + buffers: &[Vec], +) -> Result { + let mut result = Animation::default(); + let name = animation + .name() + .map(str::to_owned) + .unwrap_or(default_animation_name(animation)); + //Log::info(format!("ANIMATION: {}", name)); + result.set_name(name); + import_channels(animation, &mut result, node_handles, buffers)?; + Ok(result) +} + +fn default_animation_name(animation: &gltf::Animation) -> String { + format!("Animation {}", animation.index()) +} + +fn import_channels( + source_animation: &gltf::Animation, + build_animation: &mut Animation, + node_handles: &[Handle], + buffers: &[Vec], +) -> Result<()> { + let mut end_time: f32 = 0.0; + for channel in source_animation.channels() { + let channel_end = find_end_time_for_channel(&channel, |buf: Buffer| { + buffers.get(buf.index()).map(Vec::as_slice) + })?; + if end_time < channel_end { + end_time = channel_end; + } + let target_index = channel.target().node().index(); + let target: Handle = node_handles.get(target_index).ok_or(())?.clone(); + if let Property::MorphTargetWeights = channel.target().property() { + import_weight_channel(&channel, target, build_animation, |buf: Buffer| { + buffers.get(buf.index()).map(Vec::as_slice) + })?; + } else { + import_transform_channel(&channel, target, build_animation, |buf: Buffer| { + buffers.get(buf.index()).map(Vec::as_slice) + })?; + } + } + build_animation.set_time_slice(0.0..end_time); + build_animation.set_enabled(true); + Ok(()) +} + +fn import_transform_channel<'a, 's, F>( + channel: &'a Channel, + target: Handle, + build_animation: &mut Animation, + get_buffer_data: F, +) -> Result<()> +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let prop = match channel.target().property() { + Property::Translation => ValueBinding::Position, + Property::Rotation => ValueBinding::Rotation, + Property::Scale => ValueBinding::Scale, + Property::MorphTargetWeights => return Err(()), + }; + let sampler = import_sampler(&channel, get_buffer_data)?; + let mut track = Track::new(sampler, prop); + track.set_target(target); + + //print_track(&track); + + build_animation.add_track(track); + Ok(()) +} + +#[allow(dead_code)] +fn print_track(track: &Track>) { + let data = track.data_container(); + println!("Track: {:?}", data.value_kind()); + for (i, curve) in data.curves_ref().iter().enumerate() { + println!("Curve: {}", i); + for key in curve.keys() { + println!("{:4.2}: {}: {:?}", key.location(), key.value, key.kind); + } + } +} + +fn import_weight_channel<'a, 's, F>( + channel: &'a Channel, + target: Handle, + build_animation: &mut Animation, + get_buffer_data: F, +) -> Result<()> +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let target_count = count_morph_targets(&channel.target().node()); + for i in 0..target_count { + let prop = ValueBinding::Property { + name: format!("blend_shapes[{}].weight", i), + value_type: ValueType::F32, + }; + let sampler = match channel.sampler().interpolation() { + Interpolation::Linear => import_simple_morph_sampler( + channel, + i, + target_count, + CurveKeyKind::Linear, + get_buffer_data.clone(), + )?, + Interpolation::Step => import_simple_morph_sampler( + channel, + i, + target_count, + CurveKeyKind::Constant, + get_buffer_data.clone(), + )?, + Interpolation::CubicSpline => { + import_cubic_morph_sampler(channel, i, target_count, get_buffer_data.clone())? + } + }; + let mut track = Track::new(sampler, prop); + track.set_target(target); + build_animation.add_track(track); + } + Ok(()) +} + +fn count_morph_targets(target: &gltf::Node) -> usize { + if let Some(mesh) = target.mesh() { + if let Some(prim) = mesh.primitives().next() { + prim.morph_targets().count() + } else { + 0 + } + } else { + 0 + } +} + +fn array_to_euler(quaternion: [f32; 4]) -> Vector3 { + quaternion_to_euler(Unit::new_normalize(Quaternion::from(quaternion))) +} + +// X - pitch, Y - yaw, Z - roll +// Or maybe X - roll, Y - yaw, Z - pitch +// Or maybe X - roll, Y - pitch, Z - yaw +fn quaternion_to_euler(q: UnitQuaternion) -> Vector3 { + let (roll, pitch, yaw) = q.euler_angles(); + //let (x, y, z) = (pitch, yaw, roll); + let (x, y, z) = (roll, pitch, yaw); + Vector3::new(x, y, z) +} + +fn quaternion_to_euler_tangents( + in_tangent: [f32; 4], + value: [f32; 4], + out_tangent: [f32; 4], +) -> (Vector3, Vector3) { + let a: UnitQuaternion = Unit::new_normalize(quaternion_subtract(value, in_tangent).into()); + let v: UnitQuaternion = Unit::new_normalize(value.into()); + let b: UnitQuaternion = Unit::new_normalize(quaternion_add(value, out_tangent).into()); + ( + quaternion_to_euler(a.rotation_to(&v).into()), + quaternion_to_euler(v.rotation_to(&b).into()), + ) +} + +fn quaternion_add(a: [f32; 4], b: [f32; 4]) -> [f32; 4] { + let mut r = [0f32; 4]; + for i in 0..4 { + r[i] = a[i] + b[i]; + } + r +} +fn quaternion_subtract(a: [f32; 4], b: [f32; 4]) -> [f32; 4] { + let mut r = [0f32; 4]; + for i in 0..4 { + r[i] = a[i] - b[i]; + } + r +} + +fn find_end_time_for_channel<'a, 's, F>(channel: &'a Channel, get_buffer_data: F) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel.reader(get_buffer_data).read_inputs().ok_or(())?; + Ok(inputs.last().unwrap_or(0.0f32)) +} + +fn import_sampler<'a, 's, F>(channel: &'a Channel, get_buffer_data: F) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + if let Property::Rotation = channel.target().property() { + match channel.sampler().interpolation() { + Interpolation::Linear => { + import_simple_rotation(channel, CurveKeyKind::Linear, get_buffer_data) + } + Interpolation::Step => { + import_simple_rotation(channel, CurveKeyKind::Constant, get_buffer_data) + } + Interpolation::CubicSpline => import_cubic_rotation(channel, get_buffer_data), + } + } else { + match channel.sampler().interpolation() { + Interpolation::Linear => { + import_simple_sampler(channel, CurveKeyKind::Linear, get_buffer_data) + } + Interpolation::Step => { + import_simple_sampler(channel, CurveKeyKind::Constant, get_buffer_data) + } + Interpolation::CubicSpline => import_cubic_sampler(channel, get_buffer_data), + } + } +} + +fn import_simple_sampler<'a, 's, F>( + channel: &'a gltf::animation::Channel, + kind: CurveKeyKind, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel + .reader(get_buffer_data.clone()) + .read_inputs() + .ok_or(())?; + let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?; + let out_iter = match outputs { + ReadOutputs::Translations(iter) => iter, + ReadOutputs::Scales(iter) => iter, + ReadOutputs::Rotations(_) => return Err(()), + ReadOutputs::MorphTargetWeights(_) => return Err(()), + }; + let mut frames_container = TrackDataContainer::new(TrackValueKind::Vector3); + for i in 0..3 { + let curve_keys: Curve = inputs + .clone() + .zip(out_iter.clone()) + .map(|(time, o)| CurveKey::new(time, o[i], kind.clone())) + .collect::>() + .into(); + frames_container.curves_mut()[i] = curve_keys; + } + Ok(frames_container) +} + +fn import_simple_morph_sampler<'a, 's, F>( + channel: &'a gltf::animation::Channel, + morph_index: usize, + morph_count: usize, + kind: CurveKeyKind, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel + .reader(get_buffer_data.clone()) + .read_inputs() + .ok_or(())?; + let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?; + let out_iter = match outputs { + ReadOutputs::MorphTargetWeights(weights) => weights.into_f32(), + _ => return Err(()), + }; + let out_iter = select_iterator(out_iter, morph_index, morph_count); + let mut frames_container = TrackDataContainer::new(TrackValueKind::Real); + let curve_keys: Curve = inputs + .zip(out_iter) + .map(|(time, o)| CurveKey::new(time, o * 100.0, kind.clone())) + .collect::>() + .into(); + frames_container.curves_mut()[0] = curve_keys; + Ok(frames_container) +} + +fn import_cubic_sampler<'a, 's, F>( + channel: &'a gltf::animation::Channel, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel + .reader(get_buffer_data.clone()) + .read_inputs() + .ok_or(())?; + let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?; + let out_iter = match outputs { + ReadOutputs::Translations(iter) => iter, + ReadOutputs::Scales(iter) => iter, + ReadOutputs::Rotations(_) => return Err(()), + ReadOutputs::MorphTargetWeights(_) => return Err(()), + }; + let mut frames_container = TrackDataContainer::new(TrackValueKind::Vector3); + for i in 0..3 { + let curve_keys: Curve = iter_cubic_data(inputs.clone(), out_iter.clone()) + .map(|(time, o)| { + CurveKey::new( + time, + o[1][i], + CurveKeyKind::Cubic { + left_tangent: o[0][i], + right_tangent: o[2][i], + }, + ) + }) + .collect::>() + .into(); + frames_container.curves_mut()[i] = curve_keys; + } + Ok(frames_container) +} + +fn import_cubic_morph_sampler<'a, 's, F>( + channel: &'a gltf::animation::Channel, + morph_index: usize, + morph_count: usize, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel + .reader(get_buffer_data.clone()) + .read_inputs() + .ok_or(())?; + let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?; + let out_iter = match outputs { + ReadOutputs::MorphTargetWeights(weights) => weights.into_f32(), + _ => return Err(()), + }; + let out_iter = select_iterator(out_iter, morph_index, morph_count); + let iter = iter_cubic_data(inputs, out_iter); + let mut frames_container = TrackDataContainer::new(TrackValueKind::Real); + let curve_keys: Curve = iter + .map(|(time, o)| { + CurveKey::new( + time, + o[1] * 100.0, + CurveKeyKind::Cubic { + left_tangent: o[0] * 100.0, + right_tangent: o[2] * 100.0, + }, + ) + }) + .collect::>() + .into(); + frames_container.curves_mut()[0] = curve_keys; + Ok(frames_container) +} + +fn import_simple_rotation<'a, 's, F>( + channel: &'a gltf::animation::Channel, + kind: CurveKeyKind, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel + .reader(get_buffer_data.clone()) + .read_inputs() + .ok_or(())?; + let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?; + let out_iter = match outputs { + ReadOutputs::Translations(_) => return Err(()), + ReadOutputs::Scales(_) => return Err(()), + ReadOutputs::Rotations(iter) => iter.into_f32(), + ReadOutputs::MorphTargetWeights(_) => return Err(()), + }; + let mut frames_container = TrackDataContainer::new(TrackValueKind::UnitQuaternion); + for i in 0..3 { + let curve_keys: Curve = inputs + .clone() + .zip(out_iter.clone()) + .map(|(time, o)| CurveKey::new(time, array_to_euler(o)[i], kind.clone())) + .collect::>() + .into(); + frames_container.curves_mut()[i] = curve_keys; + } + Ok(frames_container) +} + +fn import_cubic_rotation<'a, 's, F>( + channel: &'a gltf::animation::Channel, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let inputs = channel + .reader(get_buffer_data.clone()) + .read_inputs() + .ok_or(())?; + let outputs = channel.reader(get_buffer_data).read_outputs().ok_or(())?; + let out_iter = match outputs { + ReadOutputs::Translations(_) => return Err(()), + ReadOutputs::Scales(_) => return Err(()), + ReadOutputs::Rotations(iter) => iter.into_f32(), + ReadOutputs::MorphTargetWeights(_) => return Err(()), + }; + let mut frames_container = TrackDataContainer::new(TrackValueKind::UnitQuaternion); + for i in 0..3 { + let curve_keys: Curve = iter_cubic_data(inputs.clone(), out_iter.clone()) + .map(|(time, o)| { + let (in_tang, out_tang) = quaternion_to_euler_tangents(o[0], o[1], o[2]); + let value = array_to_euler(o[1]); + CurveKey::new( + time, + value[i], + CurveKeyKind::Cubic { + left_tangent: in_tang[i], + right_tangent: out_tang[i], + }, + ) + }) + .collect::>() + .into(); + frames_container.curves_mut()[i] = curve_keys; + } + Ok(frames_container) +} diff --git a/fyrox-impl/src/resource/gltf/gltf_standard.shader b/fyrox-impl/src/resource/gltf/gltf_standard.shader new file mode 100644 index 000000000..82fdc43fc --- /dev/null +++ b/fyrox-impl/src/resource/gltf/gltf_standard.shader @@ -0,0 +1,632 @@ +( + name: "GLTFShader", + + // Each property's name must match respective uniform name. + properties: [ + ( + name: "diffuseTexture", + kind: Sampler(default: None, fallback: White), + ), + ( + name: "normalTexture", + kind: Sampler(default: None, fallback: Normal), + ), + ( + name: "metallicRoughnessTexture", + kind: Sampler(default: None, fallback: White), + ), + ( + name: "heightTexture", + kind: Sampler(default: None, fallback: Black), + ), + ( + name: "emissionTexture", + kind: Sampler(default: None, fallback: Black), + ), + ( + name: "lightmapTexture", + kind: Sampler(default: None, fallback: Black), + ), + ( + name: "aoTexture", + kind: Sampler(default: None, fallback: White), + ), + ( + name: "texCoordScale", + kind: Vector2((1.0, 1.0)), + ), + ( + name: "layerIndex", + kind: UInt(0), + ), + ( + name: "emissionStrength", + kind: Vector3((2.0, 2.0, 2.0)), + ), + ( + name: "diffuseColor", + kind: Color(r: 255, g: 255, b: 255, a: 255), + ), + ( + name: "metallicFactor", + kind: Float(0.0), + ), + ( + name: "roughnessFactor", + kind: Float(0.0), + ), + ( + name: "parallaxCenter", + kind: Float(0.0), + ), + ( + name: "parallaxScale", + kind: Float(0.08), + ), + ], + + passes: [ + ( + name: "GBuffer", + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: true, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + ), + vertex_shader: + r#" + layout(location = 0) in vec3 vertexPosition; + layout(location = 1) in vec2 vertexTexCoord; + layout(location = 2) in vec3 vertexNormal; + layout(location = 3) in vec4 vertexTangent; + layout(location = 4) in vec4 boneWeights; + layout(location = 5) in vec4 boneIndices; + layout(location = 6) in vec2 vertexSecondTexCoord; + + // Define uniforms with reserved names. Fyrox will automatically provide + // required data to these uniforms. + uniform mat4 fyrox_worldMatrix; + uniform mat4 fyrox_worldViewProjection; + uniform bool fyrox_useSkeletalAnimation; + uniform sampler2D fyrox_boneMatrices; + uniform sampler3D fyrox_blendShapesStorage; + uniform float fyrox_blendShapesWeights[128]; + uniform int fyrox_blendShapesCount; + + out vec3 position; + out vec3 normal; + out vec2 texCoord; + out vec3 tangent; + out vec3 binormal; + out vec2 secondTexCoord; + + void main() + { + vec4 localPosition = vec4(0); + vec3 localNormal = vec3(0); + vec3 localTangent = vec3(0); + + vec4 inputPosition = vec4(vertexPosition, 1.0); + vec3 inputNormal = vertexNormal; + vec3 inputTangent = vertexTangent.xyz; + + for (int i = 0; i < fyrox_blendShapesCount; ++i) { + TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(fyrox_blendShapesStorage, gl_VertexID, i); + float weight = fyrox_blendShapesWeights[i]; + inputPosition.xyz += offsets.position * weight; + inputNormal += offsets.normal * weight; + inputTangent += offsets.tangent * weight; + } + + if (fyrox_useSkeletalAnimation) + { + int i0 = int(boneIndices.x); + int i1 = int(boneIndices.y); + int i2 = int(boneIndices.z); + int i3 = int(boneIndices.w); + + mat4 m0 = S_FetchMatrix(fyrox_boneMatrices, i0); + mat4 m1 = S_FetchMatrix(fyrox_boneMatrices, i1); + mat4 m2 = S_FetchMatrix(fyrox_boneMatrices, i2); + mat4 m3 = S_FetchMatrix(fyrox_boneMatrices, i3); + + localPosition += m0 * inputPosition * boneWeights.x; + localPosition += m1 * inputPosition * boneWeights.y; + localPosition += m2 * inputPosition * boneWeights.z; + localPosition += m3 * inputPosition * boneWeights.w; + + localNormal += mat3(m0) * inputNormal * boneWeights.x; + localNormal += mat3(m1) * inputNormal * boneWeights.y; + localNormal += mat3(m2) * inputNormal * boneWeights.z; + localNormal += mat3(m3) * inputNormal * boneWeights.w; + + localTangent += mat3(m0) * inputTangent * boneWeights.x; + localTangent += mat3(m1) * inputTangent * boneWeights.y; + localTangent += mat3(m2) * inputTangent * boneWeights.z; + localTangent += mat3(m3) * inputTangent * boneWeights.w; + } + else + { + localPosition = inputPosition; + localNormal = inputNormal; + localTangent = inputTangent; + } + + mat3 nm = mat3(fyrox_worldMatrix); + normal = normalize(nm * localNormal); + tangent = normalize(nm * localTangent); + binormal = normalize(vertexTangent.w * cross(normal, tangent)); + texCoord = vertexTexCoord; + position = vec3(fyrox_worldMatrix * localPosition); + secondTexCoord = vertexSecondTexCoord; + + gl_Position = fyrox_worldViewProjection * localPosition; + } + "#, + fragment_shader: + r#" + layout(location = 0) out vec4 outColor; + layout(location = 1) out vec4 outNormal; + layout(location = 2) out vec4 outAmbient; + layout(location = 3) out vec4 outMaterial; + layout(location = 4) out uint outDecalMask; + + // Properties. + uniform sampler2D diffuseTexture; + uniform sampler2D normalTexture; + uniform sampler2D metallicRoughnessTexture; // B for metallness, G for roughness + uniform sampler2D heightTexture; + uniform sampler2D emissionTexture; + uniform sampler2D lightmapTexture; + uniform sampler2D aoTexture; + uniform vec2 texCoordScale; + uniform uint layerIndex; + uniform vec3 emissionStrength; + uniform vec4 diffuseColor; + uniform float metallicFactor; + uniform float roughnessFactor; + uniform float parallaxCenter; + uniform float parallaxScale; + + // Define uniforms with reserved names. Fyrox will automatically provide + // required data to these uniforms. + uniform vec3 fyrox_cameraPosition; + uniform bool fyrox_usePOM; + + in vec3 position; + in vec3 normal; + in vec2 texCoord; + in vec3 tangent; + in vec3 binormal; + in vec2 secondTexCoord; + + void main() + { + mat3 tangentSpace = mat3(tangent, binormal, normal); + vec3 toFragment = normalize(position - fyrox_cameraPosition); + + vec2 tc; + if (fyrox_usePOM) { + vec3 toFragmentTangentSpace = normalize(transpose(tangentSpace) * toFragment); + tc = S_ComputeParallaxTextureCoordinates( + heightTexture, + toFragmentTangentSpace, + texCoord * texCoordScale, + parallaxCenter, + parallaxScale + ); + } else { + tc = texCoord * texCoordScale; + } + + outColor = diffuseColor * texture(diffuseTexture, tc); + + // Alpha test. + if (outColor.a < 0.5) { + discard; + } + outColor.a = 1.0; + + vec4 n = normalize(texture(normalTexture, tc) * 2.0 - 1.0); + outNormal = vec4(normalize(tangentSpace * n.xyz) * 0.5 + 0.5, 1.0); + + outMaterial.x = metallicFactor * texture(metallicRoughnessTexture, tc).b; // Metallic + outMaterial.y = roughnessFactor * texture(metallicRoughnessTexture, tc).g; // Roughness + outMaterial.z = texture(aoTexture, tc).r; + outMaterial.a = 1.0; + + outAmbient.xyz = emissionStrength * texture(emissionTexture, tc).rgb + texture(lightmapTexture, secondTexCoord).rgb; + outAmbient.a = 1.0; + + outDecalMask = layerIndex; + } + "#, + ), + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: true, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + ), + vertex_shader: + r#" + layout(location = 0) in vec3 vertexPosition; + layout(location = 1) in vec2 vertexTexCoord; + layout(location = 4) in vec4 boneWeights; + layout(location = 5) in vec4 boneIndices; + + uniform mat4 fyrox_worldViewProjection; + uniform bool fyrox_useSkeletalAnimation; + uniform sampler2D fyrox_boneMatrices; + uniform sampler3D fyrox_blendShapesStorage; + uniform float fyrox_blendShapesWeights[128]; + uniform int fyrox_blendShapesCount; + + out vec3 position; + out vec2 texCoord; + + void main() + { + vec4 localPosition = vec4(0); + + vec4 inputPosition = vec4(vertexPosition, 1.0); + + for (int i = 0; i < fyrox_blendShapesCount; ++i) { + TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(fyrox_blendShapesStorage, gl_VertexID, i); + float weight = fyrox_blendShapesWeights[i]; + inputPosition.xyz += offsets.position * weight; + } + + if (fyrox_useSkeletalAnimation) + { + int i0 = int(boneIndices.x); + int i1 = int(boneIndices.y); + int i2 = int(boneIndices.z); + int i3 = int(boneIndices.w); + + mat4 m0 = S_FetchMatrix(fyrox_boneMatrices, i0); + mat4 m1 = S_FetchMatrix(fyrox_boneMatrices, i1); + mat4 m2 = S_FetchMatrix(fyrox_boneMatrices, i2); + mat4 m3 = S_FetchMatrix(fyrox_boneMatrices, i3); + + localPosition += m0 * inputPosition * boneWeights.x; + localPosition += m1 * inputPosition * boneWeights.y; + localPosition += m2 * inputPosition * boneWeights.z; + localPosition += m3 * inputPosition * boneWeights.w; + } + else + { + localPosition = inputPosition; + } + gl_Position = fyrox_worldViewProjection * localPosition; + texCoord = vertexTexCoord; + } + "#, + + fragment_shader: + r#" + uniform sampler2D diffuseTexture; + uniform vec4 diffuseColor; + + out vec4 FragColor; + + in vec2 texCoord; + + void main() + { + FragColor = diffuseColor * texture(diffuseTexture, texCoord); + } + "#, + ), + ( + name: "DirectionalShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: true, + stencil_test: None, + depth_test: true, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + ), + + vertex_shader: + r#" + layout(location = 0) in vec3 vertexPosition; + layout(location = 1) in vec2 vertexTexCoord; + layout(location = 4) in vec4 boneWeights; + layout(location = 5) in vec4 boneIndices; + + uniform mat4 fyrox_worldViewProjection; + uniform bool fyrox_useSkeletalAnimation; + uniform sampler2D fyrox_boneMatrices; + uniform sampler3D fyrox_blendShapesStorage; + uniform float fyrox_blendShapesWeights[128]; + uniform int fyrox_blendShapesCount; + + out vec2 texCoord; + + void main() + { + vec4 localPosition = vec4(0); + + vec4 inputPosition = vec4(vertexPosition, 1.0); + + for (int i = 0; i < fyrox_blendShapesCount; ++i) { + TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(fyrox_blendShapesStorage, gl_VertexID, i); + float weight = fyrox_blendShapesWeights[i]; + inputPosition.xyz += offsets.position * weight; + } + + if (fyrox_useSkeletalAnimation) + { + vec4 vertex = vec4(vertexPosition, 1.0); + + mat4 m0 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.x)); + mat4 m1 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.y)); + mat4 m2 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.z)); + mat4 m3 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.w)); + + localPosition += m0 * inputPosition * boneWeights.x; + localPosition += m1 * inputPosition * boneWeights.y; + localPosition += m2 * inputPosition * boneWeights.z; + localPosition += m3 * inputPosition * boneWeights.w; + } + else + { + localPosition = inputPosition; + } + + gl_Position = fyrox_worldViewProjection * localPosition; + texCoord = vertexTexCoord; + } + "#, + + fragment_shader: + r#" + uniform sampler2D diffuseTexture; + + in vec2 texCoord; + + void main() + { + if (texture(diffuseTexture, texCoord).a < 0.2) discard; + } + "#, + ), + ( + name: "SpotShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: true, + stencil_test: None, + depth_test: true, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + ), + + vertex_shader: + r#" + layout(location = 0) in vec3 vertexPosition; + layout(location = 1) in vec2 vertexTexCoord; + layout(location = 4) in vec4 boneWeights; + layout(location = 5) in vec4 boneIndices; + + uniform mat4 fyrox_worldViewProjection; + uniform bool fyrox_useSkeletalAnimation; + uniform sampler2D fyrox_boneMatrices; + uniform sampler3D fyrox_blendShapesStorage; + uniform float fyrox_blendShapesWeights[128]; + uniform int fyrox_blendShapesCount; + + out vec2 texCoord; + + void main() + { + vec4 localPosition = vec4(0); + + vec4 inputPosition = vec4(vertexPosition, 1.0); + + for (int i = 0; i < fyrox_blendShapesCount; ++i) { + TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(fyrox_blendShapesStorage, gl_VertexID, i); + float weight = fyrox_blendShapesWeights[i]; + inputPosition.xyz += offsets.position * weight; + } + + if (fyrox_useSkeletalAnimation) + { + vec4 vertex = vec4(vertexPosition, 1.0); + + mat4 m0 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.x)); + mat4 m1 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.y)); + mat4 m2 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.z)); + mat4 m3 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.w)); + + localPosition += m0 * inputPosition * boneWeights.x; + localPosition += m1 * inputPosition * boneWeights.y; + localPosition += m2 * inputPosition * boneWeights.z; + localPosition += m3 * inputPosition * boneWeights.w; + } + else + { + localPosition = inputPosition; + } + + gl_Position = fyrox_worldViewProjection * localPosition; + texCoord = vertexTexCoord; + } + "#, + + fragment_shader: + r#" + uniform sampler2D diffuseTexture; + + in vec2 texCoord; + + void main() + { + if (texture(diffuseTexture, texCoord).a < 0.2) discard; + } + "#, + ), + ( + name: "PointShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: true, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + ), + + vertex_shader: + r#" + layout(location = 0) in vec3 vertexPosition; + layout(location = 1) in vec2 vertexTexCoord; + layout(location = 4) in vec4 boneWeights; + layout(location = 5) in vec4 boneIndices; + + uniform mat4 fyrox_worldMatrix; + uniform mat4 fyrox_worldViewProjection; + uniform bool fyrox_useSkeletalAnimation; + uniform sampler2D fyrox_boneMatrices; + uniform sampler3D fyrox_blendShapesStorage; + uniform float fyrox_blendShapesWeights[128]; + uniform int fyrox_blendShapesCount; + + out vec2 texCoord; + out vec3 worldPosition; + + void main() + { + vec4 localPosition = vec4(0); + + vec4 inputPosition = vec4(vertexPosition, 1.0); + + for (int i = 0; i < fyrox_blendShapesCount; ++i) { + TBlendShapeOffsets offsets = S_FetchBlendShapeOffsets(fyrox_blendShapesStorage, gl_VertexID, i); + float weight = fyrox_blendShapesWeights[i]; + inputPosition.xyz += offsets.position * weight; + } + + if (fyrox_useSkeletalAnimation) + { + vec4 vertex = vec4(vertexPosition, 1.0); + + mat4 m0 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.x)); + mat4 m1 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.y)); + mat4 m2 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.z)); + mat4 m3 = S_FetchMatrix(fyrox_boneMatrices, int(boneIndices.w)); + + localPosition += m0 * inputPosition * boneWeights.x; + localPosition += m1 * inputPosition * boneWeights.y; + localPosition += m2 * inputPosition * boneWeights.z; + localPosition += m3 * inputPosition * boneWeights.w; + } + else + { + localPosition = inputPosition; + } + + gl_Position = fyrox_worldViewProjection * localPosition; + worldPosition = (fyrox_worldMatrix * localPosition).xyz; + texCoord = vertexTexCoord; + } + "#, + + fragment_shader: + r#" + uniform sampler2D diffuseTexture; + + uniform vec3 fyrox_lightPosition; + + in vec2 texCoord; + in vec3 worldPosition; + + layout(location = 0) out float depth; + + void main() + { + if (texture(diffuseTexture, texCoord).a < 0.2) discard; + depth = length(fyrox_lightPosition - worldPosition); + } + "#, + ) + ], +) \ No newline at end of file diff --git a/fyrox-impl/src/resource/gltf/iter.rs b/fyrox-impl/src/resource/gltf/iter.rs new file mode 100644 index 000000000..39fd58057 --- /dev/null +++ b/fyrox-impl/src/resource/gltf/iter.rs @@ -0,0 +1,118 @@ +/// Iterator that skips elements of its inner iterator so that it +/// returns every nth element, starting from a given offset from the +/// start of the inner iterator. +/// +/// For example: [0, 1, 2, 3, 4, 5, 6, 7, 8] +/// Becomes: [1, 4, 7] +/// +/// In this case, it is returning the second element within each three elements. +#[derive(Debug, Clone)] +pub struct SelectIterator { + source: I, + offset: usize, + window_size: usize, + starting: bool, +} + +/// Create an iterator that skips elements of the given iterator to produce a single element +/// within each group of window_size elements. The element returned is `offset` elements from the +/// start of each window. +/// +/// For example: select_iterator(iter, 3, 5) +/// If iter produces: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +/// select_iterartor(iter, 3, 5]) procues: [3, 8] +pub fn select_iterator(source: I, offset: usize, window_size: usize) -> SelectIterator { + SelectIterator { + source, + offset, + window_size, + starting: true, + } +} + +impl Iterator for SelectIterator +where + I: Iterator, +{ + type Item = ::Item; + fn next(&mut self) -> Option { + if self.starting { + self.starting = false; + for _ in 0..self.offset { + self.source.next()?; + } + self.source.next() + } else { + for _ in 0..self.window_size - 1 { + self.source.next()?; + } + self.source.next() + } + } +} + +pub fn iter_cubic_data( + inputs: I, + outputs: O, +) -> impl Iterator::Item, [::Item; 3])> +where + I: Iterator, + O: Iterator, +{ + CubicIter { inputs, outputs } +} + +#[derive(Debug, Clone)] +pub struct CubicIter { + inputs: I, + outputs: O, +} + +impl Iterator for CubicIter +where + I: Iterator, + O: Iterator, +{ + type Item = (::Item, [::Item; 3]); + fn next(&mut self) -> Option { + Some(( + self.inputs.next()?, + [ + self.outputs.next()?, + self.outputs.next()?, + self.outputs.next()?, + ], + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn select_example1() { + let iter = [0, 1, 2, 3, 4, 5, 6, 7, 8].into_iter(); + let result: Vec<_> = select_iterator(iter, 1, 3).collect(); + assert!(result.clone().into_iter().eq([1, 4, 7]), "{:?}", result); + } + #[test] + fn select_example2() { + let iter = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_iter(); + let result: Vec<_> = select_iterator(iter, 3, 5).collect(); + assert!(result.clone().into_iter().eq([3, 8]), "{:?}", result); + } + #[test] + fn cubic1() { + let input = [0, 1, 2, 3].into_iter(); + let output = [-1, 0, 1, 2, 3, 4, 5, 6].into_iter(); + let result: Vec<_> = iter_cubic_data(input, output).collect(); + assert!( + result + .clone() + .into_iter() + .eq([(0, [-1, 0, 1]), (1, [2, 3, 4])]), + "{:?}", + result + ); + } +} diff --git a/fyrox-impl/src/resource/gltf/material.rs b/fyrox-impl/src/resource/gltf/material.rs new file mode 100644 index 000000000..be39d1e9a --- /dev/null +++ b/fyrox-impl/src/resource/gltf/material.rs @@ -0,0 +1,433 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + asset::{manager::ResourceManager, state::LoadError, untyped::ResourceKind, Resource}, + core::{algebra::Vector4, color::Color, log::Log, sstorage::ImmutableString}, + material::{ + shader::{SamplerFallback, Shader, ShaderResource}, + Material, MaterialResource, PropertyValue, + }, + resource::{ + model::MaterialSearchOptions, + texture::{Texture, TextureError, TextureImportOptions, TextureResource}, + }, +}; +use gltf::{buffer::View, image, Document}; +use lazy_static::lazy_static; + +use super::uri; + +type Result = std::result::Result; + +use crate::resource::texture::TextureMagnificationFilter as FyroxMagFilter; +use crate::resource::texture::TextureMinificationFilter as FyroxMinFilter; +use gltf::texture::MagFilter as GltfMagFilter; +use gltf::texture::MinFilter as GltfMinFilter; + +pub const SHADER_NAME: &str = "glTF Shader"; +pub const SHADER_SRC: &str = include_str!("gltf_standard.shader"); + +lazy_static! { + static ref GLTF_SHADER: ShaderResource = + ShaderResource::new_ok(SHADER_NAME.into(), Shader::from_string(SHADER_SRC).unwrap()); +} + +fn convert_mini(filter: GltfMinFilter) -> FyroxMinFilter { + match filter { + GltfMinFilter::Linear => FyroxMinFilter::Linear, + GltfMinFilter::Nearest => FyroxMinFilter::Nearest, + GltfMinFilter::LinearMipmapLinear => FyroxMinFilter::LinearMipMapLinear, + GltfMinFilter::NearestMipmapLinear => FyroxMinFilter::NearestMipMapLinear, + GltfMinFilter::LinearMipmapNearest => FyroxMinFilter::LinearMipMapNearest, + GltfMinFilter::NearestMipmapNearest => FyroxMinFilter::NearestMipMapNearest, + } +} + +fn convert_mag(filter: GltfMagFilter) -> FyroxMagFilter { + match filter { + GltfMagFilter::Linear => FyroxMagFilter::Linear, + GltfMagFilter::Nearest => FyroxMagFilter::Nearest, + } +} + +use crate::resource::texture::TextureWrapMode as FyroxWrapMode; +use gltf::texture::WrappingMode as GltfWrapMode; + +fn convert_wrap(mode: GltfWrapMode) -> FyroxWrapMode { + match mode { + GltfWrapMode::Repeat => FyroxWrapMode::Repeat, + GltfWrapMode::ClampToEdge => FyroxWrapMode::ClampToEdge, + GltfWrapMode::MirroredRepeat => FyroxWrapMode::MirroredRepeat, + } +} + +#[derive(Debug)] +pub enum GltfMaterialError { + ShaderLoadFailed, + InvalidIndex, + UnsupportedURI(Box), + TextureNotFound(Box), + Load(LoadError), + Base64(base64::DecodeError), + Texture(TextureError), +} + +impl From for GltfMaterialError { + fn from(error: LoadError) -> Self { + GltfMaterialError::Load(error) + } +} + +impl From for GltfMaterialError { + fn from(error: base64::DecodeError) -> Self { + GltfMaterialError::Base64(error) + } +} + +impl From for GltfMaterialError { + fn from(error: TextureError) -> Self { + GltfMaterialError::Texture(error) + } +} + +pub enum SourceImage<'a> { + External(&'a str), + View(&'a [u8]), + Embedded(Vec), +} + +pub fn decode_base64(source: &str) -> Result> { + Ok(uri::decode_base64(source)?) +} + +/// Extract a list of [MaterialResource] from the give glTF document, if that document contains any. +/// The resulting list of materials is guaranteed to be the same length as the list of materials +/// in the document so that an index into the document's list of materials will be the same as the index +/// of the matching MaterialResource in the returned list. This is important since the glTF document +/// refers to materials by index. +/// +/// * `doc`: The document in which to find the materials. +/// +/// * `textures`: A slice containing a [TextureResource] for every texture defined in the document, in that order, so that +/// a texture can be looked up using the index of a texture within the document. Materials in glTF specify their target +/// textures by their index within the node list of the document, and these indices need to be translated into handles. +/// +/// * `buffers`: A slice containing a list of byte-vectors, one for each buffer in the glTF document. +/// Animations in glTF make reference to data stored in the document's list of buffers by index. +/// This slcie allows an index into the document's list of buffers to be translated into actual bytes of data. +/// +/// * `resource_manager`: A [ResourceManager] makes it possible to access shaders and create materials. +pub async fn import_materials( + gltf: &Document, + textures: &[TextureResource], + resource_manager: &ResourceManager, +) -> Result> { + let mut result: Vec = Vec::with_capacity(gltf.materials().len()); + for mat in gltf.materials() { + match import_material(mat, textures, resource_manager).await { + Ok(res) => result.push(res), + Err(err) => { + Log::err(format!("glTF material failed to import. Reason: {:?}", err)); + result.push(MaterialResource::new_ok( + ResourceKind::Embedded, + Material::default(), + )); + } + } + } + Ok(result) +} + +async fn import_material( + mat: gltf::Material<'_>, + textures: &[TextureResource], + resource_manager: &ResourceManager, +) -> Result { + let shader: ShaderResource = GLTF_SHADER.clone(); //resource_manager.request(SHADER_PATH).await?; + if !shader.is_ok() { + return Err(GltfMaterialError::ShaderLoadFailed); + } + let mut result: Material = Material::from_shader(shader, Some(resource_manager.clone())); + let pbr = mat.pbr_metallic_roughness(); + if let Some(tex) = pbr.base_color_texture() { + set_texture( + &mut result, + "diffuseTexture", + textures, + tex.texture().index(), + SamplerFallback::White, + )?; + } + if let Some(tex) = mat.normal_texture() { + set_texture( + &mut result, + "normalTexture", + textures, + tex.texture().index(), + SamplerFallback::Normal, + )?; + } + if let Some(tex) = pbr.metallic_roughness_texture() { + set_texture( + &mut result, + "metallicRoughnessTexture", + textures, + tex.texture().index(), + SamplerFallback::White, + )?; + } + if let Some(tex) = mat.emissive_texture() { + set_texture( + &mut result, + "emissionTexture", + textures, + tex.texture().index(), + SamplerFallback::Black, + )?; + } + if let Some(tex) = mat.occlusion_texture() { + set_texture( + &mut result, + "aoTexture", + textures, + tex.texture().index(), + SamplerFallback::White, + )?; + } + set_material_color( + &mut result, + "diffuseColor", + Vector4::::from(pbr.base_color_factor()).into(), + )?; + set_material_vector3(&mut result, "emissionStrength", mat.emissive_factor())?; + set_material_scalar(&mut result, "metallicFactor", pbr.metallic_factor())?; + set_material_scalar(&mut result, "roughnessFactor", pbr.roughness_factor())?; + Ok(Resource::new_ok(ResourceKind::Embedded, result)) +} + +fn set_material_scalar(material: &mut Material, name: &'static str, value: f32) -> Result<()> { + let value: PropertyValue = PropertyValue::Float(value); + match material.set_property(&ImmutableString::new(name), value) { + Ok(()) => Ok(()), + Err(err) => { + Log::err(format!( + "Unable to set material property {} for glTF material! Reason: {:?}", + name, err + )); + Ok(()) + } + } +} + +fn set_material_color(material: &mut Material, name: &'static str, color: Color) -> Result<()> { + let value: PropertyValue = PropertyValue::Color(color); + match material.set_property(&ImmutableString::new(name), value) { + Ok(()) => Ok(()), + Err(err) => { + Log::err(format!( + "Unable to set material property {} for GLTF material! Reason: {:?}", + name, err + )); + Ok(()) + } + } +} + +fn set_material_vector3( + material: &mut Material, + name: &'static str, + vector: [f32; 3], +) -> Result<()> { + let value: PropertyValue = PropertyValue::Vector3(vector.into()); + match material.set_property(&ImmutableString::new(name), value) { + Ok(()) => Ok(()), + Err(err) => { + Log::err(format!( + "Unable to set material property {} for GLTF material! Reason: {:?}", + name, err + )); + Ok(()) + } + } +} + +#[allow(dead_code)] +fn set_material_vector4( + material: &mut Material, + name: &'static str, + vector: [f32; 4], +) -> Result<()> { + let value: PropertyValue = PropertyValue::Vector4(vector.into()); + match material.set_property(&ImmutableString::new(name), value) { + Ok(()) => Ok(()), + Err(err) => { + Log::err(format!( + "Unable to set material property {} for GLTF material! Reason: {:?}", + name, err + )); + Ok(()) + } + } +} + +fn set_texture( + material: &mut Material, + name: &'static str, + textures: &[TextureResource], + index: usize, + fallback: SamplerFallback, +) -> Result<()> { + let tex: TextureResource = textures + .get(index) + .ok_or(GltfMaterialError::InvalidIndex)? + .clone(); + match material.set_property( + &ImmutableString::new(name), + PropertyValue::Sampler { + value: Some(tex), + fallback, + }, + ) { + Ok(()) => Ok(()), + Err(err) => { + Log::err(format!( + "Unable to set material property {} for GLTF material! Reason: {:?}", + name, err + )); + Ok(()) + } + } +} + +pub fn import_images<'a, 'b>( + gltf: &'a Document, + buffers: &'b [Vec], +) -> Result>> +where + 'a: 'b, +{ + let mut result: Vec = Vec::new(); + for image in gltf.images() { + match image.source() { + image::Source::Uri { uri, mime_type: _ } => result.push(import_image_from_uri(uri)?), + image::Source::View { view, mime_type: _ } => { + result.push(import_image_from_view(view, buffers)?) + } + } + } + Ok(result) +} + +fn import_image_from_uri<'a>(uri: &'a str) -> Result> { + let parsed_uri = uri::parse_uri(uri); + match parsed_uri.scheme { + uri::Scheme::Data if parsed_uri.data.is_some() => Ok(SourceImage::Embedded(decode_base64( + parsed_uri.data.unwrap(), + )?)), + uri::Scheme::None => Ok(SourceImage::External(uri)), + _ => Err(GltfMaterialError::UnsupportedURI(uri.into())), + } +} + +fn import_image_from_view<'a>(view: View, buffers: &'a [Vec]) -> Result> { + let offset: usize = view.offset(); + let length: usize = view.length(); + let buf: &Vec = buffers + .get(view.buffer().index()) + .ok_or(GltfMaterialError::InvalidIndex)?; + Ok(SourceImage::View(&buf[offset..offset + length])) +} + +pub struct TextureContext<'a> { + pub resource_manager: &'a ResourceManager, + pub model_path: &'a Path, + pub search_options: &'a MaterialSearchOptions, +} + +pub async fn import_textures<'a>( + gltf: &'a Document, + images: &[SourceImage<'a>], + context: TextureContext<'a>, +) -> Result> { + let mut result: Vec = Vec::with_capacity(gltf.textures().len()); + for tex in gltf.textures() { + let sampler = tex.sampler(); + let source = tex.source(); + let image = images + .get(source.index()) + .ok_or(GltfMaterialError::InvalidIndex)?; + match image { + SourceImage::Embedded(data) => result.push(import_embedded_texture(sampler, data)?), + SourceImage::View(data) => result.push(import_embedded_texture(sampler, data)?), + SourceImage::External(filename) => { + import_external_texture(filename, &context).await?; + } // result.push(import_external_texture(filename, &context).await?), + } + } + Ok(result) +} + +fn import_embedded_texture( + sampler: gltf::texture::Sampler, + data: &[u8], +) -> Result { + let mut options = TextureImportOptions::default(); + if let Some(filter) = sampler.min_filter() { + options.set_minification_filter(convert_mini(filter)); + } + if let Some(filter) = sampler.mag_filter() { + options.set_magnification_filter(convert_mag(filter)); + } + options.set_s_wrap_mode(convert_wrap(sampler.wrap_s())); + options.set_t_wrap_mode(convert_wrap(sampler.wrap_t())); + let tex = Texture::load_from_memory(data, options)?; + Ok(Resource::new_ok(ResourceKind::Embedded, tex)) +} + +async fn import_external_texture( + filename: &str, + context: &TextureContext<'_>, +) -> Result { + let path = search_for_path(filename, context) + .await + .ok_or_else(|| GltfMaterialError::TextureNotFound(filename.into()))?; + Ok(context.resource_manager.request(path)) +} + +async fn search_for_path(filename: &str, context: &TextureContext<'_>) -> Option { + match context.search_options { + MaterialSearchOptions::MaterialsDirectory(ref directory) => Some(directory.join(filename)), + MaterialSearchOptions::RecursiveUp => { + let io = context.resource_manager.resource_io(); + let mut texture_path = None; + let mut path: PathBuf = context.model_path.to_owned(); + while let Some(parent) = path.parent() { + let candidate = parent.join(filename); + if io.exists(&candidate).await { + texture_path = Some(candidate); + break; + } + path.pop(); + } + texture_path + } + MaterialSearchOptions::WorkingDirectory => { + let io = context.resource_manager.resource_io(); + let mut texture_path = None; + let path = Path::new("."); + if let Ok(iter) = io.walk_directory(path).await { + for dir in iter { + if io.is_dir(&dir).await { + let candidate = dir.join(filename); + if candidate.exists() { + texture_path = Some(candidate); + break; + } + } + } + } + texture_path + } + MaterialSearchOptions::UsePathDirectly => Some(filename.into()), + } +} diff --git a/fyrox-impl/src/resource/gltf/mod.rs b/fyrox-impl/src/resource/gltf/mod.rs new file mode 100644 index 000000000..1dd88df5a --- /dev/null +++ b/fyrox-impl/src/resource/gltf/mod.rs @@ -0,0 +1,740 @@ +///! [GltfLoader] enables the importing of *.gltf and *.glb files in the glTF format. +///! This requires the "gltf" feature. +use gltf::Document; +use gltf::Gltf; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use crate::asset::io::ResourceIo; +use crate::asset::loader; +use crate::asset::manager::ResourceManager; +use crate::asset::options; +use crate::asset::state::LoadError; +use crate::core::algebra::{Matrix4, Unit}; +use crate::core::log::Log; +use crate::core::pool::Handle; +use crate::core::TypeUuidProvider; +use crate::graph::BaseSceneGraph; +use crate::graph::NodeMapping; +use crate::gui::core::io::FileLoadError; +use crate::material::MaterialResource; +use crate::resource::model::{MaterialSearchOptions, Model, ModelImportOptions}; +use crate::resource::texture::{TextureError, TextureResource}; +use crate::scene::animation::{AnimationContainer, AnimationPlayerBuilder}; +use crate::scene::base::BaseBuilder; +use crate::scene::graph::Graph; +use crate::scene::mesh::surface::{BlendShape, Surface, SurfaceSharedData}; +use crate::scene::mesh::{Mesh, MeshBuilder}; +use crate::scene::node::Node; +use crate::scene::pivot::PivotBuilder; +use crate::scene::transform::TransformBuilder; +use crate::scene::Scene; + +mod animation; +mod iter; +mod material; +mod node_names; +mod surface; +mod uri; + +use animation::import_animations; +use material::*; +pub use surface::SurfaceDataError; +use surface::{build_surface_data, BlendShapeInfoContainer, GeometryStatistics}; +pub use uri::{parse_uri, Scheme, Uri}; + +type Result = std::result::Result; + +const TARGET_NAMES_KEY: &str = "targetNames"; + +#[derive(Debug)] +enum GltfLoadError { + InvalidIndex, + InvalidPath, + UnsupportedURI(Box), + MissingEmbeddedBin, + Gltf(gltf::Error), + Texture(TextureError), + File(FileLoadError), + Base64(base64::DecodeError), + Load(LoadError), + Material(GltfMaterialError), + Surface(SurfaceDataError), + #[cfg(feature = "gltf_blend_shapes")] + JSON(serde_json::Error), +} + +#[cfg(feature = "gltf_blend_shapes")] +impl From for GltfLoadError { + fn from(error: serde_json::Error) -> Self { + GltfLoadError::JSON(error) + } +} + +impl From for GltfLoadError { + fn from(error: gltf::Error) -> Self { + GltfLoadError::Gltf(error) + } +} + +impl From for GltfLoadError { + fn from(error: TextureError) -> Self { + GltfLoadError::Texture(error) + } +} + +impl From for GltfLoadError { + fn from(error: FileLoadError) -> Self { + GltfLoadError::File(error) + } +} + +impl From for GltfLoadError { + fn from(error: LoadError) -> Self { + GltfLoadError::Load(error) + } +} + +impl From for GltfLoadError { + fn from(error: base64::DecodeError) -> Self { + GltfLoadError::Base64(error) + } +} + +impl From for GltfLoadError { + fn from(error: GltfMaterialError) -> Self { + GltfLoadError::Material(error) + } +} + +impl From for GltfLoadError { + fn from(error: SurfaceDataError) -> Self { + GltfLoadError::Surface(error) + } +} + +fn decode_base64(source: &str) -> Result> { + Ok(uri::decode_base64(source)?) +} + +struct MeshData { + surfaces: Vec, + blend_shapes: Vec, +} + +struct NodeFamily { + main_node: Handle, + bone_children: Vec, +} + +struct SkinNodePair { + skin_index: usize, + node: Handle, +} + +type SkinData = Vec; + +#[derive(PartialEq, Debug, Clone)] +struct SkinBone { + pub node_index: usize, + pub inv_bind_pose: Matrix4, +} + +impl From<(usize, Matrix4)> for SkinBone { + fn from(pair: (usize, Matrix4)) -> Self { + let (node_index, inv_bind_pose) = pair; + SkinBone { + node_index, + inv_bind_pose, + } + } +} + +#[derive(Clone, Copy)] +struct SkinBonePair<'a> { + skin_index: usize, + bone: &'a SkinBone, +} + +struct SkinBoneIter<'a> { + skin_index: usize, + bone_index: usize, + skin_list: &'a [SkinData], +} + +impl<'a> SkinBoneIter<'a> { + fn new(skin_list: &'a [SkinData]) -> SkinBoneIter<'a> { + SkinBoneIter { + skin_index: 0, + bone_index: 0, + skin_list, + } + } +} + +impl<'a> Iterator for SkinBoneIter<'a> { + type Item = SkinBonePair<'a>; + + fn next(&mut self) -> Option { + loop { + if self.skin_index >= self.skin_list.len() { + return None; + } + let skin: &SkinData = &self.skin_list[self.skin_index]; + if self.bone_index >= skin.len() { + self.bone_index = 0; + self.skin_index += 1; + } else { + let bone = &skin[self.bone_index]; + self.bone_index += 1; + return Some(SkinBonePair { + skin_index: self.skin_index, + bone, + }); + } + } + } +} + +struct ImportContext { + io: Arc, + resource_manager: ResourceManager, + model_path: PathBuf, + search_options: MaterialSearchOptions, +} + +impl ImportContext { + fn as_texture_context(&self) -> TextureContext { + TextureContext { + resource_manager: &self.resource_manager, + model_path: &self.model_path, + search_options: &self.search_options, + } + } +} + +#[derive(Default)] +struct ImportResults { + buffers: Option>>, + textures: Option>, + materials: Option>, + skins: Option>, + meshes: Option>, + families: Option>, +} + +impl ImportResults { + fn get_buffer_data_access<'s>( + &'s self, + ) -> impl Clone + Fn(gltf::Buffer<'_>) -> Option<&'s [u8]> { + |b| { + self.buffers + .as_ref() + .unwrap() + .get(b.index()) + .map(Vec::as_slice) + } + } +} + +/// This object performs the loading of files in glTF format with extension "gltf" or "glb". +pub struct GltfLoader { + /// ResourceManager is needed so that textures and mesh data can be loaded from additional resources. + /// The glTF format allows for other assets to be referenced by file path. + pub resource_manager: ResourceManager, + /// Import options control where this loader should search for additional resources. + pub default_import_options: ModelImportOptions, +} + +impl loader::ResourceLoader for GltfLoader { + fn extensions(&self) -> &[&str] { + &["gltf", "glb"] + } + + fn data_type_uuid(&self) -> crate::core::type_traits::prelude::Uuid { + Model::type_uuid() + } + + fn load(&self, path: PathBuf, io: Arc) -> loader::BoxedLoaderFuture { + let resource_manager = self.resource_manager.clone(); + let default_import_options = self.default_import_options.clone(); + + Box::pin(async move { + let import_options = options::try_get_import_settings(&path, io.as_ref()) + .await + .unwrap_or(default_import_options); + + let model = load(path, io, resource_manager, import_options) + .await + .map_err(LoadError::new)?; + + Ok(loader::LoaderPayload::new(model)) + }) + } + + fn try_load_import_settings( + &self, + resource_path: PathBuf, + io: Arc, + ) -> loader::BoxedImportOptionsLoaderFuture { + Box::pin(async move { + options::try_get_import_settings_opaque::(&resource_path, &*io) + .await + }) + } + + fn default_import_options(&self) -> Option> { + Some(Box::::default()) + } +} + +async fn load( + path: PathBuf, + io: Arc, + resource_manager: ResourceManager, + options: ModelImportOptions, +) -> Result { + Log::info(format!("glTF load starting: {:?}", path)); + let mut scene = Scene::new(); + let context = ImportContext { + io, + resource_manager, + model_path: path.clone(), + search_options: options.material_search_options, + }; + let root_name = path + .file_name() + .ok_or(GltfLoadError::InvalidPath)? + .to_string_lossy(); + let root = scene.graph.get_root(); + scene.graph[root].set_name(root_name.clone()); + import_from_path(&mut scene.graph, &context).await?; + node_names::resolve_name_conflicts(context.model_path.as_path(), &mut scene.graph); + Log::info(format!("glTF load complete: {:?}", path)); + Ok(Model::new(NodeMapping::UseNames, scene)) +} + +async fn import_from_path(graph: &mut Graph, context: &ImportContext) -> Result<()> { + let file: Vec = context.io.load_file(context.model_path.as_path()).await?; + import_from_slice(file.as_slice(), graph, context).await +} + +async fn import_from_slice(slice: &[u8], graph: &mut Graph, context: &ImportContext) -> Result<()> { + let gltf: Gltf = Gltf::from_slice(slice)?; + let doc = gltf.document; + let data = gltf.blob; + let mut imports: ImportResults = ImportResults::default(); + let _ = imports + .buffers + .insert(import_buffers(&doc, data, context).await?); + let buffers: &[Vec] = imports.buffers.as_ref().unwrap().as_slice(); + let images: Vec = import_images(&doc, buffers)?; + let _ = imports + .textures + .insert(import_textures(&doc, images.as_slice(), context.as_texture_context()).await?); + let textures = imports.textures.as_ref().unwrap().as_slice(); + let _ = imports + .materials + .insert(import_materials(&doc, textures, &context.resource_manager).await?); + let materials = imports.materials.as_ref().unwrap().as_slice(); + let _ = imports.skins.insert(import_skins(&doc, &imports)?); + let _ = imports.meshes.insert(import_meshes( + &doc, + &context.model_path, + materials, + buffers, + )?); + let _ = imports + .families + .insert(import_nodes(&doc, graph, &imports)?); + link_child_nodes(&doc, graph, &imports)?; + let node_handles: Vec> = imports + .families + .as_ref() + .unwrap() + .iter() + .map(|f| f.main_node) + .collect(); + let animations = import_animations(&doc, &node_handles, buffers); + if !animations.is_empty() { + let mut anim_con = AnimationContainer::new(); + for animation in animations { + anim_con.add(animation); + } + AnimationPlayerBuilder::new(BaseBuilder::new().with_name("AnimationPlayer")) + .with_animations(anim_con) + .build(graph); + } + Ok(()) +} + +async fn import_buffers( + gltf: &Document, + mut data_chunk: Option>, + context: &ImportContext, +) -> Result>> { + let mut result: Vec> = Vec::with_capacity(gltf.buffers().len()); + for buf in gltf.buffers() { + match buf.source() { + gltf::buffer::Source::Bin => match data_chunk.take() { + Some(data) => result.push(data), + None => { + return Err(GltfLoadError::MissingEmbeddedBin); + } + }, + gltf::buffer::Source::Uri(uri) => result.push(load_bin_from_uri(uri, context).await?), + } + } + Ok(result) +} + +async fn load_bin_from_uri(uri: &str, context: &ImportContext) -> Result> { + let parsed_uri = uri::parse_uri(uri); + match parsed_uri.scheme { + uri::Scheme::Data if parsed_uri.data.is_some() => { + Ok(decode_base64(parsed_uri.data.unwrap())?) + } + uri::Scheme::None => load_external_bin(uri, context).await, + _ => Err(GltfLoadError::UnsupportedURI(uri.into())), + } +} + +async fn load_external_bin(path: &str, context: &ImportContext) -> Result> { + let parent = context + .model_path + .parent() + .ok_or(GltfLoadError::InvalidPath)? + .to_owned(); + let path = parent.join(path); + Ok(context.io.load_file(&path).await?) +} + +fn import_meshes( + gltf: &Document, + path: &Path, + mats: &[MaterialResource], + bufs: &[Vec], +) -> Result> { + let mut result: Vec = Vec::with_capacity(gltf.nodes().len()); + let mut stats = GeometryStatistics::default(); + for node in gltf.nodes() { + if let Some(mesh) = node.mesh() { + result.push(import_mesh(mesh, mats, bufs, &mut stats)?); + } + } + if cfg!(feature = "mesh_analysis") { + if stats.repeated_index_count > 0 { + Log::err(format!( + "{}: Model has triangles with repeated vertices: {}", + path.to_string_lossy(), + stats.repeated_index_count + )); + } else { + Log::info(format!( + "{}: Model has no repeated vertices.", + path.to_string_lossy() + )); + } + let min_length = stats.min_edge_length(); + if min_length == 0.0 { + Log::err(format!( + "{}: Mesh has a triangle with a zero-length edge!", + path.to_string_lossy() + )); + } else if min_length < f32::EPSILON { + Log::err(format!( + "{}: Mesh has a triangle with edge length: {}", + path.to_string_lossy(), + min_length + )); + } else { + Log::info(format!( + "{}: Smallest triangle edge: {}", + path.to_string_lossy(), + min_length + )); + } + } + Ok(result) +} + +fn import_mesh( + mesh: gltf::Mesh, + mats: &[MaterialResource], + bufs: &[Vec], + stats: &mut GeometryStatistics, +) -> Result { + #[cfg(feature = "gltf_blend_shapes")] + let morph_info = import_morph_info(&mesh)?; + #[cfg(not(feature = "gltf_blend_shapes"))] + let morph_info = BlendShapeInfoContainer::default(); + let mut surfs: Vec = Vec::with_capacity(mesh.primitives().len()); + let mut blend_shapes: Option> = None; + for prim in mesh.primitives() { + if let Some((surf, shapes)) = import_surface(prim, &morph_info, mats, bufs, stats)? { + surfs.push(surf); + blend_shapes.get_or_insert(shapes); + } + } + Ok(MeshData { + surfaces: surfs, + blend_shapes: blend_shapes.unwrap_or(Vec::new()), + }) +} + +#[cfg(feature = "gltf_blend_shapes")] +fn import_morph_info(mesh: &gltf::Mesh) -> Result { + let weights: &[f32] = mesh.weights().unwrap_or_default(); + let weights: Vec = weights.iter().map(|w| w * 100.0).collect(); + let extras = mesh.extras(); + let names = if let Some(extras) = extras { + let extras: serde_json::Value = serde_json::from_str(extras.get())?; + match extras { + serde_json::Value::Object(map) => { + if let Some(names) = map.get(TARGET_NAMES_KEY) { + match names { + serde_json::Value::Array(names) => { + values_to_strings(names.as_slice()).unwrap_or(Vec::default()) + } + _ => Vec::default(), + } + } else { + Vec::default() + } + } + _ => Vec::default(), + } + } else { + Vec::default() + }; + if extras.is_some() && names.is_empty() { + Log::warn(format!( + "glTF: Unable to extract blend shape names from JSON: {}", + extras.as_ref().unwrap().get() + )); + } + Ok(BlendShapeInfoContainer::new(names, weights)) +} + +#[cfg(feature = "gltf_blend_shapes")] +fn values_to_strings(values: &[serde_json::Value]) -> Option> { + let mut result: Vec = Vec::with_capacity(values.len()); + for v in values { + if let serde_json::Value::String(str) = v { + result.push(str.clone()); + } else { + return None; + } + } + Some(result) +} + +fn import_surface( + prim: gltf::Primitive, + morph_info: &BlendShapeInfoContainer, + mats: &[MaterialResource], + bufs: &[Vec], + stats: &mut GeometryStatistics, +) -> Result)>> { + if let Some(data) = build_surface_data(&prim, morph_info, bufs, stats)? { + let mut blend_shapes = Vec::new(); + if let Some(shape_con) = data.blend_shapes_container.as_ref() { + blend_shapes = shape_con.blend_shapes.clone(); + } + let mut surf = Surface::new(SurfaceSharedData::new(data)); + if let Some(mat_index) = prim.material().index() { + surf.set_material( + mats.get(mat_index) + .ok_or(GltfLoadError::InvalidIndex)? + .clone(), + ); + Ok(Some((surf, blend_shapes))) + } else { + Ok(None) + } + } else { + Ok(None) + } +} + +fn import_nodes( + doc: &gltf::Document, + graph: &mut Graph, + imports: &ImportResults, +) -> Result> { + let skins: &[SkinData] = imports.skins.as_ref().unwrap().as_slice(); + let mut result: Vec = Vec::with_capacity(doc.nodes().len()); + for node in doc.nodes() { + result.push(build_node_family(&node, skins, graph, imports)?); + } + for node in doc.nodes() { + let family: &NodeFamily = result + .get(node.index()) + .ok_or(GltfLoadError::InvalidIndex)?; + if let Some(mesh) = graph[family.main_node].cast_mut::() { + assign_bones_to_surfaces(node, mesh, result.as_slice())?; + } + } + Ok(result) +} + +fn build_node_family( + node: &gltf::Node, + skins: &[SkinData], + graph: &mut Graph, + imports: &ImportResults, +) -> Result { + let node_index = node.index(); + let skin_iter = SkinBoneIter::new(skins).filter(move |sb| sb.bone.node_index == node_index); + let mut bones: Vec = Vec::new(); + let mut new_handle: Option> = None; + let mut bone_children: Vec = Vec::new(); + let name = node.name().unwrap_or(""); + for pair in skin_iter { + if bones.is_empty() { + // Our first bone. Set the inv_bind_pose of the main node. + // We only create children if we later find an inv_bind_pose that does not match this. + let new_node = import_node(node, pair.bone.inv_bind_pose, imports)?; + let _ = new_handle.insert(graph.add_node(new_node)); + // Record that we have seen this inv_bind_pose. + bones.push(pair); + } else if let Some(p) = bones.iter().find(|p| p.bone == pair.bone) { + // We have a previously existing bone with the exact same inv_bind_pose. + // Do not record having seen this inv_bind_pose. + // Let the already recorded inv_bind_pose stand in for all bones with this inv_bind_pose. + let prev_skin_index = p.skin_index; + if let Some(prev_pair) = bone_children + .iter() + .find(|b| b.skin_index == prev_skin_index) + { + // We have a child for the skin of the previously existing bone. Re-use that child for this skin. + bone_children.push(SkinNodePair { + skin_index: pair.skin_index, + node: prev_pair.node, + }); + } + // Otherwise, the previously existing bone must be using the main node, so do nothing. + } else { + // We have a never-seen-before inv_bind_pose, so create a child for that inv_bind_pose. + let skin_index = pair.skin_index; + let base_builder = BaseBuilder::new() + .with_name(format!("{}:{}", name, skin_index)) + .with_inv_bind_pose_transform(pair.bone.inv_bind_pose); + let handle: Handle = graph.add_node(PivotBuilder::new(base_builder).build_node()); + bone_children.push(SkinNodePair { + skin_index, + node: handle, + }); + graph.link_nodes(handle, new_handle.unwrap()); + // Record that we have seen this inv_bind_pose. + bones.push(pair); + } + } + if let Some(handle) = new_handle { + Ok(NodeFamily { + main_node: handle, + bone_children, + }) + } else { + Ok(NodeFamily { + main_node: graph.add_node(import_node(node, Matrix4::identity(), imports)?), + bone_children: Vec::new(), + }) + } +} + +fn assign_bones_to_surfaces( + node: gltf::Node, + mesh: &mut Mesh, + families: &[NodeFamily], +) -> Result<()> { + if let Some(skin) = node.skin() { + let skin_index = skin.index(); + let mut bones: Vec> = Vec::with_capacity(skin.joints().len()); + for joint in skin.joints() { + let joint_family = families + .get(joint.index()) + .ok_or(GltfLoadError::InvalidIndex)?; + let bone_children = &joint_family.bone_children; + let handle = + if let Some(pair) = bone_children.iter().find(|p| p.skin_index == skin_index) { + pair.node + } else { + joint_family.main_node + }; + bones.push(handle); + } + for surf in mesh.surfaces_mut() { + surf.bones.set_value_and_mark_modified(bones.clone()); + } + } + Ok(()) +} + +fn import_node( + node: &gltf::Node, + inv_bind_pose: Matrix4, + imports: &ImportResults, +) -> Result { + let meshes: &[MeshData] = imports.meshes.as_ref().unwrap().as_slice(); + let trans = node.transform().decomposed(); + let trans_builder: TransformBuilder = TransformBuilder::new() + .with_local_position(trans.0.into()) + .with_local_rotation(Unit::new_normalize(trans.1.into())) + .with_local_scale(trans.2.into()); + let name = node.name().unwrap_or(""); + let base_builder = BaseBuilder::new() + .with_name(name) + .with_local_transform(trans_builder.build()) + .with_inv_bind_pose_transform(inv_bind_pose); + if let Some(mesh) = node.mesh() { + let mut mesh_builder = MeshBuilder::new(base_builder); + let mesh = meshes + .get(mesh.index()) + .ok_or(GltfLoadError::InvalidIndex)?; + mesh_builder = mesh_builder.with_blend_shapes(mesh.blend_shapes.clone()); + mesh_builder = mesh_builder.with_surfaces(mesh.surfaces.clone()); + Ok(mesh_builder.build_node()) + } else { + Ok(PivotBuilder::new(base_builder).build_node()) + } +} + +fn link_child_nodes(doc: &Document, graph: &mut Graph, imports: &ImportResults) -> Result<()> { + let families: &[NodeFamily] = imports.families.as_ref().unwrap().as_slice(); + for node in doc.nodes() { + let parent_family = families + .get(node.index()) + .ok_or(GltfLoadError::InvalidIndex)?; + for child in node.children() { + let child_family = families + .get(child.index()) + .ok_or(GltfLoadError::InvalidIndex)?; + graph.link_nodes(child_family.main_node, parent_family.main_node); + } + } + Ok(()) +} + +fn import_skins(doc: &gltf::Document, imports: &ImportResults) -> Result> { + let mut result: Vec = Vec::with_capacity(doc.skins().len()); + for skin in doc.skins() { + let bone_node_indices = skin.joints().map(|n| n.index()); + let bone_pairs = { + let reader = skin.reader(imports.get_buffer_data_access()); + if let Some(iter) = reader.read_inverse_bind_matrices() { + bone_node_indices + .zip(iter.map(Matrix4::from)) + .map(SkinBone::from) + .collect() + } else { + bone_node_indices + .zip(std::iter::repeat(Matrix4::identity())) + .map(SkinBone::from) + .collect() + } + }; + result.push(bone_pairs); + } + Ok(result) +} diff --git a/fyrox-impl/src/resource/gltf/node_names.rs b/fyrox-impl/src/resource/gltf/node_names.rs new file mode 100644 index 000000000..21406e4c3 --- /dev/null +++ b/fyrox-impl/src/resource/gltf/node_names.rs @@ -0,0 +1,192 @@ +use crate::core::log::Log; +use crate::core::pool::Handle; +use crate::core::NameProvider; +use crate::fxhash::{FxHashMap, FxHashSet}; +use crate::graph::BaseSceneGraph; +use crate::graph::SceneGraph; +use crate::scene::graph::Graph; +use crate::scene::node::Node; +use std::cell::RefCell; +use std::path::Path; + +#[derive(Debug)] +struct NodeName { + handle: Handle, + name: RefCell, +} + +/// Attempt to remove duplicate names by renaming the nodes of the graph +/// without depending on the order of sibling nodes. +/// +/// If a parent node has the same name as its child node, the child node +/// is renamed to "parent_name+1". Grandchildren are renamed to "parent_name+2" +/// and so on. +/// +/// If two nodes have the same name, but their parents have different names, +/// the nodes are renamed to "node_name+parentA" and "node_name+parentB". +/// +/// If the nodes have parents with the same names, then grandparent names may +/// be used if the grandparents have different names. If the whole ancestry +/// of the nodes is searched without finding ancestors with distinct names +/// then the nodes are not renamed. +/// +/// If after the whole procedure there is found to still be nodes with duplicate names, +/// an error is logged to alert the user to the problem. +pub fn resolve_name_conflicts(path: &Path, graph: &mut Graph) { + let node_sets: Vec>> = build_node_sets_from_graph(graph); + for nodes in node_sets { + if nodes.len() > 1 { + resolve_conflict(nodes, graph); + } + } + // Check if conflicts have actually been resolved. + let mut name_set: FxHashSet<&str> = FxHashSet::default(); + for node in graph.linear_iter() { + if !name_set.insert(node.name()) { + Log::err(format!( + "A node with existing name {} was found during the load of {} resource! \ + Do **NOT IGNORE** this message, please fix names in your model, otherwise \ + engine won't be able to correctly restore data from your resource!", + node.name(), + path.display() + )); + } + } +} + +fn build_node_sets_from_graph(graph: &Graph) -> Vec>> { + let mut name_map: FxHashMap<&str, Vec>> = FxHashMap::default(); + for (handle, node) in graph.pair_iter() { + let name = node.name(); + let list = name_map + .entry(name) + .or_insert_with(|| Vec::with_capacity(1)); + list.push(handle); + } + name_map.into_values().collect() +} + +fn build_node_sets_from_list(nodes: &Vec) -> Vec> { + let mut name_map: FxHashMap<&str, Vec<&NodeName>> = FxHashMap::default(); + let names: Vec<_> = nodes.iter().map(|n| n.name.borrow()).collect(); + for (node, name) in nodes.iter().zip(names.iter()) { + let list = name_map + .entry(name.as_str()) + .or_insert_with(|| Vec::with_capacity(1)); + list.push(node); + } + name_map.into_values().collect() +} + +fn resolve_conflict(handles: Vec>, graph: &mut Graph) { + // Resolve cases where parents have the same names as children. + let mut node_names = build_node_name_list(handles.as_slice(), graph); + for NodeName { handle, name } in node_names.iter_mut() { + let d = count_ancestor_depth(*handle, handles.as_slice(), graph); + if d > 0 { + name.borrow_mut().push('+'); + name.borrow_mut().push_str(d.to_string().as_str()); + } + } + // Build lists of nodes which still have duplicate names + let node_sets = build_node_sets_from_list(&mut node_names); + // Add ancestor names if that will eliminate duplicates. + for set in node_sets { + if set.len() > 1 { + if let Some(names) = find_distinct_ancestor_names(set.as_slice(), graph) { + for (node, name) in set.iter().zip(names.iter()) { + node.name.borrow_mut().push('+'); + node.name.borrow_mut().push_str(*name); + } + } + } + } + for node in node_names { + graph[node.handle].set_name(node.name.borrow().as_str()); + } +} + +fn build_node_name_list(handles: &[Handle], graph: &Graph) -> Vec { + handles + .iter() + .map(|h| NodeName { + handle: *h, + name: RefCell::new(graph[*h].name().to_owned()), + }) + .collect() +} + +fn find_distinct_ancestor_names<'a>( + node_names: &[&NodeName], + graph: &'a Graph, +) -> Option> { + for i in 1.. { + if let Some(names) = get_ancestor_names(node_names, i, graph) { + if !contains_duplicate_name(names.as_slice()) { + return Some(names); + } + } else { + return None; + } + } + None +} + +fn get_ancestor_names<'a>( + node_names: &[&NodeName], + depth: usize, + graph: &'a Graph, +) -> Option> { + let mut result: Vec<&'a str> = Vec::with_capacity(node_names.len()); + for n in node_names { + if let Some(n) = get_ancestor_name(n.handle, depth, graph) { + result.push(n); + } else { + return None; + } + } + Some(result) +} + +fn get_ancestor_name<'a>( + mut handle: Handle, + depth: usize, + graph: &'a Graph, +) -> Option<&'a str> { + for _ in 0..depth { + if let Some(n) = graph.try_get(handle) { + handle = n.parent(); + } else { + return None; + } + } + graph.try_get(handle).map(Node::name) +} + +fn contains_duplicate_name(names: &[&str]) -> bool { + let mut iter = names.iter(); + while let Some(n) = iter.next() { + let mut rest = iter.clone(); + if rest.any(|x| *n == *x) { + return true; + } + } + false +} + +fn count_ancestor_depth(mut handle: Handle, list: &[Handle], graph: &Graph) -> usize { + let mut count: usize = 0; + loop { + if let Some(node) = graph.try_get(handle) { + handle = node.parent(); + if list.iter().any(|h| *h == handle) { + count += 1; + } else { + break; + } + } else { + break; + } + } + count +} diff --git a/fyrox-impl/src/resource/gltf/surface.rs b/fyrox-impl/src/resource/gltf/surface.rs new file mode 100644 index 000000000..0f34d53cb --- /dev/null +++ b/fyrox-impl/src/resource/gltf/surface.rs @@ -0,0 +1,554 @@ +#![allow(unused)] +use crate::asset::core::math::TriangleDefinition; +use crate::core::algebra::{Vector2, Vector3, Vector4}; +use crate::core::log::Log; +use crate::core::math::Vector3Ext; +use crate::fxhash::FxHashMap; +use crate::scene::mesh; +use crate::scene::mesh::buffer::{ + self, TriangleBuffer, ValidationError, VertexAttributeUsage, VertexBuffer, VertexReadTrait, + VertexTrait, +}; +use crate::scene::mesh::surface::{InputBlendShapeData, SurfaceData}; +use crate::scene::mesh::vertex::{AnimatedVertex, SimpleVertex, StaticVertex}; +use gltf::buffer::Buffer; +use gltf::mesh::util::ReadColors; +use gltf::mesh::util::ReadIndices; +use gltf::mesh::util::ReadJoints; +use gltf::mesh::util::ReadTexCoords; +use gltf::mesh::Mode; +use gltf::mesh::Reader as GltfReader; +use gltf::mesh::Semantic; +use gltf::Accessor; +use gltf::Primitive; +use half::f16; + +use super::iter::*; + +use std::convert::TryFrom; +use std::num::TryFromIntError; + +/// This type represents any error that may occur while importing mesh data from glTF. +#[derive(Debug)] +pub enum SurfaceDataError { + /// The mesh data had more vertex positions than some other vertex attributes. + /// For example, there might be positions for 10 vertices but normals for only 9 vertices. + CountMismatch, + /// The mesh vertex data in the glTF files does not include position vectors. + MissingPosition, + /// The mesh vertex data in the glTF files does not include normal vectors. + MissingNormal, + /// The mesh vertex data in the glTF files does not include UV coordinates. + MissingTexCoords, + /// The mesh vertex data in the glTF files does not include bone weight values. + MissingBoneWeight, + /// The mesh vertex data in the glTF files does not include bone index values. + MissingBoneIndex, + /// Bone indices in glTF format can be stored as u8 or u16, but Fyrox only supports + /// bone indices in u8. This error is produced if an index is found which does not fit + /// into u8. + InvalidBoneIndex, + /// The glTF format includes options for drawing points and lines, but this module + /// only supports drawing triangles. + InvalidMode, + /// An internal error in a glTF file. The glTF format uses indices to allow one + /// resource to reference another within the same file. This error indicates that + /// one of those indices was out-of-bounds. This should never happen. + InvalidIndex, + /// An error in converting u32 to usize, or from usize to u32. + Int(TryFromIntError), + /// Depending on the geometry type, certain numbers of vertices are errors. + /// For example, if the geometry is a list of triangles, then the number of vertices + /// needs to be a multiple of three. This error indicates a glTF file had the wrong + /// number of vertices in some mesh. + InvalidVertexCount(GeometryType, u32), + /// An error may occur while constructing a mesh buffer. + Validation(ValidationError), + /// An error may occur while writing mesh data. + Fetch(buffer::VertexFetchError), +} + +impl From for SurfaceDataError { + fn from(error: ValidationError) -> Self { + SurfaceDataError::Validation(error) + } +} + +impl From for SurfaceDataError { + fn from(error: TryFromIntError) -> Self { + SurfaceDataError::Int(error) + } +} + +impl From for SurfaceDataError { + fn from(error: buffer::VertexFetchError) -> Self { + SurfaceDataError::Fetch(error) + } +} + +#[derive(Debug)] +pub enum GeometryType { + Triangles, + TriangleStrip, + TriangleFan, +} + +#[derive(Debug, Default, Clone)] +pub struct BlendShapeInfo { + pub name: String, + pub default_weight: f32, +} + +#[derive(Debug, Default, Clone)] +pub struct BlendShapeInfoContainer { + names: Vec, + weights: Vec, +} + +impl BlendShapeInfoContainer { + pub fn new(names: Vec, weights: Vec) -> Self { + BlendShapeInfoContainer { names, weights } + } + pub fn get(&self, index: usize) -> BlendShapeInfo { + BlendShapeInfo { + name: self + .names + .get(index) + .map(String::clone) + .unwrap_or_else(|| index.to_string()), + default_weight: self.weights.get(index).map(f32::clone).unwrap_or(0.0), + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct GeometryStatistics { + pub min_edge_length_squared: f32, + pub repeated_index_count: usize, +} + +impl GeometryStatistics { + pub fn update_length(&mut self, len: f32) { + if len < self.min_edge_length_squared { + self.min_edge_length_squared = len; + } + } + pub fn min_edge_length(&self) -> f32 { + self.min_edge_length_squared.sqrt() + } +} + +impl Default for GeometryStatistics { + fn default() -> Self { + GeometryStatistics { + min_edge_length_squared: f32::INFINITY, + repeated_index_count: 0, + } + } +} + +type Result = std::result::Result; +const DEFAULT_TANGENT: Vector4 = Vector4::new(0.0, 0.0, 0.0, 0.0); + +#[derive(Debug)] +enum IndexData { + Buffer(Vec), + Direct(u32), +} + +impl IndexData { + fn get_tri(&self, a: u32, b: u32, c: u32) -> Result<[u32; 3]> { + Ok([self.get(a)?, self.get(b)?, self.get(c)?]) + } + fn get(&self, source_index: u32) -> Result { + match self { + IndexData::Buffer(data) => Ok(data + .get(usize::try_from(source_index)?) + .ok_or(SurfaceDataError::InvalidIndex)? + .clone()), + IndexData::Direct(size) if source_index < *size => Ok(source_index), + _ => Err(SurfaceDataError::InvalidIndex), + } + } + fn len(&self) -> Result { + match self { + IndexData::Buffer(data) => Ok(u32::try_from(data.len())?), + IndexData::Direct(size) => Ok(*size), + } + } +} + +pub fn build_surface_data( + primitive: &Primitive, + morph_info: &BlendShapeInfoContainer, + buffers: &[Vec], + stats: &mut GeometryStatistics, +) -> Result> { + match primitive.mode() { + Mode::Points => { + return Ok(None); + } + Mode::Lines => { + return Ok(None); + } + Mode::LineLoop => { + return Ok(None); + } + Mode::LineStrip => { + return Ok(None); + } + Mode::Triangles => (), + Mode::TriangleStrip => (), + Mode::TriangleFan => (), + } + let vs: VertexBuffer = build_vertex_data(primitive, buffers)?; + let tris: TriangleBuffer = build_triangle_data(primitive, vs.vertex_count(), buffers)?; + #[cfg(feature = "mesh_analysis")] + update_statistics(&vs, &tris, stats); + #[cfg(feature = "gltf_blend_shapes")] + let morphs = build_morph_data(primitive, morph_info, buffers)?; + #[cfg(not(feature = "blend_shapes"))] + let morphs: Vec = Vec::new(); + let mut surf = if !morphs.is_empty() { + let shapes = mesh::surface::BlendShapesContainer::from_lists(&vs, morphs.as_slice()); + let mut surf = SurfaceData::new(vs, tris, false); + surf.blend_shapes_container.insert(shapes); + surf + } else { + SurfaceData::new(vs, tris, false) + }; + let has_tex = primitive.get(&Semantic::TexCoords(0)).is_some(); + let has_norm = primitive.get(&Semantic::Normals).is_some(); + let has_tang = primitive.get(&Semantic::Tangents).is_some(); + if has_tex && !has_norm { + surf.calculate_normals(); + surf.calculate_tangents(); + } else if has_tex && has_norm && !has_tang { + surf.calculate_tangents(); + } + Ok(Some(surf)) +} + +#[cfg(feature = "mesh_analysis")] +fn update_statistics( + vs: &VertexBuffer, + tris: &TriangleBuffer, + stats: &mut GeometryStatistics, +) -> Result<()> { + for tri in tris.iter() { + let indices = tri.0; + if tri[0] == tri[1] || tri[1] == tri[2] || tri[0] == tri[2] { + stats.repeated_index_count += 1; + } + stats.update_length(edge_length_squared(tri[0], tri[1], vs)?); + stats.update_length(edge_length_squared(tri[1], tri[2], vs)?); + stats.update_length(edge_length_squared(tri[0], tri[2], vs)?); + } + Ok(()) +} + +fn edge_length_squared(a: u32, b: u32, vs: &VertexBuffer) -> Result { + let a = usize::try_from(a)?; + let b = usize::try_from(b)?; + let a = vs.get(a).ok_or(SurfaceDataError::InvalidIndex)?; + let b = vs.get(b).ok_or(SurfaceDataError::InvalidIndex)?; + let a = a.read_3_f32(VertexAttributeUsage::Position)?; + let b = b.read_3_f32(VertexAttributeUsage::Position)?; + Ok(a.sqr_distance(&b)) +} + +#[cfg(feature = "gltf_blend_shapes")] +fn build_morph_data( + primitive: &Primitive, + morph_info: &BlendShapeInfoContainer, + buffers: &[Vec], +) -> Result> { + let reader = primitive.reader(|buf: Buffer| buffers.get(buf.index()).map(Vec::as_slice)); + let reader = reader.read_morph_targets(); + let mut result: Vec = Vec::new(); + for (i, (pos, norm, tang)) in reader.enumerate() { + let info = morph_info.get(i); + let positions = if let Some(iter) = pos { + iter_to_map(iter) + } else { + FxHashMap::default() + }; + let normals = if let Some(iter) = norm { + iter_to_map(iter) + } else { + FxHashMap::default() + }; + let tangents = if let Some(iter) = tang { + iter_to_map(iter) + } else { + FxHashMap::default() + }; + result.push(InputBlendShapeData { + default_weight: info.default_weight, + name: info.name, + positions, + normals, + tangents, + }); + } + Ok(result) +} + +fn iter_to_map(iter: I) -> FxHashMap> +where + I: Iterator, +{ + let mut map: FxHashMap> = FxHashMap::default(); + for (i, v) in iter.enumerate() { + if v == [0.0; 3] { + continue; + } + if let Ok(index) = u32::try_from(i) { + let v: [f16; 3] = v.map(f16::from_f32); + map.insert(index, Vector3::::from(v)); + } + } + map +} + +fn build_vertex_data(primitive: &Primitive, buffers: &[Vec]) -> Result { + build_vertex_data_with(primitive, |buf: Buffer| { + buffers.get(buf.index()).map(Vec::as_slice) + }) +} + +fn build_triangle_data( + primitive: &Primitive, + vertex_count: u32, + buffers: &[Vec], +) -> Result { + let reader = primitive.reader(|buf: Buffer| buffers.get(buf.index()).map(Vec::as_slice)); + let index_data = match reader.read_indices() { + Some(index_reader) => IndexData::Buffer(index_reader.into_u32().collect()), + None => IndexData::Direct(vertex_count), + }; + let tris: Vec = (match primitive.mode() { + Mode::Points => Err(SurfaceDataError::InvalidMode), + Mode::Lines => Err(SurfaceDataError::InvalidMode), + Mode::LineLoop => Err(SurfaceDataError::InvalidMode), + Mode::LineStrip => Err(SurfaceDataError::InvalidMode), + Mode::Triangles => build_triangles(&index_data), + Mode::TriangleStrip => build_triangle_strip(&index_data), + Mode::TriangleFan => build_triangle_fan(&index_data), + })?; + Ok(TriangleBuffer::new(tris)) +} + +fn build_triangles(data: &IndexData) -> Result> { + let vertex_count = data.len()?; + if vertex_count == 0 || vertex_count % 3 != 0 { + return Err(SurfaceDataError::InvalidVertexCount( + GeometryType::Triangles, + vertex_count, + )); + } + let tri_count: u32 = vertex_count / 3; + let mut tris: Vec = Vec::with_capacity(tri_count as usize); + for i in 0..tri_count { + let v: u32 = i * 3; + tris.push(TriangleDefinition(data.get_tri(v, v + 1, v + 2)?)); + } + Ok(tris) +} +fn build_triangle_strip(data: &IndexData) -> Result> { + let vertex_count = data.len()?; + if vertex_count < 3 { + return Err(SurfaceDataError::InvalidVertexCount( + GeometryType::TriangleStrip, + vertex_count, + )); + } + let tri_count: u32 = vertex_count - 2; + let mut tris: Vec = Vec::with_capacity(tri_count as usize); + for i in 0..tri_count { + let odd = i % 2; + tris.push(TriangleDefinition(data.get_tri( + i, + i + 1 + odd, + i + 2 - odd, + )?)); + } + Ok(tris) +} +fn build_triangle_fan(data: &IndexData) -> Result> { + let vertex_count = data.len()?; + if vertex_count < 3 { + return Err(SurfaceDataError::InvalidVertexCount( + GeometryType::TriangleFan, + vertex_count, + )); + } + let tri_count: u32 = vertex_count - 2; + let mut tris: Vec = Vec::with_capacity(tri_count as usize); + for i in 0..tri_count { + tris.push(TriangleDefinition(data.get_tri(i + 1, i + 2, 0)?)); + } + Ok(tris) +} + +fn build_vertex_data_with<'a, 's, F>( + primitive: &'a Primitive, + get_buffer_data: F, +) -> Result +where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, +{ + let reader = primitive.reader(get_buffer_data.clone()); + if reader.read_weights(0).is_some() { + let vs: Vec = AnimatedVertex::convert(primitive, get_buffer_data)?; + Ok(VertexBuffer::new(vs.len(), vs)?) + } else if reader.read_normals().is_some() { + let vs: Vec = StaticVertex::convert(primitive, get_buffer_data)?; + Ok(VertexBuffer::new(vs.len(), vs)?) + } else { + let vs: Vec = SimpleVertex::convert(primitive, get_buffer_data)?; + Ok(VertexBuffer::new(vs.len(), vs)?) + } +} + +trait GltfVertexConvert: VertexTrait { + fn convert<'a, 's, F>(primitive: &'a Primitive, get_buffer_data: F) -> Result> + where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>; +} + +impl GltfVertexConvert for SimpleVertex { + fn convert<'a, 's, F>(primitive: &'a Primitive, get_buffer_data: F) -> Result> + where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, + { + let reader = primitive.reader(get_buffer_data); + if let Some(iter) = reader.read_positions() { + Ok(iter + .map(Vector3::from) + .map(|v| SimpleVertex { position: v }) + .collect()) + } else { + Err(SurfaceDataError::MissingPosition) + } + } +} + +impl GltfVertexConvert for StaticVertex { + fn convert<'a, 's, F>(primitive: &'a Primitive, get_buffer_data: F) -> Result> + where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, + { + let reader = primitive.reader(get_buffer_data); + let mut pos_iter = reader + .read_positions() + .ok_or(SurfaceDataError::MissingPosition)?; + let mut norm_iter = reader + .read_normals() + .ok_or(SurfaceDataError::MissingNormal)?; + let mut tang_iter = reader.read_tangents(); + let mut uv_iter = reader + .read_tex_coords(0) + .ok_or(SurfaceDataError::MissingTexCoords)? + .into_f32(); + let mut result: Vec = Vec::with_capacity(pos_iter.len()); + for pos in pos_iter { + let pos: Vector3 = Vector3::from(pos); + let norm: Option> = norm_iter.next().map(Vector3::from); + let uv: Option> = uv_iter.next().map(Vector2::from); + let tang: Option> = if let Some(iter) = tang_iter.as_mut() { + iter.next().map(Vector4::from) + } else { + Some(DEFAULT_TANGENT) + }; + if let (Some(normal), Some(tex_coord), Some(tangent)) = (norm, uv, tang) { + result.push(StaticVertex { + position: pos, + normal, + tex_coord, + tangent, + }); + } else { + return Err(SurfaceDataError::CountMismatch); + } + } + Ok(result) + } +} + +impl GltfVertexConvert for AnimatedVertex { + fn convert<'a, 's, F>(primitive: &'a Primitive, get_buffer_data: F) -> Result> + where + F: Clone + Fn(Buffer<'a>) -> Option<&'s [u8]>, + { + let reader = primitive.reader(get_buffer_data); + let mut pos_iter = reader + .read_positions() + .ok_or(SurfaceDataError::MissingPosition)?; + let mut norm_iter = reader + .read_normals() + .ok_or(SurfaceDataError::MissingNormal)?; + let mut tang_iter = reader.read_tangents(); + let mut uv_iter = reader + .read_tex_coords(0) + .ok_or(SurfaceDataError::MissingTexCoords)? + .into_f32(); + let mut wgt_iter = reader + .read_weights(0) + .ok_or(SurfaceDataError::MissingBoneWeight)? + .into_f32(); + let mut jnt_iter = reader + .read_joints(0) + .ok_or(SurfaceDataError::MissingBoneIndex)?; + let mut result: Vec = Vec::with_capacity(pos_iter.len()); + for pos in pos_iter { + let pos: Vector3 = Vector3::from(pos); + let norm: Option> = norm_iter.next().map(Vector3::from); + let uv: Option> = uv_iter.next().map(Vector2::from); + let bone_weights: Option<[f32; 4]> = wgt_iter.next(); + let bone_indices: Option<[u8; 4]> = read_valid_index(&mut jnt_iter)?; + let tang: Option> = if let Some(iter) = tang_iter.as_mut() { + iter.next().map(Vector4::from) + } else { + Some(DEFAULT_TANGENT) + }; + if let ( + Some(normal), + Some(tex_coord), + Some(tangent), + Some(bone_weights), + Some(bone_indices), + ) = (norm, uv, tang, bone_weights, bone_indices) + { + result.push(AnimatedVertex { + position: pos, + normal, + tex_coord, + tangent, + bone_weights, + bone_indices, + }); + } else { + return Err(SurfaceDataError::CountMismatch); + } + } + Ok(result) + } +} + +fn read_valid_index(reader: &mut ReadJoints) -> Result> { + match reader { + ReadJoints::U8(iter) => Ok(iter.next()), + ReadJoints::U16(iter) => { + if let Some(value) = iter.next() { + let mut result: [u8; 4] = [0; 4]; + for (i, v) in value.iter().enumerate() { + result[i] = u8::try_from(*v).or(Err(SurfaceDataError::InvalidBoneIndex))?; + } + Ok(Some(result)) + } else { + Ok(None) + } + } + } +} diff --git a/fyrox-impl/src/resource/gltf/uri.rs b/fyrox-impl/src/resource/gltf/uri.rs new file mode 100644 index 000000000..c2c4321ca --- /dev/null +++ b/fyrox-impl/src/resource/gltf/uri.rs @@ -0,0 +1,182 @@ +use base64::engine::general_purpose::URL_SAFE_NO_PAD as Base64Engine; +use base64::engine::Engine as _; + +/// A breakdown of the data contained within a URI. +#[derive(Copy, Clone, Debug)] +pub struct Uri<'a> { + /// Reference to the full URI str. + pub original: &'a str, + /// The recognized scheme type, if any. + pub scheme: Scheme, + /// The text that was used to identify the scheme, preceeding the first ':', if any. + /// For example, if the URI were "https://www.somewebsite.org/books/RestInPractice.pdf" + /// then `scheme_name` would be the "https" from the beginning. + pub scheme_name: Option<&'a str>, + /// Everything that follows the initial ':', or the whole original str if there is no ':'. + /// If the URI were "https://www.somewebsite.org/books/RestInPractice.pdf" + /// then `after_scheme` would be "www.somewebsite.org/books/RestInPractice.pdf". + pub after_scheme: &'a str, + /// If the scheme is "data" then `data_type` is the slice between the first ':' and the first ','. + /// For example, if the URI were ""data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAF", then + /// `data_type` would be "image/png;base64". If there is no ',' then `data_type` is the same as + /// `after_scheme`. If the scheme is not "data" then `data_type` is None. + pub data_type: Option<&'a str>, + /// If the scheme is "data" then `data` is everything following the first ','. + /// `data_type` is None if the scheme is not "data" or if there is no ','. + pub data: Option<&'a str>, +} + +/// One of the scheme types that the parser can recognize. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Scheme { + /// A file URI indicates that this is an absolute path to a file. + /// This method of identifying files is not supported. + File, + /// A data URI contains image or model data encoded in base64. + Data, + /// There was no ':' to identify a scheme name, which means that this URI + /// will be interpreted as a relative file path and the file system will + /// be searched for the named asset. + None, + /// A scheme name was specified using a ':', but it was not among these + /// expected schemes. + Other, +} + +/// Turn a URI str slice into a Uri structure containing the parts of the URI. +pub fn parse_uri(source: &str) -> Uri { + let (scheme_name, after_scheme): (Option<&str>, &str) = { + let mut scheme_it = source.splitn(2, ':'); + let first = scheme_it.next(); + let second = scheme_it.next(); + if let Some(rest) = second { + (first, rest) + } else { + (None, first.unwrap()) + } + }; + if scheme_name == Some("data") { + let mut it = after_scheme.splitn(2, ','); + Uri { + original: source, + scheme: Scheme::Data, + scheme_name, + after_scheme, + data_type: it.next(), + data: it.next(), + } + } else if let Some(name) = scheme_name { + let scheme = match name { + "file" => Scheme::File, + _ => Scheme::Other, + }; + Uri { + original: source, + scheme, + scheme_name, + after_scheme, + data_type: None, + data: None, + } + } else { + Uri { + original: source, + scheme: Scheme::None, + scheme_name, + after_scheme, + data_type: None, + data: None, + } + } +} + +pub fn decode_base64(source: &str) -> Result, base64::DecodeError> { + Base64Engine.decode(source) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn split() { + let r: Vec<_> = "abc:def".splitn(2, ':').collect(); + assert!(r == vec!["abc", "def"], "{:?}", r); + } + #[test] + fn simple() { + let u: Uri = parse_uri("Random stuff"); + assert!( + matches!( + u, + Uri { + original: "Random stuff", + scheme: Scheme::None, + scheme_name: None, + after_scheme: "Random stuff", + data_type: None, + data: None, + } + ), + "{:?}", + u + ) + } + #[test] + fn file() { + let u: Uri = parse_uri("file:filename.test"); + assert!( + matches!( + u, + Uri { + original: "file:filename.test", + scheme: Scheme::File, + scheme_name: Some("file"), + after_scheme: "filename.test", + data_type: None, + data: None, + } + ), + "{:?}", + u + ) + } + #[test] + fn other() { + let u: Uri = parse_uri("what:Stuff"); + assert!( + matches!( + u, + Uri { + original: "what:Stuff", + scheme: Scheme::Other, + scheme_name: Some("what"), + after_scheme: "Stuff", + data_type: None, + data: None, + } + ), + "{:?}", + u + ) + } + #[test] + fn data() { + let u: Uri = parse_uri("data:type,ABCDEFG"); + assert!( + matches!( + u, + Uri { + original: "data:type,ABCDEFG", + scheme: Scheme::Data, + scheme_name: Some("data"), + after_scheme: "type,ABCDEFG", + data_type: Some("type"), + data: Some("ABCDEFG"), + } + ), + "{:?}", + u + ) + } +} diff --git a/fyrox-impl/src/resource/mod.rs b/fyrox-impl/src/resource/mod.rs index f6e23556d..e71b279fc 100644 --- a/fyrox-impl/src/resource/mod.rs +++ b/fyrox-impl/src/resource/mod.rs @@ -4,5 +4,7 @@ pub mod curve; pub mod fbx; +#[cfg(feature = "gltf")] +pub mod gltf; pub mod model; pub mod texture; diff --git a/fyrox-impl/src/utils/astar.rs b/fyrox-impl/src/utils/astar.rs index bdc1eb996..db863be4c 100644 --- a/fyrox-impl/src/utils/astar.rs +++ b/fyrox-impl/src/utils/astar.rs @@ -698,7 +698,7 @@ mod test { pathfinder.remove_vertex(0); - assert_eq!(pathfinder.vertex(0).unwrap().neighbours, vec![]); + assert_eq!(pathfinder.vertex(0).unwrap().neighbours, Vec::::new()); assert_eq!(pathfinder.vertex(1), None); assert_eq!(pathfinder.vertex(2), None); } @@ -721,7 +721,7 @@ mod test { pathfinder.insert_vertex(0, GraphVertex::new(Vector3::new(1.0, 1.0, 1.0))); - assert_eq!(pathfinder.vertex(0).unwrap().neighbours, vec![]); + assert_eq!(pathfinder.vertex(0).unwrap().neighbours, Vec::::new()); assert_eq!(pathfinder.vertex(1).unwrap().neighbours, vec![2, 3]); assert_eq!(pathfinder.vertex(2).unwrap().neighbours, vec![1, 3]); assert_eq!(pathfinder.vertex(3).unwrap().neighbours, vec![2, 1]); diff --git a/fyrox/Cargo.toml b/fyrox/Cargo.toml index b721168b6..fb672f4a1 100644 --- a/fyrox/Cargo.toml +++ b/fyrox/Cargo.toml @@ -6,6 +6,9 @@ edition = "2021" [features] default = ["fyrox-impl"] dylib = ["fyrox-dylib"] +gltf = ["fyrox-impl/gltf", "fyrox-dylib/gltf"] +mesh_analysis = ["fyrox-impl/mesh_analysis", "fyrox-dylib/mesh_analysis"] +gltf_blend_shapes = ["fyrox-impl/gltf_blend_shapes", "fyrox-dylib/gltf_blend_shapes"] [dependencies] fyrox-impl = { version = "0.33.1", path = "../fyrox-impl", optional = true }