はじめに

 今回は、hgimg4による3Dモデルのアニメーション再生についてです。 用語の説明からアニメーションクリップのブレンドまでやってみます。

3Dモデルを動かす仕組み

 3Dモデルを動かす仕組みを確認しておきます。3Dモデルは図のように3Dモデル(骨を取り囲むU時の部分)に骨(水色の丸と三角で示した部分)を入れ、ボーンが動くと連動して周りの3Dモデルも動くように設定されています。 このように動く仕組みの設定をリグと言います。また、リグを作成してモデルを動かせるような状態にすることをリギングといいます。

リグ

 図について解説しておきます。水色の丸と三角で示した骨の部分が「ボーン」。 ボーンを取り囲むU時の部分が「3Dモデル」。 図の上側がボーンを動かす前、下側がボーンを動かした後。 動かしたボーンと3Dモデルの赤い部分が関連付けられているため、ボーンを動かしたときに連動して動く。

動かせる3Dモデルの骨組みの名前

 ここからは、用語が多いので図を見ながら整理してみます。今回は、Mayaの資料を参考にしました。資料はこちら。

ジョイントとボーン - ジョイント階層
https://knowledge.autodesk.com/ja/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2016/JPN/Maya/files/GUID-1B59334F-2605-44C3-B584-A55B239A2CBE-htm.html

スケルトン

 1体の3Dモデルに設定されたボーンの集合全体のことを「スケルトン(スケルトン階層)」と呼ばれているようです。 必ずツリー構造をしており、循環しない構造になっています。(Blenderの場合は、アーマチュア(Armature)と呼ばれます。)

スケルトン

ジョイントとボーン

 スケルトンは、ジョイントとボーンで構成されています。 図の丸い部分がジョイント、三角形で示した部分がボーンです。 ジョイントは体の関節のように動きます。ボーンはジョイントとジョイントをつなぐ可視化するための直線です。 ボーンの根元には必ずジョイントが存在しています。

 また、ジョイントとボーンが一連に接続されたもので、一直線に数珠つなぎになっているものをジョイントチェーンと呼びます。

ジョイントとボーン

親ジョイント・子ジョイント

 ボーンの根元側のジョイントを親ジョイントと呼びます。 親ジョイントは必ずスケルトン階層の上位側に存在します。 逆にボーンの先端側につながっている次のジョイントを子ジョイントと呼びます。 子ジョイントは、スケルトン階層の下位側に位置します。

 親ジョイントを動かすと、その子ジョイントも一緒に動きます。親ジョイントを動かしてもその上流側(親側)には影響がありません。 このあたりの関係は、現実の人形と同じですね。

親ジョイント・子ジョイント

ルートジョイント

 スケルトン全てのジョイントを階層構造の上位側(親側)にたどっていくと、必ず最上位に位置する1つのジョイントに到達します。 このジョイントをルートジョイントと呼びます。

 ルートジョイントを移動したり回転すると、3Dモデル全体が移動したり回転したりします。

ルートジョイント

データの状態

 hgimg4で取り扱うモデルデータ(.gpbファイル)には、次のような情報が格納されています。

  • 3Dモデルデータ
     頂点とポリゴン(面)で構成された形を構成するデータ。
  • UV座標
     3Dモデルの面毎に貼り付けるテクスチャの位置情報。
  • スケルトン
     キャラクターを動かすための骨。
  • リグ
     スケルトンと3Dモデルデータを関連付けするための情報。
  • アニメーション
     スケルトンを動かしてキャラクターに動きをつけたモーションデータ。

 テクスチャは別途画像ファイルになっています。

