Manage Motion Differently for Each Part

Updated: 10/06/2022

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.

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 a motion manager

First, let’s look at the implementation on the program side.
The first step is to add a management class to the model.
Add CubismMotionManager to the sample LAppModel.
Also, add the generation process to the constructor.

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

Update process

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

public void update(){
    ...
    // Load previously saved state.
    _model.loadParameters();

    // If there is no motion playback, playback is performed at random from among the standby motions.
    if(_motionManager.isFinished()){
        startRandomMotion(LAppDefine.MotionGroup.IDLE.getId(), LAppDefine.Priority.IDLE.getPriority());
    } else{
        // Update motion.
        isMotionUpdated = _motionManager.updateMotion(_model, deltaTimeSeconds);
    }
  
    isMotionUpdated |= rightArmMotionManager.updateMotion(_model, deltaTimeSeconds); // Add.
    isMotionUpdated |= leftArmMotionManager.updateMotion(_model, deltaTimeSeconds); // Add.

    // Save the state of the model.
    _model.saveParameters();
}

When adding to LAppModel.update, make sure to place updateMotion between _model.loadParameters(); and _model.saveParameters();.
Note that if updateMotion is run multiple times, the parameters to be updated may overlap.
In this case, the content of the later executed updataMotion takes precedence.

Playback start

Then, duplicate or modify the 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 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) {
        // If not read in PreLoad, do not play back
        return -1;
    }
    if (_debugMode) {
        LAppPal.logger.print("Start motion: [" + group + "_" + number + "]");
    }
    
    return targetManage.startMotionPriority(motion, priority);
}

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

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


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

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

    for (LAppModel model : _models) {
        // Tap on a head to play back random facial expressions
        if (model.hitTest(HitAreaName.HEAD.getId(), x, y)) {
            if (DEBUG_LOG_ENABLE) {
                LAppPal.logger.print("hit area: " + HitAreaName.HEAD.getId());
            }
            model.setRandomExpression();
        }
        // Tap on a body to initiate random motion
        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);
        }
      
        // Additions from here.
        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());
        }
        // 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.

Was this article helpful?
YesNo
Please let us know what you think about this article.