Files
parkan-playground/ParkanPlayground/CAniMesh.md
T
2026-05-14 17:18:38 +03:00

8.6 KiB

CAniMesh / MSH Loading and Joint Bounds — Key Summary

New findings discovered in this chat

  • tag is best understood as a runtime loaded submodel tag for pieces created from one .msh load call.
  • tag == 0 is the primary/root .msh model.
  • tag != 0 means an attached/additional .msh whose source node 0 is skipped and used as a virtual attach root.
  • One CAniMesh can aggregate multiple .msh resources into a single flat pieces_vector.
  • Attached .msh pieces are not kept as separate models at runtime; their nodes are remapped into absolute CAniMesh::pieces_vector indices.
  • attach_parent_absolute_piece_index is an absolute index in CAniMesh::pieces_vector, not a local index inside the attached .msh.
  • The first runtime piece created by each .msh load should be marked as a submodel/subtree root.
  • MSH_PIECE_FLAG_TAG_ROOT should be renamed to MSH_PIECE_FLAG_SUBMODEL_ROOT or MSH_PIECE_FLAG_LOADED_SUBTREE_ROOT.
  • ComputeJointBoundingBox is the authoritative recursive joint/subtree bounds function.
  • ComputeJointBoundingSphere uses specialized fast paths for single-piece and root whole-mesh bounds, but for non-root subtree bounds it delegates to ComputeJointBoundingBox and wraps the resulting AABB in a sphere.
  • Geometry-less pieces are valid helper/socket joints: their bounds become a point or zero-radius sphere at the joint transform origin.
  • The renamed filter g_mesh_filter_only_subtree_and_exclude_default_bounds clarifies that cached “default filter” bounds are really cached bounds for a specific reduced subtree filter.

Key function: CAniMesh::AppendMshResourcePieces (AniMesh.dll/sub_1000ac70)

typedef struct GmsgAppendResourcePayload_CAniMesh {
    char archive_name[32];
    char msh_archive_entry_name[32];
    uint msh_tag;
    uint attach_parent_absolute_piece_index;
    uint material_id_hi;
} GmsgAppendResourcePayload_CAniMesh;

Core behavior:

One call loads one .msh resource.
All pieces created by that call receive the same tag.
The created pieces are appended to CAniMesh::pieces_vector.

For tag == 0:

source MSH 0x01 node 0 -> piece[0]
source MSH 0x01 node 1 -> piece[1]
source MSH 0x01 node 2 -> piece[2]
...

For tag != 0:

source MSH 0x01 node 0 is skipped
source MSH 0x01 node 1 -> first newly created piece
source MSH 0x01 node 2 -> next newly created piece
...

Attached parent remap rule:

source parent == 0
    -> attach_parent_absolute_piece_index

source parent > 0
    -> first_new_piece_index + (source_parent - 1)

source parent == 0xFFFF
    -> -1 / no parent

This means internal parent hierarchy inside the attached .msh is preserved, but all indices are converted to absolute pieces_vector indices.


Runtime piece flag:

#define MSH_PIECE_FLAG_SUBMODEL_ROOT 0x01000000

Meaning:

Set on the first runtime piece created by one .msh load call.
For tag == 0, this is the main model root.
For tag != 0, this is the attached submodel/subtree root.

This function creates a runtime representation of a .msh 0x01 piece

typedef struct MSH_piece {
    uint msh_tag_0x00;
    int local_parent_index_base;
    uint msh0x01_node_index;
    undefined4 field_12;
    undefined4 material_id;
    EMshPieceFlags flags;
    int parent_piece_index;
    EMeshPieceState state;
    Matrix4x4 world_pose_matrix;
    Matrix4x4 mesh_space_pose_matrix;
    Matrix4x4 local_pose_matrix;
    Quaternion orientation_blend_start_quat;
    Quaternion orientation_blend_end_quat;
    float anim1_time_start;
    float anim1_time_target;
    float anim2_time_start;
    float anim2_time_target;
    bool is_pose_cache_valid;
    bool exclude_from_pose_update_order;
    bool uses_local_anim_blend;
    undefined1 has_orientation_blend;
    float local_anim_transition_progress;
    float local_anim_blend_factor;
    float cached_anim1_sample_time;
    float cached_anim2_sample_time;
    float render_phase_0x124??;
    undefined4 material_phase_0x128??;
    MSH_Reader * msh_reader;
} MSH_piece;

Key type: MSH_0x01_node

MSH component 0x01 is a source node / piece table.

Important fields:

typedef struct MSH_0x01_node {
    uint16_t flags;
    uint16_t parent_index_or_link;
    uint16_t anim_map_start_0x13;
    uint16_t fallback_key_0x08;
    uint16_t msh02_slot_indices_by_state_and_lod[3][5];
} MSH_0x01_node;

Meaning:

