diff --git a/Cargo.toml b/Cargo.toml index a6c8512724..1d00b962c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ optional = ["audio", "network", "locale", "ui", "tiles", "animation"] tiles = ["amethyst_tiles"] animation = ["amethyst_animation"] audio = ["amethyst_audio"] -#gltf = ["amethyst_gltf", "amethyst_animation"] +gltf = ["amethyst_gltf", "amethyst_animation"] locale = ["amethyst_locale"] network = ["amethyst_network"] utils = ["amethyst_utils"] @@ -56,6 +56,7 @@ profiler = [ "amethyst_ui/profiler", "amethyst_utils/profiler", "amethyst_tiles/profiler", + "amethyst_gltf/profiler", ] # sdl_controller = ["amethyst_input/sdl_controller"] json = ["amethyst_assets/json"] @@ -69,7 +70,6 @@ parallel = ["amethyst_core/parallel"] [workspace] members = ["examples/*", "amethyst_*"] exclude = [ - "amethyst_gltf", "amethyst_test", "examples/Cargo.toml", "examples/_unused_assets", @@ -102,7 +102,7 @@ amethyst_core = { path = "amethyst_core", version = "0.15.3" } amethyst_error = { path = "amethyst_error", version = "0.15.3" } amethyst_controls = { path = "amethyst_controls", version = "0.15.3" } amethyst_derive = { path = "amethyst_derive", version = "0.15.3" } -#amethyst_gltf = { path = "amethyst_gltf", version = "0.15.3", optional = true } +amethyst_gltf = { path = "amethyst_gltf", version = "0.15.3", optional = true } amethyst_network = { path = "amethyst_network", version = "0.15.3", optional = true } amethyst_locale = { path = "amethyst_locale", version = "0.15.3", optional = true } amethyst_rendy = { path = "amethyst_rendy", version = "0.15.3", features = ["window"], optional = true } diff --git a/amethyst_assets/src/asset.rs b/amethyst_assets/src/asset.rs index dcc65366a4..60d0163ff3 100644 --- a/amethyst_assets/src/asset.rs +++ b/amethyst_assets/src/asset.rs @@ -52,7 +52,7 @@ impl> ProcessableAsset for T { /// in turn accepted by `Asset::from_data`. Examples for formats are /// `Png`, `Obj` and `Wave`. /// -/// The format type itself represents loading options, which are passed to `import`. +/// The format type itself represents loading options, which are passed to `import_simple`. /// E.g. for textures this would be stuff like mipmap levels and /// sampler info. pub trait Format: DynClone + Send + Sync + 'static { diff --git a/amethyst_gltf/Cargo.toml b/amethyst_gltf/Cargo.toml index 980dcb1d9d..0694761ebf 100644 --- a/amethyst_gltf/Cargo.toml +++ b/amethyst_gltf/Cargo.toml @@ -20,21 +20,33 @@ amethyst_animation = { path = "../amethyst_animation", version = "0.15.3" } amethyst_core = { path = "../amethyst_core", version = "0.15.3" } amethyst_error = { path = "../amethyst_error", version = "0.15.3" } amethyst_rendy = { path = "../amethyst_rendy", version = "0.15.3" } +atelier-assets = { git = "https://github.com/radium-io/atelier-assets.git", rev = "6987e6b4abe061f1810018efd7518866b62b70fb", features = [ + "serde-1", + "type_uuid", + "serde_importers", + "parallel_hash", + "rpc_io", + "handle", +] } err-derive = "0.2.3" base64 = "0.11" fnv = "1" -gltf = { version = "0.15", features = ["KHR_lights_punctual"] } +gltf = { git = "https://github.com/gltf-rs/gltf.git", rev = "4298aac70336ae0413312a61344badbf67fbcbde", features = ["KHR_lights_punctual"] } hibitset = { version = "0.6.2", features = ["parallel"] } log = "0.4" mikktspace = "0.2.0" serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +type-uuid = "0.1" +uuid = { version = "0.8", features = ["v4"] } thread_profiler = { version = "0.3", optional = true } -image = "0.22.2" +image = "0.23.12" derivative = "2.1.1" [dev-dependencies] amethyst = { path = "../", version = "0.15.3", features = ["renderer"] } +futures = "0.3" [features] profiler = ["thread_profiler/thread_profiler"] diff --git a/amethyst_gltf/src/format/animation.rs b/amethyst_gltf/src/format/animation.rs deleted file mode 100644 index 6364977fff..0000000000 --- a/amethyst_gltf/src/format/animation.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::collections::HashMap; - -use amethyst_animation::{ - AnimationPrefab, AnimationSetPrefab, InterpolationFunction, InterpolationPrimitive, Sampler, - SamplerPrimitive, TransformChannel, -}; -use amethyst_core::{ - math::{convert, Vector3, Vector4}, - Transform, -}; -use amethyst_error::Error; - -use super::Buffers; -use crate::error; - -pub fn load_animations( - gltf: &gltf::Gltf, - buffers: &Buffers, - node_map: &HashMap, -) -> Result, Error> { - let mut prefab = AnimationSetPrefab::default(); - for animation in gltf.animations() { - let anim = load_animation(&animation, buffers)?; - if anim - .samplers - .iter() - .any(|sampler| node_map.contains_key(&sampler.0)) - { - prefab.animations.push((animation.index(), anim)); - } - } - Ok(prefab) -} - -fn load_animation( - animation: &gltf::Animation<'_>, - buffers: &Buffers, -) -> Result, Error> { - let mut a = AnimationPrefab::default(); - a.samplers = animation - .channels() - .map(|ref channel| load_channel(channel, buffers)) - .collect::, Error>>()?; - Ok(a) -} - -fn load_channel( - channel: &gltf::animation::Channel<'_>, - buffers: &Buffers, -) -> Result<(usize, TransformChannel, Sampler>), Error> { - use gltf::animation::util::ReadOutputs::*; - let sampler = channel.sampler(); - let target = channel.target(); - - let reader = channel.reader(|buffer| buffers.buffer(&buffer)); - let input = reader - .read_inputs() - .ok_or(error::Error::MissingInputs)? - .collect(); - let node_index = target.node().index(); - - match reader.read_outputs().ok_or(error::Error::MissingOutputs)? { - Translations(translations) => Ok(( - node_index, - TransformChannel::Translation, - Sampler { - input, - function: map_interpolation_type(sampler.interpolation()), - output: translations - .map(Vector3::from) - .map(|t| convert::<_, Vector3>(t).into()) - .collect(), - }, - )), - Rotations(rotations) => { - let ty = map_interpolation_type(sampler.interpolation()); - let ty = if ty == InterpolationFunction::Linear { - InterpolationFunction::SphericalLinear - } else { - ty - }; - Ok(( - node_index, - TransformChannel::Rotation, - Sampler { - input, - function: ty, - output: rotations - .into_f32() - .map(Vector4::from) - .map(|q| convert::<_, Vector4>(q).into()) - .collect(), - }, - )) - } - Scales(scales) => Ok(( - node_index, - TransformChannel::Scale, - Sampler { - input, - function: map_interpolation_type(sampler.interpolation()), - output: scales - .map(Vector3::from) - .map(|s| convert::<_, Vector3>(s).into()) - .collect(), - }, - )), - MorphTargetWeights(_) => Err(error::Error::NotImplemented.into()), - } -} - -fn map_interpolation_type(ty: gltf::animation::Interpolation) -> InterpolationFunction -where - T: InterpolationPrimitive, -{ - use gltf::animation::Interpolation::*; - - match ty { - Linear => InterpolationFunction::Linear, - Step => InterpolationFunction::Step, - CubicSpline => InterpolationFunction::CubicSpline, - } -} diff --git a/amethyst_gltf/src/format/importer.rs b/amethyst_gltf/src/format/importer.rs deleted file mode 100644 index 8cf5546002..0000000000 --- a/amethyst_gltf/src/format/importer.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use amethyst_assets::Source as AssetSource; -use amethyst_error::Error; -use gltf::{self, json, Gltf}; - -use crate::error; - -#[derive(Debug)] -pub enum ImageFormat { - Png, - Jpeg, -} - -impl ImageFormat { - fn from_mime_type(mime: &str) -> Self { - match mime { - "image/jpeg" => ImageFormat::Jpeg, - "image/png" => ImageFormat::Png, - _ => unreachable!(), - } - } -} - -/// Buffer data returned from `import`. -#[derive(Clone, Debug)] -pub struct Buffers(Vec>); - -#[allow(unused)] -impl Buffers { - /// Obtain the contents of a loaded buffer. - pub fn buffer(&self, buffer: &gltf::Buffer<'_>) -> Option<&[u8]> { - self.0.get(buffer.index()).map(Vec::as_slice) - } - - /// Obtain the contents of a loaded buffer view. - pub fn view(&self, view: &gltf::buffer::View<'_>) -> Option<&[u8]> { - self.buffer(&view.buffer()).map(|data| { - let begin = view.offset(); - let end = begin + view.length(); - &data[begin..end] - }) - } - - /// Take the loaded buffer data. - pub fn take(self) -> Vec> { - self.0 - } -} - -/// Imports glTF 2.0 -pub fn import

