M2: Difference between revisions

From wowdev
Jump to navigation Jump to search
m (→‎M2ParticleOld: mapped 0.10.0 MDX fields to 0.11.0 & 1.12.1 files and normalised names; feel free to revert)
(40 intermediate revisions by 8 users not shown)
Line 18: Line 18:
               uint32_t flag_tilt_y : 1;
               uint32_t flag_tilt_y : 1;
               uint32_t : 1;
               uint32_t : 1;
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=3}}                                              // TODO: verify version
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=2}}                                              // TODO: verify version
               uint32_t flag_use_texture_combiner_combos : 1;      // add textureCombinerCombos array to end of data
               uint32_t flag_use_texture_combiner_combos : 1;      // add textureCombinerCombos array to end of data
               uint32_t : 1;
               uint32_t : 1;
Line 61: Line 61:
             {{Template:Type/M2Array|M2SkinProfile|section=Views_.28LOD.29}} skin_profiles;
             {{Template:Type/M2Array|M2SkinProfile|section=Views_.28LOD.29}} skin_profiles;
  #else
  #else
  /*0x040*/  uint32_t num_skin_profiles;                          // [[#Views_.28LOD.29|Views (LOD)]] are now in [[M2/.skin|.skins]].
  /*0x044*/  uint32_t num_skin_profiles;                          // [[#Views_.28LOD.29|Views (LOD)]] are now in [[M2/.skin|.skins]].
  #endif
  #endif
            
            
Line 96: Line 96:
  /*0x128*/  {{Template:Type/M2Array|M2Particle|section=Particle_emitters}} particle_emitters;
  /*0x128*/  {{Template:Type/M2Array|M2Particle|section=Particle_emitters}} particle_emitters;
            
            
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=3}}                                                      // TODO: verify version
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=2}}                                                      // TODO: verify version
             if (flag_use_texture_combiner_combos)
             if (flag_use_texture_combiner_combos)
             {
             {
Line 140: Line 140:
! width="70" | Version !! width="190" | Extension
! width="70" | Version !! width="190" | Extension
|-
|-
| 274-? || Legion
| 272-274 || Legion
|-
|-
| ?-? || Warlords of Draenor
| 272 || Warlords of Draenor
|-
|-
| 272 || Mists of Pandaria
| 272 || Mists of Pandaria
Line 152: Line 152:
| 260-263 || The Burning Crusade
| 260-263 || The Burning Crusade
|-
|-
| ?-256 || Classic
| 256-257 || Classic
|-
| 256 || Pre-Release
|}
|}


Line 161: Line 163:


'''NOTE''': Unlike all other chunked formats in WoW, chunk names in M2 are '''NOT''' reversed. Example: AFID == AFID in file.
'''NOTE''': Unlike all other chunked formats in WoW, chunk names in M2 are '''NOT''' reversed. Example: AFID == AFID in file.
===PFID (Legion+)===
 
===PFID===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.0.1.20740}}
  uint32_t phys_file_id;
  uint32_t phys_file_id;
===SFID (Legion+)===
 
===SFID===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.0.1.20740}}
  uint32_t skinFileDataIDs[header.nViews];
  uint32_t skinFileDataIDs[header.nViews];
  uint32_t lod_skinFileDataIDs[lodBands /* 2? */];
  uint32_t lod_skinFileDataIDs[lodBands /* 2? */];
Line 170: Line 176:
Lod skins are selected based on distance to entity/doodad and chosen based on GetCVar("entityLodDist")/X and GetCVar("doodadLodDist")/X where X - distance. Lods are ignored when "M2UseLOD" CVar is set to 0.
Lod skins are selected based on distance to entity/doodad and chosen based on GetCVar("entityLodDist")/X and GetCVar("doodadLodDist")/X where X - distance. Lods are ignored when "M2UseLOD" CVar is set to 0.


===AFID (Legion+)===
===AFID===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.0.1.20740}}
  struct
  struct
  {
  {
Line 178: Line 185:
  } anim_file_ids[];
  } anim_file_ids[];


===BFID (Legion+)===
===BFID===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.0.1.20740}}
  uint32_t boneFileDataIDs[];
  uint32_t boneFileDataIDs[];
===MD21 (Legion+)===
 
The MD21 chunk contains the not yet chunked data, just like in the old format. The content literally is an pre legion file.
===MD21===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.0.1.20740}}
The MD20 chunk contains the not yet chunked data, just like in the old format. The content literally is an pre legion file.
  M2Data pre_legion_style_data;
  M2Data pre_legion_style_data;


===TXAC (Legion+)===
===TXAC===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|note={{Unverified|Exact build unknown, not the first one though}}}}
  struct {
  struct {
   char unk[2]; // likely used in CM2SceneRender::SetupTextureTransforms and uploaded to the shader directly. 0 otherwise.
   char unk[2]; // likely used in CM2SceneRender::SetupTextureTransforms and uploaded to the shader directly. 0 otherwise.
  } texture_ac[m2data.header.materials.count + m2data.header.particles.count];
  } texture_ac[m2data.header.materials.count + m2data.header.particles.count];
===EXPT (Legion+)===
 
===EXPT===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|note={{Unverified|Exact build unknown, not the first one though}}}}
  struct {
  struct {
  char unk[0xC];
  _DWORD zSource;
  _DWORD unk1;
  _DWORD unk2;
  } extended_particle[m2data.header.particles.count];
  } extended_particle[m2data.header.particles.count];
===EXP2 (Legion+)===
 
(7.3+)
Probably outdated chunk after introduction of EXP2 chunk. If EXP2 doesnt exist, client tries to reconstruct it with data from EXPT chunk.
struct {
 
  C3Vector _0x00;
===EXP2===
  uint16_t _0x0c_base;
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.3.???|note={{Unverified|Exact build unknown}}}}
  uint16_t _0x0e_vary;
 
} extended_particle2[m2data.header.particles.count];
<source>
===PABC (Legion+)===
struct EXP2
(7.3+)
{
===PADC (Legion+)===
  M2Array<Exp2Record> content;
(7.3+)
};
===PSBC (Legion+)===
 
(7.3+)
struct Exp2Record
===PEDC (Legion+)===
{
(7.3+)
  _DWORD zSource;
===SKID (Legion+)===
  _DWORD unk1;
(7.3+)
  _DWORD unk2;
  M2Array<> unk3; //Record size is 2
  M2Array<> unk5; //Record size is 2
};
</source>
 
 
The length of this M2Array is the same as length of particle_emitters
 
===PABC===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.3.???|note={{Unverified|Exact build unknown}}}}
M2Array<uint16t> m_replacementParentSequenceLookups; // only seen in quillboarbrute*.m2. Contains AnimationIds
 
Replaces parentSequencesLookups. But unlike header.sequence_lookups of parent model, this is straight array and not a map. If index with target animation is not found in here, parentSequencesLookups are used instead.
 
Client doesnt seem to use found index and thus whole array is used only to check if the target animation is present.
 
===PADC===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.3.???|note={{Unverified|Exact build unknown}}}}
Defines replacement for header.texture_weights (WHY?)
 
<source lang="cpp">
struct PADC {
    M2Array<M2TextureWeight> texture_weights;
}
</source>
 
===PSBC===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.3.???|note={{Unverified|Exact build unknown}}}}
 
Defines ParentSequenceBounds
M2Array<M2Bounds> parentSequenceBounds;
 
===PEDC===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.3.???|note={{Unverified|Exact build unknown}}}}
M2Array<M2TrackBase> parentEventData;
 
===SKID===
{{Template:SectionBox/VersionRange|min_expansionlevel=7|min_build=7.3.???|note={{Unverified|Exact build unknown}}}}
  uint32_t SKeletonfileID;    // links to [[M2/.skel]]
  uint32_t SKeletonfileID;    // links to [[M2/.skel]]
===TXID (BfA+)===
 
===TXID===
{{Template:SectionBox/VersionRange|min_expansionlevel=8|min_build=8.0.1.26629}}
{{Template:SectionBox/VersionRange|min_expansionlevel=8|min_build=8.0.1.26629}}
Replaces in-file texture filenames.  
Replaces in-file texture filenames.  
Line 217: Line 271:
  } textureID[]
  } textureID[]


===LDV1 (BfA+)===
===LDV1===
{{Template:SectionBox/VersionRange|min_expansionlevel=8|min_build=8.0.1.26629}}
{{Template:SectionBox/VersionRange|min_expansionlevel=8|min_build=8.0.1.26629}}


Unknown use.
Defines LodData
 
<source lang="cpp">
struct LodData
{
  uint16 unk0;
  uint16 lodCount; //maxLod = lodCount-1; 
  float unk2_f;
  uint8_t particleRelatedArray[4];
  _DWORD unk4;
};
</source>
 
Somehow defines _lod%0d.skin files. On pandarenfemale.m2, lodCount == 4. SFID has 7 files first 4 are ordinary .skin files and last 3 are _lod%0d.skin files. Enumeration for _lod%0d.skin files for that model starts from 1, and last file in SFID is pandarenfemale_lod03.skin So technically maxLod indeed represents maximum Lod
 
unk2_f is used in formula, but it's purpose is unknown
fmaxf(fminf(740.0 / unk2_f, 5.0), 0.5);
 
[[User:Deamon|Deamon]] ([[User talk:Deamon|talk]])
 
===RPID===
{{Template:SectionBox/VersionRange|min_expansionlevel=8|min_build=8.1.0.27826}}
 
struct {
  uint32_t fileDataID;
} recursive_particle_models[];
 
===GPID===
{{Template:SectionBox/VersionRange|min_expansionlevel=8|min_build=8.1.0.27826}}
 
struct {
  uint32_t fileDataID;
} geometry_particle_models[];


=Skeleton and animation=
=Skeleton and animation=
Line 312: Line 398:
* If the interpolation type is 3, then cubic hermite spline interpolation is used.  This is only valid for M2SplineKey tracks.  When interpolating between two spline keys, the starting point is the first spline key's <code>value</code>, the starting tangent is the first spline key's <code>tanOut</code>, the ending tangent is the second spline key's <code>tanIn</code>, and the ending point is the second spline key's <code>value</code>.
* If the interpolation type is 3, then cubic hermite spline interpolation is used.  This is only valid for M2SplineKey tracks.  When interpolating between two spline keys, the starting point is the first spline key's <code>value</code>, the starting tangent is the first spline key's <code>tanOut</code>, the ending tangent is the second spline key's <code>tanIn</code>, and the ending point is the second spline key's <code>value</code>.


