logo
Hand

rendering

Hand

2
Unlock

Install with the CLI:

bjs download Hand

Hand

A skeletal hand with 3 to 5 digits. It opens, grabs, points, closes into a fist, and grips a pistol. It stands on its own or rides the wrist of any rigged figure.

What it does

The marketplace figures have no finger bones. Every Meshy rig stops at LeftHand and RightHand, so no clip can ever close a fist. This component builds the missing part out of renderer primitives: capsule fingers, sphere knuckles, a box palm. Pure forward kinematics poses it, and the adapter's bone lookup pins it to the wrist. The figure animates, the hand rides the wrist, the fingers curl.

The grab pose on the parametric hand: capsule fingers, sphere knuckles, a box palm.

You pick the anatomy: 2 to 4 fingers, an optional thumb, left or right, any scale. Poses glide instead of snapping, and a cycle config steps through poses with no other code.

There are two ways to wear it on a figure. The overlay (default) renders the primitive hand on the wrist like a snap-on glove. With driveMesh: true, the bundled adapter plugin grafts finger bones into the figure's own skeleton and re-paints the hand region's skin weights onto them. The figure's own mesh curls into the fist; the primitive hand hides and serves as pure armature, sized from the measured hand.

driveMesh on a marketplace figure: the model's own hand, open and closed into a fist.

prop: "pistol" welds a gun into the grip with the barrel on the forearm line, so an attached hand aims wherever the arm points.

Use it in a scene

A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.

Build it from scratch with the bjs CLI:

  1. 1Scaffold a project
    npm create @babylonjsmarket/arcade@latest my-game

    World, renderer, and dev server — ready to run.

  2. 2Install dependencies
    cd my-game && npm install
  3. 3Add Hand
    bjs download Hand

    Copies its source into src/ so the scene resolves.

    First time? Run bjs login once.

  4. 4Paste the scene into src/scenes/arcade-room.ts and run
    npm run dev

    SceneLoader builds the world from the JSON; reload to rebuild.

JSON
{
  "Figure": {
    "components": {
      "Mesh": { "src": "/twinstickshooter-character/character.glb", "scale": 1, "autoLoad": true },
      "SkeletonAnimator": {}
    }
  },
  "FigureLeftHand": {
    "components": {
      "Hand": {
        "side": "left",
        "driveMesh": true,
        "attachTo": { "entity": "Figure", "bone": "LeftHand" },
        "cycle": { "poses": ["open", "grab", "fist", "point"], "periodSeconds": 1.4 }
      }
    }
  },
  "FigureRightHand": {
    "components": {
      "Hand": {
        "side": "right",
        "driveMesh": true,
        "attachTo": { "entity": "Figure", "bone": "RightHand" },
        "prop": "pistol",
        "pose": "pistolGrip"
      }
    }
  }
}

Serve the figure GLB under public/ first (bjs download twinstickshooter-character). Without a figure, drop attachTo and the hand stands free at its own position.

Drive it with events

Any producer can pose the hand: a director system, an input system, the bundled control panel.

TypeScript
// Glide to a named pose: open, relaxed, grab, fist, point, pistolGrip.
game.eventBus.emit('hand.pose.set', { entityId: 'FigureLeftHand', pose: 'grab' });

// Squeeze the trigger once (the index finger animates on top of the pose).
game.eventBus.emit('hand.trigger', { entityId: 'FigureRightHand' });

// The squeeze's peak reports the muzzle, so a weapon system can spawn the shot.
game.eventBus.on('hand.trigger.pulled', ({ muzzlePosition, muzzleDirection }) => {
  // fire from muzzlePosition along muzzleDirection
});

One hand.trigger squeeze in the Hand Lab demo: the status line prints hand.trigger.pulled with the muzzle's world pose.

