【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秒くらいで口パクの変動を行うのが良いようです。

完成プロジェクト

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

参考