Skip to main content

fyrox_impl/resource/gltf/
material.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21#![allow(missing_docs)]
22
23use std::{
24    fmt::Display,
25    path::{Path, PathBuf},
26    sync::LazyLock,
27};
28
29use crate::{
30    asset::{manager::ResourceManager, state::LoadError, untyped::ResourceKind, Resource},
31    core::{algebra::Vector4, color::Color, log::Log},
32    material::{
33        shader::{Shader, ShaderResource},
34        Material, MaterialProperty, MaterialResource,
35    },
36    resource::{
37        model::MaterialSearchOptions,
38        texture::{Texture, TextureError, TextureImportOptions, TextureResource},
39    },
40};
41use gltf::{buffer::View, image, Document};
42
43use super::uri;
44
45type Result<T> = std::result::Result<T, GltfMaterialError>;
46
47use crate::resource::texture::TextureMagnificationFilter as FyroxMagFilter;
48use crate::resource::texture::TextureMinificationFilter as FyroxMinFilter;
49use gltf::texture::MagFilter as GltfMagFilter;
50use gltf::texture::MinFilter as GltfMinFilter;
51
52pub static GLTF_SHADER: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
53    BuiltInResource::new(
54        "__GLTF_StandardShader",
55        embedded_data_source!("gltf_standard.shader"),
56        |data| {
57            ShaderResource::new_ok(
58                uuid!("33ee0142-f345-4c0a-9aca-d1f684a3485b"),
59                ResourceKind::External,
60                Shader::from_string_bytes(data).unwrap(),
61            )
62        },
63    )
64});
65
66fn convert_mini(filter: GltfMinFilter) -> FyroxMinFilter {
67    match filter {
68        GltfMinFilter::Linear => FyroxMinFilter::Linear,
69        GltfMinFilter::Nearest => FyroxMinFilter::Nearest,
70        GltfMinFilter::LinearMipmapLinear => FyroxMinFilter::LinearMipMapLinear,
71        GltfMinFilter::NearestMipmapLinear => FyroxMinFilter::NearestMipMapLinear,
72        GltfMinFilter::LinearMipmapNearest => FyroxMinFilter::LinearMipMapNearest,
73        GltfMinFilter::NearestMipmapNearest => FyroxMinFilter::NearestMipMapNearest,
74    }
75}
76
77fn convert_mag(filter: GltfMagFilter) -> FyroxMagFilter {
78    match filter {
79        GltfMagFilter::Linear => FyroxMagFilter::Linear,
80        GltfMagFilter::Nearest => FyroxMagFilter::Nearest,
81    }
82}
83
84use crate::material::{MaterialResourceBinding, MaterialTextureBinding};
85use crate::resource::texture::TextureWrapMode as FyroxWrapMode;
86use fyrox_core::Uuid;
87use fyrox_resource::builtin::BuiltInResource;
88use fyrox_resource::embedded_data_source;
89use gltf::texture::WrappingMode as GltfWrapMode;
90use uuid::uuid;
91
92fn convert_wrap(mode: GltfWrapMode) -> FyroxWrapMode {
93    match mode {
94        GltfWrapMode::Repeat => FyroxWrapMode::Repeat,
95        GltfWrapMode::ClampToEdge => FyroxWrapMode::ClampToEdge,
96        GltfWrapMode::MirroredRepeat => FyroxWrapMode::MirroredRepeat,
97    }
98}
99
100#[derive(Debug)]
101#[allow(dead_code)]
102pub enum GltfMaterialError {
103    ShaderLoadFailed,
104    InvalidIndex,
105    UnsupportedURI(Box<str>),
106    TextureNotFound(Box<str>),
107    Load(LoadError),
108    Base64(base64::DecodeError),
109    Texture(TextureError),
110}
111
112impl std::error::Error for GltfMaterialError {}
113
114impl Display for GltfMaterialError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            GltfMaterialError::ShaderLoadFailed => f.write_str("Shader load failed"),
118            GltfMaterialError::InvalidIndex => f.write_str("Invalid material index"),
119            GltfMaterialError::UnsupportedURI(uri) => {
120                write!(f, "Unsupported material URI {uri:?}")
121            }
122            GltfMaterialError::TextureNotFound(uri) => write!(f, "Texture not found: {uri:?}"),
123            GltfMaterialError::Load(error) => Display::fmt(error, f),
124            GltfMaterialError::Base64(error) => Display::fmt(error, f),
125            GltfMaterialError::Texture(error) => Display::fmt(error, f),
126        }
127    }
128}
129
130impl From<LoadError> for GltfMaterialError {
131    fn from(error: LoadError) -> Self {
132        GltfMaterialError::Load(error)
133    }
134}
135
136impl From<base64::DecodeError> for GltfMaterialError {
137    fn from(error: base64::DecodeError) -> Self {
138        GltfMaterialError::Base64(error)
139    }
140}
141
142impl From<TextureError> for GltfMaterialError {
143    fn from(error: TextureError) -> Self {
144        GltfMaterialError::Texture(error)
145    }
146}
147
148pub enum SourceImage<'a> {
149    External(&'a str),
150    View(&'a [u8]),
151    Embedded(Vec<u8>),
152}
153
154pub fn decode_base64(source: &str) -> Result<Vec<u8>> {
155    Ok(uri::decode_base64(source)?)
156}
157
158/// Extract a list of [MaterialResource] from the give glTF document, if that document contains any.
159/// The resulting list of materials is guaranteed to be the same length as the list of materials
160/// in the document so that an index into the document's list of materials will be the same as the index
161/// of the matching MaterialResource in the returned list. This is important since the glTF document
162/// refers to materials by index.
163///
164/// * `doc`: The document in which to find the materials.
165///
166/// * `textures`: A slice containing a [TextureResource] for every texture defined in the document, in that order, so that
167/// a texture can be looked up using the index of a texture within the document. Materials in glTF specify their target
168/// textures by their index within the node list of the document, and these indices need to be translated into handles.
169///
170/// * `buffers`: A slice containing a list of byte-vectors, one for each buffer in the glTF document.
171/// Animations in glTF make reference to data stored in the document's list of buffers by index.
172/// This slcie allows an index into the document's list of buffers to be translated into actual bytes of data.
173///
174/// * `resource_manager`: A [ResourceManager] makes it possible to access shaders and create materials.
175pub async fn import_materials(
176    gltf: &Document,
177    textures: &[TextureResource],
178) -> Result<Vec<MaterialResource>> {
179    let mut result: Vec<MaterialResource> = Vec::with_capacity(gltf.materials().len());
180    for mat in gltf.materials() {
181        match import_material(mat, textures).await {
182            Ok(res) => result.push(res),
183            Err(err) => {
184                Log::err(format!("glTF material failed to import. Reason: {err:?}"));
185                result.push(MaterialResource::new_ok(
186                    Uuid::new_v4(),
187                    ResourceKind::Embedded,
188                    Material::default(),
189                ));
190            }
191        }
192    }
193    Ok(result)
194}
195
196async fn import_material(
197    mat: gltf::Material<'_>,
198    textures: &[TextureResource],
199) -> Result<MaterialResource> {
200    let shader: ShaderResource = GLTF_SHADER.resource.clone();
201    if !shader.is_ok() {
202        return Err(GltfMaterialError::ShaderLoadFailed);
203    }
204    let mut result: Material = Material::from_shader(shader);
205    let pbr = mat.pbr_metallic_roughness();
206    if let Some(tex) = pbr.base_color_texture() {
207        set_texture(
208            &mut result,
209            "diffuseTexture",
210            textures,
211            tex.texture().index(),
212        )?;
213    }
214    if let Some(tex) = mat.normal_texture() {
215        set_texture(
216            &mut result,
217            "normalTexture",
218            textures,
219            tex.texture().index(),
220        )?;
221    }
222    if let Some(tex) = pbr.metallic_roughness_texture() {
223        set_texture(
224            &mut result,
225            "metallicRoughnessTexture",
226            textures,
227            tex.texture().index(),
228        )?;
229    }
230    if let Some(tex) = mat.emissive_texture() {
231        set_texture(
232            &mut result,
233            "emissionTexture",
234            textures,
235            tex.texture().index(),
236        )?;
237    }
238    if let Some(tex) = mat.occlusion_texture() {
239        set_texture(&mut result, "aoTexture", textures, tex.texture().index())?;
240    }
241    set_material_color(
242        &mut result,
243        "diffuseColor",
244        Vector4::<f32>::from(pbr.base_color_factor()).into(),
245    );
246    let mut emission_strength = mat.emissive_factor();
247    let emission_factor = mat.emissive_strength().unwrap_or(1.0);
248    for c in emission_strength.iter_mut() {
249        *c *= emission_factor;
250    }
251    set_material_vector3(&mut result, "emissionStrength", emission_strength);
252    set_material_scalar(&mut result, "metallicFactor", pbr.metallic_factor());
253    set_material_scalar(&mut result, "roughnessFactor", pbr.roughness_factor());
254    Ok(Resource::new_ok(
255        Uuid::new_v4(),
256        ResourceKind::Embedded,
257        result,
258    ))
259}
260
261fn set_material_scalar(material: &mut Material, name: &'static str, value: f32) {
262    let value: MaterialProperty = MaterialProperty::Float(value);
263    material.set_property(name, value);
264}
265
266fn set_material_color(material: &mut Material, name: &'static str, color: Color) {
267    let value: MaterialProperty = MaterialProperty::Color(color);
268    material.set_property(name, value);
269}
270
271fn set_material_vector3(material: &mut Material, name: &'static str, vector: [f32; 3]) {
272    let value: MaterialProperty = MaterialProperty::Vector3(vector.into());
273    material.set_property(name, value);
274}
275
276#[allow(dead_code)]
277fn set_material_vector4(material: &mut Material, name: &'static str, vector: [f32; 4]) {
278    let value: MaterialProperty = MaterialProperty::Vector4(vector.into());
279    material.set_property(name, value);
280}
281
282fn set_texture(
283    material: &mut Material,
284    name: &'static str,
285    textures: &[TextureResource],
286    index: usize,
287) -> Result<()> {
288    let tex: TextureResource = textures
289        .get(index)
290        .ok_or(GltfMaterialError::InvalidIndex)?
291        .clone();
292    material.bind(
293        name,
294        MaterialResourceBinding::Texture(MaterialTextureBinding { value: Some(tex) }),
295    );
296    Ok(())
297}
298
299pub fn import_images<'a, 'b>(
300    gltf: &'a Document,
301    buffers: &'b [Vec<u8>],
302) -> Result<Vec<SourceImage<'b>>>
303where
304    'a: 'b,
305{
306    let mut result: Vec<SourceImage> = Vec::new();
307    for image in gltf.images() {
308        match image.source() {
309            image::Source::Uri { uri, mime_type: _ } => result.push(import_image_from_uri(uri)?),
310            image::Source::View { view, mime_type: _ } => {
311                result.push(import_image_from_view(view, buffers)?)
312            }
313        }
314    }
315    Ok(result)
316}
317
318fn import_image_from_uri(uri: &str) -> Result<SourceImage> {
319    let parsed_uri = uri::parse_uri(uri);
320    match parsed_uri.scheme {
321        uri::Scheme::Data if parsed_uri.data.is_some() => Ok(SourceImage::Embedded(decode_base64(
322            parsed_uri.data.unwrap(),
323        )?)),
324        uri::Scheme::None => Ok(SourceImage::External(uri)),
325        _ => Err(GltfMaterialError::UnsupportedURI(uri.into())),
326    }
327}
328
329fn import_image_from_view<'a>(view: View, buffers: &'a [Vec<u8>]) -> Result<SourceImage<'a>> {
330    let offset: usize = view.offset();
331    let length: usize = view.length();
332    let buf: &Vec<u8> = buffers
333        .get(view.buffer().index())
334        .ok_or(GltfMaterialError::InvalidIndex)?;
335    Ok(SourceImage::View(&buf[offset..offset + length]))
336}
337
338pub struct TextureContext<'a> {
339    pub resource_manager: &'a ResourceManager,
340    pub model_path: &'a Path,
341    pub search_options: &'a MaterialSearchOptions,
342}
343
344pub async fn import_textures<'a>(
345    gltf: &'a Document,
346    images: &[SourceImage<'a>],
347    context: TextureContext<'a>,
348) -> Result<Vec<TextureResource>> {
349    let mut result: Vec<TextureResource> = Vec::with_capacity(gltf.textures().len());
350    for tex in gltf.textures() {
351        let sampler = tex.sampler();
352        let source = tex.source();
353        let image = images
354            .get(source.index())
355            .ok_or(GltfMaterialError::InvalidIndex)?;
356        match image {
357            SourceImage::Embedded(data) => result.push(import_embedded_texture(sampler, data)?),
358            SourceImage::View(data) => result.push(import_embedded_texture(sampler, data)?),
359            SourceImage::External(filename) => {
360                import_external_texture(filename, &context).await?;
361            } // result.push(import_external_texture(filename, &context).await?),
362        }
363    }
364    Ok(result)
365}
366
367fn import_embedded_texture(
368    sampler: gltf::texture::Sampler,
369    data: &[u8],
370) -> Result<TextureResource> {
371    let mut options = TextureImportOptions::default();
372    if let Some(filter) = sampler.min_filter() {
373        options.set_minification_filter(convert_mini(filter));
374    }
375    if let Some(filter) = sampler.mag_filter() {
376        options.set_magnification_filter(convert_mag(filter));
377    }
378    options.set_s_wrap_mode(convert_wrap(sampler.wrap_s()));
379    options.set_t_wrap_mode(convert_wrap(sampler.wrap_t()));
380    let tex = Texture::load_from_memory(data, options)?;
381    Ok(Resource::new_ok(
382        Uuid::new_v4(),
383        ResourceKind::Embedded,
384        tex,
385    ))
386}
387
388async fn import_external_texture(
389    filename: &str,
390    context: &TextureContext<'_>,
391) -> Result<TextureResource> {
392    let path = search_for_path(filename, context)
393        .await
394        .ok_or_else(|| GltfMaterialError::TextureNotFound(filename.into()))?;
395    Ok(context.resource_manager.request(path))
396}
397
398async fn search_for_path(filename: &str, context: &TextureContext<'_>) -> Option<PathBuf> {
399    match context.search_options {
400        MaterialSearchOptions::MaterialsDirectory(ref directory) => Some(directory.join(filename)),
401        MaterialSearchOptions::RecursiveUp => {
402            let io = context.resource_manager.resource_io();
403            let mut texture_path = None;
404            let mut path: PathBuf = context.model_path.to_owned();
405            while let Some(parent) = path.parent() {
406                let candidate = parent.join(filename);
407                if io.exists(&candidate).await {
408                    texture_path = Some(candidate);
409                    break;
410                }
411                path.pop();
412            }
413            texture_path
414        }
415        MaterialSearchOptions::WorkingDirectory => {
416            let io = context.resource_manager.resource_io();
417            let mut texture_path = None;
418            let path = Path::new(".");
419            if let Ok(iter) = io.walk_directory(path, usize::MAX).await {
420                for dir in iter {
421                    if io.is_dir(&dir).await {
422                        let candidate = dir.join(filename);
423                        if candidate.exists() {
424                            texture_path = Some(candidate);
425                            break;
426                        }
427                    }
428                }
429            }
430            texture_path
431        }
432        MaterialSearchOptions::UsePathDirectly => Some(filename.into()),
433    }
434}