From wowdev
Revision as of 23:08, 23 February 2016 by Skarn (talk | contribs) (→‎MOHD chunk)
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/v17 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.

WMO root file

The root file lists the following:

  • textures (BLP File references)
  • materials
  • models (MDX / M2 File references)
  • groups
  • visibility information
  • more data

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;    
/*010h*/  uint32_t nDoodadNames; 
/*014h*/  uint32_t nDoodadDefs;                                    // *
/*018h*/  uint32_t nDoodadSets;    
/*01Ch*/  CArgb color;   
/*020h*/  foreign_key<uint32_t, &WMOAreaTableRec::m_WMOID> wmoID;
/*024h*/  CAaBox bounding_box;
/*03Ch*/  uint32_t flag_0x1 : 1;                                   // sets CMapObjGroup::field_6C | 1
/*03Ch*/  uint32_t flag_do_not_add_base_color : 1;                 // add base (ambient) color (of MOHD) to MOCV. apparently does more, e.g. required for multiple MOCVs (was earlier referenced as "add base color")
/*03Ch*/  uint32_t flag_liquid_related : 1;                        // possibly - LiquidType related, see below in the MLIQ
/*03Ch*/  uint32_t flag_has_some_outdoor_group : 1;                // possibly - has some group that is outdoors
/*03Ch*/  uint32_t Flag_Lod : 1;                                   // (Legion+)
/*03Ch*/  uint32_t : 27;                                           // unused as of
} header;

MOTX chunk

  • 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 texture_filenames[];

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

MOMT chunk

  • Materials used in this map object, 64 bytes per texture (BLP file), nMaterials entries.
struct SMOMaterial
  uint32_t flag_0x1 : 1;                   // ? (I'm not sure atm I tend to use lightmap or something like this)
  uint32_t flag_0x2 : 1;
  uint32_t flag_no_backface_culling : 1;   // two-sided
  uint32_t flag_darkened : 1;              // ?, the intern face of windows are flagged 0x08
  uint32_t flag_bright_at_night : 1;       // (unshaded) (used on windows and lamps in Stormwind, for example)
  uint32_t flag_0x20 : 1;
  uint32_t flag_clamp : 1;                 // ?, looks like GL_CLAMP
  uint32_t flag_repeat : 1;                // ?, looks like GL_REPEAT
  uint32_t flag_0x100 : 1;
  uint32_t : 23;                           // unused as of
/*004h*/  uint32_t shader;                 // Index into CMapObj::s_wmoShaderMetaData. See below (shader types).
/*008h*/  uint32_t blendMode;              // Blending: 0 for opaque, 1 for transparent
/*00Ch*/  uint32_t texture_0;              // offset into MOTX
/*010h*/  uint32_t color_0;                // rgba8 (four uint8s)
/*014h*/  uint32_t flags_0;
/*018h*/  uint32_t texture_1;
/*01Ch*/  uint32_t color_1;
/*020h*/  foreign_key<uint32_t, &TerrainTypeRec::m_ID> ground_type;            // according to CMapObjDef::GetGroundType
/*024h*/  uint32_t texture_2;
/*028h*/  uint32_t color_2;
/*02Ch*/  uint32_t flags_2;
/*030h*/  uint32_t runTimeData[4];         // This data is explicitly nulled upon loading. Contains textures or similar stuff.
} materials[];

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.

Shader types

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 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 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 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 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

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;

    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;

MOGN chunk

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

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. It (always ?) begins with two empty strings, so 0x00 0x00, and is 4-byte padded at the end of the chunk only. 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).

MOGI chunk

  • Group information for WMO groups, 32 bytes per group, nGroups entries.
