-
-
Notifications
You must be signed in to change notification settings - Fork 152
Complete Guide
Live2DModel | _________________________________________ InternalModel ______________________________________________ / | | | \ Abstraction: (core model) ModelSettings MotionManager ExpressionManager FocusController | | | | Cubism 2: |-Live2DModelWebGL |-Cubism2ModelSettings |-Cubism2MotionManager |-Cubism2ExpressionManager | | | | Cubism 4: |-CubismModel |-Cubism4ModelSettings |-Cubism4MotionManager |-Cubism4ExpressionManager
Models are created by a static method: Live2DModel.from(source, options)
.
The source can be one of these three types:
- 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');
- 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);
- An instance of
ModelSettings
. Specifically, eitherCubism2ModelSettings
orCubism4ModelSettings
.
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
.
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()
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.
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);
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);
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 ahit
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.
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);
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));
Motions are managed by the MotionManager
of each model.
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' });
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 });
// 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');
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.
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());
}
});
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.
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 |
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();
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;