//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ // //=============================================================================// #include #include #include #include #include #include "cmdlib.h" #include "scriplib.h" #include "mathlib/mathlib.h" #include "studio.h" #include "studiomdl.h" #include "bone_setup.h" #include "tier1/strtools.h" #include "mathlib/vmatrix.h" #include "optimize.h" // debugging only - enabling turns off remapping to create all lod vertexes as unique // to ensure remapping logic does not introduce collapse anomalies //#define UNIQUE_VERTEXES_FOR_LOD //----------------------------------------------------------------------------- // Forward declarations local to this file //----------------------------------------------------------------------------- class CVertexDictionary; struct VertexInfo_t; static void BuildBoneLODMapping( CUtlVector &boneMap, int lodID ); //----------------------------------------------------------------------------- // Globals //----------------------------------------------------------------------------- static int g_NumBonesInLOD[MAX_NUM_LODS]; //----------------------------------------------------------------------------- // Makes sure all boneweights in a s_boneweight_t are valid //----------------------------------------------------------------------------- static void ValidateBoneWeight( const s_boneweight_t &boneWeight ) { #ifdef _DEBUG int i; if( boneWeight.weight[0] == 1.0f ) { Assert( boneWeight.numbones == 1 ); } for( i = 0; i < boneWeight.numbones; i++ ) { Assert( boneWeight.bone[i] >= 0 && boneWeight.bone[i] < g_numbones ); } float weight = 0.0f; for( i = 0; i < boneWeight.numbones; i++ ) { weight += boneWeight.weight[i] ; } Assert( fabs( weight - 1.0f ) < 1e-3 ); #endif } //----------------------------------------------------------------------------- // Swap bones //----------------------------------------------------------------------------- static inline void SwapBones( s_boneweight_t &boneWeight, int nBone1, int nBone2 ) { // swap int nTmpBone = boneWeight.bone[nBone1]; float flTmpWeight = boneWeight.weight[nBone1]; boneWeight.bone[nBone1] = boneWeight.bone[nBone2]; boneWeight.weight[nBone1] = boneWeight.weight[nBone2]; boneWeight.bone[nBone2] = nTmpBone; boneWeight.weight[nBone2] = flTmpWeight; } //----------------------------------------------------------------------------- // Sort the bone weight structure to be sorted by bone weight //----------------------------------------------------------------------------- static void SortBoneWeightByWeight( s_boneweight_t &boneWeight ) { // bubble sort the bones by weight. . .put the largest weight first. for( int j = boneWeight.numbones; j > 1; j-- ) { for( int k = 0; k < j - 1; k++ ) { if( boneWeight.weight[k] >= boneWeight.weight[k+1] ) continue; SwapBones( boneWeight, k, k+1 ); } } } //----------------------------------------------------------------------------- // Sort the bone weight structure to be sorted by bone index //----------------------------------------------------------------------------- static void SortBoneWeightByIndex( s_boneweight_t &boneWeight ) { // bubble sort the bones by index. . .put the smallest index first. for ( int j = boneWeight.numbones; j > 1; j-- ) { for( int k = 0; k < j - 1; k++ ) { if( boneWeight.bone[k] <= boneWeight.bone[k+1] ) continue; SwapBones( boneWeight, k, k+1 ); } } } //----------------------------------------------------------------------------- // A vertex format //----------------------------------------------------------------------------- struct VertexInfo_t { Vector m_Position; Vector m_Normal; Vector2D m_TexCoord; Vector4D m_TangentS; s_boneweight_t m_BoneWeight; int m_nLodFlag; }; //----------------------------------------------------------------------------- // Stores all vertices in the vertex dictionary //----------------------------------------------------------------------------- class CVertexDictionary { public: CVertexDictionary(); // Adds a vertex to the dictionary int AddVertex( const VertexInfo_t &srcVertex ); int AddVertexFromSource( const s_source_t *pSrc, int nVertexIndex, int nLod ); // Iteration int VertexCount() const; VertexInfo_t &Vertex( int i ); const VertexInfo_t &Vertex( int i ) const; int RootLODVertexStart() const; int RootLODVertexEnd() const; // Gets the vertex count for the previous LOD int PrevLODVertexCount() const; // Marks the dictionary as starting defining vertices for a new LOD void StartNewLOD(); void SetRootVertexRange( int start, int end ); private: CUtlVector m_Verts; int m_nPrevLODCount; int m_nRootLODStart; int m_nRootLODEnd; }; //----------------------------------------------------------------------------- // Copies in a particular vertex from the s_source_t //----------------------------------------------------------------------------- CVertexDictionary::CVertexDictionary() { m_nPrevLODCount = 0; } //----------------------------------------------------------------------------- // Accessor //----------------------------------------------------------------------------- inline VertexInfo_t &CVertexDictionary::Vertex( int i ) { return m_Verts[i]; } inline const VertexInfo_t &CVertexDictionary::Vertex( int i ) const { return m_Verts[i]; } //----------------------------------------------------------------------------- // Gets the vertex count for the previous LOD //----------------------------------------------------------------------------- inline int CVertexDictionary::PrevLODVertexCount() const { return m_nPrevLODCount; } inline int CVertexDictionary::RootLODVertexStart() const { return m_nRootLODStart; } inline int CVertexDictionary::RootLODVertexEnd() const { return m_nRootLODEnd; } //----------------------------------------------------------------------------- // Marks the dictionary as starting defining vertices for a new LOD //----------------------------------------------------------------------------- void CVertexDictionary::StartNewLOD() { m_nPrevLODCount = VertexCount(); } void CVertexDictionary::SetRootVertexRange( int start, int end ) { m_nRootLODStart = start; m_nRootLODEnd = end; } //----------------------------------------------------------------------------- // Adds a vertex to the dictionary //----------------------------------------------------------------------------- int CVertexDictionary::AddVertex( const VertexInfo_t &srcVertex ) { int nDstVertID = m_Verts.AddToTail( srcVertex ); VertexInfo_t &vertex = m_Verts[ nDstVertID ]; ValidateBoneWeight( vertex.m_BoneWeight ); SortBoneWeightByIndex( vertex.m_BoneWeight ); ValidateBoneWeight( vertex.m_BoneWeight ); return nDstVertID; } //----------------------------------------------------------------------------- // Copies in a particular vertex from the s_source_t //----------------------------------------------------------------------------- int CVertexDictionary::AddVertexFromSource( const s_source_t *pSrc, int nVertexIndex, int nLod ) { int nDstVertID = m_Verts.AddToTail( ); VertexInfo_t &vertex = m_Verts[ nDstVertID ]; const s_vertexinfo_t &srcVertex = pSrc->m_GlobalVertices[nVertexIndex]; vertex.m_Position = srcVertex.position; vertex.m_Normal = srcVertex.normal; vertex.m_TexCoord = srcVertex.texcoord; vertex.m_TangentS = srcVertex.tangentS; vertex.m_BoneWeight = srcVertex.boneweight; vertex.m_nLodFlag = 1 << nLod; ValidateBoneWeight( vertex.m_BoneWeight ); SortBoneWeightByIndex( vertex.m_BoneWeight ); ValidateBoneWeight( vertex.m_BoneWeight ); return nDstVertID; } //----------------------------------------------------------------------------- // How many vertices in the dictionary? //----------------------------------------------------------------------------- int CVertexDictionary::VertexCount() const { return m_Verts.Count(); } s_source_t* GetModelLODSource( const char *pModelName, const LodScriptData_t& scriptLOD, bool* pFound ) { // When doing LOD replacement, ignore all path + extension information char* pTempBuf = (char*)_alloca( Q_strlen(pModelName) + 1 ); // Strip off extensions for the source... strcpy( pTempBuf, pModelName ); char* pDot = strrchr( pTempBuf, '.' ); if (pDot) { *pDot = 0; } for( int i = 0; i < scriptLOD.modelReplacements.Count(); i++ ) { // FIXME: Should we strip off path information? // char* pSlash = strrchr( pTempBuf1, '\\' ); // char* pSlash2 = strrchr( pTempBuf1, '/' ); // if (pSlash2 > pSlash) // pSlash = pSlash2; // if (!pSlash) // pSlash = pTempBuf1; if( !Q_stricmp( pTempBuf, scriptLOD.modelReplacements[i].GetSrcName() ) ) { *pFound = true; return scriptLOD.modelReplacements[i].m_pSource; } } *pFound = false; return 0; } //----------------------------------------------------------------------------- // Tolerances for all fields of the vertex //----------------------------------------------------------------------------- #define POSITION_EPSILON 0.01f // Was 0.05f #define TEXCOORD_EPSILON 0.01f #define NORMAL_EPSILON 10.0f // in degrees #define TANGENT_EPSILON 10.0f // in degrees #define BONEWEIGHT_EPSILON 0.05f #define UNMATCHED_BONE_WEIGHT 1.0f //----------------------------------------------------------------------------- // Computes error between two positions; returns false if the error is too great //----------------------------------------------------------------------------- bool ComparePositionFuzzy( const Vector &p1, const Vector &p2, float &flError ) { Vector vecDelta; VectorSubtract( p1, p2, vecDelta ); flError = DotProduct( vecDelta, vecDelta ); return ( flError <= (POSITION_EPSILON * POSITION_EPSILON) ); } //----------------------------------------------------------------------------- // Computes error between two normals; returns false if the error is too great //----------------------------------------------------------------------------- bool CompareNormalFuzzy( const Vector &n1, const Vector &n2, float &flError ) { static float flEpsilon = cos( DEG2RAD( NORMAL_EPSILON ) ); Vector v1, v2; v1 = n1; v2 = n2; VectorNormalize( v1 ); VectorNormalize( v2 ); float flDot = DotProduct( v1, v2 ); flError = 1.0F - flDot; return ( flDot >= flEpsilon ); } //----------------------------------------------------------------------------- // Computes error between two tangentS vectors; returns false if the error is too great //----------------------------------------------------------------------------- bool CompareTangentSFuzzy( const Vector4D &n1, const Vector4D &n2, float &flError ) { static float flEpsilon = cos( DEG2RAD( TANGENT_EPSILON ) ); Vector4D v1, v2; v1 = n1; v2 = n2; if (v1.w != v2.w) { // must match as -1 or 1 flError = 2; return false; } VectorNormalize( v1.AsVector3D() ); VectorNormalize( v2.AsVector3D() ); float flDot = DotProduct( v1.AsVector3D(), v2.AsVector3D() ); // error ranges from [0..2] flError = 1.0F - flDot; return ( flDot >= flEpsilon ); } //----------------------------------------------------------------------------- // Computes error between two texcoords; returns false if the error is too great //----------------------------------------------------------------------------- bool CompareTexCoordsFuzzy( const Vector2D &t1, const Vector2D &t2, float &flError ) { Vector2D vecError; vecError[0] = fabs( t2[0] - t1[0] ); vecError[1] = fabs( t2[1] - t1[1] ); flError = vecError.LengthSqr(); return ( flError <= (TEXCOORD_EPSILON * TEXCOORD_EPSILON) ); } //----------------------------------------------------------------------------- // Computes the error between two bone weights, returns false if they are too far //----------------------------------------------------------------------------- bool CompareBoneWeightsFuzzy( const s_boneweight_t &b1, const s_boneweight_t &b2, float &flError ) { // This is a list of which bones that exist in b1 also exist in b2. // Use the index to figure out where in the array for b2 that the corresponding bone in b1 is. int nMatchingBones = 0; int pBoneIndexMap1[MAX_NUM_BONES_PER_VERT]; int pBoneIndexMap2[MAX_NUM_BONES_PER_VERT]; int i; for ( i = 0; i < b2.numbones; ++i ) { pBoneIndexMap2[i] = -1; } for ( i = 0; i < b1.numbones; ++i ) { pBoneIndexMap1[i] = -1; for ( int j = 0; j < b2.numbones; ++j ) { if ( b2.bone[j] == b1.bone[i] ) { pBoneIndexMap1[i] = j; pBoneIndexMap2[j] = i; ++nMatchingBones; break; } } } // If no bones match, we're done if ( !nMatchingBones ) { flError = FLT_MAX; return false; } // At least one bone matches, so we're going to consider this vertex as a potential match // This loop will take care of figuring out the error for all bones that exist in // b1 alone, and all bones that exist in b1 and b2 flError = 0; for ( i = 0; i < b1.numbones; ++i ) { // If we didn't find a match for this bone, compute a more expensive weight if ( pBoneIndexMap1[i] == -1 ) { flError += b1.weight[i] * b1.weight[i] * UNMATCHED_BONE_WEIGHT; continue; } float flDeltaWeight = fabs( b1.weight[i] - b2.weight[ pBoneIndexMap1[i] ] ); flError += flDeltaWeight * flDeltaWeight; } // This loop will take care of figuring out the error for all bones that exist in b2 alone for ( i = 0; i < b2.numbones; ++i ) { // If we didn't find a match for this bone, compute a more expensive weight if ( pBoneIndexMap2[i] == -1 ) { flError += b2.weight[i] * b2.weight[i] * UNMATCHED_BONE_WEIGHT; } } // This renormalizes the error. The error will become greater with the total // number of bones in the two vertices. flError /= sqrt( (float) (b1.numbones + b2.numbones)); return ( flError <= BONEWEIGHT_EPSILON ); } //----------------------------------------------------------------------------- // Searches for a material in the texture list //----------------------------------------------------------------------------- int FindMaterialByName( const char *pMaterialName ) { int i; int allocLen = strlen( pMaterialName ) + 1; char *pBaseName = ( char * )_alloca( allocLen ); Q_FileBase( ( char * )pMaterialName, pBaseName, allocLen ); for( i = 0; i < g_numtextures; i++ ) { if( stricmp( pBaseName, g_texture[i].name ) == 0 ) { return i; } } return -1; } static s_mesh_t *FindMeshByMaterial( s_source_t *pSrc, int nMaterialID ) { for ( int m = 0; m < pSrc->nummeshes; m++ ) { if ( pSrc->meshindex[m] == nMaterialID ) return &pSrc->mesh[ pSrc->meshindex[m] ]; } // this mesh/material doesn't exist at this lod. return NULL; } static s_mesh_t *FindOrCullMesh( int nLodID, s_source_t *pSrc, int nMaterialID ) { char baseMeshName[MAX_PATH]; char baseRemovalName[MAX_PATH]; // possibly marked for removal via $removemesh // determine mesh name int nTextureID = MaterialToTexture( nMaterialID ); if (nTextureID == -1) { MdlError( "Unknown Texture for Material %d\n", nMaterialID ); } Q_FileBase(g_texture[nTextureID].name, baseMeshName, sizeof(baseMeshName)-1); for ( int i = 0; i < g_ScriptLODs[nLodID].meshRemovals.Count(); i++ ) { const char *pMeshRemovalName = g_ScriptLODs[nLodID].meshRemovals[i].GetSrcName(); Q_FileBase( pMeshRemovalName, baseRemovalName, sizeof(baseRemovalName)-1); if (!stricmp( baseRemovalName, baseMeshName )) { // mesh has been marked for removal return NULL; } } s_mesh_t *pMesh = FindMeshByMaterial( pSrc, nMaterialID ); return pMesh; } static void CopyVerts( int nLodID, const s_source_t *pSrc, const s_mesh_t *pSrcMesh, CVertexDictionary &vertexDict, s_mesh_t *pDstMesh, int *pMeshVertIndexMap ) { // populate the dictionary with the verts for( int srcVertID = 0; srcVertID < pSrcMesh->numvertices; srcVertID++ ) { int nVertexIndex = pSrcMesh->vertexoffset + srcVertID; pMeshVertIndexMap[ nVertexIndex ] = vertexDict.AddVertexFromSource( pSrc, nVertexIndex, nLodID ) - pDstMesh->vertexoffset; } pDstMesh->numvertices = pSrcMesh->numvertices; } static void CopyFaces( const s_source_t *pSrc, const s_mesh_t *pSrcMesh, CUtlVector &faces, s_mesh_t *pDstMesh ) { int srcFaceID; for( srcFaceID = 0; srcFaceID < pSrcMesh->numfaces; srcFaceID++ ) { int srcID = srcFaceID + pSrcMesh->faceoffset; s_face_t *pSrcFace = &pSrc->face[srcID]; s_face_t *pDstFace = &faces[faces.AddToTail()]; pDstFace->a = pSrcFace->a; pDstFace->b = pSrcFace->b; pDstFace->c = pSrcFace->c; pDstMesh->numfaces++; } } #define IGNORE_POSITION 0x01 #define IGNORE_TEXCOORD 0x02 #define IGNORE_BONEWEIGHT 0x04 #define IGNORE_NORMAL 0x08 #define IGNORE_TANGENTS 0x10 //----------------------------------------------------------------------------- // return -1 if there is no match. The index returned is used to index into vertexDict. //----------------------------------------------------------------------------- static int FindVertexWithinVertexDictionary( const VertexInfo_t &find, const CVertexDictionary &vertexDict, int nStartVert, int nEndVert, int fIgnore ) { int nBestIndex = -1; float flPositionError = 0.0f; float flNormalError = 0.0f; float flTangentSError = 0.0f; float flTexcoordError = 0.0f; float flBoneWeightError = 0.0f; float flMinPositionError = FLT_MAX; float flMinNormalError = FLT_MAX; float flMinTangentSError = FLT_MAX; float flMinTexcoordError = FLT_MAX; float flMinBoneWeightError = FLT_MAX; bool bFound; if (fIgnore & IGNORE_POSITION) { flMinPositionError = 0; flPositionError = 0; } if (fIgnore & IGNORE_TEXCOORD) { flMinTexcoordError = 0; flTexcoordError = 0; } if (fIgnore & IGNORE_BONEWEIGHT) { flMinBoneWeightError = 0; flBoneWeightError = 0; } if (fIgnore & IGNORE_NORMAL) { flMinNormalError = 0; flNormalError = 0; } if (fIgnore & IGNORE_TANGENTS) { flMinTangentSError = 0; flTangentSError = 0; } for ( int nVertexIndex = nStartVert; nVertexIndex < nEndVert; ++nVertexIndex ) { // see if the position is reasonable if ( !(fIgnore & IGNORE_POSITION) && !ComparePositionFuzzy( find.m_Position, vertexDict.Vertex(nVertexIndex).m_Position, flPositionError ) ) continue; if ( !(fIgnore & IGNORE_TEXCOORD) && !CompareTexCoordsFuzzy( find.m_TexCoord, vertexDict.Vertex(nVertexIndex).m_TexCoord, flTexcoordError ) ) continue; if ( !(fIgnore & IGNORE_BONEWEIGHT) && !CompareBoneWeightsFuzzy( find.m_BoneWeight, vertexDict.Vertex(nVertexIndex).m_BoneWeight, flBoneWeightError ) ) continue; if ( !(fIgnore & IGNORE_NORMAL) && !CompareNormalFuzzy( find.m_Normal, vertexDict.Vertex(nVertexIndex).m_Normal, flNormalError ) ) continue; if ( !(fIgnore & IGNORE_TANGENTS) && !CompareTangentSFuzzy( find.m_TangentS, vertexDict.Vertex(nVertexIndex).m_TangentS, flTangentSError ) ) continue; // the vert with minimum error is the best or exact candidate bFound = false; if (flMinPositionError > flPositionError) { bFound = true; } else if (flMinPositionError == flPositionError) { if (flMinTexcoordError > flTexcoordError) { bFound = true; } else if (flMinTexcoordError == flTexcoordError) { if (flMinBoneWeightError > flBoneWeightError) { bFound = true; } else if (flMinBoneWeightError == flBoneWeightError) { if (flMinNormalError > flNormalError) { bFound = true; } else if (flMinNormalError == flNormalError) { if (flMinTangentSError >= flTangentSError) { bFound = true; } } } } } if (!bFound) continue; flMinPositionError = flPositionError; flMinTexcoordError = flTexcoordError; flMinBoneWeightError = flBoneWeightError; flMinNormalError = flNormalError; flMinTangentSError = flTangentSError; nBestIndex = nVertexIndex; } return nBestIndex; } //----------------------------------------------------------------------------- // Use position, normal, and texcoord checks across the entire model to find a boneweight //----------------------------------------------------------------------------- static void FindBoneWeightWithinModel( const VertexInfo_t &searchVertex, const s_source_t *pSrc, s_boneweight_t &boneWeight, int fIgnore ) { int nBestIndex = -1; float flPositionError = 0.0f; float flNormalError = 0.0f; float flTangentSError = 0.0f; float flTexcoordError = 0.0f; float flMinPositionError = FLT_MAX; float flMinNormalError = FLT_MAX; float flMinTangentSError = FLT_MAX; float flMinTexcoordError = FLT_MAX; bool bFound; if (fIgnore & IGNORE_NORMAL) { flMinNormalError = 0; flNormalError = 0; } if (fIgnore & IGNORE_TEXCOORD) { flMinTexcoordError = 0; flTexcoordError = 0; } if (fIgnore & IGNORE_TANGENTS) { flMinTangentSError = 0; flTangentSError = 0; } int nVertexCount = pSrc->m_GlobalVertices.Count(); for ( int i = 0; i < nVertexCount; i++ ) { const s_vertexinfo_t &srcVertex = pSrc->m_GlobalVertices[i]; // Compute error metrics ComparePositionFuzzy( searchVertex.m_Position, srcVertex.position, flPositionError ); if (!(fIgnore & IGNORE_NORMAL)) { CompareNormalFuzzy( searchVertex.m_Normal, srcVertex.normal, flNormalError ); } if (!(fIgnore & IGNORE_TEXCOORD)) { CompareTexCoordsFuzzy( searchVertex.m_TexCoord, srcVertex.texcoord, flTexcoordError ); } if (!(fIgnore & IGNORE_TANGENTS)) { CompareTangentSFuzzy( searchVertex.m_TangentS, srcVertex.tangentS, flTangentSError ); } // the vert with minimum error is the best or exact candidate bFound = false; if (flMinPositionError > flPositionError) { bFound = true; } else if (flMinPositionError == flPositionError) { if (flMinTexcoordError > flTexcoordError) { bFound = true; } else if (flMinTexcoordError == flTexcoordError) { if (flMinNormalError > flNormalError) { bFound = true; } else if (flMinNormalError == flNormalError) { if (flMinTangentSError >= flTangentSError) { bFound = true; } } } } if (bFound) { flMinPositionError = flPositionError; flMinTexcoordError = flTexcoordError; flMinNormalError = flNormalError; flMinTangentSError = flTangentSError; nBestIndex = i; } } if ( nBestIndex == -1 ) { MdlError( "Encountered a mesh with no vertices!\n" ); } memcpy( &boneWeight, &pSrc->m_GlobalVertices[ nBestIndex ].boneweight, sizeof(s_boneweight_t) ); } //----------------------------------------------------------------------------- // Modify the bone weights in all of the vertices.... //----------------------------------------------------------------------------- static void RemapBoneWeights( const CUtlVector &boneMap, s_boneweight_t &boneWeight ) { for( int i = 0; i < boneWeight.numbones; i++ ) { Assert( boneWeight.bone[i] >= 0 && boneWeight.bone[i] < boneMap.Count() ); boneWeight.bone[i] = boneMap[ boneWeight.bone[i] ]; } } //----------------------------------------------------------------------------- // After the remapping, we may get multiple instances of the same bone // which we want to collapse into a single bone //----------------------------------------------------------------------------- static void CollapseBoneWeights( s_boneweight_t &boneWeight ) { // We need the bones to be sorted by bone index for the loop right below SortBoneWeightByIndex( boneWeight ); for( int i = 0; i < boneWeight.numbones-1; i++ ) { if( boneWeight.bone[i] != boneWeight.bone[i+1] ) continue; // add i+1's weight to i since they have the same bone index boneWeight.weight[i] += boneWeight.weight[i+1]; // remove i+1 for( int j = i+1; j < boneWeight.numbones-1; j++ ) { boneWeight.bone[j] = boneWeight.bone[j+1]; boneWeight.weight[j] = boneWeight.weight[j+1]; } --boneWeight.numbones; // Gotta step back one, may have many bones collapsing into one --i; } ValidateBoneWeight( boneWeight ); } //----------------------------------------------------------------------------- // Find a matching vertex within the root lod //----------------------------------------------------------------------------- static void CalculateBoneWeightFromRootLod( const VertexInfo_t &searchVertex, CVertexDictionary &vertexDict, const s_source_t *pRootLODSrc, VertexInfo_t &idealVertex ) { idealVertex = searchVertex; // Look through the part of the vertex dictionary associated with the root LODs for a match // bone weights are not defined properly in SMDs for lower LODs, so don't consider // we can only accept the boneweight from the root LOD int nVertexDictID = FindVertexWithinVertexDictionary( searchVertex, vertexDict, vertexDict.RootLODVertexStart(), vertexDict.RootLODVertexEnd(), IGNORE_BONEWEIGHT|IGNORE_TANGENTS ); if ( nVertexDictID != -1 ) { Assert( nVertexDictID >= vertexDict.RootLODVertexStart() && nVertexDictID < vertexDict.RootLODVertexEnd() ); Assert( nVertexDictID >= 0 && nVertexDictID < vertexDict.VertexCount() ); // found vertex in dictionary #ifdef UNIQUE_VERTEXES_FOR_LOD // keep entry vertex and fill in the missing bone weight attribute idealVertex.m_BoneWeight = vertexDict.Vertex( nVertexDictID ).m_BoneWeight; #else // discard entry vertex in favor of best match // this ensures all the attributes, including bone weight are correct for that vertex // the worst case is that the vertex is not an *exact* match for entry attributes just a "close" match idealVertex = vertexDict.Vertex( nVertexDictID ); #endif return; } // In this case, we didn't find anything within the tolerance, so we need to // do a *positional check only* to give us a bone weight to assign to this vertex. FindBoneWeightWithinModel( searchVertex, pRootLODSrc, idealVertex.m_BoneWeight, IGNORE_BONEWEIGHT|IGNORE_TANGENTS ); } //----------------------------------------------------------------------------- // Find a matching vertex //----------------------------------------------------------------------------- static void CalculateIdealVert( const VertexInfo_t &searchVertex, CVertexDictionary &vertexDict, const s_mesh_t *pVertexDictMesh, const s_source_t *pRootLODSrc, VertexInfo_t &idealVertex ) { #ifndef UNIQUE_VERTEXES_FOR_LOD // Only look through the part of the vertex dictionary associated with all *higher* LODs for a match int nVertexDictID = FindVertexWithinVertexDictionary( searchVertex, vertexDict, pVertexDictMesh->vertexoffset, vertexDict.PrevLODVertexCount(), 0 ); if ( nVertexDictID != -1 ) { Assert( nVertexDictID >= pVertexDictMesh->vertexoffset && nVertexDictID < vertexDict.PrevLODVertexCount() ); Assert( nVertexDictID >= 0 && nVertexDictID < vertexDict.VertexCount() ); // found vertex in dictionary idealVertex = vertexDict.Vertex( nVertexDictID ); return; } #endif // could not find a tolerant match // the search vertex is unique idealVertex = searchVertex; } static bool FuzzyFloatCompare( float f1, float f2, float epsilon ) { if( fabs( f1 - f2 ) < epsilon ) { return true; } else { return false; } } //----------------------------------------------------------------------------- // Is this bone weight structure sorted by bone? //----------------------------------------------------------------------------- static bool IsBoneWeightSortedByBone( const s_boneweight_t &src ) { for ( int i = 1; i < src.numbones; ++i ) { Assert( src.bone[i] != -1 ); if ( src.bone[ i-1 ] > src.bone[ i ] ) return false; } return true; } //----------------------------------------------------------------------------- // Are two bone-weight structures equal? //----------------------------------------------------------------------------- static bool AreBoneWeightsEqual( const s_boneweight_t &b1, const s_boneweight_t &b2 ) { // Have to have the same number of bones if ( b1.numbones != b2.numbones ) return false; // This is a list of which bones that exist in b1 also exist in b2. // Use the index to figure out where in the array for b2 that the corresponding bone in b1 is. int nMatchingBones = 0; int pBoneIndexMap[MAX_NUM_BONES_PER_VERT]; int i; for ( i = 0; i < b1.numbones; ++i ) { pBoneIndexMap[i] = -1; for ( int j = 0; j < b2.numbones; ++j ) { if ( b2.bone[j] == b1.bone[i] ) { pBoneIndexMap[i] = j; ++nMatchingBones; break; } } } // If we aren't using the same bone indices, we're done if ( nMatchingBones != b1.numbones ) return false; // Check to see if the weights are the same for ( i = 0; i < b1.numbones; ++i ) { Assert( pBoneIndexMap[i] != -1 ); if ( b1.weight[i] != b2.weight[ pBoneIndexMap[i] ] ) return false; } return true; } //----------------------------------------------------------------------------- // Finds an *exact* requested vertex in the dictionary //----------------------------------------------------------------------------- static int FindVertexInDictionaryExact( CVertexDictionary &vertexDict, int nStartVert, int nEndVert, const VertexInfo_t &vertex ) { for ( int nVertID = nStartVert; nVertID < nEndVert; ++nVertID ) { if ( vertexDict.Vertex( nVertID ).m_Position != vertex.m_Position ) continue; if ( !AreBoneWeightsEqual( vertexDict.Vertex( nVertID ).m_BoneWeight, vertex.m_BoneWeight ) ) continue; if ( vertexDict.Vertex( nVertID ).m_TexCoord != vertex.m_TexCoord ) continue; if ( vertexDict.Vertex( nVertID ).m_Normal != vertex.m_Normal ) continue; if ( vertexDict.Vertex( nVertID ).m_TangentS != vertex.m_TangentS ) continue; return nVertID; } return -1; } //----------------------------------------------------------------------------- // Finds the *exact* requested vertex in the dictionary or creates it //----------------------------------------------------------------------------- static int FindOrCreateExactVertexInDictionary( CVertexDictionary &vertexDict, const VertexInfo_t &vertex, s_mesh_t *pDstMesh ) { int nMeshVertID = FindVertexInDictionaryExact( vertexDict, pDstMesh->vertexoffset, pDstMesh->vertexoffset+pDstMesh->numvertices, vertex ); if ( nMeshVertID != -1 ) { // flag vertex for what LoD's are using it vertexDict.Vertex( nMeshVertID ).m_nLodFlag |= vertex.m_nLodFlag; return nMeshVertID - pDstMesh->vertexoffset; } nMeshVertID = vertexDict.AddVertex( vertex ); ++pDstMesh->numvertices; return nMeshVertID - pDstMesh->vertexoffset; } static void PrintBonesUsedInLOD( s_source_t *pSrc ) { printf( "PrintBonesUsedInLOD\n" ); int nVertexCount = pSrc->m_GlobalVertices.Count(); for( int i = 0; i m_GlobalVertices[i].position; Vector &norm = pSrc->m_GlobalVertices[i].normal; Vector2D &texcoord = pSrc->m_GlobalVertices[i].texcoord; printf( "pos: %f %f %f norm: %f %f %f texcoord: %f %f\n", pos[0], pos[1], pos[2], norm[0], norm[1], norm[2], texcoord[0], texcoord[1] ); s_boneweight_t *pBoneWeight = &pSrc->m_GlobalVertices[i].boneweight; int j; for( j = 0; j < pBoneWeight->numbones; j++ ) { int globalBoneID = pBoneWeight->bone[j]; const char *pBoneName = g_bonetable[globalBoneID].name; printf( "vert: %d bone: %d boneid: %d weight: %f name: \"%s\"\n", i, ( int )j, ( int )pBoneWeight->bone[j], ( float )pBoneWeight->weight[j], pBoneName ); } printf( "\n" ); fflush( stdout ); } } //----------------------------------------------------------------------------- // Indicates a particular set of bones is used by a particular LOD //----------------------------------------------------------------------------- static void MarkBonesUsedByLod( const s_boneweight_t &boneWeight, int nLodID ) { for( int j = 0; j < boneWeight.numbones; ++j ) { int nGlobalBoneID = boneWeight.bone[j]; s_bonetable_t *pBone = &g_bonetable[nGlobalBoneID]; pBone->flags |= ( BONE_USED_BY_VERTEX_LOD0 << nLodID ); } } static void PrintSBoneWeight( s_boneweight_t *pBoneWeight, const s_source_t *pSrc ) { int j; for( j = 0; j < pBoneWeight->numbones; j++ ) { int globalBoneID; globalBoneID = pBoneWeight->bone[j]; const char *pBoneName = g_bonetable[globalBoneID].name; printf( "bone: %d boneid: %d weight: %f name: \"%s\"\n", ( int )j, ( int )pBoneWeight->bone[j], ( float )pBoneWeight->weight[j], pBoneName ); } } //----------------------------------------------------------------------------- // In the non-top LOD, look for vertices that would be appropriate from the // vertex dictionary, and use them if you find them, or add new vertices to the // vertex dictionary if not and use those new vertices. //----------------------------------------------------------------------------- static void CreateLODVertsInDictionary( int nLodID, const s_source_t *pRootLODSrc, s_source_t *pCurrentLODSrc, const s_mesh_t *pCurrLODMesh, s_mesh_t *pVertexDictMesh, CVertexDictionary &vertexDict, int *pMeshVertIndexMap ) { // this function is specific to lods and not the root Assert( nLodID ); int nNumCurrentVerts = vertexDict.VertexCount(); // Used to control where we look for vertices + merging rules vertexDict.StartNewLOD(); CUtlVector boneMap; BuildBoneLODMapping( boneMap, nLodID ); for( int nSrcVertID = 0; nSrcVertID < pCurrLODMesh->numvertices; ++nSrcVertID ) { int nSrcID = nSrcVertID + pCurrLODMesh->vertexoffset; // candidate vertex // vertices at lower LODs have bogus boneweights assigned // must get the boneweight from the nearest or exact vertex at root lod const s_vertexinfo_t& srcVertex = pCurrentLODSrc->m_GlobalVertices[nSrcID]; VertexInfo_t vertex; vertex.m_Position = srcVertex.position; vertex.m_Normal = srcVertex.normal; vertex.m_TexCoord = srcVertex.texcoord; vertex.m_TangentS = srcVertex.tangentS; #ifdef _DEBUG memset( &vertex.m_BoneWeight, 0xDD, sizeof( s_boneweight_t ) ); #endif // determine the best bone weight for the desired vertex within the root lod only // the root lod contains no bone remappings // this ensures we get a vertex with its matched proper boneweight assignment VertexInfo_t idealVertex; CalculateBoneWeightFromRootLod( vertex, vertexDict, pRootLODSrc, idealVertex ); // try again to match the candidate vertex // determine the ideal vertex with desired remapped boneweight vertex = idealVertex; CalculateIdealVert( vertex, vertexDict, pVertexDictMesh, pRootLODSrc, idealVertex); // remap bone RemapBoneWeights( boneMap, idealVertex.m_BoneWeight ); CollapseBoneWeights( idealVertex.m_BoneWeight ); SortBoneWeightByWeight( idealVertex.m_BoneWeight ); // FIXME: this is marking bones based on the slammed vertex data MarkBonesUsedByLod( idealVertex.m_BoneWeight, nLodID ); // tag ideal vertex as being part of the current lod idealVertex.m_nLodFlag = 1 << nLodID; // Find the exact vertex or create it in the dictionary int nMeshVertID = FindOrCreateExactVertexInDictionary( vertexDict, idealVertex, pVertexDictMesh ); // Indicate where in the higher LODs the vertex we selected resides pMeshVertIndexMap[nSrcID] = nMeshVertID; } int nNewVertsCreated = vertexDict.VertexCount() - nNumCurrentVerts; if (!g_quiet && nNewVertsCreated) { printf( "Lod %d: vertexes: %d (%d new)\n", nLodID, vertexDict.VertexCount(), nNewVertsCreated); } } static void PrintSourceVerts( s_source_t *pSrc ) { int nVertexCount = pSrc->m_GlobalVertices.Count(); for( int i = 0; i < nVertexCount; i++ ) { const s_vertexinfo_t &srcVertex = pSrc->m_GlobalVertices[i]; printf( "v %d ", i ); printf( "pos: %f %f %f ", srcVertex.position[0], srcVertex.position[1], srcVertex.position[2] ); printf( "norm: %f %f %f ", srcVertex.normal[0], srcVertex.normal[1], srcVertex.normal[2] ); printf( "texcoord: %f %f\n", srcVertex.texcoord[0], srcVertex.texcoord[1] ); int j; for( j = 0; j < srcVertex.boneweight.numbones; j++ ) { printf( "\t%d: %d %f\n", j, ( int )srcVertex.boneweight.bone[j], srcVertex.boneweight.weight[j] ); } fflush( stdout ); } } //----------------------------------------------------------------------------- // Copy the vertex dictionary to the finalized processed data // Leaves the source data intact, necessary for later processes. // Routines can then choose which data they operate on //----------------------------------------------------------------------------- static void SetProcessedWithDictionary( s_model_t* pSrcModel, CVertexDictionary &vertexDict, CUtlVector &faces, CUtlVector &meshes, int *pMeshVertIndexMaps[MAX_NUM_LODS] ) { int i; s_loddata_t *pLodData = new s_loddata_t; memset( pLodData, 0, sizeof(s_loddata_t) ); pSrcModel->m_pLodData = pLodData; int nVertexCount = vertexDict.VertexCount(); pLodData->vertex = (s_lodvertexinfo_t *)kalloc( nVertexCount, sizeof( s_lodvertexinfo_t ) ); pLodData->numvertices = nVertexCount; pLodData->face = (s_face_t *)kalloc( faces.Count(), sizeof( s_face_t )); pLodData->numfaces = faces.Count(); for ( i = 0; i < nVertexCount; ++i ) { const VertexInfo_t &srcVertex = vertexDict.Vertex( i ); s_lodvertexinfo_t &dstVertex = pLodData->vertex[i]; dstVertex.boneweight = srcVertex.m_BoneWeight; Assert( dstVertex.boneweight.numbones <= 4 ); dstVertex.position = srcVertex.m_Position; dstVertex.normal = srcVertex.m_Normal; dstVertex.texcoord = srcVertex.m_TexCoord; dstVertex.tangentS = srcVertex.m_TangentS; dstVertex.lodFlag = srcVertex.m_nLodFlag; } memcpy( pLodData->face, faces.Base(), faces.Count() * sizeof( s_face_t ) ); memcpy( pLodData->mesh, meshes.Base(), meshes.Count() * sizeof( s_mesh_t ) ); for (i=0; ipMeshVertIndexMaps[i] = pMeshVertIndexMaps[i]; } } //----------------------------------------------------------------------------- // This fills out boneMap, which is a mapping from src bone to src bone replacement (or to itself // if there is no bone replacement. //----------------------------------------------------------------------------- static void BuildBoneLODMapping( CUtlVector &boneMap, int lodID ) { boneMap.AddMultipleToTail( g_numbones ); Assert( lodID < g_ScriptLODs.Size() ); LodScriptData_t& scriptLOD = g_ScriptLODs[lodID]; // First, create a direct mapping where no bones are collapsed int i; for( i = 0; i < g_numbones; i++ ) { boneMap[i] = i; } for( i = 0; i < scriptLOD.boneReplacements.Size(); i++ ) { const char *src, *dst; src = scriptLOD.boneReplacements[i].GetSrcName(); dst = scriptLOD.boneReplacements[i].GetDstName(); int j = findGlobalBone( src ); int k = findGlobalBone( dst ); if ( j != -1 && k != -1) { boneMap[j] = k; } else if ( j == -1) { // FIXME: is this really an error? It could just be replacement command for bone that doesnt' exist anymore. if (g_verbose) { MdlWarning( "Couldn't replace unknown bone \"%s\" with \"%s\"\n", src, dst ); } } else { // FIXME: is this really an error? It could just be replacement command for bone that doesnt' exist anymore. if (g_verbose) { MdlWarning( "Couldn't replace bone \"%s\" with unknown \"%s\"\n", src, dst ); } } } } static void MarkRootLODBones( CVertexDictionary &vertexDictionary ) { // should result in an identity mapping // because their are no bone remaps at the root lod CUtlVector boneMap; BuildBoneLODMapping( boneMap, 0 ); // iterate and mark bones for (int nVertDictID=vertexDictionary.RootLODVertexStart(); nVertDictIDname, "blank" ) ) return; // each lod has a unique vertex mapping table int nNumLODs = pSrcModel->m_LodSources.Count(); int nLodID; int *pMeshVertIndexMaps[MAX_NUM_LODS]; for ( nLodID = 0; nLodID < MAX_NUM_LODS; nLodID++ ) { if ( nLodID < nNumLODs && pSrcModel->m_LodSources[nLodID] ) { int nVertexCount = pSrcModel->m_LodSources[nLodID]->m_GlobalVertices.Count(); pMeshVertIndexMaps[nLodID] = new int[ nVertexCount ]; #ifdef _DEBUG memset( pMeshVertIndexMaps[nLodID], 0xDD, nVertexCount * sizeof(int) ); #endif } else { pMeshVertIndexMaps[nLodID] = NULL; } } // These hold the aggregate data for the model that grows as lods are processed CVertexDictionary vertexDictionary; CUtlVector faces; CUtlVector meshes; meshes.AddMultipleToTail( MAXSTUDIOSKINS ); Assert( meshes.Count() == MAXSTUDIOSKINS ); memset( meshes.Base(), 0, meshes.Count() * sizeof( s_mesh_t ) ); int nMeshID; for( nMeshID = 0; nMeshID < pSrcModel->source->nummeshes; nMeshID++ ) { s_mesh_t *pVertexDictMesh = &meshes[pSrcModel->source->meshindex[nMeshID]]; pVertexDictMesh->numvertices = 0; pVertexDictMesh->vertexoffset = vertexDictionary.VertexCount(); pVertexDictMesh->numfaces = 0; pVertexDictMesh->faceoffset = faces.Count(); // First build up information for LOD 0 if ( !pSrcModel->m_LodSources[0] ) continue; s_source_t *pLOD0Source = pSrcModel->m_LodSources[0]; // lookup the material used by this mesh int nMaterialID = pLOD0Source->meshindex[nMeshID]; s_mesh_t *pLOD0Mesh = FindMeshByMaterial( pLOD0Source, nMaterialID ); if ( !pLOD0Mesh ) continue; // populate with all vertices from LOD 0 int nStart = vertexDictionary.VertexCount(); CopyVerts( 0, pLOD0Source, pLOD0Mesh, vertexDictionary, pVertexDictMesh, pMeshVertIndexMaps[0] ); vertexDictionary.SetRootVertexRange( nStart, vertexDictionary.VertexCount() ); MarkRootLODBones( vertexDictionary ); // only fix up the faces for the highest lod since the lowest ones are going // to be reprocessed later. CopyFaces( pLOD0Source, pLOD0Mesh, faces, pVertexDictMesh ); // Now, for each LOD, try to build meshes using the vertices in LOD 0. // Ideally, vertices used in an LOD would be in LOD 0 for the benefit of shared vertices. // If we don't find vertices in LOD 0, this code will add vertices into LOD 0's list // of vertices for the next LOD to find for ( nLodID = 1; nLodID < nNumLODs; ++nLodID ) { s_source_t *pCurrLOD = pSrcModel->m_LodSources[nLodID]; if ( !pCurrLOD ) continue; // Find the mesh that matches the material // mesh may not be present or could be culled due to $removemesh commands s_mesh_t *pCurrLODMesh = FindOrCullMesh( nLodID, pCurrLOD, nMaterialID ); if ( !pCurrLODMesh ) continue; CreateLODVertsInDictionary( nLodID, pLOD0Source, pCurrLOD, pCurrLODMesh, pVertexDictMesh, vertexDictionary, pMeshVertIndexMaps[nLodID]); } } #ifdef _DEBUG Msg( "Total vertex count: %d\n", vertexDictionary.VertexCount() ); #endif // save the data we just built into the processed data section // The processed data has all of the verts that are needed for all LODs. SetProcessedWithDictionary( pSrcModel, vertexDictionary, faces, meshes, pMeshVertIndexMaps ); // PrintSourceVerts( pSrcModel->m_LodModels[0] ); } //----------------------------------------------------------------------------- // Force the vertex array for a model to have all of the vertices that are needed // for all of the LODs of the model. //----------------------------------------------------------------------------- void UnifyLODs( void ) { // todo: need to fixup the firstref/lastref stuff . . do we really need it anymore? for( int modelID = 0; modelID < g_nummodelsbeforeLOD; modelID++ ) { UnifyModelLODs( g_model[modelID] ); } } static void PrintSpaces( int numSpaces ) { int i; for( i = 0; i < numSpaces; i++ ) { printf( " " ); } } static void SpewBoneInfo( int globalBoneID, int depth ) { s_bonetable_t *pBone = &g_bonetable[globalBoneID]; if( g_bPrintBones ) { PrintSpaces( depth * 2 ); printf( "%d \"%s\" ", depth, pBone->name ); } int i; for( i = 0; i < 8; i++ ) { if( pBone->flags & ( BONE_USED_BY_VERTEX_LOD0 << i ) ) { if( g_bPrintBones ) { printf( "lod%d ", i ); } g_NumBonesInLOD[i]++; } } if( g_bPrintBones ) { printf( "\n" ); } int j; for( j = 0; j < g_numbones; j++ ) { s_bonetable_t *pBone = &g_bonetable[j]; if( pBone->parent == globalBoneID ) { SpewBoneInfo( j, depth + 1 ); } } } void SpewBoneUsageStats( void ) { memset( g_NumBonesInLOD, 0, sizeof( int ) * MAX_NUM_LODS ); if( g_numbones == 0 ) { return; } SpewBoneInfo( 0, 0 ); if( g_bPrintBones ) { int i; for( i = 0; i < g_ScriptLODs.Count(); i++ ) { printf( "\t%d bones used in lod %d\n", g_NumBonesInLOD[i], i ); } } } void MarkParentBoneLODs( void ) { int i; for( i = 0; i < g_numbones; i++ ) { int flags = g_bonetable[i].flags; flags &= BONE_USED_BY_VERTEX_MASK; int globalBoneID = g_bonetable[i].parent; while( globalBoneID != -1 ) { g_bonetable[globalBoneID].flags |= flags; globalBoneID = g_bonetable[globalBoneID].parent; } } } //----------------------------------------------------------------------------- // Returns the sources associated with the various LODs based on the script commands //----------------------------------------------------------------------------- static void GetLODSources( CUtlVector< s_source_t * > &lods, const s_model_t *pSrcModel ) { int nNumLODs = g_ScriptLODs.Count(); lods.EnsureCount( nNumLODs ); for( int lodID = 0; lodID < nNumLODs; lodID++ ) { LodScriptData_t& scriptLOD = g_ScriptLODs[lodID]; bool bFound; s_source_t* pSource = GetModelLODSource( pSrcModel->filename, scriptLOD, &bFound ); if ( !pSource && !bFound ) { pSource = pSrcModel->source; } lods[lodID] = pSource; } } //----------------------------------------------------------------------------- // Creates models to store converted data for the various LODs //----------------------------------------------------------------------------- void LoadLODSources( void ) { g_nummodelsbeforeLOD = g_nummodels; for( int modelID = 0; modelID < g_nummodelsbeforeLOD; modelID++ ) { if ( !Q_stricmp( g_model[modelID]->name, "blank" ) ) { int nNumLODs = g_ScriptLODs.Count(); g_model[modelID]->m_LodSources.SetCount( nNumLODs ); for ( int i = 0; i < nNumLODs; ++i ) { g_model[modelID]->m_LodSources[i] = NULL; } continue; } GetLODSources( g_model[modelID]->m_LodSources, g_model[modelID] ); } } static void ReplaceBonesRecursive( int globalBoneID, bool replaceThis, CUtlVector &boneReplacements, const char *replacementName ) { if( replaceThis ) { CLodScriptReplacement_t &boneReplacement = boneReplacements[boneReplacements.AddToTail()]; boneReplacement.SetSrcName( g_bonetable[globalBoneID].name ); boneReplacement.SetDstName( replacementName ); } // find children and recurse. int i; for( i = 0; i < g_numbones; i++ ) { if( g_bonetable[i].parent == globalBoneID ) { ReplaceBonesRecursive( i, true, boneReplacements, replacementName ); } } } static void ConvertSingleBoneTreeCollapseToReplaceBones( CLodScriptReplacement_t &boneTreeCollapse, CUtlVector &boneReplacements ) { // find the bone that we are starting with. int i = findGlobalBone( boneTreeCollapse.GetSrcName() ); if (i != -1) { ReplaceBonesRecursive( i, false, boneReplacements, g_bonetable[i].name ); return; } MdlWarning( "Couldn't find bone %s for bonetreecollapse, skipping\n", boneTreeCollapse.GetSrcName() ); } void ConvertBoneTreeCollapsesToReplaceBones( void ) { int i; for( i = 0; i < g_ScriptLODs.Size(); i++ ) { LodScriptData_t& lod = g_ScriptLODs[i]; int j; for( j = 0; j < lod.boneTreeCollapses.Size(); j++ ) { ConvertSingleBoneTreeCollapseToReplaceBones( lod.boneTreeCollapses[j], lod.boneReplacements ); } } } /* static void PrintReplacedBones( LodScriptData_t &lod ) { int i; for( i = 0; i < lod.boneReplacements.Count(); i++ ) { printf( "%s -> %s\n", lod.boneReplacements[i].GetSrcName(), lod.boneReplacements[i].GetDstName() ); } } */ void FixupReplacedBonesForLOD( LodScriptData_t &lod ) { /* printf( "before:\n" ); PrintReplacedBones( lod ); */ bool changed; int i; int j; do { changed = false; for( i = 0; i < lod.boneReplacements.Count(); i++ ) { for( j = 0; j < lod.boneReplacements.Count(); j++ ) { if( i == j ) { continue; } if( Q_stricmp( lod.boneReplacements[i].GetSrcName(), lod.boneReplacements[j].GetDstName() ) == 0 ) { lod.boneReplacements[j].SetDstName( lod.boneReplacements[i].GetDstName() ); changed = true; } } } } while( changed ); /* printf( "after:\n" ); PrintReplacedBones( lod ); */ } void FixupReplacedBones( void ) { int i; for( i = 0; i < g_ScriptLODs.Size(); i++ ) { FixupReplacedBonesForLOD( g_ScriptLODs[i] ); } }