Look At
Control where a VRM character's eyes are looking. Characters can follow the mouse cursor, track a specific entity, or have look-at behavior disabled entirely.
Import
import { Vrm } from "@hmcs/sdk";
Follow Cursor
vrm.lookAtCursor() makes the character's eyes follow the mouse cursor across the screen.
const character = await Vrm.findByName("MyAvatar");
await character.lookAtCursor();
Look at Entity
vrm.lookAtTarget(entity) makes the character look at a specific entity by its ID. This is useful for making characters look at each other or at specific objects in the scene.
const character = await Vrm.findByName("MyAvatar");
const other = await Vrm.findByName("OtherCharacter");
// Make MyAvatar look at OtherCharacter's head
const headEntity = await other.findBoneEntity("head");
await character.lookAtTarget(headEntity);
You can also use the entity ID of any Bevy ECS entity, not just VRM bones:
// Look at another VRM's root entity
await character.lookAtTarget(other.entity);
Disable Look-At
vrm.unlook() turns off look-at behavior entirely. The character's eyes return to their default animation-driven state.
await character.unlook();
Common Pattern: State-Driven Look-At
A typical pattern is to enable cursor tracking when the character is idle and disable it during drag or other interactions:
import { Vrm, repeat } from "@hmcs/sdk";
const character = await Vrm.spawn("my-mod:character");
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
// Start tracking the cursor
await character.lookAtCursor();
character.events().on("state-change", async (e) => {
if (e.state === "idle") {
// Resume cursor tracking after a short delay
// (delay prevents jitter during animation transitions)
await sleep(500);
await character.lookAtCursor();
} else if (e.state === "drag") {
// Stop tracking while being dragged
await character.unlook();
} else if (e.state === "sitting") {
// Continue tracking while sitting
await sleep(500);
await character.lookAtCursor();
}
});
Example: Characters Looking at Each Other
const alice = await Vrm.findByName("Alice");
const bob = await Vrm.findByName("Bob");
// Get head bone entities
const aliceHead = await alice.findBoneEntity("head");
const bobHead = await bob.findBoneEntity("head");
// Make them look at each other
await alice.lookAtTarget(bobHead);
await bob.lookAtTarget(aliceHead);
Look-At State
The current look-at state is included in the VrmSnapshot returned by Vrm.findAllDetailed():
const snapshots = await Vrm.findAllDetailed();
for (const s of snapshots) {
if (s.lookAt === null) {
console.log(`${s.name}: look-at disabled`);
} else if (s.lookAt.type === "cursor") {
console.log(`${s.name}: following cursor`);
} else if (s.lookAt.type === "target") {
console.log(`${s.name}: looking at entity ${s.lookAt.entity}`);
}
}
Types
type LookAtState =
| { type: "cursor" }
| { type: "target"; entity: number };
Next Steps
- Spawn & Find -- Find characters and bone entities for look-at targets.
- Events -- React to state changes to toggle look-at behavior.
- VRM Overview -- Full API reference table.