部位で異なるモーション管理を行う

最終更新: 2020年1月10日

このページでは、右手左手で異なるモーションを並列で管理することを実現するために、
LAppModel.cubismMotionManagerの保有数を増やしてLAppModel.updateに組み込む場合を例として、
管理やモーション作成上の注意点などを見ていきます。

CubismMotionManagerを増やす理由

CubismMotionManagerはフェードに対応するため一時的な複数のモーションを再生する機能があります。
しかし、再生開始のAPIは再生中のモーションをフェードアウトさせて終了するため、
並列でモーションを再生し続ける機能はありません。

この問題を解決するために、モーションマネージャのインスタンスを複数用意し、
再生させるモーションマネージャをすみ分けることによって並列でのモーション再生を実現させます。

CubismMotionManagerを増やす

モーションマネージャを追加

まずはプログラム側の実装を見ていきます。
最初に行うのはモデルへのモーション管理クラスの追加です。
サンプルのLAppModelへCubismMotionManagerを追加していきます。
コンストラクタとrelease関数にも生成と解放の処理を追加します。

export class LAppModel extends CubismUserModel {
  
/*省略*/
 
    /**
     * コンストラクタ
     */
    public constructor() 
    {
        super();

/*省略*/
       
        this._rightArmMotionManager = new CubismMotionManager(); // <<<追加!
        this._leftArmMotionManager = new CubismMotionManager();  // <<<追加!
    }
    _rightArmMotionManager: CubismMotionManager;    /// <<< 追加!
    _leftArmMotionManager: CubismMotionManager;     /// <<< 追加!

}

更新処理

Updateの処理にも追加して再生されたモーションがパラメータに影響を与えるようにします。

public update(): void
{

/*省略*/

  	//--------------------------------------------------------------------------
    this._model.loadParameters();   // 前回セーブされた状態をロード
    if(this._motionManager.isFinished())
    {
        // モーションの再生がない場合、待機モーションの中からランダムで再生する
        this.startRandomMotion(LAppDefine.MotionGroupIdle, LAppDefine.PriorityIdle);
        
    }
    else
    {
        motionUpdated = this._motionManager.updateMotion(this._model, deltaTimeSeconds);    // モーションを更新
    }
    motionUpdated != this._rightArmMotionManager.updateMotion(this._model, deltaTimeSeconds);   // <追加
    motionUpdated != this._leftArmMotionManager.updateMotion(this._model, deltaTimeSeconds);    // <追加
    this._model.saveParameters(); // 状態を保存
    //--------------------------------------------------------------------------

/*省略*/

}

LAppModel.update内部に追加するときは、_model.loadParameters()と_model.saveParameters()の間に
updateMotionを設置するようにしてください。
また、同時に複数のモーションを再生すると、2つ以上のモーションから同一のパラメータの値が更新されることがあります。
この場合、後に実行されたupdateMotionの内容が優先されます。

再生開始

そして、startMotion系のメンバ関数を複製、改造する形で追加したモーションマネージャでモーション再生を指示できるようにしておきます。
startHandMotion関数では、引数としてモーションマネージャを指定することに注意してください。
なお以下の例では、.motion3.jsonで指定されているモーション(preLoad関数でロードされるモーション)のみ再生する実装となっています。

// startMotionより複製して作成
public startHandMotion(targetManage: CubismMotionManager, group: string, no: number, priority: number): CubismMotionQueueEntryHandle
{
    if(priority == LAppDefine.PriorityForce)
    {
        targetManage.setReservePriority(priority);
    }
    else if(!targetManage.reserveMotion(priority))
    {
        if(this._debugMode)
        {
            LAppPal.printLog("[APP]can't start motion.");
        }
        return InvalidMotionQueueEntryHandleValue;
    }
    
    const fileName: string = this._modelSetting.getMotionFileName(group, no);

    //ex) idle_0
    let name: string = Utiles.CubismString.getFormatedString("{0}_{1}", group, no);
    let motion: CubismMotion = this._motions[name];
    let autoDelete = false;

    if(motion == null)
    {
        // preLoadで読まれていない場合は再生しない
        return InvalidMotionQueueEntryHandleValue;
    }

    if(this._debugMode)
    {
        LAppPal.printLog("[APP]start motion: [{0}_{1}]", group. no);
    }
    return targetmanage.startMotionPriority(motion, autoDelete, priority);
}

