aboutsummaryrefslogtreecommitdiff
path: root/crates/render-core/src
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-19 03:46:23 +0300
committerValentin Popov <valentin@popov.link>2026-02-19 03:46:23 +0300
commit0e19660eb5122c8c52d5e909927884ad5c50b813 (patch)
tree6a53c24544ca828f08c2b6872d568b1edc1a4cef /crates/render-core/src
parent8a69872576eed41a918643be52a80fe74a054974 (diff)
downloadfparkan-0e19660eb5122c8c52d5e909927884ad5c50b813.tar.xz
fparkan-0e19660eb5122c8c52d5e909927884ad5c50b813.zip
Refactor documentation structure and add new specifications
- Updated MSH documentation to reflect changes in material, wear, and texture specifications. - Introduced new `render.md` file detailing the render pipeline process. - Removed outdated sections from `runtime-pipeline.md` and redirected to `render.md`. - Added detailed specifications for `Texm` texture format and `WEAR` wear table. - Updated navigation in `mkdocs.yml` to align with new documentation structure.
Diffstat (limited to 'crates/render-core/src')
-rw-r--r--crates/render-core/src/lib.rs84
-rw-r--r--crates/render-core/src/tests.rs101
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]);
+}