fyrox_impl/resource/gltf/
material.rs1#![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
158pub 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 } }
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}