'''NOTE:''' There is confusion about type <code>2</code> and <code>3</code> being <code>hermite/bezier</code> or <code>bezier/hermite</code>. Alpha says that <code>2 = hermite</code>, WoD says that <code>2 = bezier</code>. It appears to have either changed when it went from MDL to M2, or sometime after. When this actually happened is currently an open question. --[[User:Schlumpf|Schlumpf]] ([[User talk:Schlumpf|talk]]) 01:53, 4 September 2017 (CEST)
'''NOTE:''' There is confusion about type <code>2</code> and <code>3</code> being <code>hermite/bezier</code> or <code>bezier/hermite</code>. Alpha says that <code>2 = hermite</code>, WoD says that <code>2 = bezier</code>. This was changed when the format went from MDL to M2. --[[User:Schlumpf|Schlumpf]] ([[User talk:Schlumpf|talk]]) 01:53, 4 September 2017 (CEST), --[[User:Barncastle|Barncastle]] ([[User talk:Barncastle|talk]]) 19:15, 14 November 2018 (GMT)
 
In WotLK 2 appears to be bezier, which pretty much confirms it being bezier for wotlk+. TBC and classic need further checking though. -- [[User:Skarn|Skarn]] ([[User talk:Skarn|talk]]) 01:38, 16 June 2018 (CEST)


==.anim files==
==.anim files==
Line 578: Line 666:
| 0x01 || Unlit
| 0x01 || Unlit
|-
|-
| 0x02 || Unfogged?
| 0x02 || Unfogged
|-
|-
| 0x04 || Two-sided (no backface culling if set)
| 0x04 || Two-sided (no backface culling if set)
Line 862: Line 950:
   {{Template:Type|fixed_point}}<uint8_t, 2, 5> multiTextureParamX[2];
   {{Template:Type|fixed_point}}<uint8_t, 2, 5> multiTextureParamX[2];
  #else
  #else
   uint8 particleType;                     // Found below.
   uint8 particleType;                       // Found below.
   uint8 headorTail;                        // 0 - Head, 1 - Tail, 2 - Both  
   uint8 headorTail;                        // 0 - Head, 1 - Tail, 2 - Both  
  #endif
  #endif
