467 lines
10 KiB
C++
467 lines
10 KiB
C++
#include "../idlib/precompiled.h"
|
|
#pragma hdrstop
|
|
|
|
#include "Game_local.h"
|
|
|
|
// 1 normal
|
|
// 2 scared
|
|
// 3 surprised
|
|
// 4 panicked
|
|
// 5 angry
|
|
// 6 suspicious with rt eyelid raised
|
|
// 7 suspicious with lft eyelid raised
|
|
// 8 curious
|
|
// 9 tired
|
|
// 10 happy
|
|
|
|
idCVar fas_debug( "fas_debug", "0", CVAR_INTEGER, "debug info for facial animation system" );
|
|
idCVar fas_threshhold0( "fas_threshhold0", "60", CVAR_INTEGER, "intensity required to use frame set 0" );
|
|
idCVar fas_threshhold1( "fas_threshhold1", "30", CVAR_INTEGER, "intensity required to use frame set 1" );
|
|
idCVar fas_blendBias( "fas_blendBias", "1.5", 0, "multiplier to the per phoneme blend time" );
|
|
idCVar fas_intensityBias( "fas_intensityBias", "0", CVAR_INTEGER, "bias applied to the intensity of the phoneme when trying to extract the viseme" );
|
|
idCVar fas_timeOffset( "fas_timeOffset", "50", CVAR_INTEGER, "ms offset to the viseme frame" );
|
|
|
|
idStr phonemeFile;
|
|
idHashTable<rvViseme> *visemeTable100;
|
|
idHashTable<rvViseme> *visemeTable66;
|
|
idHashTable<rvViseme> *visemeTable33;
|
|
|
|
/*
|
|
================
|
|
rvViseme::Init
|
|
================
|
|
*/
|
|
void rvViseme::Init( idStr &phon, int f, int bt )
|
|
{
|
|
phoneme = phon;
|
|
frame = f;
|
|
blendTime = bt;
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_LoadPhonemes
|
|
|
|
Load in the the file that cross references phonemes with visemes.
|
|
================
|
|
*/
|
|
bool FAS_LoadPhonemes( const char *visemes )
|
|
{
|
|
idStr visemeFile;
|
|
rvViseme viseme;
|
|
idLexer lexer;
|
|
idToken token;
|
|
idStr phoneme;
|
|
int frame, blendTime, intensity;
|
|
|
|
phonemeFile = visemes;
|
|
visemeTable100->Clear();
|
|
visemeTable66->Clear();
|
|
visemeTable33->Clear();
|
|
|
|
common->Printf( "Loading viseme file: %s\n", visemes );
|
|
|
|
visemeFile = "lipsync/";
|
|
visemeFile += visemes;
|
|
visemeFile += ".viseme";
|
|
|
|
lexer.SetFlags( DECL_LEXER_FLAGS );
|
|
lexer.LoadFile( visemeFile );
|
|
|
|
if( !lexer.ExpectTokenString( "visemes" ) )
|
|
{
|
|
return( false );
|
|
}
|
|
|
|
if( !lexer.ExpectTokenString( "{" ) )
|
|
{
|
|
return( false );
|
|
}
|
|
|
|
while( true )
|
|
{
|
|
if( !lexer.ReadToken( &token ) )
|
|
{
|
|
return( false );
|
|
}
|
|
|
|
if( token == "}" )
|
|
{
|
|
break;
|
|
}
|
|
|
|
phoneme = token;
|
|
lexer.ExpectTokenString( "," );
|
|
frame = lexer.ParseInt();
|
|
lexer.ExpectTokenString( "," );
|
|
blendTime = lexer.ParseInt();
|
|
lexer.ExpectTokenString( "," );
|
|
intensity = lexer.ParseInt();
|
|
|
|
viseme.Init( phoneme, frame, blendTime );
|
|
|
|
if( intensity > fas_threshhold0.GetInteger() )
|
|
{
|
|
visemeTable100->Set( phoneme, viseme );
|
|
}
|
|
else if( intensity > fas_threshhold1.GetInteger() )
|
|
{
|
|
visemeTable66->Set( phoneme, viseme );
|
|
}
|
|
else
|
|
{
|
|
visemeTable33->Set( phoneme, viseme );
|
|
}
|
|
}
|
|
|
|
return( true );
|
|
}
|
|
|
|
/*
|
|
================
|
|
rvLipSyncData
|
|
================
|
|
*/
|
|
rvLipSyncData::rvLipSyncData( const rvDeclLipSync *ls, int time )
|
|
{
|
|
const char *lsd = ls->GetLipSyncData();
|
|
|
|
mLexer.SetFlags( LEXFL_ALLOWPATHNAMES | LEXFL_ALLOWNUMBERNAMES );
|
|
mLexer.LoadMemory( lsd, idStr::Length( lsd ), ls->GetName() );
|
|
mFlags = 0;
|
|
mFrame = 0;
|
|
mBlendTime = 0;
|
|
mEmotion = "idle";
|
|
mNextTokenTime = time;
|
|
}
|
|
|
|
void rvLipSyncData::SetFrame( int frame )
|
|
{
|
|
mLastFrame = mFrame;
|
|
mFrame = frame;
|
|
mVisemeStartTime = mNextTokenTime;
|
|
}
|
|
|
|
float rvLipSyncData::GetFrontLerp( void )
|
|
{
|
|
float lerp = 1.0f - idMath::ClampFloat( 0.0f, 1.0f, ( float )( gameLocal.GetTime() + fas_timeOffset.GetInteger() - mVisemeStartTime ) / ( float )mBlendTime );
|
|
return( lerp );
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_StartVisemeExtraction
|
|
|
|
Use a lexer to extract the phoneme data. This is a pretty slow but safe way.
|
|
There shouldn't be much in the way of multiple lip syncs going on, and if there are, it should be in a non performance critical cinematic.
|
|
================
|
|
*/
|
|
rvLipSyncData *FAS_StartVisemeExtraction( const rvDeclLipSync *ls, int time )
|
|
{
|
|
rvLipSyncData *lsd;
|
|
|
|
lsd = new rvLipSyncData( ls, time );
|
|
|
|
return( lsd );
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_EndVisemeExtraction
|
|
|
|
Delete the workspace. FAS could use a void pointer if it wanted
|
|
================
|
|
*/
|
|
void FAS_EndVisemeExtraction( rvLipSyncData *lsd )
|
|
{
|
|
delete lsd;
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_ExtractViseme
|
|
|
|
Extract the correct viseme frame from the lexer
|
|
================
|
|
*/
|
|
void FAS_ExtractViseme( rvLipSyncData *lsd, int time )
|
|
{
|
|
idToken token;
|
|
rvViseme *viseme;
|
|
idStr phoneme, duration;
|
|
int index, intensity;
|
|
|
|
// Make sure not to return any garbage
|
|
lsd->ClearFlags();
|
|
|
|
// Grab all the visemes, phrases and emotions until we are current
|
|
while( lsd->Ready( time ) )
|
|
{
|
|
if( !lsd->ReadToken( &token ) )
|
|
{
|
|
lsd->SetFlags( FAS_ENDED );
|
|
return;
|
|
}
|
|
|
|
if( token == "<" )
|
|
{
|
|
// Extract phrase
|
|
if( !lsd->ReadToken( &token ) )
|
|
{
|
|
common->Printf( "Failed to parse phrase from phoneme string\n" );
|
|
lsd->SetFlags( FAS_ENDED );
|
|
return;
|
|
}
|
|
|
|
lsd->SetLastPhrase( token );
|
|
lsd->ExpectTokenString( ">" );
|
|
|
|
lsd->SetFlags( FAS_NEW_PHRASE );
|
|
}
|
|
else if( token == "{" )
|
|
{
|
|
// Extract emotion
|
|
if( !lsd->ReadToken( &token ) )
|
|
{
|
|
common->Printf( "Failed to parse emotion from phoneme string\n" );
|
|
lsd->SetFlags( FAS_ENDED );
|
|
return;
|
|
}
|
|
|
|
lsd->SetEmotion( token );
|
|
lsd->ExpectTokenString( "}" );
|
|
|
|
lsd->SetFlags( FAS_NEW_EMOTION );
|
|
}
|
|
else
|
|
{
|
|
// Extract phoneme data
|
|
index = 0;
|
|
phoneme = idStr( token[index] );
|
|
if( isupper( phoneme[0] ) )
|
|
{
|
|
index++;
|
|
phoneme += token[index];
|
|
}
|
|
|
|
// Extract duration
|
|
index++;
|
|
|
|
duration = idStr( token[index] );
|
|
index++;
|
|
if( isdigit( token[index] ) )
|
|
{
|
|
duration += token[index];
|
|
index++;
|
|
}
|
|
|
|
// Extract intensity
|
|
intensity = ( token[index] - 'a' ) * 4;
|
|
intensity += fas_intensityBias.GetInteger();
|
|
|
|
// Extract the viseme data for the selected viseme
|
|
viseme = NULL;
|
|
|
|
if( intensity > fas_threshhold0.GetInteger() )
|
|
{
|
|
visemeTable100->Get( phoneme, &viseme );
|
|
}
|
|
if( intensity > fas_threshhold1.GetInteger() )
|
|
{
|
|
visemeTable66->Get( phoneme, &viseme );
|
|
}
|
|
else
|
|
{
|
|
visemeTable33->Get( phoneme, &viseme );
|
|
}
|
|
|
|
if( !viseme )
|
|
{
|
|
common->Printf( "FAS: Failed to find phoneme %s intensity %d", phoneme.c_str(), intensity );
|
|
lsd->SetFlags( FAS_ENDED );
|
|
return;
|
|
}
|
|
|
|
lsd->SetFrame( viseme->GetFrame() );
|
|
lsd->SetNextTokenTime( atol( duration ) * 10 );
|
|
lsd->SetBlendTime( int( viseme->GetBlendTime() * fas_blendBias.GetFloat() ) );
|
|
lsd->SetFlags( FAS_NEW_VISEME );
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_Reload_f
|
|
================
|
|
*/
|
|
void FAS_Reload_f( const idCmdArgs &args )
|
|
{
|
|
// mekberg: disable non pre-cached warnings
|
|
fileSystem->SetIsFileLoadingAllowed( true );
|
|
|
|
FAS_LoadPhonemes( phonemeFile.c_str() );
|
|
|
|
fileSystem->SetIsFileLoadingAllowed( false );
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_Init
|
|
================
|
|
*/
|
|
bool FAS_Init( const char *visemes )
|
|
{
|
|
// jnewquist: Tag scope and callees to track allocations using "new".
|
|
MEM_SCOPED_TAG( tag, MA_ANIM );
|
|
|
|
cmdSystem->AddCommand( "reloadFAS", FAS_Reload_f, CMD_FL_SYSTEM, "reloads the viseme data" );
|
|
return( FAS_LoadPhonemes( visemes ) );
|
|
}
|
|
|
|
/*
|
|
================
|
|
FAS_Shutdown
|
|
================
|
|
*/
|
|
void FAS_Shutdown( void )
|
|
{
|
|
phonemeFile.Clear();
|
|
if ( visemeTable100 ) {
|
|
visemeTable100->Clear();
|
|
visemeTable66->Clear();
|
|
visemeTable33->Clear();
|
|
}
|
|
|
|
cmdSystem->RemoveCommand( "reloadFAS" );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idAFAttachment::EndLipSyncing
|
|
================
|
|
*/
|
|
void idAFAttachment::EndLipSyncing( void )
|
|
{
|
|
frameBlend_t frameBlend = { 0, 0, 0, 1.0f, 0.0f };
|
|
animator.SetFrame( ANIMCHANNEL_TORSO, lipSyncAnim, frameBlend );
|
|
|
|
animator.CycleAnim( ANIMCHANNEL_HEAD, animator.GetAnim( "emotion_idle" ), gameLocal.time, 200 );
|
|
animator.CycleAnim( ANIMCHANNEL_EYELIDS, animator.GetAnim( "emotion_idle" ), gameLocal.time, 200 );
|
|
|
|
FAS_EndVisemeExtraction( lipSyncData );
|
|
lipSyncData = NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
idAFAttachment::StartLipSyncing
|
|
================
|
|
*/
|
|
int idAFAttachment::StartLipSyncing( const char *speechDecl )
|
|
{
|
|
int length;
|
|
|
|
length = 0;
|
|
|
|
// Clean up any spurious data
|
|
EndLipSyncing();
|
|
|
|
// Start a new lipsync if there is one
|
|
if( speechDecl[0] )
|
|
{
|
|
const rvDeclLipSync *lipSync;
|
|
int emotion;
|
|
idStr anim;
|
|
|
|
lipSync = declManager->FindLipSync( speechDecl );
|
|
lipSyncData = FAS_StartVisemeExtraction( lipSync, gameLocal.GetTime() );
|
|
|
|
// Output debug info
|
|
if( lipSync->GetDescription() && fas_debug.GetInteger() )
|
|
{
|
|
gameLocal.Printf( "Name: %s\n", speechDecl );
|
|
gameLocal.Printf( "Sub: %s\n", lipSync->GetDescription().c_str() );
|
|
gameLocal.Printf( "Lip: %s\n", lipSync->GetLipSyncData() );
|
|
}
|
|
|
|
// Start the associated sound
|
|
refSound.diversity = 0.0f;
|
|
renderEntity.referenceSoundHandle = refSound.referenceSoundHandle;
|
|
StartSoundShader( declManager->FindSound( lipSync->GetName() ), SND_CHANNEL_VOICE, refSound.parms.soundShaderFlags | SSF_IS_VO, false, &length );
|
|
|
|
// Start the default emotion
|
|
anim = "emotion_";
|
|
anim += lipSyncData->GetEmotion();
|
|
emotion = animator.GetAnim( anim );
|
|
animator.CycleAnim( ANIMCHANNEL_HEAD, emotion, gameLocal.time, 200 );
|
|
animator.CycleAnim( ANIMCHANNEL_EYELIDS, emotion, gameLocal.time, 200 );
|
|
}
|
|
|
|
return( length );
|
|
}
|
|
|
|
/*
|
|
================
|
|
idAFAttachment::HandleLipSync
|
|
================
|
|
*/
|
|
void idAFAttachment::HandleLipSync( void )
|
|
{
|
|
idStr anim;
|
|
int emotion;
|
|
|
|
if( !lipSyncData )
|
|
{
|
|
return;
|
|
}
|
|
|
|
FAS_ExtractViseme( lipSyncData, gameLocal.GetTime() + fas_timeOffset.GetInteger() );
|
|
if( lipSyncData->HasEnded() )
|
|
{
|
|
EndLipSyncing();
|
|
return;
|
|
}
|
|
|
|
// If frame non zero - blend to it as a new viseme
|
|
if( lipSyncData->GetFrame() || lipSyncData->GetLastFrame() )
|
|
{
|
|
frameBlend_t frameBlend;
|
|
|
|
frameBlend.cycleCount = 0;
|
|
frameBlend.frame1 = idMath::ClampInt( 0, 120, lipSyncData->GetLastFrame() - 1 );
|
|
frameBlend.frame2 = idMath::ClampInt( 0, 120, lipSyncData->GetFrame() - 1 );
|
|
frameBlend.frontlerp = lipSyncData->GetFrontLerp();
|
|
frameBlend.backlerp = 1.0f - frameBlend.frontlerp;
|
|
|
|
animator.SetFrame( ANIMCHANNEL_TORSO, lipSyncAnim, frameBlend );
|
|
|
|
if( fas_debug.GetInteger() > 1 )
|
|
{
|
|
common->Printf( "Blending: %d (%2f) -> %d (%2f)\n", frameBlend.frame1, frameBlend.frontlerp, frameBlend.frame2, frameBlend.backlerp );
|
|
}
|
|
}
|
|
|
|
// If an embedded emotion command, play it.
|
|
if( lipSyncData->HasNewEmotion() )
|
|
{
|
|
anim = "emotion_";
|
|
anim += lipSyncData->GetEmotion();
|
|
emotion = animator.GetAnim( anim );
|
|
animator.CycleAnim( ANIMCHANNEL_HEAD, emotion, gameLocal.time, 200 );
|
|
animator.CycleAnim( ANIMCHANNEL_EYELIDS, emotion, gameLocal.time, 200 );
|
|
|
|
if( fas_debug.GetInteger() )
|
|
{
|
|
common->Printf( "Emotion: %s\n", lipSyncData->GetEmotion().c_str() );
|
|
}
|
|
}
|
|
|
|
// If a new phrase, display in debug mode
|
|
if( fas_debug.GetInteger() && lipSyncData->HasNewPhrase() )
|
|
{
|
|
common->Printf( "Phrase: %s(%i)\n", lipSyncData->GetLastPhrase().c_str(), lipSyncData->GetFrame() );
|
|
}
|
|
}
|
|
|
|
// end
|