• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

M3 Specifications

Level 29
Joined
Jul 29, 2007
Messages
5,174
This specification is mostly based on https://github.com/flo/m3addon/blob/master/structures.xml.
More values have been reverse engineered by BlinkBoy, and a couple of fixes by me.
Many things are still unknown, feel free to help in reverse engineering the format.

The notation of this specifications is as follows:

  • Chunk names will always be given in their ASCII representation.
    For example, every M3 starts with a uint32 with the hexadecimel value 0x3433444d. If you break that to bytes, and remember how small endian works, you will get the bytes '4', '3', 'D' and 'M', or if you turn it around, the string "MD34".
    The M3 format uses this concept extensively, as a way to give chunks meaningfull names.
    In your code, you will probably want to define constants for this, for example MD34_CHUNK = 0x3433444d, but this is up to you.
  • Flag variables hold different values in their bits with special meanings.
    The notation for them would be the hexadecimal number representing the correct bit.
    If for example there is 0x4, it means that the third bit holds specific information which you can get with bitwise operators.
    That is, (Flag & 0x4) == 0x4 will show you if the value in the third bit is true or false.
  • Type variables hold only one value, and will simply be written using normal decimal numbers.
  • All the code examples will assume reader is some kind of binary reader.

Unlike the MDX format, M3 doesn't store its data in linear chunks, but rather batches most of it together in large data structures, and then gives indices to access the correct data structure.

It starts (after the uint32 "43DM") with the MD34 header chunk:
C++:
MD34 {
  uint32 indexOffset
  uint32 entries
  Reference modelHeader // MODL
}

Since M3 is largely based around referencing other chunks and data structures, you will see many reference types. I will write them as "Reference name // type". You can see above "Reference modelHeader // MODL", which is a reference that points to the MODL chunk.

All the data structures - the parts that actually hold all the reptitive data like vertices, faces, animation data, materials, etc. - are given by an index.
This index is an array of entries IndexEntry chunks, and starts at the byte offset indexOffset.

The IndexEntry chunk looks like this:
C++:
IndexEntry {
  uint32 tag
  uint32 offset
  uint32 entries
  uint32 version
}

Reading the index:
C++:
if (reader.read(4) == "43DM") {
  MD34 header = new MD34(reader)

  reader.seek(header.indexOffset)

  IndexEntry[header.entries] indexEntries

  for (uint32 i = 0; i < header.entries; i++) {
    indexEntries[i] = new IndexEntry(reader)
  }
}

The version variable used by index entries is important - some chunks have multiple known versions, which change their structure.

In addition to the index, the MD34 header also contains a direct reference to the MODL chunk. This is the actual main M3 chunk that will contain the references to all the rest of the data.
This is given in the header so you wont have to loop over all the index entries, in order to find it, every time you want to load a model.

The reference structure looks like this:
C++:
Reference {
  uint32 entries
  uint32 index
  uint32 flags
}

To use a reference, you use its index to access the correct index entry, offset reader to it, and read entries entries of the data type that the index entry holds.

As an example, the following code reads the MODL chunk.
Do note, however, that the code is not reading multiple entries, that's because there is always exactly one MODL chunk.
C++:
IndexEntry modelHeader = indexEntries[header.modelHeader.index]
    
reader.seek(modelHeader.offset)

MODL model = new MODL(reader)

A more general reference reading function will look something like this:
C++:
// Parses reference of some type X
X[] parseXReference(reader, indexEntries) {
  Reference reference = new Reference(reader)
  IndexEntry indexEntry = indexEntries[reference.index]
  uint32 offset = reader.tell()
  X[reference.entries] entries
  
  // Go to the location of the entries
  reader.seek(indexEntry.offset)
  
  // Read them
  for (uint32 i = 0; i < reference.entries; i++) {
    // Pass the index entries if X also has references
    // Always pass the entry version, even if X doesn't have more than 1 version now, maybe more will appear in the future
    entries[i] = new X(reader, indexEntries, indexEntry.version)
  }
  
  // Go back to where we were
  reader.seek(offset)
  
  return entries
}

As previously stated, MODL is the main M3 chunk. It will reference all the data used by the model.
This is how it looks.
Note: remember the version variable of the index entries, this is where it starts to play a role.
C++:
MODL {
  Reference name // CHAR
  uint32 flags // 0x100000: has mesh
  Reference sequences // SEQS
  Reference stc // STC_
  Reference stg // STG_
  float unknown0
  float unknown1
  float unknown2
  float unknown3
  Reference sts // STS_
  Reference bones // BONE
  uint skinBones
  uint32 vertexFlags // 0x20000: 1 texture coordinate
                     // 0x40000: 2 texture coordinates
                     // 0x80000: 3 texture coordinates
                     // 0x100000: 4 texture coordinates
  Reference vertices // U8__
  Reference divisions // DIV_
  Reference boneLookup // U16_
  BoundingSphere boundings
  uint32[16] unknown4To19
  Reference attachmentPoints // ATT_
  Reference attachmentPointAddons // U16_
  Reference lights // LITE
  Reference shbx // SHBX
  Reference cameras // CAM_
  Reference unknown20 // U16_
  Reference materialMaps // MATM
  Reference standardMaterials // MAT_
  Reference displacementMaterials // DIS_
  Reference compositeMaterials // CMP_
  Reference terrainMaterials // TER_
  Reference volumeMaterials // VOL_
  Reference volumeNoiseMaterials // VON_
  Reference creepMaterials // CREP
  
  if (version > 24) {
    Reference unknown21 // ?
  }
  
  if (version > 25) {
    Reference unknown22 // ?
  }
   
  Reference particleEmitters // PAR_
  Reference particleEmitterCopies // PARC
  Reference ribbonEmitters // RIB_
  Reference projections // PROJ
  Reference forces // FOR_
  Reference warps // WRP_
  Reference unknown23 // ?
  Reference rigidBodies // PHRB
  Reference unknown24 // ?
  Reference physicsJoints // PHYJ
  Reference unknown25 // ?
  Reference ikjt // IKJT
  Reference unknown26 // ?
  
  if (version > 24) {
    Reference unknown27 // ?
  }
  
  Reference patu // PATU
  Reference trgd // TRGD
  Reference initialReference // IREF
  SSGS tightHitTest
  Reference fuzzyHitTestObjects // SSGS
  Reference attachmentVolums // ATVL
  Reference attachmentVolumesAddon0 // U16_
  Reference attachmentVolumesAddon1 // U16_
  Reference bbsc // BBSC
  Reference tmd // TMD_
  uint32 unknown28
  Reference unknown29 // U32_
}

As you can see, many references reference to primitive data types. For example, U16_ is a chunk of unsigned shorts.
The confusing thing is that the vertices chunk uses the name U8__, while it has nothing to do with unsigned bytes. Read this post for an explanation.

This is a list of the primitive types referenced throughout the format:
C++:
CHAR {
  uint8 x
}

U8__ {
   uint8 x
}

U16_ {
  uint16 x
}

U32_ {
  uint32 x
}

I32_ {
  int32 x
}

When you first start to make a parser, you can read all of the references as Reference objects. When you want to actually implement a chunk, you can then use the reference to construct the actual data.

All animation referencing follows the same structure called an AnimationReference, this is how it looks:
C++:
AnimationReference {
  uint16 interpolationType
  uint32 animId
  X initValue
  X nullValue
  int32 unknown0
}
X detonates the data type of the animation reference, e.g.: 3D vector (translations and scales), quaternion (rotations), and so on.
Whenever an animation reference is used, I will write it as "AnimationReference name // data type", for example, "AnimationReference translation // float[3]".

Animation references, after some work, will end up referencing into data of sequence data chunks, or SD.
The SD chunk looks as follows:
C++:
SD {
  Reference keys // I32_
  uint32 flags
  uint32 biggestKey
  Reference values // X
}
X detonates the data type a SD contains.
There are 13 different SD chunks, each with its own type, and they are listed below the STC_ chunk, which holds them.
For example, when you see SD2V, it means it is the data type of the values reference of the SD chunk.

Now to all the other chunks.
They will be listed in the order they show up in MODL.
If a chunk uses subchunks or objects, they will be listed right after it.
Some of the types (noteably vertices) need utility functions to transform their data correctly, these functions will be discussed later on.
C++:
SEQS {
  int32 unknown0
  int32 unknown1
  Reference name // CHAR
  uint32 animationStart
  uint32 animationEnd
  float movementSpeed
  uint32 flags // 0x1: not looping
               // 0x2: always global
               // 0x8: global in previewer
  uint32 frequency
  uint32 unknown2
  uint32 unknown3
  uint32 unknown4
  
  if (version < 2) {
    uint32 unknown5
  }
  
  BNDS boundingSphere
  uint32 unknown6
  uint32 unknown7
  uint32 unknown8
}

BNDS {
  float[3] minBorder
  float[3] maxBorder
  float radius
}

STC_ {
  Reference name // CHAR
  uint16 runsConcurrent
  uint16 priority
  uint16 stsIndex
  uint16 stsIndexCopy
  Reference animIds // U32_
  Reference animRefs // U32_
  uint32 unknown0
  SD sdev // SDEV
  SD sd2v // SD2V
  SD sd3v // SD3V
  SD sd4q // SD4Q
  SD sdcc // SDCC
  SD sdr3 // SDR3
  SD unknown1 // ?
  SD sds6 // SDS6
  SD sdu6 // SDU6
  SD unknown2 // ?
  SD unknown3 // ?
  SD sdfg // SDFG
  SD sdmb // SDMB
}

SDEV {
  Reference name // CHAR
  int32 unknown0
  int16 unknown1
  uint16 unknown2
  Matrix matrix
  int32 unknown3
  int32 unknown4
  int32 unknown5
  
  if (version > 0) {
    int32 unknown6
    int32 unknown7
  }
  
  if (version > 1) {
    int32 unknown8
  }
}

SD2V {
  float x
  float y
}

SD3V {
  float x
  float y
  float z
}

SD4Q {
  float x
  float y
  float z
  float w
}

SDCC {
  uint8 r
  uint8 g
  uint8 b
  uint8 a
}

SDR3 {
  float x
}

SDS6 {
  int16 x
}

SDU6 {
  uint16 x
}

SDFG {
  uint32 x
}

SDMB {
  BNDS boundingSphere
}

STG_ {
  Reference name // CHAR
  Reference stcIndices // U32_
}

STS_ {
  Reference animIds // U32_
  int32 unknown0
  int32 unknown1
  int32 unknown2
  int16 unknown3
  uint16 unknown4
}

BONE {
  int32 unknown0
  Reference name // CHAR
  uint32 flags // 0x1: inherit translation
               // 0x2: inherit scale
               // 0x4: inherit rotation
               // 0x10: billboard 1
               // 0x40: billboard 2
               // 0x100: 2D projection
               // 0x200: animated
               // 0x400: inverse kinematics
               // 0x800: skinned
               // 0x2000: real
  int16 parent
  uint16 unknown1
  AnimationReference location // float[3]
  AnimationReference rotation // float[4]
  AnimationReference scale // float[3]
  AnimationReference visibility // uint32
}

