Skip to content

Complete Guide

Guan edited this page Jan 28, 2021 · 7 revisions

Table of Contents

Main Components

                                                                 Live2DModel
                                                                      |
                      _________________________________________ InternalModel ______________________________________________
                     /                     |                          |                          |                          \
Abstraction:    (core model)          ModelSettings             MotionManager             ExpressionManager          FocusController
                |                     |                         |                         |
Cubism 2:       |-Live2DModelWebGL    |-Cubism2ModelSettings    |-Cubism2MotionManager    |-Cubism2ExpressionManager
                |                     |                         |                         |
Cubism 4:       |-CubismModel         |-Cubism4ModelSettings    |-Cubism4MotionManager    |-Cubism4ExpressionManager

Creating a Model

Models are created by a static method: Live2DModel.from(source, options).

Source

The source can be one of these three types:

  1. A URL of the model settings file, which typically ends with .model.json (Cubism 2) or .model3.json (Cubism 3 and 4).
const model = await Live2DModel.from('path/to/shizuku.model.json');
  1. A JSON object of the model settings. However, you'll still need to specify the URL by assigning it to the url property of the JSON object, this approach is the same for all Cubism versions.
const url = 'path/to/shizuku.model.json'
const json = await loadJSON(url);

json.url = url;

const model = await Live2DModel.from(json);
  1. An instance of ModelSettings. Specifically, either Cubism2ModelSettings or Cubism4ModelSettings.
const url = 'path/to/shizuku.model.json'
const json = await loadJSON(url);

json.url = url;

const settings = new Cubism2ModelSettings(json);
const model = await Live2DModel.from(settings);

You've probably noticed that the URL is always required, that's true, because a URL is essential to resolve the resource files of a model. For example, in a model with the URL foo/bar.model.json, the texture image textures/01.png will be resolved to foo/textures/01.png.

Options

The options is a combination of the options for multiple components, check the Live2DFactoryOptions.

Synchronous Creation

In case you want to participate in the creation, there's a synchronous creation method: Live2DModel.fromSync(source, options).

This method immediately returns a Live2DModel instance, whose resources have not been loaded. That means you can't manipulate or render this model - until the load event has been emitted.

// no `await` here as it's not a Promise
const model = Live2DModel.fromSync('shizuku.model.json');

// these will cause errors!
// app.stage.addChild(model);
// model.motion('tap_body');

model.once('load', () => {
    // now it's safe
    app.stage.addChild(model);
    model.motion('tap_body');
});

With this method, you're able to do extra works when certain resources have been loaded.

const model = Live2DModel.fromSync('shizuku.model.json');

model.once('settingsJSONLoaded', json => {
    // e.g. customize the layout before they are applied to the model
    Object.assign(json, {
        layout: {
            width: 2,
            height: 2
        }
    });
});

model.once('settingsLoaded', settings => {
    // e.g. set another URL to the model
    settings.url = 'path/to/model';
});

When all the essential resources have been loaded, a ready event is emitted. If you want the model to show up as soon as possible, you can render the model safely at this moment.

After that, when all the resources - including the optional resources - have been loaded, the load event is emitted. The behaviors of ready and load events are pretty much like jQuery's $(document).ready() and the window.onload() .

const model = Live2DModel.fromSync('shizuku.model.json');

model.once('ready', () => {
    // it's also safe to do these now, though not recommended because
    // a model will typically look weird when rendered without optional resources 
    app.stage.addChild(model);
    model.motion('tap_body');
});

Here's the creation procedure and all the emitted events. (Really took me a while to draw this!)

                                                                               Live2DModel
                                                                                    ^
                                                                                    |
                   Live2DModel.fromSync(source)                                  "load"
                 ______________|________________                              ______|______
                /              |                \                            /             \
               v               v                 v                          |             "ready"
            (source)        (source)          (source)     ________________ | ______________|__________________________
               |               |                 |        /            _____|_____                   |                 \
               v               v                 v       |            /           \                  |                  |
artifacts:    URL         settingsJSON          ModelSettings       Pose         Physics          Texture[]       InternalModel
               |           ^         \           ^        \          ^             ^                 ^                  ^
               |          /           \          |         \         |            /                  |                  |
