M2/.skin

From wowdev
Jump to navigation Jump to 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:

Header

Offset	Type 	Name		Description
0x00	uint32	ID		SKIN
0x04	uint32	nIndices		
0x08	uint32	ofsIndices	Indices used in this View.
0x0C	uint32	nTriangles		
0x10	uint32	ofsTriangles	The triangles made with them.
0x14	uint32	nProperties		
0x18	uint32	ofsProperties	Properties of the vertices.
0x1C	uint32	nSubmeshes		
0x20	uint32	ofsSubmeshes	Submeshes (Geosets) of this View.
0x24	uint32	nTextureUnits		
0x28	uint32	ofsTextureUnits	Texture Units.
0x2C	uint32	bones		WoW takes this and divides it by the number of bones in each submesh, then stores the biggest one.

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. Actually M2SkinSection.
Offset	Type 	Name			Description
0x00 	uint16	SubmeshID		Mesh part ID, see below.
0x02 	uint16	Level			(level << 16) is added (|ed) to startTriangle and alike to avoid having to increase those fields to uint32s.
0x04 	uint16	StartVertex		Starting vertex number.
0x06 	uint16	nVertices		Number of vertices.
0x08 	uint16	StartTriangle		Starting triangle index (that's 3* the number of triangles drawn so far).
0x0A 	uint16	nTriangles		Number of triangle indices.
0x0C 	uint16	nBones			Number of elements in the bone lookup table.
0x0E 	uint16	StartBones		Starting index in the bone lookup table.
0x10 	uint16	boneInfluences		<= 4		
0x12 	uint16	RootBone		Not sure.
0x14 	Vec3F	CenterMass		Average position of all the vertices in the submesh.
0x20 	Vec3F	CenterBoundingBox	The center of the box when an axis aligned box is built around the vertices in the submesh.
0x2C 	float	Radius			Distance of the vertex farthest from CenterBoundingBox.

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.

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

00**: Hairstyles
01**: Facial1: Beard
02**: Facial2: Mustache
03**: Facial3: Sideburns
04**: Glove
05**: Boots
06**: 
07**: Ears
08**: Wristbands/Sleeves: {1: none, 2: normal, 3: ruffled}
09**: Kneepads/Legcuffs: {1: none, 2: long, 3: short}
10**: Chest
11**: Pants: {1: regular, 2: short skirt, 4: armored pants}
12**: Tabard
13**: Trousers: {1: legs, 2: dress}
14**:
15**: Cloak
16**:
17**: Eyeglows (including the deathknight ones, --Rour, DK's are 0x3, racial eyeglows seem to be 0x2 but are specified in CharFacialHairStyles.dbc)
18**: Belt / bellypack
19**: Tail
20**: Feet (wod?) --Rour, existed in MoP but WoD models have more feet geosets for open-toed textures.

You can use this together with CreatureDisplayInfo.dbc.creatureGeosetData for nice effects. Also used in ItemDisplayInfo.dbc.m_geosetGroup[].

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.

Offset	Type 	Name		Description
0x00 	uint16	Flags		Usually 16 for static textures, and 0 for animated textures. &0x20: projected texture?
0x02 	uint16	shader_id	See below.
0x04 	uint16	SubmeshIndex	A duplicate entry of a submesh from the list above.
0x06 	uint16	SubmeshIndex2	See below.
0x08 	int16	ColorIndex	A Color out of the Colors-Block or -1 if none.
0x0A 	uint16	RenderFlags	The renderflags used on this texture-unit.
0x0C 	uint16	TexUnitNumber	Index into the texture unit lookup table.
0x0E 	uint16	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).
0x10 	uint16	Texture		Index into Texture lookup table
0x12 	uint16	TexUnitNumber2	Duplicate of TexUnitNumber.
0x14 	uint16	Transparency	Index into transparency lookup table.
0x16 	uint16	TextureAnim	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?

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 texutre, UV animation, render flags, and 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

vec2 sphereMap(vec3 vertex, vec3 normal)
{
	float m;
	vec3 r, u;
	u = normalize(vertex);
	r = reflect(u, normal);
	r.z += 1.0;
	m = 0.5 * inversesqrt(dot(r, r));
	return r.xy * m + 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",
};
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},
};
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_T1_T2
           : 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;
}