struct WMOGroup
/*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)
} groups[];

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

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

MOSB chunk

  • 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 skybox_filename[];

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 portal_vertices[];

Portals are (always?) rectangles that specify where doors or entrances are in a WMO/v17. They could be used for visibility, but I currently have no idea what relations they have to each other or how they work.

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/v17 using this and the MOPR chunk. This could be used to speed up the rendering once/if I figure out how.

It looks like there are nPortals entries, but in some models like CavernsofTime.wmo, |MOPV| = 1896 bytes = 474 floats = 158 vertices = 39.5 rectangles ???

MOPT chunk

  • Portal information. 20 bytes per portal, nPortals entries. 128 portals max.
struct SMOPortal
  uint16_t base_index;
  uint16_t index_count;
  C4Plane  plane;
} portals[];

MOPR chunk

  • Map Object Portal References from groups. Mostly twice the number of portals. Actual count defined by sum (MOGP.portals_used).

It's not correct that there's 2*nPortals of 8 bytes in all cases. For example in WOTLK data, Stormwind.wmo has 319 portals but only 627 portal relationships before MOVV chunk starts. Maybe some portals are unused or something, I haven't analyzed further - but there are many models where this is the case. ---Relaxok, 09-06-2012

struct SMOPortalRef // 04-29-2005 By ObscuR
  uint16_t portal_index;  // into MOPR
  uint16_t group_index;   // the other one
  uint16_t side;          // positive or negative.
  uint16_t unk;
} portal_references[];

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
  uint16_t firstVertex;
  uint16_t count;
) visible_blocks[];

MOLT chunk

  • Lighting information. 48 bytes per light, nLights entries
enum LightType
struct SMOLight // 04-29-2005 By ObscuR
  /*000h*/  uint8_t lightType;
  /*001h*/  uint8_t type;
  /*002h*/  uint8_t useAtten;
  /*003h*/  uint8_t pad;
  /*004h*/  uint8_t color[4];     // Color (B,G,R,A)
  /*008h*/  float   position[3];  // Position (X,Z,-Y)
  /*014h*/  float   intensity;
  /*018h*/  float   attenStart;
  /*01Ch*/  float   attenEnd;
  /*020h*/  float   unk1;
  /*024h*/  float   unk2;
  /*028h*/  float   unk3;
  /*02Ch*/  float   unk4;
} lights[];

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 paramters might be range or attenuation information, or something else entirely. Some WMO/v17 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/v17 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/v17 local lights... :)

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/v17. Like, a small house might have tables and a bed laid out neatly in one set called "Set_$DefaultGlobal", and have a horrible mess of abandoned broken things in another set called "Set_Abandoned01". The names are only informative.

The doodad set number for every WMO instance is specified in the ADT files.

struct SMODoodadSet
/*000h*/  char     name[20];            // set name
/*014h*/  uint32_t firstinstanceindex;  // index of first doodad instance in this set
/*018h*/  uint32_t numDoodads;          // number of doodad instances in this set
/*01Ch*/  uint32_t unused;
} doodad_sets[];

firstinstanceindex is not the name index, but the actual order the doodads come in the MODD chunk in the WMO -MaiN

MODN chunk

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

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

char doodad_filenames[];

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 WMO/v17s and models (M2s) in a map tile are rotated along the axes, doodads within a WMO/v17 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 WMO/v17s 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 name_offset : 24;        // reference offset into MODN
  /*003h*/  uint32_t flag_AcceptProjTex : 1;
  /*003h*/  uint32_t flag_0x2 : 1;            // MapStaticEntity::field_34 |= 1
  /*003h*/  uint32_t flag_0x4 : 1;
  /*003h*/  uint32_t flag_0x8 : 1;
  /*003h*/  uint32_t : 4;                     // unused as of
  /*004h*/  C3Vector position;                // (X,Z,-Y)
  /*010h*/  C4Quaternion orientation;         // (X, Y, Z, W)
  /*020h*/  float scale;                      // scale factor
  /*024h*/  uint8_t color[4];                 // (B,G,R,A) lightning color
} doodad_definitions[];
  • 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.multiply(placementMatrix, placementMatrix, wmoPlacementMatrix);

    mat4.translate(placementMatrix, placementMatrix, [modd.pos[0],modd.pos[1], modd.pos[2]]);

    var orientMatrix = mat4.create();
        [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; // Ignore radius in CWorldView::QueryCameraFog
  /*000h*/  uint32_t : 3;                      // unused as of
  /*000h*/  uint32_t flag_0x10 : 1;
  /*000h*/  uint32_t : 27;                     // unused as of
  /*004h*/  C3Vector pos;
  /*010h*/  float smaller_radius;
  /*014h*/  float larger_radius;
  /*018h*/  float fog_end;
  /*01Ch*/  float fog_start_multiplier;        // (0..1)
  /*020h*/  uint8_t color[4];                  // The back buffer is also cleared to this colour
  /*024h*/  float unk1;                        // almost always 222.222
  /*028h*/  float unk2;
  /*02Ch*/  uint8_t color2[4];
} fogs[];
  • 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.

