WMO

From wowdev
Jump to navigation Jump to search

WMO files contain world map objects. They, too, have a chunked structure just like the WDT files.

There are two types of WMO files, actually:

The root file and the groups are stored with the following filenames:

  • World\wmo\path\WMOName.wmo
  • World\wmo\path\WMOName_NNN.wmo

There is a hardcoded maximum of 512 group files per root object.

This section only applies to versions ≤ PreVanilla (0.5.5.3494).

In the alpha, WMO files were a single file rather than being split into root and group. For that reason the root data has been wrapped in a MOMO chunk followed by the MOGP chunks.

MVER

uint32_t version;     // < PreVanilla (0.6.0.3592) 14, PreVanilla (0.6.0.3592) … < Vanilla 16,  ≥ Vanilla 17

There never have been any additional versions after the alpha, even though the format changed a lot. Classic Blizzard.

WMO root file

The root file lists the following:

  • textures (BLP File references)
  • materials
  • models (MDX / M2 File references)
  • groups
  • visibility information
  • more data
This section only applies to versions ≤ PreVanilla (0.5.5.3494).

In version 14, the version used in the alpha, the root WMO file has an additional container MOMO chunk, like the MOGP chunk, containing all group data.

MOMO

This section only applies to versions ≤ PreVanilla (0.5.5.3494). Only used in v14..

Rather than all chunks being top level, they have been wrapped in MOMO. There has been no other additional data, rather than just everything being wrapped.

MOHD chunk

  • Header for the map object. 64 bytes.
struct SMOHeader
{
/*000h*/  uint32_t nTextures;    
/*004h*/  uint32_t nGroups;    
/*008h*/  uint32_t nPortals;   
/*00Ch*/  uint32_t nLights;                                        // Blizzard seems to add one to the MOLT entry count when there are MOLP chunks in the groups (and maybe for MOLS too?)
/*010h*/  uint32_t nDoodadNames; 
/*014h*/  uint32_t nDoodadDefs;                                    // *
/*018h*/  uint32_t nDoodadSets;    
/*01Ch*/  CArgb ambColor;                        // Color settings for base (ambient) color. See the flag at /*03Ch*/.   
/*020h*/  foreign_key<uint32_t, &WMOAreaTableRec::m_WMOID> wmoID;
#if ≤ PreVanilla (0.5.5.3494) 
/*0x24*/  uint8_t padding[0x1c];
#else 
/*024h*/  CAaBox bounding_box;                   // in the alpha, this bounding box was computed upon loading
/*03Ch*/  uint16_t flag_do_not_attenuate_vertices_based_on_distance_to_portal : 1;
/*03Ch*/  uint16_t flag_use_unified_render_path : 1;                       // In 3.3.5a this flag switches between classic render path (MOHD color is baked into MOCV values, all three batch types have their own rendering logic) and unified (MOHD color is added to lighting at runtime, int. and ext. batches share the same rendering logic). See [[1]] for more details.
/*03Ch*/  uint16_t flag_use_liquid_type_dbc_id : 1;                // use real liquid type ID from DBCs instead of local one. See MLIQ for further reference.
/*03Ch*/  uint16_t flag_do_not_fix_vertex_color_alpha: 1;                     // In 3.3.5.a (and probably before) it prevents CMapObjGroup::FixColorVertexAlpha function to be executed. Alternatively, for the wotlk version of it, the function can be called with MOCV.a being set to 64, whjch will produce the same effect for easier implementation. For wotlk+ rendering, it alters the behavior of the said function instead. See [[2]] for more details.
/*03Ch*/  uint16_t Flag_Lod : 1;                                   // ≥ Legion (20740)
/*03Ch*/  uint16_t : 11;                                           // unused as of Legion (20994)
/*03Eh*/  uint16_t numLod;                                         // ≥ Legion (21108) includes base lod (→ numLod = 3 means '.wmo', 'lod0.wmo' and 'lod1.wmo')
#endif
} header;

MOTX chunk

This section only applies to versions < Battle (8.1.0.28186). MOTX has been replaced with file data ids in MOMT.
  • List of textures (BLP Files) used in this map object. There are nTextures entries in this chunk.

A block of zero-padded, zero-terminated strings, that are complete filenames with paths. There will be further material information for each texture in the next chunk. The gaps between the filenames are padded with extra zeroes, but the material chunk does have some positional information for these strings.

char textureNameList[];

The beginning of a string is always aligned to a 4 byte address. (0, 4, 8, C). The end of the string is Zero terminated and filled with zeros until the next alignment. Sometimes there also empty alignments for no (it seems like no) real reason.

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

Starting with 8.1, MOTX is no longer used. The texture references in MOMT are file data ids directly. As of that version, there is a fallback mode though and some files still use MOTX for sake of avoiding re-export. To check if texture references in MOMT are file data ids, simply check if MOTX exist in file

MOMT chunk

  • Materials used in this map object, 64 bytes per texture (BLP file).
struct SMOMaterial
{
#if ≤ PreVanilla (0.5.5.3494)  
         uint32_t version;   
#endif

/*0x00*/ uint32_t F_UNLIT    : 1;                 // disable lighting logic in shader (but can still use vertex colors)
/*0x00*/ uint32_t F_UNFOGGED : 1;                 // disable fog shading (rarely used)
/*0x00*/ uint32_t F_UNCULLED : 1;                 // two-sided
/*0x00*/ uint32_t F_EXTLIGHT : 1;                 // darkened, the intern face of windows are flagged 0x08
/*0x01*/ uint32_t F_SIDN     : 1;                 // (bright at night, unshaded) (used on windows and lamps in Stormwind, for example) (see emissive color)
/*0x01*/ uint32_t F_WINDOW   : 1;                 // lighting related (flag checked in CMapObj::UpdateSceneMaterials)
/*0x01*/ uint32_t F_CLAMP_S  : 1;                 // tex clamp S (force this material's textures to use clamp s addressing)
/*0x01*/ uint32_t F_CLAMP_T  : 1;                 // tex clamp T (force this material's textures to use clamp t addressing)
/*0x02*/ uint32_t flag_0x100 : 1;
/*0x02*/ uint32_t            : 23;                // unused as of 7.0.1.20994

#if ≥ PreVanilla (0.6.0.3592)     
/*0x04*/ uint32_t shader;                         // Index into CMapObj::s_wmoShaderMetaData. See below (shader types).
#endif

/*0x08*/ uint32_t blendMode;                      // Blending: see EGxBlend
/*0x0C*/ uint32_t diffuseNameIndex;               // offset into MOTX; ≥ Battle (8.1.0.27826) No longer references MOTX but is a filedata id directly.
/*0x10*/ CImVector sidnColor;                    // emissive color; see below (emissive color)
/*0x14*/ CImVector frameSidnColor;               // sidn emissive color; set at runtime; gets sidn-manipulated emissive color; see below (emissive color)
/*0x18*/ uint32_t envNameIndex;
/*0x1C*/ CArgb diffColor;
/*0x20*/ foreign_key<uint32_t, &TerrainTypeRec::m_ID> ground_type;
                                                  // according to CMapObjDef::GetGroundType 

#if ≤ PreVanilla (0.6.0.3592)
         char inMemPad[8];
#else  

/*0x24*/ uint32_t texture_2;
/*0x28*/ uint32_t color_2;
/*0x2C*/ uint32_t flags_2;
/*0x30*/ uint32_t runTimeData[4];                 // This data is explicitly nulled upon loading. Contains textures or similar stuff.
/*0x40*/

#endif
} materialList[];

texture_1, 2 and 3 are start positions for texture filenames in the MOTX data block ; texture_1 for the first texture, texture_2 for the second (see shaders), etc. texture_1 defaults to "createcrappygreentexture.blp".

color_2 is diffuse color : CWorldView::GatherMapObjDefGroupLiquids(): geomFactory->SetDiffuseColor((CImVector*)(smo+7));

The flags might used to tweak alpha testing values, I'm not sure about it, but some grates and flags in IF seem to require an alpha testing threshold of 0, at other places this is greater than 0.

Texture addressing

By default, textures used by WMO materials are assigned an addressing mode of EGxTexWrapMode::GL_REPEAT (ie wrap mode).

SMOMaterial flags F_CLAMP_S and F_CLAMP_T can override this default to clamp mode for the S and T dimensions, respectively.

Emissive color

The sidnColor CImVector at offset 0x10 is used with the SIDN (self-illuminated day night) scalar from CDayNightObject to light exterior window glows (see flag 0x10 above).

The scalar is interpolated out of a static table in the client, based on the time of day.

The color value eventually is copied into offset 0x14 (frameSidnColor) after being manipulated by the SIDN scalar. This manipulation occurs in CMapObj::UpdateMaterials.

Shader types (15464)

Depending on the shader, a different amount of textures is required. If there aren't enough filenames given, it defaults to Opaque (with one filename). More filenames than required are just ignored.

Data is from 15464.