アニメーション

 hgimg4ではUnityの「Animation(Legacy)」という機能を参考に作成されているようです。 より深く理解したい場合は、Unity関係の資料が役に立つかもしれません。

 1つのモデルデータ(.gpbファイル)には、1つのアニメーションだけが格納されているようです。 例えば、HSP3付属のpronama3dフォルダに入っているサンプルだと、pronamachan.gpb はプロ生ちゃんがダンスするモーション。 pronamachan_l.gpbはプロ生ちゃんが周囲をキョロキョロと見渡すモーションだけが入っています。

 そのまま再生するとアニメーションが最初から最後までループ再生されます。 しかしゲームを作る場合は、待機・走る・ジャンプ・攻撃・防御…など色々な動きが必要です。1つのモデルに1つのアニメーションしか保存できないとなるとゲーム用途としては不便です。そこでhgimg4ではアニメーションクリップという仕組みが用意されています。

 アニメーションの再生は、作成したアニメーションクリップ単位で行うことができます。 アニメーションクリップは、モデルデータ内のアニメーションの一部時間帯を切り取って作成することができます。 1つのアニメーションから複数のアニメーションクリップを作成して、キャラクターの操作状況に応じて再生するアニメーションクリップを切り替えて使用します。

デフォルトのアニメーションクリップ

 標準のサンプルtamane2.hspでは、アニメーションクリップを作成せずにアニメーションクリップを再生しています。

gpact id_model

 これはモデルデータを読み込んだ際に、自動的にデフォルトのアニメーションクリップが1つ作成されるため、問題なく再生することができます。 デフォルトのアニメーションクリップは、次のような内容です。

  • アニメーションクリップ名:_idle
  • アニメーションクリップのインデックス:0
  • 再生時間:全時間
  • ブレンド係数:100%
  • 再生スピード:100%

アニメーションクリップ

 デフォルトのアニメーションクリップ以外は、自分で作成する必要があります。 gpaddanim命令を使うと、アニメーションから任意の時間を切り取ってアニメーションクリップにすることができます。

 gpaddanim命令でアニメーションクリップを追加すると、アニメーションクリップのインデックスが自動的に割り当てられます。 インデックスは作成した順番に、0,1,2,3…のような 0以上の重複しない整数が小さい値から順に割り当てられます。 アニメーションクリップは、主にこのインデックスで管理することになります。分からなくならないよう注意が必要です。

 付属サンプルのtamane1.hspやtamane2.hspでアニメーションクリップを取り扱っています。 サンプルのアニメーションクリップがどのようになっているか確認してみます。 サンプル内のメインループに入る直前に次のスクリプトを挿入すると、デバッグウィンドウのログ タブでアニメーションクリップの情報を確認することができます。


repeat
	gpgetanim name, id_model, cnt, GPANIM_OPT_NAME
	if stat < 0 : break
	logmes "ID " + cnt + " : " + name
	gpgetanim startf,   id_model, cnt, GPANIM_OPT_START_FRAME ; 開始フレーム(ミリ秒単位)
	gpgetanim endf,     id_model, cnt, GPANIM_OPT_END_FRAME   ; 終了フレーム(ミリ秒単位)
	gpgetanim duration, id_model, cnt, GPANIM_OPT_DURATION    ; 再生の長さ(ミリ秒単位)
	gpgetanim elapsed,  id_model, cnt, GPANIM_OPT_ELAPSED     ; 経過時間(ミリ秒単位)
	gpgetanim blend,    id_model, cnt, GPANIM_OPT_BLEND       ; ブレンド係数(%単位)
	gpgetanim isplay,   id_model, cnt, GPANIM_OPT_PLAYING     ; 再生中フラグ(0=停止/1=再生)
	gpgetanim speed,    id_model, cnt, GPANIM_OPT_SPEED       ; 再生スピード(%単位)
	logmes "開始フレーム(ミリ秒単位)   : " + startf
	logmes "終了フレーム(ミリ秒単位)   : " + endf
	logmes "再生の長さ(ミリ秒単位)     : " + duration
	logmes "経過時間(ミリ秒単位)       : " + elapsed
	logmes "ブレンド係数(%単位)        : " + blend
	logmes "再生中フラグ(0=停止/1=再生): " + isplay
	logmes "再生スピード(%単位)        : " + speed
loop

 tamane1.hsp には、1.666秒のモーションが入っていて、サンプルでは0~0.7秒を切り取ったアニメーションクリップを作成して再生しています。 残りの時間は、走っているポーズのまま動かないアニメーションになっていますね。

 tamane2.hsp は、デフォルトのアニメーションクリップ(全時間)を再生しています。

 どちらも単一のモーションしか含まれていないので、アニメーションクリップの練習に使うには難しそうですね。困った。 まだまだ開発中なので、整備が追いついていないんでしょうね。

