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

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

CubismMotionManagerを増やす

モデルへのマネージャ追加

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

class LAppModel : public Csm::CubismUserModel
{
  ・
  ・
  ・

    Live2D::Cubism::Framework::CubismMotionManager* _rightArmMotionManager; ///<追加!
    Live2D::Cubism::Framework::CubismMotionManager* _leftArmMotionManager; ///<追加!

};
LAppModel::LAppModel()
    : CubismUserModel()
    , _modelSetting(NULL)
    , _userTimeSeconds(0.0f)
{
  ・
  ・
  ・
    _rightArmMotionManager = new CubismMotionManager(); ///<追加!
    _leftArmMotionManager = new CubismMotionManager(); ///<追加!
}

LAppModel::~LAppModel()
{
  ・
  ・
  ・
    CSM_DELETE(_rightArmMotionManager); ///<追加!
    CSM_DELETE(_leftArmMotionManager); ///<追加!
}

更新処理

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

void LAppModel::Update()
{
 ・
 ・
 ・
    //-----------------------------------------------------------------
    _model->LoadParameters(); // 前回セーブされた状態をロード
    if (_motionManager->IsFinished())
    {
        // モーションの再生がない場合、待機モーションの中からランダムで再生する
        StartRandomMotion(MotionGroupIdle, PriorityIdle);
    }
    else
    {
        motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新
    }
    motionUpdated |= _rightArmMotionManager->UpdateMotion(_model, deltaTimeSeconds); ///<追加!
    motionUpdated |= _leftArmMotionManager->UpdateMotion(_model, deltaTimeSeconds); ///<追加!
    _model->SaveParameters(); // 状態を保存
    //-----------------------------------------------------------------
 ・
 ・
 ・
}

LAppModel::Update内部に追加するときは、_model->LoadParameters();と_model->SaveParameters();の間に
UpdateMotionを設置するようにしてください。
また、UpdateMotionを複数回実行すると、更新するパラメーターが被ることがあります。
この場合、後に実行されたUpdataMotionの内容が優先されることに注意してください。

再生開始

そして、StartMotion系のメンバ関数を複製、改造する形で
追加したモーションマネージャでモーション再生を指示できるようにしておきます。

StartHandMotion関数では、引数としてモーションマネージャを指定することに注意してください。
なお以下の例では、.motion3.jsonで指定されているモーション(PreLoad関数でロードされるモーション)のみ再生する実装となっています。

//StartMotionより複製して作成
CubismMotionQueueEntryHandle LAppModel::StartHandMotion(CubismMotionManager* targetManage, const csmChar* group, csmInt32 no, csmInt32 priority)
{
    if (priority == PriorityForce)
    {
        targetManage->SetReservePriority(priority);
    }
    else if (!targetManage->ReserveMotion(priority))
    {
        if (_debugMode) LAppPal::PrintLog("[APP]can't start motion.");
        return InvalidMotionQueueEntryHandleValue;
    }

    const csmString fileName = _modelSetting->GetMotionFileName(group, no);

    //ex) idle_0
    csmString name = Utils::CubismString::GetFormatedString("%s_%d", group, no);
    CubismMotion* motion = static_cast<CubismMotion*>(_motions[name.GetRawString()]);
    csmBool autoDelete = false;

    if (motion == NULL)
    {
        //PreLoadで読まれてない場合は再生しない
        return InvalidMotionQueueEntryHandleValue;
    }
    
    if (_debugMode)LAppPal::PrintLog("[APP]start motion: [%s_%d]", group, no);
    return  targetManage->StartMotionPriority(motion, autoDelete, priority);
}

//StartRandomMotionより複製して作成
CubismMotionQueueEntryHandle LAppModel::StartRandomRightHandMotion(const csmChar* group, csmInt32 priority)
{
    if (_modelSetting->GetMotionCount(group) == 0)
    {
        return InvalidMotionQueueEntryHandleValue;
    }

    csmInt32 no = rand() % _modelSetting->GetMotionCount(group);

    return StartHandMotion(_rightArmMotionManager,group, no, priority);
}

//StartRandomMotionより複製して作成
CubismMotionQueueEntryHandle LAppModel::StartRandomLeftHandMotion(const csmChar* group, csmInt32 priority)
{
    if (_modelSetting->GetMotionCount(group) == 0)
    {
        return InvalidMotionQueueEntryHandleValue;
    }

    csmInt32 no = rand() % _modelSetting->GetMotionCount(group);

    return StartHandMotion(_leftArmMotionManager,group, no, priority);
}

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

void LAppLive2DManager::OnTap(csmFloat32 x, csmFloat32 y)
{
    if (DebugLogEnable) LAppPal::PrintLog("[APP]tap point: {x:%.2f y:%.2f}", x, y);

    for (csmUint32 i = 0; i < _models.GetSize(); i++)
    {
        if (_models[i]->HitTest(HitAreaNameHead, x, y))
        {
            if (DebugLogEnable) LAppPal::PrintLog("[APP]hit area: [%s]", HitAreaNameHead);
            _models[i]->SetRandomExpression();
        }
        else if (_models[i]->HitTest(HitAreaNameBody, x, y))
        {
            if (DebugLogEnable) LAppPal::PrintLog("[APP]hit area: [%s]", HitAreaNameBody);
            _models[i]->StartRandomMotion(MotionGroupTapBody, PriorityNormal);
        }
        // ここから追加
        else if (_models[i]->HitTest("Right", x, y))
        {
            if (DebugLogEnable) LAppPal::PrintLog("[APP]hit area: [%s]", HitAreaNameBody);
            _models[i]->StartRandomRightHandMotion("Right", PriorityForce);
        }
        else if (_models[i]->HitTest("Left", x, y))
        {
            if (DebugLogEnable) LAppPal::PrintLog("[APP]hit area: [%s]", HitAreaNameBody);
            _models[i]->StartRandomLeftHandMotion("Left", PriorityForce);
        }
        // ここまで追加
    }
}

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

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

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

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

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