home..

비주얼 노벨 게임을 위한 유사 립싱크 애니메이션 구현

개요

텍스트가 진행될 때 마다 입술이 움직이도록 하는 방법은 싱크를 맞추지 않아 부자연스럽다.

이를 해결하고 싶어서 다음과 같은 방법을 응용하고자 한다.

동물의 숲 대화 오디오 시스템은 위와 같이 특정 주기의 음절 마다 랜덤하게 pitch가 조절된 사운드 클립을 배치하여 구현한다.

일관성을 위해 완전 랜덤은 아니고 hash 값을 통해 같은 음절에는 같은 소리가 나도록 한다.

이 알고리즘을 응용하여 유사 립싱크를 구현하여 자연스러운 입술 애니메이션을 선보이고자 한다.

데이터 구조

class Config
{
    int frequencyLevel;
    public float minSpeakingTimeFactor;
    public float maxSpeakingTimeFactor;
    AnimationClip idleLip;
    AnimationClip[] speakingClips;
}

공백 같은 묵음에는 idleLip을 사용하고 frequencyLevel음절 마다 speakingClips에서 랜덤한 애니메이션을 사용한다.

같은 애니메이션이 반복되어 어색함을 줄이기 위해 pitch 대신 TimeFactor 변수를 사용하여 애니메이션 길이를 랜덤하게 줄이거나 늘린 애니메이션 클립을 사용한다.

다음은 실제 예시 코드이다.

public void OnCharacterWillAppear(int index, Yarn.Markup.MarkupParseResult line)
{
    char ch = line.Text[index];

    if(index % dialogueActorInfo.frequencyLevel != 0)
    {
        return;
    }

    // 공백, 특수문자 등 묵음 걸러냄
    if(ch.IsSilentChar())
    {
        lipAnimancer.Play(dialogueActorInfo.idleLip);
    }
    else
    {
        // animation clip
        uint hash = ch.UintHashCode();
        uint speakingIndex = (hash % (uint)dialogueActorInfo.speakingLips.Length);
        AnimationClip clip = dialogueActorInfo.speakingLips[speakingIndex];

        // animation length
        int minSpeakingTime = (int)(dialogueActorInfo.minSpeakingTimeFactor * 100);
        int maxSpeakingTime = (int)(dialogueActorInfo.maxSpeakingTimeFactor * 100);
        int rangeSpeakingTime = maxSpeakingTime - minSpeakingTime;

        if(rangeSpeakingTime != 0)
        {
            uint predictableSpeakingTime = (uint)minSpeakingTime + hash % (uint)rangeSpeakingTime;
            float speakingTime = predictableSpeakingTime / 100f * clip.length; 
            lipAnimancer.Play(clip, speakingTime).Time = 0;
        }
        else
        {
            lipAnimancer.Play(clip).Time = 0;
        }
    }            
}

데모

한계점

참고

© 2025 HookSSi   •  Powered by Soopr   •  Theme  Moonwalk