アニメーションをブレンド

 アニメーションクリップは、同時再生することができます。 複数を同時に再生した場合、キャラクターは同時に再生されたアニメーションを混ぜた(ブレンド)した動きをさせることができます。 アニメーションクリップ設定のブレンド係数[%]を調整することで、アニメーションクリップの重み付けを設定することができます。 重み付けを設定しない場合(どれかがブレンド係数100%の場合)は、1つのアニメーションクリップだけしか見えない結果になるようです。

 hgimg4には、アバターマスク(キャラクターの体の一部のモーション設定を無効にする)機能がないため単純に混ぜることしかできません。 現在のバージョンで考えられる用途としては、例えば次のような感じでしょうか。

 2つのアニメーションクリップを切り替える際、再生時間がオーバーラップしている時間帯のブレンド率をなめらかに変化させることで動きをスムーズに切り替える。 例えば立っているだけの待機モーションと走るモーションとが切り替わる際、2つのモーションをただ繋げただけだと急にポーズが変わる為不自然です。 かと言って切り替わるときだけ使用する専用のモーションを作るとなると大変です。 2つのモーションを同時再生して、ブレンド率を滑らかに推移させれば2つのモーションが滑らかに切り替わるのでそれなりに自然な動きになります。 もしかするとまだまだ活用方法があるかもしれませんが、今のところ気がついたのはこれぐらいです。

サンプル

 2つのアニメーションクリップを作成して、滑らかに切り替えるサンプルを作成してみました。

 スペースキーをすと、2つのモーションクリップを滑らかに切り替えます。


#include "hgimg4.as"

title "HGIMG4 Test"

gpreset

setcls CLSMODE_SOLID, $404040

gpload id_model,"res/tamane2"		; モデル読み込み
setang id_mode, 0, 3.141592			; モデルの回転を設定
setscale id_model, 0.1,0.1,0.1

setpos GPOBJ_CAMERA, -30,20,30		; カメラ位置を設定

;-----------------------------
;	アニメーションクリップ
;-----------------------------
; この時点でのアニメーションクリップ
; ID   :0
; 名前  :_idle
; フレーム:全フレーム

; 設定されているアニメーションクリップ一覧
; 結果はデバッグウィンドウで確認できます。
repeat
	gpgetanim name, id_model, cnt, GPANIM_OPT_NAME
	if stat < 0 : break
	logmes "ID " + cnt + " : " + name
	gpgetanim startf,   id_model, cnt, GPANIM_OPT_START_FRAME ; 開始フレーム(ミリ秒単位)
	gpgetanim endf,     id_model, cnt, GPANIM_OPT_END_FRAME   ; 終了フレーム(ミリ秒単位)
	gpgetanim duration, id_model, cnt, GPANIM_OPT_DURATION    ; 再生の長さ(ミリ秒単位)
	gpgetanim elapsed,  id_model, cnt, GPANIM_OPT_ELAPSED     ; 経過時間(ミリ秒単位)
	gpgetanim blend,    id_model, cnt, GPANIM_OPT_BLEND       ; ブレンド係数(%単位)
	gpgetanim isplay,   id_model, cnt, GPANIM_OPT_PLAYING     ; 再生中フラグ(0=停止/1=再生)
	gpgetanim speed,    id_model, cnt, GPANIM_OPT_SPEED       ; 再生スピード(%単位)
	logmes "開始フレーム(ミリ秒単位)   : " + startf
	logmes "終了フレーム(ミリ秒単位)   : " + endf
	logmes "再生の長さ(ミリ秒単位)     : " + duration
	logmes "経過時間(ミリ秒単位)       : " + elapsed
	logmes "ブレンド係数(%単位)        : " + blend
	logmes "再生中フラグ(0=停止/1=再生): " + isplay
	logmes "再生スピード(%単位)        : " + speed
loop
	
