#![allow(clippy::needless_update)]
mod builder;
mod primitives;
mod terrain;
mod texture_cache;
use builder::GltfBuilder;
use primitives::build_unrolled_primitives;
pub(crate) use primitives::{MaterialKey, UnrolledPrimitive};
pub(crate) use terrain::build_wld_unrolled_primitives;
pub use texture_cache::TextureCache;
use crate::{
Result,
import::{palette::Palette, rgm::PositionedLight},
model3d::Model3DFile,
};
use gltf::binary::{Glb, Header};
use gltf_json as json;
use gltf_json::extensions::scene::khr_lights_punctual;
use json::scene::Node;
use json::validation::Checked;
use json::{Index, Root};
use log::trace;
use rayon::prelude::*;
use std::collections::HashMap;
pub(crate) const ENGINE_UNIT_SCALE: f32 = 20.0;
const IDENTITY_MATRIX: [f32; 16] = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
fn non_identity_matrix(matrix: [f32; 16]) -> Option<[f32; 16]> {
if matrix == IDENTITY_MATRIX {
None
} else {
Some(matrix)
}
}
pub(crate) const UV_FIXED_POINT_SCALE: f32 = 16.0;
#[allow(clippy::cast_possible_truncation)]
const fn index_u32(value: usize) -> u32 {
value as u32
}
#[allow(clippy::missing_errors_doc)]
pub fn convert_models_to_gltf(
models: &[Model3DFile],
palette: Option<&Palette>,
texture_cache: Option<&mut TextureCache>,
compress_textures: bool,
) -> Result<(Root, Vec<u8>)> {
if models.is_empty() {
return Err(crate::error::Error::Conversion(
"No models to convert".to_string(),
));
}
trace!("Converting {} models to GLTF", models.len());
let mut builder = GltfBuilder::new(texture_cache, compress_textures);
let texture_cache_available = builder.has_texture_cache();
let all_unrolled: Vec<_> = models
.par_iter()
.map(|model| build_unrolled_primitives(model, palette, texture_cache_available))
.collect();
for unrolled in all_unrolled {
if unrolled.is_empty() {
continue;
}
let mesh_index = builder.append_mesh(unrolled);
builder.add_node(Node {
mesh: Some(Index::new(index_u32(mesh_index))),
..Default::default()
});
}
if builder.nodes.is_empty() {
return Err(crate::error::Error::Conversion(
"No valid models found to convert".to_string(),
));
}
Ok(builder.finish())
}
#[allow(clippy::missing_errors_doc)]
pub fn to_glb(root: &Root, buffer: &[u8]) -> Result<Vec<u8>> {
let json_string = serde_json::to_string(root)?;
let glb = Glb {
header: Header {
magic: *b"glTF",
version: 2,
length: 0,
},
json: json_string.as_bytes().into(),
bin: Some(buffer.into()),
};
Ok(glb.to_vec()?)
}
fn add_positioned_models(
builder: &mut GltfBuilder,
positioned_models: &[crate::import::rgm::PositionedModel],
palette: Option<&Palette>,
texture_cache_available: bool,
) {
let mut unique_source_models: HashMap<&str, &crate::model3d::Model3DFile> = HashMap::new();
for pm in positioned_models {
if let Some(source_id) = &pm.source_id {
unique_source_models.entry(source_id).or_insert(&pm.model);
}
}
let unrolled_cache: HashMap<&str, Vec<UnrolledPrimitive>> = unique_source_models
.par_iter()
.map(|(&id, model)| {
(
id,
build_unrolled_primitives(model, palette, texture_cache_available),
)
})
.collect();
let mut mesh_instance_cache: HashMap<&str, u32> = HashMap::new();
for pm in positioned_models {
if let Some(source_id) = &pm.source_id
&& let Some(&cached_mesh) = mesh_instance_cache.get(source_id.as_str())
{
builder.add_node(Node {
mesh: Some(Index::new(cached_mesh)),
matrix: non_identity_matrix(pm.transform),
name: Some(pm.model_name.clone()),
..Default::default()
});
continue;
}
let unrolled = pm.source_id.as_ref().map_or_else(
|| build_unrolled_primitives(&pm.model, palette, texture_cache_available),
|source_id| {
unrolled_cache
.get(source_id.as_str())
.cloned()
.unwrap_or_default()
},
);
if unrolled.is_empty() {
builder.add_node(Node {
matrix: non_identity_matrix(pm.transform),
name: Some(pm.model_name.clone()),
..Default::default()
});
continue;
}
let mesh_index = builder.append_mesh(unrolled);
if let Some(source_id) = &pm.source_id {
mesh_instance_cache.insert(source_id, index_u32(mesh_index));
}
builder.add_node(Node {
mesh: Some(Index::new(index_u32(mesh_index))),
matrix: non_identity_matrix(pm.transform),
name: Some(pm.model_name.clone()),
..Default::default()
});
}
}
#[allow(clippy::missing_errors_doc)]
pub fn convert_positioned_models_to_gltf(
positioned_models: &[crate::import::rgm::PositionedModel],
lights: &[PositionedLight],
palette: Option<&Palette>,
texture_cache: Option<&mut TextureCache>,
compress_textures: bool,
) -> Result<(Root, Vec<u8>)> {
if positioned_models.is_empty() && lights.is_empty() {
return Err(crate::error::Error::Conversion(
"No positioned models or lights to convert".to_string(),
));
}
trace!(
"Converting {} positioned models to GLTF",
positioned_models.len()
);
let mut builder = GltfBuilder::new(texture_cache, compress_textures);
let texture_cache_available = builder.has_texture_cache();
add_positioned_models(
&mut builder,
positioned_models,
palette,
texture_cache_available,
);
let mut light_definitions = Vec::new();
for (light_index, light) in lights.iter().enumerate() {
light_definitions.push(khr_lights_punctual::Light {
color: light.color,
intensity: 1.0,
range: Some(light.range),
type_: Checked::Valid(khr_lights_punctual::Type::Point),
name: Some(light.name.clone()),
spot: None,
extensions: None,
extras: Default::default(),
});
builder.add_node(Node {
translation: Some(light.position),
name: Some(light.name.clone()),
extensions: Some(json::extensions::scene::Node {
khr_lights_punctual: Some(khr_lights_punctual::KhrLightsPunctual {
light: Index::new(index_u32(light_index)),
}),
..Default::default()
}),
..Default::default()
});
}
if builder.nodes.is_empty() {
return Err(crate::error::Error::Conversion(
"No valid positioned models found to convert".to_string(),
));
}
Ok(builder.finish_with_lights(light_definitions))
}
#[allow(clippy::missing_errors_doc)]
pub fn convert_wld_scene_to_gltf(
wld_file: &crate::import::wld::WldFile,
texbsi_id: u16,
positioned_models: &[crate::import::rgm::PositionedModel],
palette: Option<&Palette>,
texture_cache: Option<&mut TextureCache>,
compress_textures: bool,
) -> Result<(Root, Vec<u8>)> {
let mut builder = GltfBuilder::new(texture_cache, compress_textures);
let texture_cache_available = builder.has_texture_cache();
let terrain_primitives = build_wld_unrolled_primitives(wld_file, texbsi_id)?;
if !terrain_primitives.is_empty() {
let mesh_index = builder.append_mesh(terrain_primitives);
builder.add_node(Node {
mesh: Some(Index::new(index_u32(mesh_index))),
name: Some("Terrain".to_string()),
..Default::default()
});
}
add_positioned_models(
&mut builder,
positioned_models,
palette,
texture_cache_available,
);
if builder.nodes.is_empty() {
return Err(crate::error::Error::Conversion(
"No terrain or positioned models found to convert".to_string(),
));
}
Ok(builder.finish())
}