Avatar(key) ⇒ U.Object3D
Unia supports full VR IK for VRM, Mixamo and most humanoid avatars. All of the combined avatars in the scene use one single draw call.
Default local-player avatar
Unia has a built-in player avatar and controls workflow. When a scene loads, the editor loads the user's saved avatar (or the configured default avatar) and equips it as world.avatar. Game scripts do not normally need to load or create the local-player avatar.
Avatar loading is asynchronous, so world.avatar can temporarily be null during startup. Wait or guard until it exists before attaching weapons, health bars, or player state.
The built-in desktop/mobile/XR controls own local-player locomotion. On desktop they update world.avatar.position from the controls' camera target every frame. Use the avatar as the visual player and authoritative player-position reference, but do not implement ordinary local movement by assigning its position directly. Use world.avatar.teleport(position) for respawning or repositioning the local player.
Avatar positions and avatar.teleport(position) use root/feet coordinates. To place the local avatar on a floor at y = 0, teleport to y = 0; the engine adds the avatar's eye height when synchronizing the desktop camera target.
The equipped avatar is automatically given IK and automatic locomotion animation. Its animator derives walk, run, strafe, jump, and idle blending from avatar movement and control state. Calling avatar.animate() is mainly needed for NPC avatars loaded separately.
Main avatar bones
When a supported humanoid avatar is set up, Unia assigns its main bones directly
to the avatar. UGC and game scripts should use these properties instead of
looking up the same bones with avatar.getBone(...).
const avatar = world.avatar
// Torso and head
avatar.hips
avatar.spine
avatar.spine1
avatar.neck
avatar.head
// Arms
avatar.leftClavicle
avatar.leftArm
avatar.leftShoulder
avatar.leftForearm
avatar.leftHand
avatar.rightClavicle
avatar.rightArm
avatar.rightShoulder
avatar.rightForearm
avatar.rightHand
// Legs
avatar.leftLeg
avatar.leftKnee
avatar.leftFoot
avatar.rightLeg
avatar.rightKnee
avatar.rightFoot
// Eyes
avatar.eyes.left
avatar.eyes.right
leftClavicle and rightClavicle are the shoulder-joint bones.
leftShoulder and rightShoulder are legacy aliases for the upper-arm bones
used by Unia's IK system; new code can use the clearer leftArm and rightArm
properties. leftLeg and rightLeg are the upper-leg bones, while leftKnee
and rightKnee are the lower-leg bones.
The hands also expose commonly used finger references:
avatar.leftHand.thumb
avatar.leftHand.index
avatar.leftHand.indexCenter
avatar.leftHand.indexTip
avatar.rightHand.thumb
avatar.rightHand.index
avatar.rightHand.indexCenter
avatar.rightHand.indexTip
Every normalized finger bone is also assigned directly using its side, finger, and joint number:
avatar.rightThumb1
avatar.rightThumb2
avatar.rightThumb3
avatar.rightIndex1
avatar.rightIndex2
avatar.rightIndex3
avatar.rightMiddle1
avatar.rightMiddle2
avatar.rightMiddle3
avatar.rightRing1
avatar.rightRing2
avatar.rightRing3
avatar.rightLittle1
avatar.rightLittle2
avatar.rightLittle3
// The same properties are available with the left prefix.
avatar.leftThumb1
avatar.leftIndex1
avatar.leftMiddle1
avatar.leftRing1
avatar.leftLittle1
These are ordinary bone objects. Their local position, quaternion,
rotation, and scale can be read or changed like other U.Object3D
transforms. Because world.avatar loads asynchronously, guard it before
accessing these properties.
World-space hand IK
Supported humanoid avatars expose an analytical two-bone IK target for each
hand. Targets are world-space positions and may be set independently or
together. The solver updates the upper arm, forearm, and hand after automatic
animation and avatar.animate.override have run.
const leftTarget = new U.Vector3(0, 1.2, -0.5)
const rightTarget = new U.Vector3(0.35, 1.15, -0.45)
avatar.setHandIKTarget('left', leftTarget)
avatar.setHandIKTarget('right', rightTarget)
The target position is copied when setHandIKTarget() is called. Update it
each frame when following a moving object. An optional world-space hand
quaternion, elbow pole position, and blend weight can also be supplied:
grip.getWorldPosition(targetPosition)
grip.getWorldQuaternion(targetQuaternion)
avatar.setHandIKTarget('left', targetPosition, {
quaternion: targetQuaternion,
pole: elbowPolePosition,
weight: 1,
})
By default the target positions the hand bone, whose origin is normally at the
wrist. To position the visible palm grip line instead, use the palm
end-effector. The offset is derived from that avatar's first middle-finger
bone, so it adapts to different hand sizes without averaging across the palm:
grip.getWorldPosition(targetPosition)
avatar.setHandIKTarget('right', targetPosition, {
effector: 'palm',
})
For a custom contact point, provide an effectorOffset in the hand bone's
local space. A custom offset takes precedence over the automatically derived
palm offset:
avatar.setHandIKTarget('right', targetPosition, {
effectorOffset: new U.Vector3(0, 0.06, 0),
})
Palm targeting performs up to two correction passes after the initial arm solve. This keeps the live middle-finger effector on the target as the wrist orientation changes.
Set both sides in one call with setHandIKTargets():
avatar.setHandIKTargets({
left: { position: leftPosition, quaternion: leftQuaternion },
right: { position: rightPosition, quaternion: rightQuaternion },
})
Clear one or both targets to return those arms to normal animation:
avatar.clearHandIKTarget('left')
avatar.clearHandIKTargets()
Procedural animation override
An animated avatar can have one procedural override callback. Assign it to
avatar.animate.override:
const updateAimPose = (avatar, delta) => {
avatar.rightArm.quaternion.copy(aimArmRotation)
avatar.rightForearm.quaternion.copy(aimForearmRotation)
}
world.avatar.animate.override = updateAimPose
The automatic idle, locomotion, jump, and loaded-animation pose is calculated
first. The override runs afterward and receives the avatar and frame delta in
seconds. Bones changed by the callback use the overridden transforms; bones it
does not change keep their automatic animation. The callback may change one
bone, the upper body, or the full body. Bone changes made inside the callback
do not need a sync() call.
There is only one override callback per avatar. Assigning another callback
replaces the previous one. Set it to null to return completely to automatic
animation:
world.avatar.animate.override = null
A script should only remove an override it owns during cleanup:
this.onStop = () => {
if (world.avatar?.animate?.override === updateAimPose) {
world.avatar.animate.override = null
}
}
The override runs as part of automatic animation updates, so it is skipped
while the avatar's CPU animation is culled. As with other avatar APIs, wait for
the asynchronously loaded world.avatar before assigning it.
Bone attachments
Runtime objects can be parented directly to an avatar bone. Ordinary mesh
attachments are registered with the renderer automatically and follow the bone
without needing per-frame sync() calls.
const weapon = await Model('weapon.glb')
world.avatar.rightHand.add(weapon)
Call sync() after changing the attachment's local position, rotation, scale,
or visibility. Use world.remove(weapon) when the attachment is no longer
needed. Parenting an attachment does not replicate it to multiplayer peers;
each peer must create the corresponding attachment locally.
Loading an avatar at runtime
const optionalProps = {
stream: bool,
cache: bool,
lighting: float : default 0, // flat/PBR ratio
}
// Load using imported object name
const avatar = await Avatar("Rindo.vrm", optionalProps);
// Load using an asset key
const avatar = await Avatar("a586e6123a", optionalProps);
stream : optional boolean
By default this is true. This makes avatars use geometry streaming, so avatars are dynamically loaded and unloaded based on their distance from the camera, and frustum/occlusion culling. If you have scenes with more then 30 avatars this is recommended.
cache : optional boolean
If you are creating or cloning many identical avatars, this will keep the data in memory and speed up load time.
Functions
// This makes the current player use the avatar
avatar.equip()
world.add(avatar); // add avatar to the scene, for NPCs
// for WebXR this resets the avatar to standing, useful when switching between
// seated and standing playing environments
avatar.resetHeight();
// This makes avatars have idle animations while stationary,
// and walk/run animations when moving
avatar.animate();
// Make an avatar walk/run to a new location
avatar.moveTo(destination : new U.Vector3(0, 0, 5), speed : 1 default);
// Make an avatar rotate to look a point in world space
avatar.faceTowards(destination : new U.Vector3(0, 0, 5), speed : 1 default);
// Move avatar to a new location
avatar.teleport(destination : new U.Vector3(0, 0, 5));
For the equipped world.avatar, teleport() accepts the desired avatar root/feet position and moves the built-in desktop controller target with the required eye-height offset, keeping the camera and avatar synchronized. moveTo() is intended for independently controlled avatars/NPCs and should not replace the local player's built-in controls.
Properties
// Whether the avatar is currently visible (not occlusion or frustum culled)
avatar.inView : boolean
// When true, avatars do not compute physics, animation or bone matrix updates
// when they are not in view. Only their root transform is updated.
avatar.cullCPU : boolean [default true]
// Whether to enable spring bone physics for the avatar
avatar.physics : boolean [default true]
// By default all avatars when animated will crouch or float depending on their
// position relative to the floor. This raycasts per avatar every frame,
// it's best to disable this if you don't need it
avatar.raycastFloor : boolean [default true]
Animations
You can load any mixamo animation, docs coming soon.
Default animations:
avatar.playAnimation("samba");
// Play once, then fade back to the avatar's idle animation.
avatar.playAnimation("dieForward", {
loop: false,
fadeIn: 0.2,
fadeOut: 0.4,
});
// Keep the final pose until avatar.stopAnimation() is called.
avatar.playAnimation("dieForward", {
loop: false,
holdLastFrame: true,
});
// You can replace 'samba' with any of:
hipHopDance;
jump1;
robotDance;
singing;
swimming;
floating;
flair;
situps;
catwalk;
ymca;
walking1;
idleThinking;
standingUp;
sitting1;
running;
sittingIdle;
layingShrug;
idle3;
layingIdle;
jumpingJacks;
jogging;
layingHandMove;
jump2;
idle2;
fastRun;
falling;
dancing;
burpee;
chickenDance;
idleYawn;
macarena;
idle1;
idleHappy;
idleTalking;
sittingTalking;
samba;
idleLookingAround;
hokeyPokey;
idleLookAtWatch,
climbWall,
catwalkNeutral,
laughing,
talking1,
talking2,
shy,
excited,
headNod,
headNod2,
idleUsingTablet,
idleLookDown,
playAnimation(name, options) accepts these options:
loop— repeat the animation (trueby default).fadeIn— seconds used to blend into the animation (1.5by default).fadeOut— seconds used to blend back to idle (1.5by default).holdLastFrame— for a non-looping animation, retain its final pose instead of automatically returning to idle (falseby default). Callavatar.stopAnimation()to release a held pose.