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;
}
}
}
데모
한계점
- 유사 립싱크이기 때문에 완벽히 자연스러운 효과를 기대하기는 힘들다.
speakingClips
가 적을수록 어색함이 눈에 보일 수 있다.