aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/fparkan-animation/src/lib.rs39
-rw-r--r--crates/fparkan-inspection/src/lib.rs54
-rw-r--r--crates/fparkan-msh/src/lib.rs78
3 files changed, 166 insertions, 5 deletions
diff --git a/crates/fparkan-animation/src/lib.rs b/crates/fparkan-animation/src/lib.rs
index 36db0e7..34a3fa7 100644
--- a/crates/fparkan-animation/src/lib.rs
+++ b/crates/fparkan-animation/src/lib.rs
@@ -781,16 +781,29 @@ fn write_f32_bits(out: &mut Vec<u8>, value: f32) {
}
fn compose_pose(parent: Pose, child: Pose) -> Result<Pose, AnimationError> {
+ let translated = rotate_point(parent.rotation, child.translation);
Ok(Pose {
translation: [
- parent.translation[0] + child.translation[0],
- parent.translation[1] + child.translation[1],
- parent.translation[2] + child.translation[2],
+ parent.translation[0] + translated[0],
+ parent.translation[1] + translated[1],
+ parent.translation[2] + translated[2],
],
rotation: normalize_quat(mul_quat(parent.rotation, child.rotation))?,
})
}
+fn rotate_point(rotation: [f32; 4], point: [f32; 3]) -> [f32; 3] {
+ let [x, y, z, w] = rotation;
+ let tx = 2.0 * (y * point[2] - z * point[1]);
+ let ty = 2.0 * (z * point[0] - x * point[2]);
+ let tz = 2.0 * (x * point[1] - y * point[0]);
+ [
+ point[0] + w * tx + (y * tz - z * ty),
+ point[1] + w * ty + (z * tx - x * tz),
+ point[2] + w * tz + (x * ty - y * tx),
+ ]
+}
+
fn mul_quat(left: [f32; 4], right: [f32; 4]) -> [f32; 4] {
let [lx, ly, lz, lw] = left;
let [rx, ry, rz, rw] = right;
@@ -1175,6 +1188,26 @@ mod tests {
}
#[test]
+ fn hierarchy_rotates_child_translation_by_parent_orientation() {
+ let half = std::f32::consts::FRAC_1_SQRT_2;
+ let local = [
+ Pose {
+ translation: [1.0, 0.0, 0.0],
+ rotation: [0.0, 0.0, half, half],
+ },
+ Pose {
+ translation: [2.0, 0.0, 0.0],
+ rotation: [0.0, 0.0, 0.0, 1.0],
+ },
+ ];
+ let buffer = evaluate_hierarchy(&[ParentIndex(None), ParentIndex(Some(0))], &local)
+ .expect("hierarchy");
+ assert!((buffer.poses[1].translation[0] - 1.0).abs() < 0.0001);
+ assert!((buffer.poses[1].translation[1] - 2.0).abs() < 0.0001);
+ assert!(buffer.poses[1].translation[2].abs() < 0.0001);
+ }
+
+ #[test]
fn generated_valid_quaternions_remain_finite() {
for index in 1..64_u16 {
let mut bytes = [0_u8; 24];
diff --git a/crates/fparkan-inspection/src/lib.rs b/crates/fparkan-inspection/src/lib.rs
index 744670a..7a6a2c7 100644
--- a/crates/fparkan-inspection/src/lib.rs
+++ b/crates/fparkan-inspection/src/lib.rs
@@ -24,7 +24,9 @@ use fparkan_diagnostics::{
diagnostic, render_human, Diagnostic, DiagnosticCode, DiagnosticContext, Phase, SourceSpan,
};
use fparkan_material::{decode_wear, resolve_material, MaterialFallback};
-use fparkan_msh::{decode_msh, validate_msh, ModelAsset};
+use fparkan_msh::{
+ decode_msh, node38_metadata, selected_slot, validate_msh, Group, Lod, ModelAsset, NodeId,
+};
use fparkan_nres::{decode as decode_nres, NresDocument, ReadProfile};
use fparkan_path::{normalize_relative, PathPolicy};
use fparkan_resource::{archive_path, resource_name, CachedResourceRepository, ResourceRepository};
@@ -85,6 +87,29 @@ pub struct ModelInspection {
pub indices: usize,
/// Batch count.
pub batches: usize,
+ /// Original node record stride.
+ pub node_stride: usize,
+ /// Standard-node metadata in source order, when the model uses `Node38`.
+ pub node38: Vec<Node38Inspection>,
+ /// Number of decoded type-8 animation keys, when available.
+ pub animation_keys: Option<usize>,
+ /// Declared type-19 animation frame count, when available.
+ pub animation_frame_count: Option<u32>,
+}
+
+/// Inspection view of a standard 38-byte model node.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Node38Inspection {
+ /// Source node index.
+ pub index: usize,
+ /// Opaque source field at byte offset two.
+ pub parent_or_link_raw: u16,
+ /// Type-19 frame-map offset, or `0xFFFF`.
+ pub anim_map_start: u16,
+ /// Type-8 fallback key index.
+ pub fallback_key: u16,
+ /// Whether LOD zero/group zero selects a geometry slot.
+ pub has_lod0_group0: bool,
}
/// Texture inspection payload.
@@ -312,6 +337,23 @@ pub fn inspect_model_from_root(
})?;
let msh = decode_msh(&document).map_err(|err| err.to_string())?;
let validated = validate_msh(&msh).map_err(|err| err.to_string())?;
+ let node38 = if validated.node_stride == 38 {
+ (0..validated.node_count)
+ .filter_map(|index| {
+ let node = NodeId(u32::try_from(index).ok()?);
+ let metadata = node38_metadata(&validated, node)?;
+ Some(Node38Inspection {
+ index,
+ parent_or_link_raw: metadata.parent_or_link_raw,
+ anim_map_start: metadata.anim_map_start,
+ fallback_key: metadata.fallback_key,
+ has_lod0_group0: selected_slot(&validated, node, Lod(0), Group(0)).is_some(),
+ })
+ })
+ .collect()
+ } else {
+ Vec::new()
+ };
Ok(ModelInspection {
streams: msh.streams().len(),
nodes: validated.node_count,
@@ -319,6 +361,16 @@ pub fn inspect_model_from_root(
positions: validated.positions.len(),
indices: validated.indices.len(),
batches: validated.batches.len(),
+ node_stride: validated.node_stride,
+ node38,
+ animation_keys: validated
+ .animation
+ .as_ref()
+ .map(|animation| animation.keys.len()),
+ animation_frame_count: validated
+ .animation
+ .as_ref()
+ .map(|animation| animation.frame_count),
})
}
diff --git a/crates/fparkan-msh/src/lib.rs b/crates/fparkan-msh/src/lib.rs
index 38a8984..dfc4c02 100644
--- a/crates/fparkan-msh/src/lib.rs
+++ b/crates/fparkan-msh/src/lib.rs
@@ -21,7 +21,7 @@
//! Stage-3 MSH asset contract.
use encoding_rs::WINDOWS_1251;
-use fparkan_animation::{AnimKey24, Pose};
+use fparkan_animation::{evaluate_hierarchy, AnimKey24, NodePoseBuffer, ParentIndex, Pose};
use fparkan_nres::{EntryMeta, NresDocument, NresError};
/// Node table stream.
@@ -450,6 +450,34 @@ pub fn node38_fallback_pose(model: &ModelAsset, node: NodeId) -> Option<Pose> {
.map(AnimKey24::sampling_pose)
}
+/// Evaluates the static fallback pose hierarchy of a standard `Node38` model.
+///
+/// `0xFFFF` denotes a root. Every non-root source link must name an earlier
+/// node, which preserves the producer's parent-before-child ordering. Models
+/// that do not meet this established `Node38` contract return `None` so callers
+/// can retain an explicitly unhierarchical diagnostic path.
+#[must_use]
+pub fn node38_fallback_hierarchy(model: &ModelAsset) -> Option<NodePoseBuffer> {
+ if model.node_stride != 38 || model.node_count == 0 {
+ return None;
+ }
+ let mut parents = Vec::with_capacity(model.node_count);
+ let mut poses = Vec::with_capacity(model.node_count);
+ for index in 0..model.node_count {
+ let node = NodeId(u32::try_from(index).ok()?);
+ let metadata = node38_metadata(model, node)?;
+ let parent = if metadata.parent_or_link_raw == u16::MAX {
+ ParentIndex(None)
+ } else {
+ let parent = usize::from(metadata.parent_or_link_raw);
+ (parent < index).then_some(ParentIndex(Some(metadata.parent_or_link_raw)))?
+ };
+ parents.push(parent);
+ poses.push(node38_fallback_pose(model, node)?);
+ }
+ evaluate_hierarchy(&parents, &poses).ok()
+}
+
/// Returns draw batches for a validated slot.
///
/// # Errors
@@ -1205,6 +1233,47 @@ mod tests {
}
#[test]
+ fn standard_nodes_evaluate_fallback_parent_hierarchy() {
+ let mut root = node38([u16::MAX; 15]);
+ root[2..4].copy_from_slice(&u16::MAX.to_le_bytes());
+ root[6..8].copy_from_slice(&0_u16.to_le_bytes());
+ let mut child = node38([u16::MAX; 15]);
+ child[2..4].copy_from_slice(&0_u16.to_le_bytes());
+ child[6..8].copy_from_slice(&1_u16.to_le_bytes());
+ let mut nodes = root;
+ nodes.extend(child);
+ let mut keys = Vec::new();
+ for (x, y, z, qz, qw) in [
+ (1.0, 0.0, 0.0, 23_170_i16, 23_170_i16),
+ (2.0, 0.0, 0.0, 0_i16, 32_767_i16),
+ ] {
+ push_f32(&mut keys, x);
+ push_f32(&mut keys, y);
+ push_f32(&mut keys, z);
+ push_f32(&mut keys, 0.0);
+ push_u16(&mut keys, 0);
+ push_u16(&mut keys, 0);
+ push_u16(&mut keys, qz.cast_unsigned());
+ push_u16(&mut keys, qw.cast_unsigned());
+ }
+ let document = decode_nested(&build_nres(&[
+ stream(STREAM_NODE_TABLE, 38, b"Res1", &nodes),
+ stream(STREAM_SLOTS, 0, b"Res2", &slots_payload(&[])),
+ stream(STREAM_POSITIONS, 0, b"Res3", &[]),
+ stream(STREAM_INDICES, 0, b"Res6", &[]),
+ stream(STREAM_ANIMATION_KEYS, 0, b"Res8", &keys),
+ stream(STREAM_BATCHES, 0, b"Res13", &[]),
+ stream(STREAM_ANIMATION_FRAME_MAP, 0, b"Res19", &[]),
+ ]))
+ .expect("nested NRes");
+ let model =
+ validate_msh(&decode_msh(&document).expect("msh document")).expect("model asset");
+ let hierarchy = node38_fallback_hierarchy(&model).expect("valid node hierarchy");
+ assert!((hierarchy.poses[1].translation[0] - 1.0).abs() < 0.001);
+ assert!((hierarchy.poses[1].translation[1] - 2.0).abs() < 0.001);
+ }
+
+ #[test]
fn type2_header_and_slot_tail_framing_are_exact() {
let too_small = decode_nested(&build_nres(&[
stream(STREAM_NODE_TABLE, 38, b"Res1", &[]),
@@ -1496,6 +1565,13 @@ mod tests {
let model = validate_msh(&msh).unwrap_or_else(|err| {
panic!("{corpus} {path:?} {:?}: {err}", entry.name_bytes())
});
+ if model.node_stride == 38 {
+ assert!(
+ node38_fallback_hierarchy(&model).is_some(),
+ "{corpus} {path:?} {:?}: Node38 hierarchy is not parent-before-child",
+ entry.name_bytes()
+ );
+ }
let preserved = msh.preserved_streams().unwrap_or_else(|err| {
panic!("{corpus} {path:?} {:?}: {err}", entry.name_bytes())
});