events:        | "settingsJSONLoaded"  \  "settingsLoaded"  \  "poseLoaded"  "physicsLoaded"  "textureLoaded"     "modelLoaded"
               v        /               v        |           v       |          /                    |                  |
middlewares:  urlToJSON()     ~~>     jsonToSettings()   ~~>   setupOptionals()   ~~>   setupLive2DModel() ~~> createInternalModel()

Updating a Model

To make a Live2D model "live", it needs be updated with the delta time, which is the time elapsed from last frame to this frame, in milliseconds.

Automatically

When a full build of PixiJS is imported and assigned to window.PIXI, each model will be automatically updated using window.PIXI.Ticker.shared.

import * as PIXI from 'pixi.js';

window.PIXI = PIXI;

Otherwise, you can manually register the Ticker to achieve automatic updating.

import { Application } from '@pixi/app';
import { Ticker, TickerPlugin } from '@pixi/ticker';

Application.registerPlugin(TickerPlugin);
Live2DModel.registerTicker(Ticker);

Manually

To manually update the model, you need to first disable the autoUpdate option, and then call model.update() every tick.

import { Ticker } from '@pixi/ticker';

const model = await Live2DModel.from('shizuku.model.json', { autoUpdate: false });

const ticker = new Ticker();

ticker.add(() => model.update(ticker.elapsedMS));

When you're using the requestAnimationFrame() instead:

const model = await Live2DModel.from('shizuku.model.json', { autoUpdate: false });

let then = performance.now();

function tick(now) {
    model.update(now - then);

    then = now;

    requestAnimationFrame(tick);
}

requestAnimationFrame(tick);

Interacting

There are two basic interactions on a Live2D model:

  • Focusing: the Live2D character will look at the cursor.

  • Tapping: handles the pointertap event, then emits a hit event when any of the defined hit areas is tapped on.

    The hit event comes with an array of the names of hit hit areas.

    model.on('hit', hitAreaNames => {
        if (hitAreaNames.includes('body')) {
            // the body is hit
        }
    });

    See Collision Detection for more information about the hit test.

Automatically

When a full build of PixiJS is imported, the above interactions will be automatically set up.

import * as PIXI from 'pixi.js';

Otherwise, you can manually register Pixi's interaction plugin.

import { Renderer } from '@pixi/core';
import { InteractionManager } from '@pixi/interaction';

Renderer.registerPlugin('interaction', InteractionManager);

Manually

If you don't want the default behaviour, you can disable the autoInteract option, then manually call the interaction methods.

const model = await Live2DModel.from('shizuku.model.json', { autoInteract: false });

canvasElement.addEventListener('pointermove', event => model.focus(event.clientX, event.clientY));

canvasElement.addEventListener('pointerdown', event => model.tap(event.clientX, event.clientY));

Motion

Motions are managed by the MotionManager of each model.

Idle Motions

When the model is not playing any motion, it's considered idle, and then its motion manager will randomly start an idle motion as the idle priority.

Idle motions refer to the ones defined in a particular motion group: "idle" on Cubism 2, and "Idle" on Cubism 4. But you can specify another group according to the model's definition.

model.internalModel.motionManager.groups.idle = 'main_idle';

You can also specify it just when creating the model so this group can be correctly preloaded.

const model = await Live2DModel.from('shizuku.model.json', { idleMotionGroup: 'main_idle' });

Preloading

Motions can be preloaded to provide a seamless experience for users, but that may result in too many XHR requests that block the network.

By default, only the idle motions will be preloaded. You can change this by setting the motionPreload option.

import { MotionPreloadStrategy } from 'pixi-live2d-display';

// MotionPreloadStrategy.ALL
// MotionPreloadStrategy.IDLE
// MotionPreloadStrategy.NONE

const model = await Live2DModel.from('shizuku.model.json', { motionPreload: MotionPreloadStrategy.NONE });

Starting Motions

// start the first motion in the "tap_body" group
model.motion('tap_body', 0);

// when the index is omitted, it starts a random motion in given group
model.motion('tap_body');

