Skip to content

Hand Gestures

TL;DR: GestureLeft and GestureRight are floats in the range -1..6, with 0 as idle. Eight discrete values plus an analog band have meaning. The branch that produces the analog band is device-specific — for the default scheme (Oculus/Touch and similar) the analog value comes from the grip axis, not the trigger. Index/XR-Hand and Vive use the trigger axis instead.

ValueNameVisual
-1Hand OpenRelaxed open palm
0Idle / NeutralDefault fallback
0..1Trigger or grip curl (analog, device-specific)Continuous
2FistAll fingers closed
3Thumbs UpThumb extended, others closed
4PointIndex extended
5Peace / VictoryIndex + middle extended
6Gun / Rock ‘n’ RollIndex + thumb extended

These are also the integer keys for the blocked-avatar hand mesh — see the blocked-avatar mapping below for the actual blendshape index it produces.

CVRXRModule.Update_Gestures() dispatches based on Type:

case eXRControllerType.Index:
case eXRControllerType.XRHand:
Update_Gestures_Index(...);
break;
case eXRControllerType.Vive:
case eXRControllerType.MixedReality:
Update_Gestures_Vive();
break;
default:
Update_Gestures_Default();
break;

Update_Gestures_Default() checks three input categories — surface touch (any of Primary2DAxisTouch, PrimaryTouch, SecondaryTouch), trigger active (TriggerTouch || Trigger > 0), grip active (GripTouch || Grip > 0):

TouchTriggerGripGesture
nonono-1 (Hand Open)
yesyesyesanalog grip value
noyesyes (>0.5)2 (Fist)
nonoyes (>0.5)3 (Thumbs Up)
yesnoyes (>0.5)4 (Point)
yesnono5 (Peace)
yesyes (>0.5)no6 (Gun)

The 0..1 analog band on this scheme is the grip axis, not the trigger axis, and it requires touch + trigger + grip to all be simultaneously active.

Update_Gestures_Vive() projects Primary2DAxis (or EmoteOverride when held) into a polar angle and chooses a gesture by sector. Grip > 0.5 (without trackpad click) overrides everything to -1. Inside VrViveGestureDeadZone, Gesture = Trigger (analog).

Trackpad sectorLeft handRight hand
(252°, 324°]25
(180°, 252°]36
(324°, 360°] ∪ [0°, 36°]44
(36°, 108°]52
(108°, 180°]63
inside dead zone, no clickTriggerTrigger
Grip > 0.5, no click-1-1

Update_Gestures_Index(bool isIndex) thresholds the five FingerFullCurlNormalized* values against CVRInputManager.VrIndexLowerBound and VrIndexUpperBound. “low” = below VrIndexLowerBound; “high” = at or above VrIndexUpperBound.

ThumbIndexMiddleRingPinkyGesture
lowlowlowlowlow-1 (Hand Open)
any highhighhighhighhighTrigger (analog)
lowhighhighhighhigh2 (Fist)
lowlowhighhighhigh3 (Thumbs Up)
highlowhighhighhigh4 (Point)
highlowlowhighhigh5 (Peace)
highhighlowhighhigh6 (Gun)

The result lands in GestureRaw. Gesture only follows GestureRaw while SkeletalToggleValue is on (Index) or while SkeletalToggleValue && !IsInteractingWithButton (XR Hand).

GestureLeft/RightGestureLeftIdx/RightIdx

Section titled “GestureLeft/Right ↔ GestureLeftIdx/RightIdx”

Writing GestureLeft or GestureRight on AvatarAnimatorManager automatically computes the integer index and writes it to GestureLeftIdx or GestureRightIdx:

public float GestureRight
{
set
{
_coreSyncData.GestureRight = value;
if (Parameters.TryGetValue("GestureRight", out var p)) SetParameter_Internal(p, value);
if (Parameters.TryGetValue("GestureRightIdx", out var pIdx))
SetParameter_Internal(pIdx, GestureRightIdx = Mathf.RoundToInt(value));
}
}

You only need to declare GestureLeftIdx/GestureRightIdx on your animator if you want the integer form for state-machine transitions — the float GestureLeft/GestureRight is enough for most workflows.

CVRBlockedAvatarController.LateUpdate reads GestureLeft/GestureRight and drives a 7-element blendshape array on the blocked-avatar hand mesh. The mapping is not positional — gesture values are not used as blendshape indices directly:

GestureBlendshape index
-1 (Hand Open)0
4 (Point)1
3 (Thumbs Up)2
5 (Peace)3
anything else, non-zero (e.g. analog)4
2 (Fist)5
6 (Gun)6

Index 4 is the catch-all for any non-zero, non-handled value, so the analog 0..1 band drives blendshape 4 on blocked avatars regardless of which device produced it.