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

最終更新: 2022年12月8日

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

CubismMotionManagerを増やす理由

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

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

CubismMotionManagerを増やす

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

まずはプログラム側の実装を見ていきます。
最初に行うのはモデルへの管理クラスの追加です。
サンプルのLAppModelへCubismMotionManagerを追加していきます。
また、コンストラクタに生成処理を追加します。

public class LAppModel extends CubismUserModel{
    ...
    private CubismMotionManager rightArmMotionManager; // 追加
    private CubismMotionManager leftArmMotionManager; // 追加
}
public LAppModel(){
    ...
    rightArmMotionManager = new CubismMotionManager(); // 追加
    leftArmMotionManager = new CubismMotionManager(); // 追加
}

更新処理

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

public void update(){
    ...
    // 前回セーブされた状態をロード
    _model.loadParameters();

    // モーションの再生がない場合、待機モーションの中からランダムで再生する
    if(_motionManager.isFinished()){
        startRandomMotion(LAppDefine.MotionGroup.IDLE.getId(), LAppDefine.Priority.IDLE.getPriority());
    } else{
        // モーションを更新
        isMotionUpdated = _motionManager.updateMotion(_model, deltaTimeSeconds);
    }
  
    isMotionUpdated |= rightArmMotionManager.updateMotion(_model, deltaTimeSeconds); // 追加
    isMotionUpdated |= leftArmMotionManager.updateMotion(_model, deltaTimeSeconds); // 追加

    // モデルの状態を保存
    _model.saveParameters();
}

LAppModel.update内部に追加するときは、_model.loadParameters();と_model.saveParameters(); の間にupdateMotionを設置するようにしてください。
また、updateMotionを複数回実行すると、更新するパラメータが重複することがあります。
この場合、後に実行されたupdataMotionの内容が優先されることに注意してください。

再生開始

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

// startMotionより複製して作成
public int startHandMotion(
    CubismMotionManager targetManage,
    final String group,
    int number,
    int priority
) {
    if (priority == LAppDefine.Priority.FORCE.getPriority()) {
        targetManage.setReservationPriority(priority);
    } else if (!targetManage.reserveMotion(priority)) {
        if (_debugMode) {
            LAppPal.logger.print("Cannot start motion.");
            return -1;
        }
    }
    final String fileName = _modelSetting.getMotionFileName(group, number);

    // ex) idle_0
    String name = group + "_" + number;
    CubismMotion motion = (CubismMotion) _motions.get(name);

    if (motion == null) {
        // PreLoadで読まれていない場合は再生しない
        return -1;
    }
    if (_debugMode) {
        LAppPal.logger.print("Start motion: [" + group + "_" + number + "]");
    }
    
    return targetManage.startMotionPriority(motion, priority);
}

// startRandomMotionより複製して作成
public int startRandomRightHandMotion(final String group, int priority) {
    if(_modelSetting.getMotionCount(group) == 0){
        return -1;
    }

    Random random = new Random();
    int number = random.nextInt(Integer.MAX_VALUE) % _modelSetting.getMotionCount(group);
    return startHandMotion(rightArmMotionManager, group, number, priority);
}

// startRandomMotionより複製して作成
public int startRandomLeftHandMotion(final String group, int priority) {
    if(_modelSetting.getMotionCount(group) == 0){
        return -1;
    }

    Random random = new Random();
    int number = random.nextInt(Integer.MAX_VALUE) % _modelSetting.getMotionCount(group);
    return startHandMotion(leftArmMotionManager,group,number,priority);
}


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

public void onTap(float x, float y) {
    if (DEBUG_LOG_ENABLE) {
        CubismFramework.coreLogFunction("tap point: {" + x + ", y: " + y);
    }

    for (LAppModel model : _models) {
        // 頭をタップした場合表情をランダムで再生する
        if (model.hitTest(HitAreaName.HEAD.getId(), x, y)) {
            if (DEBUG_LOG_ENABLE) {
                LAppPal.logger.print("hit area: " + HitAreaName.HEAD.getId());
            }
            model.setRandomExpression();
        }
        // 体をタップした場合ランダムモーションを開始する
        else if (model.hitTest(HitAreaName.BODY.getId(), x, y)) {
            if (DEBUG_LOG_ENABLE) {
                LAppPal.logger.print("hit area: " + HitAreaName.BODY.getId());
            }
            model.startRandomMotion(MotionGroup.TAP_BODY.getId(), Priority.NORMAL.getPriority(), _finishedMotion);
        }
      
        // ここから追加
        else if (model.hitTest("Right", x, y)) {
            if (DEBUG_LOG_ENABLE) {
                LAppPal.logger.print("hit area: [" + HitAreaName.BODY.getId() + "]");
            }
            model.startRandomRightHandMotion("Right", Priority.FORCE.getPriority());
        } else if (model.hitTest("Left", x, y)) {
            if (DEBUG_LOG_ENABLE) {
                LAppPal.logger.print("hit area: [" + HitAreaName.BODY.getId() + "]");
            }  
            model.startRandomLeftHandMotion("Left", Priority.FORCE.getPriority());
        }
        // ここまで追加
    }
}

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

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

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

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

この記事はお役に立ちましたか?
はいいいえ
この記事に関するご意見・
ご要望をお聞かせください。