Archive 17/01/2023.

LipSync Code

artgolf1000

Hi,

I have written a lip sync logic component for my 3d character, though it is messy, it is useful for me.

First, make some blend shapes for your 3d character, here are the shapes: https://github.com/meshonline/rhubarb-lip-sync, if you like Papagayo’s shapes, refer to ‘Readme.txt’ in the ‘src-patch’ subdirectory.

Make a ‘blink’ blend shape to control eyes blink.

Then, use the above rhubarb-lip-sync utility to generated a text file for your voice.
For example:
./rhubarb voice.wav > voice.txt

Here is the LipSync Class:

LipSync.h

[code]#pragma once

#include <Urho3D/Urho3DAll.h>

/// Custom logic component for moving the animated model and rotating at area edges.
class LipSync : public LogicComponent
{
URHO3D_OBJECT(LipSync, LogicComponent);

public:
/// Construct.
LipSync(Context* context) :
LogicComponent(context)
{
EaseWeights[0] = 0.0f;
EaseWeights[1] = 0.1925f;
EaseWeights[2] = 0.605f;
EaseWeights[3] = 0.8f;
EaseWeights[4] = 0.605f;
EaseWeights[5] = 0.1925f;
EaseWeights[6] = 0.0f;

    blinking = false;
    currentStep = 0;
    accuTime = 0.0f;
    blinkEyes = StringHash("blink");
    
    lipSyncing = false;
    currentStep2 = 0;
    prior_lipIndex = -1;
    current_lipIndex = -1;
    
    priorSyncTime_ = FLT_MAX;
    
    ResourceCache* cache = GetSubsystem<ResourceCache>();
    SharedPtr<File> file = cache->GetFile("Sounds/voice.txt");
    while (!file->IsEof()) {
        String line = file->ReadLine();
        if (line.Length()) {
            Vector<String> items = line.Split('\t');
            if (items.Size() == 2) {
                MY_MORPH_KEY one_key;
                one_key.time = atof(items[0].CString());
                one_key.key = StringHash(items[1]);
                morphKeys_.Push(one_key);
            }
        }
    }
    
    // Only the scene update event is needed: unsubscribe from the rest for optimization
    SetUpdateEventMask(USE_UPDATE);
}

/// Handle scene update. Called by LogicComponent base class.
virtual void Update(float timeStep)
{
    // Get the model's first (only) animation state and advance its time. Note the convenience accessor to other components
    // in the same scene node
    AnimatedModel* model = GetComponent<AnimatedModel>();
    if (model->GetNumAnimationStates())
    {
        AnimationState* state = model->GetAnimationStates()[0];
        state->AddTime(timeStep);
        
        // blink eyes every two seconds
        accuTime += timeStep;
        if (accuTime >= 2.0f) {
            accuTime = 0.0f;
            blinking = true;
        }
        // deal with blink
        if (blinking) {
            model->SetMorphWeight(blinkEyes, EaseWeights[currentStep]);
            currentStep++;
            if (currentStep == 7) {
                currentStep = 0;
                blinking = false;
            }
        }
        
        // search next lip sync point
        if (!lipSyncing) {
            float syncTime = state->GetTime();
            // Animation loop back
            if (syncTime < priorSyncTime_) {
                // Rewind voice
                SoundSource* voicecSource = node_->GetChild("Voice")->GetComponent<SoundSource>();
                voiceSource->SetPositionAttr(0);
            }
            priorSyncTime_ = syncTime;
            // search from back to front
            for (int i=morphKeys_.Size()-1; i>=0; i--) {
                // find a lip sync point
                if (morphKeys_[i].time <= syncTime) {
                    // trigger lip sync when new sync point appeared
                    if (i != current_lipIndex) {
                        prior_lipIndex = current_lipIndex;
                        current_lipIndex = i;
                        lipSyncing = true;
                    }
                    break;
                }
            }
        }
        // deal with lip sync
        if (lipSyncing) {
            // Ease out
            if (prior_lipIndex != -1) {
                model->SetMorphWeight(morphKeys_[prior_lipIndex].key, EaseWeights[currentStep2 + 3]);
            }
            // Ease in
            if (current_lipIndex != -1) {
                model->SetMorphWeight(morphKeys_[current_lipIndex].key, EaseWeights[currentStep2]);
            }
            currentStep2++;
            if (currentStep2 == 4) {
                currentStep2 = 0;
                lipSyncing = false;
            }
        }
    }
}

private:
float EaseWeights[7];
bool blinking;
int currentStep;
float accuTime;
StringHash blinkEyes;

bool lipSyncing;
int currentStep2;
int prior_lipIndex;
int current_lipIndex;
float priorSyncTime_;

struct MY_MORPH_KEY {
    float time;
    StringHash key;
};

Vector<MY_MORPH_KEY> morphKeys_;

};
[/code]

Here is the demo: https://youtu.be/zIpupeSpXl4

1vanK

Nice!