use crate::ecs::animation::components::{
AnimationChannel, AnimationInterpolation, AnimationProperty, AnimationSamplerOutput,
};
use crate::ecs::light::components::{Light, LightType};
use crate::ecs::material::components::{AlphaMode, Material, TextureTransform};
use crate::ecs::mesh::components::Mesh;
use crate::ecs::world::components::{Camera, Projection};
use crate::ecs::world::{Entity, LOCAL_TRANSFORM, World};
use crate::render::wgpu::texture_cache::{SamplerFilter, SamplerSettings, SamplerWrap};
use nalgebra_glm::Quat;
use serde_json::{Map, Value, json};
use std::collections::{BTreeSet, HashMap, HashSet};
const GLB_MAGIC: u32 = 0x4654_6C67;
const GLB_VERSION: u32 = 2;
const GLB_CHUNK_JSON: u32 = 0x4E4F_534A;
const GLB_CHUNK_BIN: u32 = 0x004E_4942;
const TARGET_ARRAY_BUFFER: u32 = 34962;
const TARGET_ELEMENT_ARRAY_BUFFER: u32 = 34963;
const COMPONENT_U16: u32 = 5123;
const COMPONENT_U32: u32 = 5125;
const COMPONENT_F32: u32 = 5126;
#[derive(Default)]
struct ExportContext {
buffer: Vec<u8>,
buffer_views: Vec<Value>,
accessors: Vec<Value>,
samplers: Vec<Value>,
sampler_indices: HashMap<String, usize>,
images: Vec<Value>,
textures: Vec<Value>,
texture_indices: HashMap<String, Option<usize>>,
materials: Vec<Value>,
material_indices: HashMap<String, Option<usize>>,
meshes: Vec<Value>,
mesh_indices: HashMap<(String, Option<String>), Option<usize>>,
skins: Vec<Value>,
cameras: Vec<Value>,
lights: Vec<Value>,
extensions_used: BTreeSet<String>,
}
pub fn export_world_to_glb_bytes(
world: &World,
excluded_entities: &HashSet<Entity>,
) -> Result<Vec<u8>, String> {
let mut context = ExportContext::default();
let entities = collect_export_entities(world, excluded_entities);
let entity_to_node: HashMap<Entity, usize> = entities
.iter()
.enumerate()
.map(|(index, entity)| (*entity, index))
.collect();
let mut nodes: Vec<Map<String, Value>> = Vec::with_capacity(entities.len());
let mut node_children: Vec<Vec<usize>> = Vec::with_capacity(entities.len());
for entity in &entities {
nodes.push(build_entity_node(&mut context, world, *entity)?);
node_children.push(Vec::new());
}
expand_instanced_meshes(
&mut context,
world,
&entities,
&mut nodes,
&mut node_children,
);
attach_skins(&mut context, world, &entities, &entity_to_node, &mut nodes);
let mut root_nodes: Vec<usize> = Vec::new();
for (node_index, entity) in entities.iter().enumerate() {
let parent = world
.core
.get_parent(*entity)
.and_then(|parent| parent.0)
.and_then(|parent_entity| entity_to_node.get(&parent_entity).copied());
match parent {
Some(parent_index) => node_children[parent_index].push(node_index),
None => root_nodes.push(node_index),
}
}
for (node, children) in nodes.iter_mut().zip(&node_children) {
if !children.is_empty() {
node.insert(
"children".to_string(),
Value::Array(children.iter().map(|index| json!(index)).collect()),
);
}
}
let animations = build_animations(&mut context, world, &entities, &entity_to_node);
let (root, binary) = assemble_root(context, nodes, root_nodes, animations);
encode_glb(root, binary)
}
fn collect_export_entities(world: &World, excluded_entities: &HashSet<Entity>) -> Vec<Entity> {
let mut entities: Vec<Entity> = Vec::new();
world
.core
.query()
.with(LOCAL_TRANSFORM)
.iter(|entity, _, _| entities.push(entity));
entities.retain(|entity| !has_excluded_ancestor(world, excluded_entities, *entity));
entities
}
fn has_excluded_ancestor(
world: &World,
excluded_entities: &HashSet<Entity>,
entity: Entity,
) -> bool {
let mut current = Some(entity);
while let Some(candidate) = current {
if excluded_entities.contains(&candidate) {
return true;
}
current = world.core.get_parent(candidate).and_then(|parent| parent.0);
}
false
}
fn build_entity_node(
context: &mut ExportContext,
world: &World,
entity: Entity,
) -> Result<Map<String, Value>, String> {
let mut node = Map::new();
if let Some(name) = world.core.get_name(entity)
&& !name.0.is_empty()
{
node.insert("name".to_string(), json!(name.0));
}
if let Some(transform) = world.core.get_local_transform(entity) {
let translation = transform.translation;
if translation != nalgebra_glm::Vec3::zeros() {
node.insert(
"translation".to_string(),
json!([translation.x, translation.y, translation.z]),
);
}
let rotation = transform.rotation;
if !is_identity_quat(&rotation) {
node.insert(
"rotation".to_string(),
json!([rotation.i, rotation.j, rotation.k, rotation.w]),
);
}
let scale = transform.scale;
if scale != nalgebra_glm::Vec3::new(1.0, 1.0, 1.0) {
node.insert("scale".to_string(), json!([scale.x, scale.y, scale.z]));
}
}
let visible = world
.core
.get_visibility(entity)
.map(|visibility| visibility.visible)
.unwrap_or(true);
if visible && let Some(render_mesh) = world.core.get_render_mesh(entity) {
let material_name = world
.core
.get_material_ref(entity)
.map(|material_ref| material_ref.name.clone());
if let Some(mesh_index) =
resolve_mesh_index(context, world, &render_mesh.name, material_name.as_deref())?
{
node.insert("mesh".to_string(), json!(mesh_index));
if let Some(morph_weights) = world.core.get_morph_weights(entity)
&& !morph_weights.weights.is_empty()
{
node.insert("weights".to_string(), json!(morph_weights.weights));
}
}
}
if let Some(camera) = world.core.get_camera(entity) {
let camera_index = context.cameras.len();
context.cameras.push(build_camera(camera, world, entity));
node.insert("camera".to_string(), json!(camera_index));
}
if let Some(light) = world.core.get_light(entity)
&& !matches!(light.light_type, LightType::Area)
{
let light_index = context.lights.len();
context.lights.push(build_light(light));
context
.extensions_used
.insert("KHR_lights_punctual".to_string());
node.insert(
"extensions".to_string(),
json!({ "KHR_lights_punctual": { "light": light_index } }),
);
}
Ok(node)
}
fn is_identity_quat(rotation: &Quat) -> bool {
rotation.i == 0.0 && rotation.j == 0.0 && rotation.k == 0.0 && rotation.w == 1.0
}
fn expand_instanced_meshes(
context: &mut ExportContext,
world: &World,
entities: &[Entity],
nodes: &mut Vec<Map<String, Value>>,
node_children: &mut Vec<Vec<usize>>,
) {
for (node_index, entity) in entities.iter().enumerate() {
let Some(instanced) = world.core.get_instanced_mesh(*entity) else {
continue;
};
let material_name = world
.core
.get_material_ref(*entity)
.map(|material_ref| material_ref.name.clone());
let Ok(Some(mesh_index)) = resolve_mesh_index(
context,
world,
&instanced.mesh_name,
material_name.as_deref(),
) else {
continue;
};
for instance in &instanced.instances {
let mut instance_node = Map::new();
instance_node.insert("mesh".to_string(), json!(mesh_index));
if instance.translation != nalgebra_glm::Vec3::zeros() {
instance_node.insert(
"translation".to_string(),
json!([
instance.translation.x,
instance.translation.y,
instance.translation.z
]),
);
}
if !is_identity_quat(&instance.rotation) {
instance_node.insert(
"rotation".to_string(),
json!([
instance.rotation.i,
instance.rotation.j,
instance.rotation.k,
instance.rotation.w
]),
);
}
if instance.scale != nalgebra_glm::Vec3::new(1.0, 1.0, 1.0) {
instance_node.insert(
"scale".to_string(),
json!([instance.scale.x, instance.scale.y, instance.scale.z]),
);
}
let instance_index = nodes.len();
nodes.push(instance_node);
node_children.push(Vec::new());
node_children[node_index].push(instance_index);
}
}
}
fn attach_skins(
context: &mut ExportContext,
world: &World,
entities: &[Entity],
entity_to_node: &HashMap<Entity, usize>,
nodes: &mut [Map<String, Value>],
) {
let mut skins: Vec<Value> = Vec::new();
let mut skin_indices: HashMap<Vec<Entity>, usize> = HashMap::new();
for (node_index, entity) in entities.iter().enumerate() {
let Some(skin) = world.core.get_skin(*entity) else {
continue;
};
if !nodes[node_index].contains_key("mesh") {
continue;
}
let joint_nodes: Option<Vec<usize>> = skin
.joints
.iter()
.map(|joint| entity_to_node.get(joint).copied())
.collect();
let Some(joint_nodes) = joint_nodes else {
continue;
};
if joint_nodes.is_empty() {
continue;
}
let skin_index = match skin_indices.get(&skin.joints) {
Some(existing) => *existing,
None => {
let mut matrix_floats: Vec<f32> =
Vec::with_capacity(skin.inverse_bind_matrices.len() * 16);
for matrix in skin.inverse_bind_matrices.iter() {
matrix_floats.extend_from_slice(matrix.as_slice());
}
while matrix_floats.len() < joint_nodes.len() * 16 {
matrix_floats.extend_from_slice(nalgebra_glm::Mat4::identity().as_slice());
}
matrix_floats.truncate(joint_nodes.len() * 16);
let matrices_accessor =
push_f32_accessor(context, &matrix_floats, "MAT4", None, false);
let mut skin_json = Map::new();
skin_json.insert(
"joints".to_string(),
Value::Array(joint_nodes.iter().map(|index| json!(index)).collect()),
);
skin_json.insert("inverseBindMatrices".to_string(), json!(matrices_accessor));
if let Some(name) = &skin.name {
skin_json.insert("name".to_string(), json!(name));
}
let new_index = skins.len();
skins.push(Value::Object(skin_json));
skin_indices.insert(skin.joints.clone(), new_index);
new_index
}
};
nodes[node_index].insert("skin".to_string(), json!(skin_index));
}
context.skins = skins;
}
fn resolve_mesh_index(
context: &mut ExportContext,
world: &World,
mesh_name: &str,
material_name: Option<&str>,
) -> Result<Option<usize>, String> {
let key = (mesh_name.to_string(), material_name.map(str::to_string));
if let Some(existing) = context.mesh_indices.get(&key) {
return Ok(*existing);
}
let Some(mesh) = crate::ecs::generational_registry::registry_entry_by_name(
&world.resources.assets.mesh_cache.registry,
mesh_name,
) else {
context.mesh_indices.insert(key, None);
return Ok(None);
};
if mesh.vertices.is_empty() || mesh.indices.is_empty() {
context.mesh_indices.insert(key, None);
return Ok(None);
}
let material_index = match material_name {
Some(name) => resolve_material_index(context, world, name),
None => None,
};
let primitive = build_mesh_primitive(context, mesh, material_index)?;
let display_name = mesh_name.rsplit("::").next().unwrap_or(mesh_name);
let mut mesh_json = Map::new();
mesh_json.insert("name".to_string(), json!(display_name));
mesh_json.insert("primitives".to_string(), Value::Array(vec![primitive]));
if let Some(morph_targets) = &mesh.morph_targets
&& !morph_targets.default_weights.is_empty()
{
mesh_json.insert("weights".to_string(), json!(morph_targets.default_weights));
}
let mesh_index = context.meshes.len();
context.meshes.push(Value::Object(mesh_json));
context.mesh_indices.insert(key, Some(mesh_index));
Ok(Some(mesh_index))
}
fn build_mesh_primitive(
context: &mut ExportContext,
mesh: &Mesh,
material_index: Option<usize>,
) -> Result<Value, String> {
let vertex_count = mesh.vertices.len();
let mut positions: Vec<f32> = Vec::with_capacity(vertex_count * 3);
let mut normals: Vec<f32> = Vec::with_capacity(vertex_count * 3);
let mut tex_coords_0: Vec<f32> = Vec::with_capacity(vertex_count * 2);
let mut tex_coords_1: Vec<f32> = Vec::with_capacity(vertex_count * 2);
let mut tangents: Vec<f32> = Vec::with_capacity(vertex_count * 4);
let mut colors: Vec<f32> = Vec::with_capacity(vertex_count * 4);
let mut joint_indices: Vec<u32> = Vec::new();
let mut joint_weights: Vec<f32> = Vec::new();
let skinned_vertices = mesh
.skin_data
.as_ref()
.map(|skin_data| &skin_data.skinned_vertices)
.filter(|skinned| skinned.len() == vertex_count);
if let Some(skinned) = skinned_vertices {
joint_indices.reserve(vertex_count * 4);
joint_weights.reserve(vertex_count * 4);
for vertex in skinned {
positions.extend_from_slice(&vertex.position);
normals.extend_from_slice(&vertex.normal);
tex_coords_0.extend_from_slice(&vertex.tex_coords);
tex_coords_1.extend_from_slice(&vertex.tex_coords_1);
tangents.extend_from_slice(&vertex.tangent);
colors.extend_from_slice(&vertex.color);
joint_indices.extend_from_slice(&vertex.joint_indices);
joint_weights.extend_from_slice(&vertex.joint_weights);
}
} else {
for vertex in &mesh.vertices {
positions.extend_from_slice(&vertex.position);
normals.extend_from_slice(&vertex.normal);
tex_coords_0.extend_from_slice(&vertex.tex_coords);
tex_coords_1.extend_from_slice(&vertex.tex_coords_1);
tangents.extend_from_slice(&vertex.tangent);
colors.extend_from_slice(&vertex.color);
}
}
let mut attributes = Map::new();
attributes.insert(
"POSITION".to_string(),
json!(push_f32_accessor(
context,
&positions,
"VEC3",
Some(TARGET_ARRAY_BUFFER),
true,
)),
);
attributes.insert(
"NORMAL".to_string(),
json!(push_f32_accessor(
context,
&normals,
"VEC3",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
attributes.insert(
"TEXCOORD_0".to_string(),
json!(push_f32_accessor(
context,
&tex_coords_0,
"VEC2",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
if tex_coords_1.iter().any(|value| *value != 0.0) {
attributes.insert(
"TEXCOORD_1".to_string(),
json!(push_f32_accessor(
context,
&tex_coords_1,
"VEC2",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
}
attributes.insert(
"TANGENT".to_string(),
json!(push_f32_accessor(
context,
&tangents,
"VEC4",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
if colors.chunks_exact(4).any(|color| color != [1.0; 4]) {
attributes.insert(
"COLOR_0".to_string(),
json!(push_f32_accessor(
context,
&colors,
"VEC4",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
}
if !joint_indices.is_empty() {
let max_joint_index = joint_indices.iter().copied().max().unwrap_or(0);
if max_joint_index > u16::MAX as u32 {
return Err(format!(
"joint index {max_joint_index} exceeds the unsigned short range required by glTF"
));
}
let joints_u16: Vec<u16> = joint_indices
.iter()
.map(|joint_index| *joint_index as u16)
.collect();
attributes.insert(
"JOINTS_0".to_string(),
json!(push_u16_accessor(
context,
&joints_u16,
"VEC4",
Some(TARGET_ARRAY_BUFFER),
)),
);
attributes.insert(
"WEIGHTS_0".to_string(),
json!(push_f32_accessor(
context,
&joint_weights,
"VEC4",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
}
let indices_accessor = push_u32_accessor(
context,
&mesh.indices,
"SCALAR",
Some(TARGET_ELEMENT_ARRAY_BUFFER),
);
let mut primitive = Map::new();
primitive.insert("attributes".to_string(), Value::Object(attributes));
primitive.insert("indices".to_string(), json!(indices_accessor));
primitive.insert("mode".to_string(), json!(4));
if let Some(material_index) = material_index {
primitive.insert("material".to_string(), json!(material_index));
}
if let Some(morph_targets) = &mesh.morph_targets
&& !morph_targets.targets.is_empty()
{
let mut targets_json: Vec<Value> = Vec::with_capacity(morph_targets.targets.len());
for target in &morph_targets.targets {
let mut target_json = Map::new();
let position_floats: Vec<f32> = target
.position_displacements
.iter()
.flat_map(|displacement| displacement.iter().copied())
.collect();
target_json.insert(
"POSITION".to_string(),
json!(push_f32_accessor(
context,
&position_floats,
"VEC3",
Some(TARGET_ARRAY_BUFFER),
true,
)),
);
if let Some(normal_displacements) = &target.normal_displacements {
let normal_floats: Vec<f32> = normal_displacements
.iter()
.flat_map(|displacement| displacement.iter().copied())
.collect();
target_json.insert(
"NORMAL".to_string(),
json!(push_f32_accessor(
context,
&normal_floats,
"VEC3",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
}
if let Some(tangent_displacements) = &target.tangent_displacements {
let tangent_floats: Vec<f32> = tangent_displacements
.iter()
.flat_map(|displacement| displacement.iter().copied())
.collect();
target_json.insert(
"TANGENT".to_string(),
json!(push_f32_accessor(
context,
&tangent_floats,
"VEC3",
Some(TARGET_ARRAY_BUFFER),
false,
)),
);
}
targets_json.push(Value::Object(target_json));
}
primitive.insert("targets".to_string(), Value::Array(targets_json));
}
Ok(Value::Object(primitive))
}
fn resolve_material_index(
context: &mut ExportContext,
world: &World,
material_name: &str,
) -> Option<usize> {
if let Some(existing) = context.material_indices.get(material_name) {
return *existing;
}
let Some(material) = crate::ecs::generational_registry::registry_entry_by_name(
&world.resources.assets.material_registry.registry,
material_name,
) else {
context
.material_indices
.insert(material_name.to_string(), None);
return None;
};
let material_json = build_material(context, world, material, material_name);
let material_index = context.materials.len();
context.materials.push(material_json);
context
.material_indices
.insert(material_name.to_string(), Some(material_index));
Some(material_index)
}
fn build_material(
context: &mut ExportContext,
world: &World,
material: &Material,
material_name: &str,
) -> Value {
let mut material_json = Map::new();
let display_name = material_name.rsplit("::").next().unwrap_or(material_name);
material_json.insert("name".to_string(), json!(display_name));
let mut pbr = Map::new();
pbr.insert("baseColorFactor".to_string(), json!(material.base_color));
if let Some(info) = texture_info(
context,
world,
material.base_texture.as_deref(),
&material.base_texture_transform,
) {
pbr.insert("baseColorTexture".to_string(), info);
}
pbr.insert("metallicFactor".to_string(), json!(material.metallic));
pbr.insert("roughnessFactor".to_string(), json!(material.roughness));
if let Some(info) = texture_info(
context,
world,
material.metallic_roughness_texture.as_deref(),
&material.metallic_roughness_texture_transform,
) {
pbr.insert("metallicRoughnessTexture".to_string(), info);
}
material_json.insert("pbrMetallicRoughness".to_string(), Value::Object(pbr));
if let Some(mut info) = texture_info(
context,
world,
material.normal_texture.as_deref(),
&material.normal_texture_transform,
) {
if material.normal_scale != 1.0
&& let Some(object) = info.as_object_mut()
{
object.insert("scale".to_string(), json!(material.normal_scale));
}
material_json.insert("normalTexture".to_string(), info);
}
if let Some(mut info) = texture_info(
context,
world,
material.occlusion_texture.as_deref(),
&material.occlusion_texture_transform,
) {
if material.occlusion_strength != 1.0
&& let Some(object) = info.as_object_mut()
{
object.insert("strength".to_string(), json!(material.occlusion_strength));
}
material_json.insert("occlusionTexture".to_string(), info);
}
if material.emissive_factor != [0.0, 0.0, 0.0] {
material_json.insert(
"emissiveFactor".to_string(),
json!(material.emissive_factor),
);
}
if let Some(info) = texture_info(
context,
world,
material.emissive_texture.as_deref(),
&material.emissive_texture_transform,
) {
material_json.insert("emissiveTexture".to_string(), info);
}
match material.alpha_mode {
AlphaMode::Opaque => {}
AlphaMode::Mask => {
material_json.insert("alphaMode".to_string(), json!("MASK"));
material_json.insert("alphaCutoff".to_string(), json!(material.alpha_cutoff));
}
AlphaMode::Blend => {
material_json.insert("alphaMode".to_string(), json!("BLEND"));
}
}
if material.double_sided {
material_json.insert("doubleSided".to_string(), json!(true));
}
let extensions = build_material_extensions(context, world, material);
if !extensions.is_empty() {
material_json.insert("extensions".to_string(), Value::Object(extensions));
}
Value::Object(material_json)
}
fn build_material_extensions(
context: &mut ExportContext,
world: &World,
material: &Material,
) -> Map<String, Value> {
let mut extensions = Map::new();
if material.unlit {
context
.extensions_used
.insert("KHR_materials_unlit".to_string());
extensions.insert("KHR_materials_unlit".to_string(), json!({}));
}
if material.emissive_strength != 1.0 {
context
.extensions_used
.insert("KHR_materials_emissive_strength".to_string());
extensions.insert(
"KHR_materials_emissive_strength".to_string(),
json!({ "emissiveStrength": material.emissive_strength }),
);
}
if material.ior != 1.5 {
context
.extensions_used
.insert("KHR_materials_ior".to_string());
extensions.insert(
"KHR_materials_ior".to_string(),
json!({ "ior": material.ior }),
);
}
let transmission_texture = texture_info(
context,
world,
material.transmission_texture.as_deref(),
&material.transmission_texture_transform,
);
if material.transmission_factor > 0.0 || transmission_texture.is_some() {
context
.extensions_used
.insert("KHR_materials_transmission".to_string());
let mut transmission = Map::new();
transmission.insert(
"transmissionFactor".to_string(),
json!(material.transmission_factor),
);
if let Some(info) = transmission_texture {
transmission.insert("transmissionTexture".to_string(), info);
}
extensions.insert(
"KHR_materials_transmission".to_string(),
Value::Object(transmission),
);
}
let thickness_texture = texture_info(
context,
world,
material.thickness_texture.as_deref(),
&material.thickness_texture_transform,
);
if material.thickness > 0.0 || thickness_texture.is_some() {
context
.extensions_used
.insert("KHR_materials_volume".to_string());
let mut volume = Map::new();
volume.insert("thicknessFactor".to_string(), json!(material.thickness));
if let Some(info) = thickness_texture {
volume.insert("thicknessTexture".to_string(), info);
}
if material.attenuation_color != [1.0, 1.0, 1.0] {
volume.insert(
"attenuationColor".to_string(),
json!(material.attenuation_color),
);
}
if material.attenuation_distance > 0.0 && material.attenuation_distance.is_finite() {
volume.insert(
"attenuationDistance".to_string(),
json!(material.attenuation_distance),
);
}
extensions.insert("KHR_materials_volume".to_string(), Value::Object(volume));
}
let specular_texture = texture_info(
context,
world,
material.specular_texture.as_deref(),
&material.specular_texture_transform,
);
let specular_color_texture = texture_info(
context,
world,
material.specular_color_texture.as_deref(),
&material.specular_color_texture_transform,
);
if material.specular_factor != 1.0
|| material.specular_color_factor != [1.0, 1.0, 1.0]
|| specular_texture.is_some()
|| specular_color_texture.is_some()
{
context
.extensions_used
.insert("KHR_materials_specular".to_string());
let mut specular = Map::new();
if material.specular_factor != 1.0 {
specular.insert(
"specularFactor".to_string(),
json!(material.specular_factor),
);
}
if material.specular_color_factor != [1.0, 1.0, 1.0] {
specular.insert(
"specularColorFactor".to_string(),
json!(material.specular_color_factor),
);
}
if let Some(info) = specular_texture {
specular.insert("specularTexture".to_string(), info);
}
if let Some(info) = specular_color_texture {
specular.insert("specularColorTexture".to_string(), info);
}
extensions.insert(
"KHR_materials_specular".to_string(),
Value::Object(specular),
);
}
let diffuse_transmission_color_texture = texture_info(
context,
world,
material.diffuse_transmission_color_texture.as_deref(),
&material.diffuse_transmission_color_texture_transform,
);
let diffuse_transmission_texture = texture_info(
context,
world,
material
.diffuse_transmission_texture
.as_deref()
.or(material.diffuse_transmission_color_texture.as_deref()),
&material.diffuse_transmission_texture_transform,
);
if material.diffuse_transmission_factor > 0.0 {
context
.extensions_used
.insert("KHR_materials_diffuse_transmission".to_string());
let mut diffuse_transmission = Map::new();
diffuse_transmission.insert(
"diffuseTransmissionFactor".to_string(),
json!(material.diffuse_transmission_factor),
);
if let Some(info) = diffuse_transmission_texture {
diffuse_transmission.insert("diffuseTransmissionTexture".to_string(), info);
}
if material.diffuse_transmission_color_factor != [1.0, 1.0, 1.0] {
diffuse_transmission.insert(
"diffuseTransmissionColorFactor".to_string(),
json!(material.diffuse_transmission_color_factor),
);
}
if let Some(info) = diffuse_transmission_color_texture {
diffuse_transmission.insert("diffuseTransmissionColorTexture".to_string(), info);
}
extensions.insert(
"KHR_materials_diffuse_transmission".to_string(),
Value::Object(diffuse_transmission),
);
}
if material.dispersion > 0.0 {
context
.extensions_used
.insert("KHR_materials_dispersion".to_string());
extensions.insert(
"KHR_materials_dispersion".to_string(),
json!({ "dispersion": material.dispersion }),
);
}
let anisotropy_texture = texture_info(
context,
world,
material.anisotropy_texture.as_deref(),
&material.anisotropy_texture_transform,
);
if material.anisotropy_strength > 0.0 || anisotropy_texture.is_some() {
context
.extensions_used
.insert("KHR_materials_anisotropy".to_string());
let mut anisotropy = Map::new();
anisotropy.insert(
"anisotropyStrength".to_string(),
json!(material.anisotropy_strength),
);
if material.anisotropy_rotation != 0.0 {
anisotropy.insert(
"anisotropyRotation".to_string(),
json!(material.anisotropy_rotation),
);
}
if let Some(info) = anisotropy_texture {
anisotropy.insert("anisotropyTexture".to_string(), info);
}
extensions.insert(
"KHR_materials_anisotropy".to_string(),
Value::Object(anisotropy),
);
}
let iridescence_texture = texture_info(
context,
world,
material.iridescence_texture.as_deref(),
&material.iridescence_texture_transform,
);
let iridescence_thickness_texture = texture_info(
context,
world,
material
.iridescence_thickness_texture
.as_deref()
.or(material.iridescence_texture.as_deref()),
&material.iridescence_thickness_texture_transform,
);
if material.iridescence_factor > 0.0 || iridescence_texture.is_some() {
context
.extensions_used
.insert("KHR_materials_iridescence".to_string());
let mut iridescence = Map::new();
iridescence.insert(
"iridescenceFactor".to_string(),
json!(material.iridescence_factor),
);
if material.iridescence_ior != 1.3 {
iridescence.insert(
"iridescenceIor".to_string(),
json!(material.iridescence_ior),
);
}
if material.iridescence_thickness_minimum != 100.0 {
iridescence.insert(
"iridescenceThicknessMinimum".to_string(),
json!(material.iridescence_thickness_minimum),
);
}
if material.iridescence_thickness_maximum != 400.0 {
iridescence.insert(
"iridescenceThicknessMaximum".to_string(),
json!(material.iridescence_thickness_maximum),
);
}
if let Some(info) = iridescence_texture {
iridescence.insert("iridescenceTexture".to_string(), info);
}
if let Some(info) = iridescence_thickness_texture {
iridescence.insert("iridescenceThicknessTexture".to_string(), info);
}
extensions.insert(
"KHR_materials_iridescence".to_string(),
Value::Object(iridescence),
);
}
let sheen_color_texture = texture_info(
context,
world,
material.sheen_color_texture.as_deref(),
&material.sheen_color_texture_transform,
);
let sheen_roughness_texture = texture_info(
context,
world,
material
.sheen_roughness_texture
.as_deref()
.or(material.sheen_color_texture.as_deref()),
&material.sheen_roughness_texture_transform,
);
if material.sheen_color_factor != [0.0, 0.0, 0.0] || sheen_color_texture.is_some() {
context
.extensions_used
.insert("KHR_materials_sheen".to_string());
let mut sheen = Map::new();
sheen.insert(
"sheenColorFactor".to_string(),
json!(material.sheen_color_factor),
);
sheen.insert(
"sheenRoughnessFactor".to_string(),
json!(material.sheen_roughness_factor),
);
if let Some(info) = sheen_color_texture {
sheen.insert("sheenColorTexture".to_string(), info);
}
if let Some(info) = sheen_roughness_texture {
sheen.insert("sheenRoughnessTexture".to_string(), info);
}
extensions.insert("KHR_materials_sheen".to_string(), Value::Object(sheen));
}
let clearcoat_texture = texture_info(
context,
world,
material.clearcoat_texture.as_deref(),
&material.clearcoat_texture_transform,
);
let clearcoat_roughness_texture = texture_info(
context,
world,
material.clearcoat_roughness_texture.as_deref(),
&material.clearcoat_roughness_texture_transform,
);
let clearcoat_normal_texture = texture_info(
context,
world,
material.clearcoat_normal_texture.as_deref(),
&material.clearcoat_normal_texture_transform,
);
if material.clearcoat_factor > 0.0 || clearcoat_texture.is_some() {
context
.extensions_used
.insert("KHR_materials_clearcoat".to_string());
let mut clearcoat = Map::new();
clearcoat.insert(
"clearcoatFactor".to_string(),
json!(material.clearcoat_factor),
);
clearcoat.insert(
"clearcoatRoughnessFactor".to_string(),
json!(material.clearcoat_roughness_factor),
);
if let Some(info) = clearcoat_texture {
clearcoat.insert("clearcoatTexture".to_string(), info);
}
if let Some(info) = clearcoat_roughness_texture {
clearcoat.insert("clearcoatRoughnessTexture".to_string(), info);
}
if let Some(mut info) = clearcoat_normal_texture {
if material.clearcoat_normal_scale != 1.0
&& let Some(object) = info.as_object_mut()
{
object.insert("scale".to_string(), json!(material.clearcoat_normal_scale));
}
clearcoat.insert("clearcoatNormalTexture".to_string(), info);
}
extensions.insert(
"KHR_materials_clearcoat".to_string(),
Value::Object(clearcoat),
);
}
extensions
}
fn texture_info(
context: &mut ExportContext,
world: &World,
texture_name: Option<&str>,
transform: &TextureTransform,
) -> Option<Value> {
let texture_name = texture_name?;
let texture_index = resolve_texture_index(context, world, texture_name)?;
let mut info = Map::new();
info.insert("index".to_string(), json!(texture_index));
if transform.uv_set != 0 {
info.insert("texCoord".to_string(), json!(transform.uv_set));
}
if *transform != TextureTransform::IDENTITY {
context
.extensions_used
.insert("KHR_texture_transform".to_string());
let mut transform_json = Map::new();
if transform.offset != [0.0, 0.0] {
transform_json.insert("offset".to_string(), json!(transform.offset));
}
if transform.rotation != 0.0 {
transform_json.insert("rotation".to_string(), json!(transform.rotation));
}
if transform.scale != [1.0, 1.0] {
transform_json.insert("scale".to_string(), json!(transform.scale));
}
if transform.uv_set != 0 {
transform_json.insert("texCoord".to_string(), json!(transform.uv_set));
}
info.insert(
"extensions".to_string(),
json!({ "KHR_texture_transform": Value::Object(transform_json) }),
);
}
Some(Value::Object(info))
}
fn resolve_texture_index(
context: &mut ExportContext,
world: &World,
texture_name: &str,
) -> Option<usize> {
if let Some(existing) = context.texture_indices.get(texture_name) {
return *existing;
}
let Some(source) = world.resources.assets.texture_sources.get(texture_name) else {
tracing::warn!("glb export: no source bytes for texture '{texture_name}', dropping it");
context
.texture_indices
.insert(texture_name.to_string(), None);
return None;
};
let png_bytes = match &source.data {
crate::ecs::asset_state::TextureSourceData::Png(bytes) => bytes.clone(),
crate::ecs::asset_state::TextureSourceData::Rgba {
rgba,
width,
height,
} => match encode_rgba_to_png(rgba, *width, *height) {
Ok(bytes) => bytes,
Err(error) => {
tracing::warn!("glb export: failed to encode texture '{texture_name}': {error}");
context
.texture_indices
.insert(texture_name.to_string(), None);
return None;
}
},
};
let view_index = push_buffer_view(context, &png_bytes, None);
let image_index = context.images.len();
let display_name = texture_name.rsplit("::").next().unwrap_or(texture_name);
context.images.push(json!({
"bufferView": view_index,
"mimeType": "image/png",
"name": display_name,
}));
let sampler_index = resolve_sampler_index(context, &source.sampler);
let texture_index = context.textures.len();
context.textures.push(json!({
"source": image_index,
"sampler": sampler_index,
"name": display_name,
}));
context
.texture_indices
.insert(texture_name.to_string(), Some(texture_index));
Some(texture_index)
}
fn encode_rgba_to_png(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
let image_buffer = image::RgbaImage::from_raw(width, height, rgba.to_vec())
.ok_or_else(|| "pixel data does not match dimensions".to_string())?;
let mut png_bytes: Vec<u8> = Vec::new();
image::DynamicImage::ImageRgba8(image_buffer)
.write_to(
&mut std::io::Cursor::new(&mut png_bytes),
image::ImageFormat::Png,
)
.map_err(|error| error.to_string())?;
Ok(png_bytes)
}
fn resolve_sampler_index(context: &mut ExportContext, sampler: &SamplerSettings) -> usize {
let signature = sampler.signature();
if let Some(existing) = context.sampler_indices.get(&signature) {
return *existing;
}
let wrap_mode = |wrap: SamplerWrap| match wrap {
SamplerWrap::Repeat => 10497,
SamplerWrap::MirroredRepeat => 33648,
SamplerWrap::ClampToEdge => 33071,
};
let mag_filter = match sampler.mag_filter {
SamplerFilter::Nearest => 9728,
SamplerFilter::Linear => 9729,
};
let min_filter = match (sampler.min_filter, sampler.mipmap_filter) {
(SamplerFilter::Nearest, SamplerFilter::Nearest) => 9984,
(SamplerFilter::Linear, SamplerFilter::Nearest) => 9985,
(SamplerFilter::Nearest, SamplerFilter::Linear) => 9986,
(SamplerFilter::Linear, SamplerFilter::Linear) => 9987,
};
let sampler_index = context.samplers.len();
context.samplers.push(json!({
"magFilter": mag_filter,
"minFilter": min_filter,
"wrapS": wrap_mode(sampler.wrap_u),
"wrapT": wrap_mode(sampler.wrap_v),
}));
context.sampler_indices.insert(signature, sampler_index);
sampler_index
}
fn build_camera(camera: &Camera, world: &World, entity: Entity) -> Value {
let name = world
.core
.get_name(entity)
.map(|name| name.0.clone())
.filter(|name| !name.is_empty());
let mut camera_json = Map::new();
if let Some(name) = name {
camera_json.insert("name".to_string(), json!(name));
}
match &camera.projection {
Projection::Perspective(perspective) => {
camera_json.insert("type".to_string(), json!("perspective"));
let mut perspective_json = Map::new();
if let Some(aspect_ratio) = perspective.aspect_ratio {
perspective_json.insert("aspectRatio".to_string(), json!(aspect_ratio));
}
perspective_json.insert("yfov".to_string(), json!(perspective.y_fov_rad));
perspective_json.insert("znear".to_string(), json!(perspective.z_near.max(1.0e-4)));
if let Some(z_far) = perspective.z_far {
perspective_json.insert("zfar".to_string(), json!(z_far));
}
camera_json.insert("perspective".to_string(), Value::Object(perspective_json));
}
Projection::Orthographic(orthographic) => {
camera_json.insert("type".to_string(), json!("orthographic"));
camera_json.insert(
"orthographic".to_string(),
json!({
"xmag": orthographic.x_mag,
"ymag": orthographic.y_mag,
"zfar": orthographic.z_far,
"znear": orthographic.z_near,
}),
);
}
}
Value::Object(camera_json)
}
fn build_light(light: &Light) -> Value {
let mut light_json = Map::new();
let light_type = match light.light_type {
LightType::Directional => "directional",
LightType::Point => "point",
LightType::Spot => "spot",
LightType::Area => unreachable!(),
};
light_json.insert("type".to_string(), json!(light_type));
light_json.insert(
"color".to_string(),
json!([light.color.x, light.color.y, light.color.z]),
);
light_json.insert("intensity".to_string(), json!(light.intensity));
if !matches!(light.light_type, LightType::Directional) && light.range > 0.0 {
light_json.insert("range".to_string(), json!(light.range));
}
if matches!(light.light_type, LightType::Spot) {
light_json.insert(
"spot".to_string(),
json!({
"innerConeAngle": light.inner_cone_angle,
"outerConeAngle": light.outer_cone_angle,
}),
);
}
Value::Object(light_json)
}
fn build_animations(
context: &mut ExportContext,
world: &World,
entities: &[Entity],
entity_to_node: &HashMap<Entity, usize>,
) -> Vec<Value> {
let mut animations: Vec<Value> = Vec::new();
for entity in entities {
let Some(player) = world.core.get_animation_player(*entity) else {
continue;
};
for clip in &player.clips {
let mut channels: Vec<Value> = Vec::new();
let mut samplers: Vec<Value> = Vec::new();
for channel in &clip.channels {
let Some(target_entity) = player.resolve_target_entity(channel) else {
continue;
};
let Some(node_index) = entity_to_node.get(&target_entity).copied() else {
continue;
};
let Some(sampler_json) = build_animation_sampler(context, channel) else {
continue;
};
let sampler_index = samplers.len();
samplers.push(sampler_json);
let path = match channel.target_property {
AnimationProperty::Translation => "translation",
AnimationProperty::Rotation => "rotation",
AnimationProperty::Scale => "scale",
AnimationProperty::MorphWeights => "weights",
};
channels.push(json!({
"sampler": sampler_index,
"target": { "node": node_index, "path": path },
}));
}
if channels.is_empty() {
continue;
}
animations.push(json!({
"name": clip.name,
"channels": channels,
"samplers": samplers,
}));
}
}
animations
}
fn build_animation_sampler(
context: &mut ExportContext,
channel: &AnimationChannel,
) -> Option<Value> {
let sampler = &channel.sampler;
if sampler.input.is_empty() {
return None;
}
let input_accessor = push_f32_accessor(context, &sampler.input, "SCALAR", None, true);
let (output_floats, element_type) = match &sampler.output {
AnimationSamplerOutput::Vec3(values) => {
let floats: Vec<f32> = values
.iter()
.flat_map(|value| [value.x, value.y, value.z])
.collect();
(floats, "VEC3")
}
AnimationSamplerOutput::Quat(values) => {
let floats: Vec<f32> = values
.iter()
.flat_map(|value| [value.i, value.j, value.k, value.w])
.collect();
(floats, "VEC4")
}
AnimationSamplerOutput::Weights(values) => {
let floats: Vec<f32> = values.iter().flatten().copied().collect();
(floats, "SCALAR")
}
AnimationSamplerOutput::CubicSplineVec3 {
values,
in_tangents,
out_tangents,
} => {
let mut floats: Vec<f32> = Vec::with_capacity(values.len() * 9);
for (index, value) in values.iter().enumerate() {
let in_tangent = in_tangents.get(index)?;
let out_tangent = out_tangents.get(index)?;
floats.extend_from_slice(&[in_tangent.x, in_tangent.y, in_tangent.z]);
floats.extend_from_slice(&[value.x, value.y, value.z]);
floats.extend_from_slice(&[out_tangent.x, out_tangent.y, out_tangent.z]);
}
(floats, "VEC3")
}
AnimationSamplerOutput::CubicSplineQuat {
values,
in_tangents,
out_tangents,
} => {
let mut floats: Vec<f32> = Vec::with_capacity(values.len() * 12);
for (index, value) in values.iter().enumerate() {
let in_tangent = in_tangents.get(index)?;
let out_tangent = out_tangents.get(index)?;
floats.extend_from_slice(&[in_tangent.i, in_tangent.j, in_tangent.k, in_tangent.w]);
floats.extend_from_slice(&[value.i, value.j, value.k, value.w]);
floats.extend_from_slice(&[
out_tangent.i,
out_tangent.j,
out_tangent.k,
out_tangent.w,
]);
}
(floats, "VEC4")
}
AnimationSamplerOutput::CubicSplineWeights {
values,
in_tangents,
out_tangents,
} => {
let mut floats: Vec<f32> = Vec::new();
for (index, value) in values.iter().enumerate() {
floats.extend(in_tangents.get(index)?.iter().copied());
floats.extend(value.iter().copied());
floats.extend(out_tangents.get(index)?.iter().copied());
}
(floats, "SCALAR")
}
};
if output_floats.is_empty() {
return None;
}
let output_accessor = push_f32_accessor(context, &output_floats, element_type, None, false);
let interpolation = match sampler.interpolation {
AnimationInterpolation::Linear => "LINEAR",
AnimationInterpolation::Step => "STEP",
AnimationInterpolation::CubicSpline => "CUBICSPLINE",
};
Some(json!({
"input": input_accessor,
"output": output_accessor,
"interpolation": interpolation,
}))
}
fn assemble_root(
context: ExportContext,
nodes: Vec<Map<String, Value>>,
root_nodes: Vec<usize>,
animations: Vec<Value>,
) -> (Map<String, Value>, Vec<u8>) {
let mut root = Map::new();
root.insert(
"asset".to_string(),
json!({ "version": "2.0", "generator": "nightshade" }),
);
if !root_nodes.is_empty() {
let mut scene = Map::new();
scene.insert(
"nodes".to_string(),
Value::Array(root_nodes.iter().map(|index| json!(index)).collect()),
);
root.insert("scene".to_string(), json!(0));
root.insert(
"scenes".to_string(),
Value::Array(vec![Value::Object(scene)]),
);
}
if !nodes.is_empty() {
root.insert(
"nodes".to_string(),
Value::Array(nodes.into_iter().map(Value::Object).collect()),
);
}
if !context.meshes.is_empty() {
root.insert("meshes".to_string(), Value::Array(context.meshes));
}
if !context.materials.is_empty() {
root.insert("materials".to_string(), Value::Array(context.materials));
}
if !context.textures.is_empty() {
root.insert("textures".to_string(), Value::Array(context.textures));
}
if !context.images.is_empty() {
root.insert("images".to_string(), Value::Array(context.images));
}
if !context.samplers.is_empty() {
root.insert("samplers".to_string(), Value::Array(context.samplers));
}
if !context.skins.is_empty() {
root.insert("skins".to_string(), Value::Array(context.skins));
}
if !context.cameras.is_empty() {
root.insert("cameras".to_string(), Value::Array(context.cameras));
}
if !animations.is_empty() {
root.insert("animations".to_string(), Value::Array(animations));
}
if !context.lights.is_empty() {
root.insert(
"extensions".to_string(),
json!({ "KHR_lights_punctual": { "lights": context.lights } }),
);
}
if !context.accessors.is_empty() {
root.insert("accessors".to_string(), Value::Array(context.accessors));
}
if !context.buffer_views.is_empty() {
root.insert(
"bufferViews".to_string(),
Value::Array(context.buffer_views),
);
}
if !context.buffer.is_empty() {
root.insert(
"buffers".to_string(),
json!([{ "byteLength": context.buffer.len() }]),
);
}
if !context.extensions_used.is_empty() {
root.insert(
"extensionsUsed".to_string(),
Value::Array(
context
.extensions_used
.iter()
.map(|name| json!(name))
.collect(),
),
);
}
(root, context.buffer)
}
fn encode_glb(root: Map<String, Value>, mut binary: Vec<u8>) -> Result<Vec<u8>, String> {
let json_string =
serde_json::to_string(&Value::Object(root)).map_err(|error| error.to_string())?;
let mut json_bytes = json_string.into_bytes();
while !json_bytes.len().is_multiple_of(4) {
json_bytes.push(b' ');
}
while !binary.len().is_multiple_of(4) {
binary.push(0);
}
let mut total_length = 12 + 8 + json_bytes.len();
if !binary.is_empty() {
total_length += 8 + binary.len();
}
let mut output = Vec::with_capacity(total_length);
output.extend_from_slice(&GLB_MAGIC.to_le_bytes());
output.extend_from_slice(&GLB_VERSION.to_le_bytes());
output.extend_from_slice(&(total_length as u32).to_le_bytes());
output.extend_from_slice(&(json_bytes.len() as u32).to_le_bytes());
output.extend_from_slice(&GLB_CHUNK_JSON.to_le_bytes());
output.extend_from_slice(&json_bytes);
if !binary.is_empty() {
output.extend_from_slice(&(binary.len() as u32).to_le_bytes());
output.extend_from_slice(&GLB_CHUNK_BIN.to_le_bytes());
output.extend_from_slice(&binary);
}
Ok(output)
}
fn push_buffer_view(context: &mut ExportContext, bytes: &[u8], target: Option<u32>) -> usize {
while !context.buffer.len().is_multiple_of(4) {
context.buffer.push(0);
}
let byte_offset = context.buffer.len();
context.buffer.extend_from_slice(bytes);
let mut view = Map::new();
view.insert("buffer".to_string(), json!(0));
view.insert("byteOffset".to_string(), json!(byte_offset));
view.insert("byteLength".to_string(), json!(bytes.len()));
if let Some(target) = target {
view.insert("target".to_string(), json!(target));
}
let view_index = context.buffer_views.len();
context.buffer_views.push(Value::Object(view));
view_index
}
fn element_component_count(element_type: &str) -> usize {
match element_type {
"SCALAR" => 1,
"VEC2" => 2,
"VEC3" => 3,
"VEC4" => 4,
"MAT4" => 16,
_ => 1,
}
}
fn push_f32_accessor(
context: &mut ExportContext,
values: &[f32],
element_type: &str,
target: Option<u32>,
with_min_max: bool,
) -> usize {
let components = element_component_count(element_type);
let count = values.len() / components;
let bytes: Vec<u8> = values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect();
let view_index = push_buffer_view(context, &bytes, target);
let mut accessor = Map::new();
accessor.insert("bufferView".to_string(), json!(view_index));
accessor.insert("componentType".to_string(), json!(COMPONENT_F32));
accessor.insert("count".to_string(), json!(count));
accessor.insert("type".to_string(), json!(element_type));
if with_min_max && count > 0 {
let (minimums, maximums) = component_min_max(values, components);
accessor.insert("min".to_string(), json!(minimums));
accessor.insert("max".to_string(), json!(maximums));
}
let accessor_index = context.accessors.len();
context.accessors.push(Value::Object(accessor));
accessor_index
}
fn push_u32_accessor(
context: &mut ExportContext,
values: &[u32],
element_type: &str,
target: Option<u32>,
) -> usize {
let components = element_component_count(element_type);
let count = values.len() / components;
let bytes: Vec<u8> = values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect();
let view_index = push_buffer_view(context, &bytes, target);
let accessor_index = context.accessors.len();
context.accessors.push(json!({
"bufferView": view_index,
"componentType": COMPONENT_U32,
"count": count,
"type": element_type,
}));
accessor_index
}
fn push_u16_accessor(
context: &mut ExportContext,
values: &[u16],
element_type: &str,
target: Option<u32>,
) -> usize {
let components = element_component_count(element_type);
let count = values.len() / components;
let bytes: Vec<u8> = values
.iter()
.flat_map(|value| value.to_le_bytes())
.collect();
let view_index = push_buffer_view(context, &bytes, target);
let accessor_index = context.accessors.len();
context.accessors.push(json!({
"bufferView": view_index,
"componentType": COMPONENT_U16,
"count": count,
"type": element_type,
}));
accessor_index
}
fn component_min_max(values: &[f32], components: usize) -> (Vec<f32>, Vec<f32>) {
let mut minimums = vec![f32::MAX; components];
let mut maximums = vec![f32::MIN; components];
for element in values.chunks_exact(components) {
for (component_index, value) in element.iter().enumerate() {
minimums[component_index] = minimums[component_index].min(*value);
maximums[component_index] = maximums[component_index].max(*value);
}
}
(minimums, maximums)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::asset_state::{TextureSourceBytes, TextureSourceData};
use crate::ecs::generational_registry::registry_entry_by_name_mut;
use crate::ecs::world::commands::spawn_mesh_at;
use nalgebra_glm::Vec3;
#[test]
fn export_produces_a_valid_glb_with_mesh_material_and_texture() {
let mut world = World::default();
let entity = spawn_mesh_at(
&mut world,
"Cube",
Vec3::new(1.0, 2.0, 3.0),
Vec3::new(1.0, 1.0, 1.0),
);
let material_name = world
.core
.get_material_ref(entity)
.map(|material_ref| material_ref.name.clone())
.expect("spawned mesh has a material reference");
let material = registry_entry_by_name_mut(
&mut world.resources.assets.material_registry.registry,
&material_name,
)
.expect("spawned material is registered");
material.base_texture = Some("test_base".to_string());
world.resources.assets.texture_sources.insert(
"test_base".to_string(),
TextureSourceBytes {
data: TextureSourceData::Rgba {
rgba: vec![255, 128, 64, 255],
width: 1,
height: 1,
},
usage: crate::render::wgpu::texture_cache::TextureUsage::Color,
sampler: SamplerSettings::DEFAULT,
},
);
let bytes = export_world_to_glb_bytes(&world, &HashSet::new()).expect("export succeeds");
let document = gltf::Gltf::from_slice(&bytes).expect("exported GLB parses and validates");
assert_eq!(document.meshes().count(), 1);
assert_eq!(
document.materials().filter(|m| m.index().is_some()).count(),
1
);
assert_eq!(document.images().count(), 1);
assert_eq!(document.textures().count(), 1);
assert_eq!(document.samplers().count(), 1);
let node = document
.nodes()
.find(|node| node.mesh().is_some())
.expect("a node references the mesh");
let (translation, _, _) = node.transform().decomposed();
assert_eq!(translation, [1.0, 2.0, 3.0]);
let primitive = document
.meshes()
.next()
.expect("mesh present")
.primitives()
.next()
.expect("primitive present");
assert!(primitive.material().index().is_some());
assert!(
primitive
.attributes()
.any(|(semantic, _)| semantic == gltf::Semantic::Positions)
);
assert!(primitive.indices().is_some());
}
#[test]
fn excluded_entities_and_descendants_are_skipped() {
let mut world = World::default();
let parent = spawn_mesh_at(&mut world, "Cube", Vec3::zeros(), Vec3::new(1.0, 1.0, 1.0));
let child = spawn_mesh_at(
&mut world,
"Cube",
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
);
world.core.add_components(child, crate::ecs::world::PARENT);
world.core.set_parent(
child,
crate::ecs::transform::components::Parent(Some(parent)),
);
let mut excluded = HashSet::new();
excluded.insert(parent);
let bytes = export_world_to_glb_bytes(&world, &excluded).expect("export succeeds");
let document = gltf::Gltf::from_slice(&bytes).expect("exported GLB parses and validates");
assert_eq!(document.nodes().count(), 0);
}
}