MSH 0x01 node
    -> becomes an MSH_piece at runtime
    -> has parent_index_or_link
    -> has MSH01 flags
    -> maps LOD/state to MSH 0x02 geometry slots

Key type: MSH_02_geometry_slot

MSH component 0x02 stores geometry slot metadata and local bounds.

Preferred structure:

typedef struct MSH_02_geometry_slot {
    uint16_t tri_start_0x07;
    uint16_t tri_count_0x07;
    uint16_t batch_start_0x0d;
    uint16_t batch_count_0x0d;

    Vector3 local_minimum;
    Vector3 local_maximum;
    Sphere bounding_sphere;

    float base_xy_area;
    float base_volume;

    uint32_t opaque_0x38;
    uint32_t opaque_0x3C;
    uint32_t opaque_0x40;
} MSH_02_geometry_slot;

Key function: ResolveMsh0x02SlotBy_LOD_and_state

The bounds functions call it as default geometry lookup:

MSH_02_geometry_slot *
ResolveMsh0x02SlotBy_LOD_and_state(
    MSH_piece *this,
    EMeshPieceLodLevel lod_level,
    EMeshPieceState state
);

typedef enum EMeshPieceLodLevel {
    LOD_LEVEL_MAX_0 = 0,
    LOD_LEVEL_MINUS_1 = 1,
    LOD_LEVEL_MINUS_2 = 2,
    LOD_LEVEL_MINUS_3 = 3,
    LOD_LEVEL_MINUS_4 = 4,
} EMeshPieceLodLevel;

typedef enum EMeshPieceState {
    MODEL_STATE_DEFAULT = -1,
    MODEL_STATE_REGULAR = 0,
    MODEL_STATE_COLLAPSED = 1,
    _MODEL_STATE_UNKNOWN_2 = 2,
} EMeshPieceState;

Key function: IJointMesh_of_AniMesh::ComputeJointBoundingBox

Preferred name:

AniMesh_IJointMesh::ComputeJointBoundingBox

Core behavior:

1. Read the joint/piece placement matrix in requested space.
2. Resolve default geometry slot for the queried piece.
3. If the piece has geometry:
   - build local box from slot local_minimum/local_maximum;
   - optionally scale by mesh_scale;
   - transform all corners by the joint matrix.
4. If the piece has no geometry:
   - create a degenerate box at joint transform origin.
5. If scope is single-piece, return.
6. If queried piece is root piece 0, return cached whole-mesh bounds.
7. Otherwise recursively include matching children, controlled by JointBoundsFilter and MSH01 flags.

Important meaning:

This is the main recursive piece-tree bounds function.

Geometry-less piece meaning:

No MSH 0x02 slot
    -> helper/socket joint
    -> point-sized bounds at joint transform origin

Key function: IJointMesh_of_AniMesh::ComputeJointBoundingSphere

Preferred name:

AniMesh_IJointMesh::ComputeJointBoundingSphere

Core behavior:

If scope != SINGLE_PIECE and piece != 0:
    ComputeJointBoundingBox(...)
    Convert resulting AABB to center/radius sphere.

If scope != SINGLE_PIECE and piece == 0:
    Use cached whole-mesh or cached filtered mesh sphere.

If scope == SINGLE_PIECE:
    Use MSH_02_geometry_slot::bounding_sphere.
    If no geometry slot, return zero-radius sphere at joint origin.

Important meaning:

Subtree sphere is not a tight recursive sphere.
It is an AABB-derived sphere from ComputeJointBoundingBox.

Important conceptual model

CAniMesh
    owns one flat pieces_vector

Each loaded .msh
    contributes one tagged group of pieces

MSH 0x01
    source node hierarchy inside one .msh

MSH_piece
    runtime node/piece inside CAniMesh::pieces_vector

parent_piece_index
    absolute runtime parent index in CAniMesh::pieces_vector

msh_tag
    tells which loaded .msh/submodel this runtime piece came from

Runtime result:

Multiple .msh files become one combined piece tree.
The tag preserves source submodel grouping.
The parent indices define the actual runtime hierarchy.

Example runtime structure

CAniMesh pieces_vector

piece[0] body_root                     tag = 0, SUBMODEL_ROOT
├─ piece[1] left_track                 tag = 0
├─ piece[2] right_track                tag = 0
└─ piece[3] turret_socket              tag = 0
   └─ piece[4] turret_base             tag = 1, SUBMODEL_ROOT
      └─ piece[5] turret_rotor         tag = 1
         ├─ piece[6] cannon_socket     tag = 1
         │  └─ piece[8] cannon_body    tag = 2, SUBMODEL_ROOT
         └─ piece[7] rocket_socket     tag = 1
            └─ piece[9] launcher_body  tag = 3, SUBMODEL_ROOT

Key rule:

tag groups pieces by loaded .msh.
parent_piece_index builds the actual hierarchy.