MCVP chunk (optional)

  • Convex Volume Planes. Contains blocks of floating-point numbers. 0x10 bytes (4 floats) per entry.
C4Plane convex_volume_planes[];   // 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 (Legion+)

  • required when WMO is load from fileID (e.g. game objects)
  uint32_t ids[header.flags & SMOHeader::Flag_Lod ? 3 : 1];
} group_file_ids[header.nGroups];

WMO group file

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

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

Note: In its header is given a wrong size. Just use 0x44. -eLaps

  • Actually, the size is correct, the other chunks are just subchunks of MOGP :) ---Tigurius
Offset	Type		Description
0x00 	uint32 		Group name (offset into MOGN chunk)
0x04 	uint32 		Descriptive group name (offset into MOGN chunk)
0x08 	uint32 		Flags
0x0C 	float[3] 	Bounding box corner 1 (same as in MOGI)
0x18 	float[3] 	Bounding box corner 2
0x24 	uint16 		Index into the MOPR chunk
0x26 	uint16 		Number of items used from the MOPR chunk
0x28 	uint16 		Number of batches A
0x2A 	uint16 		Number of batches interior
0x2C 	uint32 		Number of batches exterior
0x30 	uint8[4] 	Up to four indices into the WMO fog list
0x34 	uint32 		LiquidType related, see below in the MLIQ chunk.
0x38 	foreign_key<uint32_t, &WMOAreaTableRec::m_WMOGroupID> 		WMO group ID
0x3C 	uint32 		&1: WoD(?)+ CanCutTerrain (by MOPL planes), others (UNUSED: 20740)
0x40 	uint32 		(UNUSED: 20740)

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

