M2/.skin: Difference between revisions

From wowdev
Jump to navigation Jump to search
Line 96: Line 96:
===Mesh override===
===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.
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()  
function CM2Shared.sub837A40()  
  {
{
    /* ''Some code is skipped'' */
  /* Some code is skipped */
   
    if ( !((_BYTE)this->field[4]->field_4 & 8) )
    {
      int nIndicies = this->skinFile->nIndices;
   
      /*
      * '''1. Create temp vertex array'''
      */
      M2Vertex * tempVertexes = (M2Vertex *)allocateMemory(
                          48 * this->skinFile->nIndices | -(48 * (unsigned __int64)this->skinFile->nIndices >> 32 != 0));
   
      /*
      * '''2. Init array with initial values'''
      */
   
      M2Vertex * firstM2Vertex;
      M2Vertex * secondM2Vertex;
   
      if ( tempVertexes )
      {
        int j = nIndicies - 1;
        if ( j >= 0 )
        {
          float *v20 = &tempVertexes->normal[2];
          do
          {
            *(v20 - 7) = 0.0;
            *(v20 - 6) = 0.0;
            *(v20 - 5) = 0.0;
            *(v20 - 2) = 0.0;
            *(v20 - 1) = 0.0;
            *v20 = 0.0;
            sub_4011D0((int)(v20 + 1), 8, 2, (int (__thiscall *)(int))sub_4079C0);
            v20 += 12;
            --j;
          }
          while ( j >= 0 );
        }
        firstM2Vertex = tempVertexes;
        secondM2Vertex = tempVertexes;
      }
      else
      {
        secondM2Vertex = 0;
        firstM2Vertex = 0;
      }
   
      /*
      * '''3. Copy data from initial vertex of m2 and override boneIndexes'''
      */
     
      v42 = 0;
      if ( this->skinFile->nSubmeshes )
      {
        i = 0;
        do
        {
          M2SkinSection * subMesh = this->skinFile->ofsSubmeshes[i];
          vertIndex = subMesh->StartVertex;
          finalIndex = vertIndex + subMesh->nVertices;
    
    
          if ( vertIndex < vertIndex + subMesh->nVertices )
  if ( !((_BYTE)field[4]->field_4 & 8) )
          {
  {
            M2Vertex * newVertex = &firstM2Vertex[vertIndex];
    M2Vertex* override_vertices = SMemNew(sizeof (M2Vertex) * skinFile->indices.count);
            while ( 1 )
 
            {
    // 2. zero-initialize (but will be overridden with real vertices in 3.)
              M2Vertex * originalVertex = &this->m_data->ofsVertices[this->skinFile->ofsIndices[vertIndex]];
    // 3. Copy data from initial vertex of m2 and override boneIndexes
              qmemcpy(newVertex, originalVertex, sizeof(M2Vertex));
   
   
    for (int meshIndex = 0; meshIndex < skinFile->submeshes.count; ++meshIndex)
              boneInd = 0;
    {
              if ( subMesh->boneInfluences > 0u )
      M2SkinSection* subMesh = skinFile->submeshes.data[meshIndex];
              {
 
                do
      for (int vertIndex = subMesh->StartVertex; vertIndex < (subMesh->StartVertex + subMesh->vertices.count); ++vertIndex)
                {
      {
                  newVertex->bone_indices[boneInd] =
        override_vertices[vertIndex] = m_data->vertices.data[skinFile->indices.data[vertIndex]];
                    *(_BYTE *)(&this->m_data->ofsBoneLookupTable[subMesh->StartBones + *(_BYTE *)(this->skinFile->ofsProperties[4*vertIndex + boneInd));
 
   
        for (int boneInd = 0; boneInd < subMesh->boneInfluences; ++boneInd)
                  ++boneInd;
        {
                }
          override_vertices[vertIndex].bone_indices[boneInd] =
                while ( boneInd < subMesh->boneInfluences );
            m_data->bone_lookup_table.data[subMesh->StartBones + skinFile->properties.data[4*vertIndex + boneInd];
              }
        }
              ++newVertex;
      }
              if ( ++vertIndex >= finalIndex )
    }
                break;
            }
            firstM2Vertex = secondM2Vertex;
          }
          ++v42;
          ++i
        }
        while ( v42 < this->skinFile->nSubmeshes );
      }
   
      /*
      * '''4. Override bone lookup table and in m2 file'''
      */
      for ( i = 0; i < this->m_data->nBoneLookupTable; ++i )
        *(_WORD *)(this->m_data->ofsBoneLookupTable[i]) = i;
    
    
      /*
    // 4. Override bone lookup table and in m2 file
      * '''5. Override indicies in skin file'''
    for (int i = 0; i < m_data->nBoneLookupTable; ++i)
      */
      m_data->bone_lookup_table.data[i] = i;
      for ( j = 0; j < this->skinFile->nIndices; ++j )
 
        this->skinFile->ofsIndices[j] = j;
    // 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;
      * '''6. Override vertex array from m2 with new data'''
  }
      */
 
      v31 = this->m_data;
  // 7. Override batch flags
      v32 = this->skinFile->nIndices;
  if ( !((_BYTE)field[4]->field_4 & 8) )
      if ( v32 <= v31->nVertices )
  {
      {
    for ( int i = 0; i < skinFile->batches.count; i++)
        memcpy(v31->ofsVertices, firstM2Vertex, 48 * v32);
    {
        if ( firstM2Vertex )
      if ( skinFile->batches.data[i].op_count > 1u )
          delete[] firstM2Vertex;
        skinFile->batches.data[i - skinFile->batches.data[i].layer].flags |= 0x40u;
      }
    }
      else
 
      {
    for ( int i = 0; i < skinFile->batches.count; i++)
        *(_DWORD *)&this->field_8 |= 8u;
    {
        v31->ofsVertices = firstM2Vertex;
      if ( skinFile->batches.data[i].layer )
      }
      {
   
        if ( skinFile->batches.data[i - skinFile->batches.data[i].layer].flags & 0x40 )
      this->m_data->nVertices = this->skinFile->nIndices;
          skinFile->batches.data[i].flags |= 0x40u;
    }
      }
   
    }
    /*
  }
    * '''7. Override batch flags'''
}
    */
    if ( !((_BYTE)this->field[4]->field_4 & 8) )
    {
      M2Batch * m2Batches = this->skinFile->ofsM2Batch;
   
      for ( int i = 0; i < this->skinFile->nM2Batch; i++)
      {
        if ( m2Batches[i].op_count > 1u )
          LOBYTE(m2Batches[i - m2Batches[i].layer].flags) |= 0x40u;
      }
   
      for ( int i = 0; i < this->skinFile->nM2Batch; i++)
      {
        int16_t layer = m2Batches[i].layer;
        if ( layer )
        {
          if ( m2Batches[i - layer].flags & 0x40 )
            LOBYTE(m2Batches[i].flags) |= 0x40u;
        }
      }
    }
  }


==Texture units==
==Texture units==

Revision as of 19:16, 3 May 2016

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> indices;
  M2Array<unsigned short> triangles;
  M2Array<???> properties;
  M2Array<M2SkinSection> submeshes;
  M2Array<M2Batch> texture_units;
  uint32_t bones;                         // WoW takes this and divides it by the number of bones in each submesh, then stores the biggest one.
#if ≥ Cata
  M2Array<M2ShadowBatch> shadow_batches;
#endif
} header;

Indices

  • nIndices 16-bit unsigned shorts, specifing vertices from the global vertex list for later use.
Offset  Type   Name    Description
0x00  uint16  Vertex    The vertex in the global vertex list.

Triangles

  • nTriangles entries of each 3 unsigned shorts. They refer to indices in the list above.
Offset  Type   Name    Description
0x00  uint16  Indices[3]  Three indices which make up a triangle.

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 / 3. I 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.

Vertex properties

  • nProperties entries, they are the Bone Indices for the Vertices
Offset  Type   Name    Description
0x00  4*uint8  Properties  Bone Indices (Index into BoneLookupTable)

Submeshes

  • nSubmeshes entries of 0x30 bytes defining submeshes.
struct M2SkinSection
{
  uint16_t SubmeshID;          // 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 StartVertex;        // Starting vertex number.
  uint16_t nVertices;          // Number of vertices.
  uint16_t StartTriangle;      // Starting triangle index (that's 3* the number of triangles drawn so far).
  uint16_t nTriangles;         // Number of triangle indices.
  uint16_t nBones;             // Number of elements in the bone lookup table.
  uint16_t StartBones;         // 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
  uint16_t RootBone;  
  C3Vector CenterMass;         // Average position of all the vertices in the sub mesh.
#if ≥ BC
  C3Vector CenterBoundingBox;  // The center of the box when an axis aligned box is built around the vertices in the submesh.
  float Radius;                // 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

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.

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

0000: Skin
00**: Hair: {1-19: 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-5: varies} (usually sideburns, but not always)
04**: Glove: {1-3}
05**: Boots: {1-4}
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}

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

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 
{
  uint16_t flags;             // probably two uint8_t? -- 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: transparency something
  uint16_t shader_id;         // See below.
  uint16_t submesh_index;     // A duplicate entry of a submesh from the list above.
  uint16_t submesh_index2;    // See below.
  int16_t color_index;        // A Color out of the Colors-Block or -1 if none.
  uint16_t render_flags;      // The renderflags used on this texture-unit.
  uint16_t layer;   // 
  uint16_t op_count;          // 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 texture;           // Index into Texture lookup table
  uint16_t tex_unit_number2;  // Index into the texture unit lookup table.
  uint16_t transparency;      // Index into transparency lookup table.
  uint16_t texture_anim;      // Index into uvanimation lookup table. 
};

SubmeshIndex2

-- 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, SubmeshIndex2 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)

shader_id and op_count

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

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));
 }
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",
};
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,
};
const char* s_modelPixelShaders[35] =
{
  "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",
};
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",
};
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",
};
#ifdef build_20886
struct
{
  unsigned int pixel;
  unsigned int vertex;
  unsigned int hull;
  unsigned int domain;
} s_modelShaderEffect[NUM_M2SHADERS] = 
{ { PS_Combiners_Mod_Opaque,                     VS_Diffuse_T1_Env,         HS_T1_T2,      DS_T1_T2},
  { 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_T1,      HS_T1_T2_T3,   DS_T1_T2_T3},
  { PS_Combiners_Opaque_Mod2xNA_Alpha_Add,       VS_Diffuse_T1_Env,         HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_Mod2xNA_Alpha,           VS_Diffuse_T1_T1,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_Mod2xNA_Alpha_Add,       VS_Diffuse_T1_T1,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Mod_AddAlpha,                   VS_Diffuse_T1_Env,         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_T1,      HS_T1_T2_T3,   DS_T1_T2_T3},
  { PS_Combiners_Opaque_Mod2xNA_Alpha_3s,        VS_Diffuse_T1_T1,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_AddAlpha_Wgt,            VS_Diffuse_T1_Env,         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_Opaque_ModNA_Alpha,             VS_Diffuse_T1_T1,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_Mod2xNA_Alpha_3s,        VS_Diffuse_T1_T2,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Mod_AddAlpha_Wgt,               VS_Diffuse_T1_Env,         HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_Mod_Add_Wgt,             VS_Diffuse_T1_Env_T1,      HS_T1_T2_T3,   DS_T1_T2_T3},
  { PS_Combiners_Opaque_Mod2xNA_Alpha_UnshAlpha, VS_Diffuse_T1,             HS_T1,         DS_T1},
  { PS_Guild_Opaque,                             VS_Diffuse_EdgeFade_T1,    HS_T1,         DS_T1},
  { PS_Combiners_Mod_Dual_Crossfade,             VS_Diffuse_T1_Env_T2,      HS_T1_T2_T3,   DS_T1_T2_T3},
  { PS_Combiners_Opaque_Opaque,                  VS_Diffuse_EdgeFade_T1_T2, HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_Mod2xNA_Alpha_Alpha,     VS_Diffuse_T1_T2,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Mod_Masked_Dual_Crossfade,      VS_Diffuse_T1_T1,          HS_T1_T2,      DS_T1_T2},
  { PS_Combiners_Opaque_Mod_Add_Wgt,             VS_Diffuse_T1_Env_T2,      HS_T1_T2_T3,   DS_T1_T2_T3},
  { PS_Guild_Opaque,                             VS_Diffuse_EdgeFade_Env,   HS_T1,         DS_T1},
  { PS_Combiners_Opaque_Alpha,                   VS_Diffuse_T1_T2_T1,       HS_T1_T2_T3,   DS_T1_T2},
  { PS_Guild,                                    VS_Diffuse_T1_T2,          HS_T1_T2,      DS_T1_T2_T3},
  { PS_Guild_NoBorder,                           VS_Diffuse_T1_T2_T1,       HS_T1_T2_T3,   DS_T1_T2},
  { PS_Combiners_Mod_Depth,                      VS_Diffuse_T1_T1,          HS_T1_T2,      DS_T1_T2},
  { PS_Illum,                                    VS_Diffuse_T1_T2_T3,       HS_T1_T2_T3,   DS_T1_T2_T3},
  { PS_Illum,                                    VS_Color_T1_T2_T3,         HS_T1_T2_T3,   DS_T1_T2_T3},
};
#elif build_15464
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},
};
#endif
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)