// the above calls are shorthands of these methods
model.internalModel.motionManager.startMotion('tap_body', 0);
model.internalModel.motionManager.startRandomMotion('tap_body');

Priority

A motion will be started as one of these priorities: IDLE, NORMAL and FORCE.

  • IDLE: Low priority. A Live2D model will typically have a set of idle motions, they will be automatically played when there's no other motion playing.
  • NORMAL: Medium priority. This is the default value if you don't provide one.
  • FORCE: High priority. This makes sure the motion will always be played regardless of the current priority, except that it meets a race condition where a subsequent motion with the same priority is loaded before this motion.

There's also a NONE priority which cannot be assigned to a motion, it's used to state that this model is currently not playing any motion.

import { MotionPriority } from 'pixi-live2d-display';

model.motion('tap_body', 0, MotionPriority.NORMAL);

// a random motion as normal priority
model.motion('tap_body', undefined, MotionPriority.NORMAL);

When a motion has been requested, and been approved to play, but meanwhile there's already a playing motion, it will not immediately take the place of the current motion, instead it reserves the place and starts to load. The current motion will keep playing until the reserved motion has finished loading.

That said, the actual rules are more complicated, see the State-transition Table section for all the cases.

Sound

If a sound file is specified within a motion definition, it'll be played together with this motion.

During the playback, you can take control of all the created <audio> elements in SoundManager, like setting a global volume.

import { SoundManager } from 'pixi-live2d-display';

SoundManager.volume = 0.5;

To handle the audios separately, you can listen to the motionStart event.

model.internalModel.motionManager.on('motionStart', (group, index, audio) => {
    if (audio) {
        // assume you've implemented a feature to show subtitles
        showSubtitle(group, index);

        audio.addEventListener('ended', () => dismissSubtitle());
    }
});

Motion Sync

There are two parallel tasks while attempting to start a motion with sound:

  • Load the motion, then play it
  • Load the sound, then play it

Typically, the playbacks of motion and its sound are supposed to start at the same time, that's what motion sync does: it defers the playbacks until the motion and sound have both been loaded.

  • Load the motion and sound, then play both

This feature can be toggled via global config, by default it's enabled.

State-transition Table

This table specifies which motion will finally be played when attempting to start a motion in a particular situation, assuming the loading will never fail and will be finished in a relatively short time.

When: 👇 Start motion C as:
NONE IDLE NORMAL FORCE
Playing none
Reserved none
none C C C
Playing A as IDLE
Reserved none
A A C C
Playing A as NORMAL
Reserved none
A A A C
Playing A as FORCE
Reserved none
A A A C
Playing A as IDLE
Reserved B as NORMAL
B B B C
Playing A as NORMAL
Reserved B as FORCE
B B B C
Playing A as FORCE
Reserved B as FORCE
B B B C
Playing none
Reserved B as IDLE
B B C C
Playing none
Reserved B as FORCE
B B B C
Playing none
Reserved B as FORCE
B B B C

Expression

Expressions are managed by ExpressionManager in MotionManager.

If the model has no expression defined in its settings, the ExpressionManager will not be created.

// apply the first expression
model.expression(0);

// apply the expression named "smile"
model.expression('smile');

// when the argument is omitted, it applies a random expression
model.expression();

// the above calls are shorthands of these methods
model.internalModel.motionManager.expressionManager.setExpression(0);
model.internalModel.motionManager.expressionManager.setExpression('smile');
model.internalModel.motionManager.expressionManager.setRandomExpression();

Global Configs

import { config } from 'pixi-live2d-display';

// log level
config.logLevel = config.LOG_LEVEL_WARNING; // LOG_LEVEL_VERBOSE, LOG_LEVEL_ERROR, LOG_LEVEL_NONE

// play sound for motions
config.sound = true;

// defer the playbacks of a motion and its sound until both are loaded
config.motionSync = true;

// the default fade-in and fade-out durations, applied when a motion/expression doesn't have these values specified
config.motionFadingDuration = 500;
config.idleMotionFadingDuration = 500;
config.expressionFadingDuration = 500;

// support for 4x4 mask division in Cubism 4, which is unofficial and experimental
config.cubism4.supportMoreMaskDivisions = true;
Clone this wiki locally