//========== Copyright (c) Valve Corporation. All Rights Reserved. ============ #include "mdlobjects/authphysfx.h" #include "resourcefile/resourcestream.h" #include "mathlib/femodeldesc.h" #include "rubikon/param_types.h" #include "tier1/utlstringtoken.h" #include "tier1/keyvalues.h" #include "tier2/fileutils.h" #include "tier2/p4helpers.h" #include "mathlib/disjoint_set_forest.h" //#include "mdlobjects/physmodelsource.h" #include "meshutils/mesh.h" // #include "meshutils/meshdisjointsetpartition.h" // #include "meshutils/meshconvexitydetector.h" // #include "mdlobjects/vpropbreakablelist.h" #include "mdlobjects/physmodelsource.h" #include "mathlib/femodelbuilder.h" #include "tier1/heapsort.h" #include "tier1/fmtstr.h" #include "mathlib/disjoint_set_forest.h" #include "datamodel/dmelement.h" #include "datamodel/dmelementfactoryhelper.h" #include "datamodel/idatamodel.h" #include "movieobjects/dmedag.h" #include "movieobjects/dmemesh.h" #include "movieobjects/dmejoint.h" #include "movieobjects/dmemodel.h" #include "mdlobjects/clothproxymesh.h" #include "dmeutils/dmmeshutils.h" // #ifdef _DEBUG // #define TNT_BOUNDS_CHECK // #else // //#define TNT_UNROLL_LOOPS // #endif // // #include "tnt.h" // #include "tnt_linalg.h" DEFINE_LOGGING_CHANNEL_NO_TAGS( LOG_AUTH_PHYS, "AuthPhys" ); const char *g_pTokenSeparators[] = { " ", "\t", "\n", "\r", ",", ";", "|" }; //----------------------------------------------------------------------------- static CDmeModel* LoadModelFromDMX( const char* pDMXFile ) { CDmElement* pRoot; if ( g_pDataModel->RestoreFromFile( pDMXFile, "CONTENT", NULL, &pRoot ) == DMFILEID_INVALID ) { return false; } // If this isn't a DME Model CDmeModel* pModel = NULL; if ( pRoot->IsA< CDmeModel >() ) { pModel = dynamic_cast( pRoot ); } // Try to find a CDmeModel if ( !pModel ) { pModel = pRoot->GetValueElement< CDmeModel >( "model" ); } if ( !pModel ) { pModel = pRoot->GetValueElement< CDmeModel >( "skeleton" ); } return pModel; } //----------------------------------------------------------------------------- int CompareCaseInsensitive( const CUtlString *pLeft, const CUtlString *pRight ) { return V_stricmp( pLeft->Get(), pRight->Get() ); } //----------------------------------------------------------------------------- inline const Vector GetAxisX( const Quaternion &q ) { return q.GetForward( ); } int CAuthPhysFx::FindNodeIndex( const char *pName ) { for ( int i = 0; i < m_Nodes.Count(); ++i ) { if ( !V_stricmp( pName, m_Nodes[ i ].m_Name.Get() ) ) return i; } return -1; } CAuthPhysFx::CBone *CAuthPhysFx::GetOrCreateBone( const char *pName ) { int nNode = FindNodeIndex( pName ); if ( nNode >= 0 ) { return &m_Nodes[ nNode ]; } CBone *pNewBone = &m_Nodes[ m_Nodes.AddToTail() ]; pNewBone->m_Name = pName; return pNewBone; } int DescSort( const int* a, const int* b ) { return *b - *a; } Vector Sqr( const Vector &v ) { return Vector( v.x * v.x, v.y * v.y, v.z * v.z ); } Vector SafeSqrt( const Vector &v ) { return Vector( sqrtf( MAX( 0, v.x ) ), sqrtf( MAX( 0, v.y ) ), sqrtf( MAX( 0, v.z ) ) ); } void CAuthPhysFx::AddRod( const CUtlVector< CBone > &nodes, uint nNode0, uint nNode1, float flRelaxationFactor ) { if ( nodes[ nNode0 ].m_bSimulated || nodes[ nNode1 ].m_bSimulated ) { CRod &rod = m_Rods[ m_Rods.AddToTail( ) ]; rod.m_nNodes[ 0 ] = nNode0; rod.m_nNodes[ 1 ] = nNode1; rod.m_flRelaxationFactor = flRelaxationFactor; } } static float GetClothFloat( KeyValues *pKeyValues, const char *keyName = NULL, float defaultValue = 0.0f ) { if ( keyName ) { CUtlString altKeyName( "s2:" ); altKeyName += keyName; if ( KeyValues *pKey = pKeyValues->FindKey( altKeyName ) ) { return pKey->GetFloat( ); } } return pKeyValues->GetFloat( keyName, defaultValue ); }; static bool FindKey( KeyValues *pParent, const char *pSubkey, float *pFloatOut ) { if ( KeyValues *pChild = pParent->FindKey( pSubkey ) ) { *pFloatOut = pChild->GetFloat( ( const char * )NULL, *pFloatOut ); return true; } return false; } static int GetClothInt( KeyValues *pKeyValues, const char *keyName = NULL, int defaultValue = 0 ) { if ( keyName ) { CUtlString altKeyName( "s2:" ); altKeyName += keyName; if ( KeyValues *pKey = pKeyValues->FindKey( altKeyName ) ) { return pKey->GetInt( ); } } return pKeyValues->GetInt( keyName, defaultValue ); }; static const char *GetClothString( KeyValues *pKeyValues, const char *keyName = NULL, const char *defaultValue = "" ) { if ( keyName ) { CUtlString altKeyName( "s2:" ); altKeyName += keyName; if ( KeyValues *pKey = pKeyValues->FindKey( altKeyName ) ) { return pKey->GetString( ); } } return pKeyValues->GetString( keyName, defaultValue ); }; void CreateGridNodeBases( const CNodeIdx &nodeIdx, int nRows, int nColumns, const CUtlVector< CAuthPhysFx::CBone > &nodes, CUtlVector< FeNodeBase_t > &nodeBases ) { //float sin45 = sqrtf( 0.5f ); //const Quaternion qAdjust = Quaternion( 0, sin45, 0, sin45 ) * Quaternion( 0, 0, sin45, sin45 ); const Quaternion qAdjust = Quaternion( .5, .5, .5, .5 ) * Quaternion( 1, 0, 0, 0 ); matrix3x4a_t tmTest; QuaternionMatrix( qAdjust, tmTest ); for ( int nRow = 0; nRow < nRows; ++nRow ) { for ( int nColumn = 0; nColumn < nColumns; ++nColumn ) { uint nNode = nodeIdx( nRow, nColumn ); if ( nodes[ nNode ].m_bVirtual || !nodes[ nNode ].m_bSimulated ) { continue; // don't care about virtual or static nodes } int nUp = Max( 0, nRow - 1 ), nDown = Min( nRows - 1, nRow + 1 ); int nLeft = Max( 0, nColumn - 1 ), nRight = Min( nColumns - 1, nColumn + 1 ); FeNodeBase_t &nb = nodeBases[ nodeBases.AddToTail( ) ]; V_memset( &nb, 0, sizeof( nb ) ); nb.nNode = nNode; nb.nNodeX0 = nodeIdx( nRow, nLeft ); nb.nNodeX1 = nodeIdx( nRow, nRight ); nb.nNodeY0 = nodeIdx( nUp, nColumn ); nb.nNodeY1 = nodeIdx( nDown, nColumn ); // nodeX -> right -> particle Z; this direction is gram-schmidt-orthogonalized (in Source1 using double-cross-product, in Source2 I just GS-orthogonalize it directly) // nodeY -> Down(-Up) -> particle X; this direction is unchanged nb.qAdjust = qAdjust; } } } void CAuthPhysFx::Load( const CFeModel *pFeModel ) { #ifdef _DEBUG // recreate the CFeModel CResourceStreamVM stream; const PhysFeModelDesc_t *pFeDesc = Compile( &stream ); CFeModel *pFeModel2 = stream.Allocate< CFeModel >(); Clone( pFeDesc, 0, stream.Allocate< char * >( pFeModel2->m_nCtrlCount ), pFeModel2 ); NOTE_UNUSED( pFeModel2 ); #endif } bool CAuthPhysFx::ImportDotaCloth( const char *pFileName, CPhysModelSource &physicsModel/*CUtlStringMap< int, CUtlSymbolTable > *pBoneToIndex*/ ) { const char *pContentPath = V_IsAbsolutePath( pFileName ) ? NULL : "CONTENT"; if ( !g_pFullFileSystem->FileExists( pFileName, pContentPath ) ) { return false; } KeyValues *kv = new KeyValues( "Cloth" ); if ( !kv->LoadFromFile( g_pFullFileSystem, pFileName, pContentPath ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse %s\n", pFileName ); kv->deleteThis(); return false; } CAuthClothParser parser; parser.SetBones( physicsModel ); bool bParsedOk = parser.Parse( kv ); kv->deleteThis(); if ( bParsedOk ) { Swap( parser ); } m_Constraints.Purge( ); m_Capsules.Purge( ); m_bFollowTheLead = true; // this is a bad default, necessary for compatibility with old Dota2 Source1 cloth for ( uint n = 0; n < ( uint )m_Nodes.Count(); ++n ) { if ( m_Nodes[ n ].m_flLocalForce != m_flLocalForce || m_Nodes[ n ].m_flLocalRotation != m_flLocalRotation ) { m_bUsePerNodeLocalForceAndRotation = true; // Dota2 Source1 uses different local rotation and force per cloth piece break; } } return true; } void CAuthClothParser::SetBones( CPhysModelSource &physicsModel ) { m_BoneToParent.SetCount( physicsModel.GetBoneCount( ) ); for ( int nBone = 0; nBone < physicsModel.GetBoneCount( ); ++nBone ) { m_BoneToIndex.Insert( physicsModel.GetBoneNameByIndex( nBone ), nBone ); m_BoneToParent[ nBone ] = physicsModel.GetParentJoint( nBone ); } m_ModelBoneToNode.SetCount( physicsModel.GetBoneCount( ) ); m_ModelBoneToNode.FillWithValue( -1 ); // we'll use the "bindpose" or similar animation, first frame, if we can /* const char * idleAnimations[] = { "bindpose", "idle", "run" }; for ( int i = 0; i < ARRAYSIZE( idleAnimations ); ++i ) { if ( physicsModel.GetAnimFrame( idleAnimations[ i ], 0, &transforms ) ) { break; // we'll use this } } if( m_BoneTransforms.Count() < physicsModel.GetBoneCount() ) { Log_Msg( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " m_BoneTransforms.Count() %d < physicsModel.GetBoneCount() %d\n", m_BoneTransforms.Count(), physicsModel.GetBoneCount() ); Log_Msg( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Could not find bindpose animation, using actual bind pose which dota s1 does NOT use when building cloth. The resulting cloth will be different from Dota Source1 cloth\n" ); // couldn't get the first frame of one of ethalon poses physicsModel.GetBindPoseWorldTransforms( m_BoneTransforms ); AdjustLegacyDotaOrientation( m_BoneTransforms ); } */ physicsModel.GetBindPoseWorldTransforms( m_BoneTransforms ); AdjustLegacyDotaOrientation( m_BoneTransforms ); } int CAuthClothParser::FindNodeByName( const char *pName ) { // fill out the node map for ( ; m_nNodeNameMapNodes < m_Nodes.Count( ); m_nNodeNameMapNodes++ ) { m_NodeNameMap[ m_Nodes[ m_nNodeNameMapNodes ].m_Name ] = m_nNodeNameMapNodes; } UtlSymId_t hFind = m_NodeNameMap.Find( pName ); if ( hFind != UTL_INVAL_SYMBOL ) { return m_NodeNameMap[ hFind ]; } else { return -1; } } bool CAuthClothParser::Parse( KeyValues *kv ) { m_nDefaultCompatibilityMode = GetClothInt( kv, "compatibilityMode", 3 ); m_bFollowTheLead = kv->GetBool( "followTheLead", true ); m_nNodeNameMapNodes = 0; for ( KeyValues *pSubKey = kv->GetFirstSubKey( ); pSubKey != NULL; pSubKey = pSubKey->GetNextKey( ) ) { const char *pSubkeyName = pSubKey->GetName( ); if ( !V_stricmp( pSubkeyName, "Defaults" ) ) { if ( !ParseDefaults( pSubKey ) ) return false; } else if ( !V_stricmp( pSubKey->GetName( ), "Cloth" ) ) { if ( !ParsePiece( pSubKey ) ) return false; } } ReconstructHierarchy( ); return true; } void CAuthClothParser::ReconstructHierarchy() { // reconstruct the hierarchy. We could just assume that the hierarchy goes along the columns to previous row, but this pass // will allow any arbitrary topology of the cloth bones for ( int nModelBone = 0; nModelBone < m_ModelBoneToNode.Count( ); ++nModelBone ) { int nChildNode = m_ModelBoneToNode[ nModelBone ]; if ( nChildNode >= 0 ) { int nModelBoneParent = m_BoneToParent[ nModelBone ]; if ( nModelBoneParent >= 0 ) { int nParentNode = m_ModelBoneToNode[ nModelBoneParent ]; if ( nParentNode >= 0 ) { int &refParent = m_Nodes[ nChildNode ].m_nParent; if ( refParent >= 0 && refParent != nParentNode ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Node %s has deducted parent %s, but skeleton parent is %s. Resolving conflict in favor of the skeleton hierarchy.\n", m_Nodes[ nChildNode ].m_Name.Get( ), m_Nodes[ refParent ].m_Name.Get( ), m_Nodes[ nParentNode ].m_Name.Get( ) ); } refParent = nParentNode; } } } } } bool CAuthClothParser::ParseDefaults( KeyValues *pSubKey ) { m_flDefaultSurfaceStretch = GetClothFloat( pSubKey, "SurfaceStretch", 0.0f ); m_flDefaultThreadStretch = GetClothFloat( pSubKey, "ThreadStretch", 0.0f ); return true; } CBoneParseParams::CBoneParseParams( KeyValues *pSubKey, int nCompatibilityMode ) { m_pBonePrefix = GetClothString( pSubKey, "BonePrefix" ); m_nNominalColumnCount = GetClothInt( pSubKey, "columns" ); m_nRowCount = GetClothInt( pSubKey, "rows" ); m_flWorldFriction = GetClothFloat( pSubKey, "WorldFriction", 0.35f ); m_flAnimationForceAttraction = GetClothFloat( pSubKey, "AnimationForceAttraction" ); m_flAnimationVertexAttraction = GetClothFloat( pSubKey, "AnimationVertexAttraction" ); m_flDamping = GetClothFloat( pSubKey, "damping" ); // this is velocity-acceleration damping: ( damping force ) = -flDamping * ( velocity ); velocity *= 1 - flDamping * dt / m m_flFixedPointDamping = GetClothFloat( pSubKey, "FixedPointDamping", 0.0f ); m_flFollowRootEnd = m_flFollowRootBegin = m_flFixedPointDamping; m_flSpringStretchiness = GetClothFloat( pSubKey, "SpringStretchiness", 0.0f ); // stretchiness meant opposite things as constructor parameter and internal member variable. This is how Dota Source1 implementation goes. I'll rename the internal parameter to something else in S2 to distinguish. // Also: CClothSpring::m_flStretchiness = 1 - SpringStretchiness from keyvalue file... yeah.. m_flRelaxationFactor = clamp( 1.0f - m_flSpringStretchiness, 0.0f, 1.0f ); // the stretch force is computed after applying SpringStretchiness (flRelaxationFactor) in Source1 cloth, so we'll follow the same pattern to be true to the original: we'll premultiply it when creating CString // in the new versions, we shouldn't support stretch force. We should use damping instead. m_flStretchForce = GetClothFloat( pSubKey, "StretchForce", nCompatibilityMode >= 2 ? 1.0f : 0.0f ); m_flStructSpringConstant = GetClothFloat( pSubKey, "StructSpringConstant", 4.0f ); m_flStructSpringDamping = GetClothFloat( pSubKey, "StructSpringDamping", 0.6f ); float flDefaultGravity = nCompatibilityMode >= 1 ? 20 : 380; m_flGravityScale = GetClothFloat( pSubKey, "gravity_scale", 0.0f ); if ( const char *pGravityString = GetClothString( pSubKey, "gravity", NULL ) ) { Vector vGravityDirection = Vector( 0, 0, -flDefaultGravity ); if ( 3 == sscanf( pGravityString, "%g %g %g", &vGravityDirection.x, &vGravityDirection.y, &vGravityDirection.z ) ) { if ( m_flGravityScale == 0.0f ) { m_flGravityScale = vGravityDirection.NormalizeInPlace(); } else { m_flGravityScale *= vGravityDirection.NormalizeInPlace(); } if ( vGravityDirection.z > 0 ) { // Everything further assumes negative gravity direction (towards -Z) vGravityDirection = -vGravityDirection; m_flGravityScale = -m_flGravityScale; } } } else { m_flGravityScale = flDefaultGravity; } if ( const char *pStabilizeAnim = GetClothString( pSubKey, "stabilizeAnim", NULL ) ) { switch ( sscanf( pStabilizeAnim, "%g %g", &m_flFollowRootBegin, &m_flFollowRootEnd ) ) { case 2: break; // ok, we parsed both case 0: Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse stabilizeAnim extended keyword parameters, leaving stabilization at the default. Fixed point damping = %g\n", m_flFixedPointDamping ); break; case 1: m_flFollowRootEnd = m_flFollowRootBegin; break; } } m_bIsRopeS1 = nCompatibilityMode >= 3 && m_nNominalColumnCount == 1; m_nVirtColumnCount = m_nNominalColumnCount; if ( m_bIsRopeS1 ) { m_nVirtColumnCount++; // add an extra column, like in Source1 rope simulation } } void CBoneParseParams::ApplyDefaultParams( CBone &node ) { // apply node settings that are not per node node.m_Integrator.flPointDamping = m_flDamping; // Important fix: the springs (particularly animation force attraction) in Source1 were tuned to the "mass" of cloth particles (see CClothParticleState::Integrate() , flDeltaTimeMass = flFrameTime * pClothParticle->GetInverseMass() ) // Source2 computes accelerations directly, so we need to pre-divide by mass here. I forgot to do that in CL 2322760, fixed in 2532503 in femodelbuilder.cpp:2220 node.m_Integrator.flAnimationForceAttraction = m_flAnimationForceAttraction; node.m_Integrator.flAnimationVertexAttraction = m_flAnimationVertexAttraction; node.m_Integrator.flGravity = m_flGravityScale; node.m_flWorldFriction = m_flWorldFriction; node.m_flLegacyStretchForce = m_flStretchForce; } bool CAuthClothParser::ParsePiece( KeyValues *pSubKey ) { m_nCompatibilityMode = GetClothInt( pSubKey, "compatibilityMode", m_nDefaultCompatibilityMode ); if ( m_nCompatibilityMode >= 2 ) { m_bUninertialRods = true; } if ( m_nCompatibilityMode >= 1 ) { m_bExplicitMasses = true; m_bUnitlessDamping = false; // in Dota2 Source1 damping was a coefficient for computing force, not acceleration. So the same damping would affect nodes with differing masses differently } CBoneParseParams parseParm( pSubKey, m_nCompatibilityMode ); m_flLocalForce = GetClothFloat( pSubKey, "LocalForce", m_flLocalForce ); m_flLocalRotation = 1.0f - GetClothFloat( pSubKey, "LocalRotation", 1.0f - m_flLocalRotation ); bool bAnonymousPiece = !parseParm.m_pBonePrefix || !*parseParm.m_pBonePrefix || parseParm.m_nNominalColumnCount <= 0 || parseParm.m_nRowCount <= 0; NOTE_UNUSED( bAnonymousPiece ); parseParm.m_pCollisionSpheres = &m_CollisionSpheres; parseParm.m_pCollisionPlanes = &m_CollisionPlanes; if ( !bAnonymousPiece ) { int nNodeBaseIndex = m_Nodes.AddMultipleToTail( parseParm.m_nRowCount * parseParm.m_nVirtColumnCount ); for ( int nNode = nNodeBaseIndex; nNode < m_Nodes.Count(); ++nNode ) { m_Nodes[ nNode ].m_flLocalForce = m_flLocalForce; m_Nodes[ nNode ].m_flLocalRotation = m_flLocalRotation; } CNodeIdx nodeIdx( nNodeBaseIndex, parseParm.m_nRowCount, parseParm.m_nVirtColumnCount ); if ( !ParseLegacyDotaNodeGrid( pSubKey, parseParm, nodeIdx ) ) return false; if ( !CreateLegacyDotaRodGrid( parseParm, nodeIdx, m_nCompatibilityMode ) ) return false; } if ( m_nCompatibilityMode <= 0 ) { ParseExplicitDefinitions( pSubKey, parseParm ); } return true; } bool Preparse( KeyValues *kv, CAuthPhysFx::CCollisionSphere &sphere ) { sphere.m_bInclusive = 0 == V_stricmp( kv->GetName(), "in_sphere" ); if ( kv->GetBool( "inside" ) || kv->GetBool( "inclusive" ) ) sphere.m_bInclusive = true; if ( kv->GetBool( "outside" ) || kv->GetBool( "exclusive" ) ) sphere.m_bInclusive = false; FindKey( kv, "radius", &sphere.m_flRadius ); const char *pCenter = kv->GetString( "center", NULL ); if ( !pCenter ) { pCenter = kv->GetString( "offset", NULL ); } if ( pCenter ) { if ( 3 != sscanf( pCenter, "%g %g %g", &sphere.m_vOrigin.x, &sphere.m_vOrigin.y, &sphere.m_vOrigin.z ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse sphere center %s\n", pCenter ); } } if ( sphere.m_flRadius < 1e-5f && !sphere.m_bInclusive ) { return false; // no collision with this sphere } return true; } bool Preparse( KeyValues *kv, CAuthPhysFx::CCollisionPlane &plane ) { FindKey( kv, "offset", &plane.m_Plane.m_flOffset ); if ( const char *pNormal = kv->GetString( "normal", NULL ) ) { if ( 3 != sscanf( pNormal, "%g %g %g", &plane.m_Plane.m_vNormal.x, &plane.m_Plane.m_vNormal.y, &plane.m_Plane.m_vNormal.z ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse plane normal %s\n", pNormal ); } else { plane.m_Plane.m_vNormal.NormalizedSafe( Vector( 1, 0, 0 ) ); } } return true; } template void CAuthClothParser::ParseExplicitColl( KeyValues *kv, CBoneParseParams &parseParm, CUtlVector< T > &collArray ) { T coll; if ( !Preparse( kv, coll ) ) return; if ( const char *pParent = kv->GetString( "parent", NULL ) ) { coll.m_nParentBone = FindNodeByName( pParent ); if ( coll.m_nParentBone < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot find coll parent bone %s\n", pParent ); return; } } if ( const char *pChild = kv->GetString( "child", NULL ) ) { coll.m_nChildBone = FindNodeByName( pChild ); if ( coll.m_nChildBone < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot find coll child bone %s\n", pChild ); } } if ( coll.m_nChildBone < 0 ) coll.m_nChildBone = coll.m_nParentBone; if ( coll.m_nParentBone < 0 ) coll.m_nParentBone = coll.m_nChildBone; if ( coll.m_nParentBone < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Collision Sphere has no parent bone\n" ); return; } if ( const char *pChildren = kv->GetString( "children", NULL ) ) { CUtlStringList tokens( pChildren, g_pTokenSeparators, ARRAYSIZE( g_pTokenSeparators ) ); for ( int i = 0; i < tokens.Count( ); ++i ) { const char *pChildName = tokens[ i ]; coll.m_nChildBone = FindNodeByName( pChildName ); if ( coll.m_nChildBone >= 0 ) { collArray.AddToTail( coll ); } else { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot find child %s\n", pChildName ); } } } else { Assert( coll.m_nChildBone >= 0 ); collArray.AddToTail( coll ); } } void CAuthClothParser::ParseExplicitDefinitions( KeyValues *pCloth, CBoneParseParams &parseParm ) { for ( KeyValues *pSubKey = pCloth->GetFirstSubKey( ); pSubKey != NULL; pSubKey = pSubKey->GetNextKey( ) ) { const char *pSubkeyName = pSubKey->GetName( ); if ( !V_stricmp( pSubkeyName, "node" ) ) { ParseExplicitNode( pSubKey, parseParm ); } else if ( !V_stricmp( pSubkeyName, "tri" ) || !V_stricmp( pSubkeyName, "quad" ) || !V_stricmp( pSubkeyName, "elem" ) ) { ParseExplicitElem( pSubKey, parseParm ); } else if ( !V_stricmp( pSubkeyName, "sphere" ) || !V_stricmp( pSubkeyName, "in_sphere" ) || !V_stricmp( pSubkeyName, "ex_sphere" ) ) { ParseExplicitColl( pSubKey, parseParm, m_CollisionSpheres ); } else if ( !V_stricmp( pSubkeyName, "plane" ) ) { ParseExplicitColl( pSubKey, parseParm, m_CollisionPlanes ); } } } void CAuthClothParser::ParseExplicitNode( KeyValues *kv, CBoneParseParams &parseParm ) { CBone bone; bone.m_Name = kv->GetString( "name" ); parseParm.ApplyDefaultParams( bone ); FindKey( kv, "mass", &bone.m_flMass ); FindKey( kv, "gravity", &bone.m_Integrator.flGravity ); FindKey( kv, "damping", &bone.m_Integrator.flPointDamping ); if ( const char *pParent = kv->GetString( "parent", NULL ) ) { bone.m_nParent = FindNodeByName( pParent ); if ( bone.m_nParent < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Explicit Node %s - cannot find parent %s\n", bone.m_Name.Get( ), pParent ); } } if ( const char *pFollowParent = kv->GetString( "followParent", NULL ) ) { bone.m_nFollowParent = FindNodeByName( pFollowParent ); if ( bone.m_nFollowParent < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Explicit Node %s - cannot find followParent %s\n", bone.m_Name.Get( ), pFollowParent ); } } if ( FindKey( kv, "followWeight", &bone.m_flFollowWeight ) && bone.m_flFollowWeight > 0 && bone.m_nParent >= 0 && bone.m_nFollowParent < 0 ) { // if there's parent, followWeight, but no followParent, we can assume followParent to be = parent bone.m_nFollowParent = bone.m_nParent; } if ( const char *pOffset = kv->GetString( "offset", NULL ) ) { Vector vOffset; if ( sscanf( pOffset, "%g %g %g", &vOffset.x, &vOffset.y, &vOffset.z ) != 3 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Explicit node %s - cannot parse offset %s\n", bone.m_Name.Get( ), pOffset ); } else if ( bone.m_nParent < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Explicit node %s has offset %s but no parent\n", bone.m_Name.Get( ), pOffset ); } else { const CTransform &parentXform = m_Nodes[ bone.m_nParent ].m_Transform; bone.m_Transform.m_orientation = parentXform.m_orientation; bone.m_Transform.m_vPosition = TransformPoint( parentXform, vOffset ); } } if ( const char *pFlags = kv->GetString( "flags", NULL ) ) { parseParm.ApplyDotaFlags( bone, pFlags ); } if ( kv->GetBool( "static" ) ) { bone.m_bSimulated = false; bone.m_bFreeRotation = false; } if ( const char *pLock = kv->GetString( "lock", NULL ) ) { CUtlStringList tokens( pLock, g_pTokenSeparators, ARRAYSIZE( g_pTokenSeparators ) ); for ( int i = 0; i < tokens.Count( ); ++i ) { if ( !V_stricmp( tokens[ i ], "position" ) ) { bone.m_bSimulated = false; } else if ( !V_stricmp( tokens[ i ], "rotation" ) ) { bone.m_bFreeRotation = false; } else { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Invalid lock parameter %s in bone %s, ignoring\n", tokens[ i ], bone.m_Name.Get() ); } } } if ( !bone.m_bFreeRotation && bone.m_bSimulated ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Bone %s rotation is locked, but the bone is simulated, which is not useful\n", bone.m_Name.Get( ) ); } int nNewNode = m_Nodes.AddToTail( bone ); TieModelToNewNode( nNewNode ); } void CAuthClothParser::ParseExplicitElem( KeyValues *kv, CBoneParseParams &parseParm ) { const char *pNodes = kv->GetString( "nodes", NULL ); if ( !pNodes ) { pNodes = kv->GetString( ); } if ( pNodes ) { CUtlStringList tokens( pNodes, g_pTokenSeparators, ARRAYSIZE( g_pTokenSeparators ) ); CQuad quad; int nNodeCount = 0; for ( int i = 0; i < tokens.Count( ); ++i ) { int nNode = FindNodeByName( tokens[ i ] ); if ( nNode >= 0 ) { quad.m_nNodes[ nNodeCount ] = nNode; ++nNodeCount; } else { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cloth cannot find element node '%s', ignoring!\n", tokens[ i ] ); } } if ( nNodeCount >= 3 ) { for ( int i = nNodeCount; i < 4; ++i ) quad.m_nNodes[ i ] = quad.m_nNodes[ nNodeCount - 1 ]; m_Quads.AddToTail( quad ); } else { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cloth element must have 3 or 4 nodes: %s\n", pNodes ); } } } // Return: true if the node was successfully tied to an existing bone bool CAuthClothParser::TieModelToNewNode( int nNewNode ) { CBone &node = m_Nodes[ nNewNode ]; UtlSymId_t nFindBone = m_BoneToIndex.Find( node.m_Name.Get( ) ); if ( nFindBone != UTL_INVAL_SYMBOL ) { int nBoneIndex = m_BoneToIndex[ nFindBone ]; node.m_Transform = m_BoneTransforms[ nBoneIndex ]; // this bone hasn't been mapped to an earlier node, it's original bone-node, map it now m_ModelBoneToNode[ nBoneIndex ] = nNewNode; return true; } else { // we'll figure the parent out later node.m_bVirtual = true; // this node is virtual. it has no bone name. return false; } } bool CAuthClothParser::ParseLegacyDotaNodeGrid( KeyValues *pSubKey, CBoneParseParams &parseParm, const CNodeIdx &nodeIdx ) { CVarBitVec nodesInitialized( m_Nodes.Count( ) ); // add the grid (or one strand) of Nodes for ( int nColumn = 0; nColumn < parseParm.m_nVirtColumnCount; ++nColumn ) { for ( int nRow = 0; nRow < parseParm.m_nRowCount; ++nRow ) { const char *pRowFlags = GetClothString( pSubKey, CFmtStr( "r%d", nRow ).Get( ) ); const char *pColumnFlags = GetClothString( pSubKey, CFmtStr( "c%d", nColumn ).Get( ) ); const char *pNodeFlags = GetClothString( pSubKey, CFmtStr( "r%dc%d", nRow, nColumn ).Get( ) ); int nNewNode = nodeIdx( nRow, nColumn ); CBone &node = m_Nodes[ nNewNode ]; // There is no possibility to create free-rotating static nodes in dota2 source1 node.m_bFreeRotation = false; // then, apply sequentially settings: per column; per individual node parseParm.m_nBoneIndex = nNewNode; if ( parseParm.m_bIsRopeS1 && nColumn ) { parseParm.m_bPrevColumnParent = true; // for the ropes, we parent virtual column #1 to the previous column #0 parseParm.m_vOffset = Vector( 0, -20, 0 ); // NOTE: OS offset! node.m_bOsOffset = true; node.m_bVirtual = true; } else { parseParm.m_bPrevColumnParent = false; parseParm.m_vOffset = vec3_origin; } node.m_Name.Format( "%sr%dc%d", parseParm.m_pBonePrefix, nRow, nColumn ); if ( nRow > 0 && node.m_bSimulated ) { // note: following happensin Dota2 Source1 in CClothModelPiece::SetupBone() in cloth_system.cpp#36:3412, the whole column follows the static parent // this happens for ropes and cloths. node.m_nFollowParent = nodeIdx( 0, parseParm.m_bIsRopeS1 ? 0 : nColumn ); node.m_flFollowWeight = nRow * ( parseParm.m_flFollowRootEnd - parseParm.m_flFollowRootBegin ) / ( parseParm.m_nRowCount - 1 ) + parseParm.m_flFollowRootBegin; } if ( !TieModelToNewNode( nNewNode ) ) { //node.m_Name += "(virtual)"; // the name doesn't matter, except for debugging, so marking it clearly as a virtual bone name } parseParm.ApplyDefaultParams( node ); parseParm.ApplyDotaFlags( node, pRowFlags ); parseParm.ApplyDotaFlags( node, pColumnFlags ); parseParm.ApplyDotaFlags( node, pNodeFlags ); if ( !node.m_bSimulated ) { node.m_Integrator.flPointDamping = parseParm.m_flFixedPointDamping * 60; // Dota cloth didn't take variability of time into account, we will } // pre-divide by nominal mass if ( node.m_flMass > 0.0333f ) { node.m_Integrator.flAnimationForceAttraction /= node.m_flMass; if ( node.m_bSimulated ) { // NOTE: the damping gets divided by mass later in the FE model builder // node.m_Integrator.flPointDamping /= node.m_flMass; } } if ( parseParm.m_bPrevColumnParent ) { if ( nColumn > 0 ) { node.m_nParent = nodeIdx( nRow, nColumn - 1 ); } } else { if ( nRow > 0 ) { node.m_nParent = nodeIdx( nRow - 1, nColumn ); } } if ( node.m_bVirtual ) { if ( node.m_nParent < 0 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Virtual particle %s has no parent to latch onto\n", node.m_Name.Get( ) ); return false; // we tried to find a parent bone , but there was none. We can't have a virtual particle without a clear parent bone to at least define its relaxed position } if ( !nodesInitialized.IsBitSet( node.m_nParent ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Virtual particle %s has parent that hasn't been initialized yet, ordering problem\n", node.m_Name.Get( ) ); return false; } if ( parseParm.m_vOffset.Length( ) < 1e-6f ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Virtual particle %s has no offset to its parent %s, it's useless, please just reuse %s\n", node.m_Name.Get( ), m_Nodes[ node.m_nParent ].m_Name.Get( ), node.m_Name.Get( ) ); } const CTransform &parentXform = m_Nodes[ node.m_nParent ].m_Transform; node.m_Transform.m_orientation = parentXform.m_orientation; if ( node.m_bOsOffset ) { node.m_Transform.m_vPosition = parentXform.m_vPosition + parseParm.m_vOffset; } else { node.m_Transform.m_vPosition = TransformPoint( parentXform, parseParm.m_vOffset ); } } nodesInitialized.Set( nNewNode ); } } return true; } void CAuthClothParser::AddDotaRod( uint nNode0, uint nNode1, CBoneParseParams &parseParm ) { AddRod( m_Nodes, nNode0, nNode1, parseParm.m_flRelaxationFactor ); // springs do not evaluate in Source1: the only call to Evaluate is after a return; // springs do ResolveStretch in Source1, but the effect is simply applying velocity of vDir * ( ( flDistance - flRestLength ) * flStretchiness * flStretchForce) in the direction vDir of fixing up the rod. // This can be accounted for in the node damping: since all nodes in a cloth piece have the same flStretchiness * flStretchForce, and verlet integrator will by default cause the delta velocity = 60 * ( flDistance - flRestLength ) //m_Springs.AddToTail( CSpring( nNode0, nNode1, parseParm.m_flStructSpringConstant, parseParm.m_flStructSpringDamping, parseParm.m_flStretchForce, parseParm.m_flRelaxationFactor ) ); } bool CAuthClothParser::CreateLegacyDotaRodGrid( CBoneParseParams &parseParm, const CNodeIdx &nodeIdx, int nCompatibilityMode ) { // add finite elements (either rods or quads) that will simulate the cloth if ( parseParm.m_nVirtColumnCount == 1 ) { // special case, single-strand cloth (rope) for ( int nRow = 1; nRow < parseParm.m_nRowCount; ++nRow ) { uint nNode0 = nodeIdx( nRow - 1, 0 ), nNode1 = nodeIdx( nRow, 0 ); AddDotaRod( nNode0, nNode1, parseParm ); if ( nRow >= 2 && /*bUseBendSprings*/false ) { uint nNodePrev = nodeIdx( nRow - 2, 0 ); AddDotaRod( nNodePrev, nNode1, parseParm ); } } } else { for ( int nRow = 1; nRow < parseParm.m_nRowCount; ++nRow ) { for ( int nColumn = 1; nColumn < parseParm.m_nVirtColumnCount; ++nColumn ) { uint nNode00 = nodeIdx( nRow - 1, nColumn - 1 ); uint nNode01 = nodeIdx( nRow - 1, nColumn ); uint nNode11 = nodeIdx( nRow, nColumn ); uint nNode10 = nodeIdx( nRow, nColumn - 1 ); if ( m_nCompatibilityMode == 0 || ( m_nCompatibilityMode == 1 && nRow >= 2 ) ) { CQuad &quad = m_Quads[ m_Quads.AddToTail( ) ]; quad.m_nNodes[ 0 ] = nNode00; quad.m_nNodes[ 1 ] = nNode01; quad.m_nNodes[ 2 ] = nNode11; quad.m_nNodes[ 3 ] = nNode10; // in compat 1, we skip the first row of quads; in compat 2+, we skip all quads } else { // for compatibility with previous version of the solver, simplify to rods // // "Structural springs", in source1 cloth lingo // AddDotaRod( nNode00, nNode01, parseParm ); AddDotaRod( nNode00, nNode10, parseParm ); if ( nColumn + 1 == parseParm.m_nVirtColumnCount ) { // last column AddDotaRod( nNode01, nNode11, parseParm ); } if ( nRow + 1 == parseParm.m_nRowCount ) { // last row AddDotaRod( nNode10, nNode11, parseParm ); } // // "Shear springs" // if ( false ) { AddDotaRod( nNode00, nNode11, parseParm ); AddDotaRod( nNode01, nNode10, parseParm ); } } } } CreateGridNodeBases( nodeIdx, parseParm.m_nRowCount, parseParm.m_nVirtColumnCount, m_Nodes, m_PresetNodeBases ); } return true; } static const char *s_pszTokenDelimiter = " ,|\t\r\n"; bool FloatToken( float &x ) { const char *pToken = strtok( NULL, s_pszTokenDelimiter ); if ( !pToken ) return false; x = atof( pToken ); return true; } bool VectorToken( Vector &v ) { return FloatToken( v.x ) && FloatToken( v.y ) && FloatToken( v.z ); } bool KeywordToken( const char *pKeyword ) { const char *pToken = strtok( NULL, s_pszTokenDelimiter ); if ( !pToken ) return false; return V_stricmp( pToken, pKeyword ) == 0; } bool CBoneParseParams::ApplyDotaFlags( CAuthPhysFx::CBone &bone, const char *pszParms ) { if ( pszParms[ 0 ] == 0 ) { return true; } int nParamLength = V_strlen( pszParms ); if ( nParamLength > 1024 ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Keyvalue parameter too long in %s\n", bone.m_Name.Get( ) ); return false; } char *pszParmsCopy = ( char * ) stackalloc( nParamLength + 1 ); if ( pszParmsCopy != NULL ) { V_strcpy( pszParmsCopy, pszParms ); char *pszToken = strtok( pszParmsCopy, s_pszTokenDelimiter ); while ( pszToken != NULL ) { if ( V_stricmp( pszToken, "fixed" ) == 0 ) { bone.m_bSimulated = false; } else if ( V_stricmp( pszToken, "world" ) == 0 ) { bone.m_bNeedsWorldCollision = true; } else if ( V_stricmp( pszToken, "mass" ) == 0 ) { float flValue; if( FloatToken( flValue ) ) { if ( flValue > 0.0f ) { bone.m_flMass = flValue; bone.m_bHasMassOverride = true; bone.m_bSimulated = true; } else { bone.m_flMass = 1.0f; // take default mass; todo: find out what "mass 0" is actually supposed to mean bone.m_bHasMassOverride = false; bone.m_bSimulated = true; } } else { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse mass in %s\n", bone.m_Name.Get() ); return false; } } else if ( V_stricmp( pszToken, "damping" ) == 0 ) { if ( !FloatToken( bone.m_Integrator.flPointDamping ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse damping in %s\n", bone.m_Name.Get( ) ); return false; } } else if ( V_stricmp( pszToken, "gravity" ) == 0 ) { if ( !FloatToken( bone.m_Integrator.flGravity ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse gravity in %s\n", bone.m_Name.Get( ) ); return false; } } else if ( V_stricmp( pszToken, "offsetx" ) == 0 ) { if ( !FloatToken( m_vOffset.x ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse offsetx in %s\n", bone.m_Name.Get( ) ); return false; } } else if ( V_stricmp( pszToken, "offsety" ) == 0 ) { if ( !FloatToken( m_vOffset.y ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse offsety in %s\n", bone.m_Name.Get( ) ); return false; } } else if ( V_stricmp( pszToken, "offsetz" ) == 0 ) { if ( !FloatToken( m_vOffset.z ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse offsetz in %s\n", bone.m_Name.Get( ) ); return false; } } else if ( V_stricmp( pszToken, "prev_col_parent" ) == 0 ) { m_bPrevColumnParent = true; } else if ( V_stricmp( pszToken, "prev_row_parent" ) == 0 ) { m_bPrevColumnParent = false; } else if ( V_stricmp( pszToken, "stabilizeAnim" ) == 0 ) { if ( !FloatToken( bone.m_flFollowWeight ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse stabilizeAnim in %s\n", bone.m_Name.Get( ) ); return false; } } else if ( V_stricmp( pszToken, "in_sphere" ) == 0 || V_stricmp( pszToken, "ex_sphere" ) == 0 ) { CCollisionSphere ce; if ( !VectorToken( ce.m_vOrigin ) || !KeywordToken( "r" ) || !FloatToken( ce.m_flRadius ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse %s in %s\n", pszToken, bone.m_Name.Get( ) ); return false; } ce.m_bInclusive = V_stricmp( pszToken, "in_sphere" ) == 0; ce.m_nChildBone = ce.m_nParentBone = m_nBoneIndex; m_pCollisionSpheres->AddToTail( ce ); } else if ( V_stricmp( pszToken, "plane" ) == 0 ) { CCollisionPlane cp; if ( !VectorToken( cp.m_Plane.m_vNormal ) || !KeywordToken( "d" ) || !FloatToken( cp.m_Plane.m_flOffset ) ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse %s in %s\n", pszToken, bone.m_Name.Get( ) ); return false; } cp.m_nChildBone = cp.m_nParentBone = m_nBoneIndex; m_pCollisionPlanes->AddToTail( cp ); } /*else if ( V_stricmp( pszToken, "0" ) == 0 ) { bone.m_flMass = 1.0f; // take default mass; it seems impossible now to find out what "mass 0" was actually supposed to mean originally bone.m_bHasMassOverride = false; bone.m_bSimulated = true; }*/ else { float flValue = V_atof( pszToken ); if ( flValue != 0.0f ) { // just a number is ignored in source1; Some artists (e.g. JO) thought it should means mass, but it doesn't mean anything in source1 cloth, so for compatibility I'll ignore it here, too //bone.m_flMass = flValue; //bone.m_bHasMassOverride = true; //bone.m_bSimulated = true; } else { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING " Cannot parse %s in %s\n", pszToken, bone.m_Name.Get( ) ); return false; } } if ( pszToken ) { pszToken = strtok( NULL, s_pszTokenDelimiter ); } } } return true; } int CAuthPhysFx::GetSimParticleCount()const { int nCount = 0; for( int nNode = 0; nNode < m_Nodes.Count(); ++nNode ) { if( m_Nodes[nNode].m_bSimulated ) { nCount++; } } return nCount; } float CAuthPhysFx::GetRodLength( int nRod )const { const CRod & rod = m_Rods[ nRod ]; if ( rod.m_bExplicitLength ) { return rod.m_flLength; } else { return ( m_Nodes[ rod.m_nNodes[ 0 ] ].m_Transform.m_vPosition - m_Nodes[ rod.m_nNodes[ 1 ] ].m_Transform.m_vPosition ).Length(); // implicit length } } bool CAuthPhysFx::IsSimilarTo( const CFeModel *pFeModel )const { if ( !pFeModel ) { return IsEmpty(); } if ( int( pFeModel->m_nNodeCount ) != m_Nodes.Count() ) return false; if ( int( pFeModel->m_nQuadCount ) != m_Quads.Count() ) return false; if ( int( pFeModel->m_nRodCount ) != m_Rods.Count() ) return false; if ( int( pFeModel->m_nCtrlOffsets ) != m_CtrlOffsets.Count() ) return false; for ( int i = 0; i < m_Nodes.Count(); ++i ) { if ( m_Nodes[ i ].m_Name != pFeModel->GetNodeName( i ) ) return false; } return true; } CFeModelBuilder::BuildNode_t CAuthPhysFx::CBone::AsBuildNode()const { CFeModelBuilder::BuildNode_t node; const CBone &bone = *this; node.pName = bone.m_Name.Get(); node.transform = bone.m_Transform; node.nParent = bone.m_nParent; node.nFollowParent = bone.m_nFollowParent; node.flFollowWeight = bone.m_flFollowWeight; if ( bone.m_bSimulated && bone.m_flMass > 0 ) { node.flMassMultiplier = bone.m_flMass; node.invMass = 1.0f / bone.m_flMass; node.bSimulated = true; node.bForceSimulated = bone.m_bForceSimulated; } else { node.flMassMultiplier = 0; node.invMass = 0; node.bSimulated = false; } node.flMassBias = bone.m_flMassBias; node.bFreeRotation = bone.m_bFreeRotation; node.bAnimRotation = bone.m_bAnimRotation; node.bVirtual = bone.m_bVirtual; node.bNeedNodeBase = bone.m_bNeedNodeBase; node.bOsOffset = bone.m_bOsOffset; node.bMassMultiplierGlobal = bone.m_bHasMassOverride; node.flSlack = 0; node.integrator = bone.m_Integrator; node.flLegacyStretchForce = bone.m_flLegacyStretchForce; node.bWorldCollision = bone.m_bNeedsWorldCollision; node.flWorldFriction = bone.m_flWorldFriction; node.flGroundFriction = bone.m_flGroundFriction; node.flCollisionRadius = bone.m_flCollisionRadius; node.flLocalRotation = bone.m_flLocalRotation; node.flLocalForce = bone.m_flLocalForce; node.nCollisionMask = bone.m_nCollisionMask; return node; } CFeModelBuilder::BuildElem_t CAuthPhysFx::CQuad::AsBuildElem() const { CFeModelBuilder::BuildElem_t elem; const CAuthPhysFx::CQuad &quad = *this; elem.nNode[ 0 ] = quad.m_nNodes[ 0 ]; elem.nNode[ 1 ] = quad.m_nNodes[ 1 ]; elem.nNode[ 2 ] = quad.m_nNodes[ 2 ]; elem.nNode[ 3 ] = quad.m_nNodes[ 3 ]; elem.nStaticNodes = 0; elem.nRank = 0; elem.flSlack = 0; return elem; } CLockedResource< PhysFeModelDesc_t > CAuthPhysFx::Compile( CResourceStream *pStream, const CVClothProxyMeshOptions *pOptions )const { CFeModelBuilder builder; if ( pOptions ) { builder.m_bNeedBacksolvedBasesOnly = pOptions->m_bDriveMeshesWithBacksolvedJointsOnly; } // Note: for now I'm skipping (pre-applying) the mapping from ctrl to node; // that mapping will be set to NULL. So, the indices and count of nodes in builder.m_Nodes and m_NOdes is different builder.EnableIdentityCtrlOrder( ); const int nParentBodyCount = /*pParent->GetBodyCount()*/ 0; builder.m_Nodes.EnsureCapacity( m_Nodes.Count() + nParentBodyCount ); builder.m_Nodes.SetCount( m_Nodes.Count() ); builder.m_Elems.EnsureCapacity( m_Quads.Count( ) ); builder.m_Springs.EnsureCapacity( m_Springs.Count( ) ); builder.m_Rods.EnsureCapacity( m_Rods.Count( ) ); builder.m_PresetNodeBases.CopyArray( m_PresetNodeBases.Base( ), m_PresetNodeBases.Count() ); builder.m_TaperedCapsuleStretches.CopyArray( m_TaperedCapsuleStretches.Base(), m_TaperedCapsuleStretches.Count() ); builder.m_TaperedCapsuleRigids.CopyArray( m_TaperedCapsuleRigids.Base(), m_TaperedCapsuleRigids.Count() ); builder.m_SphereRigids.CopyArray( m_SphereRigids.Base(), m_SphereRigids.Count() ); builder.m_CtrlOffsets.SetCount( m_CtrlOffsets.Count() ); for ( int nCtrlOffset = 0; nCtrlOffset < m_CtrlOffsets.Count(); ++nCtrlOffset ) { builder.m_CtrlOffsets[ nCtrlOffset ] = m_CtrlOffsets[ nCtrlOffset ]; } builder.m_FitInfluences.CopyArray( m_FitInfluences.Base(), m_FitInfluences.Count() ); for ( int nNode = 0; nNode < m_Nodes.Count(); ++nNode ) { builder.m_Nodes[ nNode ] = m_Nodes[ nNode ].AsBuildNode(); } for ( int nQuad = 0; nQuad < m_Quads.Count(); ++nQuad ) { const CQuad &quad = m_Quads[ nQuad ]; CFeModelBuilder::BuildElem_t elem = quad.AsBuildElem(); uint numNodes = elem.NumNodes(); BubbleSort( elem.nNode, numNodes, [&builder] ( uint left, uint right ){ return builder.m_Nodes[ left ].invMass < builder.m_Nodes[ right ].invMass; } ); elem.nNode[ 3 ] = elem.nNode[ numNodes - 1 ]; for ( elem.nStaticNodes = 0; elem.nStaticNodes < numNodes && builder.m_Nodes[ elem.nNode[ elem.nStaticNodes ] ].invMass == 0; ++elem.nStaticNodes ) continue; if ( quad.m_bUseRods ) { // convert this to rods? } builder.m_Elems.AddToTail( elem ); // if elem has 3 or 4 static nodes, the builder will skip it; builder will also sort everything as needed } for ( int nRod = 0; nRod < m_Rods.Count( ); ++nRod ) { const CRod &source = m_Rods[ nRod ]; FeRodConstraint_t rod; rod.nNode[ 0 ] = source.m_nNodes[ 0 ]; rod.nNode[ 1 ] = source.m_nNodes[ 1 ]; rod.flRelaxationFactor = source.m_flRelaxationFactor; CFeModelBuilder::BuildNode_t &n0 = builder.m_Nodes[ rod.nNode[ 0 ] ], &n1 = builder.m_Nodes[ rod.nNode[ 1 ] ]; rod.flMaxDist = source.m_bExplicitLength ? source.m_flLength : ( n0.transform.m_vPosition - n1.transform.m_vPosition ).Length( ); rod.flMinDist = source.m_flContractionFactor * rod.flMaxDist; // Trying to match source1, where min rod distance is 0, but also improve on it a bit, we don't really want cloth to ever collapse float sumInvMass = source.m_flMotionBias[ 0 ] * n0.invMass + source.m_flMotionBias[ 1 ] * n1.invMass; Assert( rod.flRelaxationFactor >= 0 && rod.flRelaxationFactor <= 1.0f ); if ( sumInvMass > 1e-6f && rod.flRelaxationFactor >= 0.0033f ) // we need to have non-zero relaxation factor for this rod to make any sense to compute; otherwise it has no effect. And we shouldn't have any relaxation facotrs outside (0,1) interval, ever { rod.flWeight0 = source.m_flMotionBias[ 0 ] * n0.invMass / sumInvMass; builder.m_Rods.AddToTail( rod ); } } for ( int nSpring = 0; nSpring < m_Springs.Count( ); ++nSpring ) { CFeModelBuilder::BuildSpring_t out; const CSpring &in = m_Springs[ nSpring ]; out.nNode[ 0 ] = in.m_nNodes[ 0 ]; out.nNode[ 1 ] = in.m_nNodes[ 1 ]; out.flSpringConstant = in.m_flSpringConstant; out.flSpringDamping = in.m_flSpringDamping; // note: Stretchiness is not implemented! out.flStretchiness = in.m_flStretchiness; builder.m_Springs.AddToTail( out ); } builder.m_CollisionSpheres.SetCount( m_CollisionSpheres.Count( ) ); for ( int nCollSphere = 0; nCollSphere < m_CollisionSpheres.Count( ); ++nCollSphere ) { const CCollisionSphere &source = m_CollisionSpheres[ nCollSphere ]; CFeModelBuilder::BuildCollisionSphere_t &sphere = builder.m_CollisionSpheres[ nCollSphere ]; sphere.m_bInclusive = source.m_bInclusive; sphere.m_nChild = source.m_nChildBone; sphere.m_nParent = source.m_nParentBone; sphere.m_vOrigin = source.m_vOrigin; sphere.m_flRadius = source.m_flRadius; sphere.m_flStickiness = source.m_flStickiness; } builder.m_CollisionPlanes.SetCount( m_CollisionPlanes.Count( ) ); for ( int nCollisionPlane = 0; nCollisionPlane < m_CollisionPlanes.Count( ); ++nCollisionPlane ) { const CCollisionPlane &source = m_CollisionPlanes[ nCollisionPlane ]; CFeModelBuilder::BuildCollisionPlane_t &plane = builder.m_CollisionPlanes[ nCollisionPlane ]; plane.m_nChild = source.m_nChildBone; plane.m_nParent = source.m_nParentBone; plane.m_Plane = source.m_Plane; plane.m_flStickiness = source.m_flStickiness; AssertDbg( uint( plane.m_nChild ) < uint( m_Nodes.Count() ) && uint( plane.m_nParent ) < uint( m_Nodes.Count() ) ); } if ( m_bForceWorldCollisionOnAllNodes ) { for ( CFeModelBuilder::BuildNode_t &node: builder.m_Nodes ) { if ( !node.bWorldCollision ) { node.bWorldCollision = true; node.flWorldFriction = m_flDefaultWorldCollisionPenetration; node.flGroundFriction = m_flDefaultGroundFriction; } } } builder.m_flDefaultSurfaceStretch = m_flDefaultSurfaceStretch; builder.m_flDefaultThreadStretch = m_flDefaultThreadStretch; builder.m_flDefaultGravityScale = m_flDefaultGravityScale; builder.m_flDefaultVelAirDrag = m_flDefaultVelAirDrag; builder.m_flDefaultExpAirDrag = m_flDefaultExpAirDrag; builder.m_flDefaultVelQuadAirDrag = m_flDefaultVelQuadAirDrag; builder.m_flDefaultExpQuadAirDrag = m_flDefaultExpQuadAirDrag; builder.m_flDefaultVelRodAirDrag = m_flDefaultVelRodAirDrag; builder.m_flDefaultExpRodAirDrag = m_flDefaultExpRodAirDrag; builder.m_flQuadVelocitySmoothRate = m_flQuadVelocitySmoothRate; builder.m_flRodVelocitySmoothRate = m_flRodVelocitySmoothRate; builder.m_flWindage = m_flWindage; builder.m_flWindDrag = m_flWindDrag; builder.m_nQuadVelocitySmoothIterations = Max( 0, m_nQuadVelocitySmoothIterations ); builder.m_nRodVelocitySmoothIterations = Max( 0, m_nRodVelocitySmoothIterations ); builder.m_flAddWorldCollisionRadius = m_flAddWorldCollisionRadius; builder.m_bAddStiffnessRods = m_bAddStiffnessRods; builder.m_bRigidEdgeHinges = m_bRigidEdgeHinges; builder.m_bUsePerNodeLocalForceAndRotation = m_bUsePerNodeLocalForceAndRotation; builder.m_flLocalForce = m_flLocalForce; builder.m_flLocalRotation = m_flLocalRotation; builder.m_flDefaultVolumetricSolveAmount = m_flVolumetricSolveAmount; builder.SetQuadBendTolerance( m_flQuadBendTolerance ); builder.EnableExplicitNodeMasses( m_bExplicitMasses ); builder.EnableUnitlessDamping( m_bUnitlessDamping ); if ( m_bFollowTheLead ) { builder.m_nDynamicNodeFlags |= FE_FLAG_ENABLE_FTL; } if ( m_bCanCollideWithWorldCapsulesAndSpheres ) { builder.m_nDynamicNodeFlags |= FE_FLAG_ENABLE_WORLD_SPHERE_COLLISION | FE_FLAG_ENABLE_WORLD_CAPSULE_COLLISION; } if( m_bCanCollideWithWorldMeshes ) { builder.m_nDynamicNodeFlags |= FE_FLAG_ENABLE_WORLD_MESH_COLLISION; } if ( m_bCanCollideWithWorldHulls ) { builder.m_nDynamicNodeFlags |= FE_FLAG_ENABLE_WORLD_HULL_COLLISION; } if ( m_bUninertialRods ) { builder.m_nDynamicNodeFlags |= FE_FLAG_UNINERTIAL_CONSTRAINTS; } builder.Finish( false, m_flAddCurvature, 0 ); if ( builder.m_pLegacyStretchForce ) { Assert( m_bUninertialRods ); } CLockedResource< PhysFeModelDesc_t > pDesc; if ( builder.m_nNodeCount > builder.m_nStaticNodes ) { pDesc = Clone( &builder, pStream ); } else if ( builder.m_nNodeCount ) { Log_Warning( LOG_AUTH_PHYS, FUNCTION_LINE_STRING "Degenerate softbody with %u static nodes - ignoring, because there are no dynamic nodes to simulate\n", builder.m_nStaticNodes ); } return pDesc; } struct VectorHash_t { uint operator()( const Vector &v ) const { uint32 *p = (uint32*)&v; return p[0] ^ RotateBitsLeft32( p[1], 5 ) ^ RotateBitsLeft32( p[2], 10 ); } }; struct VectorEqual_t { bool operator() ( const Vector &a, const Vector &b )const { return a.DistToSqr( b ) < 1e-12f; } }; class VertexDict { public: VertexDict() { m_nVerts = 0; } int Add( const Vector &v ) { UtlHashHandle_t h = m_Dict.Find( v ); if( h == m_Dict.InvalidHandle() ) { m_Dict.Insert( v, m_nVerts ); return m_nVerts++; } else { return m_Dict.Element( h ); } } uint Count() { return m_nVerts; } protected: CUtlHashtable< Vector, uint, VectorHash_t, VectorEqual_t > m_Dict; uint m_nVerts; }; const Quaternion GetRelativeRotationFromTwistAngles( const Vector &vTwist ) { float tx = tanf( vTwist.x * 0.5f ), ty = tanf( vTwist.y * 0.5f ), tz = tanf( vTwist.z * 0.5f ), f = ( 1 + ty * ty ) * ( 1 + tz * tz ); Quaternion q; q.w = 1 / sqrtf( ( 1 + tx * tx ) * f ); // there are 2 solutions: x,y,z,w and x,y,-z,-w where w>0 q.x = tx * q.w; q.y = ty / sqrtf( 1 + ty * ty ); q.z = tz / sqrtf( f ); return q; } bool IsIn( const char *pName, const CUtlVector > &choice ) { for( int i = 0; i < choice.Count(); ++i ) { if( !V_stricmp( choice[i], pName ) ) { return true; } } return false; } void AppendTo( CUtlVector< CUtlString> &appendTo, const CUtlVector< CUtlString > &appendFrom ) { for( int i = 0; i < appendFrom.Count(); ++i ) { if( appendTo.Find( appendFrom[i] ) < 0 ) { appendTo.AddToTail( appendFrom[i] ); } } } void CAuthPhysCollisionAttributes::ApplyOverride( const CAuthPhysCollisionAttributesOverride &collOverride ) { switch( collOverride.m_nMode ) { case AUTH_PHYS_COLL_ATTR_OVERRIDE: { if( !collOverride.m_CollisionGroup.IsEmpty() ) { m_CollisionGroup = collOverride.m_CollisionGroup; } m_InteractAs = collOverride.m_InteractAs; m_InteractWith = collOverride.m_InteractWith; } break; case AUTH_PHYS_COLL_ATTR_APPEND: { if( !collOverride.m_CollisionGroup.IsEmpty() ) { m_CollisionGroup = collOverride.m_CollisionGroup; } AppendTo( m_InteractAs, collOverride.m_InteractAs ); AppendTo( m_InteractWith, collOverride.m_InteractWith ); } break; } } int CAuthPhysCompileContext::ResolveCollisionAttributesIndex() { for( int i = 0; i < m_CollAttrPalette.Count(); ++i ) { if( m_CollAttrPalette[i] == m_DefaultCollisionAttributes ) { return i; } } int nAdded = m_CollAttrPalette.AddToTail() ; m_CollAttrPalette[nAdded] = m_DefaultCollisionAttributes; return nAdded; } CLockedResource CAuthPhysCompileContext::WriteString( const char *pString, uint32 *pHashOut ) { if ( pHashOut ) { uint32 nHash = MakeStringToken( pString ).GetHashCode( ); *pHashOut = nHash; } return FindOrWrite( pString, V_strlen( pString ) + 1 ); } int CAuthPhysCompileContext::ResolveSurfacePropertyIndex() { for( int i = 0; i < m_SurfacePropPalette.Count(); ++i ) { if( m_DefaultSurfaceProperty == m_SurfacePropPalette[i] ) return i; } int nNewEntry = m_SurfacePropPalette.AddToTail(); m_SurfacePropPalette[nNewEntry ] = m_DefaultSurfaceProperty; return nNewEntry; } static bool IsIn( const CUtlVector< CUtlString > &left, const CUtlVector< CUtlString > &right ) { for( int i = 0; i < left.Count(); ++i ) { bool bFound = false; for( int j = 0; j < right.Count(); ++j ) { if( !V_stricmp( left[i], right[j] ) ) { bFound = true; break; } } if( !bFound ) { return false; } } return true; } static bool IsIn( const CUtlHashtable< uint32 >& leftHashes, const CUtlHashtable< uint32 >& rightHashes ) { FOR_EACH_HASHTABLE( leftHashes, it ) { if( rightHashes.Find( leftHashes.Key( it ) ) == rightHashes.InvalidHandle() ) { return false; } } return true; } bool EqualCaseInsensitive( const CUtlVector< CUtlString > &left, const CUtlVector< CUtlString > &right ) { if( left.Count() * right.Count() < 50 ) { return IsIn( left, right ) && IsIn( right, left ); } else { // scalable version CUtlHashtable< uint32 >leftHashes, rightHashes; for( int nLeftIndex = 0; nLeftIndex < left.Count(); ++nLeftIndex ) { leftHashes.Insert( MakeStringToken( left[nLeftIndex] ).GetHashCode() ); } for( int nRightIndex = 0; nRightIndex < right.Count(); ++nRightIndex ) { rightHashes.Insert( MakeStringToken( right[nRightIndex] ).GetHashCode() ); } return IsIn( leftHashes, rightHashes ) && IsIn( rightHashes, leftHashes ); } } bool CAuthPhysCollisionAttributes::operator == ( const CAuthPhysCollisionAttributes & other ) const { return !V_stricmp( m_CollisionGroup, other.m_CollisionGroup ) && EqualCaseInsensitive( m_InteractAs, other.m_InteractAs ) && EqualCaseInsensitive( m_InteractWith, other.m_InteractWith ); } bool CAuthPhysFx::IsNewSpringAllowed( int nBone0, int nBone1 ) { if( nBone0 == nBone1 ) { // no constraining a bone to itself return false; } if( !m_Nodes[ nBone0 ].m_bSimulated && !m_Nodes[nBone1].m_bSimulated ) { // no constraining non-simulating bones return false; } // also, no constraining bones that are already constrained // note: linear search, will get slow if we get insanely complex softbodies, but we're not likely to get there anytime soon, if ever for( int i = 0; i < m_Constraints.Count(); ++i ) { if( m_Constraints[i].Equals( nBone0, nBone1 ) ) { return false; } } return true; } bool CAuthPhysFx::IsNewRodAllowed( int nBone0, int nBone1 ) { if ( nBone0 == nBone1 ) { // no constraining a bone to itself return false; } if ( !m_Nodes[ nBone0 ].m_bSimulated && !m_Nodes[ nBone1 ].m_bSimulated ) { // no constraining non-simulating bones return false; } // also, no constraining bones that are already constrained // note: linear search, will get slow if we get insanely complex softbodies, but we're not likely to get there anytime soon, if ever for ( int i = 0; i < m_Rods.Count(); ++i ) { if ( m_Rods[ i ].Equals( nBone0, nBone1 ) ) { return false; } } return true; } void CAuthPhysFx::SetBones( const CUtlVector< CBone > &bones ) { m_Nodes.CopyArray( bones.Base(), bones.Count() ); } int CAuthPhysFx::AddConstraint( int nBone0, int nBone1 ) { Assert( uint( nBone0 ) < uint( m_Nodes.Count() ) && uint( nBone1 ) < uint( m_Nodes.Count() ) ); return m_Constraints.AddToTail( CConstraint( nBone0, nBone1 ) ); } bool CAuthPhysFx::IsConstraintSimulated( int nConstraint ) const { const CConstraint &constraint = m_Constraints[ nConstraint ]; const CBone &bone0 = m_Nodes[ constraint.m_nBones[0] ], &bone1 = m_Nodes[ constraint.m_nBones[1] ]; return bone0.m_bSimulated || bone1.m_bSimulated; } bool CAuthPhysFx::IsSpringSimulated( int nSpring ) const { const CSpring &Spring = m_Springs[ nSpring ]; const CBone &bone0 = m_Nodes[ Spring.m_nNodes[ 0 ] ], &bone1 = m_Nodes[ Spring.m_nNodes[ 1 ] ]; return bone0.m_bSimulated || bone1.m_bSimulated; } void CAuthPhysFx::SortAndRemoveDuplicates( ) { HeapSort( m_Quads, [] ( const CAuthPhysFx::CQuad& left, const CAuthPhysFx::CQuad& right ) { for ( int c = 0; c < 4; ++c ) { if ( left.m_nNodes[ c ] != right.m_nNodes[ c ] ) { return left.m_nNodes[ c ] < right.m_nNodes[ c ]; } } return false; } ); RemoveDuplicates( m_Quads ); HeapSort( m_Rods, [] ( const CAuthPhysFx::CRod &left, const CAuthPhysFx::CRod &right ) { return left < right; } ); RemoveDuplicates( m_Rods ); } static bool IsIn( int nNode, const CVarBitVec &nodes ) { return ( nNode >= 0 && nNode < nodes.GetNumBits( ) && nodes.IsBitSet( nNode ) ); } void CAuthPhysFx::RemoveRodsConnecting( const CVarBitVec &nodes ) { for ( int nRod = 0; nRod < m_Rods.Count( ); ) { const CRod &rod = m_Rods[ nRod ]; if ( IsIn( rod.m_nNodes[ 0 ], nodes ) && IsIn( rod.m_nNodes[ 1 ], nodes ) ) { m_Rods.FastRemove( nRod ); } else { ++nRod; } } } void CAuthPhysFx::RemoveQuadsConnecting( const CVarBitVec &nodes ) { for ( int nQuad = 0; nQuad < m_Quads.Count( ); ) { const CQuad &quad = m_Quads[ nQuad ]; if ( IsIn( quad.m_nNodes[ 0 ], nodes ) && IsIn( quad.m_nNodes[ 1 ], nodes ) && IsIn( quad.m_nNodes[ 2 ], nodes ) && IsIn( quad.m_nNodes[ 3 ], nodes ) ) { m_Quads.FastRemove( nQuad ); } else { ++nQuad; } } } // Find connected nodes, assign each island an index, assign each node an island int CAuthPhysFx::BuildIslandMap( CUtlVector< int > &nodeToIsland ) const { int nNodeCount = m_Nodes.Count(); CDisjointSetForest forest( nNodeCount ); for ( const CQuad &quad : m_Quads ) { forest.Union( quad.m_nNodes[ 0 ], quad.m_nNodes[ 1 ] ); forest.Union( quad.m_nNodes[ 0 ], quad.m_nNodes[ 2 ] ); forest.Union( quad.m_nNodes[ 1 ], quad.m_nNodes[ 2 ] ); if ( quad.m_nNodes[ 3 ] != quad.m_nNodes[ 2 ] ) { forest.Union( quad.m_nNodes[ 0 ], quad.m_nNodes[ 3 ] ); forest.Union( quad.m_nNodes[ 1 ], quad.m_nNodes[ 3 ] ); forest.Union( quad.m_nNodes[ 2 ], quad.m_nNodes[ 3 ] ); } } for ( const CRod &rod : m_Rods ) { forest.Union( rod.m_nNodes[ 0 ], rod.m_nNodes[ 1 ] ); } nodeToIsland.SetCount( nNodeCount ); nodeToIsland.FillWithValue( -1 ); int nIslandCount = 0; for ( int n = 0; n < nNodeCount; ++n ) { int nIslandRootNode = forest.Find( n ); int &refIsland = nodeToIsland[ nIslandRootNode ]; if ( refIsland < 0 ) { refIsland = nIslandCount++; } nodeToIsland[ n ] = refIsland; } return nIslandCount; } template static int Cleanup( Array &arr, int nNodeCount ) { int nRemoved = 0; for ( int i = arr.Count(); i-- > 0; ) { if ( !arr[ i ].IsValid( nNodeCount ) ) { arr.FastRemove( i ); ++nRemoved; } } return nRemoved; } int CAuthPhysFx::Cleanup() { int nRemoved = ::Cleanup( m_SphereRigids, m_Nodes.Count() ); nRemoved += ::Cleanup( m_TaperedCapsuleRigids, m_Nodes.Count() ); nRemoved += ::Cleanup( m_TaperedCapsuleStretches, m_Nodes.Count() ); nRemoved += ::Cleanup( m_CtrlOffsets, m_Nodes.Count() ); nRemoved += ::Cleanup( m_Quads, m_Nodes.Count() ); nRemoved += ::Cleanup( m_Rods, m_Nodes.Count() ); return nRemoved; }