/* ikcontext.cpp - Inverse Kinematic implementation This file is part of XashNT engine Copyright (C) 2015 Uncle Mike This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "mathlib.h" #include "const.h" #include #include "com_model.h" #include "stringlib.h" #include "bs_defs.h" #include "ikcontext.h" matrix3x4 CIKContext :: m_boneToWorld[MAXSTUDIOBONES]; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CIKContext :: CIKContext() { m_target.EnsureCapacity( 12 ); // FIXME: this sucks, shouldn't it be grown? m_iFramecounter = -1; m_pBoneSetup = NULL; m_flTime = -1.0f; m_target.SetSize( 0 ); } void CIKContext :: Init( const CStudioBoneSetup *pBoneSetup, const Vector &angles, const Vector &pos, float flTime, int iFramecounter ) { m_ikChainRule.RemoveAll(); // m_numikrules = 0; m_pBoneSetup = (CStudioBoneSetup *)pBoneSetup; if( m_pBoneSetup->GetNumIKChains( )) { m_ikChainRule.SetSize( m_pBoneSetup->GetNumIKChains() ); // FIXME: Brutal hackery to prevent a crash if( m_target.Count() == 0 ) { m_target.SetSize( 12 ); memset( m_target.Base(), 0, sizeof( m_target[0] ) * m_target.Count() ); ClearTargets(); } } else { m_target.SetSize( 0 ); } m_rootxform = matrix3x4( pos, angles ); m_iFramecounter = iFramecounter; m_flTime = flTime; } void CIKContext :: AddDependencies( mstudioseqdesc_t *pseqdesc, int iSequence, float flCycle, float flWeight ) { int i; if( m_pBoneSetup->GetNumIKChains() == 0 ) return; if( !FBitSet( pseqdesc->flags, STUDIO_IKRULES )) return; ikcontextikrule_t ikrule; // this shouldn't be necessary, but the Assert should help us catch whoever is screwing this up flWeight = bound( 0.0f, flWeight, 1.0f ); // unify this if( FBitSet( pseqdesc->flags, STUDIO_REALTIME )) { float cps = m_pBoneSetup->LocalCPS( iSequence ); flCycle = m_flTime * cps; flCycle = flCycle - (int)flCycle; } else if( flCycle < 0 || flCycle >= 1 ) { if( FBitSet( pseqdesc->flags, STUDIO_LOOPING )) { flCycle = flCycle - (int)flCycle; if( flCycle < 0.0f ) flCycle += 1.0f; } else { flCycle = bound( 0.0f, flCycle, 0.9999f ); } } mstudioanimdesc_t *panim[4]; float weight[4]; m_pBoneSetup->LocalSeqAnims( iSequence, panim, weight ); // g-cont. all the animations of current blend has equal set of ikrules and chains. see studiomdl->simplify.cpp for details for( i = 0; i < panim[0]->numikrules; i++ ) { if( !m_pBoneSetup->IKSequenceError( iSequence, flCycle, i, panim, weight, &ikrule )) continue; // don't add rule if the bone isn't going to be calculated const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( ikrule.chain ); int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; if( !m_pBoneSetup->IsBoneUsed( bone )) continue; // or if its relative bone isn't going to be calculated if( ikrule.bone >= 0 && !m_pBoneSetup->IsBoneUsed( ikrule.bone )) continue; // FIXME: Brutal hackery to prevent a crash if( m_target.Count() == 0 ) { m_target.SetSize( 12 ); memset( m_target.Base(), 0, sizeof( m_target[0] ) * m_target.Count()); ClearTargets(); } ikrule.flRuleWeight = flWeight; if( ikrule.flRuleWeight * ikrule.flWeight > 0.999f ) { if( ikrule.type != IK_UNLATCH ) { // clear out chain if rule is 100% m_ikChainRule.Element( ikrule.chain ).RemoveAll( ); if( ikrule.type == IK_RELEASE ) continue; } } int nIndex = m_ikChainRule.Element( ikrule.chain ).AddToTail( ); m_ikChainRule.Element( ikrule.chain ).Element( nIndex ) = ikrule; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: AddAutoplayLocks( Vector pos[], Vector4D q[] ) { // skip all array access if no autoplay locks. if( m_pBoneSetup->GetNumIKAutoplayLocks() == 0 ) { return; } int ikOffset = m_ikLock.AddMultipleToTail( m_pBoneSetup->GetNumIKAutoplayLocks() ); memset( &m_ikLock[ikOffset], 0, sizeof( ikcontextikrule_t ) * m_pBoneSetup->GetNumIKAutoplayLocks() ); for( int i = 0; i < m_pBoneSetup->GetNumIKAutoplayLocks(); i++ ) { const mstudioiklock_t *lock = m_pBoneSetup->pIKAutoplayLock( i ); const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( lock->chain ); int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; // don't bother with iklock if the bone isn't going to be calculated if( !m_pBoneSetup->IsBoneUsed( bone )) continue; // eval current ik'd bone BuildBoneChain( pos, q, bone, m_boneToWorld ); ikcontextikrule_t *ikrule = &m_ikLock[ikOffset + i]; ikrule->chain = lock->chain; ikrule->type = IK_WORLD; ikrule->slot = i; ikrule->q = m_boneToWorld[bone].GetQuaternion(); ikrule->pos = m_boneToWorld[bone].GetOrigin(); // save off current knee direction if( m_pBoneSetup->pIKLink( pchain, 0 )->kneeDir.LengthSqr() > 0.0f ) { const mstudioiklink_t *link0 = m_pBoneSetup->pIKLink( pchain, 0 ); const mstudioiklink_t *link1 = m_pBoneSetup->pIKLink( pchain, 1 ); ikrule->kneeDir = m_boneToWorld[link0->bone].VectorRotate( link0->kneeDir ); ikrule->kneePos = m_boneToWorld[link1->bone].GetOrigin(); } else { ikrule->kneeDir.Init( ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: AddSequenceLocks( mstudioseqdesc_t *pseqdesc, Vector pos[], Vector4D q[] ) { if( m_pBoneSetup->GetNumIKChains() == 0 ) { return; } if( pseqdesc->numiklocks == 0 ) { return; } int ikOffset = m_ikLock.AddMultipleToTail( pseqdesc->numiklocks ); memset( &m_ikLock[ikOffset], 0, sizeof( ikcontextikrule_t ) * pseqdesc->numiklocks ); for( int i = 0; i < pseqdesc->numiklocks; i++ ) { const mstudioiklock_t *plock = m_pBoneSetup->pIKLock( pseqdesc, i ); const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( plock->chain ); int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; // don't bother with iklock if the bone isn't going to be calculated if( !m_pBoneSetup->IsBoneUsed( bone )) continue; // eval current ik'd bone BuildBoneChain( pos, q, bone, m_boneToWorld ); ikcontextikrule_t *ikrule = &m_ikLock[ikOffset+i]; ikrule->chain = i; ikrule->slot = i; ikrule->type = IK_WORLD; ikrule->q = m_boneToWorld[bone].GetQuaternion(); ikrule->pos = m_boneToWorld[bone].GetOrigin(); // save off current knee direction if( m_pBoneSetup->pIKLink( pchain, 0 )->kneeDir.LengthSqr() > 0.0f ) { const mstudioiklink_t *link0 = m_pBoneSetup->pIKLink( pchain, 0 ); ikrule->kneeDir = m_boneToWorld[link0->bone].VectorRotate( link0->kneeDir ); } else { ikrule->kneeDir.Init( ); } } } //----------------------------------------------------------------------------- // Purpose: build boneToWorld transforms for a specific bone //----------------------------------------------------------------------------- void CIKContext :: BuildBoneChain( const Vector pos[], const Vector4D q[], int iBone, matrix3x4 *pBoneToWorld, byte *pBoneSet ) { m_pBoneSetup->BuildBoneChain( m_rootxform, pos, q, iBone, pBoneToWorld, pBoneSet ); } //----------------------------------------------------------------------------- // Purpose: Invalidate any IK locks. //----------------------------------------------------------------------------- void CIKContext :: ClearTargets( void ) { for( int i = 0; i < m_target.Count(); i++ ) { m_target[i].latched.iFramecounter = -9999; } } //----------------------------------------------------------------------------- // Purpose: Run through the rules that survived and turn a specific bones boneToWorld // transform into a pos and q in parents bonespace //----------------------------------------------------------------------------- void CIKContext :: UpdateTargets( Vector pos[], Vector4D q[], matrix3x4 boneToWorld[], byte *pBoneSet ) { int i, j; for( i = 0; i < m_target.Count(); i++ ) { m_target[i].est.flWeight = 0.0f; m_target[i].est.latched = 1.0f; m_target[i].est.release = 1.0f; m_target[i].est.height = 0.0f; m_target[i].est.floor = 0.0f; m_target[i].est.radius = 0.0f; m_target[i].offset.pos.Init(); m_target[i].offset.q.Init(); } AutoIKRelease( ); for( j = 0; j < m_ikChainRule.Count(); j++ ) { for( i = 0; i < m_ikChainRule.Element( j ).Count(); i++ ) { ikcontextikrule_t *pRule = &m_ikChainRule.Element( j ).Element( i ); switch( pRule->type ) { case IK_ATTACHMENT: case IK_GROUND: { matrix3x4 footTarget; CIKTarget *pTarget = &m_target[pRule->slot]; pTarget->chain = pRule->chain; pTarget->type = pRule->type; if( pRule->type == IK_ATTACHMENT ) pTarget->offset.attachmentIndex = pRule->iAttachment; else pTarget->offset.attachmentIndex = 0; if( pRule->flRuleWeight == 1.0f || pTarget->est.flWeight == 0.0f ) { pTarget->offset.q = pRule->q; pTarget->offset.pos = pRule->pos; pTarget->est.height = pRule->height; pTarget->est.floor = pRule->floor; pTarget->est.radius = pRule->radius; pTarget->est.latched = pRule->latched * pRule->flRuleWeight; pTarget->est.release = pRule->release; pTarget->est.flWeight = pRule->flWeight * pRule->flRuleWeight; } else { QuaternionSlerp( pTarget->offset.q, pRule->q, pRule->flRuleWeight, pTarget->offset.q ); pTarget->offset.pos = Lerp( pRule->flRuleWeight, pTarget->offset.pos, pRule->pos ); pTarget->est.height = Lerp( pRule->flRuleWeight, pTarget->est.height, pRule->height ); pTarget->est.floor = Lerp( pRule->flRuleWeight, pTarget->est.floor, pRule->floor ); pTarget->est.radius = Lerp( pRule->flRuleWeight, pTarget->est.radius, pRule->radius ); //pTarget->est.latched = Lerp( pRule->flRuleWeight, pTarget->est.latched, pRule->latched ); pTarget->est.latched = Q_min( pTarget->est.latched, pRule->latched ); pTarget->est.release = Lerp( pRule->flRuleWeight, pTarget->est.release, pRule->release ); pTarget->est.flWeight = Lerp( pRule->flRuleWeight, pTarget->est.flWeight, pRule->flWeight ); } if( pRule->type == IK_GROUND ) { pTarget->latched.deltaPos.z = 0.0f; pTarget->est.pos.z = pTarget->est.floor + m_rootxform[3][2]; } } break; case IK_UNLATCH: { CIKTarget *pTarget = &m_target[pRule->slot]; if( pRule->latched > 0.0 ) pTarget->est.latched = 0.0f; else pTarget->est.latched = Q_min( pTarget->est.latched, 1.0f - pRule->flWeight ); } break; case IK_RELEASE: { CIKTarget *pTarget = &m_target[pRule->slot]; if( pRule->latched > 0.0f ) pTarget->est.latched = 0.0f; else pTarget->est.latched = Q_min( pTarget->est.latched, 1.0f - pRule->flWeight ); pTarget->est.flWeight = (pTarget->est.flWeight) * (1.0f - pRule->flWeight * pRule->flRuleWeight); } break; } } } for( i = 0; i < m_target.Count(); i++ ) { CIKTarget *pTarget = &m_target[i]; if( pTarget->est.flWeight > 0.0 ) { const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( pTarget->chain ); int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; // eval current ik'd bone BuildBoneChain( pos, q, bone, boneToWorld, pBoneSet ); // xform IK target error into world space matrix3x4 local = matrix3x4( pTarget->offset.pos, pTarget->offset.q ).Invert(); matrix3x4 worldFootpad = boneToWorld[bone].ConcatTransforms( local ); if( pTarget->est.latched == 1.0f ) pTarget->latched.bNeedsLatch = true; else pTarget->latched.bNeedsLatch = false; // disable latched position if it looks invalid if( m_iFramecounter < 0 || pTarget->latched.iFramecounter < m_iFramecounter - 1 || pTarget->latched.iFramecounter > m_iFramecounter ) { pTarget->latched.bHasLatch = false; pTarget->latched.influence = 0.0; } pTarget->latched.iFramecounter = m_iFramecounter; // find ideal contact position pTarget->est.q = pTarget->ideal.q = worldFootpad.GetQuaternion(); pTarget->est.pos = pTarget->ideal.pos = worldFootpad.GetOrigin(); float latched = pTarget->est.latched; if( pTarget->latched.bHasLatch ) { if( pTarget->est.latched == 1.0f ) { // keep track of latch position error from ideal contact position pTarget->latched.deltaPos = pTarget->latched.pos - pTarget->est.pos; QuaternionSM( -1.0f, pTarget->est.q, pTarget->latched.q, pTarget->latched.deltaQ ); pTarget->est.q = pTarget->latched.q; pTarget->est.pos = pTarget->latched.pos; } else if( pTarget->est.latched > 0.0f ) { // ramp out latch differences during decay phase of rule if( latched > 0 && latched < pTarget->latched.influence ) { // latching has decreased float dt = pTarget->latched.influence - latched; if( pTarget->latched.influence > 0.0 ) dt = dt / pTarget->latched.influence; QuaternionScale( pTarget->latched.deltaQ, (1.0f - dt), pTarget->latched.deltaQ ); pTarget->latched.deltaPos *= (1.0f - dt); } // move ideal contact position by latched error factor pTarget->est.pos = pTarget->est.pos + pTarget->latched.deltaPos; QuaternionMA( pTarget->est.q, 1, pTarget->latched.deltaQ, pTarget->est.q ); pTarget->latched.q = pTarget->est.q; pTarget->latched.pos = pTarget->est.pos; } else { pTarget->latched.bHasLatch = false; pTarget->latched.q = pTarget->est.q; pTarget->latched.pos = pTarget->est.pos; pTarget->latched.deltaPos.Init(); pTarget->latched.deltaQ.Init(); } pTarget->latched.influence = latched; } // check for illegal requests Vector p1, p2, p3; p1 = boneToWorld[m_pBoneSetup->pIKLink( pchain, 0 )->bone].GetOrigin(); // hip p2 = boneToWorld[m_pBoneSetup->pIKLink( pchain, 1 )->bone].GetOrigin(); // knee p3 = boneToWorld[m_pBoneSetup->pIKLink( pchain, 2 )->bone].GetOrigin(); // foot float d1 = (p2 - p1).Length(); float d2 = (p3 - p2).Length(); if( pTarget->latched.bHasLatch ) { float d4 = (p3 + pTarget->latched.deltaPos - p1).Length(); // unstick feet when distance is too great if(( d4 < fabs( d1 - d2 ) || d4 * 0.95f > d1 + d2 ) && pTarget->est.latched > 0.2f ) { pTarget->error.flTime = m_flTime; } // unstick feet when angle is too great if( pTarget->est.latched > 0.2f ) { float d = fabs( pTarget->latched.deltaQ.w ) * 2.0f - 1.0f; // FIXME: cos(45), make property of chain if( d < 0.707f ) { pTarget->error.flTime = m_flTime; } } } Vector dt = pTarget->est.pos - p1; pTarget->trace.hipToFoot = dt.Length(); pTarget->trace.hipToKnee = d1; pTarget->trace.kneeToFoot = d2; pTarget->trace.hip = p1; pTarget->trace.knee = p2; dt = dt.Normalize(); pTarget->trace.closest = p1 + dt * ( fabs( d1 - d2 ) * 1.01f); pTarget->trace.farthest = p1 + dt * (d1 + d2) * 0.99; pTarget->trace.lowest = p1 + Vector( 0, 0, -1.0f ) * (d1 + d2) * 0.99f; // pTarget->trace.endpos = pTarget->est.pos; } } } //----------------------------------------------------------------------------- // Purpose: insert release rules if the ik rules were in error //----------------------------------------------------------------------------- void CIKContext :: AutoIKRelease( void ) { int i; for( i = 0; i < m_target.Count(); i++ ) { CIKTarget *pTarget = &m_target[i]; float dt = m_flTime - pTarget->error.flTime; if( pTarget->error.bInError || dt < 0.5f ) { if( !pTarget->error.bInError ) { pTarget->error.ramp = 0.0; pTarget->error.flErrorTime = pTarget->error.flTime; pTarget->error.bInError = true; } float ft = m_flTime - pTarget->error.flErrorTime; if( dt < 0.25f ) { pTarget->error.ramp = Q_min( pTarget->error.ramp + ft * 4.0f, 1.0f ); } else { pTarget->error.ramp = Q_max( pTarget->error.ramp - ft * 4.0f, 0.0f ); } if( pTarget->error.ramp > 0.0f ) { ikcontextikrule_t ikrule; ikrule.chain = pTarget->chain; ikrule.bone = 0; ikrule.type = IK_RELEASE; ikrule.slot = i; ikrule.flWeight = SimpleSpline( pTarget->error.ramp ); ikrule.flRuleWeight = 1.0; ikrule.latched = dt < 0.25 ? 0.0 : ikrule.flWeight; // don't bother with AutoIKRelease if the bone isn't going to be calculated // this code is crashing for some unknown reason. if( pTarget->chain >= 0 && pTarget->chain < m_pBoneSetup->GetNumIKChains( )) { const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( pTarget->chain ); if( pchain != NULL ) { int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; if( bone >= 0 && bone < m_pBoneSetup->m_pStudioHeader->numbones ) { if( !m_pBoneSetup->IsBoneUsed( bone )) { pTarget->error.bInError = false; continue; } int nIndex = m_ikChainRule.Element( ikrule.chain ).AddToTail( ); m_ikChainRule.Element( ikrule.chain ).Element( nIndex ) = ikrule; } else { m_pBoneSetup->debugMsg( "^2Warning:^7 AutoIKRelease (%s) out of range bone %d (%d)\n", m_pBoneSetup->m_pStudioHeader->name, bone, m_pBoneSetup->m_pStudioHeader->numbones ); } } else { m_pBoneSetup->debugMsg( "^2Warning:^7 AutoIKRelease (%s) got a NULL pchain %d\n", m_pBoneSetup->m_pStudioHeader->name, pTarget->chain ); } } else { m_pBoneSetup->debugMsg( "^2Warning:^7 AutoIKRelease (%s) got an out of range chain %d (%d)\n", m_pBoneSetup->m_pStudioHeader->name, pTarget->chain, m_pBoneSetup->GetNumIKChains( )); } } else { pTarget->error.bInError = false; } pTarget->error.flErrorTime = m_flTime; } } } void CIKContext :: SolveDependencies( Vector pos[], Vector4D q[], matrix3x4 boneToWorld[], byte *pBoneSet ) { matrix3x4 worldTarget; int i, j; ikchainresult_t chainResult[32]; // allocate!!! // init chain rules for( i = 0; i < m_pBoneSetup->GetNumIKChains(); i++ ) { const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( i ); ikchainresult_t *pChainResult = &chainResult[i]; int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; pChainResult->target = -1; pChainResult->flWeight = 0.0; // don't bother with chain if the bone isn't going to be calculated if( !m_pBoneSetup->IsBoneUsed( bone )) continue; // eval current ik'd bone BuildBoneChain( pos, q, bone, boneToWorld, pBoneSet ); pChainResult->q = boneToWorld[bone].GetQuaternion(); pChainResult->pos = boneToWorld[bone].GetOrigin(); } for( j = 0; j < m_ikChainRule.Count(); j++ ) { for( i = 0; i < m_ikChainRule.Element( j ).Count(); i++ ) { ikcontextikrule_t *pRule = &m_ikChainRule.Element( j ).Element( i ); ikchainresult_t *pChainResult = &chainResult[pRule->chain]; pChainResult->target = -1; switch( pRule->type ) { case IK_SELF: { // xform IK target error into world space matrix3x4 local = matrix3x4( pRule->pos, pRule->q ); // eval target bone space if( pRule->bone != -1 ) { BuildBoneChain( pos, q, pRule->bone, boneToWorld, pBoneSet ); worldTarget = boneToWorld[pRule->bone].ConcatTransforms( local ); } else { worldTarget = m_rootxform.ConcatTransforms( local ); } float flWeight = pRule->flWeight * pRule->flRuleWeight; pChainResult->flWeight = pChainResult->flWeight * (1 - flWeight) + flWeight; Vector p2; Vector4D q2; // target p and q q2 = worldTarget.GetQuaternion(); p2 = worldTarget.GetOrigin(); // m_pBoneSetup->debugLine( pChainResult->pos, p2, 0, 0, 255, true, 0.1 ); // blend in position and angles pChainResult->pos = pChainResult->pos * (1.0f - flWeight) + p2 * flWeight; QuaternionSlerp( pChainResult->q, q2, flWeight, pChainResult->q ); } break; case IK_WORLD: break; case IK_ATTACHMENT: break; case IK_GROUND: break; case IK_RELEASE: { // move target back towards original location float flWeight = pRule->flWeight * pRule->flRuleWeight; const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( pRule->chain ); int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; Vector p2; Vector4D q2; BuildBoneChain( pos, q, bone, boneToWorld, pBoneSet ); q2 = boneToWorld[bone].GetQuaternion(); p2 = boneToWorld[bone].GetOrigin(); // blend in position and angles pChainResult->pos = pChainResult->pos * (1.0 - flWeight) + p2 * flWeight; QuaternionSlerp( pChainResult->q, q2, flWeight, pChainResult->q ); } break; case IK_UNLATCH: { /* pChainResult->flWeight = pChainResult->flWeight * (1 - pRule->flWeight) + pRule->flWeight; pChainResult->pos = pChainResult->pos * (1.0 - pRule->flWeight ) + pChainResult->local.pos * pRule->flWeight; QuaternionSlerp( pChainResult->q, pChainResult->local.q, pRule->flWeight, pChainResult->q ); */ } break; } } } for (i = 0; i < m_target.Count(); i++) { CIKTarget *pTarget = &m_target[i]; if( m_target[i].est.flWeight > 0.0f ) { ikchainresult_t *pChainResult = &chainResult[ pTarget->chain ]; matrix3x4 local = matrix3x4( pTarget->offset.pos, pTarget->offset.q ); matrix3x4 worldFootpad = matrix3x4( pTarget->est.pos, pTarget->est.q ); worldTarget = worldFootpad.ConcatTransforms( local ); Vector p2; Vector4D q2; // target p and q q2 = worldTarget.GetQuaternion(); p2 = worldTarget.GetOrigin(); // blend in position and angles pChainResult->flWeight = pTarget->est.flWeight; pChainResult->pos = pChainResult->pos * (1.0 - pChainResult->flWeight ) + p2 * pChainResult->flWeight; QuaternionSlerp( pChainResult->q, q2, pChainResult->flWeight, pChainResult->q ); } if( pTarget->latched.bNeedsLatch ) { // keep track of latch position pTarget->latched.bHasLatch = true; pTarget->latched.q = pTarget->est.q; pTarget->latched.pos = pTarget->est.pos; } } for( i = 0; i < m_pBoneSetup->GetNumIKChains(); i++ ) { ikchainresult_t *pChainResult = &chainResult[ i ]; const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( i ); if( pChainResult->flWeight > 0.0f ) { int bone0 = m_pBoneSetup->pIKLink( pchain, 0 )->bone; int bone1 = m_pBoneSetup->pIKLink( pchain, 1 )->bone; int bone2 = m_pBoneSetup->pIKLink( pchain, 2 )->bone; Vector tmp = boneToWorld[bone2].GetOrigin(); // m_pBoneSetup->debugLine( pChainResult->pos, tmp, 255, 255, 255, true, 0.1 ); // do exact IK solution // FIXME: once per link! if( m_pBoneSetup->SolveIK( pchain, pChainResult->pos, boneToWorld )) { Vector p3 = boneToWorld[bone2].GetOrigin(); // replace rotational component with IK result boneToWorld[bone2] = matrix3x4( p3, pChainResult->q ); // rebuild chain // FIXME: is this needed if everyone past this uses the boneToWorld array? m_pBoneSetup->SolveBone( bone2, boneToWorld, pos, q ); m_pBoneSetup->SolveBone( bone1, boneToWorld, pos, q ); m_pBoneSetup->SolveBone( bone0, boneToWorld, pos, q ); } else { // FIXME: need to invalidate the targets that forced this... if( pChainResult->target != -1 ) { CIKTarget *pTarget = &m_target[pChainResult->target]; QuaternionScale( pTarget->latched.deltaQ, 0.8f, pTarget->latched.deltaQ ); pTarget->latched.deltaPos *= 0.8f; } } } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: SolveAutoplayLocks( Vector pos[], Vector4D q[] ) { for( int i = 0; i < m_ikLock.Count(); i++ ) { const mstudioiklock_t *lock = m_pBoneSetup->pIKAutoplayLock( i ); SolveLock( lock, i, pos, q, m_boneToWorld ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: SolveSequenceLocks( mstudioseqdesc_t *pseqdesc, Vector pos[], Vector4D q[] ) { for( int i = 0; i < m_ikLock.Count(); i++ ) { const mstudioiklock_t *plock = m_pBoneSetup->pIKLock( pseqdesc, i ); SolveLock( plock, i, pos, q, m_boneToWorld ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: AddAllLocks( Vector pos[], Vector4D q[] ) { // skip all array access if no autoplay locks. if( m_pBoneSetup->GetNumIKChains() == 0 ) { return; } int ikOffset = m_ikLock.AddMultipleToTail( m_pBoneSetup->GetNumIKChains() ); memset( &m_ikLock[ikOffset], 0, sizeof( ikcontextikrule_t ) * m_pBoneSetup->GetNumIKChains() ); for( int i = 0; i < m_pBoneSetup->GetNumIKChains(); i++ ) { const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( i ); int bone = m_pBoneSetup->pIKLink( pchain, 2 )->bone; // don't bother with iklock if the bone isn't going to be calculated if( !m_pBoneSetup->IsBoneUsed( bone )) continue; // eval current ik'd bone BuildBoneChain( pos, q, bone, m_boneToWorld ); ikcontextikrule_t *ikrule = &m_ikLock[ikOffset + i]; ikrule->type = IK_WORLD; ikrule->chain = i; ikrule->slot = i; ikrule->q = m_boneToWorld[bone].GetQuaternion(); ikrule->pos = m_boneToWorld[bone].GetOrigin(); // save off current knee direction if( m_pBoneSetup->pIKLink( pchain, 0 )->kneeDir.LengthSqr() > 0.0f ) { const mstudioiklink_t *link0 = m_pBoneSetup->pIKLink( pchain, 0 ); ikrule->kneeDir = m_boneToWorld[link0->bone].VectorRotate( link0->kneeDir ); ikrule->kneePos = m_boneToWorld[link0->bone].GetOrigin(); } else { ikrule->kneeDir.Init( ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: SolveAllLocks( Vector pos[], Vector4D q[] ) { mstudioiklock_t lock; for( int i = 0; i < m_ikLock.Count(); i++ ) { lock.chain = i; lock.flPosWeight = 1.0f; lock.flLocalQWeight = 0.0f; lock.flags = 0; SolveLock( &lock, i, pos, q, m_boneToWorld ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKContext :: SolveLock( const mstudioiklock_t *plock, int i, Vector pos[], Vector4D q[], matrix3x4 boneToWorld[], byte *pBoneSet ) { const mstudioikchain_t *pchain = m_pBoneSetup->pIKChain( plock->chain ); int bone0 = m_pBoneSetup->pIKLink( pchain, 0 )->bone; int bone1 = m_pBoneSetup->pIKLink( pchain, 1 )->bone; int bone2 = m_pBoneSetup->pIKLink( pchain, 2 )->bone; // don't bother with iklock if the bone isn't going to be calculated if( !m_pBoneSetup->IsBoneUsed( bone2 )) return; // eval current ik'd bone BuildBoneChain( pos, q, bone2, boneToWorld, pBoneSet ); Vector p1, p2, p3; Vector4D q2, q3; // current p and q p1 = boneToWorld[bone2].GetOrigin(); // blend in position p3 = p1 * (1.0 - plock->flPosWeight ) + m_ikLock[i].pos * plock->flPosWeight; // do exact IK solution if( m_ikLock[i].kneeDir.LengthSqr() > 0.0f ) m_pBoneSetup->SolveIK( bone0, bone1, bone2, p3, m_ikLock[i].kneePos, m_ikLock[i].kneeDir, boneToWorld ); else m_pBoneSetup->SolveIK(pchain, p3, boneToWorld ); // slam orientation p3 = boneToWorld[bone2].GetOrigin(); boneToWorld[bone2] = matrix3x4( p3, m_ikLock[i].q ); // rebuild chain q2 = q[bone2]; m_pBoneSetup->SolveBone( bone2, boneToWorld, pos, q ); QuaternionSlerp( q[bone2], q2, plock->flLocalQWeight, q[bone2] ); m_pBoneSetup->SolveBone( bone1, boneToWorld, pos, q ); m_pBoneSetup->SolveBone( bone0, boneToWorld, pos, q ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKTarget :: SetOwner( int entindex, const Vector &pos, const Vector &angles ) { latched.owner = entindex; latched.absOrigin = pos; latched.absAngles = angles; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKTarget :: ClearOwner( void ) { latched.owner = -1; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CIKTarget :: GetOwner( void ) { return latched.owner; } //----------------------------------------------------------------------------- // Purpose: update the latched IK values that are in a moving frame of reference //----------------------------------------------------------------------------- void CIKTarget :: UpdateOwner( int entindex, const Vector &pos, const Vector &angles ) { if( pos == latched.absOrigin && angles == latched.absAngles ) return; matrix3x4 in = matrix3x4( pos, angles ); matrix3x4 out = matrix3x4( latched.absOrigin, latched.absAngles ).Invert(); matrix3x4 tmp1 = matrix3x4( latched.pos, latched.q ); matrix3x4 tmp2 = out.ConcatTransforms( tmp1 ); tmp1 = in.ConcatTransforms( tmp2 ); latched.q = tmp1.GetQuaternion(); latched.pos = tmp1.GetOrigin(); } //----------------------------------------------------------------------------- // Purpose: sets the ground position of an ik target //----------------------------------------------------------------------------- void CIKTarget :: SetPos( const Vector &pos ) { est.pos = pos; } //----------------------------------------------------------------------------- // Purpose: sets the ground "identity" orientation of an ik target //----------------------------------------------------------------------------- void CIKTarget :: SetAngles( const Vector &angles ) { AngleQuaternion( angles, est.q ); } //----------------------------------------------------------------------------- // Purpose: sets the ground "identity" orientation of an ik target //----------------------------------------------------------------------------- void CIKTarget :: SetQuaternion( const Vector4D &q ) { est.q = q; } //----------------------------------------------------------------------------- // Purpose: calculates a ground "identity" orientation based on the surface // normal of the ground and the desired ground identity orientation //----------------------------------------------------------------------------- void CIKTarget :: SetNormal( const Vector &normal ) { // recalculate foot angle based on slope of surface matrix3x4 m1 = matrix3x4( g_vecZero, est.q ); Vector forward, right; right = m1.GetRight(); forward = CrossProduct( right, normal ); right = CrossProduct( normal, forward ); m1.SetForward( forward ); m1.SetRight( right ); m1.SetUp( normal ); est.q = m1.GetQuaternion(); } //----------------------------------------------------------------------------- // Purpose: estimates the ground impact at the center location assuming a the edge of // an Z axis aligned disc collided with it the surface. //----------------------------------------------------------------------------- void CIKTarget :: SetPosWithNormalOffset( const Vector &pos, const Vector &normal ) { // assume it's a disc edge intersecting with the floor, so try to estimate the z location of the center est.pos = pos; if( normal.z > 0.9999f ) { return; } else if( normal.z > 0.707 ) { // clamp at 45 degrees // tan == sin / cos float tan = sqrt( 1.0f - normal.z * normal.z ) / normal.z; est.pos.z = est.pos.z - est.radius * tan; } else { est.pos.z = est.pos.z - est.radius; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKTarget :: SetOnWorld( bool bOnWorld ) { est.onWorld = bOnWorld; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CIKTarget :: IsActive( void ) { return (est.flWeight > 0.0f); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKTarget :: IKFailed( void ) { latched.deltaPos.Init(); latched.deltaQ.Init(); latched.pos = ideal.pos; latched.q = ideal.q; est.latched = 0.0; est.flWeight = 0.0; est.onWorld = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CIKTarget :: MoveReferenceFrame( Vector &deltaPos, Vector &deltaAngles ) { est.pos -= deltaPos; latched.pos -= deltaPos; offset.pos -= deltaPos; ideal.pos -= deltaPos; }