; GPBファイルが保持しているフレーム長さ以上を指定すると落ちます。
gpaddanim id_model,"walk"	; ID 1
gpaddanim id_model,"run"	; ID 2
gpsetanim id_model, 1, GPANIM_OPT_SPEED, 100	; walk
gpsetanim id_model, 2, GPANIM_OPT_SPEED, 300	; run  スピードアップ!

; アニメーションをブレンド再生
gpact id_model,"run"
gpact id_model,"walk"

; 存在しないアニメーションクリップを指定すると
; モーション適用前のポーズになるようです。
;gpact id_model,"stop"

; アニメーションブレンド率
blend = 100
; アニメーションをオーバーラップさせる時間長さ
cntBlendMax = 60*3

*main
	stick key,15
	if key&128 : end

	redraw 0			; 描画開始

	;-----------------------------
	;	カーソルキーでカメラ位置を動かす
	;-----------------------------
	if key&1 : addpos GPOBJ_CAMERA, -1.0, 0
	if key&4 : addpos GPOBJ_CAMERA,  1.0, 0
	if key&8 : addpos GPOBJ_CAMERA,  0,   0 , 1.0
	if key&2 : addpos GPOBJ_CAMERA,  0,   0 , -1.0
	gplookat GPOBJ_CAMERA, 0,14,0		; カメラから指定した座標を見る

	;-----------------------------
	;	モーション切り替え
	;-----------------------------
	; ブレンド率をなめらかに変化させてモーションを切り替えます。
	; 今回は元が同じモーションなので、タイミン次第では動作を打ち消し合う形となり
	; 一瞬動きが止まったような動作をすることがあります。
	if key&16 : iswalk ^= 1
	if iswalk {
		cntBlend++
	} else {
		cntBlend--
	}
	cntBlend = limit(cntBlend, 0, cntBlendMax)
	; イージング関数でブレンド速度を調整
	; 普通はリニアでいいと思います。
	; setease 0, 100, ease_linear		; リニア(直線補間)
	setease 0, 100, ease_quartic_inout	; 加速→減速(Quartic)
	blend = getease(cntBlend, cntBlendMax)
	; ブレンド率を変える
	gpsetanim id_model, 1, GPANIM_OPT_BLEND, blend
	gpsetanim id_model, 2, GPANIM_OPT_BLEND, 100 - blend

	;-----------------------------
	;	描画
	;-----------------------------
	;addang id_model,0,0.02		; ノード回転
	gpdraw				; シーンの描画

	color 255,255,255
	pos 8,8:mes "HGIMG4 sample"
	mes "スペースでモーションを切り替え"
	mes "ブレンド率(walk):" + blend + " %"
	
	

	redraw 1			; 描画終了
	await 1000/60			; 待ち時間

	goto *main

解説

 tamane2は走るアニメーションしかないので、同じ時間帯のアニメーションを再生速度を変えることで2つのモーションとしています。


gpaddanim id_model,"walk"	; ID 1
gpaddanim id_model,"run"	; ID 2
gpsetanim id_model, 1, GPANIM_OPT_SPEED, 100	; walk
gpsetanim id_model, 2, GPANIM_OPT_SPEED, 300	; run  スピードアップ!

 ブレンド率はイージング関数を使って滑らかに推移させています。普通はリニアでいいと思います。 ブレンド率の振り分けは、合計が100%になるように次のようにして振り分けています。


gpsetanim id_model, 1, GPANIM_OPT_BLEND, blend
gpsetanim id_model, 2, GPANIM_OPT_BLEND, 100 - blend

 切り替え中は、足がもつれたような瞬間が発生します。 繰り返し動作の速度を変えただけの同じモーションであるため、タイミングによっては完全に動作を打ち消し合ってしまう(例えば右足を前に出すアニメーションと右足を後ろに引くアニメーションになる)瞬間と 2つのアニメーションクリップが全く同じ動作のために動作が変わらない瞬間とができてしまい、その結果もつれたように見えてしまうのだと思います。

 サンプルとして使うには、相性が良くありませんでしたね。 走る速さを切り替えるだけなら再生スピード(GPANIM_OPT_SPEED)を滑らかに変えるだけの方がいいと思います。 いいサンプルデータほしいな。