value name textures without shader textures with shader texcoord count color count
0 Diffuse 1 1 1 1
1 Specular 1 1 1 1
2 Metal 1 1 1 1
3 Env 1 2 1 1
4 Opaque 1 1 1 1
5 EnvMetal 1 2 1 1
6 TwoLayerDiffuse 1 2 2 2
7 TwoLayerEnvMetal 1 3 2 2
8 TwoLayerTerrain 1 2 1 2 automatically adds _s in the filename of the second texture
9 DiffuseEmissive 1 2 2 2
10 1 1 1 1 SMOMaterial::SH_WATERWINDOW -- Seems to be invalid. Does something with MOTA (tangents).
11 MaskedEnvMetal 1 3 2 2
12 EnvMetalEmissive 1 3 2 2
13 TwoLayerDiffuseOpaque 1 2 2 2
14 TwoLayerDiffuseEmissive 1 1 1 1 SMOMaterial::SH_SUBMARINEWINDOW -- Seems to be invalid. Does something with MOTA (tangents).
15 1 2 2 2
16 Diffuse 1 1 1 1 SMOMaterial::SH_DIFFUSE_TERRAIN -- "Blend Material": used for blending WMO with terrain (dynamic blend batches)

tex coord and color count decide vertex buffer format: EGxVertexBufferFormat_PNC2T2

Shader types (18179)

value #textures without shader #textures with shader texcoord count color count
0 - Diffuse 1 1 1 1
1 - Specular 1 1 1 1
2 - Metal 1 1 1 1
3 - Env 1 2 1 1
4 - Opaque 1 1 1 1
5 - EnvMetal 1 2 1 1
6 - TwoLayerDiffuse 1 2 2 2
7 - TwoLayerEnvMetal 1 3 2 2
8 - TwoLayerTerrain 1 2 1 2 automatically adds _s in the filename of the second texture
9 - DiffuseEmissive 1 2 2 2
10 - waterWindow 1 1 1 1 SMOMaterial::SH_WATERWINDOW -- automatically generates MOTA
11 - MaskedEnvMetal 1 3 2 2
12 - EnvMetalEmissive 1 3 2 2
13 - TwoLayerDiffuseOpaque 1 2 2 2
14 - submarineWindow 1 1 1 1 SMOMaterial::SH_SUBMARINEWINDOW -- automatically generates MOTA
15 - TwoLayerDiffuseEmissive 1 2 2 2
16 - DiffuseTerrain 1 1 1 1 SMOMaterial::SH_DIFFUSE_TERRAIN -- "Blend Material": used for blending WMO with terrain (dynamic blend batches)
17 - AdditiveMaskedEnvMetal 1 3 2 2


Shader types (26522)

value vertex shader pixel shader
0 - Diffuse MapObjDiffuse_T1 MapObjDiffuse
1 - Specular MapObjSpecular_T1 MapObjSpecular
2 - Metal MapObjSpecular_T1 MapObjMetal
3 - Env MapObjDiffuse_T1_Refl MapObjEnv
4 - Opaque MapObjDiffuse_T1 MapObjOpaque
5 - EnvMetal MapObjDiffuse_T1_Refl MapObjEnvMetal
6 - TwoLayerDiffuse MapObjDiffuse_Comp MapObjTwoLayerDiffuse
7 - TwoLayerEnvMetal MapObjDiffuse_T1 MapObjTwoLayerEnvMetal
8 - TwoLayerTerrain MapObjDiffuse_Comp_Terrain MapObjTwoLayerTerrain automatically adds _s in the filename of the second texture
9 - DiffuseEmissive MapObjDiffuse_Comp MapObjDiffuseEmissive
10 - waterWindow FFXWaterWindow FFXWaterWindow It's FFX instead of normal material. SMOMaterial::SH_WATERWINDOW -- automatically generates MOTA
11 - MaskedEnvMetal MapObjDiffuse_T1_Env_T2 MapObjMaskedEnvMetal
12 - EnvMetalEmissive MapObjDiffuse_T1_Env_T2 MapObjEnvMetalEmissive
13 - TwoLayerDiffuseOpaque MapObjDiffuse_Comp MapObjTwoLayerDiffuseOpaque
14 - submarineWindow FFXSubmarineWindow FFXSubmarineWindow It's FFX instead of normal material. SMOMaterial::SH_SUBMARINEWINDOW -- automatically generates MOTA
15 - TwoLayerDiffuseEmissive MapObjDiffuse_Comp MapObjTwoLayerDiffuseEmissive
16 - DiffuseTerrain MapObjDiffuse_T1 MapObjDiffuse SMOMaterial::SH_DIFFUSE_TERRAIN -- "Blend Material": used for blending WMO with terrain (dynamic blend batches)
17 - AdditiveMaskedEnvMetal MapObjDiffuse_T1_Env_T2 MapObjAdditiveMaskedEnvMetal
18 - TwoLayerDiffuseMod2x MapObjDiffuse_CompAlpha MapObjTwoLayerDiffuseMod2x
19 - TwoLayerDiffuseMod2xNA MapObjDiffuse_Comp MapObjTwoLayerDiffuseMod2xNA
20 - TwoLayerDiffuseAlpha MapObjDiffuse_CompAlpha MapObjTwoLayerDiffuseAlpha
21 - Lod MapObjDiffuse_T1 MapObjLod
22 - Parallax MapObjParallax MapObjParallax

void CMapObj::CreateMaterial (unsigned int materialId)

void CMapObj::CreateMaterial (unsigned int materialId)
{
  assert (m_materialCount);
  assert (m_materialTexturesList);
  assert (materialId < m_materialCount);

  if (++m_materialTexturesList[materialId].refcount <= 1)
  {
    SMOMaterial* material = &m_smoMaterials[materialId];

    const char* texNames[3];
    texNames[0] = &m_textureFilenamesRaw[material->firstTextureOffset];
    texNames[1] = &m_textureFilenamesRaw[material->secondTextureOffset];
    texNames[2] = &m_textureFilenamesRaw[material->thirdTextureOffset];
    if ( *texNames[0] )
      texNames[0] = "createcrappygreentexture.blp";

    assert (material->shader < SMOMaterial::SH_COUNT);

    int const textureCount
      ( CShaderEffect::s_enableShaders
      ? s_wmoShaderMetaData[material->shader].texturesWithShader
      : s_wmoShaderMetaData[material->shader].texturesWithoutShader
      );

    int textures_set (0);

    for (; textures_set < textureCount; ++textures_set)
    {
      if (!texNames[textures_set])
      {
        material->shader = MapObjOpaque;
        textures_set = 1;
        break;
      }
    }

    for (; textures_set < 3; ++textures_set)
    {
      texNames[textures_set] = nullptr;
    }

    if (material->shader == MapObjTwoLayerTerrain && texNames[1])
    {
      texNames[1] = insert_specular_suffix (texNames[1]);
    }

    int flags (std::max (m_field_2C, 12));

    const char* parent_name (m_field_9E8 & 1 ? m_filename : nullptr);

    m_materialTexturesList[materialId]->textures[0] = texNames[0] ? CMap::CreateTexture (texNames[0], parent_name, flags) : nullptr;
    m_materialTexturesList[materialId]->textures[1] = texNames[1] ? CMap::CreateTexture (texNames[1], parent_name, flags) : nullptr;
    m_materialTexturesList[materialId]->textures[2] = texNames[2] ? CMap::CreateTexture (texNames[2], parent_name, flags) : nullptr;
  }
}

MOUV

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

Optional. If not present, values are {0, 0, 0, 0} for all materials. If present, has same count as materials, so is repeating those zeros for materials not using any transformation. Currently, only a translating animation is possible for two of the texture layers.

struct 
{
  C2Vector translation_speed[2];
} MapObjectUV[count(materials)];

The formula from translation_speed values to TexMtx translation values is along the lines of

a_i = translation_i ? 1000 / translation_i : 0
b_i = a_i ? (a_i < 0 ? (1 - (time? % -a_i) / -a_i) : ((time? % a_i) / a_i)) : 0

Note: Until Legion (7.3.0.24920) (i.e. just before release), a missing break; in the engine's loader will overwrite the data for MOGN with that of MOUV if MOUV comes second. Since MOGN comes second in Blizzard-exported files it works for those without issue.

MOGN chunk

  • List of group names for the groups in this map object.
char groupNameList[];

A contiguous block of zero-terminated strings. The names are purely informational except for "antiportal". The names are referenced from MOGI and MOGP.

There are not always nGroups entries in this chunk as it contains extra empty strings and descriptive names. There are also empty entries. The names are indeed referenced in MOGI, and both the name and a descriptive name are referenced in the group file header (2 firsts uint16 of MOGP).

Looks like ASCII but is not: BWL e.g. has , so probably UTF-8.

MOGI chunk

  • Group information for WMO groups, 32 bytes per group, nGroups entries.
struct SMOGroupInfo
{
#if ≤ PreVanilla (0.5.5.3494) 
  uint32_t offset;             // absolute address
  uint32_t size;               // includes IffChunk header
#endif
/*000h*/  uint32_t flags;      //  see information in in MOGP, they are equivalent
/*004h*/  CAaBox bounding_box;
/*01Ch*/  int32_t nameoffset;  // name in MOGN chunk (-1 for no name)
} groupInfoList[];

Groups don't have placement or orientation information, because the coordinates for the vertices in the additional. WMO files are already correctly transformed relative to (0,0,0) which is the entire WMO's base position in model space.

The name offsets point to the position in the file relative to the MOGN header.

MOSB chunk (optional)

  • Skybox. Contains an zero-terminated filename for a skybox. (padded to 4 byte alignment if "empty"). If the first byte is 0, the skybox flag in all MOGI entries are cleared and there is no skybox.
char skyboxName[];

MOSI (optional)

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.

Equivalent to MOSB, but a file data id. Client supports reading both for now.

uint32_t skyboxFileId;

MOPV chunk

  • Portal vertices, one entry is a float[3], usually 4 * 3 * float per portal (actual number of vertices given in portal entry)
C3Vector portalVertexList[];

