use crate::ecs::animation::components::{
AnimationChannel, AnimationClip, AnimationInterpolation, AnimationProperty, AnimationSampler,
AnimationSamplerOutput,
};
use crate::ecs::light::components::{Light, LightType};
use crate::ecs::loading::{SourceImageId, TextureRecipe};
use crate::ecs::material::components::{AlphaMode, TextureTransform};
use crate::ecs::mesh::components::{
Mesh, MorphTarget, MorphTargetData, SkinData, SkinnedVertex, Vertex,
};
use crate::ecs::world::components::{
Camera, LocalTransform, Material, Name, OrthographicCamera, PerspectiveCamera, Projection,
RenderMesh, Visibility,
};
use crate::render::wgpu::texture_cache::{
SamplerFilter, SamplerSettings, SamplerWrap, TextureUsage,
};
use gltf::{Document, Gltf, Node, Primitive, buffer, image, import_buffers};
use nalgebra_glm::{Mat4, Quat, Vec2, Vec3, vec2, vec3};
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::Path;
use super::super::components::{Prefab, PrefabComponents, PrefabNode};
type EncodedImages = Vec<Option<Vec<u8>>>;
type GltfImport = (Document, Vec<buffer::Data>, EncodedImages);
pub struct TextureProductionPlan {
pub name: String,
pub recipe: TextureRecipe,
pub usage: TextureUsage,
pub sampler: SamplerSettings,
}
struct TextureSpec {
recipe: TextureRecipe,
usage: TextureUsage,
sampler: SamplerSettings,
}
type TextureSpecMap = HashMap<String, TextureSpec>;
fn import_lenient_from_path(path: &Path) -> Result<GltfImport, Box<dyn std::error::Error>> {
let base = path.parent();
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let Gltf { document, blob } = Gltf::from_reader_without_validation(reader)?;
let buffers = import_buffers(&document, base, blob)?;
let encoded_images = extract_encoded_images_from_path(&document, base, &buffers)?;
Ok((document, buffers, encoded_images))
}
fn import_lenient_from_slice(bytes: &[u8]) -> Result<GltfImport, Box<dyn std::error::Error>> {
let Gltf { document, blob } = Gltf::from_slice_without_validation(bytes)?;
let buffers = import_buffers(&document, None, blob)?;
let encoded_images = extract_encoded_images_from_path(&document, None, &buffers)?;
Ok((document, buffers, encoded_images))
}
fn extract_encoded_images_from_path(
doc: &Document,
base: Option<&Path>,
buffers: &[buffer::Data],
) -> Result<EncodedImages, Box<dyn std::error::Error>> {
let mut out = Vec::with_capacity(doc.images().count());
for img in doc.images() {
let bytes = match img.source() {
image::Source::Uri { uri, .. } => {
if let Some(rest) = uri.strip_prefix("data:") {
decode_data_uri_payload(rest)?
} else if let Some(base) = base {
let decoded = percent_decode(uri);
let path = base.join(decoded);
std::fs::read(path)?
} else {
return Err(format!("no base path for image URI '{}'", uri).into());
}
}
image::Source::View { view, .. } => {
let buffer = &buffers[view.buffer().index()];
let start = view.offset();
let end = start + view.length();
buffer.0[start..end].to_vec()
}
};
out.push(Some(bytes));
}
Ok(out)
}
fn decode_data_uri_payload(rest: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let comma = rest
.find(',')
.ok_or("malformed data URI: missing comma separator")?;
let header = &rest[..comma];
let payload = &rest[comma + 1..];
if header.split(';').any(|piece| piece == "base64") {
use base64::Engine;
Ok(base64::engine::general_purpose::STANDARD.decode(payload)?)
} else {
Ok(percent_decode(payload).into_bytes())
}
}
fn sampler_settings_from_gltf(sampler: &gltf::texture::Sampler) -> SamplerSettings {
let wrap = |mode: gltf::texture::WrappingMode| match mode {
gltf::texture::WrappingMode::ClampToEdge => SamplerWrap::ClampToEdge,
gltf::texture::WrappingMode::MirroredRepeat => SamplerWrap::MirroredRepeat,
gltf::texture::WrappingMode::Repeat => SamplerWrap::Repeat,
};
let mag = match sampler.mag_filter() {
Some(gltf::texture::MagFilter::Nearest) => SamplerFilter::Nearest,
_ => SamplerFilter::Linear,
};
let (min, mipmap) = match sampler.min_filter() {
Some(gltf::texture::MinFilter::Nearest) => (SamplerFilter::Nearest, SamplerFilter::Nearest),
Some(gltf::texture::MinFilter::Linear) => (SamplerFilter::Linear, SamplerFilter::Linear),
Some(gltf::texture::MinFilter::NearestMipmapNearest) => {
(SamplerFilter::Nearest, SamplerFilter::Nearest)
}
Some(gltf::texture::MinFilter::LinearMipmapNearest) => {
(SamplerFilter::Linear, SamplerFilter::Nearest)
}
Some(gltf::texture::MinFilter::NearestMipmapLinear) => {
(SamplerFilter::Nearest, SamplerFilter::Linear)
}
Some(gltf::texture::MinFilter::LinearMipmapLinear) | None => {
(SamplerFilter::Linear, SamplerFilter::Linear)
}
};
SamplerSettings {
wrap_u: wrap(sampler.wrap_s()),
wrap_v: wrap(sampler.wrap_t()),
mag_filter: mag,
min_filter: min,
mipmap_filter: mipmap,
}
}
fn texture_transform_from_info(info: &gltf::texture::Info) -> TextureTransform {
let base_uv = info.tex_coord();
match info.texture_transform() {
Some(t) => TextureTransform {
offset: t.offset(),
rotation: t.rotation(),
scale: t.scale(),
uv_set: t.tex_coord().unwrap_or(base_uv),
},
None => TextureTransform {
offset: [0.0, 0.0],
rotation: 0.0,
scale: [1.0, 1.0],
uv_set: base_uv,
},
}
}
fn texture_transform_from_extension_value(
extension: Option<&serde_json::Value>,
base_uv: u32,
) -> TextureTransform {
let Some(transform) = extension else {
return TextureTransform {
offset: [0.0, 0.0],
rotation: 0.0,
scale: [1.0, 1.0],
uv_set: base_uv,
};
};
let offset = transform
.get("offset")
.and_then(|v| v.as_array())
.filter(|array| array.len() >= 2)
.map(|array| {
[
array[0].as_f64().unwrap_or(0.0) as f32,
array[1].as_f64().unwrap_or(0.0) as f32,
]
})
.unwrap_or([0.0, 0.0]);
let rotation = transform
.get("rotation")
.and_then(|v| v.as_f64())
.map(|value| value as f32)
.unwrap_or(0.0);
let scale = transform
.get("scale")
.and_then(|v| v.as_array())
.filter(|array| array.len() >= 2)
.map(|array| {
[
array[0].as_f64().unwrap_or(1.0) as f32,
array[1].as_f64().unwrap_or(1.0) as f32,
]
})
.unwrap_or([1.0, 1.0]);
let uv_set = transform
.get("texCoord")
.and_then(|v| v.as_u64())
.map(|value| value as u32)
.unwrap_or(base_uv);
TextureTransform {
offset,
rotation,
scale,
uv_set,
}
}
fn texture_transform_from_normal(info: &gltf::material::NormalTexture) -> TextureTransform {
texture_transform_from_extension_value(
info.extension_value("KHR_texture_transform"),
info.tex_coord(),
)
}
fn texture_transform_from_occlusion(info: &gltf::material::OcclusionTexture) -> TextureTransform {
texture_transform_from_extension_value(
info.extension_value("KHR_texture_transform"),
info.tex_coord(),
)
}
fn pack_textures_rg(
source_a_name: Option<&str>,
source_b_name: Option<&str>,
namespace: &str,
output_name_prefix: &str,
output_usage: TextureUsage,
texture_specs: &mut TextureSpecMap,
) -> Option<String> {
let a_image = source_a_name.and_then(|name| direct_image_source(name, texture_specs));
let b_image = source_b_name.and_then(|name| direct_image_source(name, texture_specs));
if a_image.is_none() && b_image.is_none() {
return None;
}
let sampler = source_a_name
.or(source_b_name)
.and_then(|name| texture_specs.get(name).map(|spec| spec.sampler))
.unwrap_or_default();
let mut hasher = DefaultHasher::new();
source_a_name.hash(&mut hasher);
source_b_name.hash(&mut hasher);
output_name_prefix.hash(&mut hasher);
let hash = hasher.finish();
let name = format!(
"{}::{}_{:x}_{}",
namespace,
output_name_prefix,
hash,
sampler.signature()
);
texture_specs.entry(name.clone()).or_insert(TextureSpec {
recipe: TextureRecipe::PackRg {
a: a_image,
b: b_image,
},
usage: output_usage,
sampler,
});
Some(name)
}
fn pack_textures_rgb_a(
source_rgb_name: Option<&str>,
source_a_name: Option<&str>,
namespace: &str,
output_name_prefix: &str,
output_usage: TextureUsage,
texture_specs: &mut TextureSpecMap,
) -> Option<String> {
let rgb_image = source_rgb_name.and_then(|name| direct_image_source(name, texture_specs));
let alpha_image = source_a_name.and_then(|name| direct_image_source(name, texture_specs));
if rgb_image.is_none() && alpha_image.is_none() {
return None;
}
let sampler = source_rgb_name
.or(source_a_name)
.and_then(|name| texture_specs.get(name).map(|spec| spec.sampler))
.unwrap_or_default();
let mut hasher = DefaultHasher::new();
source_rgb_name.hash(&mut hasher);
source_a_name.hash(&mut hasher);
output_name_prefix.hash(&mut hasher);
let hash = hasher.finish();
let name = format!(
"{}::{}_{:x}_{}",
namespace,
output_name_prefix,
hash,
sampler.signature()
);
texture_specs.entry(name.clone()).or_insert(TextureSpec {
recipe: TextureRecipe::PackRgbA {
rgb: rgb_image,
alpha: alpha_image,
},
usage: output_usage,
sampler,
});
Some(name)
}
fn direct_image_source(name: &str, texture_specs: &TextureSpecMap) -> Option<SourceImageId> {
match texture_specs.get(name)?.recipe {
TextureRecipe::Direct { image } => Some(image),
_ => None,
}
}
fn register_texture(
texture: gltf::Texture<'_>,
usage: TextureUsage,
namespace: &str,
image_index_to_id: &HashMap<usize, SourceImageId>,
texture_specs: &mut TextureSpecMap,
) -> Option<String> {
let image_index = texture.source()?.index();
let image = *image_index_to_id.get(&image_index)?;
let usage_suffix = match usage {
TextureUsage::Color => "_srgb",
TextureUsage::Linear => "_linear",
};
let sampler_settings = sampler_settings_from_gltf(&texture.sampler());
let name = format!(
"{}::img{}{}_{}",
namespace,
image_index,
usage_suffix,
sampler_settings.signature()
);
texture_specs.entry(name.clone()).or_insert(TextureSpec {
recipe: TextureRecipe::Direct { image },
usage,
sampler: sampler_settings,
});
Some(name)
}
fn texture_transform_from_json(value: &serde_json::Value, base_uv: u32) -> TextureTransform {
let ext = value
.get("extensions")
.and_then(|extensions| extensions.get("KHR_texture_transform"));
let Some(ext) = ext else {
return TextureTransform {
offset: [0.0, 0.0],
rotation: 0.0,
scale: [1.0, 1.0],
uv_set: base_uv,
};
};
let offset = ext
.get("offset")
.and_then(|o| o.as_array())
.map(|arr| {
[
arr.first().and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
arr.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
]
})
.unwrap_or([0.0, 0.0]);
let rotation = ext.get("rotation").and_then(|r| r.as_f64()).unwrap_or(0.0) as f32;
let scale = ext
.get("scale")
.and_then(|s| s.as_array())
.map(|arr| {
[
arr.first().and_then(|v| v.as_f64()).unwrap_or(1.0) as f32,
arr.get(1).and_then(|v| v.as_f64()).unwrap_or(1.0) as f32,
]
})
.unwrap_or([1.0, 1.0]);
let uv_set = ext
.get("texCoord")
.and_then(|t| t.as_u64())
.map(|v| v as u32)
.unwrap_or(base_uv);
TextureTransform {
offset,
rotation,
scale,
uv_set,
}
}
fn resolve_extension_texture(
info: &serde_json::Value,
doc: &Document,
usage: TextureUsage,
namespace: &str,
image_index_to_id: &HashMap<usize, SourceImageId>,
texture_specs: &mut TextureSpecMap,
) -> Option<(String, TextureTransform)> {
let texture_index = info.get("index").and_then(|v| v.as_u64())? as usize;
let base_uv = info
.get("texCoord")
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(0);
let gltf_texture = doc.textures().nth(texture_index)?;
let name = register_texture(
gltf_texture,
usage,
namespace,
image_index_to_id,
texture_specs,
)?;
let transform = texture_transform_from_json(info, base_uv);
Some((name, transform))
}
#[derive(Debug, Clone)]
pub struct GltfSkin {
pub name: Option<String>,
pub joints: Vec<usize>,
pub inverse_bind_matrices: Vec<Mat4>,
}
pub struct GltfLoadResult {
pub prefabs: Vec<Prefab>,
pub meshes: HashMap<String, Mesh>,
pub materials: Vec<Material>,
pub encoded_images: Vec<Vec<u8>>,
pub texture_plan: Vec<TextureProductionPlan>,
pub animations: Vec<AnimationClip>,
pub skins: Vec<GltfSkin>,
pub node_to_skin: HashMap<usize, usize>,
pub node_to_morph_target_count: HashMap<usize, usize>,
pub node_count: usize,
pub suggested_exposure: f32,
}
pub fn import_gltf_from_path(
path: &std::path::Path,
) -> Result<GltfLoadResult, Box<dyn std::error::Error>> {
let (doc, buffers, encoded_images) = import_lenient_from_path(path)?;
let canonical_path = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let namespace = canonical_path
.to_str()
.unwrap_or("unnamed")
.replace('\\', "/")
.replace('/', "::");
process_gltf_data(doc, &buffers, encoded_images, &namespace)
}
pub fn import_gltf_from_bytes(bytes: &[u8]) -> Result<GltfLoadResult, Box<dyn std::error::Error>> {
let (doc, buffers, encoded_images) = import_lenient_from_slice(bytes)?;
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
let namespace = format!("bytes_{:016x}", hasher.finish());
process_gltf_data(doc, &buffers, encoded_images, &namespace)
}
pub fn import_gltf_with_resources(
gltf_bytes: &[u8],
resources: &HashMap<String, Vec<u8>>,
) -> Result<GltfLoadResult, Box<dyn std::error::Error>> {
let Gltf { document, blob } = Gltf::from_slice_without_validation(gltf_bytes)?;
let mut buffers: Vec<buffer::Data> = Vec::with_capacity(document.buffers().count());
for buffer in document.buffers() {
match buffer.source() {
buffer::Source::Bin => {
let bin = blob
.as_ref()
.ok_or("glTF references binary chunk but none was provided")?;
buffers.push(buffer::Data(bin.clone()));
}
buffer::Source::Uri(uri) => {
let bytes = resolve_uri_bytes(uri, resources)?;
buffers.push(buffer::Data(bytes));
}
}
}
let mut encoded_images: EncodedImages = Vec::with_capacity(document.images().count());
for img in document.images() {
let encoded: Vec<u8> = match img.source() {
image::Source::Uri { uri, .. } => resolve_uri_bytes(uri, resources)?,
image::Source::View { view, .. } => {
let buffer = &buffers[view.buffer().index()];
let start = view.offset();
let end = start + view.length();
buffer.0[start..end].to_vec()
}
};
encoded_images.push(Some(encoded));
}
let mut hasher = DefaultHasher::new();
gltf_bytes.hash(&mut hasher);
let namespace = format!("bytes_{:016x}", hasher.finish());
process_gltf_data(document, &buffers, encoded_images, &namespace)
}
fn resolve_uri_bytes(
uri: &str,
resources: &HashMap<String, Vec<u8>>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
if let Some(rest) = uri.strip_prefix("data:") {
let comma = rest
.find(',')
.ok_or("malformed data URI: missing comma separator")?;
let header = &rest[..comma];
let payload = &rest[comma + 1..];
if header.split(';').any(|piece| piece == "base64") {
use base64::Engine;
return Ok(base64::engine::general_purpose::STANDARD.decode(payload)?);
}
return Ok(percent_decode(payload).into_bytes());
}
let decoded = normalize_relative_path(&percent_decode(uri));
if let Some(bytes) = resources.get(&decoded) {
return Ok(bytes.clone());
}
Err(format!("missing external resource for URI '{}'", uri).into())
}
fn normalize_relative_path(input: &str) -> String {
let normalized = input.replace('\\', "/");
let mut parts: Vec<&str> = Vec::new();
for piece in normalized.split('/') {
match piece {
"" | "." => continue,
".." => {
parts.pop();
}
other => parts.push(other),
}
}
parts.join("/")
}
fn percent_decode(input: &str) -> String {
let bytes = input.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%'
&& index + 2 < bytes.len()
&& let (Some(high), Some(low)) =
(hex_value(bytes[index + 1]), hex_value(bytes[index + 2]))
{
out.push((high << 4) | low);
index += 3;
continue;
}
out.push(bytes[index]);
index += 1;
}
String::from_utf8(out).unwrap_or_else(|_| input.to_string())
}
fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn process_gltf_data(
doc: Document,
buffers: &[buffer::Data],
encoded_images: EncodedImages,
namespace: &str,
) -> Result<GltfLoadResult, Box<dyn std::error::Error>> {
let mut meshes = HashMap::new();
let mut materials = Vec::new();
let mut texture_specs: TextureSpecMap = HashMap::new();
let mut mesh_index_to_primitives: HashMap<usize, Vec<(String, usize)>> = HashMap::new();
let mut gltf_material_index_to_deduplicated_index: HashMap<usize, usize> = HashMap::new();
let mut mesh_to_skin: HashMap<usize, usize> = HashMap::new();
let mut node_to_morph_target_count: HashMap<usize, usize> = HashMap::new();
for node in doc.nodes() {
if let (Some(mesh_index), Some(skin_index)) = (
node.mesh().map(|m| m.index()),
node.skin().map(|s| s.index()),
) {
mesh_to_skin.insert(mesh_index, skin_index);
}
if let Some(mesh) = node.mesh() {
let morph_count = mesh
.primitives()
.next()
.map(|p| p.morph_targets().count())
.unwrap_or(0);
if morph_count > 0 {
node_to_morph_target_count.insert(node.index(), morph_count);
}
}
}
let image_count = encoded_images.len();
let mut image_index_to_id: HashMap<usize, SourceImageId> = HashMap::new();
let mut encoded_image_arena: Vec<Vec<u8>> = Vec::with_capacity(image_count);
for (index, opt_bytes) in encoded_images.into_iter().enumerate() {
if let Some(bytes) = opt_bytes {
let id = SourceImageId(encoded_image_arena.len() as u32);
encoded_image_arena.push(bytes);
image_index_to_id.insert(index, id);
}
}
for material in doc.materials() {
let pbr = material.pbr_metallic_roughness();
let base_color = pbr.base_color_factor();
let emissive = material.emissive_factor();
let mut base_color_array = [base_color[0], base_color[1], base_color[2], base_color[3]];
let mut roughness_value = pbr.roughness_factor();
let mut metallic_value = pbr.metallic_factor();
let (mut base_texture, mut base_texture_transform) = pbr
.base_color_texture()
.map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Color,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
.unwrap_or((None, TextureTransform::IDENTITY));
let (emissive_texture, emissive_texture_transform) = material
.emissive_texture()
.map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Color,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
.unwrap_or((None, TextureTransform::IDENTITY));
let (normal_texture, normal_texture_transform) = material
.normal_texture()
.map(|tex| {
let transform = texture_transform_from_normal(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
.unwrap_or((None, TextureTransform::IDENTITY));
let normal_scale = material
.normal_texture()
.map(|tex| tex.scale())
.unwrap_or(1.0);
let (mut metallic_roughness_texture, mut metallic_roughness_texture_transform) = pbr
.metallic_roughness_texture()
.map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
.unwrap_or((None, TextureTransform::IDENTITY));
let (occlusion_texture, occlusion_texture_transform) = material
.occlusion_texture()
.map(|tex| {
let transform = texture_transform_from_occlusion(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
.unwrap_or((None, TextureTransform::IDENTITY));
let occlusion_strength = material
.occlusion_texture()
.map(|tex| tex.strength())
.unwrap_or(1.0);
let transmission_factor = material
.transmission()
.map(|t| t.transmission_factor())
.unwrap_or(0.0);
let (transmission_texture, transmission_texture_transform) = material
.transmission()
.and_then(|t| {
t.transmission_texture().map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
})
.unwrap_or((None, TextureTransform::IDENTITY));
let thickness = material
.volume()
.map(|v| v.thickness_factor())
.unwrap_or(0.0);
let (thickness_texture, thickness_texture_transform) = material
.volume()
.and_then(|v| {
v.thickness_texture().map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
})
.unwrap_or((None, TextureTransform::IDENTITY));
let attenuation_color = material
.volume()
.map(|v| v.attenuation_color())
.unwrap_or([1.0, 1.0, 1.0]);
let attenuation_distance = material
.volume()
.map(|v| v.attenuation_distance())
.unwrap_or(f32::INFINITY);
let ior = material.ior().unwrap_or(1.5);
let specular_factor = material
.specular()
.map(|s| s.specular_factor())
.unwrap_or(1.0);
let specular_color_factor = material
.specular()
.map(|s| s.specular_color_factor())
.unwrap_or([1.0, 1.0, 1.0]);
let (specular_texture, specular_texture_transform) = material
.specular()
.and_then(|s| {
s.specular_texture().map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
})
.unwrap_or((None, TextureTransform::IDENTITY));
let (specular_color_texture, specular_color_texture_transform) = material
.specular()
.and_then(|s| {
s.specular_color_texture().map(|tex| {
let transform = texture_transform_from_info(&tex);
(
register_texture(
tex.texture(),
TextureUsage::Color,
namespace,
&image_index_to_id,
&mut texture_specs,
),
transform,
)
})
})
.unwrap_or((None, TextureTransform::IDENTITY));
let emissive_strength = material.emissive_strength().unwrap_or(1.0);
if let Some(spec_gloss) = material.pbr_specular_glossiness() {
let diffuse_factor = spec_gloss.diffuse_factor();
let specular_factor_sg = spec_gloss.specular_factor();
let glossiness_factor = spec_gloss.glossiness_factor();
let (converted_base_rgb, converted_metallic) =
spec_gloss_to_metallic_roughness_scalar(diffuse_factor, specular_factor_sg);
base_color_array = [
converted_base_rgb[0],
converted_base_rgb[1],
converted_base_rgb[2],
diffuse_factor[3],
];
metallic_value = converted_metallic;
roughness_value = (1.0 - glossiness_factor).clamp(0.0, 1.0);
let diffuse_texture_info = spec_gloss.diffuse_texture();
let spec_gloss_texture_info = spec_gloss.specular_glossiness_texture();
let diffuse_image = diffuse_texture_info.as_ref().and_then(|tex| {
image_index_to_id
.get(&tex.texture().source()?.index())
.copied()
});
let spec_gloss_image = spec_gloss_texture_info.as_ref().and_then(|tex| {
image_index_to_id
.get(&tex.texture().source()?.index())
.copied()
});
if diffuse_image.is_some() || spec_gloss_image.is_some() {
let mat_index = material.index().unwrap_or(usize::MAX);
let base_name = format!("{}::sg_converted_base_{}", namespace, mat_index);
let mr_name = format!("{}::sg_converted_mr_{}", namespace, mat_index);
let diffuse_sampler = diffuse_texture_info
.as_ref()
.map(|tex| sampler_settings_from_gltf(&tex.texture().sampler()))
.unwrap_or(SamplerSettings::DEFAULT);
let spec_gloss_sampler = spec_gloss_texture_info
.as_ref()
.map(|tex| sampler_settings_from_gltf(&tex.texture().sampler()))
.unwrap_or(SamplerSettings::DEFAULT);
texture_specs.insert(
base_name.clone(),
TextureSpec {
recipe: TextureRecipe::SpecGlossBase {
diffuse: diffuse_image,
diffuse_factor,
spec_gloss: spec_gloss_image,
specular_factor: specular_factor_sg,
glossiness_factor,
},
usage: TextureUsage::Color,
sampler: diffuse_sampler,
},
);
texture_specs.insert(
mr_name.clone(),
TextureSpec {
recipe: TextureRecipe::SpecGlossMr {
diffuse: diffuse_image,
diffuse_factor,
spec_gloss: spec_gloss_image,
specular_factor: specular_factor_sg,
glossiness_factor,
},
usage: TextureUsage::Linear,
sampler: spec_gloss_sampler,
},
);
base_texture = Some(base_name);
base_texture_transform = diffuse_texture_info
.as_ref()
.map(texture_transform_from_info)
.unwrap_or(TextureTransform::IDENTITY);
metallic_roughness_texture = Some(mr_name);
metallic_roughness_texture_transform = spec_gloss_texture_info
.as_ref()
.map(texture_transform_from_info)
.unwrap_or(TextureTransform::IDENTITY);
}
}
let diffuse_trans_ext = material.extension_value("KHR_materials_diffuse_transmission");
let diffuse_transmission_factor = diffuse_trans_ext
.and_then(|v| v.get("diffuseTransmissionFactor"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let diffuse_transmission_color_factor = diffuse_trans_ext
.and_then(|v| v.get("diffuseTransmissionColorFactor"))
.and_then(|v| v.as_array())
.map(|arr| {
[
arr.first().and_then(|v| v.as_f64()).unwrap_or(1.0) as f32,
arr.get(1).and_then(|v| v.as_f64()).unwrap_or(1.0) as f32,
arr.get(2).and_then(|v| v.as_f64()).unwrap_or(1.0) as f32,
]
})
.unwrap_or([1.0, 1.0, 1.0]);
let (diffuse_transmission_texture, diffuse_transmission_texture_transform) =
diffuse_trans_ext
.and_then(|v| v.get("diffuseTransmissionTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let (diffuse_transmission_color_texture_pre, diffuse_transmission_color_texture_transform) =
diffuse_trans_ext
.and_then(|v| v.get("diffuseTransmissionColorTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Color,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let diffuse_transmission_color_texture = pack_textures_rgb_a(
diffuse_transmission_color_texture_pre.as_deref(),
diffuse_transmission_texture.as_deref(),
namespace,
"diffuse_transmission_packed",
TextureUsage::Color,
&mut texture_specs,
);
let diffuse_transmission_texture: Option<String> = None;
let dispersion = material
.extension_value("KHR_materials_dispersion")
.and_then(|v| v.get("dispersion"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let anisotropy_ext = material.extension_value("KHR_materials_anisotropy");
let anisotropy_strength = anisotropy_ext
.and_then(|v| v.get("anisotropyStrength"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let anisotropy_rotation = anisotropy_ext
.and_then(|v| v.get("anisotropyRotation"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let (anisotropy_texture, anisotropy_texture_transform) = anisotropy_ext
.and_then(|v| v.get("anisotropyTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let iridescence_ext = material.extension_value("KHR_materials_iridescence");
let iridescence_factor = iridescence_ext
.and_then(|v| v.get("iridescenceFactor"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let iridescence_ior = iridescence_ext
.and_then(|v| v.get("iridescenceIor"))
.and_then(|v| v.as_f64())
.unwrap_or(1.3) as f32;
let iridescence_thickness_minimum = iridescence_ext
.and_then(|v| v.get("iridescenceThicknessMinimum"))
.and_then(|v| v.as_f64())
.unwrap_or(100.0) as f32;
let iridescence_thickness_maximum = iridescence_ext
.and_then(|v| v.get("iridescenceThicknessMaximum"))
.and_then(|v| v.as_f64())
.unwrap_or(400.0) as f32;
let (iridescence_texture_pre, iridescence_texture_transform) = iridescence_ext
.and_then(|v| v.get("iridescenceTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let (iridescence_thickness_texture_pre, iridescence_thickness_texture_transform) =
iridescence_ext
.and_then(|v| v.get("iridescenceThicknessTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let iridescence_texture = pack_textures_rg(
iridescence_texture_pre.as_deref(),
iridescence_thickness_texture_pre.as_deref(),
namespace,
"iridescence_packed",
TextureUsage::Linear,
&mut texture_specs,
);
let iridescence_thickness_texture: Option<String> = None;
let sheen_ext = material.extension_value("KHR_materials_sheen");
let sheen_color_factor = sheen_ext
.and_then(|v| v.get("sheenColorFactor"))
.and_then(|v| v.as_array())
.map(|arr| {
[
arr.first().and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
arr.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
arr.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
]
})
.unwrap_or([0.0, 0.0, 0.0]);
let sheen_roughness_factor = sheen_ext
.and_then(|v| v.get("sheenRoughnessFactor"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let (sheen_color_texture_pre, sheen_color_texture_transform) = sheen_ext
.and_then(|v| v.get("sheenColorTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Color,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let (sheen_roughness_texture_pre, sheen_roughness_texture_transform) = sheen_ext
.and_then(|v| v.get("sheenRoughnessTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let sheen_color_texture = pack_textures_rgb_a(
sheen_color_texture_pre.as_deref(),
sheen_roughness_texture_pre.as_deref(),
namespace,
"sheen_packed",
TextureUsage::Color,
&mut texture_specs,
);
let sheen_roughness_texture: Option<String> = None;
let clearcoat_ext = material.extension_value("KHR_materials_clearcoat");
let clearcoat_factor = clearcoat_ext
.and_then(|v| v.get("clearcoatFactor"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let clearcoat_roughness_factor = clearcoat_ext
.and_then(|v| v.get("clearcoatRoughnessFactor"))
.and_then(|v| v.as_f64())
.unwrap_or(0.0) as f32;
let (clearcoat_texture_pre, clearcoat_texture_transform) = clearcoat_ext
.and_then(|v| v.get("clearcoatTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let (clearcoat_roughness_texture_pre, clearcoat_roughness_texture_transform) =
clearcoat_ext
.and_then(|v| v.get("clearcoatRoughnessTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let (clearcoat_normal_texture, clearcoat_normal_texture_transform) = clearcoat_ext
.and_then(|v| v.get("clearcoatNormalTexture"))
.and_then(|info| {
resolve_extension_texture(
info,
&doc,
TextureUsage::Linear,
namespace,
&image_index_to_id,
&mut texture_specs,
)
})
.map(|(name, transform)| (Some(name), transform))
.unwrap_or((None, TextureTransform::IDENTITY));
let clearcoat_normal_scale = clearcoat_ext
.and_then(|v| v.get("clearcoatNormalTexture"))
.and_then(|info| info.get("scale"))
.and_then(|v| v.as_f64())
.unwrap_or(1.0) as f32;
let clearcoat_texture = pack_textures_rg(
clearcoat_texture_pre.as_deref(),
clearcoat_roughness_texture_pre.as_deref(),
namespace,
"clearcoat_packed",
TextureUsage::Linear,
&mut texture_specs,
);
let clearcoat_roughness_texture: Option<String> = clearcoat_texture.clone();
let world_material = Material {
base_color: base_color_array,
emissive_factor: [emissive[0], emissive[1], emissive[2]],
alpha_mode: match material.alpha_mode() {
gltf::material::AlphaMode::Opaque => AlphaMode::Opaque,
gltf::material::AlphaMode::Mask => AlphaMode::Mask,
gltf::material::AlphaMode::Blend => AlphaMode::Blend,
},
alpha_cutoff: material.alpha_cutoff().unwrap_or(0.5),
blend_opaque_alpha_threshold: 0.99,
base_texture,
base_texture_transform,
emissive_texture,
emissive_texture_transform,
normal_texture,
normal_texture_transform,
normal_scale,
normal_map_flip_y: false,
normal_map_two_component: false,
metallic_roughness_texture,
metallic_roughness_texture_transform,
occlusion_texture,
occlusion_texture_transform,
occlusion_strength,
roughness: roughness_value,
metallic: metallic_value,
unlit: material.unlit(),
double_sided: material.double_sided(),
transmission_factor,
transmission_texture,
transmission_texture_transform,
thickness,
thickness_texture,
thickness_texture_transform,
attenuation_color,
attenuation_distance,
ior,
specular_factor,
specular_color_factor,
specular_texture,
specular_texture_transform,
specular_color_texture,
specular_color_texture_transform,
emissive_strength,
diffuse_transmission_factor,
diffuse_transmission_texture,
diffuse_transmission_texture_transform,
diffuse_transmission_color_factor,
diffuse_transmission_color_texture,
diffuse_transmission_color_texture_transform,
dispersion,
anisotropy_strength,
anisotropy_rotation,
anisotropy_texture,
anisotropy_texture_transform,
iridescence_factor,
iridescence_texture,
iridescence_texture_transform,
iridescence_ior,
iridescence_thickness_minimum,
iridescence_thickness_maximum,
iridescence_thickness_texture,
iridescence_thickness_texture_transform,
sheen_color_factor,
sheen_color_texture,
sheen_color_texture_transform,
sheen_roughness_factor,
sheen_roughness_texture,
sheen_roughness_texture_transform,
clearcoat_factor,
clearcoat_texture,
clearcoat_texture_transform,
clearcoat_roughness_factor,
clearcoat_roughness_texture,
clearcoat_roughness_texture_transform,
clearcoat_normal_texture,
clearcoat_normal_texture_transform,
clearcoat_normal_scale,
};
let deduplicated_index =
if let Some(existing_index) = materials.iter().position(|m| m == &world_material) {
if let Some(mat_index) = material.index() {
tracing::debug!(
"Material {} is identical to material {}, reusing",
mat_index,
existing_index
);
}
existing_index
} else {
let new_index = materials.len();
materials.push(world_material);
new_index
};
if let Some(mat_index) = material.index() {
gltf_material_index_to_deduplicated_index.insert(mat_index, deduplicated_index);
}
}
let document_variant_names: Vec<String> = doc
.as_json()
.extensions
.as_ref()
.and_then(|exts| exts.others.get("KHR_materials_variants"))
.and_then(|v| v.get("variants"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|entry| entry.get("name").and_then(|n| n.as_str()))
.map(|name| name.to_string())
.collect()
})
.unwrap_or_default();
let mut mesh_index_to_primitive_variants: MeshVariantMap = HashMap::new();
for mesh in doc.meshes() {
let base_mesh_name = mesh
.name()
.unwrap_or(&format!("Mesh_{}", mesh.index()))
.to_string();
let mut primitive_names = Vec::new();
let mut primitive_variants: Vec<Vec<(Vec<u32>, u32)>> = Vec::new();
let default_morph_weights: Option<Vec<f32>> = mesh.weights().map(|w| w.to_vec());
for (primitive_index, primitive) in mesh.primitives().enumerate() {
if let Some(mut mesh_primitive) =
load_gltf_primitive(&primitive, buffers, default_morph_weights.as_deref())
{
if let Some(&skin_index) = mesh_to_skin.get(&mesh.index())
&& let Some(skin_data) = &mut mesh_primitive.skin_data
{
skin_data.skin_index = Some(skin_index);
}
let base_primitive_name = if mesh.primitives().len() > 1 {
format!("{}_{}", base_mesh_name, primitive_index)
} else {
base_mesh_name.clone()
};
let primitive_name = format!("{}::{}", namespace, base_primitive_name);
let gltf_material_index = primitive.material().index().unwrap_or(0);
let material_index = gltf_material_index_to_deduplicated_index
.get(&gltf_material_index)
.copied()
.unwrap_or(0);
primitive_names.push((primitive_name.clone(), material_index));
let mut mappings: Vec<(Vec<u32>, u32)> = Vec::new();
if let Some(mappings_value) = primitive
.extension_value("KHR_materials_variants")
.and_then(|v| v.get("mappings"))
.and_then(|v| v.as_array())
{
for entry in mappings_value {
let Some(material_idx) = entry.get("material").and_then(|v| v.as_u64())
else {
continue;
};
let Some(variant_arr) = entry.get("variants").and_then(|v| v.as_array())
else {
continue;
};
let variant_indices: Vec<u32> = variant_arr
.iter()
.filter_map(|i| i.as_u64().map(|n| n as u32))
.collect();
if variant_indices.is_empty() {
continue;
}
let dedup = gltf_material_index_to_deduplicated_index
.get(&(material_idx as usize))
.copied()
.unwrap_or(material_idx as usize);
mappings.push((variant_indices, dedup as u32));
}
}
primitive_variants.push(mappings);
meshes.insert(primitive_name, mesh_primitive);
}
}
if !primitive_names.is_empty() {
mesh_index_to_primitives.insert(mesh.index(), primitive_names);
mesh_index_to_primitive_variants.insert(mesh.index(), primitive_variants);
}
}
let mut prefabs = Vec::new();
let point_intensity_scale =
point_light_intensity_scale_for_generator(doc.as_json().asset.generator.as_deref());
let conversion_context = NodeConversionContext {
doc: &doc,
buffers,
mesh_index_to_primitives: &mesh_index_to_primitives,
mesh_index_to_primitive_variants: &mesh_index_to_primitive_variants,
materials: &materials,
document_variant_names: &document_variant_names,
point_intensity_scale,
};
for scene in doc.scenes() {
let scene_name = scene
.name()
.unwrap_or(&format!("Scene_{}", scene.index()))
.to_string();
let mut root_nodes = Vec::new();
for node in scene.nodes() {
convert_node_to_prefab_nodes(&conversion_context, &node, &mut root_nodes);
}
prefabs.push(Prefab {
name: scene_name,
root_nodes,
material_variants: document_variant_names.clone(),
});
}
let animations: Vec<AnimationClip> = doc
.animations()
.map(|animation| load_gltf_animation(&animation, buffers, &node_to_morph_target_count))
.collect();
let skins: Vec<GltfSkin> = doc
.skins()
.map(|skin| load_gltf_skin(&skin, buffers))
.collect();
let mut node_to_skin: HashMap<usize, usize> = HashMap::new();
for node in doc.nodes() {
if let Some(skin) = node.skin() {
node_to_skin.insert(node.index(), skin.index());
}
}
let node_count = doc.nodes().count();
let mut max_intensity: f32 = 1.0;
if let Some(extensions) = doc.as_json().extensions.as_ref()
&& let Some(lights_ext) = extensions.others.get("KHR_lights_punctual")
&& let Some(lights) = lights_ext.get("lights").and_then(|v| v.as_array())
{
for light in lights {
if let Some(intensity) = light.get("intensity").and_then(|v| v.as_f64()) {
max_intensity = max_intensity.max(intensity as f32);
}
}
}
for material in materials.iter() {
max_intensity = max_intensity.max(material.emissive_strength);
}
let suggested_exposure = if max_intensity > 10.0 {
(1.0 / max_intensity).clamp(1e-4, 1.0)
} else {
1.0
};
let texture_plan: Vec<TextureProductionPlan> = texture_specs
.into_iter()
.map(|(name, spec)| TextureProductionPlan {
name,
recipe: spec.recipe,
usage: spec.usage,
sampler: spec.sampler,
})
.collect();
Ok(GltfLoadResult {
prefabs,
meshes,
materials,
encoded_images: encoded_image_arena,
texture_plan,
animations,
skins,
node_to_skin,
node_to_morph_target_count,
node_count,
suggested_exposure,
})
}
type VariantMappings = Vec<(Vec<u32>, u32)>;
type MeshVariantMap = HashMap<usize, Vec<VariantMappings>>;
fn build_prefab_material_variants(
mappings: &[(Vec<u32>, u32)],
materials: &[Material],
variant_names: &[String],
) -> Vec<crate::ecs::prefab::components::PrefabMaterialVariant> {
let mut out = Vec::with_capacity(mappings.len());
for (variant_indices, material_index) in mappings {
if let Some(material) = materials.get(*material_index as usize) {
let names: Vec<String> = variant_indices
.iter()
.filter_map(|index| variant_names.get(*index as usize).cloned())
.collect();
if names.is_empty() {
continue;
}
out.push(crate::ecs::prefab::components::PrefabMaterialVariant {
variant_names: names,
material: material.clone(),
});
}
}
out
}
fn read_accessor_floats(
doc: &Document,
buffers: &[buffer::Data],
accessor_index: usize,
components_per_element: usize,
) -> Option<Vec<f32>> {
let accessor = doc.accessors().nth(accessor_index)?;
if accessor.data_type() != gltf::accessor::DataType::F32 {
tracing::warn!(
"EXT_mesh_gpu_instancing: quantized component types are not supported (accessor {})",
accessor_index
);
return None;
}
let view = accessor.view()?;
let buffer_data = buffers.get(view.buffer().index())?;
let buffer_bytes = &buffer_data.0;
let element_bytes = components_per_element * 4;
let stride = view.stride().unwrap_or(element_bytes);
let start = view.offset() + accessor.offset();
let mut out = Vec::with_capacity(accessor.count() * components_per_element);
for element_index in 0..accessor.count() {
let element_offset = start + element_index * stride;
if element_offset + element_bytes > buffer_bytes.len() {
return None;
}
for component_index in 0..components_per_element {
let byte_offset = element_offset + component_index * 4;
let bytes = &buffer_bytes[byte_offset..byte_offset + 4];
out.push(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
}
}
Some(out)
}
fn read_gpu_instancing_transforms(
doc: &Document,
buffers: &[buffer::Data],
node: &Node,
) -> Vec<LocalTransform> {
let extension = match node.extension_value("EXT_mesh_gpu_instancing") {
Some(value) => value,
None => return Vec::new(),
};
let attributes = match extension.get("attributes").and_then(|v| v.as_object()) {
Some(map) => map,
None => return Vec::new(),
};
let translation_accessor = attributes
.get("TRANSLATION")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let rotation_accessor = attributes
.get("ROTATION")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let scale_accessor = attributes
.get("SCALE")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let translations =
translation_accessor.and_then(|index| read_accessor_floats(doc, buffers, index, 3));
let rotations =
rotation_accessor.and_then(|index| read_accessor_floats(doc, buffers, index, 4));
let scales = scale_accessor.and_then(|index| read_accessor_floats(doc, buffers, index, 3));
let instance_count = translations
.as_ref()
.map(|values| values.len() / 3)
.or_else(|| rotations.as_ref().map(|values| values.len() / 4))
.or_else(|| scales.as_ref().map(|values| values.len() / 3))
.unwrap_or(0);
if instance_count == 0 {
return Vec::new();
}
let mut out = Vec::with_capacity(instance_count);
for instance_index in 0..instance_count {
let translation = translations
.as_ref()
.map(|values| {
Vec3::new(
values[instance_index * 3],
values[instance_index * 3 + 1],
values[instance_index * 3 + 2],
)
})
.unwrap_or_else(Vec3::zeros);
let rotation = rotations
.as_ref()
.map(|values| {
Quat::new(
values[instance_index * 4 + 3],
values[instance_index * 4],
values[instance_index * 4 + 1],
values[instance_index * 4 + 2],
)
})
.unwrap_or_else(Quat::identity);
let scale = scales
.as_ref()
.map(|values| {
Vec3::new(
values[instance_index * 3],
values[instance_index * 3 + 1],
values[instance_index * 3 + 2],
)
})
.unwrap_or_else(|| Vec3::new(1.0, 1.0, 1.0));
out.push(LocalTransform {
translation,
rotation,
scale,
});
}
out
}
struct NodeConversionContext<'a> {
doc: &'a Document,
buffers: &'a [buffer::Data],
mesh_index_to_primitives: &'a HashMap<usize, Vec<(String, usize)>>,
mesh_index_to_primitive_variants: &'a MeshVariantMap,
materials: &'a [Material],
document_variant_names: &'a [String],
point_intensity_scale: f32,
}
fn convert_node_to_prefab_nodes(
context: &NodeConversionContext<'_>,
node: &Node,
output_nodes: &mut Vec<PrefabNode>,
) {
let (translation, rotation, scale) = node.transform().decomposed();
let quat = Quat::new(rotation[3], rotation[0], rotation[1], rotation[2]);
let local_transform = LocalTransform {
translation: Vec3::new(translation[0], translation[1], translation[2]),
rotation: quat,
scale: Vec3::new(scale[0], scale[1], scale[2]),
};
let node_name = node.name().map(|s| s.to_string());
let node_skin_index = node.skin().map(|s| s.index());
let instance_transforms = read_gpu_instancing_transforms(context.doc, context.buffers, node);
if let Some(mesh) = node.mesh() {
let mesh_variants = context.mesh_index_to_primitive_variants.get(&mesh.index());
if let Some(primitives) = context.mesh_index_to_primitives.get(&mesh.index()) {
if primitives.len() == 1 {
let (mesh_name, material_index) = &primitives[0];
let mut components = PrefabComponents::default();
if let Some(name_str) = &node_name {
components.name = Some(Name(name_str.clone()));
}
components.render_mesh = Some(RenderMesh::new(mesh_name.clone()));
components.material = Some(
context
.materials
.get(*material_index)
.cloned()
.unwrap_or_else(Material::default),
);
if let Some(mappings) = mesh_variants.and_then(|v| v.first()) {
components.material_variants = build_prefab_material_variants(
mappings,
context.materials,
context.document_variant_names,
);
}
if let Some(camera) = node.camera() {
components.camera = Some(load_gltf_camera(&camera));
}
if let Some(light) = node.light() {
components.light = Some(load_gltf_light(&light, context.point_intensity_scale));
}
components.visibility = Some(Visibility { visible: true });
components.skin_index = node_skin_index;
components.instances = instance_transforms.clone();
let mut children = Vec::new();
for child in node.children() {
convert_node_to_prefab_nodes(context, &child, &mut children);
}
output_nodes.push(PrefabNode {
local_transform,
components,
children,
node_index: Some(node.index()),
});
} else {
let mut parent_components = PrefabComponents::default();
if let Some(name_str) = &node_name {
parent_components.name = Some(Name(name_str.clone()));
}
if let Some(camera) = node.camera() {
parent_components.camera = Some(load_gltf_camera(&camera));
}
if let Some(light) = node.light() {
parent_components.light =
Some(load_gltf_light(&light, context.point_intensity_scale));
}
parent_components.skin_index = node_skin_index;
let mut children = Vec::new();
for (index, (mesh_name, material_index)) in primitives.iter().enumerate() {
let mut primitive_components = PrefabComponents {
name: Some(Name(format!("Primitive_{}", index))),
render_mesh: Some(RenderMesh::new(mesh_name.clone())),
visibility: Some(Visibility { visible: true }),
skin_index: node_skin_index,
instances: instance_transforms.clone(),
..Default::default()
};
primitive_components.material = Some(
context
.materials
.get(*material_index)
.cloned()
.unwrap_or_else(Material::default),
);
if let Some(mappings) = mesh_variants.and_then(|v| v.get(index)) {
primitive_components.material_variants = build_prefab_material_variants(
mappings,
context.materials,
context.document_variant_names,
);
}
children.push(PrefabNode {
local_transform: LocalTransform::default(),
components: primitive_components,
children: Vec::new(),
node_index: None,
});
}
for child in node.children() {
convert_node_to_prefab_nodes(context, &child, &mut children);
}
output_nodes.push(PrefabNode {
local_transform,
components: parent_components,
children,
node_index: Some(node.index()),
});
}
} else {
let mut components = PrefabComponents::default();
if let Some(name_str) = &node_name {
components.name = Some(Name(name_str.clone()));
}
if let Some(camera) = node.camera() {
components.camera = Some(load_gltf_camera(&camera));
}
if let Some(light) = node.light() {
components.light = Some(load_gltf_light(&light, context.point_intensity_scale));
}
components.skin_index = node_skin_index;
let mut children = Vec::new();
for child in node.children() {
convert_node_to_prefab_nodes(context, &child, &mut children);
}
output_nodes.push(PrefabNode {
local_transform,
components,
children,
node_index: Some(node.index()),
});
}
} else {
let mut components = PrefabComponents::default();
if let Some(name_str) = &node_name {
components.name = Some(Name(name_str.clone()));
}
if let Some(camera) = node.camera() {
components.camera = Some(load_gltf_camera(&camera));
}
if let Some(light) = node.light() {
components.light = Some(load_gltf_light(&light, context.point_intensity_scale));
}
components.skin_index = node_skin_index;
let mut children = Vec::new();
for child in node.children() {
convert_node_to_prefab_nodes(context, &child, &mut children);
}
output_nodes.push(PrefabNode {
local_transform,
components,
children,
node_index: Some(node.index()),
});
}
}
fn load_gltf_light(
light: &gltf::khr_lights_punctual::Light<'_>,
point_intensity_scale: f32,
) -> Light {
let kind = light.kind();
let (light_type, inner_cone_angle, outer_cone_angle) = match kind {
gltf::khr_lights_punctual::Kind::Directional => (LightType::Directional, 0.0, 0.0),
gltf::khr_lights_punctual::Kind::Point => (LightType::Point, 0.0, 0.0),
gltf::khr_lights_punctual::Kind::Spot {
inner_cone_angle,
outer_cone_angle,
} => (LightType::Spot, inner_cone_angle, outer_cone_angle),
};
let color = light.color();
let mut intensity = light.intensity();
if !matches!(light_type, LightType::Directional) {
intensity *= point_intensity_scale;
}
let range = light.range().unwrap_or(0.0);
Light {
light_type,
color: Vec3::new(color[0], color[1], color[2]),
intensity,
range,
inner_cone_angle,
outer_cone_angle,
cast_shadows: matches!(light_type, LightType::Directional),
shadow_bias: 0.0005,
shadow_resolution: 0,
shadow_distance: 0.0,
cookie_texture: None,
}
}
fn point_light_intensity_scale_for_generator(generator: Option<&str>) -> f32 {
let Some(generator) = generator else {
return 1.0;
};
if !generator.contains("Khronos glTF Blender I/O") {
return 1.0;
}
let Some(version_part) = generator.rsplit('v').next() else {
return 1.0;
};
let Some(major_str) = version_part.split('.').next() else {
return 1.0;
};
let Ok(major) = major_str.parse::<u32>() else {
return 1.0;
};
if major < 3 {
1.0 / (4.0 * std::f32::consts::PI)
} else {
1.0
}
}
fn load_gltf_camera(camera: &gltf::Camera<'_>) -> Camera {
let projection = match camera.projection() {
gltf::camera::Projection::Perspective(persp) => {
Projection::Perspective(PerspectiveCamera {
aspect_ratio: persp.aspect_ratio(),
y_fov_rad: persp.yfov(),
z_far: persp.zfar(),
z_near: persp.znear(),
})
}
gltf::camera::Projection::Orthographic(ortho) => {
Projection::Orthographic(OrthographicCamera {
x_mag: ortho.xmag(),
y_mag: ortho.ymag(),
z_far: ortho.zfar(),
z_near: ortho.znear(),
})
}
};
Camera {
projection,
smoothing: None,
}
}
fn load_gltf_primitive(
primitive: &Primitive,
buffers: &[buffer::Data],
default_morph_weights: Option<&[f32]>,
) -> Option<Mesh> {
let reader = primitive.reader(|buffer| buffers.get(buffer.index()).map(|b| &b.0[..]));
let positions: Vec<Vec3> = reader
.read_positions()?
.map(|p| vec3(p[0], p[1], p[2]))
.collect();
if positions.is_empty() {
tracing::warn!("glTF primitive has no positions, skipping");
return None;
}
let normals: Vec<Vec3> = if let Some(normals_iter) = reader.read_normals() {
let normals: Vec<_> = normals_iter.map(|n| vec3(n[0], n[1], n[2])).collect();
if normals.len() != positions.len() {
tracing::warn!("glTF primitive has mismatched normal count, generating normals");
generate_normals(&positions, &reader.read_indices())
} else {
normals
}
} else {
generate_normals(&positions, &reader.read_indices())
};
let mut texcoords: Vec<Vec2> = vec![vec2(0.0, 0.0); positions.len()];
if let Some(tex_iter) = reader.read_tex_coords(0) {
let coords: Vec<_> = tex_iter.into_f32().map(|t| vec2(t[0], t[1])).collect();
if coords.len() == positions.len() {
texcoords = coords;
} else {
tracing::warn!("glTF primitive has mismatched texcoord count, using defaults");
}
}
let mut texcoords_1: Vec<Vec2> = vec![vec2(0.0, 0.0); positions.len()];
if let Some(tex_iter) = reader.read_tex_coords(1) {
let coords: Vec<_> = tex_iter.into_f32().map(|t| vec2(t[0], t[1])).collect();
if coords.len() == positions.len() {
texcoords_1 = coords;
}
}
let colors: Vec<[f32; 4]> = if let Some(color_iter) = reader.read_colors(0) {
let color_data: Vec<[f32; 4]> = color_iter.into_rgba_f32().collect();
if color_data.len() == positions.len() {
color_data
} else {
vec![[1.0, 1.0, 1.0, 1.0]; positions.len()]
}
} else {
vec![[1.0, 1.0, 1.0, 1.0]; positions.len()]
};
let raw_indices: Vec<u32> = if let Some(indices_iter) = reader.read_indices() {
indices_iter.into_u32().collect()
} else {
(0..positions.len() as u32).collect()
};
if raw_indices.is_empty() {
tracing::warn!("glTF primitive has no indices, skipping");
return None;
}
let has_normal_texture = primitive.material().normal_texture().is_some();
let raw_tangents: Option<Vec<[f32; 4]>> =
reader.read_tangents().map(|iter| iter.collect::<Vec<_>>());
let tangents_were_provided = raw_tangents
.as_ref()
.is_some_and(|tangents| tangents.len() == positions.len());
let tangents: Vec<[f32; 4]> = match raw_tangents {
Some(tangent_data) if tangent_data.len() == positions.len() => tangent_data,
_ => vec![[1.0, 0.0, 0.0, 1.0]; positions.len()],
};
let has_joints = reader.read_joints(0).is_some();
let joint_indices_opt: Option<Vec<[u32; 4]>> = if has_joints {
let mut joint_indices: Vec<[u32; 4]> = vec![[0, 0, 0, 0]; positions.len()];
if let Some(joints_iter) = reader.read_joints(0) {
let joints: Vec<[u16; 4]> = joints_iter.into_u16().collect();
if joints.len() == positions.len() {
for (vertex_index, joint) in joints.iter().enumerate() {
joint_indices[vertex_index] = [
joint[0] as u32,
joint[1] as u32,
joint[2] as u32,
joint[3] as u32,
];
}
}
}
Some(joint_indices)
} else {
None
};
let joint_weights_opt: Option<Vec<[f32; 4]>> = if has_joints {
let joint_weights: Vec<[f32; 4]> = if let Some(weights_iter) = reader.read_weights(0) {
let weights: Vec<[f32; 4]> = weights_iter.into_f32().collect();
if weights.len() == positions.len() {
weights
.into_iter()
.map(|w| {
let sum = w[0] + w[1] + w[2] + w[3];
if sum > 0.0 {
[w[0] / sum, w[1] / sum, w[2] / sum, w[3] / sum]
} else {
[1.0, 0.0, 0.0, 0.0]
}
})
.collect()
} else {
vec![[1.0, 0.0, 0.0, 0.0]; positions.len()]
}
} else {
vec![[1.0, 0.0, 0.0, 0.0]; positions.len()]
};
Some(joint_weights)
} else {
None
};
let mode = primitive.mode();
let bbox_extent = bounding_box_extent(&positions);
let mut attributes = PrimitiveAttributes {
positions,
normals,
texcoords,
texcoords_1,
colors,
tangents,
joint_indices: joint_indices_opt,
joint_weights: joint_weights_opt,
};
let indices = convert_primitive_to_triangles(mode, &raw_indices, &mut attributes, bbox_extent);
if indices.is_empty() {
tracing::warn!("glTF primitive has no triangles after mode conversion, skipping");
return None;
}
if !tangents_were_provided
&& has_normal_texture
&& matches!(
mode,
gltf::mesh::Mode::Triangles
| gltf::mesh::Mode::TriangleStrip
| gltf::mesh::Mode::TriangleFan
)
{
attributes.tangents = generate_mikktspace_tangents(
&attributes.positions,
&attributes.normals,
&attributes.texcoords,
&indices,
);
}
let PrimitiveAttributes {
positions,
normals,
texcoords,
texcoords_1,
colors,
tangents,
joint_indices: joint_indices_opt,
joint_weights: joint_weights_opt,
} = attributes;
let skin_data = if let (Some(joint_indices), Some(joint_weights)) =
(joint_indices_opt, joint_weights_opt)
{
let skinned_vertices: Vec<SkinnedVertex> = positions
.iter()
.zip(normals.iter())
.zip(texcoords.iter())
.zip(texcoords_1.iter())
.zip(tangents.iter())
.zip(colors.iter())
.zip(joint_indices.iter())
.zip(joint_weights.iter())
.map(
|(
(
(((((position, normal), tex_coords), tex_coords_1), tangent), color),
joint_indices,
),
joint_weights,
)| {
SkinnedVertex {
position: [position.x, position.y, position.z],
normal: [normal.x, normal.y, normal.z],
tex_coords: [tex_coords.x, tex_coords.y],
tex_coords_1: [tex_coords_1.x, tex_coords_1.y],
tangent: *tangent,
color: *color,
joint_indices: *joint_indices,
joint_weights: *joint_weights,
}
},
)
.collect();
Some(SkinData::new(skinned_vertices))
} else {
None
};
if positions.len() != normals.len() || positions.len() != texcoords.len() {
tracing::error!("glTF primitive has inconsistent vertex attribute lengths");
return None;
}
let vertices: Vec<Vertex> = positions
.iter()
.zip(&normals)
.zip(&texcoords)
.zip(&texcoords_1)
.zip(&tangents)
.zip(&colors)
.map(
|(((((position, normal), tex_coords), tex_coords_1), tangent), color)| Vertex {
position: [position.x, position.y, position.z],
normal: [normal.x, normal.y, normal.z],
tex_coords: [tex_coords.x, tex_coords.y],
tex_coords_1: [tex_coords_1.x, tex_coords_1.y],
tangent: *tangent,
color: *color,
},
)
.collect();
let mut min = nalgebra_glm::vec3(f32::MAX, f32::MAX, f32::MAX);
let mut max = nalgebra_glm::vec3(f32::MIN, f32::MIN, f32::MIN);
for position in &positions {
min.x = min.x.min(position.x);
min.y = min.y.min(position.y);
min.z = min.z.min(position.z);
max.x = max.x.max(position.x);
max.y = max.y.max(position.y);
max.z = max.z.max(position.z);
}
let (center, half_extents, sphere_radius) = if positions.is_empty() {
(
nalgebra_glm::Vec3::zeros(),
nalgebra_glm::Vec3::zeros(),
0.0,
)
} else {
let center = (min + max) * 0.5;
let half_extents = (max - min) * 0.5;
let sphere_radius = nalgebra_glm::length(&half_extents);
(center, half_extents, sphere_radius)
};
let bounding_volume = crate::ecs::bounding_volume::components::BoundingVolume {
obb: crate::ecs::bounding_volume::components::OrientedBoundingBox {
center,
half_extents,
orientation: nalgebra_glm::quat_identity(),
},
sphere_radius,
};
let morph_targets = load_morph_targets(
primitive,
buffers,
&positions,
&normals,
default_morph_weights,
);
let mut mesh = Mesh::with_bounding_volume(vertices, indices, bounding_volume);
mesh.skin_data = skin_data;
mesh.morph_targets = morph_targets;
Some(mesh)
}
struct PrimitiveAttributes {
positions: Vec<Vec3>,
normals: Vec<Vec3>,
texcoords: Vec<Vec2>,
texcoords_1: Vec<Vec2>,
colors: Vec<[f32; 4]>,
tangents: Vec<[f32; 4]>,
joint_indices: Option<Vec<[u32; 4]>>,
joint_weights: Option<Vec<[f32; 4]>>,
}
fn bounding_box_extent(positions: &[Vec3]) -> f32 {
if positions.is_empty() {
return 1.0;
}
let mut min = vec3(f32::MAX, f32::MAX, f32::MAX);
let mut max = vec3(f32::MIN, f32::MIN, f32::MIN);
for position in positions {
min.x = min.x.min(position.x);
min.y = min.y.min(position.y);
min.z = min.z.min(position.z);
max.x = max.x.max(position.x);
max.y = max.y.max(position.y);
max.z = max.z.max(position.z);
}
let extent = max - min;
extent.x.max(extent.y).max(extent.z).max(1.0e-6)
}
fn convert_primitive_to_triangles(
mode: gltf::mesh::Mode,
raw_indices: &[u32],
attributes: &mut PrimitiveAttributes,
bbox_extent: f32,
) -> Vec<u32> {
use gltf::mesh::Mode;
match mode {
Mode::Triangles => {
let triangle_count = raw_indices.len() / 3;
raw_indices[..triangle_count * 3].to_vec()
}
Mode::TriangleStrip => triangle_strip_to_list(raw_indices),
Mode::TriangleFan => triangle_fan_to_list(raw_indices),
Mode::Points => points_to_triangles(raw_indices, attributes, bbox_extent * 0.025),
Mode::Lines => {
let segments: Vec<(u32, u32)> = raw_indices
.chunks_exact(2)
.map(|chunk| (chunk[0], chunk[1]))
.collect();
line_segments_to_triangles(&segments, attributes, bbox_extent * 0.005)
}
Mode::LineStrip => {
if raw_indices.len() < 2 {
return Vec::new();
}
let segments: Vec<(u32, u32)> = (0..raw_indices.len() - 1)
.map(|segment_index| (raw_indices[segment_index], raw_indices[segment_index + 1]))
.collect();
line_segments_to_triangles(&segments, attributes, bbox_extent * 0.005)
}
Mode::LineLoop => {
if raw_indices.len() < 2 {
return Vec::new();
}
let mut segments: Vec<(u32, u32)> = (0..raw_indices.len() - 1)
.map(|segment_index| (raw_indices[segment_index], raw_indices[segment_index + 1]))
.collect();
segments.push((raw_indices[raw_indices.len() - 1], raw_indices[0]));
line_segments_to_triangles(&segments, attributes, bbox_extent * 0.005)
}
}
}
fn triangle_strip_to_list(strip_indices: &[u32]) -> Vec<u32> {
if strip_indices.len() < 3 {
return Vec::new();
}
let mut output = Vec::with_capacity((strip_indices.len() - 2) * 3);
for triangle_offset in 0..strip_indices.len() - 2 {
let (a, b, c) = if triangle_offset.is_multiple_of(2) {
(
strip_indices[triangle_offset],
strip_indices[triangle_offset + 1],
strip_indices[triangle_offset + 2],
)
} else {
(
strip_indices[triangle_offset + 1],
strip_indices[triangle_offset],
strip_indices[triangle_offset + 2],
)
};
if a == b || b == c || a == c {
continue;
}
output.push(a);
output.push(b);
output.push(c);
}
output
}
fn triangle_fan_to_list(fan_indices: &[u32]) -> Vec<u32> {
if fan_indices.len() < 3 {
return Vec::new();
}
let mut output = Vec::with_capacity((fan_indices.len() - 2) * 3);
let center = fan_indices[0];
for triangle_offset in 1..fan_indices.len() - 1 {
let edge_start = fan_indices[triangle_offset];
let edge_end = fan_indices[triangle_offset + 1];
if center == edge_start || edge_start == edge_end || center == edge_end {
continue;
}
output.push(center);
output.push(edge_start);
output.push(edge_end);
}
output
}
fn points_to_triangles(
point_indices: &[u32],
attributes: &mut PrimitiveAttributes,
radius: f32,
) -> Vec<u32> {
let octahedron_offsets = [
vec3(radius, 0.0, 0.0),
vec3(-radius, 0.0, 0.0),
vec3(0.0, radius, 0.0),
vec3(0.0, -radius, 0.0),
vec3(0.0, 0.0, radius),
vec3(0.0, 0.0, -radius),
];
let octahedron_triangles: [[u32; 3]; 8] = [
[0, 2, 4],
[2, 1, 4],
[1, 3, 4],
[3, 0, 4],
[2, 0, 5],
[1, 2, 5],
[3, 1, 5],
[0, 3, 5],
];
let mut new_attributes = PrimitiveAttributes {
positions: Vec::new(),
normals: Vec::new(),
texcoords: Vec::new(),
texcoords_1: Vec::new(),
colors: Vec::new(),
tangents: Vec::new(),
joint_indices: attributes.joint_indices.as_ref().map(|_| Vec::new()),
joint_weights: attributes.joint_weights.as_ref().map(|_| Vec::new()),
};
let mut output_indices = Vec::new();
for &point_index in point_indices {
let source = point_index as usize;
if source >= attributes.positions.len() {
continue;
}
let center = attributes.positions[source];
let texcoord = attributes.texcoords[source];
let texcoord_1 = attributes.texcoords_1[source];
let color = attributes.colors[source];
let tangent = attributes.tangents[source];
let source_joint_indices = attributes
.joint_indices
.as_ref()
.map(|joints| joints[source]);
let source_joint_weights = attributes
.joint_weights
.as_ref()
.map(|weights| weights[source]);
let base = new_attributes.positions.len() as u32;
for offset in &octahedron_offsets {
new_attributes.positions.push(center + offset);
new_attributes.normals.push(nalgebra_glm::normalize(offset));
new_attributes.texcoords.push(texcoord);
new_attributes.texcoords_1.push(texcoord_1);
new_attributes.colors.push(color);
new_attributes.tangents.push(tangent);
if let (Some(target), Some(value)) =
(new_attributes.joint_indices.as_mut(), source_joint_indices)
{
target.push(value);
}
if let (Some(target), Some(value)) =
(new_attributes.joint_weights.as_mut(), source_joint_weights)
{
target.push(value);
}
}
for triangle in &octahedron_triangles {
output_indices.push(base + triangle[0]);
output_indices.push(base + triangle[1]);
output_indices.push(base + triangle[2]);
}
}
*attributes = new_attributes;
output_indices
}
fn line_segments_to_triangles(
segments: &[(u32, u32)],
attributes: &mut PrimitiveAttributes,
radius: f32,
) -> Vec<u32> {
let prism_triangles: [[u32; 3]; 12] = [
[0, 3, 2],
[0, 2, 1],
[4, 5, 6],
[4, 6, 7],
[0, 4, 7],
[0, 7, 3],
[1, 2, 6],
[1, 6, 5],
[0, 1, 5],
[0, 5, 4],
[3, 7, 6],
[3, 6, 2],
];
let mut new_attributes = PrimitiveAttributes {
positions: Vec::new(),
normals: Vec::new(),
texcoords: Vec::new(),
texcoords_1: Vec::new(),
colors: Vec::new(),
tangents: Vec::new(),
joint_indices: attributes.joint_indices.as_ref().map(|_| Vec::new()),
joint_weights: attributes.joint_weights.as_ref().map(|_| Vec::new()),
};
let mut output_indices = Vec::new();
for &(start_index, end_index) in segments {
let start = start_index as usize;
let end = end_index as usize;
if start >= attributes.positions.len() || end >= attributes.positions.len() {
continue;
}
let p0 = attributes.positions[start];
let p1 = attributes.positions[end];
let direction = p1 - p0;
if nalgebra_glm::length(&direction) < 1.0e-8 {
continue;
}
let direction_norm = nalgebra_glm::normalize(&direction);
let up_candidate = if direction_norm.y.abs() < 0.99 {
vec3(0.0, 1.0, 0.0)
} else {
vec3(1.0, 0.0, 0.0)
};
let side_a =
nalgebra_glm::normalize(&nalgebra_glm::cross(&direction_norm, &up_candidate)) * radius;
let side_b =
nalgebra_glm::normalize(&nalgebra_glm::cross(&direction_norm, &side_a)) * radius;
let corners = [
p0 - side_a - side_b,
p0 + side_a - side_b,
p0 + side_a + side_b,
p0 - side_a + side_b,
p1 - side_a - side_b,
p1 + side_a - side_b,
p1 + side_a + side_b,
p1 - side_a + side_b,
];
let texcoord = attributes.texcoords[start];
let texcoord_1 = attributes.texcoords_1[start];
let color = attributes.colors[start];
let tangent = attributes.tangents[start];
let source_joint_indices = attributes
.joint_indices
.as_ref()
.map(|joints| joints[start]);
let source_joint_weights = attributes
.joint_weights
.as_ref()
.map(|weights| weights[start]);
let segment_center = p0 + (p1 - p0) * 0.5;
let base = new_attributes.positions.len() as u32;
for corner in &corners {
let outward = corner - segment_center;
let normal = if nalgebra_glm::length(&outward) > 1.0e-6 {
nalgebra_glm::normalize(&outward)
} else {
direction_norm
};
new_attributes.positions.push(*corner);
new_attributes.normals.push(normal);
new_attributes.texcoords.push(texcoord);
new_attributes.texcoords_1.push(texcoord_1);
new_attributes.colors.push(color);
new_attributes.tangents.push(tangent);
if let (Some(target), Some(value)) =
(new_attributes.joint_indices.as_mut(), source_joint_indices)
{
target.push(value);
}
if let (Some(target), Some(value)) =
(new_attributes.joint_weights.as_mut(), source_joint_weights)
{
target.push(value);
}
}
for triangle in &prism_triangles {
output_indices.push(base + triangle[0]);
output_indices.push(base + triangle[1]);
output_indices.push(base + triangle[2]);
}
}
*attributes = new_attributes;
output_indices
}
fn load_morph_targets(
primitive: &Primitive,
buffers: &[buffer::Data],
positions: &[Vec3],
normals: &[Vec3],
default_weights: Option<&[f32]>,
) -> Option<MorphTargetData> {
let reader = primitive.reader(|buffer| buffers.get(buffer.index()).map(|b| &b.0[..]));
let vertex_count = positions.len();
let morph_target_iter = reader.read_morph_targets();
let targets: Vec<MorphTarget> = morph_target_iter
.filter_map(|morph_target| {
let position_displacements: Vec<[f32; 3]> =
morph_target.0?.map(|p| [p[0], p[1], p[2]]).collect();
if position_displacements.len() != vertex_count {
return None;
}
let mut target = MorphTarget::new(position_displacements);
if let Some(normals_iter) = morph_target.1 {
let normal_displacements: Vec<[f32; 3]> =
normals_iter.map(|n| [n[0], n[1], n[2]]).collect();
if normal_displacements.len() == vertex_count {
target = target.with_normals(normal_displacements);
}
}
if let Some(tangents_iter) = morph_target.2 {
let tangent_displacements: Vec<[f32; 3]> =
tangents_iter.map(|t| [t[0], t[1], t[2]]).collect();
if tangent_displacements.len() == vertex_count {
target = target.with_tangents(tangent_displacements);
}
}
Some(target)
})
.collect();
if targets.is_empty() {
None
} else {
let base_positions: Vec<[f32; 3]> = positions.iter().map(|p| [p.x, p.y, p.z]).collect();
let base_normals: Vec<[f32; 3]> = normals.iter().map(|n| [n.x, n.y, n.z]).collect();
let weights = match default_weights {
Some(w) => w.iter().take(targets.len()).copied().collect(),
None => vec![0.0; targets.len()],
};
Some(
MorphTargetData::new(targets)
.with_base_data(base_positions, base_normals)
.with_default_weights(weights),
)
}
}
fn generate_normals(
positions: &[Vec3],
indices_reader: &Option<gltf::mesh::util::ReadIndices>,
) -> Vec<Vec3> {
let mut normals = vec![Vec3::zeros(); positions.len()];
let mut face_counts = vec![0u32; positions.len()];
let indices: Vec<u32> = if let Some(indices_iter) = indices_reader {
indices_iter.clone().into_u32().collect()
} else {
(0..positions.len() as u32).collect()
};
for triangle in indices.chunks(3) {
if triangle.len() != 3 {
continue;
}
let i0 = triangle[0] as usize;
let i1 = triangle[1] as usize;
let i2 = triangle[2] as usize;
if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
continue;
}
let v0 = positions[i0];
let v1 = positions[i1];
let v2 = positions[i2];
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let face_normal = nalgebra_glm::cross(&edge1, &edge2);
let face_normal = if nalgebra_glm::length(&face_normal) > 1e-6 {
nalgebra_glm::normalize(&face_normal)
} else {
vec3(0.0, 1.0, 0.0)
};
normals[i0] += face_normal;
normals[i1] += face_normal;
normals[i2] += face_normal;
face_counts[i0] += 1;
face_counts[i1] += 1;
face_counts[i2] += 1;
}
for (index, normal) in normals.iter_mut().enumerate() {
if face_counts[index] > 0 {
*normal /= face_counts[index] as f32;
let magnitude = nalgebra_glm::length(normal);
if magnitude > 1e-6 {
*normal = nalgebra_glm::normalize(normal);
} else {
*normal = vec3(0.0, 1.0, 0.0);
}
} else {
*normal = vec3(0.0, 1.0, 0.0);
}
}
normals
}
fn load_gltf_animation(
animation: &gltf::Animation,
buffers: &[buffer::Data],
node_to_morph_target_count: &HashMap<usize, usize>,
) -> AnimationClip {
let mut channels = Vec::new();
for (channel_index, channel) in animation.channels().enumerate() {
let reader = channel.reader(|buffer| buffers.get(buffer.index()).map(|b| &b.0[..]));
let target_gltf_node = channel.target().node();
let target_node = target_gltf_node.index();
let target_bone_name = target_gltf_node.name().map(|s| s.to_string());
let target_property = match channel.target().property() {
gltf::animation::Property::Translation => AnimationProperty::Translation,
gltf::animation::Property::Rotation => AnimationProperty::Rotation,
gltf::animation::Property::Scale => AnimationProperty::Scale,
gltf::animation::Property::MorphTargetWeights => AnimationProperty::MorphWeights,
};
let interpolation = match channel.sampler().interpolation() {
gltf::animation::Interpolation::Linear => AnimationInterpolation::Linear,
gltf::animation::Interpolation::Step => AnimationInterpolation::Step,
gltf::animation::Interpolation::CubicSpline => AnimationInterpolation::CubicSpline,
};
let Some(inputs) = reader.read_inputs() else {
tracing::warn!(" Channel {}: skipping - no input data", channel_index);
continue;
};
let input: Vec<f32> = inputs.collect();
let Some(outputs) = reader.read_outputs() else {
tracing::warn!(" Channel {}: skipping - no output data", channel_index);
continue;
};
let output = match outputs {
gltf::animation::util::ReadOutputs::Translations(translations) => {
if interpolation == AnimationInterpolation::CubicSpline {
let all_data: Vec<[f32; 3]> = translations.collect();
let num_keyframes = all_data.len() / 3;
let mut in_tangents = Vec::with_capacity(num_keyframes);
let mut values = Vec::with_capacity(num_keyframes);
let mut out_tangents = Vec::with_capacity(num_keyframes);
for keyframe_index in 0..num_keyframes {
let base_idx = keyframe_index * 3;
in_tangents.push(vec3(
all_data[base_idx][0],
all_data[base_idx][1],
all_data[base_idx][2],
));
values.push(vec3(
all_data[base_idx + 1][0],
all_data[base_idx + 1][1],
all_data[base_idx + 1][2],
));
out_tangents.push(vec3(
all_data[base_idx + 2][0],
all_data[base_idx + 2][1],
all_data[base_idx + 2][2],
));
}
AnimationSamplerOutput::CubicSplineVec3 {
values,
in_tangents,
out_tangents,
}
} else {
let values = translations.map(|t| vec3(t[0], t[1], t[2])).collect();
AnimationSamplerOutput::Vec3(values)
}
}
gltf::animation::util::ReadOutputs::Rotations(rotations) => {
if interpolation == AnimationInterpolation::CubicSpline {
let all_data: Vec<[f32; 4]> = rotations.into_f32().collect();
let num_keyframes = all_data.len() / 3;
let mut in_tangents = Vec::with_capacity(num_keyframes);
let mut values = Vec::with_capacity(num_keyframes);
let mut out_tangents = Vec::with_capacity(num_keyframes);
for keyframe_index in 0..num_keyframes {
let base_idx = keyframe_index * 3;
in_tangents.push(Quat::new(
all_data[base_idx][3],
all_data[base_idx][0],
all_data[base_idx][1],
all_data[base_idx][2],
));
values.push(
Quat::new(
all_data[base_idx + 1][3],
all_data[base_idx + 1][0],
all_data[base_idx + 1][1],
all_data[base_idx + 1][2],
)
.normalize(),
);
out_tangents.push(Quat::new(
all_data[base_idx + 2][3],
all_data[base_idx + 2][0],
all_data[base_idx + 2][1],
all_data[base_idx + 2][2],
));
}
AnimationSamplerOutput::CubicSplineQuat {
values,
in_tangents,
out_tangents,
}
} else {
let values = rotations
.into_f32()
.map(|r| Quat::new(r[3], r[0], r[1], r[2]).normalize())
.collect();
AnimationSamplerOutput::Quat(values)
}
}
gltf::animation::util::ReadOutputs::Scales(scales) => {
if interpolation == AnimationInterpolation::CubicSpline {
let all_data: Vec<[f32; 3]> = scales.collect();
let num_keyframes = all_data.len() / 3;
let mut in_tangents = Vec::with_capacity(num_keyframes);
let mut values = Vec::with_capacity(num_keyframes);
let mut out_tangents = Vec::with_capacity(num_keyframes);
for keyframe_index in 0..num_keyframes {
let base_idx = keyframe_index * 3;
in_tangents.push(vec3(
all_data[base_idx][0],
all_data[base_idx][1],
all_data[base_idx][2],
));
values.push(vec3(
all_data[base_idx + 1][0],
all_data[base_idx + 1][1],
all_data[base_idx + 1][2],
));
out_tangents.push(vec3(
all_data[base_idx + 2][0],
all_data[base_idx + 2][1],
all_data[base_idx + 2][2],
));
}
AnimationSamplerOutput::CubicSplineVec3 {
values,
in_tangents,
out_tangents,
}
} else {
let values = scales.map(|s| vec3(s[0], s[1], s[2])).collect();
AnimationSamplerOutput::Vec3(values)
}
}
gltf::animation::util::ReadOutputs::MorphTargetWeights(weights) => {
let weight_count = node_to_morph_target_count
.get(&target_node)
.copied()
.unwrap_or(1);
if weight_count == 0 {
continue;
}
if interpolation == AnimationInterpolation::CubicSpline {
let all_data: Vec<f32> = weights.into_f32().collect();
let elements_per_keyframe = weight_count * 3;
if !all_data.len().is_multiple_of(elements_per_keyframe) {
continue;
}
let num_keyframes = all_data.len() / elements_per_keyframe;
let mut in_tangents = Vec::with_capacity(num_keyframes);
let mut values = Vec::with_capacity(num_keyframes);
let mut out_tangents = Vec::with_capacity(num_keyframes);
for keyframe_index in 0..num_keyframes {
let base_idx = keyframe_index * elements_per_keyframe;
let in_tangent: Vec<f32> =
all_data[base_idx..base_idx + weight_count].to_vec();
let value: Vec<f32> =
all_data[base_idx + weight_count..base_idx + weight_count * 2].to_vec();
let out_tangent: Vec<f32> = all_data
[base_idx + weight_count * 2..base_idx + weight_count * 3]
.to_vec();
in_tangents.push(in_tangent);
values.push(value);
out_tangents.push(out_tangent);
}
AnimationSamplerOutput::CubicSplineWeights {
values,
in_tangents,
out_tangents,
}
} else {
let all_data: Vec<f32> = weights.into_f32().collect();
if !all_data.len().is_multiple_of(weight_count) {
continue;
}
let values: Vec<Vec<f32>> = all_data
.chunks(weight_count)
.map(|chunk| chunk.to_vec())
.collect();
AnimationSamplerOutput::Weights(values)
}
}
};
let sampler = AnimationSampler {
input,
output,
interpolation,
};
channels.push(AnimationChannel {
target_node,
target_bone_name,
target_property,
sampler,
});
}
let duration = channels
.iter()
.flat_map(|channel| channel.sampler.input.last())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.copied()
.unwrap_or(0.0);
AnimationClip {
name: animation.name().unwrap_or("Animation").to_string(),
duration,
channels,
}
}
fn load_gltf_skin(skin: &gltf::Skin, buffers: &[buffer::Data]) -> GltfSkin {
let joints: Vec<usize> = skin.joints().map(|j| j.index()).collect();
let mut inverse_bind_matrices = Vec::new();
if let Some(accessor) = skin.inverse_bind_matrices() {
let view = accessor
.view()
.expect("Inverse bind matrices accessor has no view");
let buffer_index = view.buffer().index();
if let Some(buffer_data) = buffers.get(buffer_index) {
let buffer_bytes = &buffer_data.0;
let view_offset = view.offset();
let accessor_offset = accessor.offset();
let start = view_offset + accessor_offset;
let stride = view.stride().unwrap_or(64);
for joint_index in 0..accessor.count() {
let matrix_start = start + joint_index * stride;
if matrix_start + 64 <= buffer_bytes.len() {
let mut floats = [0.0f32; 16];
for (float_index, float_value) in floats.iter_mut().enumerate() {
let byte_offset = matrix_start + float_index * 4;
let bytes = &buffer_bytes[byte_offset..byte_offset + 4];
*float_value = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
}
let matrix = Mat4::new(
floats[0], floats[4], floats[8], floats[12], floats[1], floats[5],
floats[9], floats[13], floats[2], floats[6], floats[10], floats[14],
floats[3], floats[7], floats[11], floats[15],
);
inverse_bind_matrices.push(matrix);
}
}
}
}
while inverse_bind_matrices.len() < joints.len() {
inverse_bind_matrices.push(Mat4::identity());
}
GltfSkin {
name: skin.name().map(|s| s.to_string()),
joints,
inverse_bind_matrices,
}
}
struct IndexedMesh<'a> {
positions: &'a [Vec3],
normals: &'a [Vec3],
texcoords: &'a [Vec2],
indices: &'a [u32],
tangents: Vec<[f32; 4]>,
written: Vec<bool>,
}
impl bevy_mikktspace::Geometry for IndexedMesh<'_> {
fn num_faces(&self) -> usize {
self.indices.len() / 3
}
fn num_vertices_of_face(&self, _face: usize) -> usize {
3
}
fn position(&self, face: usize, vert: usize) -> [f32; 3] {
let position = self.positions[self.indices[face * 3 + vert] as usize];
[position.x, position.y, position.z]
}
fn normal(&self, face: usize, vert: usize) -> [f32; 3] {
let normal = self.normals[self.indices[face * 3 + vert] as usize];
[normal.x, normal.y, normal.z]
}
fn tex_coord(&self, face: usize, vert: usize) -> [f32; 2] {
let texcoord = self.texcoords[self.indices[face * 3 + vert] as usize];
[texcoord.x, 1.0 - texcoord.y]
}
fn set_tangent(
&mut self,
tangent_space: Option<bevy_mikktspace::TangentSpace>,
face: usize,
vert: usize,
) {
let Some(ts) = tangent_space else {
return;
};
let vertex_index = self.indices[face * 3 + vert] as usize;
if self.written[vertex_index] {
return;
}
let tangent = ts.tangent();
let sign = if ts.is_orientation_preserving() {
1.0
} else {
-1.0
};
self.tangents[vertex_index] = [tangent[0], tangent[1], tangent[2], sign];
self.written[vertex_index] = true;
}
}
fn generate_mikktspace_tangents(
positions: &[Vec3],
normals: &[Vec3],
texcoords: &[Vec2],
indices: &[u32],
) -> Vec<[f32; 4]> {
let mut mesh = IndexedMesh {
positions,
normals,
texcoords,
indices,
tangents: vec![[1.0, 0.0, 0.0, 1.0]; positions.len()],
written: vec![false; positions.len()],
};
if let Err(error) = bevy_mikktspace::generate_tangents(&mut mesh) {
tracing::warn!(
"mikktspace tangent generation failed ({:?}); falling back to default tangents",
error
);
return vec![[1.0, 0.0, 0.0, 1.0]; positions.len()];
}
mesh.tangents
}
fn solve_metallic_for_spec_gloss(
diffuse: f32,
specular: f32,
one_minus_specular_strength: f32,
) -> f32 {
const DIELECTRIC_SPECULAR: f32 = 0.04;
if specular < DIELECTRIC_SPECULAR {
return 0.0;
}
let a = DIELECTRIC_SPECULAR;
let b = diffuse * one_minus_specular_strength / (1.0 - DIELECTRIC_SPECULAR) + specular
- 2.0 * DIELECTRIC_SPECULAR;
let c = DIELECTRIC_SPECULAR - specular;
let discriminant = (b * b - 4.0 * a * c).max(0.0);
((-b + discriminant.sqrt()) / (2.0 * a)).clamp(0.0, 1.0)
}
fn spec_gloss_to_metallic_roughness_scalar(
diffuse: [f32; 4],
specular: [f32; 3],
) -> ([f32; 3], f32) {
const DIELECTRIC_SPECULAR: f32 = 0.04;
const EPSILON: f32 = 1e-6;
let max_specular = specular[0].max(specular[1]).max(specular[2]);
let one_minus_specular_strength = 1.0 - max_specular;
let max_diffuse = diffuse[0].max(diffuse[1]).max(diffuse[2]);
let metallic =
solve_metallic_for_spec_gloss(max_diffuse, max_specular, one_minus_specular_strength);
let base_from_diffuse = |c: f32| -> f32 {
c * one_minus_specular_strength
/ (1.0 - DIELECTRIC_SPECULAR).max(EPSILON)
/ (1.0 - metallic).max(EPSILON)
};
let base_from_specular =
|c: f32| -> f32 { (c - DIELECTRIC_SPECULAR * (1.0 - metallic)) / metallic.max(EPSILON) };
let blend = metallic * metallic;
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
let base_color = [
lerp(
base_from_diffuse(diffuse[0]),
base_from_specular(specular[0]),
blend,
)
.clamp(0.0, 1.0),
lerp(
base_from_diffuse(diffuse[1]),
base_from_specular(specular[1]),
blend,
)
.clamp(0.0, 1.0),
lerp(
base_from_diffuse(diffuse[2]),
base_from_specular(specular[2]),
blend,
)
.clamp(0.0, 1.0),
];
(base_color, metallic)
}