// The real U8__ is unsigned byte
U8__ {
  float[3] position
  uint8[4] boneWeights
  uint8[4] boneLookupIndices
  uint8[4] normal
  int16[?][2] uvs // based on vertexFlags
  uint8[4] tangent
}

DIV_ {
  Reference triangles // U16_
  Reference regions // REGN
  Reference batches // BAT_
  Reference boundingShapes // MSEC
  uint32 unknown0
}

REGN {
  uint32 unknown0
  uint32 unknown1
  uint32 firstVertexIndex
  uint32 verticesCount
  uint32 firstTriangleIndex
  uint32 triangleIndicesCount
  uint16 bonesCount
  uint16 firstBoneLookupIndex
  uint16 boneLookupIndicesCount
  uint16 unknown2
  uint8 boneWeightPairsCount
  uint8 unknown3
  uint16 rootBoneIndex
}

BAT_ {
  uint32 unknown0
  uint16 regionIndex
  uint32 unknown1
  uint16 materialReferenceIndex
  uint16 unknown2
}

MSEC {
  uint32 unknown0
  AnimationReference boundings // BNDS
}

ATT_ {
  int32 unknown0
  Reference name // CHAR
  uint32 bone
}

LITE {
  uint8 type
  uint8 unknown0
  int16 bone
  uint32 flags // 0x1: cast shadows
               // 0x2: specular
               // 0x4: ?
               // 0x8: turn on
  uint32 unknown1
  int32 unknown2
  AnimationReference lightColor // float[3]
  AnimationReference lightIntensity // float
  AnimationReference specularColor // float[3]
  AnimationReference specularIntensity // float
  AnimationReference attenuationFar // float
  float unknown3
  AnimationReference attenuationNear // float
  AnimationReference hotSpot // float
  AnimationReference falloff // float
}

SHBX {
  ?
}

CAM_ {
  uint32 bone
  Reference name // CHAR
  AnimationReference filedOfView // float
  uint32 unknown0
  AnimationReference farClip // float
  AnimationReference nearClip // float
  AnimationReference clip2 // float
  AnimationReference focalDepth // float
  AnimationReference falloffStart // float
  AnimationReference falloffEnd // float
  AnimationReference depthOfField // float
}

MATM {
  uint32 materialType
  uint32 materialIndex
}

LAYR {
  uint32 unknown0
  Reference imathPath // CHAR
  AnimationReference color // float[3]
  uint32 flags // 0x4: texture wrap x
               // 0x8: texture wrap y
               // 0x10: invert color
               // 0x20: clamp
               // 0x100: use particle flipbook
               // 0x400: use color from animation
  uint32 uvSource // 0: explicit unwrap 0
                  // 1: explicit unwrap 1
                  // 2: reflective cube env
                  // 3: reflective sphere env
                  // 4: planar local z
                  // 5: planar world z
                  // 6: particle flipbook
                  // 7: cubic env
                  // 8: sphere env
                  // 9: explicit unwrap 3
                  // 10: explicit unwrap 4
                  // 11: planar local x
                  // 12: planar local y
                  // 13: planar world x
                  // 14: planar world y
                  // 15: screen space
                  // 16: tri planar local
                  // 17: tri planar world
                  // 18: tri planar world local z
  uint32 colorChannels // 0: RGB
                       // 1: RGBA
                       // 2: A
                       // 3: R
                       // 4: G
                       // 5: B
  AnimationReference rgbMultiply // float
  AnimationReference rgbAdd // float
  uint32 unknown1
  float perlinNoiseAmplifier
  float perlinNoiseFrequency
  uint32 replaceableChannel // -1 no replaceable
                            // 0: channel 1
                            // 1: channel 2
                            // 2: channel 3
                            // 3: channel 4
                            // 4: channel 5
                            // 5: channel 6
                            // 6: channel 7
  uint32 unknown4
  uint32 unknown5
  uint32 unknown6
  AnimationReference unknown7 // uint32
  AnimationReference unknown8 // float
  uint32 flipbookRows
  uint32 flipbookColumns
  AnimationReference flipbookFrame // uint16
  AnimationReference uvOffset // float[2]
  AnimationReference uvAngle // float[3]
  AnimationReference uvTiling // float[2]
  AnimationReference unknown9 // uint32
  AnimationReference unknown10 // uint32
  AnimationReference brightness // float
  
  if (version > 23) {
    AnimationReference triPlannarOffset // float[3]
    AnimationReference triPlannarScale // float[3]
  }
  
  uint32 unknown11
  uint32 fresnelType
  float fresnelExponent
  float fresnelMin
  float fresnelMax
  float unknown12
  float unknown13
  float unknown14
  float fresnelMask
  float fresnelRotation
}

MAT_ {
  Reference name // CHAR
  uint32 specialFlags // 0x1: use depth blend
                      // 0x4: use vertex color
                      // 0x8: use vertex alpha
                      // 0x200: use transparent shadows
  uint flags // 0x1: use vertex color
             // 0x2: use vertex alpha
             // 0x4: unfogged
             // 0x8: two sided
             // 0x10: unshaded
             // 0x20: no shadows cast
             // 0x40: no hit test
             // 0x80: no shadows recieved
             // 0x100: depth prepass
             // 0x200: use terrain HDR
             // 0x800: splat UV fix
             // 0x1000: soft blending
             // 0x4000: transparent shadows
             // 0x8000: decal lighting
             // 0x10000: transparency affects depth
             // 0x20000: local lights on transparencies
             // 0x40000: disable soft depth blend
             // 0x80000: double lambert
             // 0x100000: hair layer sorting
             // 0x200000: accepts splats
             // 0x400000: decal required on low end
             // 0x800000: emissive required on low end
             // 0x1000000: specular required on low end
             // 0x2000000: accepts splats only
             // 0x4000000: is background object
             // 0x10000000: zfill required on low end
             // 0x20000000: exclude from highlighting
             // 0x40000000: clamp output
             // 0x80000000: visible
  uint32 blendMode // 0: opaque
                   // 1: blend
                   // 2: additive
                   // 3: addAlpha
                   // 4: mod
                   // 5: mod2x
  int32 priority
  uint32 unknown0
  float specularity
  float depthBlend
  uint8 alphaTestThreshold
  uint8 unknown1
  uint8 unknown2
  uint8 unknown3
  float specMult
  float emisMult
  Reference diffuseLayer // LAYR
  Reference decalLayer // LAYR
  Reference specularLayer // LAYR
  
  if (version > 15) {
    Reference glossLayer // LAYR
  }
  
  Reference emissiveLayer // LAYR
  Reference emissive2Layer // LAYR
  Reference diffuseLayer // LAYR
  Reference evioLayer // LAYR
  Reference evioMaskLayer // LAYR
  Reference alphaMaskLayer // LAYR
  Reference alphaMask2Layer // LAYR
  Reference normalLayer // LAYR
  Reference heightLayer // LAYR
  Reference lightMapLayer // LAYR
  Reference ambientOcclusionLayer // LAYR
  uint32 unknown4
  uint32 layerBlendType
  uint32 emisBlendType
  uint32 emisMode
  uint32 specrType
  AnimationReference parallaxHeight // float
  AnimationReference unknown6 // float
}

DIS_ {
  Reference name // CHAR
  uint32 unknown0
  AnimationReference strengthFactor // float
  Reference normalLayer // LAYR
  Reference strengthLayer // LAYR
  uint32 flags
  int32 priority
}

CMP_ {
  Reference name // CHAR
  uint32 unknown0
  Reference sections // CMS_
}

CMS_ {
  uint32 materialReferenceIndex
  AnimationReference alphaFactor // float
}

TER_ {
  Reference name // CHAR
  Reference terrainLayer // LAYR
  
  if (version > 0) {
    uint32 unknown0
  }
}

VOL_ {
  Reference name // CHAR
  uint32 blendMode
  uint32 falloffType
  AnimationReference volumeDensity // float
  Reference colorDefiningLayer // LAYR
  Reference noiseLayer1 // LAYR
  Reference noiseLayer2 // LAYR
  uint32 unknown0
  uint32 unknown1
}

VON_ {
  Reference name // CHAR
  uint32 falloffType // 0: linear
                        // 1: exponential
  uint32 flags // 0x1: draw before transparency
  AnimationReference volumeDensity // foat
  AnimationReference nearPlane // float
  AnimationReference falloff // float
  Reference colorDefiningLayer // ?
  Reference noiseLayer1 // ?
  Reference noiseLayer2 // ?
  AnimationReference scrollRate // float[3]
  AnimationReference translation // float[3]
  AnimationReference scale // float[3]
  AnimationReference rotation // float[3]
  uint32 alphaThreshold
  uint32 unknown0
}

CREP {
  Reference name // CHAR
  Reference creepLayer // LAYR
}

