#![allow(missing_docs)]
use std::{
fmt::Display,
path::{Path, PathBuf},
sync::LazyLock,
};
use crate::{
asset::{manager::ResourceManager, state::LoadError, untyped::ResourceKind, Resource},
core::{algebra::Vector4, color::Color, log::Log},
material::{
shader::{Shader, ShaderResource},
Material, MaterialProperty, MaterialResource,
},
resource::{
model::MaterialSearchOptions,
texture::{Texture, TextureError, TextureImportOptions, TextureResource},
},
};
use gltf::{buffer::View, image, Document};
use super::uri;
type Result<T> = std::result::Result<T, GltfMaterialError>;
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 static GLTF_SHADER: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
"GltfShader",
embedded_data_source!("gltf_standard.shader"),
|data| {
ShaderResource::new_ok(
uuid!("33ee0142-f345-4c0a-9aca-d1f684a3485b"),
ResourceKind::External,
Shader::from_string_bytes(data).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::material::{MaterialResourceBinding, MaterialTextureBinding};
use crate::resource::texture::TextureWrapMode as FyroxWrapMode;
use fyrox_core::Uuid;
use fyrox_resource::builtin::BuiltInResource;
use fyrox_resource::embedded_data_source;
use gltf::texture::WrappingMode as GltfWrapMode;
use uuid::uuid;
fn convert_wrap(mode: GltfWrapMode) -> FyroxWrapMode {
match mode {
GltfWrapMode::Repeat => FyroxWrapMode::Repeat,
GltfWrapMode::ClampToEdge => FyroxWrapMode::ClampToEdge,
GltfWrapMode::MirroredRepeat => FyroxWrapMode::MirroredRepeat,
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum GltfMaterialError {
ShaderLoadFailed,
InvalidIndex,
UnsupportedURI(Box<str>),
TextureNotFound(Box<str>),
Load(LoadError),
Base64(base64::DecodeError),
Texture(TextureError),
}
impl std::error::Error for GltfMaterialError {}
impl Display for GltfMaterialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GltfMaterialError::ShaderLoadFailed => f.write_str("Shader load failed"),
GltfMaterialError::InvalidIndex => f.write_str("Invalid material index"),
GltfMaterialError::UnsupportedURI(uri) => {
write!(f, "Unsupported material URI {uri:?}")
}
GltfMaterialError::TextureNotFound(uri) => write!(f, "Texture not found: {uri:?}"),
GltfMaterialError::Load(error) => Display::fmt(error, f),
GltfMaterialError::Base64(error) => Display::fmt(error, f),
GltfMaterialError::Texture(error) => Display::fmt(error, f),
}
}
}
impl From<LoadError> for GltfMaterialError {
fn from(error: LoadError) -> Self {
GltfMaterialError::Load(error)
}
}
impl From<base64::DecodeError> for GltfMaterialError {
fn from(error: base64::DecodeError) -> Self {
GltfMaterialError::Base64(error)
}
}
impl From<TextureError> for GltfMaterialError {
fn from(error: TextureError) -> Self {
GltfMaterialError::Texture(error)
}
}
pub enum SourceImage<'a> {
External(&'a str),
View(&'a [u8]),
Embedded(Vec<u8>),
}
pub fn decode_base64(source: &str) -> Result<Vec<u8>> {
Ok(uri::decode_base64(source)?)
}
pub async fn import_materials(
gltf: &Document,
textures: &[TextureResource],
) -> Result<Vec<MaterialResource>> {
let mut result: Vec<MaterialResource> = Vec::with_capacity(gltf.materials().len());
for mat in gltf.materials() {
match import_material(mat, textures).await {
Ok(res) => result.push(res),
Err(err) => {
Log::err(format!("glTF material failed to import. Reason: {err:?}"));
result.push(MaterialResource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
Material::default(),
));
}
}
}
Ok(result)
}
async fn import_material(
mat: gltf::Material<'_>,
textures: &[TextureResource],
) -> Result<MaterialResource> {
let shader: ShaderResource = GLTF_SHADER.resource.clone();
if !shader.is_ok() {
return Err(GltfMaterialError::ShaderLoadFailed);
}
let mut result: Material = Material::from_shader(shader);
let pbr = mat.pbr_metallic_roughness();
if let Some(tex) = pbr.base_color_texture() {
set_texture(
&mut result,
"diffuseTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = mat.normal_texture() {
set_texture(
&mut result,
"normalTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = pbr.metallic_roughness_texture() {
set_texture(
&mut result,
"metallicRoughnessTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = mat.emissive_texture() {
set_texture(
&mut result,
"emissionTexture",
textures,
tex.texture().index(),
)?;
}
if let Some(tex) = mat.occlusion_texture() {
set_texture(&mut result, "aoTexture", textures, tex.texture().index())?;
}
set_material_color(
&mut result,
"diffuseColor",
Vector4::<f32>::from(pbr.base_color_factor()).into(),
);
let mut emission_strength = mat.emissive_factor();
let emission_factor = mat.emissive_strength().unwrap_or(1.0);
for c in emission_strength.iter_mut() {
*c *= emission_factor;
}
set_material_vector3(&mut result, "emissionStrength", emission_strength);
set_material_scalar(&mut result, "metallicFactor", pbr.metallic_factor());
set_material_scalar(&mut result, "roughnessFactor", pbr.roughness_factor());
Ok(Resource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
result,
))
}
fn set_material_scalar(material: &mut Material, name: &'static str, value: f32) {
let value: MaterialProperty = MaterialProperty::Float(value);
material.set_property(name, value);
}
fn set_material_color(material: &mut Material, name: &'static str, color: Color) {
let value: MaterialProperty = MaterialProperty::Color(color);
material.set_property(name, value);
}
fn set_material_vector3(material: &mut Material, name: &'static str, vector: [f32; 3]) {
let value: MaterialProperty = MaterialProperty::Vector3(vector.into());
material.set_property(name, value);
}
#[allow(dead_code)]
fn set_material_vector4(material: &mut Material, name: &'static str, vector: [f32; 4]) {
let value: MaterialProperty = MaterialProperty::Vector4(vector.into());
material.set_property(name, value);
}
fn set_texture(
material: &mut Material,
name: &'static str,
textures: &[TextureResource],
index: usize,
) -> Result<()> {
let tex: TextureResource = textures
.get(index)
.ok_or(GltfMaterialError::InvalidIndex)?
.clone();
material.bind(
name,
MaterialResourceBinding::Texture(MaterialTextureBinding { value: Some(tex) }),
);
Ok(())
}
pub fn import_images<'a, 'b>(
gltf: &'a Document,
buffers: &'b [Vec<u8>],
) -> Result<Vec<SourceImage<'b>>>
where
'a: 'b,
{
let mut result: Vec<SourceImage> = 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(uri: &str) -> Result<SourceImage> {
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<u8>]) -> Result<SourceImage<'a>> {
let offset: usize = view.offset();
let length: usize = view.length();
let buf: &Vec<u8> = 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<Vec<TextureResource>> {
let mut result: Vec<TextureResource> = 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?;
} }
}
Ok(result)
}
fn import_embedded_texture(
sampler: gltf::texture::Sampler,
data: &[u8],
) -> Result<TextureResource> {
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(
Uuid::new_v4(),
ResourceKind::Embedded,
tex,
))
}
async fn import_external_texture(
filename: &str,
context: &TextureContext<'_>,
) -> Result<TextureResource> {
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<PathBuf> {
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, usize::MAX).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()),
}
}