diff options
Diffstat (limited to 'crates/render-core/src')
| -rw-r--r-- | crates/render-core/src/lib.rs | 84 | ||||
| -rw-r--r-- | crates/render-core/src/tests.rs | 101 |
2 files changed, 185 insertions, 0 deletions
diff --git a/crates/render-core/src/lib.rs b/crates/render-core/src/lib.rs new file mode 100644 index 0000000..8e0b5e8 --- /dev/null +++ b/crates/render-core/src/lib.rs @@ -0,0 +1,84 @@ +use msh_core::Model; + +#[derive(Clone, Debug)] +pub struct RenderMesh { + pub vertices: Vec<[f32; 3]>, + pub batch_count: usize, +} + +impl RenderMesh { + pub fn triangle_count(&self) -> usize { + self.vertices.len() / 3 + } +} + +/// Builds an expanded triangle list for a specific LOD/group pair. +/// +/// The output is suitable for simple `glDrawArrays(GL_TRIANGLES, ...)` paths. +pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh { + let mut vertices = Vec::new(); + let mut batch_count = 0usize; + + for node_index in 0..model.node_count { + let Some(slot_idx) = model.slot_index(node_index, lod, group) else { + continue; + }; + let Some(slot) = model.slots.get(slot_idx) else { + continue; + }; + let batch_start = usize::from(slot.batch_start); + let batch_end = batch_start.saturating_add(usize::from(slot.batch_count)); + if batch_end > model.batches.len() { + continue; + } + + for batch in &model.batches[batch_start..batch_end] { + let index_start = usize::try_from(batch.index_start).unwrap_or(usize::MAX); + let index_count = usize::from(batch.index_count); + let index_end = index_start.saturating_add(index_count); + if index_end > model.indices.len() || index_count < 3 { + continue; + } + + for &idx in &model.indices[index_start..index_end] { + let final_idx_u64 = u64::from(batch.base_vertex).saturating_add(u64::from(idx)); + let Ok(final_idx) = usize::try_from(final_idx_u64) else { + continue; + }; + let Some(pos) = model.positions.get(final_idx) else { + continue; + }; + vertices.push(*pos); + } + batch_count += 1; + } + } + + RenderMesh { + vertices, + batch_count, + } +} + +pub fn compute_bounds(vertices: &[[f32; 3]]) -> Option<([f32; 3], [f32; 3])> { + let mut iter = vertices.iter(); + let first = iter.next()?; + let mut min_v = *first; + let mut max_v = *first; + + for v in iter { + for i in 0..3 { + if v[i] < min_v[i] { + min_v[i] = v[i]; + } + if v[i] > max_v[i] { + max_v[i] = v[i]; + } + } + } + + Some((min_v, max_v)) +} + +#[cfg(test)] +mod tests; diff --git a/crates/render-core/src/tests.rs b/crates/render-core/src/tests.rs new file mode 100644 index 0000000..9c5eb5d --- /dev/null +++ b/crates/render-core/src/tests.rs @@ -0,0 +1,101 @@ +use super::*; +use msh_core::parse_model_payload; +use nres::Archive; +use std::fs; +use std::path::{Path, PathBuf}; + +fn collect_files_recursive(root: &Path, out: &mut Vec<PathBuf>) { + let Ok(entries) = fs::read_dir(root) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_files_recursive(&path, out); + } else if path.is_file() { + out.push(path); + } + } +} + +fn nres_test_files() -> Vec<PathBuf> { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("testdata"); + let mut files = Vec::new(); + collect_files_recursive(&root, &mut files); + files.sort(); + files + .into_iter() + .filter(|path| { + fs::read(path) + .map(|bytes| bytes.get(0..4) == Some(b"NRes")) + .unwrap_or(false) + }) + .collect() +} + +#[test] +fn build_render_mesh_for_real_models() { + let archives = nres_test_files(); + if archives.is_empty() { + eprintln!("skipping build_render_mesh_for_real_models: no NRes files in testdata"); + return; + } + + let mut models_checked = 0usize; + let mut meshes_non_empty = 0usize; + let mut bounds_non_empty = 0usize; + + for archive_path in archives { + let archive = Archive::open_path(&archive_path) + .unwrap_or_else(|err| panic!("failed to open {}: {err}", archive_path.display())); + for entry in archive.entries() { + if !entry.meta.name.to_ascii_lowercase().ends_with(".msh") { + continue; + } + models_checked += 1; + let payload = archive.read(entry.id).unwrap_or_else(|err| { + panic!( + "failed to read model '{}' from {}: {err}", + entry.meta.name, + archive_path.display() + ) + }); + let model = parse_model_payload(payload.as_slice()).unwrap_or_else(|err| { + panic!( + "failed to parse model '{}' from {}: {err}", + entry.meta.name, + archive_path.display() + ) + }); + let mesh = build_render_mesh(&model, 0, 0); + if !mesh.vertices.is_empty() { + meshes_non_empty += 1; + } + if compute_bounds(&mesh.vertices).is_some() { + bounds_non_empty += 1; + } + } + } + + assert!(models_checked > 0, "no MSH models found"); + assert!( + meshes_non_empty > 0, + "all generated render meshes are empty" + ); + assert_eq!( + meshes_non_empty, bounds_non_empty, + "bounds must be available for every non-empty mesh" + ); +} + +#[test] +fn compute_bounds_handles_empty_and_non_empty() { + assert!(compute_bounds(&[]).is_none()); + let bounds = compute_bounds(&[[1.0, 2.0, 3.0], [-2.0, 5.0, 0.5], [0.0, -1.0, 9.0]]) + .expect("bounds expected"); + assert_eq!(bounds.0, [-2.0, -1.0, 0.5]); + assert_eq!(bounds.1, [1.0, 5.0, 9.0]); +} |
