Skip to main content

VRM Module

The Vrm class is the core module of @hmcs/sdk. It manages the full lifecycle of VRM 3D characters -- spawning, finding, animating, controlling expressions, handling pointer/drag events, lip-sync speech, and more.

import { Vrm } from "@hmcs/sdk";

Spawning a Character

Use Vrm.spawn() to create a new VRM character from a MOD asset. By convention, asset IDs use the format "mod-name:asset-name".

import { Vrm } from "@hmcs/sdk";

// Basic spawn
const character = await Vrm.spawn("my-mod:character");

// Spawn with initial transform and persona
const character = await Vrm.spawn("my-mod:character", {
transform: {
translation: [0, 0, 0],
scale: [1, 1, 1],
},
persona: {
profile: "A cheerful virtual assistant",
ocean: { openness: 0.8, extraversion: 0.7 },
metadata: {},
},
});

Vrm.spawn() returns a Vrm instance bound to the spawned entity. All subsequent operations use this instance.

Finding Existing Characters

If a character has already been spawned (e.g., by another MOD or a previous session), you can find it by name or list all instances.

// Find by VRM model name
const character = await Vrm.findByName("MyAvatar");

// Wait for a character to be loaded (blocks until ready)
const character = await Vrm.waitLoadByName("MyAvatar");

// Get all loaded VRM instances
const allCharacters = await Vrm.findAll();

// Get detailed snapshots of all VRMs (state, transform, expressions, animations)
const snapshots = await Vrm.findAllDetailed();
for (const s of snapshots) {
console.log(`${s.name}: ${s.state} at (${s.globalViewport?.[0]}, ${s.globalViewport?.[1]})`);
}

Playing Animations

Play VRMA animations on a character with playVrma(). Animations are referenced by asset ID. The built-in @hmcs/assets MOD provides default animations: vrma:idle-maid, vrma:grabbed, and vrma:idle-sitting.

import { Vrm, repeat } from "@hmcs/sdk";

const character = await Vrm.spawn("my-mod:character");

// Play a looping idle animation with a 0.5s crossfade
await character.playVrma({
asset: "vrma:idle-maid",
repeat: repeat.forever(),
transitionSecs: 0.5,
});

// Play a one-shot animation (plays once, then stops)
await character.playVrma({
asset: "my-mod:wave",
repeat: repeat.never(),
});

// Play an animation N times
await character.playVrma({
asset: "my-mod:nod",
repeat: repeat.count(3),
});

// Wait for a one-shot animation to finish before continuing
await character.playVrma({
asset: "my-mod:dance",
repeat: repeat.never(),
waitForCompletion: true,
});

// Reset spring bones during transition to prevent hair/clothing bouncing
await character.playVrma({
asset: "vrma:grabbed",
repeat: repeat.forever(),
resetSpringBones: true,
});

You can also query animation state, stop animations, and control playback speed:

// Get all active animations
const animations = await character.listVrma();

// Check a specific animation's state
const state = await character.vrmaState("vrma:idle-maid");
console.log(`Playing: ${state.playing}, Elapsed: ${state.elapsedSecs}s`);

// Change playback speed
await character.setVrmaSpeed("vrma:idle-maid", 1.5);

// Stop an animation
await character.stopVrma("vrma:idle-maid");

Character Events

Subscribe to real-time events from a character using events(). This opens a Server-Sent Events (SSE) connection that streams events as they happen.

const character = await Vrm.spawn("my-mod:character");
const eventSource = character.events();

// State changes: idle, drag, sitting, etc.
eventSource.on("state-change", async (e) => {
console.log("New state:", e.state);
});

// Pointer interactions
eventSource.on("pointer-click", (e) => {
console.log(`Clicked at (${e.globalViewport[0]}, ${e.globalViewport[1]}), button: ${e.button}`);
});

// Drag events
eventSource.on("drag-start", (e) => console.log("Drag started"));
eventSource.on("drag", (e) => console.log(`Dragging, delta: ${e.delta}`));
eventSource.on("drag-end", (e) => console.log("Drag ended"));

// Hover events
eventSource.on("pointer-over", (e) => console.log("Mouse entered character"));
eventSource.on("pointer-out", (e) => console.log("Mouse left character"));

// Animation events
eventSource.on("vrma-play", (e) => console.log("Animation started:", e.state));
eventSource.on("vrma-finish", (e) => console.log("Animation finished:", e.state));

// Persona changes
eventSource.on("persona-change", (e) => {
console.log("Persona updated:", e.persona.profile);
});

// Close the event stream when done
eventSource.close();

The VrmEventSource implements Disposable, so you can use it with using in TypeScript 5.2+:

using eventSource = character.events();
eventSource.on("state-change", (e) => { /* ... */ });

Available Events