For the "Number of batches" fields, A + batches_interior + batches_exterior == the total number of batches in the WMO/v17 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 MOBN and MOBR chunk.
0x2		(UNUSED: 20740) possibly: subtract mohd.color in mocv fixing 
0x4 		Has vertex colors (MOCV chunk).
0x8 		SMOGroup::EXTERIOR -- Outdoor
0x10		(UNUSED: 20740)
0x20		(UNUSED: 20740)
0x200 		Has lights  (MOLR chunk)
0x400		<= Cataclysm: Has MPBV, MPBP, MPBI, MPBG chunks, neither 3.3.5a nor Cataclysm alpha actually use them though, but just skips them. Legion+(?): 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)
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.
0x400000	(UNUSED: 20740)
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     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	(UNUSED: 20740)
0x40000000	SMOGroup::TVERTS3: Has three MOTV chunks, eg. for MOMT with shader 18.


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.

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[movi_index].points[0]]
      , &this->m_vertices[this->movi[movi_index].points[1]]
      , &this->m_vertices[this->movi[movi_index].points[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.
enum SMOPolyFlags // 03-29-2005 By ObscuR
struct SMOPoly
  /*000h*/ uint8 flags;
  /*001h*/ uint8 material_id;
Flag	Description
0x00 	?
0x01 	?
0x04 	no collision
0x08 	?
0x20 	?
0x40 	?

Frequently used flags are 0x20 and 0x40, but I have no idea what they do.

Be quiet peasants! Glorious Ohai of Modcraft Von Superparanoid discovered disabling collision for a triangle in the following structure; MOPYs triangle_material_info - struct MOPY materialInfoForTriangles - struct MOPY_FLAGS flags - ubyte MOPY_FLAG_NO_CAM_COLLIDE : 1 setting its value to 1 with ubyte MOPY_FLAG_UNK_0x20 : 1 value to 0, will disable collision for the selected triangle.

From 15464:

bool isNoCamCollide (uint8 flags) { return flags & 2; }
bool isDetailFace (uint8 flags) { return flags & 4; }
bool isCollisionFace (uint8 flags) { return flags & 8; }
bool isColor (uint8 flags) { return !(flags & 8); }
bool isRenderFace (uint8 flags) { return (flags & 0x24) == 0x20; }
bool isTransFace (uint8 flags) { return (flags & 1) && (flags & 0x24); }
bool isCollidable (uint8 flags) { return isCollisionFace (flags) || isRenderFace (flags); }

Material ID specifies an index into the material table in the root WMO/v17 file's MOMT chunk. Some of the triangles have 0xFF for the material ID, I skip these. (but there might very well be a use for them?)

The triangles with 0xFF Material ID seem to be a simplified mesh. Like for collision detection or something like that. At least stairs are flattened to ramps if you only display these polys. --shlainn 7 Jun 2009

0xFF representing -1 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.

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 WMO/v17s 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.

MONR chunk

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

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.

MOBA chunk

  • Render batches. Records of 24 bytes.
struct Batch
  /*0x00*/ int16_t  a[2*3];        // indices? a box? (-2,-2,-1,2,2,3 in cameron)
  /*0x0C*/ uint32_t first_index;   // index of the first face index used in MOVI
  /*0x10*/ uint16_t num_indices;   // number of indices used
  /*0x12*/ uint16_t first_vertex;  // index of the first vertex used in MOVT
  /*0x14*/ uint16_t last_vertex;   // index of the last vertex used (batch includes this one)
  /*0x16*/ uint8_t  flags;
    use_uint16_t_material = 2, // Legion+. instead of materialId use a uint16_t at 0xA (a[5])
  /*0x17*/ uint8_t  materialId;    // index in MOMT

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 first_vertex to last_vertex 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)

MOLR chunk

  • Light references, one 16-bit integer per light reference.

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

For some WMO/v17 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.

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

MOBN chunk

  • Nodes of the BSP tree, used for collision (along with bounding boxes ?). Array of t_BSP_NODE. / CAaBspNode. 0x10 bytes.
struct t_BSP_NODE
  uint16_t planeType;    // 4: leaf, 0 for YZ-plane, 1 for XZ-plane, 2 for XY-plane
  int16_t  children[2];  // index of bsp child node (right in this array)
  uint16_t numFaces;     // num of triangle faces in MOBR
  uint32_t firstFace;    // index of the first triangle index(in MOBR)
  float    fDist;

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 --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)
  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 )
    tBox1[0][plane] = pNode->fDist;
    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);
    if ( eyesmin_fdist > epsilon && eyes_max_fdist < epsilon)
      if ( pNode->children[1] != (short)-1 ) TraverseBsp(pNode->children[1], pEyes, tBox1,pAction,param);
    if ( eyesmin_fdist < -epsilon && eyes_max_fdist < -epsilon)
      if ( pNode->children[0] != (short)-1 ) TraverseBsp(pNode->children[0] , pEyes, tBox2,pAction,param);
    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);
     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];
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);

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.

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

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

Example code to get indicies 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[bpsIndicies[3*triangleInd + 0]]
    movt[bpsIndicies[3*triangleInd + 1]]
    movt[bpsIndicies[3*triangleInd + 2]]

MOCV chunk

  • Vertex colors, 4 bytes per vertex (BGRA), for WMO/v17 groups using indoor lighting.

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 WMO/v17s...

After further inspection, this is it, actual pre-lit vertex colors for WMO/v17s - 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)

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

We then have [xverts*yverts] of the following:

Offset	Type 			Description
0x00 	uint16[2] 		Unknown Data, first value is usually 86. Second value is (so far) always 0.
0x04 	float 			height data

Followed by [xtiles*ytiles] of:

Offset	Type 			Description
0x00 	uint8 	 	 	types?	// Unsure, if really types. (0 - 20)

The liquid data contains the vertex height map (xverts * yverts * 8 bytes) and the tile flags (xtiles * ytiles bytes) as descripbed 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

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,


  // ...

enum SMOGroup::flags
  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);
    this->liquid_type = smoGroup->field_34;
  if ( smoGroup->field_34 == LIQUID_Green_Lava )
    this->liquid_type = 0;
    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);
      this->liquid_type = smoGroup->field_34 + 1;
    assert (!liquidType || !(smoGroup->flags & SMOGroup::LIQUIDSURFACE));


uint16_t triangle_strip_indices[];


  • 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).


  • 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().


  • size = 0x18
struct MOBS_entry
  char unk[0x18];


  • likely new in WoD, unknown contents.
  CArgb replacement_for_header_color; // if -1 or not present, take color from header
} mdal;

MOPL (WoD(?)+)

  • requires MOGP.canCutTerrain
C4Plane terrain_cutting_planes[<=32];