(source: Arc, path: P) -> Result<(Gltf, Buffers), Error> -where - P: AsRef, -{ - let path = path.as_ref(); - let data = read_to_end(source.clone(), path)?; - if data.starts_with(b"glTF") { - import_binary(&data, source, path) - } else { - import_standard(&data, source, path) - } -} - -fn read_to_end>(source: Arc, path: P) -> Result, Error> { - let path = path.as_ref(); - source.load( - path.to_str() - .expect("Path contains invalid UTF-8 charcters"), - ) -} - -fn parse_data_uri(uri: &str) -> Result, Error> { - let encoded = uri.split(',').nth(1).expect("URI does not contain ','"); - let decoded = base64::decode(&encoded)?; - Ok(decoded) -} - -fn load_external_buffers( - source: Arc, - base_path: &Path, - gltf: &Gltf, - mut bin: Option>, -) -> Result>, Error> { - use gltf::buffer::Source; - let mut buffers = vec![]; - for (index, buffer) in gltf.buffers().enumerate() { - let data = match buffer.source() { - Source::Uri(uri) => { - if uri.starts_with("data:") { - parse_data_uri(uri)? - } else { - let path = base_path - .parent() - .unwrap_or_else(|| Path::new("./")) - .join(uri); - read_to_end(source.clone(), &path)? - } - } - Source::Bin => bin - .take() - .expect("`BIN` section of binary glTF file is empty or used by another buffer"), - }; - - if data.len() < buffer.length() { - let path = json::Path::new().field("buffers").index(index); - return Err(error::Error::BufferLength(path).into()); - } - buffers.push(data); - } - Ok(buffers) -} - -fn import_standard( - data: &[u8], - source: Arc, - base_path: &Path, -) -> Result<(Gltf, Buffers), Error> { - let gltf = Gltf::from_slice(data)?; - let buffers = Buffers(load_external_buffers(source, base_path, &gltf, None)?); - Ok((gltf, buffers)) -} - -fn import_binary( - data: &[u8], - source: Arc, - base_path: &Path, -) -> Result<(Gltf, Buffers), Error> { - let gltf::binary::Glb { json, bin, .. } = gltf::binary::Glb::from_slice(data)?; - let gltf = Gltf::from_slice(&json)?; - let bin = bin.map(|x| x.to_vec()); - let buffers = Buffers(load_external_buffers(source, base_path, &gltf, bin)?); - Ok((gltf, buffers)) -} - -pub fn get_image_data( - image: &gltf::Image<'_>, - buffers: &Buffers, - source: Arc, - base_path: &Path, -) -> Result<(Vec, ImageFormat), Error> { - use gltf::image::Source; - match image.source() { - Source::View { view, mime_type } => { - let data = buffers - .view(&view) - .expect("`view` of image data points to a buffer which does not exist"); - Ok((data.to_vec(), ImageFormat::from_mime_type(mime_type))) - } - - Source::Uri { uri, mime_type } => { - if uri.starts_with("data:") { - let data = parse_data_uri(uri)?; - if let Some(ty) = mime_type { - Ok((data, ImageFormat::from_mime_type(ty))) - } else { - let mimetype = uri - .split(',') - .next() - .expect("Unreachable: `split` will always return at least one element") - .split(':') - .nth(1) - .expect("URI does not contain ':'") - .split(';') - .next() - .expect("Unreachable: `split` will always return at least one element"); - Ok((data, ImageFormat::from_mime_type(mimetype))) - } - } else { - let path = base_path - .parent() - .unwrap_or_else(|| Path::new("./")) - .join(uri); - let data = source.load( - path.to_str() - .expect("Path contains invalid UTF-8 characters"), - )?; - if let Some(ty) = mime_type { - Ok((data, ImageFormat::from_mime_type(ty))) - } else { - let ext = path - .extension() - .and_then(|s| s.to_str()) - .map_or("".to_string(), |s| s.to_ascii_lowercase()); - let format = match &ext[..] { - "jpg" | "jpeg" => ImageFormat::Jpeg, - "png" => ImageFormat::Png, - _ => unreachable!(), - }; - Ok((data, format)) - } - } - } - } -} diff --git a/amethyst_gltf/src/format/material.rs b/amethyst_gltf/src/format/material.rs deleted file mode 100644 index da99728058..0000000000 --- a/amethyst_gltf/src/format/material.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::sync::Arc; - -use amethyst_assets::Source; -use amethyst_error::Error; -use amethyst_rendy::{ - formats::{mtl::MaterialPrefab, texture::TexturePrefab}, - palette::{LinSrgba, Srgba}, - rendy::{ - hal, - texture::{ - image::{load_from_image, ImageFormat as DataFormat, ImageTextureConfig, Repr}, - palette::{load_from_linear_rgba, load_from_srgba}, - MipLevels, TextureBuilder, - }, - }, -}; -use gltf::{self, material::AlphaMode}; - -use super::{get_image_data, Buffers, ImageFormat as ImportDataFormat}; - -// Load a single material, and transform into a format usable by the engine -pub fn load_material( - material: &gltf::Material<'_>, - buffers: &Buffers, - source: Arc, - name: &str, -) -> Result { - let mut prefab = MaterialPrefab::default(); - - let pbr = material.pbr_metallic_roughness(); - - prefab.albedo = Some( - load_texture_with_factor( - pbr.base_color_texture(), - pbr.base_color_factor(), - buffers, - source.clone(), - name, - true, - ) - .map(|(texture, _)| TexturePrefab::Data(texture.into()))?, - ); - - // metallic from B channel - // roughness from G channel - let metallic_roughness = load_texture_with_factor( - pbr.metallic_roughness_texture(), - [1.0, pbr.roughness_factor(), pbr.metallic_factor(), 1.0], - buffers, - source.clone(), - name, - false, - )? - .0; - - prefab.metallic_roughness = Some(TexturePrefab::Data(metallic_roughness.into())); - - let em_factor = material.emissive_factor(); - prefab.emission = Some(TexturePrefab::Data( - load_texture_with_factor( - material.emissive_texture(), - [em_factor[0], em_factor[1], em_factor[2], 1.0], - buffers, - source.clone(), - name, - true, - )? - .0 - .into(), - )); - - // Can't use map/and_then because of Result returning from the load_texture function - prefab.normal = match material.normal_texture() { - Some(normal_texture) => { - Some( - load_texture( - &normal_texture.texture(), - buffers, - source.clone(), - name, - false, - ) - .map(|data| TexturePrefab::Data(data.into()))?, - ) - } - - None => None, - }; - - // Can't use map/and_then because of Result returning from the load_texture function - prefab.ambient_occlusion = match material.occlusion_texture() { - Some(occlusion_texture) => { - Some( - load_texture( - &occlusion_texture.texture(), - buffers, - source.clone(), - name, - false, - ) - .map(|data| TexturePrefab::Data(data.into()))?, - ) - } - - None => None, - }; - - match material.alpha_mode() { - AlphaMode::Blend => { - prefab.transparent = true; - } - AlphaMode::Mask => { - prefab.alpha_cutoff = material.alpha_cutoff(); - } - AlphaMode::Opaque => { - prefab.alpha_cutoff = 0.0; - } - } - Ok(prefab) -} - -fn load_texture_with_factor( - texture: Option>, - factor: [f32; 4], - buffers: &Buffers, - source: Arc, - name: &str, - srgb: bool, -) -> Result<(TextureBuilder<'static>, [f32; 4]), Error> { - match texture { - Some(info) => { - Ok(( - load_texture(&info.texture(), buffers, source, name, srgb)? - .with_mip_levels(MipLevels::GenerateAuto), - factor, - )) - } - None => { - Ok(( - if srgb { - load_from_srgba(Srgba::new(factor[0], factor[1], factor[2], factor[3])) - } else { - load_from_linear_rgba(LinSrgba::new(factor[0], factor[1], factor[2], factor[3])) - }, - [1.0, 1.0, 1.0, 1.0], - )) - } - } -} - -fn load_texture( - texture: &gltf::Texture<'_>, - buffers: &Buffers, - source: Arc, - name: &str, - srgb: bool, -) -> Result, Error> { - let (data, format) = get_image_data(&texture.source(), buffers, source, name.as_ref())?; - - let metadata = ImageTextureConfig { - repr: if srgb { Repr::Srgb } else { Repr::Unorm }, - format: match format { - ImportDataFormat::Png => Some(DataFormat::PNG), - ImportDataFormat::Jpeg => Some(DataFormat::JPEG), - }, - sampler_info: load_sampler_info(&texture.sampler()), - ..Default::default() - }; - - load_from_image(std::io::Cursor::new(&data), metadata).map_err(|e| e.into()) -} - -fn load_sampler_info(sampler: &gltf::texture::Sampler<'_>) -> hal::image::SamplerDesc { - use gltf::texture::{MagFilter, MinFilter}; - use hal::image::{Filter, SamplerDesc}; - - let mag_filter = match sampler.mag_filter() { - Some(MagFilter::Nearest) => Filter::Nearest, - None | Some(MagFilter::Linear) => Filter::Linear, - }; - - let (min_filter, mip_filter) = match sampler.min_filter() { - Some(MinFilter::Nearest) | Some(MinFilter::NearestMipmapNearest) => { - (Filter::Nearest, Filter::Nearest) - } - None | Some(MinFilter::Linear) | Some(MinFilter::LinearMipmapLinear) => { - (Filter::Linear, Filter::Linear) - } - Some(MinFilter::NearestMipmapLinear) => (Filter::Nearest, Filter::Linear), - Some(MinFilter::LinearMipmapNearest) => (Filter::Linear, Filter::Nearest), - }; - - let wrap_s = map_wrapping(sampler.wrap_s()); - let wrap_t = map_wrapping(sampler.wrap_t()); - - let mut s = SamplerDesc::new(min_filter, wrap_s); - s.wrap_mode = (wrap_s, wrap_t, wrap_t); - s.mag_filter = mag_filter; - s.mip_filter = mip_filter; - s -} - -fn map_wrapping(gltf_wrap: gltf::texture::WrappingMode) -> hal::image::WrapMode { - match gltf_wrap { - gltf::texture::WrappingMode::ClampToEdge => hal::image::WrapMode::Clamp, - gltf::texture::WrappingMode::MirroredRepeat => hal::image::WrapMode::Mirror, - gltf::texture::WrappingMode::Repeat => hal::image::WrapMode::Tile, - } -} diff --git a/amethyst_gltf/src/format/mesh.rs b/amethyst_gltf/src/format/mesh.rs deleted file mode 100644 index 957988ee1d..0000000000 --- a/amethyst_gltf/src/format/mesh.rs +++ /dev/null @@ -1,323 +0,0 @@ -use std::{iter::repeat, ops::Range}; - -use amethyst_core::math::{zero, Vector3}; -use amethyst_error::Error; -use amethyst_rendy::{ - rendy::mesh::{Color, MeshBuilder, Normal, Position, Tangent, TexCoord}, - skinning::JointCombined, -}; -use log::{trace, warn}; -use mikktspace::{generate_tangents, Geometry}; - -use super::Buffers; -use crate::{error, GltfSceneOptions}; - -fn compute_if T>(predicate: bool, func: F) -> Option { - if predicate { - Some(func()) - } else { - None - } -} - -fn try_compute_if Option>(predicate: bool, func: F) -> Option { - if predicate { - func() - } else { - None - } -} - -enum Indices { - None, - U16(Vec), - U32(Vec), -} - -impl Indices { - fn len(&self) -> Option { - match self { - Indices::None => None, - Indices::U16(vec) => Some(vec.len()), - Indices::U32(vec) => Some(vec.len()), - } - } - - fn map(&self, face: usize, vert: usize) -> usize { - match self { - Indices::None => face * 3 + vert, - Indices::U16(vec) => vec[face * 3 + vert] as usize, - Indices::U32(vec) => vec[face * 3 + vert] as usize, - } - } -} - -pub fn load_mesh( - mesh: &gltf::Mesh<'_>, - buffers: &Buffers, - options: &GltfSceneOptions, -) -> Result, Option, Range<[f32; 3]>)>, Error> { - trace!("Loading mesh"); - let mut primitives = vec![]; - - for primitive in mesh.primitives() { - trace!("Loading mesh primitive"); - let reader = primitive.reader(|buffer| buffers.buffer(&buffer)); - let mut builder = MeshBuilder::new(); - - trace!("Loading indices"); - use gltf::mesh::util::ReadIndices; - let indices = match reader.read_indices() { - Some(ReadIndices::U8(iter)) => Indices::U16(iter.map(u16::from).collect()), - Some(ReadIndices::U16(iter)) => Indices::U16(iter.collect()), - Some(ReadIndices::U32(iter)) => Indices::U32(iter.collect()), - None => Indices::None, - }; - - trace!("Loading positions"); - let positions = reader - .read_positions() - .ok_or(error::Error::MissingPositions)? - .map(Position) - .collect::>(); - - let normals = compute_if(options.load_normals || options.load_tangents, || { - trace!("Loading normals"); - if let Some(normals) = reader.read_normals() { - normals.map(Normal).collect::>() - } else { - trace!("Calculating normals"); - calculate_normals(&positions, &indices) - } - }); - - let tex_coords = compute_if(options.load_texcoords || options.load_tangents, || { - trace!("Loading texture coordinates"); - if let Some(tex_coords) = reader.read_tex_coords(0).map(|t| t.into_f32()) { - if options.flip_v_coord { - tex_coords - .map(|[u, v]| TexCoord([u, 1. - v])) - .collect::>() - } else { - tex_coords.map(TexCoord).collect::>() - } - } else { - let (u, v) = options.generate_tex_coords; - let v = if options.flip_v_coord { v } else { 1.0 - v }; - repeat(TexCoord([u, v])) - .take(positions.len()) - .collect::>() - } - }); - - let tangents = compute_if(options.load_tangents, || { - trace!("Loading tangents"); - let tangents = reader.read_tangents(); - match tangents { - Some(tangents) => tangents.map(Tangent).collect::>(), - None => { - trace!("Calculating tangents"); - calculate_tangents( - &positions, - normals.as_ref().unwrap(), - tex_coords.as_ref().unwrap(), - &indices, - ) - } - } - }); - - let colors = try_compute_if(options.load_colors, || { - trace!("Loading colors"); - if let Some(colors) = reader.read_colors(0) { - Some(colors.into_rgba_f32().map(Color).collect::>()) - } else { - None - } - }); - - let joints = try_compute_if(options.load_animations, || { - trace!("Loading animations"); - if let (Some(ids), Some(weights)) = (reader.read_joints(0), reader.read_weights(0)) { - let zip = ids.into_u16().zip(weights.into_f32()); - let joints = zip - .map(|(ids, weights)| JointCombined::new(ids, weights)) - .collect::>(); - - Some(joints) - } else { - None - } - }); - - match indices { - Indices::U16(vec) => { - builder.set_indices(vec); - } - Indices::U32(vec) => { - builder.set_indices(vec); - } - Indices::None => {} - }; - - builder.add_vertices(positions); - normals.map(|v| builder.add_vertices(v)); - tangents.map(|v| builder.add_vertices(v)); - tex_coords.map(|v| builder.add_vertices(v)); - colors.map(|v| builder.add_vertices(v)); - joints.map(|v| builder.add_vertices(v)); - - trace!("Loading bounding box"); - let bounds = primitive.bounding_box(); - let bounds = bounds.min..bounds.max; - let material = primitive.material().index(); - - primitives.push((builder, material, bounds)); - } - trace!("Loaded mesh"); - Ok(primitives) -} - -fn calculate_normals(positions: &[Position], indices: &Indices) -> Vec { - let mut normals = vec![zero::>(); positions.len()]; - let num_faces = indices.len().unwrap_or_else(|| positions.len()) / 3; - for face in 0..num_faces { - let i0 = indices.map(face, 0); - let i1 = indices.map(face, 1); - let i2 = indices.map(face, 2); - let a = Vector3::from(positions[i0].0); - let b = Vector3::from(positions[i1].0); - let c = Vector3::from(positions[i2].0); - let n = (b - a).cross(&(c - a)); - normals[i0] += n; - normals[i1] += n; - normals[i2] += n; - } - normals - .into_iter() - .map(|n| Normal(n.normalize().into())) - .collect::>() -} - -struct TangentsGeometry<'a> { - tangents: Vec, - num_faces: usize, - positions: &'a [Position], - normals: &'a [Normal], - tex_coords: &'a [TexCoord], - indices: &'a Indices, -} - -impl<'a> Geometry for TangentsGeometry<'a> { - fn num_faces(&self) -> usize { - self.num_faces - } - fn num_vertices_of_face(&self, _face: usize) -> usize { - 3 - } - fn position(&self, face: usize, vert: usize) -> [f32; 3] { - self.positions[self.indices.map(face, vert)].0 - } - fn normal(&self, face: usize, vert: usize) -> [f32; 3] { - self.normals[self.indices.map(face, vert)].0 - } - fn tex_coord(&self, face: usize, vert: usize) -> [f32; 2] { - self.tex_coords[self.indices.map(face, vert)].0 - } - - fn set_tangent_encoded(&mut self, tangent: [f32; 4], face: usize, vert: usize) { - let [x, y, z, w] = tangent; - self.tangents[self.indices.map(face, vert)] = Tangent([x, y, z, -w]); - } -} - -fn calculate_tangents( - positions: &[Position], - normals: &[Normal], - tex_coords: &[TexCoord], - indices: &Indices, -) -> Vec { - let mut geometry = TangentsGeometry { - tangents: vec![Tangent([0.0, 0.0, 0.0, 0.0]); positions.len()], - num_faces: indices.len().unwrap_or_else(|| positions.len()) / 3, - positions, - normals, - tex_coords, - indices, - }; - - if !generate_tangents(&mut geometry) { - warn!("Could not generate tangents!"); - } - - geometry.tangents -} - -#[cfg(test)] -mod tests { - use amethyst_rendy::rendy::mesh::{Normal, Position, Tangent, TexCoord}; - - use super::{calculate_tangents, Indices}; - - const POSITIONS: &[Position] = &[ - Position([0.0, 0.0, 0.0]), - Position([0.0, 1.0, 0.0]), - Position([1.0, 1.0, 0.0]), - Position([0.0, 1.0, 0.0]), - Position([1.0, 1.0, 0.0]), - Position([1.0, 0.0, 0.0]), - ]; - const NORMALS: &[Normal] = &[ - Normal([0.0, 0.0, 1.0]), - Normal([0.0, 0.0, 1.0]), - Normal([0.0, 0.0, 1.0]), - Normal([0.0, 0.0, 1.0]), - Normal([0.0, 0.0, 1.0]), - Normal([1.0, 0.0, 0.0]), - ]; - const TEX_COORDS: &[TexCoord] = &[ - TexCoord([0.0, 0.0]), - TexCoord([0.0, 1.0]), - TexCoord([1.0, 1.0]), - TexCoord([0.0, 1.0]), - TexCoord([1.0, 1.0]), - TexCoord([1.0, 0.0]), - ]; - - #[test] - fn test_tangent_calc() { - let tangents = calculate_tangents(POSITIONS, NORMALS, TEX_COORDS, &Indices::None); - assert_eq!( - tangents, - vec![ - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([0.0, 0.0, 0.0, 1.0]), - ] - ); - } - - #[test] - fn test_indexed_tangent_calc() { - let tangents = calculate_tangents( - POSITIONS, - NORMALS, - TEX_COORDS, - &Indices::U32(vec![3, 4, 5, 0, 1, 2]), - ); - assert_eq!( - tangents, - vec![ - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([1.0, 0.0, 0.0, 1.0]), - Tangent([0.0, 0.0, 0.0, 1.0]), - ] - ); - } -} diff --git a/amethyst_gltf/src/format/mod.rs b/amethyst_gltf/src/format/mod.rs deleted file mode 100644 index 13de807c64..0000000000 --- a/amethyst_gltf/src/format/mod.rs +++ /dev/null @@ -1,345 +0,0 @@ -//! GLTF format - -use std::{cmp::Ordering, collections::HashMap, sync::Arc}; - -use amethyst_animation::AnimationHierarchyPrefab; -use amethyst_assets::{Format, FormatValue, Prefab, Source}; -use amethyst_core::{ - math::{convert, Quaternion, Unit, Vector3, Vector4}, - transform::Transform, -}; -use amethyst_error::{format_err, Error, ResultExt}; -use amethyst_rendy::{camera::CameraPrefab, light::LightPrefab}; -use gltf::{self, Gltf}; -use log::debug; -use serde::{Deserialize, Serialize}; - -use self::{ - animation::load_animations, - importer::{get_image_data, import, Buffers, ImageFormat}, - material::load_material, - mesh::load_mesh, - skin::load_skin, -}; -use crate::{error, GltfMaterialSet, GltfNodeExtent, GltfPrefab, GltfSceneOptions, Named}; - -mod animation; -mod importer; -mod material; -mod mesh; -mod skin; - -/// Gltf scene format, will load a single scene from a Gltf file. -/// -/// Using the `GltfSceneLoaderSystem` a `Handle` from this format can be attached -/// to an entity in ECS, and the system will then load the full scene using the given entity -/// as the root node of the scene hierarchy. -/// -/// See `GltfSceneOptions` for more information about the load options. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(transparent)] -pub struct GltfSceneFormat(pub GltfSceneOptions); - -impl Format> for GltfSceneFormat { - fn name(&self) -> &'static str { - "GLTFScene" - } - - fn import( - &self, - name: String, - source: Arc, - _create_reload: Option>>>, - ) -> Result>, Error> { - Ok(FormatValue::data( - load_gltf(source, &name, &self.0) - .with_context(|_| format_err!("Failed to import gltf scene '{:?}'", name))?, - )) - } -} - -fn load_gltf( - source: Arc, - name: &str, - options: &GltfSceneOptions, -) -> Result, Error> { - debug!("Loading GLTF scene '{}'", name); - import(source.clone(), name) - .with_context(|_| error::Error::GltfImporterError) - .and_then(|(gltf, buffers)| { - load_data(&gltf, &buffers, options, source, name).map_err(Into::into) - }) -} - -fn load_data( - gltf: &Gltf, - buffers: &Buffers, - options: &GltfSceneOptions, - source: Arc, - name: &str, -) -> Result, Error> { - let scene_index = get_scene_index(gltf, options)?; - let mut prefab = Prefab::::new(); - load_scene( - gltf, - scene_index, - buffers, - options, - source, - name, - &mut prefab, - )?; - Ok(prefab) -} - -fn get_scene_index(gltf: &Gltf, options: &GltfSceneOptions) -> Result { - let num_scenes = gltf.scenes().len(); - match (options.scene_index, gltf.default_scene()) { - (Some(index), _) if index >= num_scenes => { - Err(error::Error::InvalidSceneGltf(num_scenes).into()) - } - (Some(index), _) => Ok(index), - (None, Some(scene)) => Ok(scene.index()), - (None, _) if num_scenes > 1 => Err(error::Error::InvalidSceneGltf(num_scenes).into()), - (None, _) => Ok(0), - } -} - -fn load_scene( - gltf: &Gltf, - scene_index: usize, - buffers: &Buffers, - options: &GltfSceneOptions, - source: Arc, - name: &str, - prefab: &mut Prefab, -) -> Result<(), Error> { - let scene = gltf - .scenes() - .nth(scene_index) - .expect("Tried to load a scene which does not exist"); - let mut node_map = HashMap::new(); - let mut skin_map = HashMap::new(); - let mut bounding_box = GltfNodeExtent::default(); - let mut material_set = GltfMaterialSet::default(); - for node in scene.nodes() { - let index = prefab.add(Some(0), None); - load_node( - gltf, - &node, - index, - buffers, - options, - source.clone(), - name, - prefab, - &mut node_map, - &mut skin_map, - &mut bounding_box, - &mut material_set, - )?; - } - if bounding_box.valid() { - prefab.data_or_default(0).extent = Some(bounding_box); - } - prefab.data_or_default(0).materials = Some(material_set); - - // load skins - for (node_index, skin_info) in skin_map { - load_skin( - &gltf.skins().nth(skin_info.skin_index).expect( - "Unreachable: `skin_map` is initialized with indexes from the `Gltf` object", - ), - buffers, - *node_map - .get(&node_index) - .expect("Unreachable: `node_map` should contain all nodes present in `skin_map`"), - &node_map, - skin_info.mesh_indices, - prefab, - )?; - } - - // load animations, if applicable - if options.load_animations { - let mut hierarchy_prefab = AnimationHierarchyPrefab::default(); - hierarchy_prefab.nodes = node_map - .iter() - .map(|(node, entity)| (*node, *entity)) - .collect(); - prefab - .data_or_default(0) - .animatable - .get_or_insert_with(Default::default) - .hierarchy = Some(hierarchy_prefab); - - prefab - .data_or_default(0) - .animatable - .get_or_insert_with(Default::default) - .animation_set = Some(load_animations(gltf, buffers, &node_map)?); - } - - Ok(()) -} - -#[derive(Debug)] -struct SkinInfo { - skin_index: usize, - mesh_indices: Vec, -} - -fn load_node( - gltf: &Gltf, - node: &gltf::Node<'_>, - entity_index: usize, - buffers: &Buffers, - options: &GltfSceneOptions, - source: Arc, - name: &str, - prefab: &mut Prefab, - node_map: &mut HashMap, - skin_map: &mut HashMap, - parent_bounding_box: &mut GltfNodeExtent, - material_set: &mut GltfMaterialSet, -) -> Result<(), Error> { - node_map.insert(node.index(), entity_index); - - // Load node name. - if let Some(name) = node.name() { - prefab.data_or_default(entity_index).name = Some(Named::new(name.to_string())); - } - - // Load transformation data, default will be identity - let (translation, rotation, scale) = node.transform().decomposed(); - let mut local_transform = Transform::default(); - *local_transform.translation_mut() = convert::<_, Vector3>(Vector3::from(translation)); - *local_transform.rotation_mut() = Unit::new_normalize(convert::<_, Quaternion>( - Quaternion::from(Vector4::from(rotation)), - )); - *local_transform.scale_mut() = convert::<_, Vector3>(Vector3::from(scale)); - prefab.data_or_default(entity_index).transform = Some(local_transform); - - // Load camera - if let Some(camera) = node.camera() { - prefab.data_or_default(entity_index).camera = Some(match camera.projection() { - gltf::camera::Projection::Orthographic(proj) => CameraPrefab::Orthographic { - left: -proj.xmag(), - right: proj.xmag(), - bottom: -proj.ymag(), - top: proj.ymag(), - znear: proj.znear(), - zfar: proj.zfar(), - }, - gltf::camera::Projection::Perspective(proj) => CameraPrefab::Perspective { - aspect: proj.aspect_ratio().ok_or_else(|| { - format_err!( - "Camera {} is a perspective projection, but has no aspect ratio", - camera.index() - ) - })?, - fovy: proj.yfov(), - znear: proj.znear(), - }, - }); - } - - // Load lights - if let Some(light) = node.light() { - prefab.data_or_default(entity_index).light = Some(LightPrefab::from(light)); - } - - // check for skinning - let mut skin = node.skin().map(|skin| SkinInfo { - skin_index: skin.index(), - mesh_indices: Vec::default(), - }); - - let mut bounding_box = GltfNodeExtent::default(); - - // load graphics - if let Some(mesh) = node.mesh() { - let mut graphics = load_mesh(&mesh, buffers, options)?; - match graphics.len().cmp(&1) { - Ordering::Equal => { - // single primitive can be loaded directly onto the node - let (mesh, material_index, bounds) = graphics.remove(0); - bounding_box.extend_range(&bounds); - let prefab_data = prefab.data_or_default(entity_index); - prefab_data.mesh = Some(mesh); - if let Some((material_id, material)) = - material_index.and_then(|index| gltf.materials().nth(index).map(|m| (index, m))) - { - material_set - .materials - .entry(material_id) - .or_insert(load_material(&material, buffers, source.clone(), name)?); - prefab_data.material_id = Some(material_id); - } - // if we have a skin we need to track the mesh entities - if let Some(ref mut skin) = skin { - skin.mesh_indices.push(entity_index); - } - } - Ordering::Greater => { - // if we have multiple primitives, - // we need to add each primitive as a child entity to the node - for (mesh, material_index, bounds) in graphics { - let mesh_entity = prefab.add(Some(entity_index), None); - let prefab_data = prefab.data_or_default(mesh_entity); - prefab_data.transform = Some(Transform::default()); - prefab_data.mesh = Some(mesh); - if let Some((material_id, material)) = material_index - .and_then(|index| gltf.materials().nth(index).map(|m| (index, m))) - { - material_set - .materials - .entry(material_id) - .or_insert(load_material(&material, buffers, source.clone(), name)?); - prefab_data.material_id = Some(material_id); - } - - // if we have a skin we need to track the mesh entities - if let Some(ref mut skin) = skin { - skin.mesh_indices.push(mesh_entity); - } - - // extent - bounding_box.extend_range(&bounds); - prefab_data.extent = Some(bounds.into()); - } - } - Ordering::Less => {} - } - } - - // load children - for child in node.children() { - let index = prefab.add(Some(entity_index), None); - load_node( - gltf, - &child, - index, - buffers, - options, - source.clone(), - name, - prefab, - node_map, - skin_map, - &mut bounding_box, - material_set, - )?; - } - if bounding_box.valid() { - parent_bounding_box.extend(&bounding_box); - prefab.data_or_default(entity_index).extent = Some(bounding_box); - } - - // propagate skin information - if let Some(skin) = skin { - skin_map.insert(node.index(), skin); - } - - Ok(()) -} diff --git a/amethyst_gltf/src/format/skin.rs b/amethyst_gltf/src/format/skin.rs deleted file mode 100644 index be318a4006..0000000000 --- a/amethyst_gltf/src/format/skin.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::collections::HashMap; - -use amethyst_animation::{JointPrefab, SkinPrefab, SkinnablePrefab}; -use amethyst_assets::Prefab; -use amethyst_core::math::{convert, Matrix4}; -use amethyst_error::Error; -use amethyst_rendy::skinning::JointTransformsPrefab; - -use super::Buffers; -use crate::GltfPrefab; - -pub fn load_skin( - skin: &gltf::Skin<'_>, - buffers: &Buffers, - skin_entity: usize, - node_map: &HashMap, - meshes: Vec, - prefab: &mut Prefab, -) -> Result<(), Error> { - let joints = skin - .joints() - .map(|j| { - node_map.get(&j.index()).cloned().expect( - "Unreachable: `node_map` is initialized with the indexes from the `Gltf` object", - ) - }) - .collect::>(); - - let reader = skin.reader(|buffer| buffers.buffer(&buffer)); - - let inverse_bind_matrices = reader - .read_inverse_bind_matrices() - .map(|matrices| { - matrices - .map(Matrix4::from) - .map(convert::<_, Matrix4>) - .collect() - }) - .unwrap_or_else(|| vec![Matrix4::identity(); joints.len()]); - - for (_bind_index, joint_index) in joints.iter().enumerate() { - prefab - .data_or_default(*joint_index) - .skinnable - .get_or_insert_with(SkinnablePrefab::default) - .joint - .get_or_insert_with(JointPrefab::default) - .skins - .push(skin_entity); - } - let joint_transforms = JointTransformsPrefab::new(skin_entity, joints.len()); - for mesh_index in &meshes { - prefab - .data_or_default(*mesh_index) - .skinnable - .get_or_insert_with(SkinnablePrefab::default) - .joint_transforms = Some(joint_transforms.clone()); - } - - let skin_prefab = SkinPrefab { - joints, - meshes, - bind_shape_matrix: Matrix4::identity(), - inverse_bind_matrices, - }; - prefab - .data_or_default(skin_entity) - .skinnable - .get_or_insert_with(SkinnablePrefab::default) - .skin = Some(skin_prefab); - - Ok(()) -} diff --git a/amethyst_gltf/src/importer/gltf_bytes_converter.rs b/amethyst_gltf/src/importer/gltf_bytes_converter.rs new file mode 100644 index 0000000000..329d68da65 --- /dev/null +++ b/amethyst_gltf/src/importer/gltf_bytes_converter.rs @@ -0,0 +1,70 @@ +use gltf::Document; +use amethyst_assets::error::Error; + +pub fn convert_bytes(bytes: &Vec) -> Result<(Document, Vec, Vec), Error> { + log::debug!("Starting Gltf import"); + if bytes.starts_with(b"glTF") { + log::debug!("Importing as a .glb"); + convert_glb(&bytes) + } else { + log::debug!("Importing as a standard .gltf"); + convert_standard(&bytes) + } +} + +fn convert_standard( + bytes: &Vec, +) -> Result<(Document, Vec, Vec), Error> { + let result = gltf::import_slice(&bytes.as_slice()); + log::debug!("Standard import slice has been done with result {:?}", result); + if let Err(err) = result { + log::error!("GLTF Standard import error: {:?}", err); + return Err(Error::Boxed(Box::new(err))); + } + Ok(result.unwrap()) +} + +fn convert_glb( + data: &Vec, +) -> Result<(Document, Vec, Vec), Error> { + let result = gltf::import_slice(&data); + if let Err(err) = result { + log::error!("GLTF Import error: {:?}", err); + return Err(Error::Boxed(Box::new(err))); + } + Ok(result.unwrap()) +} + +#[cfg(test)] +mod test { + use super::*; + use super::super::GltfSceneOptions; + use std::fs::File; + use std::io::Read; + use atelier_assets::importer::BoxedImporter; + use type_uuid::TypeUuid; + + + #[test] + fn should_import_glb_gltf() { + let mut f = File::open("test/suzanne.glb").expect("suzanne.glb not found"); + let mut buffer = Vec::new(); + // read the whole file + f.read_to_end(&mut buffer).expect("read_to_end did not work"); + let gltf_import = convert_glb(&buffer); + match gltf_import { + Ok(gltf) => { + let doc = gltf.0; + assert_eq!(2, doc.images().len()); + assert_eq!(1, doc.materials().len()); + assert_eq!(1, doc.meshes().len()); + assert_eq!(1, doc.nodes().len()); + assert_eq!(1, doc.scenes().len()); + assert_eq!(2, doc.textures().len()); + }, + Err(e) => { + panic!("Error during gltf import {:?}", e) + } + } + } +} \ No newline at end of file diff --git a/amethyst_gltf/src/importer/images.rs b/amethyst_gltf/src/importer/images.rs new file mode 100644 index 0000000000..3cd61563f1 --- /dev/null +++ b/amethyst_gltf/src/importer/images.rs @@ -0,0 +1,279 @@ +use fnv::FnvHashMap; +use gltf::image::{Format, Data}; +use image::buffer::ConvertBuffer; +use serde::{Deserialize, Serialize}; +use crate::importer::GltfObjectId; +use gltf::image::Data as GltfImageData; +use image::RgbaImage; + +use type_uuid::TypeUuid; +use amethyst_assets::resource_arc::ResourceArc; +use std::sync::Arc; +/* +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub enum ImageAssetColorSpace { + Srgb, + Linear, +} + +pub struct ImageToImport { + pub id: GltfObjectId, + pub asset: ImageAssetData, +} + +#[derive(TypeUuid, Clone)] +#[uuid = "7a67b850-17f9-4877-8a6e-293a1589bbd8"] +pub struct ImageAsset { + pub image: ResourceArc, + pub image_view: ResourceArc, +} + +#[derive(TypeUuid, Serialize, Deserialize, Clone)] +#[uuid = "e6166902-8716-401b-9d2e-8b01701c5626"] +pub struct ImageAssetData { + pub width: u32, + pub height: u32, + pub color_space: ImageAssetColorSpace, + + #[serde(with = "serde_bytes")] + pub data: Vec, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct ImageKey { + id: u64, +} + +#[derive(Debug, Clone)] +pub struct ImageResource { + pub image: Arc, + // Dynamic resources have no key + pub image_key: Option, +} + +/// Enum that holds either a texture or render target. +/// +/// `texture()` and `texture_def()` can always be called, but `render_target()` may return none if +/// the image is a texture but not a render target +#[derive(Debug)] +pub enum RafxImage { + Texture(RafxTexture), + RenderTarget(RafxRenderTarget), +} + +impl RafxImage { + pub fn texture_def(&self) -> &RafxTextureDef { + match self { + RafxImage::Texture(inner) => inner.texture_def(), + RafxImage::RenderTarget(inner) => inner.texture().texture_def(), + } + } + + pub fn texture(&self) -> &RafxTexture { + match self { + RafxImage::Texture(inner) => inner, + RafxImage::RenderTarget(inner) => inner.texture(), + } + } + + pub fn render_target(&self) -> Option<&RafxRenderTarget> { + match self { + RafxImage::Texture(_inner) => None, + RafxImage::RenderTarget(inner) => Some(inner), + } + } +} + +impl From for RafxImage { + fn from(texture: RafxTexture) -> Self { + RafxImage::Texture(texture) + } +} + +impl From for RafxImage { + fn from(render_target: RafxRenderTarget) -> Self { + RafxImage::RenderTarget(render_target) + } +} + +#[derive(Debug, Clone)] +pub enum RafxTextureBindType { + Srv, + SrvStencil, + UavMipChain, + UavMipSlice(u32), +} + +#[derive(Debug, Clone)] +pub struct ImageViewResource { + pub image: ResourceArc, + // Dynamic resources have no key + pub image_view_key: Option, + pub texture_bind_type: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ImageViewKey { + image_key: ImageKey, + texture_bind_type: Option, +} + +impl std::fmt::Debug for ImageAssetData { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + f.debug_struct("Point") + .field("width", &self.width) + .field("width", &self.height) + .field("byte_count", &self.data.len()) + .finish() + } +} + +pub fn build_image_color_space_assignments_from_materials( + doc: &gltf::Document +) -> FnvHashMap { + let mut image_color_space_assignments = FnvHashMap::default(); + + for material in doc.materials() { + let pbr_metallic_roughness = &material.pbr_metallic_roughness(); + + if let Some(texture) = material.emissive_texture() { + image_color_space_assignments.insert( + texture.texture().source().index(), + ImageAssetColorSpace::Srgb, + ); + }else if let Some(texture) = material.occlusion_texture() { + image_color_space_assignments.insert( + texture.texture().source().index(), + ImageAssetColorSpace::Srgb, + ); + }else if let Some(texture) = material.normal_texture() { + image_color_space_assignments.insert( + texture.texture().source().index(), + ImageAssetColorSpace::Linear, + ); + } else if let Some(texture) = pbr_metallic_roughness.metallic_roughness_texture() { + image_color_space_assignments.insert( + texture.texture().source().index(), + ImageAssetColorSpace::Linear, + ); + } else if let Some(texture) = pbr_metallic_roughness.base_color_texture() { + image_color_space_assignments.insert( + texture.texture().source().index(), + ImageAssetColorSpace::Srgb, + ); + } + } + image_color_space_assignments +} + +pub fn extract_images_to_import( + doc: &gltf::Document, + images: &[GltfImageData], + image_color_space_assignments: &FnvHashMap, +) -> Vec { + let mut images_to_import = Vec::with_capacity(images.len()); + for image in doc.images() { + let image_data = &images[image.index()]; + let converted_image: image::RgbaImage = convert_image_format_to_rgba(&image_data); + + let color_space = *image_color_space_assignments + .get(&image.index()) + .unwrap_or(&ImageAssetColorSpace::Linear); + log::info!( + "Choosing color space {:?} for image index {}", + color_space, + image.index() + ); + + let asset = ImageAssetData { + data: converted_image.to_vec(), + width: image_data.width, + height: image_data.height, + color_space, + }; + let id = image + .name() + .map(|s| GltfObjectId::Name(s.to_string())) + .unwrap_or_else(|| GltfObjectId::Index(image.index())); + + let image_to_import = ImageToImport { id, asset }; + + // Verify that we iterate images in order so that our resulting assets are in order + assert!(image.index() == images_to_import.len()); + log::debug!( + "Importing Texture name: {:?} index: {} width: {} height: {} bytes: {}", + image.name(), + image.index(), + image_to_import.asset.width, + image_to_import.asset.height, + image_to_import.asset.data.len() + ); + + images_to_import.push(image_to_import); + } + + images_to_import +} + +fn convert_image_format_to_rgba(image_data: &Data) -> RgbaImage { + match image_data.format { + Format::R8 => image::ImageBuffer::, Vec>::from_vec( + image_data.width, + image_data.height, + image_data.pixels.clone(), + ) + .unwrap() + .convert(), + Format::R8G8 => image::ImageBuffer::, Vec>::from_vec( + image_data.width, + image_data.height, + image_data.pixels.clone(), + ) + .unwrap() + .convert(), + Format::R8G8B8 => image::ImageBuffer::, Vec>::from_vec( + image_data.width, + image_data.height, + image_data.pixels.clone(), + ) + .unwrap() + .convert(), + Format::R8G8B8A8 => image::ImageBuffer::, Vec>::from_vec( + image_data.width, + image_data.height, + image_data.pixels.clone(), + ) + .unwrap() + .convert(), + Format::B8G8R8 => image::ImageBuffer::, Vec>::from_vec( + image_data.width, + image_data.height, + image_data.pixels.clone(), + ) + .unwrap() + .convert(), + Format::B8G8R8A8 => image::ImageBuffer::, Vec>::from_vec( + image_data.width, + image_data.height, + image_data.pixels.clone(), + ) + .unwrap() + .convert(), + Format::R16 => { + unimplemented!(); + } + Format::R16G16 => { + unimplemented!(); + } + Format::R16G16B16 => { + unimplemented!(); + } + Format::R16G16B16A16 => { + unimplemented!(); + } + } +} + */ \ No newline at end of file diff --git a/amethyst_gltf/src/importer/mod.rs b/amethyst_gltf/src/importer/mod.rs new file mode 100644 index 0000000000..1c4d06b1f4 --- /dev/null +++ b/amethyst_gltf/src/importer/mod.rs @@ -0,0 +1,159 @@ +use gltf::{Gltf, Document, buffer, Node}; +use gltf::buffer::Data; +use gltf::iter::Buffers; +use atelier_assets::importer::{Importer, ImportOp, ImporterValue, Error, ImportedAsset}; +use std::io::Read; +use crate::{GltfSceneOptions, error, GltfAsset}; +use atelier_assets::core::AssetUuid; +use serde::{Deserialize, Serialize}; +use type_uuid::TypeUuid; +use amethyst_assets::atelier_importer; +use crate::importer::images::{build_image_color_space_assignments_from_materials, extract_images_to_import, ImageAsset}; +use atelier_assets::make_handle; +use atelier_assets::loader::handle::Handle; +use crate::importer::gltf_bytes_converter::convert_bytes; +use amethyst_core::transform::Transform; +use amethyst_core::math::{convert, Vector3, Unit, Quaternion, Vector4}; +use amethyst_core::Named; + +mod gltf_bytes_converter; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GltfObjectId { + Name(String), + Index(usize), +} + +/// A simple state for Importer to retain the same UUID between imports +/// for all single-asset source files +#[derive(Default, Deserialize, Serialize, TypeUuid)] +#[uuid = "3c5571c0-abec-436e-9b28-6bce92f1070a"] +pub struct GltfImporterState { + pub id: Option, +} + +/// The importer for '.gltf' or '.glb' files. +#[derive(Default, TypeUuid)] +#[uuid = "6dbb4496-bd73-42cd-b817-11046e964e30"] +pub struct GltfImporter {} + +impl Importer for GltfImporter { + fn version_static() -> u32 { + 1 + } + + fn version(&self) -> u32 { + Self::version_static() + } + + type Options = GltfSceneOptions; + type State = GltfImporterState; + + fn import(&self, op: &mut ImportOp, source: &mut dyn Read, options: &Self::Options, state: &mut Self::State) -> atelier_importer::Result { + log::info!("Importing scene"); + + let mut bytes = Vec::new(); + source.read_to_end(&mut bytes)?; + let result = convert_bytes(&bytes); + + if let Err(err) = result { + log::error!("GLTF Import error: {:?}", err); + return Err(Error::Boxed(Box::new(err))); + } + + let mut asset_accumulator = Vec::new(); + + let (doc, buffers, images) = result.unwrap(); + + let scene_index = get_scene_index(&doc, options).expect("No scene has been found !"); + let scene = doc + .scenes() + .nth(scene_index) + .expect("Tried to load a scene which does not exist"); + + scene.nodes().into_iter().for_each(|node| { + if let Some(asset) = load_node(&node, op) { + asset_accumulator.push(asset); + } + }); + + Ok(ImporterValue { + assets: asset_accumulator + }) + } +} + +fn load_node(node: &Node, op: &mut ImportOp) -> Option { + let mut node_asset = GltfAsset::default(); + let mut search_tags: Vec<(String, Option)> = vec![]; + + if let Some(name) = node.name() { + node_asset.name = Some(Named::new(name.to_string())); + search_tags.push(("node_name".to_string(), Some(name.to_string()))); + } + node_asset.transform = Some(load_transform(node)); + //TODO: Load camera + + Some(ImportedAsset { + id: op.new_asset_uuid(), + search_tags, + build_deps: vec![], + load_deps: vec![], + build_pipeline: None, + asset_data: Box::new(node_asset), + }) +} + +fn load_transform(node: &Node) -> Transform{ + // Load transformation data, default will be identity + let (translation, rotation, scale) = node.transform().decomposed(); + let mut local_transform = Transform::default(); + + *local_transform.translation_mut() = convert::<_, Vector3>(Vector3::from(translation)); + *local_transform.rotation_mut() = Unit::new_normalize(convert::<_, Quaternion>( + Quaternion::from(Vector4::from(rotation)), + )); + *local_transform.scale_mut() = convert::<_, Vector3>(Vector3::from(scale)); + local_transform +} + +fn get_scene_index(document: &Document, options: &GltfSceneOptions) -> Result { + let num_scenes = document.scenes().len(); + match (options.scene_index, document.default_scene()) { + (Some(index), _) if index >= num_scenes => { + Err(error::Error::InvalidSceneGltf(num_scenes).into()) + } + (Some(index), _) => Ok(index), + (None, Some(scene)) => Ok(scene.index()), + (None, _) if num_scenes > 1 => Err(error::Error::InvalidSceneGltf(num_scenes).into()), + (None, _) => Ok(0), + } +} + + +#[cfg(test)] +mod test { + use super::*; + use super::super::GltfSceneOptions; + use std::fs::File; + use std::io::Read; + use atelier_assets::importer::BoxedImporter; + use type_uuid::TypeUuid; + + #[test] + fn importer_basic_test() { + let mut f = File::open("test/suzanne.glb").expect("suzanne.glb not found"); + let mut buffer = Vec::new(); + // read the whole file + f.read_to_end(&mut buffer).expect("read_to_end did not work"); + let mut buffer_slice = buffer.as_slice(); + let importer: Box = Box::new(GltfImporter::default()); + let mut import_op = ImportOp::default(); + let res = futures::executor::block_on(importer.import_boxed(&mut import_op, &mut buffer_slice, Box::new(GltfSceneOptions::default()), Box::new(GltfImporterState{ id: None }) )); + match res { + Ok(r) => {println!("res : {:?}", r.value.assets.len());} + Err(e)=> {println!("error e {:?}", e);} + }; + } + +} \ No newline at end of file diff --git a/amethyst_gltf/src/lib.rs b/amethyst_gltf/src/lib.rs index 20e8bae480..3f70f08e71 100644 --- a/amethyst_gltf/src/lib.rs +++ b/amethyst_gltf/src/lib.rs @@ -1,36 +1,37 @@ //! A crate for loading GLTF format scenes into Amethyst #![doc( - html_logo_url = "https://amethyst.rs/brand/logo-standard.svg", - html_root_url = "https://docs.amethyst.rs/stable" +html_logo_url = "https://amethyst.rs/brand/logo-standard.svg", +html_root_url = "https://docs.amethyst.rs/stable" )] #![warn( - missing_debug_implementations, - missing_docs, - rust_2018_idioms, - rust_2018_compatibility +missing_debug_implementations, +missing_docs, +rust_2018_idioms, +rust_2018_compatibility )] #![warn(clippy::all)] #![allow(clippy::new_without_default)] use std::{collections::HashMap, ops::Range}; -use amethyst_assets::{AssetStorage, Handle, Loader, ProgressCounter}; +use amethyst_assets::{AssetStorage, Handle, Loader, ProgressCounter, prefab::Prefab}; use amethyst_core::{ ecs::{Entity, Read, ReadExpect, Write, WriteStorage}, + ecs::*, math::{convert, Point3, Vector3}, transform::Transform, Named, }; use amethyst_error::Error; -use amethyst_rendy::{rendy::mesh::MeshBuilder, types::Mesh, visibility::BoundingSphere}; +use amethyst_rendy::{rendy::mesh::MeshBuilder, types::Mesh, visibility::BoundingSphere, Camera, Material}; use derivative::Derivative; use serde::{Deserialize, Serialize}; - -pub use crate::format::GltfSceneFormat; +use type_uuid::TypeUuid; +use amethyst_rendy::light::Light; mod error; -mod format; +mod importer; /// A GLTF node extent #[derive(Clone, Debug)] @@ -115,17 +116,21 @@ impl From> for GltfNodeExtent { } /// Used during gltf loading to contain the materials used from scenes in the file -#[derive(Debug, Derivative)] +#[derive(Derivative)] #[derivative(Default(bound = ""))] pub struct GltfMaterialSet { - pub(crate) materials: HashMap, + pub(crate) materials: HashMap, } /// Options used when loading a GLTF file -#[derive(Debug, Clone, Derivative, Serialize, Deserialize)] -#[derivative(Default)] +#[derive(Debug, Clone, Default, Derivative, Serialize, Deserialize, TypeUuid)] #[serde(default)] +#[uuid = "8e3da51a-26d4-4b0f-b9f7-7f52d1b78945"] pub struct GltfSceneOptions { + /// Path of the gltf scene file + pub scene_path: String, + /// Name of the current scene + pub scene_name: String, /// Generate texture coordinates if none exist in the Gltf file pub generate_tex_coords: (f32, f32), #[derivative(Default(value = "true"))] @@ -149,3 +154,32 @@ pub struct GltfSceneOptions { /// or the first scene (only if there is only one scene, otherwise an `Error` will be returned). pub scene_index: Option, } + +/// `AssetData` for gltf objects. +#[derive(Debug, Deserialize, PartialEq, Serialize, Default)] +pub struct GltfAsset { + /// `Transform` will almost always be placed, the only exception is for the main `Entity` for + /// certain scenarios (based on the data in the Gltf file) + pub transform: Option, + /// `Camera` will always be placed + pub camera: Option, + /// Lights can be added to a prefab with the `KHR_lights_punctual` feature enabled + pub light: Option, + /// `MeshData` is placed on all `Entity`s with graphics primitives + pub mesh: Option>, + /// Mesh handle after sub asset loading is done + pub mesh_handle: Option>, + /// `Material` is placed on all `Entity`s with graphics primitives with material + pub material: Option, + /// Loaded animations, if applicable, will always only be placed on the main `Entity` + pub animatable: Option>, + /// Skin data is placed on `Entity`s involved in the skin, skeleton or graphical primitives + /// using the skin + pub skinnable: Option, + /// Node extent + pub extent: Option, + /// Node name + pub name: Option, + pub(crate) materials: Option, + pub(crate) material_id: Option, +} \ No newline at end of file diff --git a/amethyst_gltf/test/suzanne.glb b/amethyst_gltf/test/suzanne.glb new file mode 100644 index 0000000000..120e37787f Binary files /dev/null and b/amethyst_gltf/test/suzanne.glb differ