M2/.skin: Difference between revisions

From wowdev
Jump to navigation Jump to search
(27 intermediate revisions by 9 users not shown)
Line 6: Line 6:
  {
  {
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=3}}
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=3}}
   uint32_t magic;                        // 'SKIN'
   uint32_t               magic;                        // 'SKIN'
  #endif
  #endif
   M2Array<unsigned short> [[#Indices|vertices ]];
   M2Array<unsigned short> [[#Vertices|vertices]];
   M2Array<unsigned short> [[#Triangles|indices ]];
   M2Array<unsigned short> [[#Indices|indices]];
   M2Array<ubyte4> [[#Vertex_properties|bones]];
   M2Array<ubyte4>         [[#Bones|bones]];
   M2Array<M2SkinSection> [[#Submeshes|submeshes]];
   M2Array<M2SkinSection> [[#Submeshes|submeshes]];
   M2Array<M2Batch> [[#Texture_units|batches]];
   M2Array<M2Batch>       [[#Texture_units|batches]];
   uint32_t boneCountMax;                  // WoW takes this and divides it by the number of bones in each submesh, then stores the biggest one.
   uint32_t               boneCountMax;                  // WoW takes this and divides it by the number of bones in each submesh, then stores the biggest one.
                                          // Maximum number of bones per drawcall for each view. Related to (old) GPU numbers of registers.  
                                                          // Maximum number of bones per drawcall for each view. Related to (old) GPU numbers of registers.  
                                          // Values seen : 256, 64, 53, 21
                                                          // Values seen : 256, 64, 53, 21
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=4}}
  #if {{Template:Sandbox/VersionRange|min_expansionlevel=4}}
   M2Array<M2ShadowBatch> [[#shadow_batches|shadow_batches]];
   M2Array<M2ShadowBatch> [[#shadow_batches|shadow_batches]];
  #endif
  #endif
  } header;
  }  
header;
 
==Vertices==
This is a lookup table to select a subset of vertices from the [[M2#Vertices|global vertex list]] used by this skin.
 
{| class="wikitable"
! Offset (hex) !! Type !! Name !! Description
|-
| 00 || uint16 || Vertex || an index into the [[M2#Vertices|global vertex list]]
|}


==Indices==
==Indices==
*'''nIndices 16-bit unsigned shorts, specifing vertices from the global vertex list for later use.'''
This is a lookup table to select a subset of vertices from the [[M2/.skin#Vertices|local vertex list]] used by this skin. This array can be used as an index buffer for draw calls.
'''Offset Type   Name   Description'''
 
''0x00''  uint16 Vertex    The vertex in the [[M2#Vertices|global vertex list]].
{| class="wikitable"
! Offset (hex) !! Type !! Name !! Description
|-
| 00 || uint16 || Index || an index into the [[M2/.skin#Vertices|local skin vertex list]]
|}
 
Indices form a list of triangles, with a set of 3 indices forming a single one. The overall number of indices therefore shall be divisible by 3. Triangles are right-handed; for left-handed draw calls, the 2nd and 3rd index in every 3-index-triangle-set need to be swapped.
 
==Bones==
This is a lookup table to select a subset of bones from the [[M2#Bones|global bone list]] used by this skin.


==Triangles==
{| class="wikitable"
*'''nTriangles entries of each 3 unsigned shorts. They refer to indices in the list above.'''
! Offset (hex) !! Type !! Name !! Description
'''Offset Type   Name   Description'''
|-
''0x00''  uint16  Indices[3] Three indices which make up a triangle.
| 00 || uint8 [4] || Bones || 4 indices into the [[M2#Bones|global bone list]] (see remarks below)
|}


I believe (empirically tested) that nTriangles is actually not the number of triangles in the list, but the number of vertexes in the triangle list. That is, the actual number of triangles in the list is actually nTriangles / 3I discovered this when a test application I've been writing attempted to read past the end of the file when not first dividing the number of triangles to read by 3.
Blizzard uses a standard 4-bone rig for animations. Each entry therefore represents 4 bone indices.   


==Vertex properties==
Remarks:
*'''nProperties entries, they are the Bone Indices for the Vertices'''
: ''It seems to be an index into actual bones struct, not the lookup table -- Skarn''
'''Offset  Type  Name    Description'''
: ''An index into the [[M2#Bones|bone list]] would make more sense, than into the [[M2#Bone Lookup Table|bone lookup table (boneCombos)]]. Vertex bone weights point to that list too. -- Nieriel''
''0x00''  4*uint8  Properties  Bone Indices (Index into BoneLookupTable) -- It seems to be an index into actual bones struct, not the lookup table ~ Skarn
: ''Seems like an index here points to bone_lookup_table with offset = [[M2/.skin#Submeshes|Submesh]].boneComboIndex. I.e. [[M2#Bone Lookup Table|bone_lookup_table]][submesh.boneComboIndex + Bones[i][j]] == [[M2#Vertices|M2Vertices]] [ [[M2/.skin#Vertices|SkinVertices]][i] ].bone_indices[j]  -- Vovangrat''


==Submeshes==
==Submeshes==
Line 67: Line 87:


===Mesh part ID===
===Mesh part ID===
For character models, each hairstyle/thick armor/etc is present in the mesh, so to render a character with a specific set of looks, some of the submeshes should be omitted based on this ID.
Submeshes are sorted into groups. As Blizzard uses multiple integers (3 ?) for masking them, there are 8*i groups possible.


The submeshes are sorted into groups. As Blizzard uses multiple integers (3 ?) for masking them, there are 8*i groups possible. Groups are like this for character models. They can be different for other models. Note that ** starts with 01, not 00 (with the exception of entry 0, which is the skin).
'''For groups specific to character models, see the table on the [[Character_Customization#Geosets|Character Customization]] page.''' Groups for other models can be different from the ones listed there.
0000: Skin
00**: Hair: {1-21: various hairstyles}
01**: Facial1: {1-8: varies} (usually beard, but not always)
02**: Facial2: {1: none (DNE), 2-6: varies} (usually mustache, but not always)
03**: Facial3: {1: none (DNE), 2-11: varies} (usually sideburns, but not always)
04**: Glove: {1-4}
05**: Boots: {1-5}
06**:
07**: Ears: {1: none (DNE), 2: ears}
08**: Wristbands / Sleeves: {1: none (DNE), 2: normal, 3: ruffled}
09**: Kneepads / Legcuffs: {1: none (DNE), 2: long, 3: short}
10**: Chest: {1: none (DNE), 2: ??? (exists but purpose unknown)}
11**: Pants: {1: regular, 2: short skirt, 4: armored pants}
12**: Tabard: {1: none (DNE), 2: tabard}
13**: Trousers: {1: legs, 2: dress}
14**:
15**: Cloak: {1-10: various cloak styles}
16**:
17**: Eyeglows: {1: none (DNE), 2: racial eyeglow, 3: DK eyeglow}
18**: Belt / bellypack: {1: none (sometimes DNE), 2: bulky belt}
19**: Tail (in Legion this group also has Undead bones)
20**: Feet: {1: none, 2: feet}
23**: New in Legion (in addition to character models also exist in DH collections) { 1: hands for blood elf/night elf }
24**: Horns (only exist in DH collections) {1-x}
25**: Blindfolds (only exist in DH collections) {1-x}


Some particular geosets (such as 701) are marked as 'DNE' (Does Not Exist). This is to indicate that that particular geoset does not actually exist in any skin files. However, the game will still reference these geosets in the case that it wants nothing in that geoset group to show up.
You can use this together with [[CreatureDisplayInfo.dbc]].creatureGeosetData for nice effects. Also used in [[ItemDisplayInfo.dbc]].m_geosetGroup[] (see that page for an explanation of how the geoset group fields relate to this).


You can use this together with [[CreatureDisplayInfo.dbc]].creatureGeosetData for nice effects. Also used in [[ItemDisplayInfo.dbc]].m_geosetGroup[] (see that page for an explanation of how the geoset group fields relate to this).
===Creature Geoset Data Example===
Following code used by Blizzard for non character based creature models:
void ApplyMonsterGeosets(CM2Model *pModel, CreatureDisplayInfoRec *pDisplayInfo, CharacterComponent *pCharacterComponent)
{
    if (pModel && pDisplayInfo)
    {
        CreatureModelData *modelDataRec = ClientDB::CreatureModelDataDB::GetRow(pCharacterComponent->ModelId); // why not take ModelId from pDisplayInfo?
       
        if (!modelDataRec)
            __debugbreak();
        if (modelDataRec->CreatureGeosetDataID)
        {
            CM2Model::SetGeometryVisible(pModel, 1, 899, false); // 899 seems wrong because there's now geosets >= 9xx in the data
            int displayInfoID = pDisplayInfo->ID;
            auto geosetDatas = ClientDB::CreatureDisplayInfoGeosetDataDB::GetRows([](auto cdigd) { return cdigd->CreatureDisplayInfoID == displayInfoID; });
            for (auto geosetData : geosetDatas)
            {
                int meshId1 = 100 * (geosetData->GeosetIndex + 1);
                CM2Model::SetGeometryVisible(pModel, meshId1, meshId1 + 99, false);
                int meshId2 = meshId1 + geosetData->GeosetValue;
                CM2Model::SetGeometryVisible(pModel, meshId2, meshId2, true);
            }
        }
    }
}


===Mesh override===
===Mesh override===
Line 178: Line 201:
  struct M2Batch  
  struct M2Batch  
  {
  {
   uint8_t flags;                      // Usually 16 for static textures, and 0 for animated textures. &0x1: materials invert something; &0x2: transform &0x4: projected texture; &0x10: something batch compatible; &0x20: projected texture?; &0x40: use textureWeights
   uint8_t flags;                      // Usually 16 for static textures, and 0 for animated textures. &0x1: materials invert something; &0x2: transform &0x4: projected texture; &0x10: something batch compatible; &0x20: projected texture?; &0x40: possibly don't multiply transparency by texture weight transparency to get final transparency value(?)
   int8_t priorityPlane;
   int8_t priorityPlane;
   uint16_t shader_id;                  // See below.
   uint16_t shader_id;                  // See below.
   uint16_t skinSectionIndex;          // A duplicate entry of a submesh from the list above.
   uint16_t skinSectionIndex;          // A duplicate entry of a submesh from the list above.
   uint16_t geosetIndex;                // See below.
   uint16_t geosetIndex;                // See below. New name: flags2. 0x2 - projected. 0x8 - EDGF chunk in m2 is mandatory and data from is applied to this mesh
   uint16_t colorIndex;                // A Color out of the [[M2#Submesh_Animations|Colors-Block]] or -1 if none.
   uint16_t colorIndex;                // A Color out of the [[M2#Submesh_Animations|Colors-Block]] or -1 if none.
   uint16_t materialIndex;              // The [[M2#Render_flags|renderflags]] used on this texture-unit.
   uint16_t materialIndex;              // The [[M2#Render_flags|renderflags]] used on this texture-unit.
Line 196: Line 219:


-- FWIW, this offset is never touched by the client. It is possibly some sort of legacy field that is unused nowadays. [[User:Simca|Simca]] ([[User talk:Simca|talk]]) 02:20, 10 April 2016 (CEST)
-- FWIW, this offset is never touched by the client. It is possibly some sort of legacy field that is unused nowadays. [[User:Simca|Simca]] ([[User talk:Simca|talk]]) 02:20, 10 April 2016 (CEST)
Since BfA this field was renamed into flags2


===shader_id and textureCount===
===shader_id and textureCount===
Line 246: Line 271:
   }
   }
[[User:Deamon|Deamon]] ([[User talk:Deamon|talk]])
[[User:Deamon|Deamon]] ([[User talk:Deamon|talk]])
 
 
I would add that most of the problematic submeshes are flat. So if we take the shortest edge, back to front, of an AABB and create a normal from it.  Then we can combine it with the center of the AABB to make a plane. Next, we use the plane test to determine which plane or other object is closest and sort. The rest of the nonflat objects can be sorted by their centers.  The remaining case is round objects that surround other objects. We can cheat and sort them by their farthest point, using the stored radius and center to approximate it, then use front face culling to render only the back half. I'm not sure this case is required though. At least for the sky sphere we can just cull all triangles that are nearer than the nearplane of the view frustum.
[[User:Lumirion|Lumirion]] ([[User talk:Lumirion|talk]])


===Environment mapping===
===Environment mapping===

Revision as of 14:58, 13 July 2021

Okay, there is no ofsViews anymore in M2-files, but we still got nViews at 4 so there has to be a place where this information is stored. This is when the .skin-files come to the light. They got added in WotLK and are in the same folder as the M2s. They are named like Modelname0x.skin, where Modelname is the same name as the model has and x is a digit from 0 to 3 representing each View / LOD. They are in the same structure as the ofsViews-block has been, just with all offsets now being relative to the .skin-files of course. The vertices are still in the M2 itself since they are the same for all views.

The files are made up in several blocks. First is a header:

Header

struct M2SkinProfile
{
#if ≥ Wrath
  uint32_t                magic;                         // 'SKIN'
#endif
  M2Array<unsigned short> vertices;
  M2Array<unsigned short> indices;
  M2Array<ubyte4>         bones;
  M2Array<M2SkinSection>  submeshes;
  M2Array<M2Batch>        batches;
  uint32_t                boneCountMax;                  // WoW takes this and divides it by the number of bones in each submesh, then stores the biggest one.
                                                         // Maximum number of bones per drawcall for each view. Related to (old) GPU numbers of registers. 
                                                         // Values seen : 256, 64, 53, 21
#if ≥ Cata
  M2Array<M2ShadowBatch>  shadow_batches;
#endif
} 
header;

Vertices

This is a lookup table to select a subset of vertices from the global vertex list used by this skin.

Offset (hex) Type Name Description
00 uint16 Vertex an index into the global vertex list

Indices

This is a lookup table to select a subset of vertices from the local vertex list used by this skin. This array can be used as an index buffer for draw calls.

Offset (hex) Type Name Description
00 uint16 Index an index into the local skin vertex list

Indices form a list of triangles, with a set of 3 indices forming a single one. The overall number of indices therefore shall be divisible by 3. Triangles are right-handed; for left-handed draw calls, the 2nd and 3rd index in every 3-index-triangle-set need to be swapped.

Bones

This is a lookup table to select a subset of bones from the global bone list used by this skin.

Offset (hex) Type Name Description
00 uint8 [4] Bones 4 indices into the global bone list (see remarks below)

Blizzard uses a standard 4-bone rig for animations. Each entry therefore represents 4 bone indices.

Remarks:

It seems to be an index into actual bones struct, not the lookup table -- Skarn
An index into the bone list would make more sense, than into the bone lookup table (boneCombos). Vertex bone weights point to that list too. -- Nieriel
Seems like an index here points to bone_lookup_table with offset = Submesh.boneComboIndex. I.e. bone_lookup_table[submesh.boneComboIndex + Bones[i][j]] == M2Vertices [ SkinVertices[i] ].bone_indices[j] -- Vovangrat

Submeshes

  • nSubmeshes entries of 0x30 bytes defining submeshes.
struct M2SkinSection
{ 
  uint16_t skinSectionId;       // Mesh part ID, see below.
  uint16_t Level;               // (level << 16) is added (|ed) to startTriangle and alike to avoid having to increase those fields to uint32s.
  uint16_t vertexStart;         // Starting vertex number.
  uint16_t vertexCount;         // Number of vertices.
  uint16_t indexStart;          // Starting triangle index (that's 3* the number of triangles drawn so far).
  uint16_t indexCount;          // Number of triangle indices.
  uint16_t boneCount;           // Number of elements in the bone lookup table. Max seems to be 256 in Wrath. Shall be ≠ 0.
  uint16_t boneComboIndex;      // Starting index in the bone lookup table.
  uint16_t boneInfluences;      // <= 4
                                // from <=BC documentation: Highest number of bones needed at one time in this Submesh --Tinyn (wowdev.org) 
                                // In 2.x this is the amount of of bones up the parent-chain affecting the submesh --NaK
                                // Highest number of bones referenced by a vertex of this submesh. 3.3.5a and suspectedly all other client revisions. -- Skarn
  uint16_t centerBoneIndex;  
  C3Vector centerPosition;     // Average position of all the vertices in the sub mesh.
#if ≥ BC
  C3Vector sortCenterPosition; // The center of the box when an axis aligned box is built around the vertices in the submesh.
  float sortRadius;             // Distance of the vertex farthest from CenterBoundingBox.
#endif
} submeshes[];

Reference to the bone lookup table: the base number seems to increase per LOD, and the numbers in the bone lookup table, in turn, point to bone-indices at ofsBones.

In 2.x it seems that StartBones & boneInfluences seem to be the partial bone chain affecting the submesh, boneInfluences being the bone furthest down in hierarchy + (n-1) parent bones up. "n" being the amount given at StartBones. (weirdly i dont know what this means for the submesh_0, seeing how hands and feet/toes are part of it, yet their bones would be part of different subchains, and thus dont receive referencing. Where is the point?) --NaK

Mesh part ID

Submeshes are sorted into groups. As Blizzard uses multiple integers (3 ?) for masking them, there are 8*i groups possible.

For groups specific to character models, see the table on the Character Customization page. Groups for other models can be different from the ones listed there.

You can use this together with CreatureDisplayInfo.dbc.creatureGeosetData for nice effects. Also used in ItemDisplayInfo.dbc.m_geosetGroup[] (see that page for an explanation of how the geoset group fields relate to this).

Creature Geoset Data Example

Following code used by Blizzard for non character based creature models:

void ApplyMonsterGeosets(CM2Model *pModel, CreatureDisplayInfoRec *pDisplayInfo, CharacterComponent *pCharacterComponent)
{
    if (pModel && pDisplayInfo)
    {
        CreatureModelData *modelDataRec = ClientDB::CreatureModelDataDB::GetRow(pCharacterComponent->ModelId); // why not take ModelId from pDisplayInfo?
        
        if (!modelDataRec)
            __debugbreak();

        if (modelDataRec->CreatureGeosetDataID)
        {
            CM2Model::SetGeometryVisible(pModel, 1, 899, false); // 899 seems wrong because there's now geosets >= 9xx in the data

            int displayInfoID = pDisplayInfo->ID;

            auto geosetDatas = ClientDB::CreatureDisplayInfoGeosetDataDB::GetRows([](auto cdigd) { return cdigd->CreatureDisplayInfoID == displayInfoID; });

            for (auto geosetData : geosetDatas)
            {
                int meshId1 = 100 * (geosetData->GeosetIndex + 1);
                CM2Model::SetGeometryVisible(pModel, meshId1, meshId1 + 99, false);
                int meshId2 = meshId1 + geosetData->GeosetValue;
                CM2Model::SetGeometryVisible(pModel, meshId2, meshId2, true);
            }
        }
    }
}

Mesh override

In wotlk client, the vertex data in M2 file is overridden if CM2Shared->field_4->field4 has 0x8 flag set. The override process includes usage of boneInfluences field.

function CM2Shared.sub837A40() 
{
  /* Some code is skipped */
  
  if ( !((_BYTE)field[4]->field_4 & 8) )
  {
    M2Vertex* override_vertices = SMemNew(sizeof (M2Vertex) * skinFile->indices.count);

    // 2. zero-initialize (but will be overridden with real vertices in 3.)
    // 3. Copy data from initial vertex of m2 and override boneIndexes
    
    for (int meshIndex = 0; meshIndex < skinFile->submeshes.count; ++meshIndex)
    {
      M2SkinSection* subMesh = skinFile->submeshes.data[meshIndex];

      for (int vertIndex = subMesh->StartVertex; vertIndex < (subMesh->StartVertex + subMesh->vertices.count); ++vertIndex)
      {
        override_vertices[vertIndex] = m_data->vertices.data[skinFile->indices.data[vertIndex]];

        for (int boneInd = 0; boneInd < subMesh->boneInfluences; ++boneInd)
        {
          override_vertices[vertIndex].bone_indices[boneInd] =
            m_data->bone_lookup_table.data[subMesh->StartBones + skinFile->properties.data[4*vertIndex + boneInd];
        }
      }
    }
  
    // 4. Override bone lookup table and in m2 file
    for (int i = 0; i < m_data->nBoneLookupTable; ++i)
      m_data->bone_lookup_table.data[i] = i;
 
    // 5. Override indicies in skin file
    for (int j = 0; j < skinFile->indices.count; ++j)
      skinFile->indices.data[j] = j;
 
    // 6. Override vertex array from m2 with new data
    if ( skinFile->indices.count <= m_data->vertices.count )
    {
      memcpy(m_data->vertices.data, override_vertices, sizeof (M2Vertex) * skinFile->indices.count);
      SMemFree (override_vertices);
    }
    else
    {
      field_8 |= 8u;
      m_data->vertices.data = override_vertices;
    }
  
    m_data->vertices.count = skinFile->indices.count;
  }
  
  // 7. Override batch flags
  if ( !((_BYTE)field[4]->field_4 & 8) )
  {
    for ( int i = 0; i < skinFile->batches.count; i++)
    {
      if ( skinFile->batches.data[i].op_count > 1u )
        skinFile->batches.data[i - skinFile->batches.data[i].layer].flags |= 0x40u;
    }
  
    for ( int i = 0; i < skinFile->batches.count; i++)
    {
      if ( skinFile->batches.data[i].layer )
      {
        if ( skinFile->batches.data[i - skinFile->batches.data[i].layer].flags & 0x40 )
          skinFile->batches.data[i].flags |= 0x40u;
      }
    }
  }
}

Texture units

  • nTextureUnits blocks of 0x18 bytes per record. (Actually named batches)

More specifically, textures for each texture unit. Based on the current submesh number, one or two of these are used to determine the texture(s) to bind.

struct M2Batch 
{
  uint8_t flags;                       // Usually 16 for static textures, and 0 for animated textures. &0x1: materials invert something; &0x2: transform &0x4: projected texture; &0x10: something batch compatible; &0x20: projected texture?; &0x40: possibly don't multiply transparency by texture weight transparency to get final transparency value(?)
  int8_t priorityPlane;
  uint16_t shader_id;                  // See below.
  uint16_t skinSectionIndex;           // A duplicate entry of a submesh from the list above.
  uint16_t geosetIndex;                // See below. New name: flags2. 0x2 - projected. 0x8 - EDGF chunk in m2 is mandatory and data from is applied to this mesh
  uint16_t colorIndex;                 // A Color out of the Colors-Block or -1 if none.
  uint16_t materialIndex;              // The renderflags used on this texture-unit.
  uint16_t materialLayer;              // Capped at 7 (see CM2Scene::BeginDraw)
  uint16_t textureCount;               // 1 to 4. See below. Also seems to be the number of textures to load, starting at the texture lookup in the next field (0x10).
  uint16_t textureComboIndex;          // Index into Texture lookup table
  uint16_t textureCoordComboIndex;     // Index into the texture unit lookup table.
  uint16_t textureWeightComboIndex;    // Index into transparency lookup table.
  uint16_t textureTransformComboIndex; // Index into uvanimation lookup table. 
};

geosetIndex

-- Rour, what is this? It really doesn't look like a submesh index, I've seen it be !=0 and !=SubmeshIndex, the WoD login screen looks to have some submeshes might have different layouts than normal texunits. The ribbon effects (they're not ribbon emitters) appear to use texture indices that don't match the usual (texture_id + i) pattern. Infact, geosetIndex has what looks like a valid texture index in it. Perhaps a new flag?

-- FWIW, this offset is never touched by the client. It is possibly some sort of legacy field that is unused nowadays. Simca (talk) 02:20, 10 April 2016 (CEST)

Since BfA this field was renamed into flags2

shader_id and textureCount

Note that this is based on 5.0.1.15464. It may have more values in later versions and less (especially not hull and domain shaders) in lower versions. To get a list of values for your client, look at CM2Shared::GetEffect().

Based on these two fields, the shaders to load are determined. If shader_id is negative, the (absolute) value of it is used directly to look into s_modelShaderEffect and select from there. If it is positive, selection of the shaders to use will be based on M2Get*ShaderID() functions. Vertex and pixel shaders names are used directly, hull and domain shaders are either prefixed with "Model2_" (tessellation) or "Model2Displ_" (displacement). If neither is enabled, they are not used.

T1 and T2 seem to point to the Texture Coordinates in the vertex (first or second set), and are listed in order of which texture they apply to.

Env shaders map texture coords for that texture to a spheremap. This is most often used to give armour and weapon pieces their "shine" by spheremapping the shine texture onto the item.

IMPORTANT: The texture and UV animation (and maybe render flags/transparency?) indices listed in the texture units are only the 'base' index. If the textureCount is e.g. 3 and the texunit's uv anim lookup is 2, then the 3 uv animation lookups are 2, 3, and 4. ---Relaxok, 12-08-2014

shader_id in WotLK

Note: this entire section only applies to selecting appropriate shaders for WotLK. It may also apply to earlier WoW versions, but it definitely stops applying from Cata and on.

Unlike shader_id in Cata and later version of WoW, the shader_id field in WotLK M2s is generally 0 in the on-disk skin file. However, this isn't simply because WotLK and earlier don't use a large suite of shaders when rendering. Rather, in WotLK (and potentially TBC and Vanilla), the real value of shader_id is determined at runtime.

In WotLK, the runtime value of shader_id is computed via two functions which take in to account the blending mode, render flags, op count, texture mapping (T1, T2, Env), and transparency animations. If disassembling with IDA, in Wow.exe Build 12340, the relevant functions are: sub_836980 and sub_837680. They are called in that order from sub_837A40 (the function that loads a skin profile).

Occasionally, the runtime value for shader_id is computed as 0x8000. In WotLK, 0x8000 translates to no shader. In general, M2Batches with a runtime shader_id of 0x8000 appear to be safe to not render. As an example: The model NorthrendPenguin.m2, in skin 01, has 6 total batches: 3 batches that get assigned shader_id 0x8000 at runtime, and 3 batches that get Combiners_Opaque_Mod2xNA_Alpha. In Cata+, the same model only has 3 batches, all of which are given the shader Combiners_Opaque_Mod2xNA_Alpha.

WotLK runtime shader selection in JavaScript

Login screens

The main issue with login screens is that many M2Batch's there point to render flag that tells to turn off write into depth buffer. This combined with the fact that M2Batch'es are not ordered for a proper rendering makes it broken in most renderers at the moment. So basically the z-depth test is off for these screens and it's up to developer to sort the materials.

Sorting using M2SkinSection.CenterBoundingBox or M2SkinSection.CenterMass doesnt give desired effect for WotLK login screen. My best guess so far is to calculate bounding boxes upon loading for each M2SkinSection. Then transform each bounding box with ModelViewMatrix to get transformedAABB array(How to transform AABB with Mat4). And next sort materials based on z coordinate, which is distance from mesh to screen in view space. It should also be taken into account if the camera is inside Bounding Box or not. The criteria function looks like this:

 var zeroVect = [0, 0, 0]
 function test1 (a, b) {
   var aabb1_t = transformedAABB[a.submesh_index];
   var aabb2_t = transformedAABB[b.submesh_index];
   var isInsideAABB1 = isPointInsideAABB(aabb1_t,zeroVect);
   var isInsideAABB2 = isPointInsideAABB(aabb2_t,zeroVect);
   if (!isInsideAABB1 && isInsideAABB2) {
       return 1
   } else if (isInsideAABB1 && !isInsideAABB2) {
       return -1
   }
   var result;
   if (isInsideAABB1 && isInsideAABB1) {
       result = aabb1_t.min.z - aabb2_t.min.z;
   } else if (!(isInsideAABB1 && isInsideAABB1)) {
       result = aabb2_t.min.z - aabb1_t.min.z;
   }
   return result;
 }

Deamon (talk)

I would add that most of the problematic submeshes are flat. So if we take the shortest edge, back to front, of an AABB and create a normal from it. Then we can combine it with the center of the AABB to make a plane. Next, we use the plane test to determine which plane or other object is closest and sort. The rest of the nonflat objects can be sorted by their centers. The remaining case is round objects that surround other objects. We can cheat and sort them by their farthest point, using the stored radius and center to approximate it, then use front face culling to render only the back half. I'm not sure this case is required though. At least for the sky sphere we can just cull all triangles that are nearer than the nearplane of the view frustum. Lumirion (talk)

Environment mapping

This is the actual formula blizz use for env mapping (vertex and normal are in camera space):

 vec2 sphereMap(vec3 vertex, vec3 normal)
 {
     vec3 normPos = -(normalize(vertex.xyz));
     vec3 temp = (normPos - (normal * (2.0 * dot(normPos, normal))));
     temp = vec3(temp.x, temp.y, temp.z + 1.0);
 
     texCoord = ((normalize(temp).xy * 0.5) + vec2(0.5));
 }


Vertex shaders

enum modelVertexShaders
{
  VS_Diffuse_T1,
  VS_Diffuse_Env,
  VS_Diffuse_T1_T2,
  VS_Diffuse_T1_Env,
  VS_Diffuse_Env_T1,
  VS_Diffuse_Env_Env,
  VS_Diffuse_T1_Env_T1,
  VS_Diffuse_T1_T1,
  VS_Diffuse_T1_T1_T1,
  VS_Diffuse_EdgeFade_T1,
  VS_Diffuse_T2,
  VS_Diffuse_T1_Env_T2,
  VS_Diffuse_EdgeFade_T1_T2,
  VS_Diffuse_T1_T1_T1_T2,
  VS_Diffuse_EdgeFade_Env,
  VS_Diffuse_T1_T2_T1,
};
const char* s_modelVertexShaders[16] =
{
  "Diffuse_T1",
  "Diffuse_Env",
  "Diffuse_T1_T2",
  "Diffuse_T1_Env",
  "Diffuse_Env_T1",
  "Diffuse_Env_Env",
  "Diffuse_T1_Env_T1",
  "Diffuse_T1_T1",
  "Diffuse_T1_T1_T1",
  "Diffuse_EdgeFade_T1",
  "Diffuse_T2",
  "Diffuse_T1_Env_T2",
  "Diffuse_EdgeFade_T1_T2",
  "Diffuse_T1_T1_T1_T2",
  "Diffuse_EdgeFade_Env",
  "Diffuse_T1_T2_T1",
};

Vertex shaders (8.0.1)

enum modelVertexShaders
{
  VS_Diffuse_T1,
  VS_Diffuse_Env,
  VS_Diffuse_T1_T2,
  VS_Diffuse_T1_Env,
  VS_Diffuse_Env_T1,
  VS_Diffuse_Env_Env,
  VS_Diffuse_T1_Env_T1,
  VS_Diffuse_T1_T1,
  VS_Diffuse_T1_T1_T1,
  VS_Diffuse_EdgeFade_T1,
  VS_Diffuse_T2,
  VS_Diffuse_T1_Env_T2,
  VS_Diffuse_EdgeFade_T1_T2,
  VS_Diffuse_EdgeFade_Env,
  VS_Diffuse_T1_T2_T1,
  VS_Diffuse_T1_T2_T3,
  VS_Color_T1_T2_T3,
  VS_BW_Diffuse_T1,
  VS_BW_Diffuse_T1_T2,
};

Pixel shaders

enum modelPixelShaders
{
  PS_Combiners_Opaque,
  PS_Combiners_Mod,
  PS_Combiners_Opaque_Mod,
  PS_Combiners_Opaque_Mod2x,
  PS_Combiners_Opaque_Mod2xNA,
  PS_Combiners_Opaque_Opaque,
  PS_Combiners_Mod_Mod,
  PS_Combiners_Mod_Mod2x,
  PS_Combiners_Mod_Add,
  PS_Combiners_Mod_Mod2xNA,
  PS_Combiners_Mod_AddNA,
  PS_Combiners_Mod_Opaque,
  PS_Combiners_Opaque_Mod2xNA_Alpha,
  PS_Combiners_Opaque_AddAlpha,
  PS_Combiners_Opaque_AddAlpha_Alpha,
  PS_Combiners_Opaque_Mod2xNA_Alpha_Add,
  PS_Combiners_Mod_AddAlpha,
  PS_Combiners_Mod_AddAlpha_Alpha,
  PS_Combiners_Opaque_Alpha_Alpha,
  PS_Combiners_Opaque_Mod2xNA_Alpha_3s,
  PS_Combiners_Opaque_AddAlpha_Wgt,
  PS_Combiners_Mod_Add_Alpha,
  PS_Combiners_Opaque_ModNA_Alpha,
  PS_Combiners_Mod_AddAlpha_Wgt,
  PS_Combiners_Opaque_Mod_Add_Wgt,
  PS_Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha,
  PS_Combiners_Mod_Dual_Crossfade,
  PS_Combiners_Opaque_Mod2xNA_Alpha_Alpha,
  PS_Combiners_Mod_Masked_Dual_Crossfade,
  PS_Combiners_Opaque_Alpha,
  PS_Guild,
  PS_Guild_NoBorder,
  PS_Guild_Opaque,
  PS_Combiners_Mod_Depth,
  PS_Illum,
  PS_Combiners_Mod_Mod_Mod_Const,
};
const char* s_modelPixelShaders[36] =
{
  "Combiners_Opaque",
  "Combiners_Mod",
  "Combiners_Opaque_Mod",
  "Combiners_Opaque_Mod2x",
  "Combiners_Opaque_Mod2xNA",
  "Combiners_Opaque_Opaque",
  "Combiners_Mod_Mod",
  "Combiners_Mod_Mod2x",
  "Combiners_Mod_Add",
  "Combiners_Mod_Mod2xNA",
  "Combiners_Mod_AddNA",
  "Combiners_Mod_Opaque",
  "Combiners_Opaque_Mod2xNA_Alpha",
  "Combiners_Opaque_AddAlpha",
  "Combiners_Opaque_AddAlpha_Alpha",
  "Combiners_Opaque_Mod2xNA_Alpha_Add",
  "Combiners_Mod_AddAlpha",
  "Combiners_Mod_AddAlpha_Alpha",
  "Combiners_Opaque_Alpha_Alpha",
  "Combiners_Opaque_Mod2xNA_Alpha_3s",
  "Combiners_Opaque_AddAlpha_Wgt",
  "Combiners_Mod_Add_Alpha",
  "Combiners_Opaque_ModNA_Alpha",
  "Combiners_Mod_AddAlpha_Wgt",
  "Combiners_Opaque_Mod_Add_Wgt",
  "Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha",
  "Combiners_Mod_Dual_Crossfade",
  "Combiners_Opaque_Mod2xNA_Alpha_Alpha",
  "Combiners_Mod_Masked_Dual_Crossfade",
  "Combiners_Opaque_Alpha",
  "Guild",
  "Guild_NoBorder",
  "Guild_Opaque",
  "Combiners_Mod_Depth",
  "Illum",
  "Combiners_Mod_Mod_Mod_Const",
};

Hull shaders

enum modelHullShaders
{
  HS_T1,
  HS_T1_T2,
  HS_T1_T2_T3,
  HS_T1_T2_T3_T4,
};
const char* s_modelHullShaders[16] =
{
  "T1",
  "T1_T2",
  "T1_T2_T3",
  "T1_T2_T3_T4",
};

Domain shaders

enum modelDomainShaders
{
  DS_T1,
  DS_T1_T2,
  DS_T1_T2_T3,
  DS_T1_T2_T3_T4,
};
const char* s_modelDomainShaders[16] =
{
  "T1",
  "T1_T2",
  "T1_T2_T3",
  "T1_T2_T3_T4",
};


Shader table

struct
{
  unsigned int pixel;
  unsigned int vertex;
  unsigned int hull;
  unsigned int domain;
  unsigned int ff_colorOp;
  unsigned int ff_alphaOp;
} s_modelShaderEffect[NUM_M2SHADERS] = 
{ {PS_Combiners_Opaque_Mod2xNA_Alpha,           VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_AddAlpha,                VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_AddAlpha_Alpha,          VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_Mod2xNA_Alpha_Add,       VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3,    DS_T1_T2_T3,    0, 3},
  {PS_Combiners_Mod_AddAlpha,                   VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 0},
  {PS_Combiners_Opaque_AddAlpha,                VS_Diffuse_T1_T1,          HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Mod_AddAlpha,                   VS_Diffuse_T1_T1,          HS_T1_T2,       DS_T1_T2,       0, 0},
  {PS_Combiners_Mod_AddAlpha_Alpha,             VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 0},
  {PS_Combiners_Opaque_Alpha_Alpha,             VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_Mod2xNA_Alpha_3s,        VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3,    DS_T1_T2_T3,    0, 3},
  {PS_Combiners_Opaque_AddAlpha_Wgt,            VS_Diffuse_T1_T1,          HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Mod_Add_Alpha,                  VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 0},
  {PS_Combiners_Opaque_ModNA_Alpha,             VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Mod_AddAlpha_Wgt,               VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Mod_AddAlpha_Wgt,               VS_Diffuse_T1_T1,          HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_AddAlpha_Wgt,            VS_Diffuse_T1_T2,          HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_Mod_Add_Wgt,             VS_Diffuse_T1_Env,         HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha, VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3,    DS_T1_T2_T3,    0, 3},
  {PS_Combiners_Mod_Dual_Crossfade,             VS_Diffuse_T1_T1_T1,       HS_T1_T2_T3,    DS_T1_T2_T3,    0, 0},
  {PS_Combiners_Mod_Depth,                      VS_Diffuse_EdgeFade_T1,    HS_T1,          DS_T1,          0, 0},
  {PS_Combiners_Mod_AddAlpha_Alpha,             VS_Diffuse_T1_Env_T2,      HS_T1_T2_T3,    DS_T1_T2_T3,    0, 3},
  {PS_Combiners_Mod_Mod,                        VS_Diffuse_EdgeFade_T1_T2, HS_T1_T2,       DS_T1_T2,       0, 0},
  {PS_Combiners_Mod_Masked_Dual_Crossfade,      VS_Diffuse_T1_T1_T1_T2,    HS_T1_T2_T3_T4, DS_T1_T2_T3_T4, 0, 0},
  {PS_Combiners_Opaque_Alpha,                   VS_Diffuse_T1_T1,          HS_T1_T2,       DS_T1_T2,       0, 3},
  {PS_Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha, VS_Diffuse_T1_Env_T2,      HS_T1_T2_T3,    DS_T1_T2_T3,    0, 3},
  {PS_Combiners_Mod_Depth,                      VS_Diffuse_EdgeFade_Env,   HS_T1,          DS_T1,          0, 0},
  {PS_Guild,                                    VS_Diffuse_T1_T2_T1,       HS_T1_T2_T3,    DS_T1_T2,       0, 0},
  {PS_Guild_NoBorder,                           VS_Diffuse_T1_T2,          HS_T1_T2,       DS_T1_T2_T3,    0, 0},
  {PS_Guild_Opaque,                             VS_Diffuse_T1_T2_T1,       HS_T1_T2_T3,    DS_T1_T2,       0, 0},
  {PS_Illum,                                    VS_Diffuse_T1_T1,          HS_T1_T2,       DS_T1_T2,       0, 0},
};

Shader table (8.0.1)

struct
{
  unsigned int pixel;
  unsigned int vertex;
  unsigned int hull;
  unsigned int domain;
} s_modelShaderEffect[NUM_M2SHADERS] = 
{ { PS_Combiners_Opaque_Mod2xNA_Alpha,           VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_AddAlpha,                VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_AddAlpha_Alpha,          VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Mod2xNA_Alpha_Add,       VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Mod_AddAlpha,                   VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_AddAlpha,                VS_Diffuse_T1_T1,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_AddAlpha,                   VS_Diffuse_T1_T1,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_AddAlpha_Alpha,             VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Alpha_Alpha,             VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Mod2xNA_Alpha_3s,        VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Opaque_AddAlpha_Wgt,            VS_Diffuse_T1_T1,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_Add_Alpha,                  VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_ModNA_Alpha,             VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_AddAlpha_Wgt,               VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_AddAlpha_Wgt,               VS_Diffuse_T1_T1,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_AddAlpha_Wgt,            VS_Diffuse_T1_T2,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Mod_Add_Wgt,             VS_Diffuse_T1_Env,         HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha, VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Mod_Dual_Crossfade,             VS_Diffuse_T1,             HS_T1,       DS_T1        },
  { PS_Combiners_Mod_Depth,                      VS_Diffuse_EdgeFade_T1,    HS_T1,       DS_T1        },
  { PS_Combiners_Opaque_Mod2xNA_Alpha_Alpha,     VS_Diffuse_T1_Env_T2,      HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Mod_Mod,                        VS_Diffuse_EdgeFade_T1_T2, HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_Masked_Dual_Crossfade,      VS_Diffuse_T1_T2,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Alpha,                   VS_Diffuse_T1_T1,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha, VS_Diffuse_T1_Env_T2,      HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Mod_Depth,                      VS_Diffuse_EdgeFade_Env,   HS_T1,       DS_T1        },
  { PS_Guild,                                    VS_Diffuse_T1_T2_T1,       HS_T1_T2_T3, DS_T1_T2     },
  { PS_Guild_NoBorder,                           VS_Diffuse_T1_T2,          HS_T1_T2,    DS_T1_T2_T3  },
  { PS_Guild_Opaque,                             VS_Diffuse_T1_T2_T1,       HS_T1_T2_T3, DS_T1_T2     },
  { PS_Illum,                                    VS_Diffuse_T1_T1,          HS_T1_T2,    DS_T1_T2     },
  { PS_Combiners_Mod_Mod_Mod_Const,              VS_Diffuse_T1_T2_T3,       HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Mod_Mod_Mod_Const,              VS_Color_T1_T2_T3,         HS_T1_T2_T3, DS_T1_T2_T3  },
  { PS_Combiners_Opaque,                         VS_Diffuse_T1,             HS_T1,       DS_T1        },
  { PS_Combiners_Mod_Mod2x,                      VS_Diffuse_EdgeFade_T1_T2, HS_T1_T2,    DS_T1_T2     },
};
unsigned int M2GetPixelShaderID (unsigned int op_count, unsigned short shader_id)
{
  if (shader_id & 0x8000)
  {
    unsigned short const shaderID (shader_id & (~0x8000));
    assert (shaderID < NUM_M2SHADERS);
    return s_modelShaderEffect (shaderID).pixel;
  }
  else
  {
    if (op_count == 1)
    {
      return shader_id & 0x70 ? PS_Combiners_Mod : PS_Combiners_Opaque;
    }
    else
    {
      const unsigned int lower (shader_id & 7);
      if (shader_id & 0x70)
      {
        return lower == 0 ? PS_Combiners_Mod_Opaque
             : lower == 3 ? PS_Combiners_Mod_Add
             : lower == 4 ? PS_Combiners_Mod_Mod2x
             : lower == 6 ? PS_Combiners_Mod_Mod2xNA
             : lower == 7 ? PS_Combiners_Mod_AddNA
                          : PS_Combiners_Mod_Mod;
      }
      else
      {
        return lower == 0 ? PS_Combiners_Opaque_Opaque
             : lower == 3 ? PS_Combiners_Opaque_AddAlpha
             : lower == 4 ? PS_Combiners_Opaque_Mod2x
             : lower == 6 ? PS_Combiners_Opaque_Mod2xNA
             : lower == 7 ? PS_Combiners_Opaque_AddAlpha
                          : PS_Combiners_Opaque_Mod;
      }
    }
  }
}
unsigned int M2GetVertexShaderID (unsigned int op_count, unsigned short shader_id)
{
  if (shader_id & 0x8000)
  {
    unsigned short const shaderID (shader_id & (~0x8000));
    assert (shaderID < NUM_M2SHADERS);
    return s_modelShaderEffect (shaderID).vertex;
  }
  else
  {
    if (op_count == 1)
    {
      return shader_id & 0x80   ? VS_Diffuse_Env
           : shader_id & 0x4000 ? VS_Diffuse_T2
                                : VS_Diffuse_T1;
    }
    else
    {
      if (shader_id & 0x80)
      {
        return shader_id & 0x8 ? VS_Diffuse_Env_Env
                               : VS_Diffuse_Env_T1;
      }
      else
      {
        return shader_id & 0x8    ? VS_Diffuse_T1_Env
             : shader_id & 0x4000 ? VS_Diffuse_T1_T2
                                  : VS_Diffuse_T1_T1;
      }
    }
  }
}
unsigned int M2GetHullShaderID (unsigned int op_count, unsigned short shader_id)
{
  if (shader_id & 0x8000)
  {
    unsigned short const shaderID (shader_id & (~0x8000));
    assert (shaderID < NUM_M2SHADERS);
    return s_modelShaderEffect (shaderID).hull;
  }
  else
  {
    return op_count == 1 ? HS_T1 : HS_T1_T2;
  }
}
unsigned int M2GetDomainShaderID (unsigned int op_count, unsigned short shader_id)
{
  if (shader_id & 0x8000)
  {
    unsigned short const shaderID (shader_id & (~0x8000));
    assert (shaderID < NUM_M2SHADERS);
    return s_modelShaderEffect (shaderID).domain;
  }
  else
  {
    return op_count == 1 ? DS_T1 : DS_T1_T2;
  }
}
void M2GetFixedFunctionFallback (unsigned short shader_id, EGxTexOp* colorOp, EGxTexOp* alphaOp)
{
  if (shader_id & 0x8000)
  {
    unsigned short const shaderID (shader_id & (~0x8000));
    assert (shaderID < NUM_M2SHADERS);
    *colorOp = s_modelShaderEffect (shaderID).ff_colorOp;
    *alphaOp = s_modelShaderEffect (shaderID).ff_alphaOp;
  }
  else
  {
    *colorOp = 0;
    *alphaOp = shader_id & 0x70 ? 0 : 3;
  }
}
void M2GetCombinerOps (unsigned short shader_id, unsigned int op_count, EGxTexOp* colorOp, EGxTexOp* alphaOp)
{
  int helper[2] = {(shader_id >> 4) & 7, shader_id & 7};
  for (int i = 0; i < op_count; ++i)
  {
    //! \todo Add enum.
    static const unsigned int alphaOpTable[] = {3, 0, 3, 2, 1, 3, 3, 3};
    static const unsigned int colorOpTable[] = {0, 0, 4, 2, 1, 5, 1, 2};
    *colorOp[i] = colorOpTable[helper[i]];
    *alphaOp[i] = alphaOpTable[helper[i]];
  }
}
const char* M2GetPixelShaderName (unsigned int op_count, unsigned short shader_id)
{
  unsigned int pixelShaderID (M2GetPixelShaderID (op_count, shader_id));
  array_size_check (pixelShaderID, s_modelPixelShaders);
  return s_modelPixelShaders[pixelShaderID];
}
const char* M2GetVertexShaderName (unsigned int op_count, unsigned short shader_id)
{
  unsigned int vertexShaderID (M2GetVertexShaderID (op_count, shader_id));
  array_size_check (vertexShaderID, s_modelVertexShaders);
  return s_modelVertexShaders[vertexShaderID];
}
const char* M2GetHullShaderName (unsigned int op_count, unsigned short shader_id)
{
  unsigned int hullShaderID (M2GetHullShaderID (op_count, shader_id));
  array_size_check (hullShaderID, s_modelHullShaders);
  return s_modelHullShaders[hullShaderID];
}
const char* M2GetDomainShaderName (unsigned int op_count, unsigned short shader_id)
{
  unsigned int domainShaderID (M2GetDomainShaderID (op_count, shader_id));
  array_size_check (domainShaderID, s_modelDomainShaders);
  return s_modelDomainShaders[domainShaderID];
}
CShaderEffect* CM2Shared::GetEffect (M2Batch *batch)
{
  assert (batch);
 
  // get names for shaders
 
  const char* vertex_shader_name (M2GetVertexShaderName (batch->op_count, batch->shader_id));
  const char* pixel_shader_name (M2GetPixelShaderName (batch->op_count, batch->shader_id));
 
  char hull_shader_name_prefixed[0x100];
  hull_shader_name_prefixed[0] = 0;
  char domain_shader_name_prefixed[0x100];
  domain_shader_name_prefixed[0] = 0;
 
  if (CShaderEffect::TesselationEnabled())
  {
    SStrPrintf (hull_shader_name_prefixed, 0x100u, "Model2_%s", M2GetHullShaderName(batch->op_count, batch->shader_id));
    SStrPrintf (domain_shader_name_prefixed, 0x100u, "Model2_%s", M2GetDomainShaderName(batch->op_count, batch->shader_id));
  }
  else if (CShaderEffect::DisplacementEnabled())
  {
    SStrPrintf (hull_shader_name_prefixed, 0x100u, "Model2Displ_%s", M2GetHullShaderName(batch->op_count, batch->shader_id));
    SStrPrintf (domain_shader_name_prefixed, 0x100u, "Model2Displ_%s", M2GetDomainShaderName(batch->op_count, batch->shader_id));
  }
 
  // assemble effect name and look in cache
 
  char effect_name[0x100];
  if (batch->shader_id & 0x8000)
  {
    SStrPrintf (effect_name, 0x100u, "M2Effect %d", batch->shader_id & (~0x8000));
  }
  else
  {
    strcpy (effect_name, vertex_shader_name);
    strcat (effect_name, pixel_shader_name);
  }

  CShaderEffect* effect (CShaderEffectManager::GetEffect (effect_name));
  if (effect)
  {
    effect->AddRef();
    return effect;
  }

  // create shader and initialize
 
  effect = CShaderEffectManager::CreateEffect (effect_name);
  effect->InitEffect (vertex_shader_name, hull_shader_name_prefixed, domain_shader_name_prefixed, pixel_shader_name);
 
  if (batch->shader_id < 0)
  {
    EGxTexOp colorOp;
    EGxTexOp alphaOp;
    M2GetFixedFunctionFallback (batch->shader_id, &colorOp, &alphaOp);
    effect->InitFixedFuncPass (&colorOp, &alphaOp, 1);
  }
  else
  {
    EGxTexOp colorOps[2];
    EGxTexOp alphaOps[2];
    M2GetCombinerOps (batch->shader_id, batch->op_count, colorOps, alphaOps);
    effect->InitFixedFuncPass (colorOps, alphaOps, batch->op_count);
  }
 
  assert (effect);
  return effect;
}

shadow batches

Apparently based on M2Batch (texture unit).

struct M2ShadowBatch 
{
  uint8_t flags;              // if auto-generated: M2Batch.flags & 0xFF
  uint8_t flags2;             // if auto-generated: (renderFlag[i].flags & 0x04 ? 0x01 : 0x00)
                              //                  | (!renderFlag[i].blendingmode ? 0x02 : 0x00)
                              //                  | (renderFlag[i].flags & 0x80 ? 0x04 : 0x00)
                              //                  | (renderFlag[i].flags & 0x400 ? 0x06 : 0x00)
  uint16_t _unknown1;
  uint16_t submesh_id;
  uint16_t texture_id;        // already looked-up
  uint16_t color_id;
  uint16_t transparency_id;   // already looked-up
}; 

Generated on the fly, if !(batches[i].flags & 4) && !batches[i].texunit && !(renderflags[batches[i].renderFlag].flags & 0x40) && (renderflags[batches[i].renderFlag].blendingmode < 2u || renderflags[batches[i].renderFlag].flags & 0x80)