EventPayloadDescription
state-change{ state: string }Character state changed (idle, drag, sitting, etc.)
expression-change{ state: string }Expression changed
vrma-play{ state: string }VRMA animation started playing
vrma-finish{ state: string }VRMA animation finished
pointer-click{ globalViewport, button }Character was clicked
pointer-press{ globalViewport, button }Mouse button pressed on character
pointer-release{ globalViewport, button }Mouse button released on character
pointer-over{ globalViewport }Mouse entered character area
pointer-out{ globalViewport }Mouse left character area
pointer-move{ globalViewport }Mouse moved within character area
pointer-cancel{ globalViewport }Pointer interaction cancelled
drag-start{ globalViewport }Drag started
drag{ globalViewport, delta }Dragging in progress (includes cursor delta)
drag-end{ globalViewport }Drag ended
persona-change{ persona }Persona data was updated

Key APIs

Lifecycle

MethodDescription
Vrm.spawn(asset, options?)Spawn a new VRM from a MOD asset ID. Returns a Vrm instance.
Vrm.findByName(name)Find a VRM by its model name. Throws if not found.
Vrm.waitLoadByName(name)Wait for a VRM to finish loading, then return it.
Vrm.findAll()Get all loaded VRM instances as Vrm[].
Vrm.findAllEntities()Get all loaded VRM entity IDs as number[].
Vrm.findAllDetailed()Get detailed snapshots of all VRMs (state, transform, expressions, animations, persona).
Vrm.stream(callback)Stream existing and future VRM instances as they are created.
vrm.despawn()Remove this VRM from the scene.

Animation

MethodDescription
vrm.playVrma(options)Play a VRMA animation with repeat, transition, and completion options.
vrm.stopVrma(asset)Stop a specific VRMA animation by asset ID.
vrm.listVrma()List all VRMA animations attached to this VRM.
vrm.vrmaState(asset)Get the playback state of a specific animation (playing, speed, elapsed).
vrm.setVrmaSpeed(asset, speed)Change the playback speed of an animation.

Expressions

MethodDescription
vrm.expressions()Get all expressions and their current weights.
vrm.setExpressions(weights)Set expression weights, replacing all previous overrides.
vrm.modifyExpressions(weights)Partially update expression weights (other overrides remain).
vrm.clearExpressions()Clear all expression overrides, returning control to VRMA animation.
vrm.modifyMouth(weights)Set mouth expressions for lip-sync (non-mouth overrides are preserved).

Look-At

MethodDescription
vrm.lookAtCursor()Make the character's eyes follow the mouse cursor.
vrm.lookAtTarget(entity)Make the character look at a specific entity.
vrm.unlook()Disable the look-at behavior.

Speech

MethodDescription
vrm.speakWithTimeline(audio, keyframes, options?)Play WAV audio with frame-synchronized expression keyframes for lip-sync.

State and Position

MethodDescription
vrm.state()Get the current state string (e.g., "idle", "drag", "sitting").
vrm.setState(state)Set the character's state.
vrm.position()Get position in both screen (globalViewport) and world coordinates.
vrm.name()Get the VRM model name.

Persona

MethodDescription
vrm.persona()Get the character's persona (profile, personality, OCEAN traits, metadata).
vrm.setPersona(persona)Set the character's persona data.

Physics

MethodDescription
vrm.springBones()Get all spring bone chains (hair, clothing physics).
vrm.springBone(chainId)Get a specific spring bone chain by entity ID.
vrm.setSpringBone(chainId, props)Update spring bone physics properties (stiffness, drag, gravity).

Events

MethodDescription
vrm.events()Open a VrmEventSource for real-time event streaming.
vrm.findBoneEntity(bone)Find the entity ID of a named bone (e.g., "head", "leftHand").

Complete Example

The following is the full service from the @hmcs/elmer MOD. It demonstrates spawning a character, playing animations based on state, and cursor tracking.

import { type TransformArgs, Vrm, preferences, repeat } from "@hmcs/sdk";

// Load the character's last known position from preferences
const transform = await preferences.load<TransformArgs>("transform::elmer:vrm");

// Spawn the Elmer character using its VRM asset
const elmer = await Vrm.spawn("elmer:vrm", {
transform,
});

// Helper for async delays
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Shared animation options: loop forever with a 0.5s crossfade
const option = {
repeat: repeat.forever(),
transitionSecs: 0.5,
} as const;

// Start with the idle animation
await elmer.playVrma({
asset: "vrma:idle-maid",
...option,
});

// React to character state changes
elmer.events().on("state-change", async (e) => {
if (e.state === "idle") {
// When idle: play idle animation, then track the cursor
await elmer.playVrma({
asset: "vrma:idle-maid",
...option,
});
await sleep(500);
await elmer.lookAtCursor();
} else if (e.state === "drag") {
// When being dragged: stop cursor tracking, play grabbed animation
await elmer.unlook();
await elmer.playVrma({
asset: "vrma:grabbed",
...option,
resetSpringBones: true,
});
} else if (e.state === "sitting") {
// When sitting on a window edge: play sitting animation
await elmer.playVrma({
asset: "vrma:idle-sitting",
...option,
});
await sleep(500);
await elmer.lookAtCursor();
}
});

Next Steps

  • SDK Overview -- Full list of all SDK modules and their descriptions.