Portals are polygon planes (usually quads, but they can have more complex shapes) that specify where separation points between groups in a WMO are - these are usually doors or entrances, but can be placed elsewhere. Portals are used for occlusion culling, and is a known rendering technique used in many games (among them Unreal Tournament 2004 and Descent. See Portal Rendering on Wikipedia and Antiportal on Wikipedia for more information.

Since when "playing" WoW, you're confined to the ground, checking for passing through these portals would be enough to toggle visibility for indoors or outdoors areas, however, when randomly flying around, this is not necessarily the case.

So.... What happens when you're flying around on a gryphon, and you fly into that arch-shaped portal into Ironforge? How is that portal calculated? It's all cool as long as you're inside "legal" areas, I suppose.

It's fun, you can actually map out the topology of the WMO using this and the MOPR chunk. This could be used to speed up the rendering once/if I figure out how.


This image explains how portal equation in MOPT and relations in MOPR are connected: Portal explanation. Deamon (talk) 17:06, 23 February 2017 (CET)

MOPT chunk

  • Portal information. 20 bytes per portal, nPortals entries. There is a hardcoded maximum of 128 portals in a single WMO.
struct SMOPortal
{
  uint16_t startVertex;
  uint16_t count;
  C4Plane plane;
} portalList[];

This structure describes one portal separating two WMO groups. A single portal is usually made up of four vertices in a quad (starting at startVertex and going to startVertex + count). However, portals support more complex shapes, and can fully encompass holes such as the archway leading into Ironforge and parts of the Caverns of Time.

It is likely that portals are drawn as GL_TRIANGLE_STRIP in WoW's occlusion pipeline, since some portals have a vertex count that is not evenly divisible by four. One example of this is portal #21 in CavernsOfTime.wmo from Build #5875 (WoW 1.12.1), which has 10 vertices.

MOPR chunk

  • Map Object Portal References from groups. Mostly twice the number of portals. Actual count defined by sum (MOGP.portals_used).
struct SMOPortalRef // 04-29-2005 By ObscuR
{
  uint16_t portalIndex;  // into MOPT
  uint16_t groupIndex;   // the other one
  int16_t side;          // positive or negative.
  uint16_t filler;
} portalRefList[];

MOVV chunk

  • Visible block vertices, 0xC byte per entry.

Just a list of vertices that corresponds to the visible block list.

C3Vector visible_block_vertices[];

MOVB chunk

  • Visible block list
struct
{
  uint16_t firstVertex;
  uint16_t count;
) visible_blocks[];

MOLT chunk

  • Lighting information. 48 bytes per light, nLights entries
struct SMOLight
{
  enum LightType
  {
    OMNI_LGT = 0,
    SPOT_LGT = 1,
    DIRECT_LGT = 2,
    AMBIENT_LGT = 3,
  };
  /*000h*/  uint8_t type;
  /*001h*/  uint8_t useAtten;
  /*002h*/  uint8_t pad[2];      // not padding as of v16
  /*004h*/  CImVector color;
  /*008h*/  C3Vector position;
  /*014h*/  float intensity;
#if ≥ PreVanilla (0.6.0.3592)
  /*018h*/  float _unk18[4];     // 2 C2Vector ranges
#endif
  /*028h*/  float attenStart;
  /*02Ch*/  float attenEnd;
} lightList[];

First 4 uint8_t are probably flags, mostly with the values (0,1,1,1).

I haven't quite figured out how WoW actually does lighting, as it seems much smoother than the regular vertex lighting in my screenshots. The light parameters might be range or attenuation information, or something else entirely. Some WMO groups reference a lot of lights at once.

The WoW client (at least on my system) uses only one light, which is always directional. Attenuation is always (0, 0.7, 0.03). So I suppose for models/doodads (both are M2 files anyway) it selects an appropriate light to turn on. Global light is handled similarly. Some WMO textures (BLP files) have specular maps in the alpha channel, the pixel shader renderpath uses these. Still don't know how to determine direction/color for either the outdoor light or WMO local lights... :)

The entire MOLT and related chunks seem to be unused at least in 3.3.5a. Changing light colors and other settings on original WMOs leads to no effect. Removing the light leads to no effect either. I assume that MOLT rendering is disabled somewhere in the WoW.exe, as it might use the same principle as the M2 light emitters which are not properly supported up to WoD. However, when you explore the WMOs in 3D editors you can clearly see that MOCV layer is different under those lamps. So, I assume they are used for baking MOCV colors and also written to the actual file in case the renderer will ever get updated, or just because you can easily import the WMO back and rebake the colors. --- Skarn (talk)

MODS chunk

  • This chunk defines doodad sets.

Doodads in WoW are M2 model files. There are 32 bytes per doodad set, and nSets entries. Doodad sets specify several versions of "interior decoration" for a WMO. Like, a small house might have tables and a bed laid out neatly in one set, and have a horrible mess of abandoned broken things in another set called "Set_Abandoned01".

Sets are exclusive except for the very first one, "Set_$DefaultGlobal" which is additive and is always displayed. The client determines that set by index, not name though. Up to 8 doodad sets can be enabled at the same time, e.g. via destructible buildings or garrisons.

The doodad set number for every WMO instance is specified in the ADT files, or via DBC or via game object fields, depending on how it is spawned.

struct SMODoodadSet
{
/*0x00*/  char     name[0x14];     // set name, informational
/*0x14*/  uint32_t startIndex;     // index of first doodad instance in this set, into #MODD_chunk directly.
/*0x18*/  uint32_t count;          // number of doodad instances in this set
/*0x1C*/  char     pad[4];
/*0x20*/
} doodadSetList[];

MODN chunk

  • List of filenames for M2 (mdx) models that appear in this WMO.

A block of zero-padded, zero-terminated strings. There are nModels file names in this list. They have to be .MDX!

char doodadNameList[];

MODI chunk

This section only applies to versions ≥ Battle (8.1.0.27826). Replaces filenames in MODN.
uint32_t doodadFileIDs[];

MODD chunk

  • Information for doodad instances. 40 bytes per doodad instance, nDoodads entries.

-- There are not nDoodads entries here! Divide the chunk length by 40 to get the correct amount.

While WMOs and models (M2s) in a map tile are rotated along the axes, doodads within a WMO are oriented using quaternions! Hooray for consistency!

I had to do some tinkering and mirroring to orient the doodads correctly using the quaternion, see model.cpp in the WoWmapview source code for the exact transform matrix. It's probably because I'm using another coordinate system, as a lot of other coordinates in WMOs and models also have to be read as (X,Z,-Y) to work in my system. But then again, the ADT files have the "correct" order of coordinates. Weird.

struct SMODoodadDef
{
  /*000h*/  uint32_t nameIndex : 24;          // reference offset into MODN, or MODI, depending on version and presence.
  /*003h*/  uint32_t flag_AcceptProjTex : 1;
  /*003h*/  uint32_t flag_0x2 : 1;            // MapStaticEntity::field_34 |= 1 (if set, MapStaticEntity::AdjustLighting is _not_ called)
  /*003h*/  uint32_t flag_0x4 : 1;
  /*003h*/  uint32_t flag_0x8 : 1;
  /*003h*/  uint32_t : 4;                     // unused as of 7.0.1.20994
  /*004h*/  C3Vector position;               // (X,Z,-Y)
  /*010h*/  C4Quaternion orientation;        // (X, Y, Z, W)
  /*020h*/  float scale;                      // scale factor
  /*024h*/  CImVector color;                 // (B,G,R,A) overrides pc_sunColor
} doodadDefList[];

It looks like in order to get correct picture the color from SMODoodadDef should be applied only to opaque submeshes of M2. Deamon (talk)


  • How to compute a matrix to map WMO's M2 to world coordinates

The coordinate system here is WMO's local coordinate system. It's Z-up already, that differs it from Y-up in MODF(ADT), MODF(WDT) and MDDF chunks. To compute the whole placement matrix for doodad you would need take positionMatrix of WMO from MODF(ADT) or MODF(WDT) and multiply it by positionMatrix calculated here.

Example implementation in js with gl-matrix library:

 function createPlacementMatrix(modd, wmoPlacementMatrix){
     var placementMatrix = mat4.create();
     mat4.identity(placementMatrix);
     mat4.multiply(placementMatrix, placementMatrix, wmoPlacementMatrix);
 
     mat4.translate(placementMatrix, placementMatrix, [modd.pos[0],modd.pos[1], modd.pos[2]]);
 
     var orientMatrix = mat4.create();
     mat4.fromQuat(orientMatrix,
         [modd.rotation[0], //imag.x
         modd.rotation[1],  //imag.y,
         modd.rotation[2],  //imag.z,
         modd.rotation[3]   //real
         ]
     );
     mat4.multiply(placementMatrix, placementMatrix, orientMatrix);
 
     mat4.scale(placementMatrix, placementMatrix, [modd.scale, modd.scale, modd.scale]);
     return placementMatrix;
 }

MFOG chunk

  • Fog information. Made up of blocks of 48 bytes.
struct SMOFog
{
  /*000h*/  uint32_t flag_infinite_radius : 1; // F_IEBLEND: Ignore radius in CWorldView::QueryCameraFog
  /*000h*/  uint32_t : 3;                      // unused as of 7.0.1.20994
  /*000h*/  uint32_t flag_0x10 : 1;
  /*000h*/  uint32_t : 27;                     // unused as of 7.0.1.20994
  /*004h*/  C3Vector pos;
  /*010h*/  float smaller_radius;              // start
  /*014h*/  float larger_radius;               // end
            enum EFogs 
            {
              FOG,
              UWFOG,                           // uw = under water
              NUM_FOGS,
            };
            struct Fog
            {
              float end;
              float start_scalar;              // (0..1) -- minimum distance is end * start_scalar
              CImVector color;                // The back buffer is also cleared to this colour
  /*018h*/  } fogs[NUM_FOGS];
} fogList[];
  • Fog end: This is the distance at which all visibility ceases, and you see no objects or terrain except for the fog color.
  • Fog start: This is where the fog starts. Obtained by multiplying the fog end value by the fog start multiplier.
  • There should always be at least one fog entry in MFOG. The empty fog entry has both radiuses set to zero, 444.4445 for end, 0.25 for start_scalar, 222.2222 for underwater end, -0.5 for underwater start_scalar.
  • F_IEBLEND - InteriorExteriorBlend
These fog entries are used to reduce fog visibility based on the player's proximity i.e. the closer you are, the less on-screen fog. They are usually placed near exits to prevent fog showing in unintended places such as behind instance portals (e.g. Stockades fog showing on the Stormwind side of the portal). Whilst not being rendered they are still computed; the resulting blend percentage is applied as a multiplier (1.0 - ComputedBlendPercentage) to the scalar and colour calculations of the area fog.
This fog ignores all visibility checks (so that the multiplier is always applied) and is excluded from fog queries. Only one is used per mapObjGroup->fogList with the last taking precedence. (verified ≤ Wrath)

MCVP chunk (optional)

  • Convex Volume Planes. Contains blocks of floating-point numbers. 0x10 bytes (4 floats) per entry.
C4Plane convexVolumePlanes[];   // normal points out

These are used to define the volume of when you are inside this WMO. Important for transports. If a point is behind all planes (i.e. point-plane distance is negative for all planes), it is inside.

GFID

This section only applies to versions ≥ Legion.
  • required when WMO is load from fileID (e.g. game objects)
struct {
    uint32 id[MOHD.nGroups];
} groupFileDataIDs[ !MOHD.Flag_Lod ? 1
                  : MOHD.numLod ? MOHD.numLod : 3   // fallback for missing numLod: assume numLod=2+1base
                  ];

WMO group file

WMO group files contain the actual polygon soup for a particular section of the entire WMO.

Every group file has one top-level MOGP chunk, that has a 68-byte header followed by more subchunks. So it can be effectively treated as a file with a header at 0x14 and chunks starting at 0x58.

The subchunks are not always present. Some are fixed and needed while others are only checked for if some flags in the header are set. The chunks need to be in the right order if you want WoW to read it.

The following chunks are always present in the following order:

These chunks are only present if a flag in the header is set. See the list below for the flags.

MOGP chunk

IMPORTANT: This chunk contains all other chunks! The following variables are a header only. The MOGP chunk size will be way more than the header variables!

struct {
/*0x00*/  uint32_t groupName;               // offset into MOGN
/*0x04*/  uint32_t descriptiveGroupName;    // offset into MOGN
/*0x08*/  uint32_t flags;                   // see below
/*0x0C*/  CAaBox boundingBox;              // as with flags, same as in corresponding MOGI entry

#if ≤ PreVanilla (0.5.5.3494) 
          uint32_t portalStart;             // index into MOPR
          uint32_t portalCount;             // number of MOPR items used after portalStart
#else
/*0x24*/  uint16_t portalStart;             // index into MOPR
/*0x26*/  uint16_t portalCount;             // number of MOPR items used after portalStart
#endif

#if ≥ PreVanilla (0.6.0.3592) 
/*0x28*/  uint16_t transBatchCount;
/*0x2A*/  uint16_t intBatchCount;
/*0x2C*/  uint16_t extBatchCount;
/*0x2E*/  uint16_t padding_or_batch_type_d; // probably padding, but might be data?
#endif 

/*0x30*/  uint8_t fogIds[4];                // ids in MFOG
/*0x34*/  uint32_t groupLiquid;             // see below in the MLIQ chunk

#if ≤ PreVanilla (0.5.5.3494) 
          SMOGxBatch intBatch[4];
          SMOGxBatch extBatch[4];
#endif

/*0x38*/  foreign_key<uint32_t, &WMOAreaTableRec::m_WMOGroupID> uniqueID;

#if ≤ PreVanilla (0.5.5.3494) 
          uint8_t padding[8];
#else
          enum
          {
            flag2_CanCutTerrain = 1,        // ≥ Mists has portal planes to cut
          };
/*0x3C*/  uint32_t flags2;
/*0x40*/  uint32_t unk;                     // UNUSED: 20740
#endif
} map_object_group_header;
// remaining chunks follow

#if ≤ PreVanilla (0.5.5.3494) 
struct SMOGxBatch
{
  uint16_t vertStart;
  uint16_t vertCount;
  uint16_t batchStart;
  uint16_t batchCount;
};
#endif

The fields referenced from the MOPR chunk indicate portals leading out of the WMO group in question.

For the "Number of batches" fields, transBatchCount + intBatchCount + extBatchCount == the total number of batches in the WMO group (in the MOBA chunk). This might be some kind of LOD thing, or just separating the batches into different types/groups…?

Flags: always contain more information than flags in MOGI. I suppose MOGI only deals with topology/culling, while flags here also include rendering info.

group flags

Flag		Meaning
0x1		Has BSP tree (MOBN and MOBR chunk).
0x2		Has light map (MOLM, MOLD). (UNUSED: 20740) possibly: subtract mohd.color in mocv fixing 
0x4 		Has vertex colors (MOCV chunk).
0x8 		SMOGroup::EXTERIOR -- Outdoor - also influences how doodads are culled
0x10		(UNUSED: 20740)
0x20		(UNUSED: 20740)
0x40		SMOGroup::EXTERIOR_LIT -- "Do not use local diffuse lightning". Applicable for both doodads from this wmo group(color from MODD) and water(CWorldView::GatherMapObjDefGroupLiquids). 
0x80 		SMOGroup::UNREACHABLE
0x100          
0x200 		Has lights (MOLR chunk)
0x400		<= Cataclysm: Has MPBV, MPBP, MPBI, MPBG chunks, neither 0.5.5, 3.3.5a nor Cataclysm alpha actually use them though, but just skips them. Legion+(?): SMOGroup::LOD: Also load for LoD != 0 (_lod* groups)
0x800 		Has doodads (MODR chunk)
0x1000		SMOGroup::LIQUIDSURFACE -- Has water (MLIQ chunk)
0x2000		SMOGroup::INTERIOR -- Indoor
0x4000		(UNUSED: 20740)
0x8000
0x10000         SMOGroup::ALWAYSDRAW -- clear 0x8 after CMapObjGroup::Create() in MOGP and MOGI
0x20000		(UNUSED: 20740) Has MORI and MORB chunks.
0x40000		Show skybox -- automatically unset if MOSB not present.
0x80000		is_not_water_but_ocean, LiquidType related, see below in the MLIQ chunk.
0x100000
0x200000	IsMountAllowed
0x400000	(UNUSED: 20740)
0x800000
0x1000000	SMOGroup::CVERTS2: Has two MOCV chunks: Just add two or don't set 0x4 to only use cverts2.
0x2000000	SMOGroup::TVERTS2: Has two MOTV chunks: Just add two.
0x4000000	SMOGroup::ANTIPORTAL: Just call CMapObjGroup::CreateOccluders() independent of groupname being "antiportal". requires intBatchCount == 0, extBatchCount == 0, UNREACHABLE.
0x8000000	unk. requires intBatchCount == 0, extBatchCount == 0, UNREACHABLE.
0x10000000	(UNUSED: 20740)
0x20000000	>> 20740 SMOGroup::EXTERIOR_CULL
0x40000000	SMOGroup::TVERTS3: Has three MOTV chunks, eg. for MOMT with shader 18.
0x80000000     Seen in world/wmo/kultiras/human/8hu_warfronts_armory_v2_000.wmo
vv flags2
0x01????????   canCutTerrain
0x30000000	SMOGroup::depSHADOWMAPGEN | SMOGroup::depSHADOWMAPGEN_DEPTH as per "(m_groupFlags & (SMOGroup::depSHADOWMAPGEN | SMOGroup::depSHADOWMAPGEN_DEPTH)) == 0" and *(_DWORD *)(a1 + 36) & 0x30000000. yes, this clashes with EXTERIOR_CULL, but that's in the same version. weird.

"antiportal"

If a group wmo is named "antiportal", CMapObjGroup::CreateOccluders() is called and group flags 0x4000000 and 0x80 are set automatically in both, MOGP and MOGI. Also, the BSP tree is cleared and batch_count[interior] and [exterior] is set to 0. If flags & 0x4000000 is set, just CMapObjGroup::CreateOccluders() is called, without setting flags or clearing bsp.

m_vertices is content of MOVT

void CMapObjGroup::CreateOccluders()
{
  for ( unsigned int mopy_index (0), movi_index (0)
      ; mopy_index < this->mopy_count
      ; ++mopy_index, ++movi_index
      ) 
  {
    C3Vector* points[3] = 
      { &this->m_vertices[this->movi[3*mopy_index + 0]]
      , &this->m_vertices[this->movi[3*mopy_index + 1]]
      , &this->m_vertices[this->movi[3*mopy_index + 2]]
      };

    float avg ((points[0]->z + points[1]->z + points[2]->z) / 3.0); 

    unsigned int two_points[2];
    unsigned int two_points_index (0);

    for (unsigned int i (0); i < 3; ++i)
    {
      if (points[i]->z > avg)
      {
        two_points[two_points_index++] = i;
      }
    }

    if (two_points_index > 1)
    {
      CMapObjOccluder* occluder (CMapObj::AllocOccluder());
      occluder->p1 = points[two_points[0]];
      occluder->p2 = points[two_points[1]];

      append (this->occluders, occluder);
    }
  }
}

MOPY chunk

  • Material info for triangles, two bytes per triangle. So size of this chunk in bytes is twice the number of triangles in the WMO group.
struct SMOPoly
{
  struct
  {
    /*0x01*/ uint8_t F_UNK_0x01: 1;
    /*0x02*/ uint8_t F_NOCAMCOLLIDE : 1;
    /*0x04*/ uint8_t F_DETAIL : 1;
    /*0x08*/ uint8_t F_COLLISION : 1; // Turns off rendering of water ripple effects. May also do more. Should be used for ghost material triangles.
    /*0x10*/ uint8_t F_HINT : 1;
    /*0x20*/ uint8_t F_RENDER : 1;
    /*0x40*/ uint8_t F_UNK_0x40 : 1;
    /*0x80*/ uint8_t F_COLLIDE_HIT : 1;

    bool isTransFace() { return F_UNK_0x01 && (F_DETAIL || F_RENDER); } // triangles flagged as TRANSITION.  These triangles blend lighting from exterior to interior
    bool isColor() { return !F_COLLISION; }
    bool isRenderFace() { return F_RENDER && !F_DETAIL; }
    bool isCollidable() { return F_COLLISION || isRenderFace(); }
  } flags;

#if version < Vanilla 
  uint8_t lightmapTex;           // index into MOLD
#endif
  uint8_t material_id;           // index into MOMT, 0xff for collision faces
#if version < Vanilla 
  uint8_t padding;
#endif
} polyList[];

0xFF is used for collision-only triangles. They aren't rendered but have collision. Problem with it: WoW seems to cast and reflect light on them. Its a bug in the engine. --schlumpf_ 20:40, 7 June 2009 (CEST)

Triangles stored here are more-or-less pre-sorted by texture, so it's ok to draw them sequentially.

MOVI chunk

  • Vertex indices for triangles., count = size / sizeof(unsigned short). Three 16-bit integers per triangle, that are indices into the vertex list. The numbers specify the 3 vertices for each triangle, their order makes it possible to do backface culling.
uint16_t indexList[];

MOVT chunk

  • Vertices chunk., count = size / (sizeof(float) * 3). 3 floats per vertex, the coordinates are in (X,Z,-Y) order. It's likely that WMOs and models (M2s) were created in a coordinate system with the Z axis pointing up and the Y axis into the screen, whereas in OpenGL, the coordinate system used in WoWmapview the Z axis points toward the viewer and the Y axis points up. Hence the juggling around with coordinates.
C3Vector vertexList[];

MONR chunk

  • Normals. count = size / (sizeof(float) * 3). 3 floats per vertex normal, in (X,Z,-Y) order.
C3Vector normalList[];

MOTV chunk

  • Texture coordinates, 2 floats per vertex in (X,Y) order. The values usually range from 0.0 to 1.0, but it's ok to have coordinates out of that range. Vertices, normals and texture coordinates are in corresponding order, of course. Not present in antiportal WMO groups.
C2Vector textureVertexList[];    // ranging [0, 1], can be outside that range though and will be normalised.

MOLV

This section only applies to versions ≤ PreVanilla (0.5.5.3494). Only used in v14.

This chunk is referenced by MOPY index with 3 entries per SMOPoly.

C2Vector lightmapVertexList[];

MOIN

This section only applies to versions ≤ PreVanilla (0.5.5.3494). Only used in v14.
uint16_t indexList[];

It's most of the time only a list incrementing from 0 to nFaces * 3 or less, not always up to nPolygons (calculated with MOPY).

Unlike in ≥ Vanilla where the faces indices (MOVI) point to a vertex in MOVT, here there are exactly nFaces * 3 vertices in MOVT, and the client just read them straightforward. If you want to read them, just make nPolygons faces going incrementing, like (0, 1, 2), (3, 4, 5), … --Gamhea 15:44, 10 March 2013 (UTC)

MOBA chunk

  • Render batches. Records of 24 bytes.
struct SMOBatch
{
#if ≤ PreVanilla (0.5.5.3494) 
  uint8_t lightMap;                                 // index into MOLM
  uint8_t texture;                                  // index into MOMT
#endif
#if < Legion
  /*0x00*/ int16_t bx, by, bz;                      // a bounding box for culling, see "unknown_box" below
  /*0x06*/ int16_t tx, ty, tz;
#else
  /*0x00*/ uint8_t unknown[0xA];
  /*0x0A*/ uint16_t material_id_large;              // used if flag_use_uint16_t_material is set.
#endif
#if ≤ PreVanilla (0.5.5.3494) 
  uint16_t startIndex;                              // index of the first face index used in MOVI
#else
  /*0x0C*/ uint32_t startIndex;                     // index of the first face index used in MOVI
#endif
  /*0x10*/ uint16_t count;                          // number of MOVI indices used
  /*0x12*/ uint16_t minIndex;                       // index of the first vertex used in MOVT
  /*0x14*/ uint16_t maxIndex;                       // index of the last vertex used (batch includes this one)
  /*0x16*/ uint8_t flag_unknown_1 : 1;
#if ≥ Legion
  /*0x16*/ uint8_t flag_use_material_id_large : 1;  // instead of material_id use material_id_large
#endif
                                                    // F_RENDERED = 0xf0, so probably upper nibble isn't unused

#if ≥ PreVanilla (0.6.0.3592) 
  /*0x17*/ uint8_t material_id;                     // index in MOMT
#else
  uint8_t padding;
#endif
#if PreVanilla (0.6.0.3592) … < Vanilla  
  uint8_t unknown[8];                               // always 0 filled
#endif
} batchList[];

Batches are groups of faces with the same material ID in root's MOMT, and they're used to accelerate rendering. Note that the client doesn't use them in the same way while rendering in D3D or OpenGL (only D3D uses all batches information). The vertex buffer containing vertices from minIndex to maxIndex can contain vertices that aren't used by the batch. On the other hand, if one of the faces used need a vertex, it has to be in the buffer. Concerning the byte at 0x16, as a material ID is coded on a uint8, I guess it is completely unused. --Gamhea 12:23, 29 July 2013 (UTC)

unknown_box

This is a very low resolution bounding box of the contained vertices. The client appears to be using them to do batch-level culling, so if they are set incorrectly, the batch may be randomly disappearing. According to Adspartan (talk), the box can be calculated by just iterating over all vertices contained (by following minIndex and maxIndex to MOVT and taking the minimum/maximum of those. They should probably be rounded away from zero instead of being truncated on conversion to int16_t.

This section only applies to version PreVanilla (0.5.3.3368)

In the 0.5.3 Alpha this box is used for batch-level culling. The values are converted to a CAaBox inside CMapObj::CullBatch, by being directly cast to floats, this box is then passed to CWorldScene::FrustumCull for rendering.

This section only applies to versions ≥ Legion.

unknown_box seems no longer used (and nulled). Instead, flag_use_material_id_large can be set to use material_id_large which was the last of unknown_box's fields. This means that when "retroporting" files, unknown_box's values need to be calculated (by building minimum and maximum from the corresponding vertices) and material_id should be set, if it can fit a uint8_t. --based on Rangorn (talk)

MOLR chunk

  • Light references, one 16-bit integer per light reference.
uint16_t lightRefList[];

This is basically a list of lights used in this WMO group, the numbers are indices into the WMO root file's MOLT table.

For some WMO groups there is a large number of lights specified here, more than what a typical video card will handle at once. I wonder how they do lighting properly. Currently, I just turn on the first GL_MAX_LIGHTS and hope for the best. :(

MODR chunk

  • Doodad references, one 16-bit integer per doodad.
uint16_t doodadRefList[];

The numbers are indices into the doodad instance table (MODD chunk) of the WMO root file. These have to be filtered to the doodad set being used in any given WMO instance.

MOBN chunk

  • Nodes of the BSP tree, used for collision (along with bounding boxes ?). Array of t_BSP_NODE. / CAaBspNode. 0x10 bytes.
enum Flags
{
  Flag_XAxis = 0x0,
  Flag_YAxis = 0x1,
  Flag_ZAxis = 0x2,
  Flag_AxisMask = 0x3,
  Flag_Leaf = 0x4,
  Flag_NoChild = 0xFFFF,
};

struct CAaBspNode
{	
  uint16_t flags;        // See above enum. 4: leaf, 0 for YZ-plane, 1 for XZ-plane, 2 for XY-plane
  int16_t negChild;      // index of bsp child node (right in this array)
  int16_t posChild;
  uint16_t nFaces;       // num of triangle faces in MOBR
  uint32_t faceStart;    // index of the first triangle index(in MOBR)
  float planeDist;
};

planetype might be 0 for YZ-plane, 1 for XZ-plane, 2 for XY-plane, 4 for BSP leaf. fDist is where split plane locates based on planetype, ex, you have a planetype 0 and fDist 15, so the split plane is located at offset ( 15, 0, 0 ) with Normal as ( 1, 0, 0 ), I think the offset is relative to current node's bounding box center. The BSP root ( ie. node 0 )'s bounding box is the WMO's boundingbox, then you subdivide it with plane and fdist, then you got two children with two bounding box, and so on. you got the whole BSP tree. As the bsp leaf might overlapping the dividing plane, i think you might have two same face exist on two different bsp leaf. I'll make further tests to prove this. --mobius.

The biggest leaf in terms of number of faces in 3.3.5 contains more than 2100 faces (some ice giant in the Storm Peaks), so it's not advised to use more. (While I haven't investigated properly, there might be a limit at 8192 in 6.0.1.18179 --Schlumpf (talk) 11:18, 3 January 2016 (UTC))

fDist is relative to point (0,0,0) of whole WMO. children[0] is child on negative side of dividing plane, children[1] is on positive side. --Deamon (talk) 10:01, 15 January 2016 (UTC)


#define epsilon 0.01F
void MergeBox(CVect3 (&result)[2], float  *box1, float  *box2)
{
 result[0][0] = box1[0];
 result[0][1] = box1[1];
 result[0][2] = box1[2];
 result[1][0] = box2[0];
 result[1][1] = box2[1];
 result[1][2] = box2[2];
}
void AjustDelta(CVect3 (&src)[2], float *dst, float coef)
{
 float d1 = (src[1][0]- src[0][0]) * coef;// delta x
 float d2 = (src[1][1]- src[0][1]) * coef;// delta y
 float d3 = (src[1][2]- src[0][2]) * coef;// delta z
 dst[1] = d1 + src[0][1];
 dst[0] = d2 + src[0][0];
 dst[2] = d3 + src[0][2];
}
void TraverseBsp(int iNode, CVect3 (&pEyes)[2] , CVect3 (&pBox)[2],void *(pAction)(T_BSP_NODE *,void *param),void *param)
 {
 int plane;
 float eyesmin_boxmin;
 float boxmax_eyesmax;
 float eyesmin_fdist;
 float eyes_max_fdist;
 float eyesmin_div_deltadist;
 CVect3 tBox1[2];
 CVect3 tBox2[2];
 CVect3 newEyes[2];
 CVect3 ajusted;
 T_BSP_NODE *pNode = &m_tNode[iNode];
 if ( pNode)
 {
  if (pNode->planetype & 4 )
  {
   if(pAction == 0)
   {
    RenderGeometry(GetEngine3DInstance(),pNode);
    return;
   }
   else
   {
    pAction(pNode,param);
   }
  }
  plane =pNode->planetype  & 3;
  eyesmin_boxmin = pEyes[0][plane] - pBox[0][plane];
  if ( ( -epsilon < eyesmin_boxmin) | (-epsilon == eyesmin_boxmin) || (pEyes[1][plane]- pBox[0][plane])  >= -epsilon )
  {
   boxmax_eyesmax = pBox[1][plane] - pEyes[1][plane];
   if ( (epsilon < boxmax_eyesmax) | (epsilon == boxmax_eyesmax) || (pBox[1][plane] -  pEyes[0][plane]) >= epsilon )
   {
    memmove(tBox1,pBox,sizeof(pBox));
    tBox1[0][plane] = pNode->fDist;
    memmove(tBox2,pBox,sizeof(pBox));
    tBox2[1][plane] = pNode->fDist;
    eyesmin_fdist = pEyes[0][plane] - pNode->fDist;
    eyes_max_fdist = (pEyes[1][plane]) - pNode->fDist;
    if ( eyesmin_fdist >= -epsilon && eyesmin_fdist <= epsilon|| (eyes_max_fdist >= -epsilon) && eyes_max_fdist <= epsilon )
    {
     if ( pNode->children[1] != (short)-1 ) TraverseBsp(pNode->children[1],  pEyes,  tBox1,pAction,param);
     if ( pNode->children[0] != (short)-1 ) TraverseBsp(pNode->children[0] , pEyes, tBox2,pAction,param);
     return;
    }
    if ( eyesmin_fdist > epsilon && eyes_max_fdist < epsilon)
    {
      if ( pNode->children[1] != (short)-1 ) TraverseBsp(pNode->children[1], pEyes, tBox1,pAction,param);
      return;
    }
    if ( eyesmin_fdist < -epsilon && eyes_max_fdist < -epsilon)
    {
      if ( pNode->children[0] != (short)-1 ) TraverseBsp(pNode->children[0] , pEyes, tBox2,pAction,param);
      return;
    }
    eyesmin_div_deltadist = (float)(eyesmin_fdist / (eyesmin_fdist - eyes_max_fdist));
    AjustDelta(pEyes, ajusted, eyesmin_div_deltadist);
    if ( eyesmin_fdist <= 0.0 )
    {
     if ( pNode->children[0]  != (short)-1 )
     {
      MergeBox(newEyes, &pEyes[0][0], ajusted);
      TraverseBsp(pNode->children[0] , newEyes, tBox2,pAction,param);
     }
     if (pNode->children[1]  != (short)-1 )
     {
      MergeBox(newEyes, ajusted, &pEyes[1][0]);
      TraverseBsp(pNode->children[1] , newEyes, tBox1,pAction,param);
     }
    }
    else
    {
     if ( pNode->children[1]  != (short)-1 )
     {
      MergeBox(newEyes, &pEyes[0][0], ajusted);
      TraverseBsp(pNode->children[1] , newEyes, tBox1,pAction,param);
     }
     if (pNode->children[0]  != (short)-1 )
     {
      MergeBox(newEyes, ajusted, &pEyes[1][0]);
      TraverseBsp(pNode->children[0] , newEyes, tBox2,pAction,param);
     }
    }
   }
  }
 }
}
CheckFromEyes(CVect3 (&pEyes)[2],void *(pAction)(T_BSP_NODE *,void *param),void *param )
{
/*CVect3 eyes[2];
instance_mat.invert();
eyes[0] = _fixCoordSystemInv((instance_mat*p->m_pCameraViewport->GetCameraTarget())+CVect3(0,-10,0) );
eyes[1] = _fixCoordSystemInv((instance_mat*p->m_pCameraViewport->GetCameraTarget())+CVect3(0,60,0) ); 
 // make vector down
*/
/* eyes[0] = CVect3(-1.474797e+001F, -1.195053e+001F,  5.416779e+000F); // Debug absolute position from WP  Azaroth 1164,58,-10645.83
eyes[1] = CVect3(-1.474797e+001F, -1.195053e+001F, -1.754583e+003F);
*/
TraverseBsp(0,pEyes,m_bbox,pAction);
}

This BSP seems to be used for collision purpose only.

An object could have has 2 collision system. The first one is encoded in a simplified Geometry (when MOPY. MaterialID=0xFF) the second one is encoded in T_BSP_NODE. Some object has collision method 1 only, some other uses method 2 only. Some object have both collision systems (some polygons are missing in the BSP but are present in the simplified geometry). how to use these 2 system remains unclear.

For the time being, I check first the simplified geometry, and then if there is no collision, I apply a second pass using the BSP. It is sub-optimum, but it seems to work. Probably there is somewhere a flag telling us with which method we should use for the object.

The code attached seems to work fine for BSP method--peter-pan.

MOBR chunk

  • Face indices for CAaBsp (MOBN). Unsigned shorts.
  • Triangle indices (in MOVI which define triangles) to describe polygon planes defined by MOBN BSP nodes.
uint16_t nodeFaceIndices[];

Example code required to get an actual indices array from MOBR array:

var bpsIndicies = new Array(mobr.length*3);
for (var i = 0; i < mobr.length; i++) {
    bpsIndices[i*3 + 0] = movi[3*mobr[i]+0];
    bpsIndices[i*3 + 1] = movi[3*mobr[i]+1];
    bpsIndices[i*3 + 2] = movi[3*mobr[i]+2];
}

Example code to get indices into MOVT for triangles, referenced from BSP node definition:

for (var triangleInd = node.firstFace; triangleInd<node.firstFace+node.numFaces; triangleInd++) {
    //3 vertices per triangle
    movt[bpsIndices[3*triangleInd + 0]]
    movt[bpsIndices[3*triangleInd + 1]]
    movt[bpsIndices[3*triangleInd + 2]]
}

MOCV chunk

  • Vertex colors, 4 bytes per vertex (BGRA), for WMO groups using indoor lighting.
CImVector colorVertexList[];

I don't know if this is supposed to work together with, or replace, the lights referenced in MOLR. But it sure is the only way for the ground around the goblin smelting pot to turn red in the Deadmines. (but some corridors are, in turn, too dark - how the hell does lighting work anyway, are there lightmaps hidden somewhere?)

- I'm pretty sure WoW does not use lightmaps in it's WMOs...

After further inspection, this is it, actual pre-lit vertex colors for WMOs - vertex lighting is turned off. This is used if flag 0x2000 in the MOGI chunk is on for this group. This pretty much fixes indoor lighting in Ironforge and Undercity. The "light" lights are used only for M2 models (doodads and characters). (The "too dark" corridors seemed like that because I was looking at it in a window - in full screen it looks pretty much the same as in the game) Now THAT's progress!!!

Yes, 0x2000 (INDOOR) flagged WMO groups use _only_ MOCV for lighting, however this chunk is also used to light outdoor groups as well like lantern glow on buildings, etc. If 0x8 (OUTDOOR) flag is set, you start out with normal world lighting (like with light db params) and then you multiply these vertex colors by the texture color and add it to the world lighting. This makes many models look much better. See the Forsaken buildings in Howling Fjord for an example of some that make use of this a lot for glowing windows and lamps. Relaxok 18:29, 20 March 2013 (UTC)

CMapObjGroup::FixColorVertexAlpha

Prior to being passed to the shaders, MOCV values are manipulated by the CMapObj::FixColorVertexAlpha function in the client. This function performs different manipulations depending on the relationship between the vertex and the MOBA it appears in. It's possible that FixColorVertexAlpha did not always exist, or does not exist in later versions of WoW. It appears to have existed in WotLK, Cata, MoP, and WoD.

In client versions that use FixColorVertexAlpha, without applying the function, certain parts of WMOs are noticeably wrong: fireplaces lack a glowing effect; the red light cast from bellows in blacksmith WMOs is undersaturated; etc.

WMOs with MOHD->flags & 0x08

Only one manipulation takes place:

MOCVs matching vertices in MOGP->batchCounts[1] and MOGP->batchCounts[2] are modified like so:

1. If MOGP.flags & 0x08, replace MOCV->color[a] with 255; else replace MOCV->color[a] with 0

All other WMOs

The following manipulations take place:

MOCVs matching vertices in MOGP->batchCounts[0] (aka unkBatchCount) are modified like so:

1. Subtract MOHD->color[r|g|b]
2. Subtract MOCV->color[r|g|b] * MOCV->color[a]
3. Divide new MOCV->color[r|g|b] values by 2.0

MOCVs matching vertices in MOGP->batchCounts[1] and MOGP->batchCounts[2] are modified like so:

1. Subtract MOHD->color
2. Add (MOCV->color[r|g|b] * MOCV->color[a]) >> 6
3. Divide MOCV->color[r|g|b] values by 2.0
4. If values are >= 0 and  <= 255, keep value as is; else clamp new value to 0, 255.
5. If MOGP.flags & 0x08, replace MOCV->color[a] with 255; else replace MOCV->color[a] with 0

Decompiled code

From build 18179, courtesy of schlumpf

void CMapObjGroup::FixColorVertexAlpha(CMapObjGroup *mapObjGroup)
{
  int begin_second_fixup = 0;
  if ( mapObjGroup->unkBatchCount )
  {
    begin_second_fixup = mapObjGroup->moba[mapObjGroup->transBatchCount-1].maxIndex+ 1;
  }

  if ( mapObjGroup->m_mapObj->mohd->flags & flag_has_some_outdoor_group )
  {
    for (int i (begin_second_fixup); i < mapObjGroup->mocv_count; ++i)
    {
      mapObjGroup->mocv[i].w = mapObjGroup->m_groupFlags & SMOGroup::EXTERIOR ? 0xFF : 0x00;
    }
  }
  else
  {
    if ( mapObjGroup->m_mapObj->mohd->flags & flag_skip_base_color )
    {
      v35 = 0;
      v36 = 0;
      v37 = 0;
    }
    else
    {
      v35 = (mapObjGroup->m_mapObj->mohd.color >> 0) & 0xff;
      v37 = (mapObjGroup->m_mapObj->mohd.color >> 8) & 0xff;
      v36 = (mapObjGroup->m_mapObj->mohd.color >> 16) & 0xff;
    }

    for (int mocv_index (0); mocv_index < begin_second_fixup; ++mocv_index)
    {
      mapObjGroup->mocv[mocv_index].x -= v36;
      mapObjGroup->mocv[mocv_index].y -= v37;
      mapObjGroup->mocv[mocv_index].z -= v35;

      v38 = mapObjGroup->mocv[mocv_index].w / 255.0f;

      v11 = mapObjGroup->mocv[mocv_index].x - v38 * mapObjGroup->mocv[mocv_index].x;
      assert (v11 > -0.5f);
      assert (v11 < 255.5f);
      mapObjGroup->mocv[mocv_index].x = v11 / 2;
      v13 = mapObjGroup->mocv[mocv_index].y - v38 * mapObjGroup->mocv[mocv_index].y;
      assert (v13 > -0.5f);
      assert (v13 < 255.5f);
      mapObjGroup->mocv[mocv_index].y = v13 / 2;
      v14 = mapObjGroup->mocv[mocv_index].z - v38 * mapObjGroup->mocv[mocv_index].z;
      assert (v14 > -0.5f);
      assert (v14 < 255.5f);
      mapObjGroup->mocv[mocv_index++].z = v14 / 2;
    }

    for (int i (begin_second_fixup); i < mapObjGroup->mocv_count; ++i)
    {
      v19 = (mapObjGroup->mocv[i].x * mapObjGroup->mocv[i].w) / 64 + mapObjGroup->mocv[i].x - v36;
      mapObjGroup->mocv[i].x = std::min (255, std::max (v19 / 2, 0));

      v30 = (mapObjGroup->mocv[i].y * mapObjGroup->mocv[i].w) / 64 + mapObjGroup->mocv[i].y - v37;
      mapObjGroup->mocv[i].y = std::min (255, std::max (v30 / 2, 0));

      v33 = (mapObjGroup->mocv[i].w * mapObjGroup->mocv[i].z) / 64 + mapObjGroup->mocv[i].z - v35;
      mapObjGroup->mocv[i].z = std::min (255, std::max (v33 / 2, 0));

      mapObjGroup->mocv[i].w = mapObjGroup->m_groupFlags & SMOGroup::EXTERIOR ? 0xFF : 0x00;
    }
  }
}

CMapObj::AttenTransVerts

Similar to FixColorVertexAlpha above, the client will also run MOCV values through the CMapObj::AttenTransVerts function prior to rendering.

In MoP and WoD, it appears that the client only runs AttenTransVerts in cases where flag 0x01 is NOT set on MOHD.flags.

AttenTransVerts only modifies MOCV values for vertices in MOGP.batchCounts[0] (aka unkBatchCount) batches.

The function iterates over all vertices in MOGP.batchCounts[0], and checks all portals for the group:

  • If no portals are found that lead to a group with MOGI.flags & (0x08 | 0x40), all MOCV alpha values are set to 0.0.
  • If a portal is found leading to a group with MOGI.flags & (0x08 | 0x40), each MOCV alpha is manipulated to be a range of 0.0 to 1.0 based on the distance of the corresponding vertex to the portal. Additionally, the RGB values for each MOCV are bumped by: (0.0 to 1.0) * (127 - existingRGB)

Decompiled code

void CMapObj::AttenTransVerts (CMapObj *mapObj, CMapObjGroup *mapObjGroup)
{
  mapObjGroup->field_98 |= 1u;
  if (!mapObjGroup->unkBatchCount)
  {
    return;
  }

  for ( std::size_t vertex_index (0)
      ; vertex_index < (*((unsigned __int16 *)&mapObjGroup->moba[(unsigned __int16)mapObjGroup->unkBatchCount] - 2) + 1)
      ; ++vertex_index
      )
  {
    float opacity_accum (0.0);

    for ( std::size_t portal_ref_index (mapObjGroup->mogp->mopr_index)
        ; portal_ref_index < (mapObjGroup->mogp->mopr_index + mapObjGroup->mogp->mopr_count)
        ; ++portal_ref_index
        )
    {
      SMOPortalRef const& portalRef (mapObj->mopr[portal_ref_index]);
      SMOPortal const& portal (mapObj->mopt[portalRef.portalIndex]);
      C3Vector const& vertex (&mapObjGroup->movt[vertex_index]);

      float const portal_to_vertex (distance (portal.plane, vertex));

      C3Vector vertex_to_use (vertex);

      if (portal_to_vertex > 0.001 || portal_to_vertex < -0.001)
      {
        C3Ray ray ( C3Ray::FromStartEnd
                      ( vertex
                      , vertex
                      + (portal_to_vertex > 0 ? -1 : 1) * portal.plane.normal
                      , 0
                      )
                  );
        NTempest::Intersect
          (ray, &portal.plane, 0LL, &vertex_to_use, 0.0099999998);
      }

      float distance_to_use;

      if ( NTempest::Intersect ( vertex_to_use
                               , &mapObj->mopv[portal.base_index]
                               , portal.index_count
                               , C3Vector::MajorAxis (portal.plane.normal)
                               )
         )
      {
        distance_to_use = portalRef.side * distance (portal.plane, vertex);
      }
      else
      {
        distance_to_use = NTempest::DistanceFromPolygonEdge
          (vertex, &mapObj->mopv[portal.base_index], portal.index_count);
      }

      if (mapObj->mogi[portalRef.group_index].flags & 0x48)
      {
        float v25 (distance_to_use >= 0.0 ? distance_to_use / 6.0f : 0.0f);
        if ((1.0 - v25) > 0.001)
        {
          opacity_accum += 1.0 - v25;
        }
      }
      else if (distance_to_use > -1.0)
      {
        opacity_accum = 0.0;
        if (distance_to_use < 1.0)
        {
          break;
        }
      }
    }

    float const opacity ( opacity_accum > 0.001
                        ? std::min (1.0f, opacity_accum)
                        : 0.0f
                        );

    //! \note all assignments asserted to be > -0.5 && < 255.5f
    CArgb& color (mapObjGroup->mocv[vertex_index]);
    color.r = ((127.0f - color.r) * opacity) + color.r;
    color.g = ((127.0f - color.g) * opacity) + color.g;
    color.b = ((127.0f - color.b) * opacity) + color.b;
    color.a = opacity * 255.0;
  }
}

MLIQ chunk

  • Specifies liquids inside WMOs.

This is where the water from Stormwind and BFD etc. is hidden. (slime in Undercity, pool water in the Darnassus temple, some lava in IF)

Chunk header:

Offset	Type 			Description
0x00 	uint32 			number of X vertices (xverts)
0x04 	uint32 			number of Y vertices (yverts)
0x08 	uint32 			number of X tiles (xtiles = xverts-1)
0x0C 	uint32 			number of Y tiles (ytiles = yverts-1)
0x10 	float[3] 		base coordinates for X and Y
0x1C 	uint16 			material ID (index into MOMT)

After the header, verts and tiles follow:

struct SMOLVert
{
  union
  {
    struct SMOWVert
    {
      uint8_t flow1;
      uint8_t flow2;
      uint8_t flow1Pct;
      uint8_t filler;
      float height;
    }  waterVert;
    struct SMOMVert
    {
      int16_t s;
      int16_t t;
      float height;
    } magmaVert;
  };
} liquidVertexList[xverts*yverts];

struct SMOLTile
{
  uint8_t liquid : 6;
  uint8_t fishable : 1;
  uint8_t shared : 1;
} liquidTileList[xtiles*ytiles];

The liquid data contains the vertex height map (xverts * yverts * 8 bytes) and the tile flags (xtiles * ytiles bytes) as described in ADT files (MCLQ chunk). The length and width of a liquid tile is the same as on the map, that is, 1/8th of the length of a map chunk. (which is in turn 1/16th the length of a map tile).

Note that although I could read Mh2o's heightmap and existstable in row major order (like reading a book), I had to read this one in column major order to compensate for a 90° misrotation. --Bananenbrot 22:02, 1 August 2012 (UTC)

Either the unknown data or the "types" must somehow control how the points at the edges work. In looking at 3D mesh screen captures, something is changed to create a flat edge where it meets other MLIQ chunks. The first Unknown data is always 0 when a point isn't used. Other seen values: 1, 4, 12, 22, 27, 31, 105, & 124. Not yet sure what they mean/how to use them, I suspect they become the modifier for the edge placement points. --Kjasi 14 February 2016

WMOs can have liquid in them even if MLIQ is not present! If MOGP.groupLiquid is set but no MLIQ is present or xtiles = 0 or ytiles = 0 then entire group is filled with liquid. In this case liquid height is equal to MOGP.boundingBox.max.z

how to determine LiquidTypeRec to use

enum liquid_basic_types
{
  liquid_basic_types_water = 0,
  liquid_basic_types_ocean = 1,
  liquid_basic_types_magma = 2,
  liquid_basic_types_slime = 3,

  liquid_basic_types_MASK = 3,
};
enum liquid_types
{
  // ...
  LIQUID_WMO_Water = 13,
  LIQUID_WMO_Ocean = 14,
  LIQUID_Green_Lava = 15,
  LIQUID_WMO_Magma = 19,
  LIQUID_WMO_Slime = 20,

  LIQUID_END_BASIC_LIQUIDS = 20,
  LIQUID_FIRST_NONBASIC_LIQUID_TYPE = 21,

  LIQUID_NAXX_SLIME = 21,
  // ...
};

enum SMOGroup::flags
{
  LIQUIDSURFACE = 0x1000,
  is_not_water_but_ocean = 0x80000,
};

liquid_types to_wmo_liquid (int x)
{
  liquid_basic_types const basic (x & liquid_basic_types_MASK);
  switch (basic)
  {
  case liquid_basic_types_water:
    return (smoGroup->flags & is_not_water_but_ocean) ? LIQUID_WMO_Ocean : LIQUID_WMO_Water;
  case liquid_basic_types_ocean:
    return LIQUID_WMO_Ocean;
  case liquid_basic_types_magma:
    return LIQUID_WMO_Magma;
  case liquid_basic_types_slime:
    return LIQUID_WMO_Slime;
  }
}


if ( mapObj->mohd_data->field_3C & 4 )
{
  if ( smoGroup->field_34 < LIQUID_FIRST_NONBASIC_LIQUID_TYPE )
  {
    this->liquid_type = to_wmo_liquid (smoGroup->field_34 - 1);
  }
  else
  {
    this->liquid_type = smoGroup->field_34;
  }
}
else
{
  if ( smoGroup->field_34 == LIQUID_Green_Lava )
  {
    this->liquid_type = 0;
  }
  else
  {
    int const liquidType (smoGroup->field_34 + 1);
    int const tmp (smoGroup->field_34);
    if ( smoGroup->field_34 < LIQUID_END_BASIC_LIQUIDS )
    {
      this->liquid_type = to_wmo_liquid (smoGroup->field_34);
    }
    else
    {
      this->liquid_type = smoGroup->field_34 + 1;
    }
    assert (!liquidType || !(smoGroup->flags & SMOGroup::LIQUIDSURFACE));
  }
}

MORI

uint16_t triangle_strip_indices[];

MORB

This section only applies to versions ≥ CataCould have been added earlier.
  • ignored if !CMap::enableTriangleStrips
  • modifies MOBA, therefore has same count.
  • size is not checked, but 2 * sizeof(int), even though it is only (int, short).
struct MORB_entry
{
  uint32_t start_index;
  uint16_t index_count;
  uint16_t padding;
}
  • overwrites 0xC and 0x10 of MOBA (start, count).

MOTA

This section only applies to versions ≥ CataCould have been added earlier.
  • Map Object Tangent Array
struct MOTA
{
  unsigned short first_index[moba_count]; // either -1 or first index of batch.count indices into tangents[]. 
                                          // if auto-generated, only has entries for batches with 
                                          // material[batch.material].shader == 10 or 14.
  C4Vector tangents[accumulated_num_indices]; // sum (batches[i].count | material[batches[i].material].shader == 10 or 14)
};

Is auto generated, if there are batches with shaders 10 or 14, but no tangents. (And maybe some additional condition.) See CMapObjGroup::Create().

MOBS

This section only applies to versions ≥ CataCould have been added earlier.
struct {
  char unk[0x18];
} map_object_shadow_batches[];

MDAL

This section only applies to versions ≥ WoDCould have been added earlier.
struct
{
  CArgb replacement_for_header_color; // if -1 or not present, take color from header
} mdal;

MOPL

This section only applies to versions ≥ WoDCould have been added earlier.
  • requires MOGP.canCutTerrain
C4Plane terrain_cutting_planes[<=32];

MOPB

This section only applies to versions ≥ LegionCould have been added earlier.
struct {
  char _1[0x18];
} map_object_prepass_batches[];

MOLS

This section only applies to versions ≥ LegionCould have been added earlier.
struct {
  char _1[0x38];
} map_object_spot_lights[];

MOLP

This section only applies to versions ≥ LegionCould have been added earlier.
struct {
   uint32_t unk;
   CImVector color; 
   C3Vector pos; //position of light
   float intensity; 
   float attenStart;
   float attenEnd;
   float unk4;   //Only seen zeros here 
   uint32_t unk5;
   uint32_t unk6; //CArgb?
} map_object_point_lights[];

MLSS

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.
struct {
  uint32_t mols_count; // spotlights per set
} map_object_lightset_spotlights[];

MLSP

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.
struct {
  uint32_t molp_count; // pointlights per set
} map_object_lightset_pointlights[];

MLSO

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.

In binary, not in files

MLSK

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.

Currently only in file 2143042 as of 8.1.5.28938: world/wmo/zuldazar/orc/8or_pvp_warsongbg_main01.wmo. MOP2 lightset.

MOS2

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.

In binary, not in files

MOP2

This section only applies to versions ≥ Battle (8.1.0.27826). Could have been added earlier.

Currently only in file 2143042 as of 8.1.5.28938: world/wmo/zuldazar/orc/8or_pvp_warsongbg_main01.wmo. Version 2 of MOLP

MOLM

This section only applies to versions ≤ PreVanilla (0.5.5.3494). Only used in v14.

Lightmaps were the original lighting implementation for WMOs and the default light mode used in the alpha clients. They were replaced by "vertex lighting" in PreVanilla (0.6.0.3592). The alpha clients can switch between light modes using the mapObjLightMode console command (CWorld:enables & 0x400).

This chunk contains information for blitting the MOLD colour palette. There is one entry for each MOPY and is referenced by matching index.

Exterior lit groups (SMOGroup::EXTERIOR | SMOGroup::EXTERIOR_LIT) are excluded and default to (0,0,0). All other groups have their light colour calculated from the visible SMOPolys using their associated MOLV, MOLM and MOLD entries. This colour is then blended with the texture. The client enforces a minimum of 24 for each colour component and skews the colour based on the dominant RGB component.

struct SMOLightmap
{
  char x;
  char y;
  char width;
  char height;
} lightmapList[];

MOLD

This section only applies to versions ≤ PreVanilla (0.5.5.3494). Only used in v14.

This chunk stores a 255x255 DXT1 compressed colour palette.

struct SMOLightmapTex
{
  char texels[32768];
  union
  {
    char inMemPad[4];
    CGxTex *gxTexture;
    HTEXTURE__ *hTexture;
  };                      // always inMemPad == 0 in file
} lightmapTexList[];

MPB*

These chunks are barely ever present (the one file known is StonetalonWheelPlatform.wmo from alpha). No version of the client ever read them though. They might be an early form of PD4 files, inlined into the WMO and not per root but per group.

MPBV and MPBP appear to be (uint16_t start, uint16_t count)s. This is reasoned by the values being sequential and totalling the entry count of the next chunk. If this is the case, the structure may actually produce groups of groups of vertices e.g. StonetalonWheelPlatform.

MPBV

uint16_t mpbv[];

MPBP

uint16_t mpbp[];

MPBI

uint16_t mpb_indices[];     // triangle vertex indices into into #MPBG

MPBG

C3Vector mpb_vertices[];