Line 870: Line 958:
   [[#Standard_animation_block|M2Track]]<float> emissionSpeed;            // Base velocity at which particles are emitted.
   [[#Standard_animation_block|M2Track]]<float> emissionSpeed;            // Base velocity at which particles are emitted.
   [[#Standard_animation_block|M2Track]]<float> speedVariation;            // Random variation in particle emission speed. (range: 0 to 1)
   [[#Standard_animation_block|M2Track]]<float> speedVariation;            // Random variation in particle emission speed. (range: 0 to 1)
   [[#Standard_animation_block|M2Track]]<float> verticalRange;            // Drifting away vertically. (range: 0 to pi) For plane generators, this is the maximum polar angle of the initial velocity;  
   [[#Standard_animation_block|M2Track]]<float> verticalRange;            // longitude; Drifting away vertically. (range: 0 to pi) For plane generators, this is the maximum polar angle of the initial velocity;  
                                             // 0 makes the velocity straight up (+z). For sphere generators, this is the maximum elevation of the initial position;  
                                             // 0 makes the velocity straight up (+z). For sphere generators, this is the maximum elevation of the initial position;  
                                             // 0 makes the initial position entirely in the x-y plane (z=0).
                                             // 0 makes the initial position entirely in the x-y plane (z=0).
   [[#Standard_animation_block|M2Track]]<float> horizontalRange;          // They can do it horizontally too! (range: 0 to 2*pi) For plane generators, this is the maximum azimuth angle of the initial velocity;  
   [[#Standard_animation_block|M2Track]]<float> horizontalRange;          // latitude; They can do it horizontally too! (range: 0 to 2*pi) For plane generators, this is the maximum azimuth angle of the initial velocity;  
                                             // 0 makes the velocity have no sideways (y-axis) component.  
                                             // 0 makes the velocity have no sideways (y-axis) component.  
                                             // For sphere generators, this is the maximum azimuth angle of the initial position.
                                             // For sphere generators, this is the maximum azimuth angle of the initial position.
Line 898: Line 986:
   [[#The_Fake-AnimationBlock|FBlock]]<uint16> tailCellTrack;
   [[#The_Fake-AnimationBlock|FBlock]]<uint16> tailCellTrack;
  #else
  #else
   float midPoint; // Middle point in lifespan (0 to 1).
   float midPoint;                           // middleTime; Middle point in lifespan (0 to 1).
   byte[4][3] colorValues; // 3*BGRA
   {{Type|CImVector}}[3] colorValues;               // start, middle, end
   float[3] scaleValues;
   float[3] scaleValues;
   uint16[2] headCellBegin;
   uint16[3] lifespanUVAnim;
   uint16 between1; // Always 1
   uint16[3] decayUVAnim;
   uint16[2] headCellEnd;
   int16[2] tailUVAnim;                     // start, end
  uint16 between1; // Always 1
   int16[2] tailDecayUVAnim;
   short[4] tiles; // Indices into the tiles on the texture ? Or tailCell maybe ?
  #endif
  #endif
   float tailLength;                         // TailCellTime?
   float tailLength;
   float TwinkleSpeed;                      // has something to do with the spread
   float twinkleSpeed;                      // twinkleFPS; has something to do with the spread
   float TwinklePercent;                    // has something to do with the spread
   float twinklePercent;                    // same mechanic as MDL twinkleOnOff but non-binary in 0.11.0
   {{Template:Type|CRange}} twinkleScale;
   {{Template:Type|CRange}} twinkleScale;
   float BurstMultiplier;                    // ivelScale
   float burstMultiplier;                    // ivelScale
   float drag;                              // For a non-zero values, instead of travelling linearly the particles seem to slow down sooner. Speed is multiplied by exp( -drag * t ).
   float drag;                              // For a non-zero values, instead of travelling linearly the particles seem to slow down sooner. Speed is multiplied by exp( -drag * t ).
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=3}}
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=3}}
   float baseSpin;                          // Initial rotation of the particle quad
   float baseSpin;                          // Initial rotation of the particle quad
   float baseSpinVary;
   float baseSpinVary;
   float Spin;                              // Rotation of the particle quad per second
   float spin;                              // Rotation of the particle quad per second
   float spinVary;
   float spinVary;
  #else
  #else
Line 923: Line 1,010:
   
   
   [[#M2Box|M2Box]] tumble;
   [[#M2Box|M2Box]] tumble;
   {{Template:Type|C3Vector}} WindVector;
   {{Template:Type|C3Vector}} windVector;
   float WindTime;
   float windTime;
   
   
   float followSpeed1;
   float followSpeed1;
Line 930: Line 1,017:
   float followSpeed2;
   float followSpeed2;
   float followScale2;
   float followScale2;
   M2Array<C3Vector> splinePoints;                                  //Set only for spline praticle emitter. Contains array of points for spline
   M2Array<C3Vector> splinePoints;                                  // Set only for spline praticle emitter. Contains array of points for spline
   [[#Standard_animation_block|M2Track]]<uchar> enabledIn;                // (boolean) Appears to be used sparely now, probably there's a flag that links particles to animation sets where they are enabled.
   [[#Standard_animation_block|M2Track]]<uchar> enabledIn;                // (boolean) Appears to be used sparely now, probably there's a flag that links particles to animation sets where they are enabled.
  } particles[];
  } particles[];
Line 982: Line 1,069:
| 0x1000 || XYQuad Particles. They align to XY axis facing Z axis direction.
| 0x1000 || XYQuad Particles. They align to XY axis facing Z axis direction.
|-
|-
| 0x2000 || clamp to ground
| 0x2000 || clamp to ground; call CParticleEmitter2::ProjectParticle
|-
|-
| 0x4000 ||  
| 0x4000 ||  
Line 1,008: Line 1,095:
| 0x10000000 || Particle uses multi-texturing (could be one of the other WoD-specific flags), see multi-textured section.
| 0x10000000 || Particle uses multi-texturing (could be one of the other WoD-specific flags), see multi-textured section.
|}
|}
--[[User:Barncastle|Barncastle]] ([[User talk:Barncastle|talk]]) A comparison of 0.10.0's MDX files to 0.11.0's and 1.12.1's M2 files indicates that the [[MDX#Flags|MDX PRE2 flags]] (≥0x8000) were probably unchanged during the switch from MDL to M2, albeit separated into their own enum.


===ParticleColorIndex===
===ParticleColorIndex===

Revision as of 16:37, 23 December 2018

M2 files (also called MDX) contain model objects. Each M2 file describes the vertices, faces, materials, texture names, animations and properties of one model. M2 files don't have a chunked format like most other WoW formats (except in Legion). Since it is chunked in Legion, all offsets are relative to beginning of the MD21 chunk's data rather than the beginning of the file.

Models are used for doodads (decoration objects), players, monsters and really everything in the game except for Terrain and WMOs.

Header

The header has mostly the layout of number-offset pairs, containing the number of a particular record in the file, and the offset. These appear at fixed places in the header. Record sizes are not specified in the file.

struct 
{
// note: Offsets are for ≥ Wrath!
/*0x000*/  uint32_t magic;                                       // "MD20". Legion uses a chunked file format starting with MD21.
/*0x004*/  uint32_t version;
/*0x008*/  M2Array<char> name;                                   // should be globally unique, used to reload by name in internal clients

/*0x010*/  struct
           {
             uint32_t flag_tilt_x : 1;
             uint32_t flag_tilt_y : 1;
             uint32_t : 1;
#if ≥ BC                                              // TODO: verify version
             uint32_t flag_use_texture_combiner_combos : 1;      // add textureCombinerCombos array to end of data
             uint32_t : 1;
#if ≥ Mists
             uint32_t flag_load_phys_data : 1;
             uint32_t : 1;
#if ≥ WoD
             uint32_t flag_unk_0x80 : 1;                         // with this flag unset, demon hunter tattoos stop glowing
                                                                 // since Cata (4.0.1.12911) every model now has this flag
             uint32_t flag_camera_related : 1;                   // TODO: verify version
#if ≥ Legion                                            // TODO: verify version, these are just added based on where I first saw them -- schlumpf.
             uint32_t flag_new_particle_record : 1;              // In CATA: new version of ParticleEmitters. By default, length of M2ParticleOld is 476. 
                                                                 // But if 0x200 is set or if version is bigger than 271, length of M2ParticleOld is 492.
             uint32_t flag_unk_0x400 : 1;
             uint32_t flag_texture_transforms_use_bone_sequences : 1; // ≥ WoD 0x800 -- When set, texture transforms are animated using the sequence being played on the bone found by index in tex_unit_lookup_table[textureTransformIndex], instead of using the sequence being played on the model's first bone. Example model: 6DU_HellfireRaid_FelSiege03_Creature
             uint32_t flag_unk_0x1000 : 1;
             uint32_t flag_unk_0x2000 : 1;                       // seen in various legion models
             uint32_t flag_unk_0x4000 : 1;
             uint32_t flag_unk_0x8000 : 1;                       // seen in UI_MainMenu_Legion
             uint32_t flag_unk_0x10000 : 1;
             uint32_t flag_unk_0x20000 : 1;
             uint32_t flag_unk_0x40000 : 1;
             uint32_t flag_unk_0x80000 : 1;
             uint32_t flag_unk_0x100000 : 1;
             uint32_t flag_unk_0x200000 : 1;                     // apparently: use 24500 upgraded model format: chunked .anim files, change in the exporter reordering sequence+bone blocks before name
#endif
#endif
#endif
#endif
           } global_flags;
        
/*0x014*/  M2Array<M2Loop> global_loops;                        // Timestamps used in global looping animations.
/*0x01C*/  M2Array<M2Sequence> sequences;                       // Information about the animations in the model.
/*0x024*/  M2Array<uint16_t> sequence_lookups;                  // Mapping of sequence IDs to the entries in the Animation sequences block.
#if ≤ BC
           M2Array<?> playable_animation_lookup;
#endif
/*0x02C*/  M2Array<M2CompBone> bones;                           // MAX_BONES = 0x100 => Creature\SlimeGiant\GiantSlime.M2 has 312 bones (Wrath)
/*0x034*/  M2Array<uint16_t> key_bone_lookup;                   // Lookup table for key skeletal bones.
/*0x03C*/  M2Array<M2Vertex> vertices;
#if ≤ BC
           M2Array<M2SkinProfile> skin_profiles;
#else
/*0x044*/  uint32_t num_skin_profiles;                           // Views (LOD) are now in .skins.
#endif
         
/*0x048*/  M2Array<M2Color> colors;                             // Color and alpha animations definitions.
/*0x050*/  M2Array<M2Texture> textures;
/*0x05C*/  M2Array<M2TextureWeight> texture_weights;            // Transparency of textures.
#if ≤ BC
           M2Array<?> unknown;
#endif
/*0x060*/  M2Array<M2TextureTransform> texture_transforms;
/*0x068*/  M2Array<uint16_t> replacable_texture_lookup;
/*0x070*/  M2Array<M2Material> materials;                       // Blending modes / render flags.
/*0x078*/  M2Array<uint16_t> bone_lookup_table;
/*0x080*/  M2Array<uint16_t> texture_lookup_table;
/*0x088*/  M2Array<uint16_t> tex_unit_lookup_table;             // ≥ Cata: unused
/*0x090*/  M2Array<uint16_t> transparency_lookup_table;
/*0x098*/  M2Array<uint16_t> texture_transforms_lookup_table;
         
/*0x0A0*/  CAaBox bounding_box;                                 // min/max( [1].z, 2.0277779f ) - 0.16f seems to be the maximum camera height
/*0x0B8*/  float bounding_sphere_radius;                         // detail doodad draw dist = clamp (bounding_sphere_radius * detailDoodadDensityFade * detailDoodadDist, …)
/*0x0BC*/  CAaBox collision_box;
/*0x0D4*/  float collision_sphere_radius;
         
/*0x0D8*/  M2Array<uint16_t> collision_triangles;
/*0x0E0*/  M2Array<C3Vector> collision_vertices;
/*0x0E8*/  M2Array<C3Vector> collision_normals;
/*0x0F0*/  M2Array<M2Attachment> attachments;                   // position of equipped weapons or effects
/*0x0F8*/  M2Array<uint16_t> attachment_lookup_table;
/*0x100*/  M2Array<M2Event> events;                             // Used for playing sounds when dying and a lot else.
/*0x108*/  M2Array<M2Light> lights;                             // Lights are mainly used in loginscreens but in wands and some doodads too.
/*0x110*/  M2Array<M2Camera> cameras;                           // The cameras are present in most models for having a model in the character tab. 
/*0x118*/  M2Array<uint16_t> camera_lookup_table;
/*0x120*/  M2Array<M2Ribbon> ribbon_emitters;                   // Things swirling around. See the CoT-entrance for light-trails.
/*0x128*/  M2Array<M2Particle> particle_emitters;
         
#if ≥ BC                                                       // TODO: verify version
           if (flag_use_texture_combiner_combos)
           {
/*0x130*/    M2Array<uint16_t> textureCombinerCombos;           // When set, textures blending is overriden by the associated array.
           }
#endif
} header;

Types

struct M2Bounds {
  CAaBox extent;
  float radius;
};
template<typename T>
struct M2Array {
  uint32_t size;
  uint32_t offset; // pointer to T, relative to begin of m2 data block (i.e. MD21 chunk content or begin of file)
};
struct M2TrackBase {
  uint16_t trackType;
  uint16_t loopIndex;
  M2Array<M2SequenceTimes> sequenceTimes;
};
template<typename T> 
struct M2PartTrack {
  M2Array<fixed16> times;
  M2Array<T> values;
};
template<typename T> 
struct M2SplineKey {
  T value;
  T inTan;
  T outTan;
};
struct M2Range {
  uint32_t minimum;
  uint32_t maximum;
};

Versions

Files get handled differently depending on this! Ranges are inclusive.

Version Extension
272-274 Legion
272 Warlords of Draenor
272 Mists of Pandaria
265-272 Cataclysm
264 Wrath of the Lich King
260-263 The Burning Crusade
256-257 Classic
256 Pre-Release

Chunks

This section only applies to versions ≥ Legion (7.0.1.20740).

From Legion and up, the file might be chunked instead. If this is the case, the magic will be anything but 'MD20' and the m2 data will be in the 'MD21' chunk. If the first magic is 'MD20', it will be loaded just fine like it did previously. Note that the chunks can be in any order with MD21 often being first.

NOTE: Unlike all other chunked formats in WoW, chunk names in M2 are NOT reversed. Example: AFID == AFID in file.

PFID

This section only applies to versions ≥ Legion (7.0.1.20740).
uint32_t phys_file_id;

SFID

This section only applies to versions ≥ Legion (7.0.1.20740).
uint32_t skinFileDataIDs[header.nViews];
uint32_t lod_skinFileDataIDs[lodBands /* 2? */];

Some model files, for example 'Creature\NightborneFemaleCitizen\NightborneFemaleCitizen.m2' have 4 skin files and 2 lod files but only 20 bytes are in chunk. In chunk there are 4 skins and 1 lod present.

Lod skins are selected based on distance to entity/doodad and chosen based on GetCVar("entityLodDist")/X and GetCVar("doodadLodDist")/X where X - distance. Lods are ignored when "M2UseLOD" CVar is set to 0.

AFID

This section only applies to versions ≥ Legion (7.0.1.20740).
struct
{
  uint16_t anim_id;
  uint16_t sub_anim_id;
  uint32_t file_id;        // might be 0 for "none" (so this is probably not sparse, even if it could be)
} anim_file_ids[];

BFID

This section only applies to versions ≥ Legion (7.0.1.20740).
uint32_t boneFileDataIDs[];

MD21

This section only applies to versions ≥ Legion (7.0.1.20740).

The MD20 chunk contains the not yet chunked data, just like in the old format. The content literally is an pre legion file.

M2Data pre_legion_style_data;

TXAC

This section only applies to versions ≥ LegionExact build unknown, not the first one though.
struct {
  char unk[2]; // likely used in CM2SceneRender::SetupTextureTransforms and uploaded to the shader directly. 0 otherwise.
} texture_ac[m2data.header.materials.count + m2data.header.particles.count];

EXPT

This section only applies to versions ≥ LegionExact build unknown, not the first one though.
struct {
 _DWORD zSource;
 _DWORD unk1;
 _DWORD unk2;
} extended_particle[m2data.header.particles.count];

Probably outdated chunk after introduction of EXP2 chunk. If EXP2 doesnt exist, client tries to reconstruct it with data from EXPT chunk.

EXP2

This section only applies to versions ≥ Legion (7.3.???). Exact build unknown.
struct EXP2
{
  M2Array<Exp2Record> content;
};

struct Exp2Record
{
  _DWORD zSource;
  _DWORD unk1;
  _DWORD unk2;
  M2Array<> unk3; //Record size is 2
  M2Array<> unk5; //Record size is 2
};


The length of this M2Array is the same as length of particle_emitters

PABC

This section only applies to versions ≥ Legion (7.3.???). Exact build unknown.
M2Array<uint16t> m_replacementParentSequenceLookups; // only seen in quillboarbrute*.m2. Contains AnimationIds

Replaces parentSequencesLookups. But unlike header.sequence_lookups of parent model, this is straight array and not a map. If index with target animation is not found in here, parentSequencesLookups are used instead.

Client doesnt seem to use found index and thus whole array is used only to check if the target animation is present.

PADC

This section only applies to versions ≥ Legion (7.3.???). Exact build unknown.

Defines replacement for header.texture_weights (WHY?)

struct PADC {
    M2Array<M2TextureWeight> texture_weights;
}

PSBC

This section only applies to versions ≥ Legion (7.3.???). Exact build unknown.

Defines ParentSequenceBounds

M2Array<M2Bounds> parentSequenceBounds;

PEDC

This section only applies to versions ≥ Legion (7.3.???). Exact build unknown.
M2Array<M2TrackBase> parentEventData;

SKID

This section only applies to versions ≥ Legion (7.3.???). Exact build unknown.
uint32_t SKeletonfileID;    // links to M2/.skel

TXID

This section only applies to versions ≥ Battle (8.0.1.26629).

Replaces in-file texture filenames.

struct {
  uint32_t fileDataID;
} textureID[]

LDV1

This section only applies to versions ≥ Battle (8.0.1.26629).

Defines LodData

struct LodData
{
  uint16 unk0; 
  uint16 lodCount; //maxLod = lodCount-1;  
  float unk2_f;
  uint8_t particleRelatedArray[4];
  _DWORD unk4;
};

Somehow defines _lod%0d.skin files. On pandarenfemale.m2, lodCount == 4. SFID has 7 files first 4 are ordinary .skin files and last 3 are _lod%0d.skin files. Enumeration for _lod%0d.skin files for that model starts from 1, and last file in SFID is pandarenfemale_lod03.skin So technically maxLod indeed represents maximum Lod

unk2_f is used in formula, but it's purpose is unknown

fmaxf(fminf(740.0 / unk2_f, 5.0), 0.5);

Deamon (talk)

RPID

This section only applies to versions ≥ Battle (8.1.0.27826).
struct {
  uint32_t fileDataID;
} recursive_particle_models[];

GPID

This section only applies to versions ≥ Battle (8.1.0.27826).
struct {
  uint32_t fileDataID;
} geometry_particle_models[];

Skeleton and animation

Global sequences

A list of timestamps that act as upper limits for global sequence ranges.

struct M2Loop
{
  uint32_t timestamp;
} loops[];

Standard animation block

  • Wrath uses a single-timeline approach, chaining all animations into one long piece and separating them via begin and end given in animation data. ≥ Wrath, each animation has an own timeline.
  • Animation blocks contain a list of lists of timestamps and a list of lists of values, where the first list is by animation and the second one by timestamp-entry.
  • Many values that change with time are specified using blocks like the following.
 template<typename T>
 struct M2Array
 {
 /*0x00*/  uint32_t number;
 /*0x04*/  uint32_t offset_elements;
 /*0x08*/
 };
 struct M2TrackBase
 {
 /*0x00*/  uint16_t interpolation_type;
 /*0x02*/  uint16_t global_sequence;
#if < Wrath
   M2Array<pair<uint32_t>> interpolation_ranges;   // no longer required ≥ Wrath, as implicit by minimum and maximum timestamp per sequence.
   M2Array<uint32_t> timestamps;
#else
 /*0x04*/  M2Array<M2Array<uint32_t>> timestamps;
#endif
 /*0x0C*/
 };
 
 template<typename T>
 struct M2Track : M2TrackBase
 {
 /*0x00*/  // base 
#if < Wrath 
   M2Array<T> values;
#else
 /*0x0C*/  M2Array<M2Array<T>> values;
#endif
 /*0x14*/
 };
  • Thus, as example, with
 M2CompBone b;

one may get the number of animations having translation information with

 b.translation.timestamps.number

and the number of timestamps in the first animation using

 b.translation.timestamps.elements[0].number

and the first timestamp value of the first animation via

 b.translation.timestamps.elements[0].elements[0]

The actual translation vector for animation 0 at timestamp 0 is at

 b.translation.values.elements[0].elements[0]
  • Some timestamps/values.elements entries may have number/elements = 0, if for that animation id no animation is given.
  • .anim files are just a blob of data which may as well be in the main model file, that is pointed to by the first array_ref layer.
  • [model file name][animation id]-[animation sub-id].anim
  • it seems like it is possible to detect if animation data is stored in-m2 or externally via
    • All animations which have flags & 0x20 are stored internally.
    • Animations which do not have flags & 0x20 are not stored internally.
    • Animations which do not have flags & 0x20 AND do not have flags & 0x40 are in .anim files
    • Animations which do not have flags & 0x20 AND DO have flags 0x40 are stored... somewhere. I have no clue.

Global Sequences

If a block has a sequence >= 0, the block has a completely separate max timestamp. This is the value in the model's ofsGlobalSequences table; index into that table with this sequence value and use that as the block's max timestamp. Blocks that use these global sequences also only have one track, so at the same time as clipping the current timestamp to the max time above, interpolated value should always be taken from track 0 in the block.

A global sequence is completely unrelated to animations. It just always loops. This way, the sequence is not interrupted when an animation is launched.

This appears to be frequently used by models that don't have more conventional animations (login screen animations, items/weapons with animated effects, etc).

-- Rour, additionally, these sequences can be longer or shorter than whatever animation is running for a given model, so I recommend taking a global scene timestamp and then clipping that value into the given max timestamp range. Otherwise animations will appear to reset when the regular animation loops, which is not good.

Interpolation

  • If the interpolation type is 0, then values will change instantly at the timestamp, with no interpolation whatsoever.
  • If the interpolation type is 1, then the block linearly interpolates between keyframe values (lerp for vectors/colours, nlerp for quaternions).
  • If the interpolation type is 2, then cubic bezier spline interpolation is used. This is only valid for M2SplineKey tracks. When interpolating between two spline keys, the first control point is the first spline key's value, the second control point is the first spline key's tanOut, the third control point is the second spline key's tanIn, and the fourth control point is the second spline key's value.
  • If the interpolation type is 3, then cubic hermite spline interpolation is used. This is only valid for M2SplineKey tracks. When interpolating between two spline keys, the starting point is the first spline key's value, the starting tangent is the first spline key's tanOut, the ending tangent is the second spline key's tanIn, and the ending point is the second spline key's value.

NOTE: There is confusion about type 2 and 3 being hermite/bezier or bezier/hermite. Alpha says that 2 = hermite, WoD says that 2 = bezier. This was changed when the format went from MDL to M2. --Schlumpf (talk) 01:53, 4 September 2017 (CEST), --Barncastle (talk) 19:15, 14 November 2018 (GMT)

In WotLK 2 appears to be bezier, which pretty much confirms it being bezier for wotlk+. TBC and classic need further checking though. -- Skarn (talk) 01:38, 16 June 2018 (CEST)

.anim files

This section only applies to versions ≥ Wrath.

Low priority sequences (e.g. emotes, one shot animations) are in extra files to allow for lazy loading. These files are raw data of timestamps and values for animation blocks. Blizzard's exporter prefers to align blocks to a 16 byte boundary, but that's not required.

The client loads .anim files if (M2Sequence.flags & 0x130 ) == 0. The .anim file to use is "%s%04d-%02d.anim" % (model_filename_without_extension, anim.id, anim.sub_anim_id).

Legion 24500

In Legion, these files are optionally chunked now. They are chunked either

  • if M2 header's 0x200000 flag is set and thus the new mid expansion format change is used
  • if the M2 has a .skel file

For new format M2s, .anim is pretty much unchanged except that there is the AFM2 chunk header. The AFSA and AFSB chunks do not appear in that case. If it is a .skel file based model, the chunks are present and animation data is split into bone and attach data. The AFM2 chunk then contains the animation data for ????, the AFSA chunk that for attachments and the AFSB chunk that for bones. See .skel files for that.

AFM2

The same content as an old anim file would have. In fact, files that were just converted to the new format are bit identical except for the chunk header.

AFSA

skeleton data for attachments

AFSB

skeleton data for bones

Animation sequences

List of animations present in the model.

struct M2Sequence
{
  uint16_t id;                   // Animation id in AnimationData.dbc
  uint16_t variationIndex;       // Sub-animation id: Which number in a row of animations this one is.
#if ≤ BC
  uint32_t start_timestamp;
  uint32_t end_timestamp;
#else
  uint32_t duration;             // The length of this animation sequence in milliseconds.
#endif
  float movespeed;               // This is the speed the character moves with in this animation.
  uint32_t flags;                // See below.
  int16_t frequency;             // This is used to determine how often the animation is played. For all animations of the same type, this adds up to 0x7FFF (32767).
  uint16_t _padding;
  M2Range replay;                // May both be 0 to not repeat. Client will pick a random number of repetitions within bounds if given.
#if version < ??? < 6.0.1
  uint32_t blendTime;
#else
  uint16_t blendTimeIn;          // The client blends (lerp) animation states between animations where the end and start values differ. This specifies how long that blending takes. Values: 0, 50, 100, 150, 200, 250, 300, 350, 500.
  uint16_t blendTimeOut;         // The client blends between this sequence and the next sequence for blendTimeOut milliseconds.
#endif
                                 // For both blendTimeIn and blendTimeOut, the client plays both sequences simultaneously while interpolating between their animation transforms.
  M2Bounds bounds;
  int16_t variationNext;         // id of the following animation of this AnimationID, points to an Index or is -1 if none.
  uint16_t aliasNext;            // id in the list of animations. Used to find actual animation if this sequence is an alias (flags & 0x40)
} sequences[];

--Koward (talk) 09:50, 18 December 2015 (UTC) In M2 v274 (Legion), it looks like blend_time has been divided in two uint16_t, and for standard animations the old blend_time is duplicated in both fields (ex : uint32 150 becomes two uint16 150). Maybe start and end blend_time values ? See Creature/GennGreymane/GennGreymane.m2 .

Flags

One thing I saw in the source is that "-1 animationblocks" in bones wont get parsed if 0x20 is not set.

Flag Description
0x01 Sets 0x80 when loaded. (M2Init)
0x02
0x04
0x08
0x10 apparently set during runtime in CM2Shared::LoadLowPrioritySequence for all entries of a loaded sequence (including aliases)
0x20 primary bone sequence -- If set, the animation data is in the .m2 file. If not set, the animation data is in an .anim file. Was named 'looped animation' by schlumpf years ago, without source.
0x40 has next / is alias (To find the animation data, the client skips these by following aliasNext until an animation without 0x40 is found.)
0x80 Blended animation (if either side of a transition has 0x80, lerp between end->start states, unless end==start by comparing bone values)
0x100 sequence stored in model ?
0x200
0x400
0x800 seen in Legion 24500 models

-- Rour, some animations rely on blending to look right. The MoP mage CM shoulders only animate half of their movement and rely on lerping back to the start position to look correct.

Animation Lookup

Hash table for Animations in AnimationData.dbc.

struct
{
  uint16_t animation_index; // Index at ofsAnimations which represents the animation in AnimationData.dbc. -1 if none.
} animation_lookups[];

The hash used is anim_id % num_buckets. If a bucket is used, a stride of n^2 is added with n = 1, 2, … until the entry is matching:

M2Sequence* find_entry (uint32_t anim_id)
{
  size_t i (anim_id % animation_lookups.count);
  
  for (size_t stride (1); true; ++stride)
  {
    if (animation_lookups[i] == -1)
    {
      return nullptr;
    }
    if (animation_sequences[animation_lookups[i]].id == anim_id)
    {
      return &animation_sequences[i];
    }

    i = (i + stride * stride) % animation_lookups.count;
    // so original_i + 1, original_i + 1 + 4, original_i + 1 + 4 + 9, …
  }

  [[unreachable]];
}

The entry referenced is the first in the `nextAlias` chain of a given animation id. Thus, num_buckets < num_animations, even if a model would have all animations multiple times.

Playable Animation Lookup

This section only applies to versions ≤ BC. Partially inlined into M2Sequences.

Lookup table for Playable Animation in AnimationData.dbc

Offset   Type     Description
0x00   int16     Fallback Animation ID in AnimationData.dbc
0x02   int16     Flags (0, 1, 3 seen)

Models don't present all animations sequences. This table maps from global animation list to available animation for the current model. The engine uses it to know which animation clip to play. That's why there are a lot of zeros ("Stand") for creatures.

Flags are used to modify how the clip should be played:

Value       Meaning
0       Play normal
1       Play backwards?
3       Freeze

For instance, the "HumanMale/HumanMale.m2" model has just one "Loot" animation sequence. "LootHold" and "LootUp" are obtained with flags 3 and 1.

Bones

struct M2CompBone                 // probably M2Bone ≤ Vanilla
{
  int32_t key_bone_id;            // Back-reference to the key bone lookup table. -1 if this is no key bone.
  enum
  {
    spherical_billboard = 0x8,
    cylindrical_billboard_lock_x = 0x10,
    cylindrical_billboard_lock_y = 0x20,
    cylindrical_billboard_lock_z = 0x40,
    transformed = 0x200,
    kinematic_bone = 0x400,       // MoP+: allow physics to influence this bone
    helmet_anim_scaled = 0x1000,  // set blend_modificator to helmetAnimScalingRec.m_amount for this bone
  };
  uint32_t flags;                 
  int16_t parent_bone;            // Parent bone ID or -1 if there is none.
  uint16_t submesh_id;            // Mesh part ID OR uDistToParent?
  union {                         // only ≥ BC?
    struct {
      uint16_t uDistToFurthDesc;
      uint16_t uZRatioOfChain;
    } CompressData;               // No model has ever had this part of the union used.
    uint32_t boneNameCRC;         // these are for debugging only. their bone names match those in key bone lookup.
  };
  M2Track<C3Vector> translation;
#if ≤ Vanilla
  M2Track<C4Quaternion> rotation;
#else
  M2Track<M2CompQuat> rotation;   // compressed values, default is (32767,32767,32767,65535) == (0,0,0,1) == identity
#endif
  M2Track<C3Vector> scale;
  C3Vector pivot;                 // The pivot point of that bone.
} bones[];

The bone indices in the vertex definitions seem to index into this data.

Billboards

The billboarding bits are used for various things:

  • Light halos around lamps must always face the viewer
  • The cannonball stack model (in the Deadmines or Booty Bay), where each cannonball is a crude hemisphere, they always face the viewer to create the illusion of actual cannonballs.

Spherical and cylindrical billboard bits are mutually exclusive. Only one of them can be used for the bone.

Bone Lookup Table

Lookup table for bones referenced from M2SkinSection.

struct 
{
  uint16_t bone;
} bone_lookup[];

Key-Bone Lookup

Its a lookup table for key skeletal bones like hands, arms, legs, etc. nKeyBoneLookup is 27 for the most models. At static models it is mostly 1.

struct 
{
  uint16_t bone; // -1 if none
} key_bone_lookup[];

Official list:

  • 00 "ArmL"
  • 01 "ArmR"
  • 02 "ShoulderL"
  • 03 "ShoulderR"
  • 04 "SpineLow"
  • 05 "Waist"
  • 06 "Head"
  • 07 "Jaw"
  • 08 "IndexFingerR"
  • 09 "MiddleFingerR"
  • 10 "PinkyFingerR"
  • 11 "RingFingerR"
  • 12 "ThumbR"
  • 13 "IndexFingerL"
  • 14 "MiddleFingerL"
  • 15 "PinkyFingerL"
  • 16 "RingFingerL"
  • 17 "ThumbL"
  • 18 "$BTH"
  • 19 "$CSR"
  • 20 "$CSL"
  • 21 "_Breath"
  • 22 "_Name"
  • 23 "_NameMount"
  • 24 "$CHD"
  • 25 "$CCH"
  • 26 "Root"
  • 27 "Wheel1" ≥ Wrath
  • 28 "Wheel2" ≥ Wrath
  • 29 "Wheel3" ≥ Wrath
  • 30 "Wheel4" ≥ Wrath
  • 31 "Wheel5" ≥ Wrath
  • 32 "Wheel6" ≥ Wrath
  • 33 "Wheel7" ≥ Wrath
  • 34 "Wheel8" ≥ Wrath

Geometry and rendering

Vertices

struct M2Vertex
{
  C3Vector pos;
  uint8 bone_weights[4];
  uint8 bone_indices[4];
  C3Vector normal;
  C2Vector tex_coords[2];  // two textures, depending on shader used
};

Models, too, use a Z-up coordinate systems, so in order to convert to Y-up, the X, Y, Z values become (X, -Z, Y).

-- Rour, the WoW vertex shaders all follow the same pattern, "Diffuse_XX_YY" (or sometimes XX, YY and Env). The particular vertex shader that is used chooses which set of texture coordinates to use. So Diffuse_T1 sends T1 texcoords to the fragment shader. Where Diffuse_T1_T2 sends both (for textures 0 and 1) but Diffuse_T1_T1 sends the same coords for both textures. Etc.

Views (LOD)

Skin profiles describe LOD views onto the model. They use all those lookup tables to be able to reference only parts of the lists while not being dynamically sized within the profile data.

≤ BC they were stored in the M2 itself, ≥ Wrath they have been moved to .skin files. The offsets are relative to the file the skin profile header is defined in. There is one .skin file per profile, each with a separate header, while in the inlined version, all headers are sequential. See the .skin file page for formats of both versions.

Render flags

struct M2Material
{
  uint16_t flags;
  uint16_t blending_mode; // apparently a bitfield
} materials[];
  • Flags:
Flag Meaning
0x01 Unlit
0x02 Unfogged
0x04 Two-sided (no backface culling if set)
0x08 depthTest
0x10 depthWrite
0x40 shadow batch related ??? (seen in WoD)
0x80 shadow batch related ??? (seen in WoD)
0x400 ??? (seen in WoD)
0x800 prevent alpha for custom elements. if set, use (fully) opaque or transparent. (litSphere, shadowMonk) (MoP+)
  • Blending mode
Value source dest notes
0 GL_ONE GL_ZERO blending disabled (GxBlendStateDesc: 0)
1 GL_ONE GL_ZERO Mod (1)
2 GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA Decal (2)
3 GL_ONE GL_ONE Add (10)
4 GL_SRC_ALPHA GL_ONE Mod2x (3)
5 GL_DST_COLOR GL_ZERO Fade (4)
6 GL_DST_COLOR GL_SRC_COLOR Deeprun Tram glass (5)
7 GL_ONE GL_ONE_MINUS_SRC_ALPHA WoD+ (13)

(blend equation is always GL_FUNC_ADD. Values are retrieved via GxBlendStateDesc's lower 5 bits. no separate blend func for alpha.)

  • Blending mode
Value Mapped to Meaning
0 0 Combiners_Opaque (Blend disabled)
1 1 Combiners_Mod (Blend enabled, Src = ONE, Dest = ZERO, SrcAlpha = ONE, DestAlpha = ZERO)
2 1 Combiners_Decal (Blend enabled, Src = SRC_ALPHA, Dest = INV_SRC_ALPHA, SrcAlpha = SRC_ALPHA, DestAlpha = INV_SRC_ALPHA )
3 1 Combiners_Add (Blend enabled, Src = SRC_COLOR, Dest = DEST_COLOR, SrcAlpha = SRC_ALPHA, DestAlpha = DEST_ALPHA )
4 1 Combiners_Mod2x (Blend enabled, Src = SRC_ALPHA, Dest = ONE, SrcAlpha = SRC_ALPHA, DestAlpha = ONE )
5 4 Combiners_Fade (Blend enabled, Src = SRC_ALPHA, Dest = INV_SRC_ALPHA, SrcAlpha = SRC_ALPHA, DestAlpha = INV_SRC_ALPHA )
6 4 Used in the Deeprun Tram subway glass, supposedly (Blend enabled, Src = DEST_COLOR, Dest = SRC_COLOR, SrcAlpha = DEST_ALPHA, DestAlpha = SRC_ALPHA )
7 ? New in WoD, needs research! Example model: World\Expansion05\Doodads\Shadowmoon\Doodads\6FX_Fire_Grassline_Doodad_blue_LARGE.m2

*Blend values are taken from D3D11 debugging of the client

Blend mode overrides

If this block is present (globalflags&8) and the "shading" flags of a textureunit wont be &0x8000, blending modes wont get mapped to the values above but to the ones in this block.

Instead of Mapping[renderflags->blendingmode] it will be UnknownBlock[textureunit->Shading].

As shading is not &0x8000 and (in their code) needs to be above 0, this may only touch Diffuse_T1.

According to wod, if the M2 Header has flag 0x08, instead of reading blend mode from M2 RenderFlags, blendMode is read from the raw blend maps referenced in header.

var flags = renderFlags[texUnit.renderFlags];
var blendMode = flags >> 16;
if ((header.GlobalModelFlags & 0x08) != 0 && texUnit.shader_id < mBlendMap.Length)
  blendMode = mBlendMap[texUnit.shader_id];

Texture unit lookup table

This section only applies to versions < Cata. Still present but unused in Cataclysm.
struct 
{
  uint16_t unit; // -1, 0, or 1. see below
} texture_units[];

For models that use multitexturing, this maps given texture unit numbers into actual texture unit numbers (0 or 1).

Values of -1 seem to mean environment mapping.

One model is of special interest, Creature/KelThuzad/KelThuzad.m2, which is the only one that has an nTexUnits of 3, and has three texture units specified for some of its submeshes. Sure enough, two of those map to 0 and 1, and one maps to -1.

More confusion thanks to my favorite "weird" model, World/Generic/Gnome/Passive Doodads/GnomeMachine/GnomeSubwayGlass.m2, which is the translucent, environment mapped glass tunnel in the Deeprun Tram. It only has a single value in this block, -1, which is used for the single texture layer in both render operations in the model. This and the magic with rendering flags/blend modes make up the neat transparent-reflective glass effect, but confuse me even more about how envmapping and such is handled. (and where it seems to get the bluish color from - is it in the model (no color blocks in this particular model), the wmo, a solid background color, or simply the result of the blending used?)

As a side note, on my (dated) system WoW does every texture unit in a single pass.

Colors and transparency

Colors

struct M2Color
{
  M2Track<C3Vector> color; // vertex colors in rgb order
  M2Track<fixed16> alpha; // 0 - transparent, 0x7FFF - opaque. Normaly NonInterp
} colors[];

This block is the M2 equivalent to the GEOA chunk in MDX files, it represents the vertex color and visibility animations for meshes. Referenced from the Texture Unit blocks in the *.skin. If a texunit belonging to a submesh has a value of -1 then the submesh doesnot use this block. Contains a separate timeline for transparency values. If no animation is used, the given value is constant.

Transparency

struct M2TextureWeight
{
  M2Track<fixed16> weight;
} textureWeights[];

Specifies global transparency values in addition to the values given in the Color block. I assume these are multiplied together eventually.

Transparency lookup table

struct 
{
  uint16_t transparency;
} transparency_lookup[];

Contains indices into the Transparency block. Used by the texture unit definitions in the LOD block.

Textures

  • Textures are defined globally in a list, additionally, a lookup table is given, referenced during rendering, to select textures.
struct M2Texture
{
  uint32_t type;          // see below
  uint32_t flags;         // see below
  M2Array<char> filename; // for non-hardcoded textures (type != 0), this still points to a zero-sized string
} textures[];

Texture Types

Texture type is 0 for regular textures, nonzero for skinned textures (filename not referenced in the M2 file!) For instance, in the NightElfFemale model, her eye glow is a type 0 texture and has a file name, the other 3 textures have types of 1, 2 and 6. The texture filenames for these come from client database files:

Value Meaning
0 - NONE - -- Texture given in filename
1 TEX_COMPONENT_SKIN -- Skin -- Body + clothes
2 TEX_COMPONENT_OBJECT_SKIN -- Object Skin -- Item, Capes ("Item\ObjectComponents\Cape\*.blp")
3 TEX_COMPONENT_WEAPON_BLADE -- Weapon Blade -- Used on several models but not used in the client as far as I see. Armor Reflect?
4 TEX_COMPONENT_WEAPON_HANDLE -- Weapon Handle
5 TEX_COMPONENT_ENVIRONMENT -- (OBSOLETE) Environment (Please remove from source art)
6 TEX_COMPONENT_CHAR_HAIR -- Character Hair
7 TEX_COMPONENT_CHAR_FACIAL_HAIR -- (OBSOLETE) Character Facial Hair (Please remove from source art)
8 TEX_COMPONENT_SKIN_EXTRA -- Skin Extra
9 TEX_COMPONENT_UI_SKIN -- UI Skin -- Used on inventory art M2s (1): inventoryartgeometry.m2 and inventoryartgeometryold.m2
10 TEX_COMPONENT_TAUREN_MANE -- (OBSOLETE) Tauren Mane (Please remove from source art) -- Only used in quillboarpinata.m2. I can't even find something referencing that file. Oo Is it used?
11 TEX_COMPONENT_MONSTER_1 -- Monster Skin 1 -- Skin for creatures or gameobjects #1
12 TEX_COMPONENT_MONSTER_2 -- Monster Skin 2 -- Skin for creatures or gameobjects #2
13 TEX_COMPONENT_MONSTER_3 -- Monster Skin 3 -- Skin for creatures or gameobjects #3
14 TEX_COMPONENT_ITEM_ICON -- Item Icon -- Used on inventory art M2s (2): ui-button.m2 and forcedbackpackitem.m2 (CSimpleModel_ReplaceIconTexture("texture"))
15 ≥ Cata Guild Background Color
16 ≥ Cata Guild Emblem Color
17 ≥ Cata Guild Border Color
18 ≥ Cata Guild Emblem

Flags

Value Meaning
1 Texture wrap X
2 Texture wrap Y

Texture lookup table

struct 
{
  uint16_t texture;
} texture_lookup[];

Replacable texture lookup

struct 
{
  uint16_t replacement;
} texture_replacements[];

A reverse lookup table for 'replaced' textures, mapping replacable ids to texture indices or -1. Only goes up to the maximum id used in the model.

Its strange, that HARDCODED is in the list, as a model can have more than one of course. Its just the last one written to the file.

Effects

Texture Transforms

  • This block contains definitions for texture animations, for example, flowing water or lava in some models. The keyframe values are used in the texture transform matrix.
struct M2TextureTransform
{
  M2Track<C3Vector> translation;
  M2Track<C4Quaternion> rotation;    // rotation center is texture center (0.5, 0.5)
  M2Track<C3Vector> scaling;
} textureTransforms[];

Seems like UV rotation in some models are made against (0.5, 0.5) point instead of (0, 0). At least it's the case for world\goober\g_scourgerunecirclecrystal.m2

So to get the proper UV rotation it would be necessary apply rotation this way:

  • Translate UV anim matrix to point (0.5, 0.5)
  • Apply rotation mat from quaternion
  • UV anim matrix to point (-0.5, -0.5)

Texture Transforms lookup table

struct 
{
  uint16_t anim_texture_id; // -1 for static
} anim_texture_lookup[];

Ribbon emitters

struct M2Ribbon
{
  uint32_t ribbonId;                  // Always (as I have seen): -1.
  uint32_t boneIndex;                 // A bone to attach to.
  C3Vector position;                 // And a position, relative to that bone.
  M2Array<uint16_t> textureIndices;   // into textures
  M2Array<uint16_t> materialIndices;  // into materials
  M2Track<C3Vector> colorTrack;
  M2Track<fixed16> alphaTrack;          // And an alpha value in a short, where: 0 - transparent, 0x7FFF - opaque.
  M2Track<float> heightAboveTrack;
  M2Track<float> heightBelowTrack;        // do not set to same!
  float edgesPerSecond;                  // this defines how smooth the ribbon is. A low value may produce a lot of edges.
  float edgeLifetime;                    // the length aka Lifespan. in seconds
  float gravity;                      // use arcsin(val) to get the emission angle in degree
  uint16_t textureRows;                    // tiles in texture
  uint16_t textureCols;
  M2Track<uint16_t> texSlotTrack;
  M2Track<uchar> visibilityTrack;
#if ≥ Wrath                           // TODO: verify version
  int16_t priorityPlane;
  uint16_t padding;
#endif
} ribbons[];

Some models that contain ribbon emitters and are viewable in the game world are: Wisps in BFD, Al'ar the Phoenix in Tempest Keep and any other phoenix models and the energy trails in the COT (not the actual instance, but the entrance cave in Tanaris Desert). Other models with ribbon emitters are spells and effects.

Parameters from the MDL format that are probably in here somewhere: emission rate, rows, cols ...?

Particle emitters

This is partly wrong as hell! Do not rely on this block, at all. It might even be wrong for WotLK.

M2ParticleOld

struct M2ParticleOld {
  uint32 particleId;                        // Always (as I have seen): -1.
  uint32 flags;                             // See Below
  C3Vector Position;                       // The position. Relative to the following bone.
  uint16 bone;                              // The bone its attached to.
  union
  {
    uint16 texture;                         // And the textures that are used. 
#if ≥ Cata
    struct                                  // For multi-textured particles actually three ids
    {
      uint16_t texture_0 : 5;
      uint16_t texture_1 : 5;
      uint16_t texture_2 : 5;
      uint16_t : 1;
    };
#endif
  };
  M2Array<char> geometry_model_filename;    // if given, this emitter spawns models
  M2Array<char> recursion_model_filename;   // if given, this emitter is an alias for the (maximum 4) emitters of the given model

#if >= 263 (late Burning Crusade)
  uint8 blendingType;                       // A blending type for the particle. See Below
  uint8 emitterType;                        // 1 - Plane (rectangle), 2 - Sphere, 3 - Spline, 4 - Bone
  uint16 particleColorIndex;                // This one is used for ParticleColor.dbc. See below.
#else
  uint16 blendingType;                      // A blending type for the particle. See Below
  uint16 emitterType;                       // 1 - Plane (rectangle), 2 - Sphere, 3 - Spline, 4 - Bone
#endif
#if ≥ Cata
  fixed_point<uint8_t, 2, 5> multiTextureParamX[2];
#else
  uint8 particleType;                       // Found below.
  uint8 headorTail;                         // 0 - Head, 1 - Tail, 2 - Both 
#endif
  uint16 textureTileRotation;               // Rotation for the texture tile. (Values: -1,0,1) -- priorityPlane
  uint16 textureDimensions_rows;            // for tiled textures
  uint16 textureDimensions_columns;
  M2Track<float> emissionSpeed;             // Base velocity at which particles are emitted.
  M2Track<float> speedVariation;            // Random variation in particle emission speed. (range: 0 to 1)
  M2Track<float> verticalRange;             // longitude; Drifting away vertically. (range: 0 to pi) For plane generators, this is the maximum polar angle of the initial velocity; 
                                            // 0 makes the velocity straight up (+z). For sphere generators, this is the maximum elevation of the initial position; 
                                            // 0 makes the initial position entirely in the x-y plane (z=0).
  M2Track<float> horizontalRange;           // latitude; They can do it horizontally too! (range: 0 to 2*pi) For plane generators, this is the maximum azimuth angle of the initial velocity; 
                                            // 0 makes the velocity have no sideways (y-axis) component. 
                                            // For sphere generators, this is the maximum azimuth angle of the initial position.
  M2Track<float> gravity;                   // Not necessarily a float; see below.
  M2Track<float> lifespan;
#if ≥ Wrath
  float lifespanVary;                       // An individual particle's lifespan is added to by lifespanVary * random(-1, 1)
#endif
  M2Track<float> emissionRate; 
#if ≥ Wrath
  float emissionRateVary;                   // This adds to the base emissionRate value the same way as lifespanVary. The random value is different every update.
#endif
  M2Track<float> emissionAreaLength;        // For plane generators, this is the width of the plane in the x-axis.
                                            // For sphere generators, this is the minimum radius.
  M2Track<float> emissionAreaWidth;         // For plane generators, this is the width of the plane in the y-axis.
                                            // For sphere generators, this is the maximum radius.
  M2Track<float> zSource;                   // When greater than 0, the initial velocity of the particle is (particle.position - C3Vector(0, 0, zSource)).Normalize()
#if ≥ Wrath
  FBlock<C3Vector> colorTrack;             // Most likely they all have 3 timestamps for {start, middle, end}.
  FBlock<fixed16> alphaTrack;
  FBlock<C2Vector> scaleTrack;
  C2Vector scaleVary;                      // A percentage amount to randomly vary the scale of each particle
  FBlock<uint16> headCellTrack;             // Some kind of intensity values seen: 0,16,17,32 (if set to different it will have high intensity)
  FBlock<uint16> tailCellTrack;
#else
  float midPoint;                           // middleTime; Middle point in lifespan (0 to 1).
  CImVector[3] colorValues;                // start, middle, end
  float[3] scaleValues;
  uint16[3] lifespanUVAnim;
  uint16[3] decayUVAnim;
  int16[2] tailUVAnim;                      // start, end
  int16[2] tailDecayUVAnim;
#endif
  float tailLength;
  float twinkleSpeed;                       // twinkleFPS; has something to do with the spread
  float twinklePercent;                     // same mechanic as MDL twinkleOnOff but non-binary in 0.11.0
  CRange twinkleScale;
  float burstMultiplier;                    // ivelScale
  float drag;                               // For a non-zero values, instead of travelling linearly the particles seem to slow down sooner. Speed is multiplied by exp( -drag * t ).
#if ≥ Wrath
  float baseSpin;                           // Initial rotation of the particle quad
  float baseSpinVary;
  float spin;                               // Rotation of the particle quad per second
  float spinVary;
#else
  float spin;                               // 0.0 for none, 1.0 to rotate the particle 360 degrees throughout its lifetime.
#endif

  M2Box tumble;
  C3Vector windVector;
  float windTime;

  float followSpeed1;
  float followScale1;
  float followSpeed2;
  float followScale2;
  M2Array<C3Vector> splinePoints;                                  // Set only for spline praticle emitter. Contains array of points for spline
  M2Track<uchar> enabledIn;                 // (boolean) Appears to be used sparely now, probably there's a flag that links particles to animation sets where they are enabled.
} particles[];

Spin can be a float value greater or less one. Results look better if use it as a "phase shift": particle_rotate = randfloat(-sys->rotation * pi, sys->rotation * pi); --Igor

M2Particle (Cata+)

This section only applies to versions ≥ Cata.
  • Cata+ has multi texture support
using fp_6_9 = fixed_point<uint16_t, 6, 9>;
struct vector_2fp_6_9 { fp_6_9 x; fp_6_9 y; };
struct M2Particle
{
  M2ParticleOld old;
  vector_2fp_6_9 multiTextureParam0[2];
  vector_2fp_6_9 multiTextureParam1[2];
} particles[];

In addition to these two parameters, ParticleType and HeadOrTail got reused (as in replaced at their current position) as multiTextureParamX[2] where all arrays are one entry per additional texture.

I don't know if the previous meaning of the two parameters still exists, got moved, or was just never used to begin with. ParticleType appears to be implicit by having flags & 0x10100000 (→ multi texture), a model (→ model) or neither (→ default).--Schlumpf (talk) 23:47, 29 October 2015 (UTC)

Particle Flags

Value Description
0x1 Particles are affected by lighting;
0x2
0x4
0x8 Particles travel "up" in world space, rather than model.
0x10 Do not Trail
0x20 Unlightning
0x40
0x80 Particles in Model Space
0x100
0x200 spawn position randomized in some way?
0x400 STYLE: Pinned Particles, their quad enlarges from their creation position to where they expand.
0x800
0x1000 XYQuad Particles. They align to XY axis facing Z axis direction.
0x2000 clamp to ground; call CParticleEmitter2::ProjectParticle
0x4000
0x8000
0x10000 ChooseRandomTexture
0x20000 STYLE: "Outward" particles, most emitters have this and their particles move away from the origin, when they don't the particles start at origin+(speed*life) and move towards the origin.
0x40000 STYLE: unknown. In a large proportion of particles this seems to be simply the opposite of the above flag, but in some (e.g. voidgod.m2 or wingedlionmount.m2) both flags are true.
0x80000 If set, ScaleVary affects x and y independently; if not set, ScaleVary.x affects x and y uniformly, and ScaleVary.y is not used.
0x200000 Random FlipBookStart
0x400000 Ignores Distance (or 0x4000000?!, CMapObjDef::SetDoodadEmittersIgnoresDistance has this one)
0x800000 gravity values are compressed vectors instead of z-axis values (see Compressed Particle Gravity below)
0x1000000 bone generator = bone, not joint
0x4000000 do not throttle emission rate based on distance
0x10000000 Particle uses multi-texturing (could be one of the other WoD-specific flags), see multi-textured section.

--Barncastle (talk) A comparison of 0.10.0's MDX files to 0.11.0's and 1.12.1's M2 files indicates that the MDX PRE2 flags (≥0x8000) were probably unchanged during the switch from MDL to M2, albeit separated into their own enum.

ParticleColorIndex

This is used in conjunction with ParticleColor.dbc to alter the default colour of particles. If the particle colour is not meant to be changed then its ParticleColorIndex will have a value of zero. If the particle colour may be changed then the value will be 11, 12 or 13, indicating whether the first, second or third Start, Mid and End colours are to be used, respectively. The row of ParticleColor.dbc to be used is determined its ID value, which should correspond to the ParticleColorID value supplied by CreatureDisplayInfo.dbc or ItemDisplayInfo.dbc for creatures or items.

Particle types

Value Description
0 "normal" particle
1 large quad from the particle's origin to its position (used in Moonwell water effects)
2 seems to be the same as 0 (found some in the Deeprun Tram blinky-lights-sign thing)

ParticleType is always 0 and, maybe, now (Flags & 0x40000) != 0 means "particles from origin to position". --Igor Checked and verified --BlinkHawk

Particle Blendings

Value Description
0 glDisable(GL_BLEND); glDisable(GL_ALPHA_TEST);
1 glBlendFunc(GL_SRC_COLOR, GL_ONE);
2 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
3 glDisable(GL_BLEND); glEnable(GL_ALPHA_TEST);
4 glBlendFunc(GL_SRC_ALPHA, GL_ONE);

from Modelviewer source -- Rour, some WoD particle effects are using blend mode 0x7 here.

The Fake-AnimationBlock

  • Its pretty much like the real one but without the "header".
Offset Type Name Description
0x000 uint32 nTimestamps The number of timestamps.
0x004 uint32 ofsTimestamps And the offset to them. The timestamps are shorts! (?)
0x008 uint32 nKeys The same number again. This time its the number of Keys / Values.
0x00C uint32 ofsKeys And their offset.

But they're unable to change between different animations, so they directly point to the data.

Compressed Particle Gravity

Key values in the gravity track are decompressed at load time from a 4-byte value to a C3Vector.

struct CompressedParticleGravity {
  int8_t x, y;
  int16_t z;
};
for (/* each 4-byte value in the particle gravity track */) {
  float *pValue;
  C3Vector *pDst;
  if (particle.flags & 0x800000) {
    // interpret the 4 bytes at pValue as CompressedParticleGravity:
    CompressedParticleGravity v = new (pValue) CompressedParticleGravity();
    C3Vector dir = C3Vector(v.x, v.y, 0) * (1.0f / 128.0f);
    float z = sqrtf(1.0f - dir.Dot(dir));
    float mag = v.z * 0.04238648f;
    if (mag < 0) {
      z = -z;
      mag = -mag;
    }
    dir.z = z;
    dir *= mag;
    *pDst = dir;
  } else {
    *pDst = C3Vector(0, 0, -(*pValue));
  }
}


M2Box

struct M2Box {
  C3Vector ModelRotationSpeedMin;
  C3Vector ModelRotationSpeedMax;
}

Miscellaneous

Name

char name[];

Informative name used for debugging purposes. Not used in retail clients.

Bounding volumes

These blocks give a simplified bounding volume for the model. Characters and creatures have just a simple box.

Vertices

This block defines the possible points used for the model. They are referenced in the triangles block later.

struct 
{
  C3Vector position;
} bounding_vertices[];

Triangles

The number in the header tells us how many uint16s there are, not how many triangles. To use this better, you should group three of them into an array. The nBoundingTriangles/3 indices will tell you which vertices are used for the triangle then.

struct 
{
  uint16_t index; // three entries pointing to vertices per triangle
} bounding_triangles[];

The number nBoundingTriangles once again contains the number of indices used, so divide by 3 to get the number of triangles.

Normals

This one defines a normal per triangle. The vectors are normalized, but Blizzard seems to have some problems getting a simple vector normalized leading in several 0,0,0.999999999 ones. Whatever.

As each vertex has a corresponding normal vector, it should be true that nBoundingNormals = nBoundingTriangles / 3.

struct 
{
  C3Vector normal;
} bounding_normals[];

Lights

struct M2Light
{
/*0x00*/  uint16_t type;                      // Types are listed below.
/*0x02*/  int16_t bone;                       // -1 if not attached to a bone
/*0x04*/  C3Vector position;                 // relative to bone, if given
/*0x10*/  M2Track<C3Vector> ambient_color;
/*0x24*/  M2Track<float> ambient_intensity;   // defaults to 1.0
/*0x38*/  M2Track<C3Vector> diffuse_color;
/*0x4C*/  M2Track<float> diffuse_intensity;   // defaults to 1.0
/*0x60*/  M2Track<float> attenuation_start;
/*0x74*/  M2Track<float> attenuation_end;
/*0x88*/  M2Track<uint8_t> visibility;        // enabled?
/*0x9C*/
} lights[];

Two light types:

Value Description
0 Directional
1 Point light

Directional light type is not used (at least in 3.3.5) outside login screen, and doesn't seem to be taken into account in world.

Cameras

These blocks are present in the "flyby" camera models which completely lack geometry and the main menu backdrop models which are supposed to have a fixed camera. Additionally, characters and monsters also have this block. The reason that non-mainmenu and non-geometry M2s have cameras was is you can see the unit's portrait and the character info tab.

struct M2Camera
{
  uint32_t type; // 0: portrait, 1: characterinfo; -1: else (flyby etc.); referenced backwards in the lookup table.
#if < Cata
  float fov; // Diagonal FOV in radians. See below for conversion.
#endif
  float far_clip;
  float near_clip;
  M2Track<M2SplineKey<C3Vector>> positions; // How the camera's position moves. Should be 3*3 floats.
  C3Vector position_base;
  M2Track<M2SplineKey<C3Vector>> target_position; // How the target moves. Should be 3*3 floats.
  C3Vector target_position_base;
  M2Track<M2SplineKey<float>> roll; // The camera can have some roll-effect. Its 0 to 2*Pi. 
#if ≥ Cata
  M2Track<M2SplineKey<float>> FoV; //Diagonal FOV in radians. See below for conversion.
#endif
} cameras[];

Camera field of view

The fov included in M2Camera is a diagonal field of view (in radians). The client converts it to a vertical field of view at runtime using the following formula:

float vfov = dfov / sqrt(1.0 + pow(aspect, 2.0));

The aspect ratio used is determined by the rect being presented on (eg. the game window).

Note that this formula makes an assumption that the screen being projected to is planar (ie a monitor).

Camera lookup table

This block lists the different cameras existing in the model. The index in the array is also the type. CameraLookupTable[1] is always the character tab camera.

"-1" type cameras are not referenced.

If nCameras >= 1, then nCameraLookup will be >= 1 regardless of whether any camera will be actually referenced in it. See interface/glues/models/ui_mainmenu_warlords/ui_mainmenu_warlords.m2. A valid block thus may be -1s only. This appears to be an exporter-quirk rather than a requirement.

struct
{
  uint16_t camera;
} camera_lookup[];

Attachments

This block specifies a bunch of locations on the body - hands, shoulders, head, back, knees etc. It is used to put items on a character. This seems very likely as this block also contains positions for sheathed weapons, a shield, etc.

struct M2Attachment
{
  uint32_t id;                      // Referenced in the lookup-block below.
  uint16_t bone;                    // attachment base
  uint16_t unknown;                 // see BogBeast.m2 in vanilla for a model having values here
  C3Vector position;               // relative to bone; Often this value is the same as bone's pivot point 
  M2Track<uchar> animate_attached;  // whether or not the attached model is animated when this model is. only a bool is used. default is true.
} attachments[];

Meaning depends on type of model. The following are for creatures/characters mainly:

ID Description ID Description ID Description ID Description ID Description
0 Shield / MountMain / ItemVisual0 12 Back 24 Special2 36 Bullet (version: somewhen after alpha) 48 RightFoot
1 HandRight / ItemVisual1 13 ShoulderFlapRight 25 Special3 37 SpellHandOmni (version: somewhen after alpha) 49 ShieldNoGlove
2 HandLeft / ItemVisual2 14 ShoulderFlapLeft 26 SheathMainHand 38 SpellHandDirected (version: somewhen after alpha) 50 SpineLow
3 ElbowRight / ItemVisual3 15 ChestBloodFront 27 SheathOffHand 39 VehicleSeat1 ≥ Wrath 51 AlteredShoulderR
4 ElbowLeft / ItemVisual4 16 ChestBloodBack 28 SheathShield 40 VehicleSeat2 ≥ Wrath 52 AlteredShoulderL
5 ShoulderRight 17 Breath 29 PlayerNameMounted 41 VehicleSeat3 ≥ Wrath 53 BeltBuckle ≥ Mists
6 ShoulderLeft 18 PlayerName 30 LargeWeaponLeft 42 VehicleSeat4 ≥ Wrath 54 SheathCrossbow
7 KneeRight 19 Base 31 LargeWeaponRight 43 VehicleSeat5 ≥ Wrath 55 HeadTop ≥ Legion
8 KneeLeft 20 Head 32 HipWeaponLeft 44 VehicleSeat6 ≥ Wrath
9 HipRight 21 SpellLeftHand 33 HipWeaponRight 45 VehicleSeat7 ≥ Wrath
10 HipLeft 22 SpellRightHand 34 Chest 46 VehicleSeat8 ≥ Wrath
11 Helm 23 Special1 35 HandArrow 47 LeftFoot

For weapons, usually 5 of these points are present, which correspond to the 5 columns in ItemVisuals.dbc, which in turn has 5 models from ItemVisualEffects.dbc. This is for the weapon glowy effects and such. The effect ID is the last column in ItemDisplayInfo.dbc. They take the ids 0 to 4. Mounts take the id 0 for their rider. Breath (17) is used by CGCamera::FinishLoadingTarget() aswell as some other one. The name above the head of a Unit (CGUnit_C::GetNamePosition) looks for PlayerNameMounted (29), then PlayerName (18).

Attachment Lookup

The index of the array defines, which type that attachment is of. Its the same as the list above. The lookups and the id of the animations point in a circle.

struct 
{
  uint16_t attachment;
} attachment_lookup[];

Events

These events are used for timing sounds for example. You can find the $DTH (death) event on nearly every model. It will play the death sound for the unit.

The events you can use depend on the way, the model is used. Dynamic objects can shake the camera, doodads shouldn't. Units can do a lot more than other objects.

Somehow there are some entries, that don't use the $... names but identifiers like "DEST" (destination), "POIN" (point) or "WHEE" (wheel). How they are used? Idk.

struct M2Event
{
  uint32_t identifier;  // mostly a 3 character name prefixed with '$'.
  uint32_t data;        // This data is passed when the event is fired. 
  uint32_t bone;        // Somewhere it has to be attached.
  C3Vector position;   // Relative to that bone of course, animated. Pivot without animating.
  M2TrackBase enabled;  // This is a timestamp-only animation block. It is built up the same as a normal AnimationBlocks, but is missing values, as every timestamp is an implicit "fire now".
} events[];

Possible Events

There are a lot more of them. I did not list all up to now.

ID data what Type seen to be fired on Description
$AH[0-3] PlaySoundKit (customAttack[x]) soundEffect ID is defined by CreatureSoundDataRec::m_customAttack[x]
$BMD BowMissleDestination RangedWeapon
$AIM Vehicles CGUnit_C::ComputeMissileTrajectory Position used as MissileFirePos.
$ALT anim_swap_event / DisplayTransition Unit CUnitDisplayTransition_C::UpdateState(1) or CUnitDisplayTransition_C::HandleAnimSwapEvent
$BL[0-3] FootstepAnimEventHit (left) Unit Backwards
$BR[0-3] FootstepAnimEventHit (right) Unit Backwards
$BRT PlaySoundKit (birth) soundEffect ID is defined by CreatureSoundDatarec::m_birthSoundID
$BTH Breath Unit All situations, where nothing happens or breathing. Adds Special Unit Effect based on unit state (under water, in-snow, …)
$BWP PlayRangedItemPull (Bow Pull) Unit LoadRifle, LoadBow
$BWR BowRelease Unit AttackRifle, AttackBow, AttackThrown
$CAH Unit Attack*, *Unarmed, ShieldBash, Special* attack hold? CGUnit_C::HandleCombatAnimEvent
$CCH Unit mostly not fired, AttackThrown CEffect::DrawFishingString needs this on the model for getting the string attachments.
$CFM Unit CGCamera::UpdateMountHeightOrOffset CGCamera::UpdateMountHeightOrOffset: Only z is used. Non-animated. Not used if $CMA
$CHD Unit not fired probably does not exist?!
$CMA Unit CGCamera::UpdateMountHeightOrOffset: Position for camera
$CPP PlayCombatActionAnimKit parry, anims, depending on some state, also some callback which might do more
$CSD soundEntryId PlayEmoteSound Unit Emote*
$CSL release_missiles_on_next_update if has_pending_missiles (left) Unit AttackRifle, SpellCast*, ChannelCast* "x is {L or R} (""Left/right hand"") (?)"
$CSR release_missiles_on_next_update if has_pending_missiles (right) Unit AttackBow, AttackRifle, AttackThrown, SpellCast*, ChannelCast* "x is {L or R} (""Left/right hand"") (?)"
$CSS PlayWeaponSwooshSound sound played depends on CGUnit_C::GetWeaponSwingType
$CST release_missiles_on_next_update if has_pending_missiles Unit Attack*, *Unarmed, ShieldBash, Special*, SpellCast, Parry*, EmoteEat, EmoteRoar, Kick, ... $CSL/R/T are also used in CGUnit_C::ComputeDefaultMissileFirePos.
$CVS  ? Data: SoundEntriesAdvanced.dbc, Sound — Not present in 6.0.1.18179
$DSE DestroyEmitter MapObj
$DSL soundEntryId GO
$DSO soundEntryId DoodadSoundOneShot GO
$DTH DeathThud + LootEffect Unit Death, Drown, Knockdown """I'm dead now!"", UnitCombat_C, this plays death sounds and more." Note that this is NOT triggering CreatureSoundDataRec::m_soundDeathID, but that is just always triggered as soon as the death animation plays.
$EAC object package state enter 3, exit 2, 4, 5
$EDC object package state enter 5, exit 3, 4, 2
$EMV object package state enter 4, exit 3, 2, 5
$ESD PlayEmoteStateSound Unit soundEffect ID is implicit by currently played emote
$EWT object package state enter 2, exit 3, 4, 5
$FD[1-9] PlayFidgetSound CreatureSoundDataRec::m_soundFidget (only has 5 entries, so don’t use 6-9)
$FDX PlayUnitSound (stand) soundEffect ID is defined by CreatureSoundDataRec::m_soundStandID
$FL[0-3] FootstepAnimEventHit (left) Forward
$FR[0-3] FootstepAnimEventHit (right) Forward
$FSD HandleFootfallAnimEvent Unit Walk, Run (multiple times), ... Plays some sound. Footstep? Also seen at several emotes etc. where feet are moved. CGUnit_C::HandleFootfallAnimEvent
$GC[0-3] GameObject_C_PlayAnimatedSound soundEffect ID is defined by GameObjectDisplayInfoRec::m_Sound[x + 6] ({Custom0, Custom1, Custom2, Custom3})
$GO[0-5] GameObject_C_PlayAnimatedSound soundEffect ID is defined by GameObjectDisplayInfoRec::m_Sound[x] ({Stand, Open, Loop, Close, Destroy, Opened})
$HIT PlayWoundAnimKit Unit Attack*, *Unarmed, ShieldBash, Special* soundEntryId depends on SpellVisualKit
$KVS  ? MapLoad.cpp -- not found in 6.0.1.18179
$RL[0-3] FootstepAnimEventHit (left) Running
$RR[0-3] FootstepAnimEventHit (right) Running
$SCD PlaySoundKit (spellCastDirectedSound) soundEffect ID is defined by CreatureSoundDataRec::m_spellCastDirectedSoundID
$SHK spellEffectCameraShakesID AddShake GO
$SHL ExchangeSheathedWeapon (left) Sheath, HipSheath
$SHR ExchangeSheathedWeapon (right) Sheath, HipSheath
$SL[0-3] FootstepAnimEventHit (left) Stop, (JumpEnd), (Shuffle*) Stop
$SMD PlaySoundKit (submerged) soundEffect ID is defined by CreatureSoundDatarec::m_submergedSoundID
$SMG PlaySoundKit (submerge) soundEffect ID is defined by CreatureSoundDatarec::m_submergeSoundID
$SND soundEntryId PlaySoundKit (custom) GO
$SR[0-3] FootstepAnimEventHit (right) Stop, (JumpEnd), (Shuffle*) Stop
$STx Mounts MountTransitionObject::UpdateCharacterData Not seen in 6.0.1.18179 -- x is {E and B} , sequence time is taken of both, pivot of $STB. (Also, attachment info for attachment 0)
$TRD HandleSpellEventSound Unit EmoteWork*, UseStanding* soundEffect ID is implicit by SpellRec
$VG[0-8] HandleBoneAnimGrabEvent
$VT[0-8] HandleBoneAnimThrowEvent
$WGG PlayUnitSound (wingGlide) soundEffect ID is defined by CreatureSoundDataRec::m_soundWingGlideID
$WL[0-3] FootstepAnimEventHit (left)
$WNG PlayUnitSound (wingFlap) soundEffect ID is defined by CreatureSoundDataRec::m_soundWingFlapID
$WR[0-3] FootstepAnimEventHit (right)
$WTB Weapons Weapon Trail Bottom position, also used for Bow String
$WTT Weapons Weapon Trail Top position
$WWG  ? Calls some function in the Object VMT. -- Not seen in 6.0.1.18179
DEST  ? exploding ballista, that one has a really fucked up block. Oo
POIN Unit not fired Data: ?, seen on multiple models. Basilisk for example. (6801)
WHEE  ? Data: 601+, Used on wheels at vehicles.
BOTT  ? Purpose unknown. Seen in well_vortex01.m2
TOP  ? Purpose unknown. Seen in well_vortex01.m2