M2/.skin: Difference between revisions
Line 102: | Line 102: | ||
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. | 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 which texture they apply to. | 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. | ||
enum modelVertexShaders | enum modelVertexShaders |
Revision as of 00:25, 18 October 2014
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 Almost always 0, pre-WoD. When the vertex/triangle numbers start to get too high, this value is incremented by 1 and the triangle/vertex numbers start again from 0, building on top of the previous ones. 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**: pants additional? 11**: Pants: {1: regular, 2: short skirt, 4: armored pants} 12**: Tabard 13**: Trousers: {1: legs, 2: dress} 14**: 15**: Cape 16**: 17**: Eyeglows (including the deathknight ones) 18**: Belt / bellypack 20**: feet (wod?)
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 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 3. 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.
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.
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; }