From wowdev
Jump to: navigation, search

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:


struct M2SkinProfile
#if ≥ Wrath 
  uint32_t magic;                         // 'SKIN'
  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.
                                          // 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;
} header;


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


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


  • 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.
} 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}
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}
15**: Cloak: {1-10: various cloak styles}
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 { 1: hands for blood elf/night elf }

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);
      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;             // Capped at 7 (see CM2Scene::BeginDraw)
  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. 


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

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)

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
const char* s_modelVertexShaders[16] =

Pixel shaders

enum modelPixelShaders
const char* s_modelPixelShaders[35] =

Hull shaders

enum modelHullShaders
const char* s_modelHullShaders[16] =

Domain shaders

enum modelDomainShaders
const char* s_modelDomainShaders[16] =

Shader table

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

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;
    if (op_count == 1)
      return shader_id & 0x70 ? PS_Combiners_Mod : PS_Combiners_Opaque;
      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;
        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;
    if (op_count == 1)
      return shader_id & 0x80   ? VS_Diffuse_Env
           : shader_id & 0x4000 ? VS_Diffuse_T2
                                : VS_Diffuse_T1;
      if (shader_id & 0x80)
        return shader_id & 0x8 ? VS_Diffuse_Env_Env
                               : VS_Diffuse_Env_T1;
        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;
    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;
    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;
    *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));
    strcpy (effect_name, vertex_shader_name);
    strcat (effect_name, pixel_shader_name);

  CShaderEffect* effect (CShaderEffectManager::GetEffect (effect_name));
  if (effect)
    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);
    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)