【Godot】口パク(リップシンク)の実装方法

この記事では音声に合わせて口パク(リップシンク)を実装する方法について解説します

口パクの実装方法

素材データの作成と登録

まずは口パクを行うキャラクターの画像と音声を用意します。

素材データとして以下のものを用意しました。

最低限2パターン(口を閉じる・口を開く)あれば問題ないですが、今回は3パターン用意しています

  • 口を閉じている: ch01_1.png
  • 口を少し開いている: ch01_2.png
  • 口を大きく開いている: ch01_3.png

プロジェクトを作成し、これらの画像を追加しておきます。

それとボイスデータを追加します。

これは音読さんという音声作成ツールで作成したものとなります

これらもプロジェクトに追加しておきます

なお、mp3ファイルを使用する場合、自動的にループ設定がされているので、「インポート」タブから Loop のチェックを外して「再インポート」をクリックすると、ループ設定が無効となります

AnimatedSpriteで口パクアニメーションを作成する

Node2Dを作成し、ノード名を「Main」にリネームしておきます。

そして「AnimatedSprite」を追加します。

インスペクターから「Frames > [空]」をクリックして「新規 SpriteFrames」を選びます。

作成した「SpriteFrames」をクリックして、アニメーションフレームを表示します。

default には「ch01_1.png」のみ登録します。

次にアニメーションを追加してアニメーション名を「lipsync1」にリネームし、「ch01_1.png」と「ch01_2.png」を登録します。

速度は10フレームにしておきます。

続けてアニメーションを追加してアニメーション名を「lipsync2」にリネームし、「ch01_1.png」と「ch01_3.png」を登録します。

こちらも速度は10フレームにしておきます。

インスペクターから「Playing」にチェックを入れると口パクアニメーションが確認できます。

オーディオバスの作成とエフェクトの追加

次にボイス再生用のオーディオバスを作成します。

画面下部にある「オーディオ」をクリックしてレイアウトを表示して、「バスを追加」をクリックします。

すると新しいバスが作られるので、名前を「Voice」にリネームしておきます。

なお、ボイス用にバスを分ける理由は、Godotは波形データから直接音量を取得することができず、バス経由で音量を取得します。その際にバスを分けておかないと、他のサウンドの音量も拾ってしまうためとなります。

次に「エフェクト」をクリックして「SpectrumAnalyzer」を選びます。

音量を引っ張るためには「SpectrumAnalyzer」が都合良いみたいでしたので、これを使います。

AudioStreamPlayer2Dの追加

ボイスを再生するためのプレイヤー「AudioStreamPlayer2D」ノードを追加します。

追加したら、インスペクタから「Bus」を「Voice」に変更します。

これによりこのプレイヤーで再生したサウンドは「Voice」バスに送られます。

Labelの追加

音量を数値で確認するために Labelノードを追加しておきます

これはデバッグ用なので、リップシンクに必要というわけではありません

スクリプトの実装

Mainノードにスクリプトを追加して以下のように記述します。

extends Node2D

const FREQ_MAX = 11050.0 # 最大周波数
const VU_COUNT = 16 # 周波数の分解度
const MIN_DB = 60

var spectrum = null # スペクトラムエフェクト
var db_now = 0.0 # 現在の音量
var past_time = 0.0 # 経過時間
var db_list = [] # 音量の平均を取るためのリスト

onready var player = $AudioStreamPlayer2D
onready var label = $Label
onready var sprite = $AnimatedSprite

func _ready():
    # スペクトラムエフェクトを取得
    # バス番号1 (Voice)
    # エフェクト番号0 (Spectrum)
    spectrum = AudioServer.get_bus_effect_instance(1, 0)
    
func _process(_delta):
    if Input.is_action_just_pressed("ui_accept"):
        # ボイス再生
        player.stream = load("res://test1.mp3")
        #player.stream = load("res://test2.mp3")
        player.play()
    
    # 最大音量を取得.
    var max_energy = _get_max_energy()
    db_list.append(max_energy)
    
    # 経過時間を加算
    past_time += _delta
    if past_time > 0.2:
        # 0.2秒ごとに平均を求める
        _calc_db_average()
        past_time = 0
    
    if player.playing == false:
        # 再生していなければ音量を0にする
        db_now = 0
    
    # リップシンク更新
    _update_lipsync()

    label.text = "Volume: %3.2f"%db_now

func _calc_db_average():
    # 音量の平均を求める
    var sum = 0
    for v in db_list:
        sum += v
    # 平均を求める
    db_now = sum / db_list.size()
    db_list.clear()

func _get_max_energy():
    # 音量の最大値を取得する
    var prev_hz = 0
    var max_energy = 0
    
    # 各周波数ごとの音量から最大値を取り出す
    for i in range(1, VU_COUNT+1):
        var hz = i * FREQ_MAX / VU_COUNT;
        var magnitude: float = spectrum.get_magnitude_for_frequency_range(prev_hz, hz).length()
        var energy = clamp((MIN_DB + linear2db(magnitude)) / MIN_DB, 0, 1)
        if energy > max_energy:
            max_energy = energy # 最大音量
        prev_hz = hz
    
    return max_energy

func _update_lipsync():
    if db_now < 0.1:
        # 口パク停止
        sprite.play("default")
    else:
        if db_now > 0.4:
            # 大きい口パク
            sprite.play("lipsync2")
        else:
            # 小さい口パク
            sprite.play("lipsync1")

 

実行して動作を確認します。

口パクアニメーションの速度が 10フレーム だと少し速いような気がしたので、7フレームに減らしました。

Enter/Spaceキーを押すとボイスが再生され、それに合わせてキャラクターが口パクします。

スクリプトの簡単な説明ですが、音量を計算する際に、0.2秒ごとにその区間の音量の平均を取っていますが、これには理由があります。

音量というのは小さい区間では上下の変動が大きいです。なので平均を取らずに口の大きさを決めてしまうと、今回のような口の大きさのパターンが少ない場合は口の動きが激しくなってしまいます。

そのため、0.2秒くらいで口パクの変動を行うのが良いようです。

完成プロジェクト

今回作成したプロジェクトファイルを添付しておきます

参考