// startRandomMotionより複製して作成
public startRandomRightHandMotion(group: string, priority: number): CubismMotionQueueEntryHandle
{
    if(this._modelSetting.getMotionCount(group) == 0)
    {
        return InvalidMotionQueueEntryHandleValue;
    }

    let no: number = Math.floor(Math.random() * this._modelSetting.getMotionCount(group));
    
    return this.startHandMotion(this._rightArmMotionManager, group, no, priority);
}

// startRandomMotionより複製して作成
public startRandomLeftHandMotion(group: string, priority: number): CubismMotionQueueEntryHandle
{
    if(this._modelSetting.getMotionCount(group) == 0)
    {
        return InvalidMotionQueueEntryHandleValue;
    }

    let no: number = Math.floor(Math.random() * this._modelSetting.getMotionCount(group));

    return this.startHandMotion(this._leftArmMotionManager, group, no, priority);
}

モーション再生はモデルに当たり判定を追加して、クリックによって動作するようにします。

public onTap(x: number, y: number): void
{
    if(LAppDefine.DebugLogEnable)
    {
        LAppPal.printLog("[APP]tap point: {x: {0} y: {1}}", x.toFixed(2), y.toFixed(2));
    }

    for(let i: number = 0; i < this._models.getSize(); i++)
    {
        if(this._models.at(i).hitTest(LAppDefine.HitAreaNameHead, x, y))
        {
            if(LAppDefine.DebugLogEnable)
            {
                LAppPal.printLog("[APP]hit area: [{0}]", LAppDefine.HitAreaNameHead);
            }
            this._models.at(i).setRandomExpression();
        }
        else if(this._models.at(i).hitTest(LAppDefine.HitAreaNameBody, x, y))
        {
            if(LAppDefine.DebugLogEnable)
            {
                LAppPal.printLog("[APP]hit area: [{0}]", LAppDefine.HitAreaNameBody);
            }
            this._models.at(i).startRandomMotion(LAppDefine.MotionGroupTapBody, LAppDefine.PriorityNormal);
        }

        // ここから追加
        else if(this._models.at(i).hitTest("Right", x, y))
        {
            if(LAppDefine.DebugLogEnable)
            {
                LAppPal.printLog("[APP]hit area: [{0}]", LAppDefine.HitAreaNameBody);
            }
            this._models.at(i).startRandomRightHandMotion("Right", LAppDefine.PriorityForce);
        }
        else if(this._models.at(i).hitTest("Left", x, y))
        {
            if(LAppDefine.DebugLogEnable)
            {
                LAppPal.printLog("[APP]hit area: [{0}], LAppDefine.HitAreaNameBody");
            }
            this._models.at(i).startRandomLeftHandMotion("Left", LAppDefine.PriorityForce);
        }
        // ここまで追加
    }
}

担当パラメータの振り分け

更新処理でも示したように、再生する複数のモーション間でパラメータが重複すると、後の方に実行された更新内容が優先されてパラメータが上書きされます。
このためモーションマネージャを複数使用するときには、どのモーションマネージャがどの部位の動きを担当するのか?
ひいてはどのパラメータ操作を担当するのか決めておく必要があります。

上の動画は、優先順位の高いモーションマネージャ(左腕・右腕のモーションを担当)の再生が終了した直後に、優先順位の低いモーションマネージャ(モデル全体のアイドルモーションを担当)によって左腕・右腕のパラメータが更新されてしまった事例です。
優先順位の高いモーションマネージャによって更新されるパラメータを、モーション再生終了直後の状態に維持しておきたい場合には、望ましい表現とは言えません。
この現象は、優先順位が低いモーションマネージャで再生するモーションデータ内に更新対象としては意図していないパラメータに基準値が設定されている場合に見られます。
この時、モーションデータごとに更新するパラメータを分離しておくことで、優先順位の高いモーションマネージャによって更新されるパラメータを、再生終了直後の状態に維持しておくことができます。

パラメータごとに分離しておくのか、上書きを前提とするのか、
膨大な修正をしないためにもモーションの作成前に仕様をはっきりと定めておくことが重要です。

この記事に関するご意見・
ご要望をお聞かせください。