PAR_ {
  uint32 bone
  uint32 materialReferenceIndex
  
  if (version > 16) {
    uint32 additionalFlags // 0x1: randomize with emission speed 2
                           // 0x2: randomize with lifespan 2
                           // 0x4: randomize with mass 2
                           // 0x8: trailing enabled
  }
  
  AnimationReference emissionSpeed1 // float
  AnimationReference emisisonSpeed2 // float
  
  if (version < 13) {
    uint32 randomizeWithEmissionSpeed2
  }
  
  AnimationReference emissionAnbleX // float
  AnimationReference emissionAnbleY // float
  AnimationReference emissionSpreadX // float
  AnimationReference emissionSpreadY // float
  AnimationReference lifespan1 // float
  AnimationReference lifespan2 // float
  
  if (version < 13) {
    uint32 randomizeWithLifespan2
  }
  
  Reference unknown0 // ?
  float zAcceleration
  float sizeAnimationMiddle
  float colorAnimationMiddle
  float rotationAnimationMiddle
  
  if (version > 16) {
    float sizeHoldTime
    float colorHoldTime
    float alphaHoldTime
    float rotationHoldTime
  }
  
  AnimationReference particleSizes1 // float[3]
  AnimationReference rotationValues1 // float[3]
  AnimationReference initialColor1 // uint8[4]
  AnimationReference middleColor1 // uint8[4]
  AnimationReference finalColor1 // uint8[4]
  float slowdown
  float mass
  float mass2
  float unknown1
  
  if (version < 13) {
    uint32 unknown2
    uint32 trailingEnabled
  }
  
  uint16 localForceChannels
  uint16 worldForceChannels
  uint16 forceChannelsFillerBytes
  uint16 worldForceChannelsCopy
  float noiseAmplitude
  float noiseFrequency
  float noiseCohesion
  float noiseEdge
  uint32 indexPlusHighestIndex
  uint32 maxParticles
  AnimationReference emissionRate // float
  uint32 emissionAreaType
  AnimationReference emissionAreaSize // float[3]
  AnimationReference emissionAreaCutoutSize // float[3]
  AnimationReference emissionAreaRadius // float
  AnimationReference emissionAreaCutoutRadius // float
  
  if (version > 16) {
    Reference unknown3 // U32_
  }
  
  uint32 emissionType
  uint32 randomizeWithParticleSizes2
  AnimationReference particleSizes2 // float[3]
  uint32 randomizeWithRotationValues2
  AnimationReference rotationValues2 // float[3]
  uint32 randomizeWithColor2
  AnimationReference initialColor2 // uint8[4]
  AnimationReference middleColor2 // uint8[4]
  AnimationReference finalColor2 // uint8[4]
  uint32 unknown4
  AnimationReference partEmit // int16
  uint8 phase1StartImageIndex
  uint8 phase1EndImageIndex
  uint8 phase2StartImageIndex
  uint8 phase2EndImageIndex
  float relativePhase1Length
  uint16 numberOfColumns
  uint16 numberOfRows
  float columnWidth
  float rowHeight
  float unknown5
  float unknown6
  float unknown7
  byte[20] unknown8
  uint32 particleType // ?
  float lengthWidthRatio
  byte[8] unknown9
  float unknown10
  
  if (version > 16) {
    float unknown11
  }
  
  uint32 unknown12
  AnimationReference unknown13 // float
  AnimationReference unknown14 // float
  uint32 unknown15
  AnimationReference unknown16 // float
  AnimationReference unknown17 // float
  uint32 unknown18
  AnimationReference unknown19 // float
  AnimationReference unknown20 // float
  uint32 unknown21
  AnimationReference unknown22 // float
  AnimationReference unknown23 // float
  uint32 unknown24
  AnimationReference unknown25 // float
  AnimationReference unknown26 // float
  uint32 unknown27
  AnimationReference unknown28 // uint32
  AnimationReference unknown29 // uint32
  uint32 unknown30
  AnimationReference unknown31 // uint32
  AnimationReference unknown32 // uint32
  uint32 unknown33
  AnimationReference unknown34 // uint32
  AnimationReference unknown35 // uint32
  uint32 unknown36
  AnimationReference unknown37 // uint32
  AnimationReference unknown38 // uint32
  AnimationReference unknown39 // float
  
  if (version > 21) {
    AnimationReference unknown40 // uint32
  }
  
  uint32 flags // 0x1: sort
               // 0x2: collide terrain
               // 0x4: collide objects
               // 0x8: spawn on bounce
               // 0x10: cutout emission area
               // 0x20: inherit emission params
               // 0x40: inherit parent velocity
               // 0x80: sort by z height
               // 0x100: reverse iteration
               // 0x200: smooth rotation
               // 0x400: bezier smooth rotation
               // 0x2000: smooth color
               // 0x4000: bezier smooth color
               // 0x8000: lit parts
               // 0x10000: random flipboo start
               // 0x20000: multiply by gravity
               // 0x40000: clamp tail parts
               // 0x80000: spawn trailing parts
               // 0x100000: fixed length tail parts
               // 0x200000: use vertex alpha
               // 0x400000: model parts
               // 0x800000: swap y zon model parts
               // 0x1000000: scale time by parent
               // 0x2000000: use local time
               // 0x4000000: simulate on init
               // 0x8000000: copy
  
  if (version > 17) {
    uint32 rotationFlags // 0x2: relative rotation
                         // 0x4: always set
  }
  
  if (version > 16) {
    uint32 colorSmoothingType    // 0: linear
    uint32 sizeSmoothingType     // 1: smooth
    uint32 rotationSmoothingType // 2: bezier
                                 // 3: linear hold
                                 // 4: bezier hold
    AnimationReference unknown41 // float
    AnimationReference unknown42 // float[2]
    AnimationReference unknown43 // float[3]
    AnimationReference unknown44 // float[2]
  }
  
  Reference spawnPoints // SVC3
  float unknown45
  uint32 unknown46
  uint32 unknown47
  AnimationReference unknown48 // uint32
  AnimationReference unknown49 // float
  int32 trailingParticleIndex
  float trailingParticleChance
  AnimationReference trailingParticlesRate // float
  byte[8] unknown50
  Reference usedModel // SCHR
  Reference copyIndices // U32_
}

SVC3 {
  float x
  float y
  float z
}

SCHR {
  Reference path // CHAR
}

PARC {
  AnimationReference emissionRate // float
  AnimationReference partEmit // int16
  uint32 bone
}

RIB_ {
  uint8 bone
  uint8 short1
  uint8 short2
  uint8 short3
  uint32 materialReferenceIndex

  if (version > 7) {
    uint32 unknown0
  }

  AnimationReference waveLength // float
  AnimationReference unknown1 // uint32

  if (version < 7) {
    int32 unknown2
  }

  AnimationReference unknown3 // float
  AnimationReference unknown4 // float
  AnimationReference unknown5 // float
  AnimationReference unknown6 // float
  AnimationReference unknown7 // float
  AnimationReference unknown8 // float
  uint32 unknown9
  uint32 unknown10
  float unknown11
  float unknown12
  float tipOffsetZ
  float centerBias
  float unknown13
  float unknown14
  float unknown15

  if (version > 7) {
    Reference unknown16 // ?
  }

  AnimationReference radiusScale // float[3]
  AnimationReference twist // float
  uint32 unknown17
  uint32 unknown18
  uint32 unknown19
  uint32 unknown20
  AnimationReference baseColoring // uint8[4]
  AnimationReference centerColoring // uint8[4]
  AnimationReference tipColoring // uint8[4]
  float stretchAmount

  if (version < 7) {
    float unknown21
  }

  if (version > 7) {
    float unknown22
  }
  
  float stretchLimit
  float unknown23
  float unknown24
  uint32 unknown25
  
  if (version < 7) {
    uint32 unknown26
    uint32 unknown27
  }
  
  float surfaceNoiseAmplitude
  float surfaceNoiseNumberOfWaves
  float surfaceNoiseFrequency
  float surfaceNoiseScale
  uint32 unknown28
  uint32 type
  uint32 unknown29
  float ribbonDivisions
  uint32 ribbonSides
  float unknown30
  AnimationReference ribbonLength // float
  
  if (version < 7) {
    int32 filler1
  }
  
  Reference srib // SRIB
  AnimationReference unknown31 // uint32
  uint32 flags // 0x2: collide with terrain
               // 0x4: collide with objects
               // 0x8: edge falloff
               // 0x10: inherit parent velocity
               // 0x20: smooth size
               // 0x40: bezier smooth size
               // 0x80: use vertex alpha
               // 0x100: scale time by parent
               // 0x200: force legacy
               // 0x400: use locale time
               // 0x800: simulate on initialization
               // 0x1000: use length and time
  uint32 unknown32
  float unknown33
  uint32 unknown34
  uint32 unknown35
  
  if (version > 7) {
    byte[8] unknown36
  }
  
  uint32 directionVariationBool
  AnimationReference directionVariationAmount // float
  AnimationReference directionVariationFrequency // float
  uint32 amplitudeVariationBool
  AnimationReference amplitudeVariationAmount // float
  AnimationReference amplitudeVariationFrequency // float
  uint32 lengthVariationBool
  AnimationReference lengthVariationAmount // float
  AnimationReference lengthVariationFrequency // float
  uint32 radiusVariationBool
  AnimationReference radiusVariationAmount // float
  AnimationReference radiusVariationFrequency // float
  int32 unknown37
  AnimationReference unknown38 // float
  AnimationReference unknown39 // float
  AnimationReference unknown40 // float
  AnimationReference unknown41 // float
}

SRIB {
  ?
}

PROJ {
  uint32 unknown0
  uint32 bone
  uint32 materialReferenceIndex // Maybe
  AnimationReference unknown1 // float[3]
  AnimationReference unknown2 // uint32
  AnimationReference unknown3 // uint32
  AnimationReference unknown4 // uint32
  AnimationReference unknown5 // uint32
  AnimationReference unknown6 // uint32
  AnimationReference unknown7 // uint32
  AnimationReference unknown8 // uint32
  AnimationReference unknown9 // float
  AnimationReference unknown10 // float
  AnimationReference unknown11 // float
  AnimationReference unknown12 // float
  AnimationReference unknown13 // float
  AnimationReference unknown14 // float
  uint32 unknown15
  float unknown16
  float unknown17
  float unknown18
  float unknown19
  uint32 unknown20
  float unknown21
  uint32 unknown22
  float unknown23
  uint32 unknown24
  float unknown25
  AnimationReference unknown26 // uint32
  uint32 unknown27
  uint32 unknown28
  uint32 unknown29
  uint32 unknown30
}

FOR_ {
 uint32 type
 uint32 unknown0
 uint32 unknown1
 uint32 bone
 uint32 unknown2
 uint32 forceChannels
 AnimationReference forceStrength // float
 AnimationReference forceRange // float
 AnimationReference unknown3 // float
 AnimationReference unknown4 // float
}

WRP_ {
  uint32 unknown0
  uint32 bone
  uint32 unknown1
  AnimationReference radius // float
  AnimationReference unknown2 // float
  AnimationReference compressionStrength // float
  AnimationReference unknown3 // float
  AnimationReference unknown4 // float
  AnimationReference unknown5 // float
}

PHRB {
  if (version < 3) {
    float[15] unknown0To14
    uint16 bone
    uint16 boneUnused
    byte[15] unknown15
  }
  
  if (version > 3) {
    uint16 unknown16
    uint16 unknown17
    uint32 unknown18
    uint32 bone
    float unknown19
    float unknown20
    float unknown21
    float unknown22
    float unknown23
    float unknown24
    AnimationReference unknown25 // uint32
    float unknown26
  }
  
  Reference physicsShapes // PHSH
  uint32 flags // 0x1: collideable
               // 0x2: walkable
               // 0x4: stackable
               // 0x8: simulate on collision
               // 0x10: ignore local bodies
               // 0x20: always exists
               // 0x80: do not simulate
  uint16 localForces // 0x1: 1
                     // 0x2: 2
                     // 0x4: 3
                     // 0x8: 4
                     // 0x10: 5
                     // 0x20: 6
                     // 0x40: 7
                     // 0x80: 8
                     // 0x100: 9
                     // 0x200: 10
                     // 0x400: 11
                     // 0x800: 12
                     // 0x1000: 13
                     // 0x2000: 14
                     // 0x4000: 15
                     // 0x8000: 16
  uint16 worldForces // 0x1: wind
                     // 0x2: explosion
                     // 0x4: energy
                     // 0x8: blood
                     // 0x10: magnetic
                     // 0x20: grass
                     // 0x40: brush
                     // 0x80: trees
  uint32 priority
}

PHSH {
  float[16] matrix
  
  if (version < 2) {
    uint32 unknown0
  }
  
  uint8 shape // 0: box
              // 1: sphere
              // 2: capsule
              // 3: cylinder
              // 4: convex hull
              // 5: mesh
  uint8 unknown1
  uint16 unknown2
  
  if (version < 2) {
    Reference vertices // VEC3
    Reference unknown3 // U8__
    Reference triangles // U16_
    Reference planeEquations // VEC4
  }
  
  if (version > 2) {
    byte[24] unknown4
  }
  
  float size0
  float size1
  float size2
  
  if (version > 2) {
    Reference unknown5 // VEC3
    Reference unknown6 // VEC4
    Reference unknown7 // DMSE
    Reference unknown8 // U8__
    Reference unknown9 // ?
    uint32 unknown10
    uint32 unknown11
    uint32 unknown12
    uint32 unknown13
    uint32 unknown14
    byte[84] unknown15
    uint32 unknown16
    uint32 unknown17
    uint32 unknown18
    uint32 unknown19
    uint32 unknown20
    uint32 unknown21
    uint32 unknown22
    uint32 unknown23
  }
}

