Manage Motion Differently for Each Part (Web)

Updated: 01/10/2020

This page includes some notes on management and motion creation, using the example of increasing the number of LAppModel.cubismMotionManager holdings and incorporating them into LAppModel.update in order to manage different motions for the right and left hands in parallel.

Reasons to Increase CubismMotionManager

CubismMotionManager has the ability to play multiple temporary motions to accommodate fading.
However, since the API for starting playback ends with a fade-out of the motion being played, there is no function to keep playing motions in parallel.

To solve this problem, multiple instances of the motion manager are prepared, and the motion managers to be played back are operated individually to achieve parallel motion playback.

Increase CubismMotionManager

Adding motion managers

First, let’s look at the implementation on the program side.
The first step is to add a motion management class to the model.
Add CubismMotionManager to the sample LAppModel.
Add creation and release processing to the constructor and the release function as well.

export class LAppModel extends CubismUserModel {
  
/* Omitted */
 
    /**
     * Constructor
     */
    public constructor() 
    {
        super();

/* Omitted */
       
        this._rightArmMotionManager = new CubismMotionManager(); // <<< Add!
        this._leftArmMotionManager = new CubismMotionManager();  // <<< Add!
    }
    _rightArmMotionManager: CubismMotionManager;    /// <<< Add!
    _leftArmMotionManager: CubismMotionManager;     /// <<< Add!

}

Update process

The Update process also allows the motion that is added and played to affect the parameters.

public update(): void
{

/* Omitted */

  	//--------------------------------------------------------------------------
    this._model.loadParameters();   // Load previously saved state.
    if(this._motionManager.isFinished())
    {
        // If there is no motion playback, playback is performed at random from among the standby motions.
        this.startRandomMotion(LAppDefine.MotionGroupIdle, LAppDefine.PriorityIdle);
        
    }
    else
    {
        motionUpdated = this._motionManager.updateMotion(this._model, deltaTimeSeconds);    // Update motion.
    }
    motionUpdated ! = this._rightArmMotionManager.updateMotion(this._model, deltaTimeSeconds);   // < Add.
    motionUpdated ! = this._leftArmMotionManager.updateMotion(this._model, deltaTimeSeconds);    // < Add.
    this._model.saveParameters(); // Save the state.
    //--------------------------------------------------------------------------

/* Omitted */

}

When adding to LAppModel.update, make sure to place updateMotion between _model.loadParameters() and _model.saveParameters().

In addition, when multiple motions are played back simultaneously, the same parameter values may be updated from two or more motions.
In this case, the content of the later updateMotion takes precedence.

Playback start

Then, duplicate or modify the member functions of the startMotion system so that motion playback can be directed by the motion manager that has been added.
Note that in the startHandMotion function, the motion manager is specified as an argument.
In the following example, only the motion specified in .motion3.json (the motion loaded by the preLoad function) is implemented for playback.

// Create by duplicating from 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)
    {
        // If not read in preLoad, do not play back
        return InvalidMotionQueueEntryHandleValue;
    }

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

// Create by duplicating from 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);
}

// Create by duplicating from 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);
}

Motion playback adds collision detection to the model so that it is triggered by a click.

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);
        }

        // Additions from here.
        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);
        }
        // Additions up to here
    }
}

Assign Responsible Parameters

As indicated in the update process, if parameters overlap among multiple motions to be played back, the update performed later takes precedence and the parameters are overwritten.
Therefore, when using multiple motion managers, it is necessary to decide which motion manager is responsible for which part of the body, i.e., which parameter operation.

The video above shows an example where the parameters of the left and right arms were updated by a lower priority motion manager (responsible for idle motion of the entire model) immediately after the higher priority motion manager (responsible for motion of the left and right arms) finished playback.
This is not a desirable expression if you want to keep parameters that are updated by a motion manager with a higher priority in the state immediately after the motion playback ends.
This phenomenon is seen when a reference value is set for a parameter that is not intended to be updated in the motion data played by a motion manager with a lower priority.
By separating the parameters to be updated for each motion data, the parameters that are updated by the motion manager with the higher priority can be maintained in the state immediately after the end of playback.

It is important to clearly define the specifications before creating the motion in order to avoid extensive modifications, whether the decision is to keep each parameter separate or assume overwriting.

Please let us know what you think about this article.