csgo-2018-source/mdlobjects/authphysfx.cpp
2021-07-24 21:11:47 -07:00

2060 lines
59 KiB
C++

//========== 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<CDmeModel*>( 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 );
// <sergiy> 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;
// <sergiy> 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 <typename T >
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 )
{
// <sergiy> 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<CUtlString,CUtlMemory< CUtlString, int > > &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<char> 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 <typename Array>
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;
}