VEC3 {
  float x
  float y
  float z
}

VEC4 {
  float x
  float y
  float z
  float w
}

DMSE {
  uint8 unknown0
  uint8 i0
  uint8 i1
  uint8 i2
}

PHYJ {
  ?
}

IKJT {
  ?
}

PATU {
  ?
}

TRGD {
  Reference unknown0 // U32_
  Reference name // CHAR
}

IREF {
  float[16] matrix
}

SSGS {
  uint32 shape // 0: cube
               // 1: sphere
               // 2: cylinder
  int16 bone
  uint16 unknown0
  float[16] matrix
  uint32 unknown1
  uint32 unknown2
  uint32 unknown3
  uint32 unknown4
  uint32 unknown5
  uint32 unknown6
  float size0
  float size1
  float size2
}

ATVL {
  uint32 bone0
  uint32 bone1
  uint32 type
  uint32 bone2
  float[16] matrix
  Reference unknown0 // VEC3
  Reference unknown1 // U16_
  float size0
  float size1
  float size2
}

BBSC {
  ?
}

TMD_ {
  ?
}

Handling divisions

While the DIV_ reference is a normal reference, there is actually only one DIV_ chunk.
This chunk holds all the chunks that will bridge between the vertices and triangle indices to form regions.

The list of triangles is right there in the triangles unsigned short reference.

Each chunk in the regions reference of type REGN, will describe a geometry mesh, much like MDX's geoset.

The batch reference holds BAT_ chunks. Each one maps between regions and the materials they are using.

Finally, the boundingShapes reference holds MSEC objects which are the bounding shapes of all the different regions.

Handling regions

To construct the real regions, you need to take into account their offsets.

The firstVertexIndex variable, for example, is an index into the global vertex chunk (the U8__ reference, remember?).
That is, if we have a triangle index 5, it in fact points to vertices[region.firstVertexIndex + 5].

The same goes for firstTriangleIndex, and firstBoneLookupIndex.

Handling the bone lookup table

The bone lookup indices do not actually point to bones at all.
They are indices into the bone lookup reference belonging to MODL.

To get the first actual bone index of a vertex, you need the following:
C++:
boneIndex = MODL.boneLookup[region.firstBoneLookupIndex + vertex.boneLookupIndices[0]]

The region.boneWeightPairsCount variable tells you how many bones (up to 4 of course) any specific vertex actually uses.

Handling vertices

Vertices in particular need transformations for their data.
Specifically, the normal, uvs and tangent.

For the normal and tangent, you must apply the following equation for each axis:
C++:
x = (x / 255.0) * 2.0) - 1.0

For the uvs, each axis needs to be divided by 2048:
C++:
x = x / 2048.0

Handling triangles

After all of the above data, this is the easiest. Just remember adding the region offset and you are fine.
C++:
triangleIndex[i] = division.triangles[region.firstTriangleIndex + i]

Handling materials

Each batch chunk, or BAT_, holds the index of the region to use for the batch, and the material index.
This material index doesn't actually point to an actual material, but rather into the material map reference, or MATM.

Each material map has a material type, and material index.
The material type starts with 1, being standard materials, 2 being displacement materials, and so on, in the same order as they are read in the MODL chunk.
The material index, is the index into the appropriate material reference.
That is, if a batch references material index 1, and we see that MODL.materialMap[1] has materialType 1 and materialIndex 3, then the correct material is MODL.standardMaterials[3].

Sequences, STC, STG and STS chunks

The SEQS chunk holds mostly sequence metadata.
The STC chunks are the chunks that actually hold all the animation data, more about using them in the following section.
STS chunks give an easy way to check if we have valid animation data, given an animation reference and a frame of animation. More about them after STC chunks.
STG chunks are the chunks that define how the animation should look - it has a list of indices into the STC array, which you will use for animation. More about them after STS.

Handling animation references - STC

Every animation reference has an animId variable.
This variable holds a random unique number, which lets us figure where the actual data is.

For each STC object, there is a list of animIds and animRefs.

First, you take your animId value, and search for the equivalent value in the STC animIds list.
If you found it, then save the index in the list where you found it.
For example, if STC.animIds[2] == animationReference.animId, then save the index 2.

In case the STC doesn't have the animId, return the animation reference's initValue variable.

Now, I wrote that STC.animRefs is a U32_ reference, because it is, but each element is in fact two separate unsigned short numbers.
Because of this, you might want to simply read them as a pair of unsigned shorts instead of an unsigned long.

Remember the 2 you saved from animIds?
Now index STC.animRefs with that, so save STC.animRefs[2].

The first part of this animRef is yet another index, into another array, so save it.
Let's say it was 5.
The second part of the animRef is the index of the array you want to index.
Let's say it was 2.

There are 13 different sequence data chunks, that hold all the actual animation data for the STC.
The first one is sdev, the second is sd2v, the third is sd3v, and so on.

Now, since our saved animRef was [5, 2], the correct animation data is from the third (0-based indexing) SD, like so:
STC.sd3v[5]

Finally, once you've got the correct block inside the correct sequence data chunk, you can use its keys (which form the animation's timeline) and values to get the actual animation data.

In code:
C++:
animIds = STC.animIds
animRefId = -1

for (uint32 i = 0; i < animIds.length; i++) {
  if (animIds[i] == animationReference.animId) {
    animRefId = i
  }
}

if (animRefId != -1) {
  animRef = STC.animRefs[animRefId]
  // This part assumes you read all the sequence data chunks into an array, e.g. [sdev, sd2v, sd3v, sd4q, ...]
  sd = STC.SD[animRef[1]]
  sdblock = sd[animRef[0]]
  
  // Now do something with the sequence data block
} else {
  return animationReference.initValue
}

Handling animation references - STS

The only purpose of STS is to do a fast check whether some STC chunk will actually hold any animation data for an animation reference.
Each STC chunk has a reference index to a STS chunk.
If you want to check if a STC has animation data for an animation reference, simply select the relevant STS, and see if it has the animation reference's animId in its animIds list.
C++:
animIds = STS[STC.stsIndex].animIds

for (uint32 i = 0; i < animIds.length; i++) {
  if (animIds[i] == animationReference.animId) {
    // The relevant STC has animation data
  }
}

Handling animation references - STG

STG chunks are the last part of handling animation references.
They hold a list of indices into the STC array. Usually this list will only have one element, but sometimes it will have two, or more (for example, splitbody animations).

When you have an animation reference and want to get data from it, you don't directly access STC chunks.
Instead you do the following:
  1. Get the STG you want.
  2. Loop over all the STC indices it holds.
  3. If a STC chunk has animation data for this animation reference, return it, else continue looping.
  4. If no STC had animation data, return the animation reference's initValue variable.

In code:
C++:
stcIndices = STG.stcIndices

for (uint32 i = 0; i < stcIndices.length; i++) {
  stc = STC[stcIndices[i]]
  sts = STS[stc.stsIndex]
  
  // First check the STS to see if there is animation data
  if (hasAnimationData(sts, animationReference)) {
    // And since there is, get it
    return getAnimationData(stc, animationReference)
  }
}

// Non of the STCs in the list had animation data, so return initValue
return animationReference.initValue

Note: STC chunks have a priority variable. You must always loop over the STC chunks according to priority, highest goes first. So far, however, every model that I tested had STG.stcIndices already sorted by priorities.

Handling bones

The bone chunk reference gives us the skeletal structure of the model.

Generally speaking, to animate a skeleton, you first want to start with the root, transform it, then move on to its children, and to their children, and so on.
So far every model that I checked came sorted, such that a child bone will always come after its parent bone, in the array.
This helps, because there is no need to sort them on every run, instead you just loop over all the bones and transform each one as you loop over it.

To transfrom the bones for some chosen sequence i:
  1. Grab STG i.
  2. Use it as explained in the previous section to get the animation data of the bone's translation, scale, and rotation animation references.
  3. Apply them all in that order to an identity matrix, let's call the resulting matrix the local matrix.
  4. If the bone has a parent, transform it by its parent's world matrix.
  5. Finally the result (if there is no parent, the result is the local matrix) is the bone's world matrix.

C++:
for (uint32 i = 0; i < bones.length; i++) {
  bone = bones[i]
  // Start with an identity matrix
  localMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
  // Assuming getAnimationData() gets correctly animation data from an STG as described above, for some known frame of animation
  translation = getAnimationData(stg, bone.translation)
  scale = getAnimationData(stg, bone.scale)
  rotation = getAnimationData(stg, bone.rotation)
  
  // Transform the local matrix by the above animation data
  // This section is highly dependant on your application code and handiness
  // For the sake of simplicity, let's assume that all the animation data is stored in matrices, in which case we can throw the identity matrix to the garbage
  localMatrix = translation * scale * rotation
  
  if (bone.parent != -1) {
    // If there is a parent, transform the local matrix by it
    bone.worldMatrix = bones[bone.parent].worldMatrix * localMatrix
  } else {
    bone.worldMatrix = localMatrix
  }
}

Before rendering, all the bones will need to have another transformation on them.
For each bone i, transform its world matrix with the i'th matrix from MODL.initialReference.
The purpose of the initial reference matrices is to move the model into bind pose, to which the bone animation is then applied.

C++:
for (uint32 i = 0; i < bones.length; i++) {
  bones[i].finalMatrix = bones[i].worldMatrix * MODL.initialReference[i]
}

Optimizations

  • While I wrote all the material types and all the SD chunks as their own variables, it will make life easier if they were read into an array.
    This way you can directly index that array when you need to get the correct SD or material type.
    For example, instead of STC.sd3v[5], have STC.SD[2][5].
  • When you read in the STC and STS chunks, it is a good idea to transform their animIds arrays into maps.
    Simply take each element of the animIds array into a map, and set it to its index:
    C++:
    animIds = new Map()
    
    // Allows direct checks instead of loops
    for (uint32 i = 0; i < STC.animIds.length; i++) {
      animIds[STC.animIds[i]] = i;
    }
    With this map, when you want to figure out if a STC/STS object has a desired animId from an animation reference, you can get it directly with animIds[animationReference.animId].
  • VEC3 and SVC3 both describe a 3D float vector, use one structure for the both of them.
  • VEC4 and QUAT both describe a 4D float vector, use one structure for the both of them.
  • SCHR holds only a CHAR reference, so you can read it directly as a CHAR reference.
 
Last edited:

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
For example, every M3 starts with a uint32 with the hexadecimel value 0x3433444d. If you break that to bytes, and remember how small endian works, you will get the bytes '4', '3', 'D' and 'M', or if you turn it around, the string "MD34".
You may want to mention the byte order that uint32 is. Platforms using the opposite byte order need to change the byte order of the uint32 or use a constant of the opposite byte order to perform the test.

The confusing thing is that the vertices chunk uses the name U8__, while it has nothing to do with unsigned bytes.
Where did these names come from? I cannot seem to see any hint that they are stored in the file as it appears everything is in a known location (unlike mdx where you had to search using strings). Or are these strings put inside the file but not used for data lookup?
 