Props

  • fingers (number, default 4): non-thumb fingers, clamped 2 to 4. Fewer fingers get chunkier, so a 3-digit hand reads as a cartoon hand, not a maimed one.
  • thumb (boolean, default true): the opposed thumb. Fingers plus thumb is the 3 to 5 digit range.
  • side ("left" | "right", default "right"): a left hand is the exact mirror.
  • scale (number, default 1): size over the canonical hand, which measures 1 unit from wrist to open fingertip.
  • prop ("none" | "pistol", default "none"): rigid prop welded into the grip.
  • color ({r,g,b} 0 to 1, default bone white): segment tint; knuckles render a darker shade.
  • position / rotation (Vec3, radians): root pose while not attached.
  • pose (string, default "open"): starting pose.
  • poseLerpSpeed (number, default 10): how fast poses glide.
  • cycle ({ poses, periodSeconds } | null): step through named poses on a timer.
  • attachTo ({ entity, bone, offsetPosition?, offsetRotation? } | null): ride a bone of that entity's loaded model. Offsets apply in the bone's frame.
  • driveMesh (boolean, default false): graft the host's own hand mesh onto curl bones instead of overlaying. Falls back to the overlay with one warning when the plugin is missing.
  • autoScale (boolean, default true): size an attached hand (and its prop) from the host's measured hand length; scale is the fallback.
  • triggerDuration (number, default 0.28): seconds one trigger squeeze takes.

Events

Listens:

  • hand.pose.set { entityId, pose }: glide to a named pose. Clears curl overrides.
  • hand.curl.set { entityId, curl, digit? }: set curl directly, one digit (0 is the index, thumb last) or all of them.
  • hand.attach { entityId, hostEntityId, bone, offsetPosition?, offsetRotation? }: re-target at runtime.
  • hand.detach { entityId }: back to the free-standing transform.
  • hand.trigger { entityId }: one squeeze. Ignored while one is in flight.

Emits:

  • hand.pose.changed { entityId, pose }.
  • hand.attached { entityId, hostEntityId, bone }: first frame the host bone resolved.
  • hand.trigger.pulled { entityId, muzzlePosition, muzzleDirection }: fired at the squeeze's peak. Both muzzle fields are null without the pistol.

The graft plugin

driveMesh needs the bundle's adapter plugin registered once at bootstrap. The playground and the create-arcade scaffolds already do this for every component.

TypeScript
import { registerHandRigExtension } from './components/Hand/adapter/register';

const adapter = new BabylonAdapter();
registerHandRigExtension(adapter);

Babylon and Three plugins ship in the bundle. On babylon-lite or mock the hand degrades to the overlay and says so once.

While the host loads

The hand waits at its own position until the host model finishes loading, then snaps to the bone on the first frame it resolves. A transient bone miss after that holds the last pose instead of flicking to the origin. All orientation goes through quaternions, because the two engines disagree on Euler order. Parking a pooled hand hides its meshes and rests a live graft; a true destroy disposes them.

Dependencies

None required. Put Mesh on the host entity when attaching, since the bone lookup keys off that entity's id. Add SkeletonAnimator to loop the figure's clip so the wrists move under the hands.

More like this

AiPilot
AnimateMeshy
Animation
ArcCamera
Asteroid
Bullet
CameraFollow
DirectionalLight
EnemySpawner
EnvironmentTexture
FreighterCar
FreighterChain
FreighterHead
GameConfig
Health
HemisphericLight
Jump
KeyboardInput
KeyboardMover
Lifetime
LineOfSight
MachineGun
Mesh
MeshPrimitive
Missile
MissileLauncher
MissionDirector
MouseInput
Movement
Obstacle
ObstacleField
OilBlob
Parts
Physics
PlayerInput
PlayerWalkAnimator
Renderable
RespawnTimer
RoundReset
Score
Shadow
ShipLoadout
SkeletonAnimator
Skybox
SpaceDust
SpaceShooterBullet
SpaceShooterHealth
SpaceShooterScore
ToneMapping
TwinStickEnemy
WaveDirector
Waypoint
WaypointTrack

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search