rakata_formats/mdl/types.rs
1//! Node-specific data types for the MDL format.
2//!
3//! Each MDL node carries a base header (name, transform, controllers) plus
4//! type-specific data determined by the node's bitflags. This module defines
5//! the [`MdlNodeData`] enum that discriminates between node types, along with
6//! the concrete structs for each variant.
7//!
8//! ## Rust Representation
9//!
10//! The engine's C++ code uses an inheritance hierarchy (`MdlNodeSkin` extends
11//! `MdlNodeTriMesh` extends `MdlNode`). We represent this as a flat enum
12//! with composition -- mesh-derived variants contain an [`MdlMesh`] field
13//! rather than inheriting from a base class. This is idiomatic Rust: the
14//! enum discriminant replaces the C++ vtable, and shared mesh data is
15//! accessed through the composed struct.
16//!
17//! ```text
18//! MdlNodeData (enum)
19//! |-- Base (no extra data)
20//! |-- Light(MdlLight) (92 extra bytes)
21//! |-- Emitter(MdlEmitter) (224 extra bytes)
22//! |-- Camera(MdlCamera) (no extra data)
23//! |-- Reference(MdlReference) (36 extra bytes)
24//! |-- Mesh(MdlMesh) (332 extra bytes)
25//! |-- Skin(MdlSkin) (332 + 100 extra bytes)
26//! | `-- contains MdlMesh (composition, not inheritance)
27//! |-- AnimMesh(MdlAnimMesh) (332 + 56 extra bytes)
28//! | `-- contains MdlMesh
29//! |-- Dangly(MdlDangly) (332 + 28 extra bytes)
30//! | `-- contains MdlMesh
31//! |-- Aabb(MdlAabb) (332 + 4 extra bytes)
32//! | `-- contains MdlMesh
33//! `-- Saber(MdlSaber) (332 + 20 extra bytes)
34//! `-- contains MdlMesh
35//! ```
36//!
37//! See [`super`] module docs for the binary format layout and
38//! `docs/notes/mdl_mdx.md` for Ghidra-verified allocation sizes.
39
40/// Type-specific payload for an MDL node.
41///
42/// Determined at parse time from the node's [`super::node_flags`] bitfield.
43/// Mesh-derived variants (Skin, AnimMesh, Dangly, Aabb, Saber) each contain
44/// an [`MdlMesh`] plus type-specific extension fields.
45#[derive(Debug, Clone, PartialEq)]
46pub enum MdlNodeData {
47 /// Pure hierarchy node with no type-specific data (flags = 0x0001).
48 Base,
49 /// Light node (flags & 0x0002).
50 Light(MdlLight),
51 /// Emitter node (flags & 0x0004).
52 Emitter(MdlEmitter),
53 /// Camera node (flags & 0x0008).
54 Camera(MdlCamera),
55 /// Reference to an external model (flags & 0x0010).
56 Reference(MdlReference),
57 /// Triangle mesh node (flags & 0x0020, no subtype flags).
58 Mesh(MdlMesh),
59 /// Skinned mesh with bone weights (flags & 0x0060).
60 Skin(MdlSkin),
61 /// Animated mesh with per-frame vertex sets (flags & 0x00A0).
62 AnimMesh(MdlAnimMesh),
63 /// Dangly mesh with physics constraints (flags & 0x0120).
64 Dangly(MdlDangly),
65 /// AABB walkmesh tree (flags & 0x0220).
66 Aabb(MdlAabb),
67 /// Lightsaber blade mesh (flags & 0x0820).
68 Saber(MdlSaber),
69}
70
71impl MdlNodeData {
72 /// Returns the binary flags for this node type.
73 ///
74 /// These are the on-disk bitflags written at node offset `0x00`.
75 /// Derived from the variant rather than stored independently, ensuring
76 /// the type system and binary representation stay in sync.
77 ///
78 /// See [`super::node_flags`] for the individual flag constants.
79 #[must_use = "flags returns a computed value and has no side effects"]
80 pub fn flags(&self) -> u32 {
81 use super::node_flags;
82 match self {
83 MdlNodeData::Base => node_flags::HEADER,
84 MdlNodeData::Light(_) => node_flags::HEADER | node_flags::LIGHT,
85 MdlNodeData::Emitter(_) => node_flags::HEADER | node_flags::EMITTER,
86 MdlNodeData::Camera(_) => node_flags::HEADER | node_flags::CAMERA,
87 MdlNodeData::Reference(_) => node_flags::HEADER | node_flags::REFERENCE,
88 MdlNodeData::Mesh(_) => node_flags::HEADER | node_flags::MESH,
89 MdlNodeData::Skin(_) => node_flags::HEADER | node_flags::MESH | node_flags::SKIN,
90 MdlNodeData::AnimMesh(_) => node_flags::HEADER | node_flags::MESH | node_flags::ANIM,
91 MdlNodeData::Dangly(_) => node_flags::HEADER | node_flags::MESH | node_flags::DANGLY,
92 MdlNodeData::Aabb(_) => node_flags::HEADER | node_flags::MESH | node_flags::AABB,
93 MdlNodeData::Saber(_) => node_flags::HEADER | node_flags::MESH | node_flags::SABER,
94 }
95 }
96
97 /// Returns a reference to the contained mesh, if this is a mesh-derived variant.
98 #[must_use = "mesh returns a reference and has no side effects"]
99 pub fn mesh(&self) -> Option<&MdlMesh> {
100 match self {
101 MdlNodeData::Mesh(m) => Some(m),
102 MdlNodeData::Skin(s) => Some(&s.mesh),
103 MdlNodeData::AnimMesh(a) => Some(&a.mesh),
104 MdlNodeData::Dangly(d) => Some(&d.mesh),
105 MdlNodeData::Aabb(a) => Some(&a.mesh),
106 MdlNodeData::Saber(s) => Some(&s.mesh),
107 _ => None,
108 }
109 }
110
111 /// Returns a mutable reference to the contained mesh, if this is a mesh-derived variant.
112 #[must_use = "mesh_mut returns a reference and has no side effects"]
113 pub fn mesh_mut(&mut self) -> Option<&mut MdlMesh> {
114 match self {
115 MdlNodeData::Mesh(m) => Some(m),
116 MdlNodeData::Skin(s) => Some(&mut s.mesh),
117 MdlNodeData::AnimMesh(a) => Some(&mut a.mesh),
118 MdlNodeData::Dangly(d) => Some(&mut d.mesh),
119 MdlNodeData::Aabb(a) => Some(&mut a.mesh),
120 MdlNodeData::Saber(s) => Some(&mut s.mesh),
121 _ => None,
122 }
123 }
124}
125
126/// A face in the binary MaxFace format (32 bytes on disk).
127///
128/// Contains geometric data for rendering and collision: a plane equation,
129/// surface material ID, face adjacency indices, and vertex indices.
130///
131/// Layout verified via Ghidra struct `MaxFace` (32 bytes, `/KotOR Types/Rendering`)
132/// and cross-checked with the `0x1A` vertex-index offset constant used in both
133/// `MdlNodeDanglyMesh::InternalGenVertices` and `MdlNodeAnimMesh::InternalGenVertices`.
134/// See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh.
135#[derive(Debug, Clone, PartialEq, Default)]
136pub struct MdlFace {
137 /// Face plane normal vector (3 × f32). MaxFace offset +0x00.
138 pub plane_normal: [f32; 3],
139 /// Face plane distance from origin (f32). MaxFace offset +0x0C.
140 pub plane_distance: f32,
141 /// Surface material/type ID (u32). MaxFace offset +0x10.
142 pub surface_id: u32,
143 /// Indices of the three adjacent faces (3 × u16). MaxFace offset +0x14.
144 pub adjacent: [u16; 3],
145 /// Vertex indices into the mesh vertex array (3 × u16). MaxFace offset +0x1A.
146 pub vertex_indices: [u16; 3],
147}
148
149/// A triangle mesh contained within a node.
150///
151/// Vertex attribute data comes from the companion MDX file. Each attribute
152/// is stored in a separate array, matching the interleaved MDX vertex layout
153/// controlled by the `mdx_vertex_flags` bitfield and per-attribute byte offsets
154/// in the TriMesh header (+0x100..+0x120).
155///
156/// See `docs/notes/mdl_mdx.md` §MDX Vertex Layout for the full specification.
157#[derive(Debug, Clone, Default, PartialEq)]
158pub struct MdlMesh {
159 // --- Toolset function pointer stubs (extra +0x00, +0x04) ---
160 // These are stale code addresses from BioWare's build toolset binary,
161 // baked into the MDL file during serialization. The engine overwrites
162 // them at load time with its own function pointers. They have no
163 // runtime meaning, but serve as a toolset version fingerprint:
164 // all vanilla K1 files share the same pair per mesh subtype.
165 //
166 // See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh Binary Layout.
167 /// `gen_vertices` function pointer stub (u32). Extra +0x00.
168 pub fn_ptr_gen_vertices: u32,
169 /// `remove_temporary_array` function pointer stub (u32). Extra +0x04.
170 pub fn_ptr_remove_temp_array: u32,
171
172 // --- Inline scalar/fixed-size fields (Ghidra-verified) ---
173 // See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh Binary Layout.
174 /// Bounding box minimum corner (3×f32). Extra +0x14.
175 pub bounding_min: [f32; 3],
176 /// Bounding box maximum corner (3×f32). Extra +0x20.
177 pub bounding_max: [f32; 3],
178 /// Bounding sphere radius (f32). Extra +0x2C.
179 pub bsphere_radius: f32,
180 /// Bounding sphere center (3×f32). Extra +0x30.
181 pub bsphere_center: [f32; 3],
182 /// RGB diffuse color (3×f32). Extra +0x3C.
183 pub diffuse_color: [f32; 3],
184 /// RGB ambient color (3×f32). Extra +0x48.
185 pub ambient_color: [f32; 3],
186 /// Transparency hint (0=opaque, 1=transparent). Extra +0x54.
187 pub transparency_hint: i32,
188 /// Primary texture name (up to 32 chars). Extra +0x58.
189 pub texture_0: String,
190 /// Secondary/lightmap texture name (up to 32 chars). Extra +0x78.
191 pub texture_1: String,
192 /// UV animation enable flag. Extra +0xE8.
193 pub animate_uv: i32,
194 /// UV animation direction X. Extra +0xEC.
195 pub uv_direction_x: f32,
196 /// UV animation direction Y. Extra +0xF0.
197 pub uv_direction_y: f32,
198 /// UV jitter amount. Extra +0xF4.
199 pub uv_jitter: f32,
200 /// UV jitter speed. Extra +0xF8.
201 pub uv_jitter_speed: f32,
202 /// Number of UV texture channels. Extra +0x132.
203 pub texture_channel_count: u16,
204 /// Lightmapped flag. Extra +0x134.
205 pub light_mapped: bool,
206 /// Rotate texture flag. Extra +0x135.
207 pub rotate_texture: bool,
208 /// Background geometry flag. Extra +0x136.
209 pub is_background_geometry: bool,
210 /// Beaming flag. Extra +0x138.
211 pub beaming: bool,
212 /// Total surface area (computed by toolset). Extra +0x13C.
213 pub total_surface_area: f32,
214
215 // --- MDX vertex attribute arrays ---
216 // Each array has `vertex_count` elements when populated, or is empty if
217 // the attribute is not present (offset == -1 in the mesh header).
218 /// Vertex positions (3×f32: x, y, z). MDX flag 0x01.
219 pub positions: Vec<[f32; 3]>,
220 /// Vertex normals (3×f32: x, y, z). MDX flag 0x20.
221 pub normals: Vec<[f32; 3]>,
222 /// Vertex colors (4×u8: R, G, B, A). No flag bit - offset != -1 indicates presence.
223 pub vertex_colors: Vec<[u8; 4]>,
224 /// Primary UV coordinates (2×f32: u, v). MDX flag 0x02.
225 pub uv1: Vec<[f32; 2]>,
226 /// Secondary UV coordinates (2×f32: u, v). MDX flag 0x04.
227 pub uv2: Vec<[f32; 2]>,
228 /// Tertiary UV coordinates (2×f32: u, v). MDX flag 0x08.
229 pub uv3: Vec<[f32; 2]>,
230 /// Quaternary UV coordinates (2×f32: u, v). MDX flag 0x10.
231 pub uv4: Vec<[f32; 2]>,
232 /// Tangent space basis (3×3×f32: tangent, bitangent, cross). MDX flag 0x80.
233 ///
234 /// Each entry is 36 bytes: three vec3s forming the tangent-space basis
235 /// used for bump/normal mapping.
236 pub tangent_space: Vec<[[f32; 3]; 3]>,
237
238 /// Face data parsed from the MaxFace array (32 bytes per face on disk).
239 pub faces: Vec<MdlFace>,
240
241 // --- TriMesh internal CExoArrayList fields (+0x98..+0xC8) ---
242 // These 5 CExoArrayList slots form a GL index buffer submission system.
243 // Three are derivable (face_count*3, packed u16 face indices, content
244 // pointer); one is a dead field (always zeros in KotOR); one stores the
245 // "inverted counter" mesh sequence value.
246 //
247 // See `docs/notes/mesh_derived_fields.md` for full documentation.
248 /// Mesh sequence "inverted counter" value from `index_buffer_pools` (+0xC8).
249 ///
250 /// Preserved from binary on roundtrip. For newly constructed models, compute
251 /// via the inverted counter formula (see `mesh_derived_fields.md` §1.5).
252 /// The engine overwrites this at load time with a GL pool handle.
253 pub inverted_counter: u32,
254
255 /// Whether the source binary used the "embedded position" variant for the
256 /// `vertex_indices_count` payload (+0xB0).
257 ///
258 /// When true, the writer emits a 4-byte tag + `vertex_count * 12` bytes
259 /// of position data instead of a single u32 `face_count * 3`.
260 pub has_embedded_positions: bool,
261
262 /// TriMesh shared index offset scalar (+0xD4).
263 pub shared_index_offset: i32,
264 /// TriMesh shared index pool scalar (+0xD8).
265 pub shared_index_pool: i32,
266 /// TriMesh shared index size scalar (+0xDC).
267 pub shared_index_size: i32,
268 /// TriMesh indices-per-face scalar (+0xE0). Vanilla value is typically 3.
269 pub indices_per_face: u32,
270 /// Number of vertices declared in the mesh header.
271 pub vertex_count: u16,
272 /// Is this mesh renderable? (MdlNodeTriMesh +0x189).
273 pub render: bool,
274 /// Does this mesh cast shadows? (MdlNodeTriMesh +0x187).
275 pub shadow: bool,
276}
277
278impl MdlMesh {
279 /// Compute the axis-aligned bounding box from vertex positions.
280 ///
281 /// Returns `(bounding_min, bounding_max)`, or `None` if positions are empty.
282 #[must_use]
283 pub fn compute_bounding_box(&self) -> Option<([f32; 3], [f32; 3])> {
284 let mut iter = self.positions.iter();
285 let first = iter.next()?;
286 let mut min = *first;
287 let mut max = *first;
288 for pos in iter {
289 for i in 0..3 {
290 if pos[i] < min[i] {
291 min[i] = pos[i];
292 }
293 if pos[i] > max[i] {
294 max[i] = pos[i];
295 }
296 }
297 }
298 Some((min, max))
299 }
300
301 /// Compute the centroid-based bounding sphere from vertex positions.
302 ///
303 /// Uses the engine's algorithm from `PartTriMesh::GetMinimumSphere`
304 /// (`0x00443330`): center = centroid, radius = max distance to any vertex.
305 /// This is NOT the true minimum bounding sphere (Welzl), but matches
306 /// vanilla file values.
307 ///
308 /// Returns `(center, radius)`, or `None` if positions are empty.
309 #[must_use]
310 pub fn compute_bounding_sphere(&self) -> Option<([f32; 3], f32)> {
311 if self.positions.is_empty() {
312 return None;
313 }
314 // Precision loss impossible: KotOR vertex counts fit easily in f64's 53-bit mantissa.
315 #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
316 let n = self.positions.len() as f64;
317 let mut cx = 0.0f64;
318 let mut cy = 0.0f64;
319 let mut cz = 0.0f64;
320 for pos in &self.positions {
321 cx += f64::from(pos[0]);
322 cy += f64::from(pos[1]);
323 cz += f64::from(pos[2]);
324 }
325 // Intentional f64->f32 narrowing: geometry averaging is computed in double
326 // precision but stored as f32 per the MDL format.
327 #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
328 let center = [(cx / n) as f32, (cy / n) as f32, (cz / n) as f32];
329 let max_dist_sq = self
330 .positions
331 .iter()
332 .map(|pos| {
333 let dx = pos[0] - center[0];
334 let dy = pos[1] - center[1];
335 let dz = pos[2] - center[2];
336 dx * dx + dy * dy + dz * dz
337 })
338 .fold(0.0f32, f32::max);
339 Some((center, max_dist_sq.sqrt()))
340 }
341
342 /// Compute the total surface area of all triangles in the mesh.
343 ///
344 /// Returns 0.0 if positions or faces are empty.
345 #[must_use]
346 pub fn compute_total_surface_area(&self) -> f32 {
347 let positions = &self.positions;
348 if positions.is_empty() {
349 return 0.0;
350 }
351 let total: f64 = self
352 .faces
353 .iter()
354 .filter_map(|face| {
355 let v0 = usize::from(face.vertex_indices[0]);
356 let v1 = usize::from(face.vertex_indices[1]);
357 let v2 = usize::from(face.vertex_indices[2]);
358 if v0 >= positions.len() || v1 >= positions.len() || v2 >= positions.len() {
359 return None;
360 }
361 let p0 = positions[v0];
362 let p1 = positions[v1];
363 let p2 = positions[v2];
364 let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
365 let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
366 let [cx, cy, cz] = cross_f32_as_f64(e1, e2);
367 Some((cx * cx + cy * cy + cz * cz).sqrt() * 0.5)
368 })
369 .sum();
370 // Intentional f64->f32 narrowing: cross-product magnitude sums are computed
371 // in double precision but the result is stored as f32.
372 #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
373 let result = total as f32;
374 result
375 }
376
377 /// Recompute `plane_normal` and `plane_distance` for all faces from vertex positions.
378 ///
379 /// Uses the standard cross-product formula:
380 /// ```text
381 /// normal = normalize(cross(v1 - v0, v2 - v0))
382 /// distance = -dot(normal, v0)
383 /// ```
384 /// Degenerate triangles (zero-area) get a zero normal and zero distance.
385 pub fn recompute_face_planes(&mut self) {
386 let positions = &self.positions;
387 for face in &mut self.faces {
388 let v0 = usize::from(face.vertex_indices[0]);
389 let v1 = usize::from(face.vertex_indices[1]);
390 let v2 = usize::from(face.vertex_indices[2]);
391 if v0 >= positions.len() || v1 >= positions.len() || v2 >= positions.len() {
392 face.plane_normal = [0.0; 3];
393 face.plane_distance = 0.0;
394 continue;
395 }
396 let p0 = positions[v0];
397 let p1 = positions[v1];
398 let p2 = positions[v2];
399 let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
400 let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
401 let [nx, ny, nz] = cross_f32(e1, e2);
402 let len = (nx * nx + ny * ny + nz * nz).sqrt();
403 if len > 0.0 {
404 face.plane_normal = [nx / len, ny / len, nz / len];
405 face.plane_distance = -(face.plane_normal[0] * p0[0]
406 + face.plane_normal[1] * p0[1]
407 + face.plane_normal[2] * p0[2]);
408 } else {
409 face.plane_normal = [0.0; 3];
410 face.plane_distance = 0.0;
411 }
412 }
413 }
414
415 /// Compute face adjacency from vertex positions using position-based edge matching.
416 ///
417 /// For each edge of each face, finds the adjacent face sharing that edge.
418 /// Uses `0xFFFF` as the no-neighbor sentinel. Position matching uses `{:.4e}`
419 /// formatting for floating-point key comparison (handles duplicate vertices
420 /// at the same position with different normals/UVs).
421 ///
422 /// For non-manifold edges (more than 2 faces sharing an edge), the smallest
423 /// face index is used (deterministic, matches PyKotor behavior).
424 pub fn compute_face_adjacency(&mut self) {
425 use std::collections::HashMap;
426
427 let positions = &self.positions;
428 if positions.is_empty() || self.faces.is_empty() {
429 return;
430 }
431
432 // Step 1: Build position key for each vertex.
433 let pos_key = |idx: usize| -> String {
434 if idx >= positions.len() {
435 return String::new();
436 }
437 let p = positions[idx];
438 format!("{:.4e},{:.4e},{:.4e}", p[0], p[1], p[2])
439 };
440
441 // Step 2: Group vertices by position.
442 let mut vertex_group: HashMap<String, Vec<usize>> = HashMap::new();
443 for i in 0..positions.len() {
444 vertex_group.entry(pos_key(i)).or_default().push(i);
445 }
446
447 // Step 3: Build vertex-to-faces map.
448 let mut vertex_to_faces: HashMap<usize, Vec<usize>> = HashMap::new();
449 for (fi, face) in self.faces.iter().enumerate() {
450 for &vi in &face.vertex_indices {
451 vertex_to_faces.entry(usize::from(vi)).or_default().push(fi);
452 }
453 }
454
455 // Step 4: For each vertex, collect all faces touching any vertex at the
456 // same position (handles UV seam / hard edge duplicates).
457 let face_set_for_vertex = |vi: usize| -> Vec<usize> {
458 let key = pos_key(vi);
459 let mut faces = Vec::new();
460 if let Some(group) = vertex_group.get(&key) {
461 for &gv in group {
462 if let Some(flist) = vertex_to_faces.get(&gv) {
463 faces.extend_from_slice(flist);
464 }
465 }
466 }
467 faces.sort_unstable();
468 faces.dedup();
469 faces
470 };
471
472 // Step 5: For each face and each edge, find the adjacent face.
473 let face_count = self.faces.len();
474 let mut adjacency = vec![[0xFFFFu16; 3]; face_count];
475
476 for (fi, face) in self.faces.iter().enumerate() {
477 let vis = face.vertex_indices;
478 let edges = [
479 (usize::from(vis[0]), usize::from(vis[1])),
480 (usize::from(vis[1]), usize::from(vis[2])),
481 (usize::from(vis[2]), usize::from(vis[0])),
482 ];
483 for (ei, &(va, vb)) in edges.iter().enumerate() {
484 let faces_a = face_set_for_vertex(va);
485 let faces_b = face_set_for_vertex(vb);
486
487 // Intersect face sets, excluding self.
488 let mut best = None;
489 let (mut ia, mut ib) = (0, 0);
490 while ia < faces_a.len() && ib < faces_b.len() {
491 match faces_a[ia].cmp(&faces_b[ib]) {
492 std::cmp::Ordering::Less => ia += 1,
493 std::cmp::Ordering::Greater => ib += 1,
494 std::cmp::Ordering::Equal => {
495 let candidate = faces_a[ia];
496 if candidate != fi {
497 // Use min (smallest index) for deterministic non-manifold handling.
498 if best.is_none() {
499 best = Some(candidate);
500 }
501 }
502 ia += 1;
503 ib += 1;
504 }
505 }
506 }
507
508 if let Some(adj) = best {
509 if adj <= 0xFFFE {
510 adjacency[fi][ei] =
511 u16::try_from(adj).expect("adj <= 0xFFFE guarantees it fits u16");
512 }
513 }
514 }
515 }
516
517 // Apply computed adjacency.
518 for (fi, adj) in adjacency.into_iter().enumerate() {
519 self.faces[fi].adjacent = adj;
520 }
521 }
522
523 /// Recompute all derivable geometric fields at once.
524 ///
525 /// Updates: bounding box, bounding sphere, total surface area,
526 /// face plane normals/distances, and face adjacency.
527 pub fn recompute_derived_fields(&mut self) {
528 if let Some((bmin, bmax)) = self.compute_bounding_box() {
529 self.bounding_min = bmin;
530 self.bounding_max = bmax;
531 }
532 if let Some((center, radius)) = self.compute_bounding_sphere() {
533 self.bsphere_center = center;
534 self.bsphere_radius = radius;
535 }
536 self.total_surface_area = self.compute_total_surface_area();
537 self.recompute_face_planes();
538 self.compute_face_adjacency();
539 }
540}
541
542/// Light node data.
543///
544/// Binary layout: 92 extra bytes after the base node header.
545/// Contains scalar properties and flare data arrays.
546///
547/// Verified via `MdlNodeLight::MdlNodeLight` constructor (`0x0044a3f0`),
548/// `InputBinary::ResetLight` (`0x004a05e0`), and Ghidra struct
549/// `MdlNodeLight` (172 bytes total = 80 base + 92 extra).
550/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
551#[derive(Debug, Clone, PartialEq)]
552pub struct MdlLight {
553 /// Flare radius (f32, default 0.0). Extra offset +0x00.
554 pub flare_radius: f32,
555 /// Texture SafePointers (runtime-only). Extra +0x04.
556 ///
557 /// Three u32 values populated at runtime by `AurTextureGetReference`
558 /// after resolving texture names. In vanilla binaries these contain
559 /// stale addresses from the build toolset. Preserved for lossless
560 /// roundtrip and toolset fingerprinting (same rationale as the
561 /// mesh function pointer stubs).
562 pub texture_safe_ptrs: [u32; 3],
563 /// Flare sizes (one f32 per flare). Extra +0x10 CExoArrayList pointer.
564 pub flare_sizes: Vec<f32>,
565 /// Flare positions (one f32 per flare). Extra +0x1C CExoArrayList pointer.
566 pub flare_positions: Vec<f32>,
567 /// Flare color shifts (one vec3 per flare). Extra +0x28 CExoArrayList pointer.
568 pub flare_color_shifts: Vec<[f32; 3]>,
569 /// Flare texture names. Extra +0x34 CExoArrayList pointer.
570 ///
571 /// Stored as a CExoArrayList of char* pointers, where each pointer is
572 /// also relocated and points to a null-terminated string. Flattened to
573 /// a `Vec<String>` for ergonomic access.
574 pub flare_texture_names: Vec<String>,
575 /// Light priority (default 5). Extra offset +0x40.
576 pub priority: i32,
577 /// Dynamic type count (default 1). Extra offset +0x44.
578 pub num_dynamic_types: i32,
579 /// Affects dynamic objects (default 1). Extra offset +0x48.
580 pub affectdynamic: i32,
581 /// Casts shadow (default 1). Extra offset +0x4C.
582 pub shadow: i32,
583 /// Ambient-only light (default 0). Extra offset +0x50.
584 pub ambientonly: i32,
585 /// Generate flare effect (default 0). Extra offset +0x54.
586 pub generateflare: i32,
587 /// Fading light (default 1). Extra offset +0x58.
588 pub fading_light: i32,
589}
590
591impl Default for MdlLight {
592 /// Returns a light node with engine default values.
593 ///
594 /// Defaults match the `MdlNodeLight::MdlNodeLight` constructor (`0x0044a3f0`):
595 /// priority=5, num_dynamic_types=1, affectdynamic=1, shadow=1, fading_light=1.
596 fn default() -> Self {
597 MdlLight {
598 flare_radius: 0.0,
599 texture_safe_ptrs: [0u32; 3],
600 flare_sizes: vec![],
601 flare_positions: vec![],
602 flare_color_shifts: vec![],
603 flare_texture_names: vec![],
604 priority: 5,
605 num_dynamic_types: 1,
606 affectdynamic: 1,
607 shadow: 1,
608 ambientonly: 0,
609 generateflare: 0,
610 fading_light: 1,
611 }
612 }
613}
614
615/// Emitter node data.
616///
617/// Binary layout: 224 extra bytes (0xE0) after the 80-byte base node header.
618/// All fields are inline fixed-size data with no pointers - no relocation
619/// needed (only `ResetMdlNodeParts` is called, not a type-specific Reset).
620///
621/// The `update` field is architecturally significant: it determines which
622/// runtime emitter class is instantiated ("Fountain", "Explosion", "Single",
623/// or "Lightning"). Controller 502 ("detonate") is only valid for "Explosion"
624/// emitters.
625///
626/// Verified via `MdlNodeEmitter` constructor (`0x0044a300`, 304 bytes total),
627/// `MdlNodeEmitter::InternalParseField` (`0x00469700`, 2,632 bytes),
628/// and `MdlNodeEmitter::InternalCreateInstance` (`0x0049d5c0`).
629/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
630#[derive(Debug, Clone, PartialEq)]
631pub struct MdlEmitter {
632 /// Dead space / unused float (default 0.0). Extra offset +0x00.
633 pub deadspace: f32,
634 /// Blast radius (default 0.0). Extra offset +0x04.
635 pub blast_radius: f32,
636 /// Blast length (default 0.0). Extra offset +0x08.
637 pub blast_length: f32,
638 /// Number of branches (default 0). Extra offset +0x0C.
639 pub num_branches: i32,
640 /// Control point smoothing (default 0). Extra offset +0x10.
641 pub control_pt_smoothing: i32,
642 /// X grid size (default 0). Extra offset +0x14.
643 pub x_grid: i32,
644 /// Y grid size (default 0). Extra offset +0x18.
645 pub y_grid: i32,
646 /// Spawn type (default 0). Extra offset +0x1C.
647 pub spawn_type: i32,
648 /// Emitter update type string (up to 32 chars). Extra offset +0x20.
649 ///
650 /// Determines which runtime emitter class is instantiated:
651 /// - `"Fountain"` - steady particle effects (most common)
652 /// - `"Explosion"` - burst effects (supports controller 502 "detonate")
653 /// - `"Single"` - one-shot particle
654 /// - `"Lightning"` - lightning bolt effects
655 pub update: String,
656 /// Render type string (up to 32 chars). Extra offset +0x40.
657 pub render: String,
658 /// Blend type string (up to 32 chars). Extra offset +0x60.
659 pub blend: String,
660 /// Texture name (up to 32 chars). Extra offset +0x80.
661 pub texture: String,
662 /// Chunk name (up to 16 chars). Extra offset +0xA0.
663 pub chunk_name: String,
664 /// Two-sided texturing (default 0). Extra offset +0xB0.
665 pub two_sided_tex: i32,
666 /// Loop emitter (default 0). Extra offset +0xB4.
667 pub loop_emitter: i32,
668 /// Render order (default 0). Extra offset +0xB8.
669 pub render_order: u16,
670 /// Frame blending enabled (default false). Extra offset +0xBA.
671 pub frame_blending: bool,
672 /// Depth texture name (up to 16 chars). Extra offset +0xBB.
673 pub depth_texture_name: String,
674 /// Reserved/padding bytes at extra offset +0xCB..+0xE0 (21 bytes).
675 /// Preserved verbatim for roundtrip fidelity per the reserved field rule.
676 pub reserved: [u8; 21],
677}
678
679impl Default for MdlEmitter {
680 /// Returns an emitter with engine default values.
681 ///
682 /// All numeric fields default to zero, all strings default to empty.
683 fn default() -> Self {
684 MdlEmitter {
685 deadspace: 0.0,
686 blast_radius: 0.0,
687 blast_length: 0.0,
688 num_branches: 0,
689 control_pt_smoothing: 0,
690 x_grid: 0,
691 y_grid: 0,
692 spawn_type: 0,
693 update: String::new(),
694 render: String::new(),
695 blend: String::new(),
696 texture: String::new(),
697 chunk_name: String::new(),
698 two_sided_tex: 0,
699 loop_emitter: 0,
700 render_order: 0,
701 frame_blending: false,
702 depth_texture_name: String::new(),
703 reserved: [0u8; 21],
704 }
705 }
706}
707
708/// Camera node data.
709///
710/// Camera nodes have no type-specific binary data beyond the base 80-byte
711/// node header. Confirmed via `ParseFieldDispatch` dispatch table - camera
712/// (0x009) routes to the base `MdlNode::InternalParseField`, not a
713/// camera-specific parser.
714///
715/// See `docs/notes/mdl_mdx.md` Non-Mesh Node Type Structs.
716#[derive(Debug, Clone, PartialEq, Eq)]
717pub struct MdlCamera {
718 _private: (),
719}
720
721impl MdlCamera {
722 /// Creates a camera node (no type-specific data).
723 pub fn new() -> Self {
724 MdlCamera { _private: () }
725 }
726}
727
728impl Default for MdlCamera {
729 fn default() -> Self {
730 Self::new()
731 }
732}
733
734/// Reference node data.
735///
736/// References an external model by name, with an optional reattachment flag.
737/// Binary layout: 36 extra bytes after the base node header.
738///
739/// Verified via `MdlNodeReference::InternalParseField` (`0x00468070`) and
740/// Ghidra struct `MdlNodeReference` (116 bytes total = 80 base + 36 extra).
741/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
742#[derive(Debug, Clone, Default, PartialEq, Eq)]
743pub struct MdlReference {
744 /// Referenced model name (up to 32 chars). Extra offset +0x00.
745 pub ref_model: String,
746 /// Whether this reference can be reattached (default 0). Extra offset +0x20.
747 pub reattachable: i32,
748}
749
750/// Skinned mesh with bone weight data.
751///
752/// Extends [`MdlMesh`] with inverse bind pose data and bone index mappings
753/// used for skeletal animation. Each bone has an inverse bind rotation
754/// (quaternion) and translation (vector) that transform from bone space
755/// to model space.
756///
757/// Binary layout: 100 extra bytes (0x64) after the 332-byte TriMesh header.
758/// Verified via `InputBinary::ResetSkin` (`0x004a01b0`) and Ghidra struct
759/// `MdlNodeSkin` (512 bytes total = 412 TriMesh + 100 extra).
760/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
761#[derive(Debug, Clone, PartialEq)]
762pub struct MdlSkin {
763 /// The base triangle mesh.
764 pub mesh: MdlMesh,
765 /// MDX per-vertex bone weights byte offset (i32). Skin extra +0x0C.
766 ///
767 /// Byte offset within each MDX vertex stride to the 4-float bone weights.
768 /// Value -1 means not present.
769 pub mdx_bone_weights_offset: i32,
770 /// MDX per-vertex bone indices byte offset (i32). Skin extra +0x10.
771 ///
772 /// Byte offset within each MDX vertex stride to the 4-float bone indices.
773 /// Value -1 means not present.
774 pub mdx_bone_indices_offset: i32,
775 /// Per-vertex bone weights (4 x f32, one per influence slot).
776 ///
777 /// Each vertex has up to 4 bone influences. Unused slots are zero.
778 /// One entry per vertex when populated, empty when no skin weights are present.
779 pub bone_weights: Vec<[f32; 4]>,
780 /// Per-vertex bone indices (4 x f32, one per influence slot).
781 ///
782 /// Stored as f32 by engine convention (small non-negative integers).
783 /// Each index references a bone in the bonemap. Unused slots are zero.
784 /// One entry per vertex when populated, empty when no skin weights are present.
785 pub bone_indices: Vec<[f32; 4]>,
786 /// Bone-to-node index mapping from skin extra +0x14 pointer.
787 ///
788 /// Each u32 maps a local bone index to a global skeleton node index.
789 /// The engine relocates the pointer when the count (extra +0x18) > 0.
790 pub bonemap: Vec<u32>,
791 /// Inverse bind rotation per bone (quaternion: w, x, y, z).
792 ///
793 /// One entry per bone. Transforms from bone space to model space.
794 /// Data pointed to by CExoArrayList at skin extra +0x1C.
795 pub qbone_ref_inv: Vec<[f32; 4]>,
796 /// Inverse bind translation per bone (vector: x, y, z).
797 ///
798 /// One entry per bone. Transforms from bone space to model space.
799 /// Data pointed to by CExoArrayList at skin extra +0x28.
800 pub tbone_ref_inv: Vec<[f32; 3]>,
801 /// Bone constant indices mapping local bone index to global skeleton node index.
802 ///
803 /// Data pointed to by CExoArrayList at skin extra +0x34.
804 pub bone_constant_indices: Vec<i32>,
805 /// Fixed-size array of 16 bone node serial numbers (u16). Skin extra +0x40.
806 ///
807 /// Maps up to 16 local bone slots to node indices in the model hierarchy.
808 /// Unused slots are zero.
809 pub bone_node_numbers: [u16; 16],
810}
811
812impl Default for MdlSkin {
813 /// Defaults with MDX bone offsets set to -1 (not present).
814 fn default() -> Self {
815 Self {
816 mesh: MdlMesh::default(),
817 mdx_bone_weights_offset: -1,
818 mdx_bone_indices_offset: -1,
819 bone_weights: Vec::new(),
820 bone_indices: Vec::new(),
821 bonemap: Vec::new(),
822 qbone_ref_inv: Vec::new(),
823 tbone_ref_inv: Vec::new(),
824 bone_constant_indices: Vec::new(),
825 bone_node_numbers: [0u16; 16],
826 }
827 }
828}
829
830/// Animated mesh with per-frame vertex sets.
831///
832/// Extends [`MdlMesh`] with animated vertex positions and texture coordinates
833/// that vary over time. The `sample_period` controls the animation playback rate.
834///
835/// Binary layout: 56 extra bytes (0x38) after the 332-byte TriMesh header.
836/// Note: `ResetAnim` processes the extra data BEFORE calling `ResetTriMeshParts`
837/// (reversed order vs other subtypes).
838///
839/// Verified via `InputBinary::ResetAnim` (`0x004a0060`) and Ghidra struct
840/// `MdlNodeAnimMesh` (468 bytes total = 412 TriMesh + 56 extra).
841/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
842#[derive(Debug, Clone, Default, PartialEq)]
843pub struct MdlAnimMesh {
844 /// The base triangle mesh.
845 pub mesh: MdlMesh,
846 /// Animation sampling period (f32). AnimMesh extra +0x00.
847 pub sample_period: f32,
848 /// Animated vertex positions (Vector: x, y, z per entry).
849 ///
850 /// Data pointed to by CExoArrayList at anim extra +0x04.
851 pub anim_verts: Vec<[f32; 3]>,
852 /// Animated texture coordinates (Vector: u, v, w per entry).
853 ///
854 /// Data pointed to by CExoArrayList at anim extra +0x10.
855 pub anim_t_verts: Vec<[f32; 3]>,
856 // --- Runtime-only fields (always zero in authored files) ---
857 // These 6 fields have no ASCII parser names (confirmed via
858 // `MdlNodeAnimMesh::InternalParseField` at 0x0046a240).
859 // They are populated at runtime by `InternalGenVertices`.
860 // Preserved for roundtrip fidelity.
861 /// Runtime data pointer 1 (u32, relocated if `data_count_1 != 0`). Extra +0x1C.
862 pub data_ptr_1: u32,
863 /// Size guard for `data_ptr_1` (u32). Extra +0x20.
864 pub data_count_1: u32,
865 /// Padding (u32). Extra +0x24.
866 pub padding_24: u32,
867 /// Runtime animated vertices pointer (u32). Extra +0x28.
868 pub anim_vertices_ptr: u32,
869 /// Runtime animated texture vertices pointer (u32). Extra +0x2C.
870 pub anim_tex_vertices_ptr: u32,
871 /// Count for `anim_vertices_ptr` (u32). Extra +0x30.
872 pub anim_vertices_count: u32,
873 /// Count for `anim_tex_vertices_ptr` (u32). Extra +0x34.
874 pub anim_tex_vertices_count: u32,
875}
876
877/// Dangly mesh with physics constraints.
878///
879/// Extends [`MdlMesh`] with per-vertex constraint weights and physics parameters
880/// that control dangling geometry behavior (hair, cloaks, banners, etc.).
881///
882/// Binary layout: 28 extra bytes (0x1C) after the 332-byte TriMesh header.
883/// Verified via `InputBinary::ResetDangly` (`0x004a0100`) and Ghidra struct
884/// `MdlNodeDanglyMesh` (440 bytes total = 412 TriMesh + 28 extra).
885/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
886#[derive(Debug, Clone, Default, PartialEq)]
887pub struct MdlDangly {
888 /// The base triangle mesh.
889 pub mesh: MdlMesh,
890 /// Per-vertex constraint weights (float array).
891 ///
892 /// Each value constrains how much a vertex can move (0.0 = fully fixed,
893 /// higher = more freedom). Array length matches `mesh.vertex_count`.
894 /// Data pointed to by CExoArrayList at dangly extra +0x00.
895 pub constraints: Vec<f32>,
896 /// Maximum displacement distance (f32). Dangly extra +0x0C.
897 pub displacement: f32,
898 /// Spring tightness factor (f32). Dangly extra +0x10.
899 pub tightness: f32,
900 /// Oscillation period (f32). Dangly extra +0x14.
901 pub period: f32,
902 /// Per-vertex positions for the dangly constraint system (vec3 array).
903 ///
904 /// Each entry is a 3-component position `[x, y, z]` for one constraint
905 /// vertex. Array length matches `mesh.vertex_count`. Pointed to by the
906 /// conditional data pointer at dangly extra +0x18 (only present when
907 /// `vertex_count > 0`).
908 ///
909 /// At runtime, the engine copies this array into a GL vertex pool via
910 /// `PartDanglyMesh` constructor (`0x00447980`): allocates
911 /// `vertex_count * 12` bytes, copies `vertex_count` vec3s from this
912 /// pointer, then uploads to GL.
913 pub dangly_vertices: Vec<[f32; 3]>,
914}
915
916/// AABB walkmesh tree node.
917///
918/// Extends [`MdlMesh`] with a bounding-volume hierarchy used for
919/// walkmesh collision detection. The tree is stored inline in the MDL
920/// content blob as a flattened binary tree of axis-aligned bounding boxes.
921///
922/// Binary layout: 4 extra bytes after the 332-byte TriMesh header.
923/// The single field is a pointer to the root AABB tree node.
924///
925/// Verified via `ResetMdlNode` inline processing and `ResetAABBTree`
926/// (`0x004a0260`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
927#[derive(Debug, Clone, Default, PartialEq)]
928pub struct MdlAabb {
929 /// The base triangle mesh.
930 pub mesh: MdlMesh,
931 /// Parsed AABB binary search tree. `None` if the root pointer was null.
932 pub aabb_tree: Option<Box<AabbNode>>,
933}
934
935/// A node in the AABB binary search tree used for spatial face queries.
936///
937/// Each node occupies 40 bytes on disk: 6×f32 bounding box, right/left child
938/// pointers, face index, and split direction flags. Internal nodes partition
939/// space along an axis (encoded in [`split_direction_flags`](AabbNode::split_direction_flags))
940/// and have two children. Leaf nodes reference a single face by index.
941///
942/// Binary layout (40 bytes):
943///
944/// | Offset | Size | Field |
945/// |--------|------|-------|
946/// | +0x00 | 12 | `box_min` (3×f32) |
947/// | +0x0C | 12 | `box_max` (3×f32) |
948/// | +0x18 | 4 | `right_child` (u32, content-relative offset, 0 = leaf) |
949/// | +0x1C | 4 | `left_child` (u32, content-relative offset, 0 = leaf) |
950/// | +0x20 | 4 | `face_index` (i32, -1 for internal nodes) |
951/// | +0x24 | 4 | `split_direction_flags` (u32, axis bitmask) |
952///
953/// Verified via Ghidra AABB struct (size 40), `operator_new(0x28)` in
954/// `Parse_AABB`, and `ResetAABBTree` (`0x004a0260`).
955/// See `docs/notes/mdl_mdx.md` §AABB Tree Node Layout.
956#[derive(Debug, Clone, PartialEq)]
957pub struct AabbNode {
958 /// Axis-aligned bounding box minimum corner.
959 pub box_min: [f32; 3],
960 /// Axis-aligned bounding box maximum corner.
961 pub box_max: [f32; 3],
962 /// Face index for leaf nodes (>= 0). Internal nodes use -1.
963 pub face_index: i32,
964 /// Split plane direction flags (internal nodes only, 0 for leaves).
965 ///
966 /// Bitmask encoding which axis the split occurred on:
967 /// 1=+X, 2=+Y, 4=+Z, 8=-X, 16=-Y, 32=-Z.
968 pub split_direction_flags: u32,
969 /// Left child subtree (`None` for leaf nodes).
970 pub left: Option<Box<AabbNode>>,
971 /// Right child subtree (`None` for leaf nodes).
972 pub right: Option<Box<AabbNode>>,
973}
974
975/// Lightsaber blade mesh.
976///
977/// Extends [`MdlMesh`] with saber-specific geometry data for 176 fixed
978/// vertices. All lightsaber models use exactly `NUM_SABER_VERTS` (176)
979/// vertices for blade geometry.
980///
981/// Binary layout: 20 extra bytes (0x14) after the 332-byte TriMesh header.
982/// Contains 3 relocated data pointers for saber vertex data and 2 runtime
983/// GL render pool IDs (allocated by `GLRender::RequestPool` at load time,
984/// not meaningful in the binary file).
985///
986/// Semantic names from kotorblender: `off_saber_verts`, `off_saber_uv`,
987/// `off_saber_normals`, `inv_count1`, `inv_count2`.
988///
989/// Verified via `InputBinary::ResetLightsaber` (`0x004a0460`) and
990/// `ParseNode` allocation (`operator_new(0x1B0)`).
991/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
992#[derive(Debug, Clone, Default, PartialEq)]
993pub struct MdlSaber {
994 /// The base triangle mesh.
995 pub mesh: MdlMesh,
996 /// Saber vertex positions (176 × vec3). Saber extra +0x00 pointer.
997 pub saber_verts: Vec<[f32; 3]>,
998 /// Saber texture coordinates (176 × vec2). Saber extra +0x04 pointer.
999 pub saber_uvs: Vec<[f32; 2]>,
1000 /// Saber vertex normals (176 × vec3). Saber extra +0x08 pointer.
1001 pub saber_normals: Vec<[f32; 3]>,
1002 /// GL vertex pool ID (runtime-only, stale in binary). Saber extra +0x0C.
1003 pub gl_pool_vert: u32,
1004 /// GL index pool ID (runtime-only, stale in binary). Saber extra +0x10.
1005 pub gl_pool_index: u32,
1006}
1007
1008/// Cross product of two 3D vectors in f32.
1009fn cross_f32(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
1010 [
1011 a[1] * b[2] - a[2] * b[1],
1012 a[2] * b[0] - a[0] * b[2],
1013 a[0] * b[1] - a[1] * b[0],
1014 ]
1015}
1016
1017/// Cross product of two f32 vectors computed in f64 precision.
1018fn cross_f32_as_f64(a: [f32; 3], b: [f32; 3]) -> [f64; 3] {
1019 [
1020 f64::from(a[1]) * f64::from(b[2]) - f64::from(a[2]) * f64::from(b[1]),
1021 f64::from(a[2]) * f64::from(b[0]) - f64::from(a[0]) * f64::from(b[2]),
1022 f64::from(a[0]) * f64::from(b[1]) - f64::from(a[1]) * f64::from(b[0]),
1023 ]
1024}