Level 29
Joined
Jul 29, 2007
Messages
5,174
You may want to mention the byte order that uint32 is. Platforms using the opposite byte order need to change the byte order of the uint32 or use a constant of the opposite byte order to perform the test.


Where did these names come from? I cannot seem to see any hint that they are stored in the file as it appears everything is in a known location (unlike mdx where you had to search using strings). Or are these strings put inside the file but not used for data lookup?

I stopped caring about big endian years ago when it became obsolete.
In any case, all Blizzard games and files are in small endian (as far as I can see).

The chunk names are tags, exactly like they were in MDX files.
In M3 you don't actually need the tags, since you know what's what, but Blizzard being Blizzard, they still put them.
The tags are members of the index entries structure (sorry, I somehow didn't add them to it above, fixed it).
 
Last edited:
I stopped caring about big endian years ago when it became obsolete.
In any case, all Blizzard games and files are in small endian (as far as I can see).

The chunk names are tags, exactly like they were in MDX files.
In M3 you don't actually need the tags, since you know what's what, but Blizzard being Blizzard, they still put them.
The tags are members of the index entries structure (sorry, I somehow didn't add them to it above, fixed it).

the tags are useful if you want to modify an specific part of the model without adding new bytes. The only problem is that somethings like strings and anim keys aren't static but dynamic so modifying them means that you can't add any extra bytes unless you fix the entry table afterwards.
 
Level 29
Joined
Jul 29, 2007
Messages
5,174
Found an interesting thing while trying to compress M3 files.

All the chunks are aligned to 16 bytes (16, 32, 48...), no matter the actual size of the data, the extra bytes (if any), are padding and don't seem to have any meaning (the padding bytes are all 0xAA).
This might be related to how the MPQ later compresses the files.

If the padding is removed, the files no longer work in SC2 or the editor.

This means that if you want to write a file, you always have to get the real size of a chunk as data + padding:
C++:
int32 size = entries * sizeof(entry) // E.g. for U32_, entries * 4
int32 padding = 16 * ceil(size / 16) - size
int32 realsize = size + padding

Another way to get the size when reading files is to compare the offsets of two adjacent index entries, since they are sorted by offset.
This also removes the need to know the size of the data type if you are not intending to parse it anyway (and thus your code will keep working also if new versions of this data type are introduced).
C++:
int32 realsize = nextEntry.offset - entry.offset

For the last entry, the next entry is in fact the index itself. That's the offset that you get from the MD34 chunk.
C++:
int32 realsize = md34.offset - entry.offset

In addition, it seems like SC2 can't handle missing key frames.
That is, if in a sequence there is a key frame at some frame X, then all sequence data blocks must have a key at X.
This is unlike WC3, where there could be any number of key frames for any sequence data.
If keys are missing, the model is loaded, but it may have weird effects (like the model sinking into the ground).
 
Last edited:
Level 29
Joined
Jul 29, 2007
Messages
5,174
Not that this interests anyone, but here goes.

I realized why the geometry is structured the way it is.

The vertices use the U8__ tag because they aren't parsed in any way, they are read simply as a binary chunk.
Converting the normals, tangents and uvs is done in the GPU shaders, so no pre-processing is needed there.
This binary chunk is uploaded directly to the GPU.

The triangle indices don't need any pre-processing either, which I'll explain why, so they can be directly uploaded to the GPU too.

Now, every region has offsets for the triangle indices and vertices they use.
That is, for every index i, it really points to indices[i + firstTriangleIndex] + firstVertexIndex.
This seems like it should add pre-processing to make this mapping, except there are special variants of draw functions in OpenGL/DirectX that allow to input base offsets for indices, so these offsets are actually just parameters of these draw calls.

Since the offsets are just parameters, it means that when you draw a model, you need only two buffer binds - one for the vertices, one for the triangles.
After that, every region is only a draw call, with no buffer bindings.
This makes rendering very efficient, since binding buffers is relatively slow.

In addition, since the vertices are compressed, the bandwidth is reduced by a lot (60% to be exact, from 80 bytes to 32 bytes per vertex), which helps a lot, since most of the time games are bandwidth-bound.

The bone look-up table exists to allow the code to only upload to the shaders the actual bones.
That is, every M3 bone is the same as a MDX node, in that they are not specifically bones that any vertices are attached to. Rather, they form a node hierarchy. The bone look-up table says which of these nodes are actual bones that vertices are attached to, and these are the ones that need to get to the shaders.
 
Last edited:
Okey, I finished reverse engineering Ribbon Emitters. I still need to figure out SRIBs but they aren't too different

Code:
	struct Light 
	(
		fn tag = M3Lib.tags.LITE,
		version,
		lightType,
		unknown0,
		boneIndex,
		flags,
				-- 0x1 casts shadows
				-- 0x2 specular.
				-- 0x4 affected by ambient occlussion.
				-- 0x8 apply on opaque objects.
				-- 0x10 apply on transparent objects.
		lightLODCut,
			-- 0 - None
			-- 1 - Low
			-- 2 - Medium
			-- 3 - High
		shadowLODCut,
			-- 0 - None
			-- 1 - Low
			-- 2 - Medium
			-- 3 - High
		lightColor,
		lightIntensity,
		specColor,
		specIntensity,
		attenuationFar,
		unknown3,
		attenuationNear,
		hotSpot,
		falloff
	)
	struct RibbonEmitter (
		fn tag = M3Lib.tags.RIB_,
		version,
		boneIndex,
		materialReferenceIndex,
		extraFlags,
			-- 0x8 WorldSpace
		speed,
		unknown1,
		yaw,
		pitch,
		unknown2,
		unknown3,
		lifeTime,
			-- only used if lifeSpanInterpolant is 0
		unknown4,
		unknown5,
		unknown6,
		unknown7,
		gravity,
		scaleMidTime,
		colorMidTime,
		alphaMidTime,
		rotationMidTime,
		-- Hold Times
		scaleHoldTime,
		colorHoldTime, 
		alphaHoldTime,
		rotationHoldTime,
			-- Only if used if its interpolation type is a hold type
		scale,
		rotation,
			-- set up with a tricky fake animation block
			-- if type is 0, then it's not used.
		unknown8,
		unknown9,
		unknown10,
		unknown11,
		startColor,
		midColor, -- if colorMidTime is 1.0 then only midColor and startColor are used
		endColor,
			-- Note on Colors
			-- the alpha is added to them as byte 4.
			-- It's only used if flag 0x80 is on.
		drag,
		mass,
		stretchLimit,
		unknown12,
		localForceChannels,
			-- 16 channels, each active bit represents a force channel
			-- it also has the world channels
		worldChannels,
			-- 0x10000 Wind Channel
			-- 0x20000 Explosion Channel
			-- 0x40000 Energy Channel
			-- 0x80000 Blood Channel
			-- 0x100000 Magnetic Channel
			-- 0x200000 Grass Channel
			-- 0x400000 Brush Channel
			-- 0x800000 Trees Channel
		noiseAmplitude,
		noiseFrequency,
		noiseSpeed,
		noiseEdge,
		unknown13,
		unknown14,
		type,
			-- 0 Planar billboarded
			-- 1 Planar
			-- 2 Cylinder
			-- 3 Star
		lifeSpanInterpolant,
			-- 0 time based lifespan
			-- 1 length based lifespan
		edgesPerSecond,
		edges, --refers to edges on cylinder form
			--used only for cylinder and star
		innerRadius,
			--used only by star emitters
		maxLength,
			-- only used if lifeSpanInterpolant is 1
		splineEndPoints,
			-- reference to SRIB
		active,
		flags,
			-- 0x2 collideWithTerrain 
			-- 0x4 collideWithObjects 
			-- 0x8 edgeFalloff 
			-- 0x10 inheritParentVelocity 
			-- 0x20 smoothSize 
			-- 0x40 bezierSmoothSize 
			-- 0x80 useVertexAlpha //when alpha is not always 1
			-- 0x100 scaleTimeByParent 
			-- 0x200 forceLegacy 
			-- 0x400 useLocaleTime 
			-- 0x800 simulateOnInitialization 
			-- 0x1000 useLengthAndTime
			-- 0x2000 Accurate GPU Tangents
		scaleInterpolationType,
			-- 0 Linear
			-- 1 Smooth //Probably TCB Hermite with 1,1,1 values
			-- 2 Bezier
			-- 3 Hold Linear // Uses hold Time
			-- 4 Hold Bezier // Uses hold Time
		colorInterpolationType, --IMPORTANT shared by Alpha as well
			-- 0 Linear
			-- 1 Smooth //Probably TCB Hermite with 1,1,1 values
			-- 2 Bezier
			-- 3 Hold Linear // Uses hold Time
			-- 4 Hold Bezier // Uses hold Time
		friction,
		collisionBounce,
		LODReduce,
			-- 0 None
			-- 1 Low
			-- 2 Medium
			-- 3 High
		LODCut,
			-- 0 None
			-- 1 Low
			-- 2 Medium
			-- 3 High
		yawInterpolationType,
			-- 0 None
			-- 1 Sin
			-- 2 Cos
			-- 3 Saw Tooth
			-- 4 Square Wave
			-- 5 Random Noise
			-- 6 Continouos Noise
		yawAmplitude,
		yawFrequency,
		pitchInterpolationType,
			-- 0 None
			-- 1 Sin
			-- 2 Cos
			-- 3 Saw Tooth
			-- 4 Square Wave
			-- 5 Random Noise
			-- 6 Continouos Noise
		pitchAmplitude,
		pitchFrequency,
		speedInterpolationType,
			-- 0 None
			-- 1 Sin
			-- 2 Cos
			-- 3 Saw Tooth
			-- 4 Square Wave
			-- 5 Random Noise
			-- 6 Continouos Noise
		speedAmplitude,
		speedFrequency,
		scaleNoiseInterpolationType,
		scaleNoiseAmplitude,
		scaleNoiseFrequency,
		alphaNoiseInterpolationType,
		alphaNoiseAmplitude,
		alphaNoiseFrequency,
		inheritParentVelocityPercent,
		overlayPhase
		fn read version =
		(
			local r = RibbonEmitter ()
			r.version = version
			r.boneIndex = IO.readUInt32 ()
			r.materialReferenceIndex = IO.readUInt32 ()
			r.extraFlags = IO.readUInt32 ()
			r.speed = AnimationReference.read IO.readFloat
			r.unknown1 = AnimationReference.read IO.readUInt32
			r.yaw = AnimationReference.read IO.readFloat
			r.pitch = AnimationReference.read IO.readFloat
			r.unknown2 = AnimationReference.read IO.readFloat
			r.unknown3 = AnimationReference.read IO.readFloat
			r.lifeTime = AnimationReference.read IO.readFloat
			r.unknown4 = AnimationReference.read IO.readFloat
			r.unknown5 = IO.readUInt32 ()
			r.unknown6 = IO.readUInt32 ()
			r.unknown7 = IO.readFloat ()
			r.gravity = IO.readFloat ()
			r.scaleMidTime = IO.readFloat ()
			r.colorMidTime = IO.readFloat ()
			r.alphaMidTime = IO.readFloat ()
			r.rotationMidTime = IO.readFloat ()
			r.scaleHoldTime = IO.readFloat ()
			r.colorHoldTime = IO.readFloat ()
			r.alphaHoldTime = IO.readFloat ()
			r.rotationHoldTime = IO.readFloat ()
			r.scale = AnimationReference.read IO.readVector3
			r.rotation = AnimationReference.read IO.readFloat
			r.unknown8 = IO.readUInt32 ()
			r.unknown9 = IO.readUInt32 ()
			r.unknown10 = IO.readUInt32 ()
			r.unknown11 = IO.readUInt32 ()
			r.startColor = AnimationReference.read IO.readColor
			r.midColor = AnimationReference.read IO.readColor
			r.endColor = AnimationReference.read IO.readColor
			r.drag = IO.readFloat ()
			r.mass = IO.readFloat ()
			r.stretchLimit = IO.readFloat ()
			r.unknown12 = IO.readFloat ()
			r.localForceChannels = IO.readUInt32 ()
			r.worldChannels = IO.readUInt32 ()
			r.noiseAmplitude = IO.readFloat ()
			r.noiseFrequency = IO.readFloat ()
			r.noiseSpeed = IO.readFloat ()
			r.noiseEdge = IO.readFloat ()
			r.unknown13 = IO.readUInt32 ()
			if (version > 8) then
				r.unknown14 = IO.readUInt32 ()
			r.type = IO.readUInt32 ()
			r.lifeSpanInterpolant = IO.readUInt32 ()
			r.edgesPerSecond = IO.readFloat ()
			r.edges = IO.readUInt32 ()
			r.innerRadius = IO.readFloat ()
			r.maxLength = AnimationReference.read IO.readFloat
			r.splineEndPoints = ReadM3Reference ()
			r.active = AnimationReference.read IO.readUInt32
			r.flags = IO.readUInt32 ()
			r.scaleInterpolationType = IO.readUInt32 ()
			r.colorInterpolationType = IO.readUInt32 ()
			r.friction = IO.readFloat ()
			r.collisionBounce = IO.readFloat ()
			r.LODReduce = IO.readUInt32 ()
			r.LODCut = IO.readUInt32 ()
			r.yawInterpolationType = IO.readUInt32 ()
			r.yawAmplitude = AnimationReference.read IO.readFloat
			r.yawFrequency = AnimationReference.read IO.readFloat
			r.pitchInterpolationType = IO.readUInt32 ()
			r.pitchAmplitude = AnimationReference.read IO.readFloat
			r.pitchFrequency = AnimationReference.read IO.readFloat
			r.speedInterpolationType = IO.readUInt32 ()
			r.speedAmplitude = AnimationReference.read IO.readFloat
			r.speedFrequency = AnimationReference.read IO.readFloat
			r.scaleNoiseInterpolationType  = IO.readUInt32 ()
			r.scaleNoiseAmplitude = AnimationReference.read IO.readFloat
			r.scaleNoiseFrequency = AnimationReference.read IO.readFloat
			r.alphaNoiseInterpolationType = IO.readInt32 ()
			r.alphaNoiseAmplitude = AnimationReference.read IO.readFloat
			r.alphaNoiseFrequency = AnimationReference.read IO.readFloat
			r.inheritParentVelocityPercent = AnimationReference.read IO.readFloat
			r.overlayPhase =AnimationReference.read IO.readFloat
			r
		)
	)
 

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
What about Heroes of the Storm models? They are also m3 but I have yet to get them to load into SC2 so I have a feeling there may be some difference in them. People say they have some extensions that are not supported by StarCraft II (maybe will be in Legacy of the Void?).

I can provide examples if required via PM (as I do not think it is a good idea to put alpha stuff publicly).
 
BlinkBoy looked into it, and he saw a few differences that, without them, the models can be loaded in SC2.
They just have updated versions of some of the chunks (layers, particle emitters, and ribbon emitters).

Actualy they kept updating the format.

They added cloth physics and some other chunks on june. I still need some more samples but yeah the format is continuously changing.
 
Here's an update:
  • Fixed versioning on Ribbon Emitters
  • Added: Spline Ribbon, Projection, Force, Shadow Box, IK Joint Behavior and Rigid Body.

Code:
	struct SplineRibbon
	(
		fn tag = M3Lib.tags.SRIB,
		version,
			-- 0 / documented
		unknown1 = 0,
		unknown2,
		tangent1,
		speed,
		unknowneee1a711,
		boneIndex,
		unknown3,
		unknown4,
		yawNoiseInterpolationType,
		yawNoiseAmplitude,
		yawNoiseFrequency,
		pitchNoiseInterpolationType,
		pitchNoiseAmplitude,
		pitchNoiseFrequency,
		speedNoiseInterpolationType,
		speedNoiseAmplitude,
		speedNoiseFrequency,
		yaw,
		pitch,
		tangent2,
		tangent3,
		fn read version =
		(
			local s = SplineRibbon ()
			s.version = version
			s.unknown1= IO.readUInt32()
			s.unknown2 = IO.readArray 4 IO.readUInt32 
			s.tangent1 = IO.readFloat()
			s.speed = AnimationReference.read IO.readFloat
			s.unknowneee1a711 = IO.readUInt32()
			s.boneIndex = IO.readUInt32()
			s.unknown3 = AnimationReference.read IO.readFloat
			s.unknown4= AnimationReference.read IO.readFloat
			s.yawNoiseInterpolationType = IO.readUInt32()
			s.yawNoiseAmplitude = AnimationReference.read IO.readFloat
			s.yawNoiseFrequency = AnimationReference.read IO.readFloat
			s.pitchNoiseInterpolationType = IO.readUInt32()
			s.pitchNoiseAmplitude = AnimationReference.read IO.readFloat
			s.pitchNoiseFrequency = AnimationReference.read IO.readFloat
			s.speedNoiseInterpolationType = IO.readUInt32()
			s.speedNoiseAmplitude = AnimationReference.read IO.readFloat
			s.speedNoiseFrequency = AnimationReference.read IO.readFloat
			s.yaw = AnimationReference.read IO.readFloat
			s.pitch = AnimationReference.read IO.readFloat
			s.tangent2 = IO.readFloat()
			s.tangent3 = IO.readFloat()
			s
		)
	)
	struct RibbonEmitter (
		fn tag = M3Lib.tags.RIB_,
		version,
			-- 6 / documented
			-- 8 / documented
			-- 9 / documented (Heroes & LotV)
		boneIndex,
		materialReferenceIndex,
		extraFlags,
			-- 0x8 WorldSpace
		speed,
		unknown1,
		yaw,
		pitch,
		unknown2,
		unknown3,
		lifeTime,
			-- only used if lifeSpanInterpolant is 0
		unknown4,
		unknown5,
		unknown6,
		unknown7,
		gravity,
		scaleMidTime,
		colorMidTime,
		alphaMidTime,
		rotationMidTime,
		-- Hold Times
		scaleHoldTime,
		colorHoldTime, 
		alphaHoldTime,
		rotationHoldTime,
			-- Only if used if its interpolation type is a hold type
		scale,
		rotation,
			-- set up with a tricky fake animation block
			-- if type is 0, then it's not used.
		unknown8,
		unknown9,
		unknown10,
		unknown11,
		startColor,
		midColor, -- if colorMidTime is 1.0 then only midColor and startColor are used
		endColor,
			-- Note on Colors
			-- the alpha is added to them as byte 4.
			-- It's only used if flag 0x80 is on.
		drag,
		mass,
		stretchLimit,
		unknown12,
		localForceChannels,
			-- 16 channels, each active bit represents a force channel
			-- it also has the world channels
		worldChannels,
			-- 0x10000 Wind Channel
			-- 0x20000 Explosion Channel
			-- 0x40000 Energy Channel
			-- 0x80000 Blood Channel
			-- 0x100000 Magnetic Channel
			-- 0x200000 Grass Channel
			-- 0x400000 Brush Channel
			-- 0x800000 Trees Channel
		noiseAmplitude,
		noiseFrequency,
		noiseSpeed,
		noiseEdge,
		unknown13,
		unknown14,
		type,
			-- 0 Planar billboarded
			-- 1 Planar
			-- 2 Cylinder
			-- 3 Star
		lifeSpanInterpolant,
			-- 0 time based lifespan
			-- 1 length based lifespan
		edgesPerSecond,
		edges, --refers to edges on cylinder form
			--used only for cylinder and star
		innerRadius,
			--used only by star emitters
		maxLength,
			-- only used if lifeSpanInterpolant is 1
		splineEndPoints,
			-- reference to SRIB
		active,
		flags,
			-- 0x2 collideWithTerrain 
			-- 0x4 collideWithObjects 
			-- 0x8 edgeFalloff 
			-- 0x10 inheritParentVelocity 
			-- 0x20 smoothSize 
			-- 0x40 bezierSmoothSize 
			-- 0x80 useVertexAlpha //when alpha is not always 1
			-- 0x100 scaleTimeByParent 
			-- 0x200 forceLegacy 
			-- 0x400 useLocaleTime 
			-- 0x800 simulateOnInitialization 
			-- 0x1000 useLengthAndTime
			-- 0x2000 Accurate GPU Tangents
		scaleInterpolationType,
			-- 0 Linear
			-- 1 Smooth //Probably TCB Hermite with 1,1,1 values
			-- 2 Bezier
			-- 3 Hold Linear // Uses hold Time
			-- 4 Hold Bezier // Uses hold Time
		colorInterpolationType, --IMPORTANT shared by Alpha as well
			-- 0 Linear
			-- 1 Smooth //Probably TCB Hermite with 1,1,1 values
			-- 2 Bezier
			-- 3 Hold Linear // Uses hold Time
			-- 4 Hold Bezier // Uses hold Time
		friction,
		collisionBounce,
		LODReduce,
			-- 0 None
			-- 1 Low
			-- 2 Medium
			-- 3 High
		LODCut,
			-- 0 None
			-- 1 Low
			-- 2 Medium
			-- 3 High
		yawInterpolationType,
			-- 0 None
			-- 1 Sin
			-- 2 Cos
			-- 3 Saw Tooth
			-- 4 Square Wave
			-- 5 Random Noise
			-- 6 Continouos Noise
		yawAmplitude,
		yawFrequency,
		pitchInterpolationType,
			-- 0 None
			-- 1 Sin
			-- 2 Cos
			-- 3 Saw Tooth
			-- 4 Square Wave
			-- 5 Random Noise
			-- 6 Continouos Noise
		pitchAmplitude,
		pitchFrequency,
		speedInterpolationType,
			-- 0 None
			-- 1 Sin
			-- 2 Cos
			-- 3 Saw Tooth
			-- 4 Square Wave
			-- 5 Random Noise
			-- 6 Continouos Noise
		speedAmplitude,
		speedFrequency,
		scaleNoiseInterpolationType,
		scaleNoiseAmplitude,
		scaleNoiseFrequency,
		alphaNoiseInterpolationType,
		alphaNoiseAmplitude,
		alphaNoiseFrequency,
		inheritParentVelocityPercent,
		overlayPhase,
		--V6 Only
		unknownv6_1,
		unknownv6_2,
		unknownv6_3,
		unknownv6_4,
		filler,
		fn read version =
		(
			local r = RibbonEmitter ()
			r.version = version
			r.boneIndex = IO.readUInt32 ()
			r.materialReferenceIndex = IO.readUInt32 ()
			r.extraFlags = IO.readUInt32 ()
			r.speed = AnimationReference.read IO.readFloat
			r.unknown1 = AnimationReference.read IO.readUInt32
			if version <= 6 then
				r.unknownv6_1 = IO.readUInt32
			r.yaw = AnimationReference.read IO.readFloat
			r.pitch = AnimationReference.read IO.readFloat
			r.unknown2 = AnimationReference.read IO.readFloat
			r.unknown3 = AnimationReference.read IO.readFloat
			r.lifeTime = AnimationReference.read IO.readFloat
			r.unknown4 = AnimationReference.read IO.readFloat
			r.unknown5 = IO.readUInt32 ()
			r.unknown6 = IO.readUInt32 ()
			r.unknown7 = IO.readFloat ()
			r.gravity = IO.readFloat ()
			r.scaleMidTime = IO.readFloat ()
			r.colorMidTime = IO.readFloat ()
			r.alphaMidTime = IO.readFloat ()
			r.rotationMidTime = IO.readFloat ()
			r.scaleHoldTime = IO.readFloat ()
			r.colorHoldTime = IO.readFloat ()
			r.alphaHoldTime = IO.readFloat ()
			r.rotationHoldTime = IO.readFloat ()
			r.scale = AnimationReference.read IO.readVector3
			r.rotation = AnimationReference.read IO.readFloat
			r.unknown8 = IO.readUInt32 ()
			r.unknown9 = IO.readUInt32 ()
			r.unknown10 = IO.readUInt32 ()
			r.unknown11 = IO.readUInt32 ()
			r.startColor = AnimationReference.read IO.readColor
			r.midColor = AnimationReference.read IO.readColor
			r.endColor = AnimationReference.read IO.readColor
			r.drag = IO.readFloat ()
			if version <= 6 then
				r.unknownv6_2 = IO.readFloat ()
			if version >= 8 then
				r.mass = IO.readFloat ()
			r.stretchLimit = IO.readFloat ()
			r.unknown12 = IO.readFloat ()
			r.localForceChannels = IO.readUInt32 ()
			r.worldChannels = IO.readUInt32 ()
			if version <= 6 then
			(
				r.unknownv6_3 = IO.readUInt32 ()
				r.unknownv6_4 = IO.readUInt32 ()
			)
			r.noiseAmplitude = IO.readFloat ()
			r.noiseFrequency = IO.readFloat ()
			r.noiseSpeed = IO.readFloat ()
			r.noiseEdge = IO.readFloat ()
			r.unknown13 = IO.readUInt32 ()
			if (version >= 9) then
				r.unknown14 = IO.readUInt32 ()
			r.type = IO.readUInt32 ()
			r.lifeSpanInterpolant = IO.readUInt32 ()
			r.edgesPerSecond = IO.readFloat ()
			r.edges = IO.readUInt32 ()
			r.innerRadius = IO.readFloat ()
			r.maxLength = AnimationReference.read IO.readFloat
			if version <= 6 then
			(
				r.filler = IO.readUInt32 ()
			)
			r.splineEndPoints = ReadM3Reference ()
			r.active = AnimationReference.read IO.readUInt32
			r.flags = IO.readUInt32 ()
			r.scaleInterpolationType = IO.readUInt32 ()
			r.colorInterpolationType = IO.readUInt32 ()
			r.friction = IO.readFloat ()
			r.collisionBounce = IO.readFloat ()
			if version >= 8 then
			(
				r.LODReduce = IO.readUInt32 ()
				r.LODCut = IO.readUInt32 ()
			)
			r.yawInterpolationType = IO.readUInt32 ()
			r.yawAmplitude = AnimationReference.read IO.readFloat
			r.yawFrequency = AnimationReference.read IO.readFloat
			r.pitchInterpolationType = IO.readUInt32 ()
			r.pitchAmplitude = AnimationReference.read IO.readFloat
			r.pitchFrequency = AnimationReference.read IO.readFloat
			r.speedInterpolationType = IO.readUInt32 ()
			r.speedAmplitude = AnimationReference.read IO.readFloat
			r.speedFrequency = AnimationReference.read IO.readFloat
			r.scaleNoiseInterpolationType  = IO.readUInt32 ()
			r.scaleNoiseAmplitude = AnimationReference.read IO.readFloat
			r.scaleNoiseFrequency = AnimationReference.read IO.readFloat
			r.alphaNoiseInterpolationType = IO.readInt32 ()
			r.alphaNoiseAmplitude = AnimationReference.read IO.readFloat
			r.alphaNoiseFrequency = AnimationReference.read IO.readFloat
			r.inheritParentVelocityPercent = AnimationReference.read IO.readFloat
			r.overlayPhase =AnimationReference.read IO.readFloat
			r
		)
	)
	struct Projection
	(
		fn tag = M3Lib.tags.PROJ,
		version,
			-- 4 / documented
			-- 5 / documented
		type,
			-- 1 Orthonormal
			-- 2 Perspective
		boneIndex,
		materialReferenceIndex,
		-- Probably values for a deprecated projection type
		unknown1,
		unknown2,
		unknown3,
		unknown4,
		-- Perspective values
			-- They are 0.0 when Projection is Orthonormal
		fieldOfView,
		aspectRatio,
		near,
		far,
		-- Orthonormal values. 
			-- They are 0.0 when Projection is Perspective
			-- negDepth == -depth and negWidth == - width and negHeight == -height
		negDepth,
		depth,
		negWidth,
		width,
		negHeight,
		height,
		unknown5, -- flags?
		-- Alphas
		startAlpha,
		midAlpha,
		endAlpha,
		-- Splat Life Times
			-- the range value is the varience of the specific lifetime
		splatLifeTimeAttack,
		splatLifeTimeAttackRange,
		splatLifeTimeHold,
		splatLifeTimeHoldRange,
		splatLifeTimeDecay,
		splatLifeTimeDecayRange,
		attenuationPlaneDistance,
		active,
		unknown6 = 0,
		splatLayer,
		-- 	it's a weird set up. It appears that the more bits the value has to its right, it means is a layer below.
			-- layer 0 				0xffff
			-- layer 1 				0x1ffff
			-- layer 2 				0x2ffff
			-- layer 3 				0x3ffff
			-- building Layer 		0x4ffff
			-- AOE Layer 		0x6ffff
			-- power Layer 		0x7ffff
			-- material UI Layer 0x8ffff
			-- hardtile layer 		0xbffff
			-- under creep layer 	0xcffff
		LODReduce,
			-- 0 None
			-- 0x10000 Low
			-- 0x20000 Medium
			-- 0x30000 High
		LODCut,
			-- 0 None
			-- 0x10000 Low
			-- 0x20000 Medium
			-- 0x30000 High
		fn read version =
		(
			local p = Projection ()
			p.type = IO.readUInt32 ()
			p.boneIndex = IO.readUInt32 ()
			p.materialReferenceIndex = IO.readUInt32 ()
			p.unknown1 = AnimationReference.read IO.readVector3
			p.unknown2 = AnimationReference.read IO.readFloat
			p.unknown3 = AnimationReference.read IO.readFloat
			p.unknown4 = AnimationReference.read IO.readFloat
			p.fieldOfView = AnimationReference.read IO.readFloat
			p.aspectRatio = AnimationReference.read IO.readFloat
			p.near = AnimationReference.read IO.readFloat
			p.far = AnimationReference.read IO.readFloat
			p.negDepth = AnimationReference.read IO.readFloat
			p.depth = AnimationReference.read IO.readFloat
			p.negWidth = AnimationReference.read IO.readFloat
			p.width = AnimationReference.read IO.readFloat
			p.negHeight = AnimationReference.read IO.readFloat
			p.height = AnimationReference.read IO.readFloat
			p.unknown5 = IO.readUInt32 ()
			p.startAlpha = IO.readFloat ()
			p.midAlpha = IO.readFloat ()
			p.endAlpha = IO.readFloat ()
			p.splatLifeTimeAttack = IO.readFloat ()
			p.splatLifeTimeAttackRange = IO.readFloat ()
			p.splatLifeTimeHold = IO.readFloat ()
			p.splatLifeTimeHoldRange = IO.readFloat ()
			p.splatLifeTimeDecay = IO.readFloat ()
			p.splatLifeTimeDecayRange =IO.readFloat ()
			p.attenuationPlaneDistance= IO.readFloat ()
			p.active = AnimationReference.read readM3UByte
			p.unknown6 = IO.readUInt32 ()
			p.splatLayer = IO.readUInt32 ()
			p.LODReduce = IO.readUInt32 ()
			p.LODCut = IO.readUInt32 ()
			p
		)
	)
	struct Force
	(
		fn tag = M3Lib.tags.FOR_,
		version,
			-- 1 / documented
			-- 2 / documented
			-- no apparent changes between versions 1 and 2
		type,
			-- 0 Directional
			-- 1 Radial
			-- 2 Dampering
			-- 3 Vortex
		shape,
			-- 0 Sphere {radius}
			-- 1 Cylinder  {radius, height}
			-- 2 Box {width,height,length}
			-- 3 Hemisphere  {radius}
			-- 4 ConeDome  {radius, angle}
		unknown1 = 0,
		boneIndex,
		flags,
			-- 0x01 Use Fallof
			-- 0x02 Use Height Gradient
			-- 0x04 unbounded
		channels,
			-- up to 16 local channels
			-- 17 to 32 - global channels
			-- 0x10000 Wind Channel
			-- 0x20000 Explosion Channel
			-- 0x40000 Energy Channel
			-- 0x80000 Blood Channel
			-- 0x100000 Magnetic Channel
			-- 0x200000 Grass Channel
			-- 0x400000 Brush Channel
			-- 0x800000 Trees Channel
		strength,
		width, --or radius
		height, --or angle in case of ConeDome
		length, --only used on boxes
		fn read version =
		(
			local f = Force ()
			f.version = version
			f.type = IO.readUInt32 ()
			f.shape = IO.readUInt32 ()
			f.unknown1 = IO.readUInt32 ()
			f.boneIndex = IO.readUInt32 ()
			f.flags = IO.readUInt32 ()
			f.channels = IO.readUInt32 ()
			f.strength = AnimationReference.read IO.readFloat
			f.width = AnimationReference.read IO.readFloat
			f.height = AnimationReference.read IO.readFloat
			f.length = AnimationReference.read IO.readFloat
			f
		)
	)
	
	struct ShadowBox
	(
		fn tag = M3Lib.tags.SHBX,
		version,
			-- 0 / documented
		length,
		width,
		height,
		bone,
		flags,
		fn read version =
		(
			local sb = ShadowBox ()
			sb.version = version
			sb.length = AnimationReference.read IO.readFloat
			sb.width = AnimationReference.read IO.readFloat
			sb.height = AnimationReference.read IO.readFloat
			sb.bone = IO.readUInt32()
			sb.flags = IO.readUInt32()
			sb
		)
	)
	
	struct BehaviorIkJoint
	(
		fn tag = M3Lib.tags.IKJT,
		version,
			-- 0 / documented
		unknown1 = 0,
		unknown2 = 0,
		unknown3 = -1, -- always -1
		unknown4, -- 0x0000FFFF or -1
		maxSearchUp,
		maxSearchDown,
		maxSpeed,
		IKGoalPosThreshold,
		fn read version =
		(
			local ik = BehaviorIkJoint ()
			ik.version = version
			ik.unknown1 = IO.readUInt32 ()
			ik.unknown2 = IO.readUInt32 ()
			ik.unknown3 = IO.readUInt32 ()
			ik.unknown4 = IO.readUInt32 ()
			ik.maxSearchUp = IO.readFloat ()
			ik.maxSearchDown = IO.readFloat ()
			ik.maxSpeed = IO.readFloat ()
			ik.IKGoalPosThreshold = IO.readFloat ()
			ik
		)
	)
	
	struct RigidBody
	(
		fn tag = M3Lib.tags.PHRB,
		version,
			-- 2 / not documented, deprecated, used by Havok
			-- 4 / documented, used by Domino Engine
		-- Version 2 Specific
		unknownAt0 = 5.0,
		unknownAt4 = 4.0,
		unknownAt8 = 0.8,
		unknownAt12 = 0.0,
		unknownAt16 = 0.0,
		unknownAt20 = 0.0,
		unknownAt24 = 0.0,
		unknownAt28 = 0.0,
		unknownAt32 = 0.0,
		unknownAt36 = 0.0,
		unknownAt40 = 0.0,
		unknownAt44 = 0.0,
		unknownAt48 = 0.0,
		unknownAt52 = 0.0,
		unknownAt56 = 0.0,
		boneIndexUnused = 0x0,
		unknownAt64,
		-- Version 4 Specific
		boneIndex,
		materialType,
			-- 0 Metal Heavy
		density,
		friction,
		restitution,
		linearDamp,
		angularDamp,
		gravityScale,
		activeState,
		blendOut,
		physicsShapes,
		flags,
			-- 0x1 Collidable
			-- 0x2 Walkable
			-- 0x4 Stackable
			-- 0x8 simulateOnCollision
			-- 0x10 ignoreLocalBodies
			-- 0x20 Uses Level of Detail Priority
			-- 0x40 Inherit Physics State
			-- 0x100 Ignore Event Simulate
			-- 0x200 Uses Custom Material Properties
		localForces,
		worldForces,
		priority,
		fn read version =
		(
			local rb = RigidBody ()
			rb.version = version
			if (version <= 2) then
			(
				rb.unknownAt0 = IO.readFloat ()
				rb.unknownAt4 = IO.readFloat ()
				rb.unknownAt8 = IO.readFloat ()
				rb.unknownAt12 = IO.readFloat ()
				rb.unknownAt16 = IO.readFloat ()
				rb.unknownAt20 = IO.readFloat ()
				rb.unknownAt24 = IO.readFloat ()
				rb.unknownAt28 = IO.readFloat ()
				rb.unknownAt32 = IO.readFloat ()
				rb.unknownAt36 = IO.readFloat ()
				rb.unknownAt40 = IO.readFloat ()
				rb.unknownAt44 = IO.readFloat ()
				rb.unknownAt48 = IO.readFloat ()
				rb.unknownAt52 = IO.readFloat ()
				rb.unknownAt56 = IO.readFloat ()
				rb.boneIndex = IO.readUInt16 ()
				rb.boneIndexUnused = IO.readUInt16 ()
				rb.unknownAt64 = IO.readArray 4 IO.readUInt32
			)
			if (version >= 4) then
			(
				rb.boneIndex = IO.readUInt32 ()
				rb.materialType = IO.readUInt32 ()
				rb.density = IO.readFloat ()
				rb.friction = IO.readFloat ()
				rb.restitution = IO.readFloat ()
				rb.linearDamp = IO.readFloat ()
				rb.angularDamp = IO.readFloat ()
				rb.gravityScale = IO.readFloat ()
				rb.activeState = AnimationReference.read IO.readUInt32
				rb.blendOut = IO.readFloat ()
			)
			rb.physicsShapes = Reference.read -- references a Physics Shape (PHSH)
			rb.flags = IO.readUInt32 ()
			rb.localForces = IO.readUInt16 ()
			rb.worldForces = IO.readUInt16 ()
			rb.priority = IO.readUInt32 ()
			rb
		)
	)

Still pending for reverse engineering:
  • Particle Emitters
  • Physics Shapes (lots of unknowns for convex hull and concave mesh)
  • IK Joint Behavior (Figure out relation between the structure and bones and the meaning of unknown 3,4 (I think it's an axis thing))
  • Ragdoll Behavior
  • Turret Behaviors

Heroes of the Storm recent changes:
  • REGN version 3 to version 4. added 2 bytes, probably a signal for cloth physics.
  • MODL version 26 to version 28. 2 new references.
  • New chunks: PHCL(version 2 size 128 bytes) and PHCC(version 0 siye 76 bytes) Physics Cloth Constraint?

Heroes of the Storm current MODL:
C:
MODL {
  Reference name // CHAR
  uint32 flags // 0x100000: has mesh
  Reference sequences // SEQS
  Reference stc // STC_
  Reference stg // STG_
  float unknown0
  float unknown1
  float unknown2
  float unknown3
  Reference sts // STS_
  Reference bones // BONE
  uint skinBones
  uint32 vertexFlags // 0x20000: 1 texture coordinate
                     // 0x40000: 2 texture coordinates
                     // 0x80000: 3 texture coordinates
                     // 0x100000: 4 texture coordinates
  Reference vertices // U8__
  Reference divisions // DIV_
  Reference boneLookup // U16_
  BoundingSphere boundings
  uint32[16] unknown4To19
  Reference attachmentPoints // ATT_
  Reference attachmentPointAddons // U16_
  Reference lights // LITE
  Reference shbx // SHBX
  Reference cameras // CAM_
  Reference unknown20 // U16_
  Reference materialMaps // MATM
  Reference standardMaterials // MAT_
  Reference displacementMaterials // DIS_
  Reference compositeMaterials // CMP_
  Reference terrainMaterials // TER_
  Reference volumeMaterials // VOL_
  Reference volumeNoiseMaterials // VON_
  Reference creepMaterials // CREP
  
  if (version > 24) {
    Reference unknown21 // ?
  }
  
  if (version > 25) {
    Reference unknown22 // ?
  }

  if (version > 26) {
    Reference unknown25 // ?
  }
   
  Reference particleEmitters // PAR_
  Reference particleEmitterCopies // PARC
  Reference ribbonEmitters // RIB_
  Reference projections // PROJ
  Reference forces // FOR_
  Reference warps // WRP_
  Reference unknown23 // ?
  Reference rigidBodies // PHRB
  Reference unknown24 // ?
  Reference physicsJoints // PHYJ
  if (version > 26) {
    Reference clothPhysics // PHCL
  }
  Reference unknown26 // ?
  Reference ikjt // IKJT
  Reference unknown27 // ?
  
  if (version > 24) {
    Reference unknown28 // ?
  }
  
  Reference patu // PATU
  Reference trgd // TRGD
  Reference initialReference // IREF
  SSGS tightHitTest
  Reference fuzzyHitTestObjects // SSGS
  Reference attachmentVolums // ATVL
  Reference attachmentVolumesAddon0 // U16_
  Reference attachmentVolumesAddon1 // U16_
  Reference bbsc // BBSC
  Reference tmd // TMD_
  uint32 unknown29
  Reference unknown30 // U32_
}

PHP:
PHCL {
    uint32 unknown1
    uint32 unknown2
    ref U16_
    ref U8__
    ref U32_
    ref U32_
    ref PHCC
    ref PHAC
    float[16] unknown3
}

PHCC {
    float[3] unknown1_1
    uint32 unknown1_2
    float[3] unknown2_1
    uint32 unknown2_2
    float[3] unknown3_1
    uint32 unknown3_2
    float[6] unknown4
    uint32 unknown5
}

PHAC {
    uint32 unknown1
    uint32 unknown2
    ref U64_
    ref U32_
}
 
Last edited:
Level 2
Joined
Aug 24, 2015
Messages
19
What is the entries field in IndexEntry used for if it's not for reading since we use the entries field of Reference ? They are often equal but why would Blizzard lose so much space ?
 
Last edited:
Level 29
Joined
Jul 29, 2007
Messages
5,174
First and foremost, all of their file formats are messy and filled with garbage.

In this case, however, it's always good to know how many objects of some specific type you have in total, so that you can allocate memory and read all of them.
The references are exactly what they are called - references into the main arrays of objects.
I wrote my parser so that it starts with the MODL chunk, and reads the objects while parsing (top to bottom), but I am assuming the game first reads all of the objects in the index, and only then goes to MODL and attaches all of the references to actual objects (bottom to top).

References also have a count because they might be slices of the main arrays.
E.g. imagine that you hold all of the animation bone translations in one array, and each reference just sees a small part of that array that's relevant to a specific bone (I don't remember if that's how stuff actually works in M3, because I didn't touch it in ages, but you get the idea).
 
Level 2
Joined
Aug 24, 2015
Messages
19
Thanks.

By the way, in the SEQS block, unknown4 seems to be blendTime.
The client blends (lerp) animation states between animations where the end and start values differ. This specifies how long that blending takes.
Possible values: 0, 50, 100, 150, 200, 250, 300, 350, 500.
I have not checked the rest yet.
Also, I wonder why they still use 4-float quaternions. For WoW they moved from 4-float to 4-short at BC (ten years ago) and I thought they would not keep them in newer games either. Weird weird Blizzard.
I have not asked, but is this still the place with the most up to date M3 doc or are there other websites (perhaps more specialized in SC2) more advanced in the reverse engineering ?
 
Level 29
Joined
Jul 29, 2007
Messages
5,174
As far as I know, BlinkBoy is the only person bothering with M3 at all, although I didn't check any other sites recently.
You can see his updates in this thread, but I never updated the first post with them.
I myself stopped caring a long time ago, it's too bothersome to work with so much unknown data and logical guesses.
 
Thanks.

By the way, in the SEQS block, unknown4 seems to be blendTime.
The client blends (lerp) animation states between animations where the end and start values differ. This specifies how long that blending takes.
Possible values: 0, 50, 100, 150, 200, 250, 300, 350, 500.
I have not checked the rest yet.
Also, I wonder why they still use 4-float quaternions. For WoW they moved from 4-float to 4-short at BC (ten years ago) and I thought they would not keep them in newer games either. Weird weird Blizzard.
I have not asked, but is this still the place with the most up to date M3 doc or are there other websites (perhaps more specialized in SC2) more advanced in the reverse engineering ?

Depends on optimization. Most of the quirks of the format it's because it's data-oriented. Most things in there are for GPU optimization and best use of processor cache. There's a reason for everything. There's even raw data like vertices that is sent directly to gpu after loading.

That's how the engine can handle 500 units being rendered at the same time.
 
Top