//===== Copyright 1996-2005, Valve Corporation, All rights reserved. ======// // // Purpose: // // $NoKeywords: $ //===========================================================================// #include "cbase.h" #include "filesystem.h" #include "sentence.h" #include "hud_closecaption.h" #include "engine/ivmodelinfo.h" #include "engine/ivdebugoverlay.h" #include "bone_setup.h" #include "soundinfo.h" #include "tools/bonelist.h" #include "KeyValues.h" #include "tier0/vprof.h" #include "toolframework/itoolframework.h" #include "choreoevent.h" #include "choreoscene.h" #include "choreoactor.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ConVar g_CV_PhonemeDelay("phonemedelay", "0", 0, "Phoneme delay to account for sound system latency." ); ConVar g_CV_PhonemeFilter("phonemefilter", "0.08", 0, "Time duration of box filter to pass over phonemes." ); ConVar g_CV_FlexRules("flex_rules", "1", 0, "Allow flex animation rules to run." ); ConVar g_CV_BlinkDuration("blink_duration", "0.2", 0, "How many seconds an eye blink will last." ); ConVar g_CV_FlexSmooth("flex_smooth", "1", 0, "Applies smoothing/decay curve to flex animation controller changes." ); #if defined( CBaseFlex ) #undef CBaseFlex #endif IMPLEMENT_CLIENTCLASS_DT(C_BaseFlex, DT_BaseFlex, CBaseFlex) RecvPropArray3( RECVINFO_ARRAY(m_flexWeight), RecvPropFloat(RECVINFO(m_flexWeight[0]))), RecvPropInt(RECVINFO(m_blinktoggle)), RecvPropVector(RECVINFO(m_viewtarget)), #ifdef HL2_CLIENT_DLL RecvPropFloat( RECVINFO(m_vecViewOffset[0]) ), RecvPropFloat( RECVINFO(m_vecViewOffset[1]) ), RecvPropFloat( RECVINFO(m_vecViewOffset[2]) ), #endif END_RECV_TABLE() BEGIN_PREDICTION_DATA( C_BaseFlex ) /* // DEFINE_FIELD( C_BaseFlex, m_viewtarget, FIELD_VECTOR ), // DEFINE_ARRAY( C_BaseFlex, m_flexWeight, FIELD_FLOAT, 64 ), // DEFINE_FIELD( C_BaseFlex, m_blinktoggle, FIELD_INTEGER ), // DEFINE_FIELD( C_BaseFlex, m_blinktime, FIELD_FLOAT ), // DEFINE_FIELD( C_BaseFlex, m_prevviewtarget, FIELD_VECTOR ), // DEFINE_ARRAY( C_BaseFlex, m_prevflexWeight, FIELD_FLOAT, 64 ), // DEFINE_FIELD( C_BaseFlex, m_prevblinktoggle, FIELD_INTEGER ), // DEFINE_FIELD( C_BaseFlex, m_iBlink, FIELD_INTEGER ), // DEFINE_FIELD( C_BaseFlex, m_iEyeUpdown, FIELD_INTEGER ), // DEFINE_FIELD( C_BaseFlex, m_iEyeRightleft, FIELD_INTEGER ), // DEFINE_FIELD( C_BaseFlex, m_FileList, CUtlVector < CFlexSceneFile * > ), */ END_PREDICTION_DATA() C_BaseFlex::C_BaseFlex() : m_iv_viewtarget( "C_BaseFlex::m_iv_viewtarget" ), m_iv_flexWeight("C_BaseFlex:m_iv_flexWeight" ), m_LocalToGlobal( 0, 0, FlexSettingLessFunc ) { #ifdef _DEBUG ((Vector&)m_viewtarget).Init(); #endif AddVar( &m_viewtarget, &m_iv_viewtarget, LATCH_ANIMATION_VAR | INTERPOLATE_LINEAR_ONLY ); AddVar( m_flexWeight, &m_iv_flexWeight, LATCH_ANIMATION_VAR ); // Fill in phoneme class lookup memset( m_PhonemeClasses, 0, sizeof( m_PhonemeClasses ) ); Emphasized_Phoneme *weak = &m_PhonemeClasses[ PHONEME_CLASS_WEAK ]; Q_strncpy( weak->classname, "phonemes_weak", sizeof( weak->classname ) ); weak->required = false; Emphasized_Phoneme *normal = &m_PhonemeClasses[ PHONEME_CLASS_NORMAL ]; Q_strncpy( normal->classname, "phonemes", sizeof( normal->classname ) ); normal->required = true; Emphasized_Phoneme *strong = &m_PhonemeClasses[ PHONEME_CLASS_STRONG ]; Q_strncpy( strong->classname, "phonemes_strong", sizeof( strong->classname ) ); strong->required = false; m_flFlexDelayedWeight = NULL; /// Make sure size is correct Assert( PHONEME_CLASS_STRONG + 1 == NUM_PHONEME_CLASSES ); } C_BaseFlex::~C_BaseFlex() { delete[] m_flFlexDelayedWeight; m_SceneEvents.RemoveAll(); m_LocalToGlobal.RemoveAll(); } //----------------------------------------------------------------------------- // Purpose: initialize fast lookups when model changes //----------------------------------------------------------------------------- CStudioHdr *C_BaseFlex::OnNewModel() { CStudioHdr *hdr = BaseClass::OnNewModel(); // init to invalid setting m_iBlink = -1; m_iEyeUpdown = -1; m_iEyeRightleft = -1; m_iMouthAttachment = 0; delete[] m_flFlexDelayedWeight; m_flFlexDelayedWeight = NULL; if (hdr) { if (hdr->numflexdesc()) { m_flFlexDelayedWeight = new float [hdr->numflexdesc()]; for (int i = 0; i < hdr->numflexdesc(); i++) { m_flFlexDelayedWeight[i] = 0.0; } } m_iv_flexWeight.SetMaxCount( hdr->numflexcontrollers() ); m_iMouthAttachment = LookupAttachment( "mouth" ); } return hdr; } //----------------------------------------------------------------------------- // Purpose: place "voice" sounds on mouth //----------------------------------------------------------------------------- bool C_BaseFlex::GetSoundSpatialization( SpatializationInfo_t& info ) { bool bret = BaseClass::GetSoundSpatialization( info ); // Default things it's audible, put it at a better spot? if ( bret ) { if (info.info.nChannel == CHAN_VOICE && m_iMouthAttachment > 0) { Vector origin; QAngle angles; C_BaseAnimating::PushAllowBoneAccess( true, false ); if (GetAttachment( m_iMouthAttachment, origin, angles )) { if (info.pOrigin) { *info.pOrigin = origin; } if (info.pAngles) { *info.pAngles = angles; } } C_BaseAnimating::PopBoneAccess(); } } return bret; } //----------------------------------------------------------------------------- // Purpose: run the interpreted FAC's expressions, converting flex_controller // values into FAC weights //----------------------------------------------------------------------------- void C_BaseFlex::RunFlexRules( CStudioHdr *hdr, float *dest ) { if ( !g_CV_FlexRules.GetInt() ) return; if ( !hdr ) return; /* // 0 means run them all int nFlexRulesToRun = 0; const char *pszExpression = flex_expression.GetString(); if ( pszExpression ) { nFlexRulesToRun = atoi(pszExpression); // 0 will be returned if not a numeric string } //*/ hdr->RunFlexRules( g_flexweight, dest ); } class CFlexSceneFileManager : CAutoGameSystem { public: CFlexSceneFileManager() : CAutoGameSystem( "CFlexSceneFileManager" ) { } virtual bool Init() { // Trakcer 16692: Preload these at startup to avoid hitch first time we try to load them during actual gameplay FindSceneFile( NULL, "phonemes", true ); FindSceneFile( NULL, "phonemes_weak", true ); FindSceneFile(NULL, "phonemes_strong", true ); #if defined( HL2_CLIENT_DLL ) FindSceneFile( NULL, "random", true ); FindSceneFile( NULL, "randomAlert", true ); #endif return true; } // Tracker 14992: We used to load 18K of .vfes for every C_BaseFlex who lipsynced, but now we only load those files once globally. // Note, we could wipe these between levels, but they don't ever load more than the weak/normal/strong phoneme classes that I can tell // so I'll just leave them loaded forever for now virtual void Shutdown() { DeleteSceneFiles(); } //----------------------------------------------------------------------------- // Purpose: Sets up translations // Input : *instance - // *pSettinghdr - // Output : void //----------------------------------------------------------------------------- void EnsureTranslations( C_BaseFlex *instance, const flexsettinghdr_t *pSettinghdr ) { // The only time instance is NULL is in Init() above, where we're just loading the .vfe files off of the hard disk. if ( instance ) { instance->EnsureTranslations( pSettinghdr ); } } void *FindSceneFile( C_BaseFlex *instance, const char *filename, bool allowBlockingIO ) { // See if it's already loaded int i; for ( i = 0; i < m_FileList.Count(); i++ ) { CFlexSceneFile *file = m_FileList[ i ]; if ( file && !stricmp( file->filename, filename ) ) { // Make sure translations (local to global flex controller) are set up for this instance EnsureTranslations( instance, ( const flexsettinghdr_t * )file->buffer ); return file->buffer; } } if ( !allowBlockingIO ) { return NULL; } // Load file into memory void *buffer = NULL; int len = filesystem->ReadFileEx( VarArgs( "expressions/%s.vfe", filename ), "GAME", &buffer ); if ( !len ) return NULL; // Create scene entry CFlexSceneFile *pfile = new CFlexSceneFile; // Remember filename Q_strncpy( pfile->filename, filename, sizeof( pfile->filename ) ); // Remember data pointer pfile->buffer = buffer; // Add to list m_FileList.AddToTail( pfile ); // Fill in translation table EnsureTranslations( instance, ( const flexsettinghdr_t * )pfile->buffer ); // Return data return pfile->buffer; } private: void DeleteSceneFiles() { while ( m_FileList.Count() > 0 ) { CFlexSceneFile *file = m_FileList[ 0 ]; m_FileList.Remove( 0 ); delete[] file->buffer; delete file; } } CUtlVector< CFlexSceneFile * > m_FileList; }; CFlexSceneFileManager g_FlexSceneFileManager; //----------------------------------------------------------------------------- // Purpose: // Input : *filename - //----------------------------------------------------------------------------- void *C_BaseFlex::FindSceneFile( const char *filename ) { return g_FlexSceneFileManager.FindSceneFile( this, filename, false ); } //----------------------------------------------------------------------------- // Purpose: make sure the eyes are within 30 degrees of forward //----------------------------------------------------------------------------- Vector C_BaseFlex::SetViewTarget( CStudioHdr *pStudioHdr ) { if ( !pStudioHdr ) return Vector( 0, 0, 0); // aim the eyes Vector tmp = m_viewtarget; if (m_iEyeUpdown == -1) m_iEyeUpdown = AddGlobalFlexController( "eyes_updown" ); if (m_iEyeRightleft == -1) m_iEyeRightleft = AddGlobalFlexController( "eyes_rightleft" ); if (m_iEyeAttachment > 0) { matrix3x4_t attToWorld; if (!GetAttachment( m_iEyeAttachment, attToWorld )) { return Vector( 0, 0, 0); } Vector local; VectorITransform( tmp, attToWorld, local ); // FIXME: clamp distance to something based on eyeball distance if (local.x < 6) { local.x = 6; } float flDist = local.Length(); VectorNormalize( local ); // calculate animated eye deflection Vector eyeDeflect; QAngle eyeAng( 0, 0, 0 ); if ( m_iEyeUpdown != -1) { eyeAng.x = g_flexweight[ m_iEyeUpdown ]; } if ( m_iEyeRightleft != -1) { eyeAng.y = g_flexweight[ m_iEyeRightleft ]; } // debugoverlay->AddTextOverlay( GetAbsOrigin() + Vector( 0, 0, 64 ), 0, 0, "%5.3f %5.3f", eyeAng.x, eyeAng.y ); AngleVectors( eyeAng, &eyeDeflect ); eyeDeflect.x = 0; // reduce deflection the more the eye is off center // FIXME: this angles make no damn sense eyeDeflect = eyeDeflect * (local.x * local.x); local = local + eyeDeflect; VectorNormalize( local ); // check to see if the eye is aiming outside a 30 degree cone if (local.x < 0.866) // cos(30) { // if so, clamp it to 30 degrees offset // debugoverlay->AddTextOverlay( GetAbsOrigin() + Vector( 0, 0, 64 ), 1, 0, "%5.3f %5.3f %5.3f", local.x, local.y, local.z ); local.x = 0; float d = local.LengthSqr(); if (d > 0.0) { d = sqrtf( (1.0 - 0.866 * 0.866) / (local.y*local.y + local.z*local.z) ); local.x = 0.866; local.y = local.y * d; local.z = local.z * d; } else { local.x = 1.0; } } local = local * flDist; VectorTransform( local, attToWorld, tmp ); } modelrender->SetViewTarget( tmp ); /* debugoverlay->AddTextOverlay( GetAbsOrigin() + Vector( 0, 0, 64 ), 0, 0, "%.2f %.2f %.2f : %.2f %.2f %.2f", m_viewtarget.x, m_viewtarget.y, m_viewtarget.z, m_prevviewtarget.x, m_prevviewtarget.y, m_prevviewtarget.z ); */ return tmp; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- static void NewMarkovIndex( flexsetting_t *pSetting ) { if ( pSetting->type != FS_MARKOV ) return; int weighttotal = 0; int member = 0; for (int i = 0; i < pSetting->numsettings; i++) { flexmarkovgroup_t *group = pSetting->pMarkovGroup( i ); if ( !group ) continue; weighttotal += group->weight; if ( !weighttotal || random->RandomInt(0,weighttotal-1) < group->weight ) { member = i; } } pSetting->currentindex = member; } #define STRONG_CROSSFADE_START 0.60f #define WEAK_CROSSFADE_START 0.40f //----------------------------------------------------------------------------- // Purpose: // Here's the formula // 0.5 is neutral 100 % of the default setting // Crossfade starts at STRONG_CROSSFADE_START and is full at STRONG_CROSSFADE_END // If there isn't a strong then the intensity of the underlying phoneme is fixed at 2 x STRONG_CROSSFADE_START // so we don't get huge numbers // Input : *classes - // emphasis_intensity - //----------------------------------------------------------------------------- void C_BaseFlex::ComputeBlendedSetting( Emphasized_Phoneme *classes, float emphasis_intensity ) { // See which overrides are available for the current phoneme bool has_weak = classes[ PHONEME_CLASS_WEAK ].valid; bool has_strong = classes[ PHONEME_CLASS_STRONG ].valid; // Better have phonemes in general Assert( classes[ PHONEME_CLASS_NORMAL ].valid ); if ( emphasis_intensity > STRONG_CROSSFADE_START ) { if ( has_strong ) { // Blend in some of strong float dist_remaining = 1.0f - emphasis_intensity; float frac = dist_remaining / ( 1.0f - STRONG_CROSSFADE_START ); classes[ PHONEME_CLASS_NORMAL ].amount = (frac) * 2.0f * STRONG_CROSSFADE_START; classes[ PHONEME_CLASS_STRONG ].amount = 1.0f - frac; } else { emphasis_intensity = min( emphasis_intensity, STRONG_CROSSFADE_START ); classes[ PHONEME_CLASS_NORMAL ].amount = 2.0f * emphasis_intensity; } } else if ( emphasis_intensity < WEAK_CROSSFADE_START ) { if ( has_weak ) { // Blend in some weak float dist_remaining = WEAK_CROSSFADE_START - emphasis_intensity; float frac = dist_remaining / ( WEAK_CROSSFADE_START ); classes[ PHONEME_CLASS_NORMAL ].amount = (1.0f - frac) * 2.0f * WEAK_CROSSFADE_START; classes[ PHONEME_CLASS_WEAK ].amount = frac; } else { emphasis_intensity = max( emphasis_intensity, WEAK_CROSSFADE_START ); classes[ PHONEME_CLASS_NORMAL ].amount = 2.0f * emphasis_intensity; } } else { // Assume 0.5 (neutral) becomes a scaling of 1.0f classes[ PHONEME_CLASS_NORMAL ].amount = 2.0f * emphasis_intensity; } } //----------------------------------------------------------------------------- // Purpose: // Input : *classes - // phoneme - // scale - // newexpression - //----------------------------------------------------------------------------- void C_BaseFlex::AddViseme( Emphasized_Phoneme *classes, float emphasis_intensity, int phoneme, float scale, bool newexpression ) { int type; // Setup weights for any emphasis blends bool skip = SetupEmphasisBlend( classes, phoneme ); // Uh-oh, missing or unknown phoneme??? if ( skip ) { return; } // Compute blend weights ComputeBlendedSetting( classes, emphasis_intensity ); for ( type = 0; type < NUM_PHONEME_CLASSES; type++ ) { Emphasized_Phoneme *info = &classes[ type ]; if ( !info->valid || info->amount == 0.0f ) continue; // Assume that we're not using overrieds const flexsettinghdr_t *actual_flexsetting_header = info->base; const flexsetting_t *pSetting = actual_flexsetting_header->pIndexedSetting( phoneme ); if (!pSetting) { continue; } if ( newexpression ) { if ( pSetting->type == FS_MARKOV ) { NewMarkovIndex( ( flexsetting_t * )pSetting ); } } // Determine its index int i = pSetting - actual_flexsetting_header->pSetting( 0 ); Assert( i >= 0 ); Assert( i < actual_flexsetting_header->numflexsettings ); // Resolve markov chain for the returned setting, probably not an issue for visemes pSetting = actual_flexsetting_header->pTranslatedSetting( i ); #if !defined( NO_ENTITY_PREDICTION ) // Check for overrides if ( info->override ) { // Get name from setting const char *resolvedName = pSetting->pszName(); if ( resolvedName ) { // See if resolvedName exists in the override file const flexsetting_t *override = FindNamedSetting( info->override, resolvedName ); if ( override ) { // If so, point at the override file instead actual_flexsetting_header = info->override; pSetting = override; } } } #endif flexweight_t *pWeights = NULL; int truecount = pSetting->psetting( (byte *)actual_flexsetting_header, 0, &pWeights ); if ( pWeights ) { for (i = 0; i < truecount; i++) { // Translate to global controller number int j = FlexControllerLocalToGlobal( actual_flexsetting_header, pWeights->key ); // Add scaled weighting in g_flexweight[j] += info->amount * scale * pWeights->weight; // Go to next setting pWeights++; } } } } //----------------------------------------------------------------------------- // Purpose: A lot of the one time setup and also resets amount to 0.0f default // for strong/weak/normal tracks // Returning true == skip this phoneme // Input : *classes - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool C_BaseFlex::SetupEmphasisBlend( Emphasized_Phoneme *classes, int phoneme ) { int i; bool skip = false; for ( i = 0; i < NUM_PHONEME_CLASSES; i++ ) { Emphasized_Phoneme *info = &classes[ i ]; // Assume it's bogus info->valid = false; info->amount = 0.0f; // One time setup if ( !info->basechecked ) { info->basechecked = true; info->base = (flexsettinghdr_t *)FindSceneFile( info->classname ); } #if !defined( NO_ENTITY_PREDICTION ) info->override = NULL; #endif info->exp = NULL; if ( info->base ) { Assert( info->base->id == ('V' << 16) + ('F' << 8) + ('E') ); info->exp = info->base->pIndexedSetting( phoneme ); } if ( info->required && ( !info->base || !info->exp ) ) { skip = true; break; } if ( info->exp ) { info->valid = true; } // NOTE: We never actually used any overrides in HL2/Aftermath, so doing this disk check could lead to hitches due to calling filesystem->Open on each // possibility. If we ever need to use these overrides, I would suggest adding a flag on the server to the keyvalues for NPCs specifying "use overrides", // networking the flag down, and then only checking for overrides if the flag is set on the client. ALternateley, we could crawl the expressions folders // with findfirst/next and find all override files and create a database, we'd do that at startup if we did it. However, that would add a bit of time to startup // due to recursively crawling the directories (though we could just enumerate dirs off of the expressions dir...). // ywb 2/8/06 #if 0 #if !defined( NO_ENTITY_PREDICTION ) // Find overrides, if any exist // Also a one-time setup if ( !info->overridechecked ) { char overridefile[ 512 ]; char shortname[ 128 ]; char modelname[ 128 ]; Q_strncpy( modelname, modelinfo->GetModelName( GetModel() ), sizeof( modelname ) ); // Fix up the name Q_FileBase( modelname, shortname, sizeof( shortname ) ); Q_snprintf( overridefile, sizeof( overridefile ), "%s/%s", shortname, info->classname ); info->overridechecked = true; info->override = ( flexsettinghdr_t * )FindSceneFile( overridefile ); } #else info->overridechecked = true; info->override = 0; #endif #endif } return skip; } //----------------------------------------------------------------------------- // Purpose: // Input : *classes - // *sentence - // t - // dt - // juststarted - //----------------------------------------------------------------------------- ConVar g_CV_PhonemeSnap("phonemesnap", "1", 0, "Don't force visemes to always consider two phonemes, regardless of duration." ); void C_BaseFlex::AddVisemesForSentence( Emphasized_Phoneme *classes, float emphasis_intensity, CSentence *sentence, float t, float dt, bool juststarted ) { CStudioHdr *hdr = GetModelPtr(); if ( !hdr ) { return; } int pcount = sentence->GetRuntimePhonemeCount(); for ( int k = 0; k < pcount; k++ ) { const CBasePhonemeTag *phoneme = sentence->GetRuntimePhoneme( k ); if ((!g_CV_PhonemeSnap.GetBool() || (hdr->flags() & STUDIOHDR_FLAGS_FORCE_PHONEME_CROSSFADE)) && t > phoneme->GetStartTime() && t < phoneme->GetEndTime()) { if (k < pcount-1) { const CBasePhonemeTag *next = sentence->GetRuntimePhoneme( k + 1 ); if ( next ) { dt = max( dt, min( next->GetEndTime() - t, phoneme->GetEndTime() - phoneme->GetStartTime() ) ); } } } float t1 = ( phoneme->GetStartTime() - t) / dt; float t2 = ( phoneme->GetEndTime() - t) / dt; if (t1 < 1.0 && t2 > 0) { float scale; // clamp if (t2 > 1) t2 = 1; if (t1 < 0) t1 = 0; // FIXME: simple box filter. Should use something fancier scale = (t2 - t1); AddViseme( classes, emphasis_intensity, phoneme->GetPhonemeCode(), scale, juststarted ); } } } //----------------------------------------------------------------------------- // Purpose: // Input : *classes - //----------------------------------------------------------------------------- void C_BaseFlex::ProcessVisemes( Emphasized_Phoneme *classes ) { // Any sounds being played? if ( !MouthInfo().IsActive() ) return; // Multiple phoneme tracks can overlap, look across all such tracks. for ( int source = 0 ; source < MouthInfo().GetNumVoiceSources(); source++ ) { CVoiceData *vd = MouthInfo().GetVoiceSource( source ); if ( !vd ) continue; CSentence *sentence = engine->GetSentence( vd->GetSource() ); if ( !sentence ) continue; float sentence_length = engine->GetSentenceLength( vd->GetSource() ); float timesincestart = vd->GetElapsedTime(); // This sound should be done...why hasn't it been removed yet??? if ( timesincestart >= ( sentence_length + 2.0f ) ) continue; // Adjust actual time float t = timesincestart - g_CV_PhonemeDelay.GetFloat(); // Get box filter duration float dt = g_CV_PhonemeFilter.GetFloat(); // Streaming sounds get an additional delay... /* // Tracker 20534: Probably not needed any more with the async sound stuff that // we now have (we don't have a disk i/o hitch on startup which might have been // messing up the startup timing a bit ) bool streaming = engine->IsStreaming( vd->m_pAudioSource ); if ( streaming ) { t -= g_CV_PhonemeDelayStreaming.GetFloat(); } */ // Assume sound has been playing for a while... bool juststarted = false; /* // FIXME: Do we really want to support markov chains for the phonemes? // If so, we'll need to uncomment out these lines. if ( timesincestart < 0.001 ) { juststarted = true; } */ // Get intensity setting for this time (from spline) float emphasis_intensity = sentence->GetIntensity( t, sentence_length ); // Blend and add visemes together AddVisemesForSentence( classes, emphasis_intensity, sentence, t, dt, juststarted ); } } //----------------------------------------------------------------------------- // Purpose: fill keyvalues message with flex state // Input : //----------------------------------------------------------------------------- void C_BaseFlex::GetToolRecordingState( KeyValues *msg ) { if ( !ToolsEnabled() ) return; VPROF_BUDGET( "C_BaseFlex::GetToolRecordingState", VPROF_BUDGETGROUP_TOOLS ); BaseClass::GetToolRecordingState( msg ); CStudioHdr *hdr = GetModelPtr(); if ( !hdr ) return; memset( g_flexweight, 0, sizeof( g_flexweight ) ); if ( hdr->numflexcontrollers() == 0 ) return; int i, j; ProcessSceneEvents( true ); // FIXME: shouldn't this happen at runtime? // initialize the models local to global flex controller mappings if (hdr->pFlexcontroller( 0 )->link == -1) { for (i = 0; i < hdr->numflexcontrollers(); i++) { j = AddGlobalFlexController( hdr->pFlexcontroller( i )->pszName() ); hdr->pFlexcontroller( i )->link = j; } } // blend weights from server for (i = 0; i < hdr->numflexcontrollers(); i++) { mstudioflexcontroller_t *pflex = hdr->pFlexcontroller( i ); g_flexweight[pflex->link] = m_flexWeight[i]; // rescale g_flexweight[pflex->link] = g_flexweight[pflex->link] * (pflex->max - pflex->min) + pflex->min; } ProcessSceneEvents( false ); // check for blinking if (m_blinktoggle != m_prevblinktoggle) { m_prevblinktoggle = m_blinktoggle; m_blinktime = gpGlobals->curtime + g_CV_BlinkDuration.GetFloat(); } if (m_iBlink == -1) m_iBlink = AddGlobalFlexController( "blink" ); g_flexweight[m_iBlink] = 0; // FIXME: this needs a better algorithm // blink the eyes float t = (m_blinktime - gpGlobals->curtime) * M_PI * 0.5 * (1.0/g_CV_BlinkDuration.GetFloat()); if (t > 0) { // do eyeblink falloff curve t = cos(t); if (t > 0) { g_flexweight[m_iBlink] = sqrtf( t ) * 2; if (g_flexweight[m_iBlink] > 1) g_flexweight[m_iBlink] = 2.0 - g_flexweight[m_iBlink]; } } // Drive the mouth from .wav file playback... ProcessVisemes( m_PhonemeClasses ); Vector viewtarget = SetViewTarget( hdr ); static BaseFlexRecordingState_t state; state.m_nFlexCount = MAXSTUDIOFLEXCTRL; state.m_pDestWeight = g_flexweight; state.m_vecViewTarget = viewtarget; msg->SetPtr( "baseflex", &state ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void C_BaseFlex::SetupWeights( ) { CStudioHdr *hdr = GetModelPtr(); if ( !hdr ) { return; } memset( g_flexweight, 0, sizeof( g_flexweight ) ); // FIXME: this should assert then, it's too complex a class for the model if (hdr->numflexcontrollers() == 0) return; int i, j; ProcessSceneEvents( true ); // FIXME: shouldn't this happen at runtime? // initialize the models local to global flex controller mappings if (hdr->pFlexcontroller( 0 )->link == -1) { for (i = 0; i < hdr->numflexcontrollers(); i++) { j = AddGlobalFlexController( hdr->pFlexcontroller( i )->pszName() ); hdr->pFlexcontroller( i )->link = j; } } // get the networked flexweights and convert them from 0..1 to real dynamic range for (i = 0; i < hdr->numflexcontrollers(); i++) { mstudioflexcontroller_t *pflex = hdr->pFlexcontroller( i ); g_flexweight[pflex->link] = m_flexWeight[i]; // rescale g_flexweight[pflex->link] = g_flexweight[pflex->link] * (pflex->max - pflex->min) + pflex->min; } ProcessSceneEvents( false ); // check for blinking if (m_blinktoggle != m_prevblinktoggle) { m_prevblinktoggle = m_blinktoggle; m_blinktime = gpGlobals->curtime + g_CV_BlinkDuration.GetFloat(); } if (m_iBlink == -1) m_iBlink = AddGlobalFlexController( "blink" ); // FIXME: this needs a better algorithm // blink the eyes float t = (m_blinktime - gpGlobals->curtime) * M_PI * 0.5 * (1.0/g_CV_BlinkDuration.GetFloat()); if (t > 0) { // do eyeblink falloff curve t = cos(t); if (t > 0.0f && t < 1.0f) { t = sqrtf( t ) * 2.0f; if (t > 1.0f) t = 2.0f - t; t = clamp( t, 0.0f, 1.0f ); // add it to whatever the blink track is doing g_flexweight[m_iBlink] = clamp( g_flexweight[m_iBlink] + t, 0.0, 1.0 ); } } // Drive the mouth from .wav file playback... ProcessVisemes( m_PhonemeClasses ); // convert the flex controllers into actual flex values float destweight[MAXSTUDIOFLEXDESC]; RunFlexRules( hdr, destweight ); // aim the eyes SetViewTarget( hdr ); if (m_flFlexDelayedWeight && g_CV_FlexSmooth.GetBool()) { // process the delayed version of the flexweights float d = 1.0; if (gpGlobals->frametime != 0) { d = ExponentialDecay( 0.8, 0.033, gpGlobals->frametime ); } for ( i = 0; i < hdr->numflexdesc(); i++) { m_flFlexDelayedWeight[i] = m_flFlexDelayedWeight[i] * d + destweight[i] * (1 - d); } // debugoverlay->AddTextOverlay( GetAbsOrigin() + Vector( 0, 0, 64 ), i-hdr->numflexcontrollers, 0, "%.3f", d ); // send the flex values to the renderer modelrender->SetFlexWeights( hdr->numflexdesc(), destweight, m_flFlexDelayedWeight ); } else { // send the flex values to the renderer modelrender->SetFlexWeights( hdr->numflexdesc(), destweight ); } /* for (i = 0; i < hdr->numflexdesc; i++) { debugoverlay->AddTextOverlay( GetAbsOrigin() + Vector( 0, 0, 64 ), i-hdr->numflexcontrollers, 0, "%2d:%s : %3.2f", i, hdr->pFlexdesc( i )->pszFACS(), destweight[i] ); } */ /* for (i = 0; i < g_numflexcontrollers; i++) { int j = hdr->pFlexcontroller( i )->link; debugoverlay->AddTextOverlay( GetAbsOrigin() + Vector( 0, 0, 64 ), -i, 0, "%s %3.2f", g_flexcontroller[i], g_flexweight[j] ); } */ } int C_BaseFlex::g_numflexcontrollers; char * C_BaseFlex::g_flexcontroller[MAXSTUDIOFLEXCTRL*4]; float C_BaseFlex::g_flexweight[MAXSTUDIOFLEXDESC]; int C_BaseFlex::AddGlobalFlexController( char *szName ) { int i; for (i = 0; i < g_numflexcontrollers; i++) { if (Q_stricmp( g_flexcontroller[i], szName ) == 0) { return i; } } if ( g_numflexcontrollers < MAXSTUDIOFLEXCTRL * 4 ) { g_flexcontroller[g_numflexcontrollers++] = strdup( szName ); } else { // FIXME: missing runtime error condition } return i; } char const *C_BaseFlex::GetGlobalFlexControllerName( int idx ) { if ( idx < 0 || idx >= g_numflexcontrollers ) { return ""; } return g_flexcontroller[ idx ]; } const flexsetting_t *C_BaseFlex::FindNamedSetting( const flexsettinghdr_t *pSettinghdr, const char *expr ) { int i; const flexsetting_t *pSetting = NULL; for ( i = 0; i < pSettinghdr->numflexsettings; i++ ) { pSetting = pSettinghdr->pSetting( i ); if ( !pSetting ) continue; const char *name = pSetting->pszName(); if ( !stricmp( name, expr ) ) break; } if ( i>=pSettinghdr->numflexsettings ) { return NULL; } return pSetting; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void C_BaseFlex::StartChoreoScene( CChoreoScene *scene ) { if ( m_ActiveChoreoScenes.Find( scene ) != m_ActiveChoreoScenes.InvalidIndex() ) { return; } m_ActiveChoreoScenes.AddToTail( scene ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void C_BaseFlex::RemoveChoreoScene( CChoreoScene *scene ) { // Assert( m_ActiveChoreoScenes.Find( scene ) != m_ActiveChoreoScenes.InvalidIndex() ); m_ActiveChoreoScenes.FindAndRemove( scene ); } //----------------------------------------------------------------------------- // Purpose: Remove all active SceneEvents //----------------------------------------------------------------------------- void C_BaseFlex::ClearSceneEvents( CChoreoScene *scene, bool canceled ) { if ( !scene ) { m_SceneEvents.RemoveAll(); return; } for ( int i = m_SceneEvents.Count() - 1; i >= 0; i-- ) { CSceneEventInfo *info = &m_SceneEvents[ i ]; Assert( info ); Assert( info->m_pScene ); Assert( info->m_pEvent ); if ( info->m_pScene != scene ) continue; if ( !ClearSceneEvent( info, false, canceled )) { // unknown expression to clear!! Assert( 0 ); } // Free this slot info->m_pEvent = NULL; info->m_pScene = NULL; info->m_bStarted = false; m_SceneEvents.Remove( i ); } } //----------------------------------------------------------------------------- // Purpose: Stop specifics of expression //----------------------------------------------------------------------------- bool C_BaseFlex::ClearSceneEvent( CSceneEventInfo *info, bool fastKill, bool canceled ) { Assert( info ); Assert( info->m_pScene ); Assert( info->m_pEvent ); return true; } //----------------------------------------------------------------------------- // Purpose: Add string indexed scene/expression/duration to list of active SceneEvents // Input : scenefile - // expression - // duration - //----------------------------------------------------------------------------- void C_BaseFlex::AddSceneEvent( CChoreoScene *scene, CChoreoEvent *event, CBaseEntity *pTarget ) { if ( !scene || !event ) { Msg( "C_BaseFlex::AddSceneEvent: scene or event was NULL!!!\n" ); return; } CChoreoActor *actor = event->GetActor(); if ( !actor ) { Msg( "C_BaseFlex::AddSceneEvent: event->GetActor() was NULL!!!\n" ); return; } CSceneEventInfo info; memset( (void *)&info, 0, sizeof( info ) ); info.m_pEvent = event; info.m_pScene = scene; info.m_hTarget = pTarget; info.m_bStarted = false; if (StartSceneEvent( &info, scene, event, actor, pTarget )) { m_SceneEvents.AddToTail( info ); } else { scene->SceneMsg( "C_BaseFlex::AddSceneEvent: event failed\n" ); // Assert( 0 ); // expression failed to start } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool C_BaseFlex::StartSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event, CChoreoActor *actor, CBaseEntity *pTarget ) { switch ( event->GetType() ) { default: break; case CChoreoEvent::FLEXANIMATION: info->InitWeight( this ); return true; case CChoreoEvent::EXPRESSION: return true; } return false; } //----------------------------------------------------------------------------- // Purpose: Remove expression // Input : scenefile - // expression - //----------------------------------------------------------------------------- void C_BaseFlex::RemoveSceneEvent( CChoreoScene *scene, CChoreoEvent *event, bool fastKill ) { Assert( event ); for ( int i = 0 ; i < m_SceneEvents.Count(); i++ ) { CSceneEventInfo *info = &m_SceneEvents[ i ]; Assert( info ); Assert( info->m_pEvent ); if ( info->m_pScene != scene ) continue; if ( info->m_pEvent != event) continue; if (ClearSceneEvent( info, fastKill, false )) { // Free this slot info->m_pEvent = NULL; info->m_pScene = NULL; info->m_bStarted = false; m_SceneEvents.Remove( i ); return; } } // many events refuse to start due to bogus parameters } //----------------------------------------------------------------------------- // Purpose: Checks to see if the event should be considered "completed" //----------------------------------------------------------------------------- bool C_BaseFlex::CheckSceneEvent( float currenttime, CChoreoScene *scene, CChoreoEvent *event ) { for ( int i = 0 ; i < m_SceneEvents.Count(); i++ ) { CSceneEventInfo *info = &m_SceneEvents[ i ]; Assert( info ); Assert( info->m_pEvent ); if ( info->m_pScene != scene ) continue; if ( info->m_pEvent != event) continue; return CheckSceneEventCompletion( info, currenttime, scene, event ); } return true; } bool C_BaseFlex::CheckSceneEventCompletion( CSceneEventInfo *info, float currenttime, CChoreoScene *scene, CChoreoEvent *event ) { return true; } void C_BaseFlex::SetFlexWeight( int index, float value ) { if (index >= 0 && index < GetNumFlexControllers()) { CStudioHdr *pstudiohdr = GetModelPtr( ); if (! pstudiohdr) return; mstudioflexcontroller_t *pflexcontroller = pstudiohdr->pFlexcontroller( index ); if (pflexcontroller->max != pflexcontroller->min) { value = (value - pflexcontroller->min) / (pflexcontroller->max - pflexcontroller->min); value = clamp( value, 0.0, 1.0 ); } m_flexWeight[ index ] = value; } } float C_BaseFlex::GetFlexWeight( int index ) { if (index >= 0 && index < GetNumFlexControllers()) { CStudioHdr *pstudiohdr = GetModelPtr( ); if (! pstudiohdr) return 0; mstudioflexcontroller_t *pflexcontroller = pstudiohdr->pFlexcontroller( index ); if (pflexcontroller->max != pflexcontroller->min) { return m_flexWeight[index] * (pflexcontroller->max - pflexcontroller->min) + pflexcontroller->min; } return m_flexWeight[index]; } return 0.0; } int C_BaseFlex::FindFlexController( const char *szName ) { for (int i = 0; i < GetNumFlexControllers(); i++) { if (stricmp( GetFlexControllerName( i ), szName ) == 0) { return i; } } // AssertMsg( 0, UTIL_VarArgs( "flexcontroller %s couldn't be mapped!!!\n", szName ) ); return 0; } //----------------------------------------------------------------------------- // Purpose: Default implementation //----------------------------------------------------------------------------- void C_BaseFlex::ProcessSceneEvents( bool bFlexEvents ) { CStudioHdr *hdr = GetModelPtr(); if ( !hdr ) { return; } // slowly decay to netural expression int i; if ( bFlexEvents ) { for ( i = 0; i < GetNumFlexControllers(); i++) { SetFlexWeight( i, GetFlexWeight( i ) * 0.95 ); } } // Iterate SceneEvents and look for active slots for ( i = 0; i < m_SceneEvents.Count(); i++ ) { CSceneEventInfo *info = &m_SceneEvents[ i ]; Assert( info ); // FIXME: Need a safe handle to m_pEvent in case of memory deletion? CChoreoEvent *event = info->m_pEvent; Assert( event ); CChoreoScene *scene = info->m_pScene; Assert( scene ); if ( ProcessSceneEvent( bFlexEvents, info, scene, event ) ) { info->m_bStarted = true; } } } //----------------------------------------------------------------------------- // Various methods to process facial SceneEvents: //----------------------------------------------------------------------------- bool C_BaseFlex::ProcessFlexAnimationSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) { Assert( event->HasEndTime() ); if ( event->HasEndTime() ) { AddFlexAnimation( info ); } return true; } #define AllowSceneOverrides() 0 bool C_BaseFlex::ProcessFlexSettingSceneEvent( CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) { // Flexanimations have to have an end time!!! if ( !event->HasEndTime() ) return true; VPROF( "C_BaseFlex::ProcessFlexSettingSceneEvent" ); // Look up the actual strings const char *scenefile = event->GetParameters(); const char *name = event->GetParameters2(); // Have to find both strings if ( scenefile && name ) { // Find the scene file const flexsettinghdr_t *pExpHdr = ( const flexsettinghdr_t * )g_FlexSceneFileManager.FindSceneFile( this, scenefile, true ); if ( pExpHdr ) { const flexsettinghdr_t *pOverrideHdr = NULL; // Find overrides, if any exist CStudioHdr *hdr; if ( AllowSceneOverrides() && ( hdr = GetModelPtr() ) != NULL ) { char overridefile[ 512 ]; char shortname[ 128 ]; char modelname[ 128 ]; //Q_strncpy( modelname, modelinfo->GetModelName( model ) ,sizeof(modelname)); Q_strncpy( modelname, hdr->pszName() ,sizeof(modelname)); // Fix up the name Q_FileBase( modelname, shortname, sizeof( shortname ) ); Q_snprintf( overridefile,sizeof(overridefile), "%s/%s", shortname, scenefile ); pOverrideHdr = ( const flexsettinghdr_t * )g_FlexSceneFileManager.FindSceneFile( this, overridefile, true ); } float scenetime = scene->GetTime(); float scale = event->GetIntensity( event, scenetime ); // Add the named expression AddFlexSetting( name, scale, pExpHdr, pOverrideHdr, !info->m_bStarted ); } } return true; } //----------------------------------------------------------------------------- // Purpose: Each CBaseFlex maintains a UtlRBTree of mappings, one for each loaded flex scene file it uses. This is used to // sort the entries in the RBTree // Input : lhs - // rhs - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool C_BaseFlex::FlexSettingLessFunc( const FS_LocalToGlobal_t& lhs, const FS_LocalToGlobal_t& rhs ) { return lhs.m_Key < rhs.m_Key; } //----------------------------------------------------------------------------- // Purpose: Since everyone shared a pSettinghdr now, we need to set up the localtoglobal mapping per entity, but // we just do this in memory with an array of integers (could be shorts, I suppose) // Input : *pSettinghdr - //----------------------------------------------------------------------------- void C_BaseFlex::EnsureTranslations( const flexsettinghdr_t *pSettinghdr ) { Assert( pSettinghdr ); FS_LocalToGlobal_t entry( pSettinghdr ); unsigned short idx = m_LocalToGlobal.Find( entry ); if ( idx != m_LocalToGlobal.InvalidIndex() ) return; entry.SetCount( pSettinghdr->numkeys ); for ( int i = 0; i < pSettinghdr->numkeys; ++i ) { entry.m_Mapping[ i ] = AddGlobalFlexController( pSettinghdr->pLocalName( i ) ); } m_LocalToGlobal.Insert( entry ); } //----------------------------------------------------------------------------- // Purpose: Look up instance specific mapping // Input : *pSettinghdr - // key - // Output : int //----------------------------------------------------------------------------- int C_BaseFlex::FlexControllerLocalToGlobal( const flexsettinghdr_t *pSettinghdr, int key ) { FS_LocalToGlobal_t entry( pSettinghdr ); int idx = m_LocalToGlobal.Find( entry ); if ( idx == m_LocalToGlobal.InvalidIndex() ) { // This should never happen!!! Assert( 0 ); Warning( "Unable to find mapping for flexcontroller %i, settings %p on %i/%s\n", key, pSettinghdr, entindex(), GetClassname() ); EnsureTranslations( pSettinghdr ); idx = m_LocalToGlobal.Find( entry ); if ( idx == m_LocalToGlobal.InvalidIndex() ) { Error( "CBaseFlex::FlexControllerLocalToGlobal failed!\n" ); } } FS_LocalToGlobal_t& result = m_LocalToGlobal[ idx ]; // Validate lookup Assert( result.m_nCount != 0 && key < result.m_nCount ); int index = result.m_Mapping[ key ]; return index; } //----------------------------------------------------------------------------- // Purpose: // Input : *expr - // scale - // *pSettinghdr - // *pOverrideHdr - // newexpression - //----------------------------------------------------------------------------- void C_BaseFlex::AddFlexSetting( const char *expr, float scale, const flexsettinghdr_t *pSettinghdr, const flexsettinghdr_t *pOverrideHdr, bool newexpression ) { int i; const flexsetting_t *pSetting = NULL; // Find the named setting in the base for ( i = 0; i < pSettinghdr->numflexsettings; i++ ) { pSetting = pSettinghdr->pSetting( i ); if ( !pSetting ) continue; const char *name = pSetting->pszName(); if ( !stricmp( name, expr ) ) break; } if ( i>=pSettinghdr->numflexsettings ) { return; } // Update markov chain if needed if ( newexpression ) { if ( pSetting->type == FS_MARKOV ) { NewMarkovIndex( (flexsetting_t *)pSetting ); } } // Resolve markov chain for the returned setting pSetting = pSettinghdr->pTranslatedSetting( i ); // Check for overrides if ( AllowSceneOverrides() && pOverrideHdr ) { // Get name from setting const char *resolvedName = pSetting->pszName(); if ( resolvedName ) { // See if resolvedName exists in the override file const flexsetting_t *override = FindNamedSetting( pOverrideHdr, resolvedName ); if ( override ) { // If so, point at the override file instead pSettinghdr = pOverrideHdr; pSetting = override; } } } flexweight_t *pWeights = NULL; int truecount = pSetting->psetting( (byte *)pSettinghdr, 0, &pWeights ); if ( !pWeights ) return; for (i = 0; i < truecount; i++, pWeights++) { // Translate to local flex controller // this is translating from the settings's local index to the models local index int index = FlexControllerLocalToGlobal( pSettinghdr, pWeights->key ); // Add scaled weighting in to total (post networking g_flexweight!!!!) float value = g_flexweight[index] + scale * pWeights->weight; g_flexweight[index] = value; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool C_BaseFlex::ProcessSceneEvent( bool bFlexEvents, CSceneEventInfo *info, CChoreoScene *scene, CChoreoEvent *event ) { switch ( event->GetType() ) { default: break; case CChoreoEvent::FLEXANIMATION: if ( bFlexEvents ) { return ProcessFlexAnimationSceneEvent( info, scene, event ); } return true; case CChoreoEvent::EXPRESSION: if ( !bFlexEvents ) { return ProcessFlexSettingSceneEvent( info, scene, event ); } return true; } return false; } //----------------------------------------------------------------------------- // Purpose: // Input : *event - //----------------------------------------------------------------------------- void C_BaseFlex::AddFlexAnimation( CSceneEventInfo *info ) { if ( !info ) return; CChoreoEvent *event = info->m_pEvent; if ( !event ) return; CChoreoScene *scene = info->m_pScene; if ( !scene ) return; if ( !event->GetTrackLookupSet() ) { // Create lookup data for ( int i = 0; i < event->GetNumFlexAnimationTracks(); i++ ) { CFlexAnimationTrack *track = event->GetFlexAnimationTrack( i ); if ( !track ) continue; if ( track->IsComboType() ) { char name[ 512 ]; Q_strncpy( name, "right_" ,sizeof(name)); Q_strncat( name, track->GetFlexControllerName(),sizeof(name), COPY_ALL_CHARACTERS ); track->SetFlexControllerIndex( 0, FindFlexController( name ), 0 ); Q_strncpy( name, "left_" ,sizeof(name)); Q_strncat( name, track->GetFlexControllerName(),sizeof(name), COPY_ALL_CHARACTERS ); track->SetFlexControllerIndex( 0, FindFlexController( name ), 1 ); } else { track->SetFlexControllerIndex( 0, FindFlexController( (char *)track->GetFlexControllerName() ) ); } } event->SetTrackLookupSet( true ); } if ( !scene_clientflex.GetBool() ) return; float scenetime = scene->GetTime(); float weight = event->GetIntensity( event, scenetime ); // decay if this is a background scene and there's other flex animations playing weight = weight * info->UpdateWeight( this ); // Compute intensity for each track in animation and apply // Iterate animation tracks for ( int i = 0; i < event->GetNumFlexAnimationTracks(); i++ ) { CFlexAnimationTrack *track = event->GetFlexAnimationTrack( i ); if ( !track ) continue; // Disabled if ( !track->IsTrackActive() ) continue; // Map track flex controller to global name if ( track->IsComboType() ) { for ( int side = 0; side < 2; side++ ) { int controller = track->GetFlexControllerIndex( side ); // Get spline intensity for controller float flIntensity = track->GetIntensity( scenetime, side ); if ( controller >= 0 ) { float orig = GetFlexWeight( controller ); float value = orig * (1 - weight) + flIntensity * weight; SetFlexWeight( controller, value ); } } } else { int controller = track->GetFlexControllerIndex( 0 ); // Get spline intensity for controller float flIntensity = track->GetIntensity( scenetime, 0 ); if ( controller >= 0 ) { float orig = GetFlexWeight( controller ); float value = orig * (1 - weight) + flIntensity * weight; SetFlexWeight( controller, value ); } } } info->m_bStarted = true; } void CSceneEventInfo::InitWeight( C_BaseFlex *pActor ) { m_flWeight = 1.0; } //----------------------------------------------------------------------------- // Purpose: update weight for background events. Only call once per think //----------------------------------------------------------------------------- float CSceneEventInfo::UpdateWeight( C_BaseFlex *pActor ) { m_flWeight = min( m_flWeight + 0.1, 1.0 ); return m_flWeight; }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 5821 | Knut Wikstrom |
Added Valve Source code. This is NOT to be commited to other than new code from Valve. |