update: enable hand tracking for repair steps (not only when we are close to something)

This commit is contained in:
Tom Boullay
2026-05-08 01:30:27 +01:00
parent ed60114d06
commit f5da2f4994
3 changed files with 42 additions and 9 deletions
+12 -5
View File
@@ -4,9 +4,9 @@ This document describes the hand tracking system that exists in the current code
## Purpose ## Purpose
Hand tracking is a debug-stage interaction system used to test direct 3D object manipulation with a webcam. It allows a user to close their fist to grab a nearby object and move it in 3D space without relying on the center crosshair. Hand tracking started as a debug-stage interaction system used to test direct 3D object manipulation with a webcam. It allows a user to close their fist to grab a nearby object and move it in 3D space without relying on the center crosshair.
The feature is scoped to the debug physics scene rather than production gameplay input. It is now also available to the production repair flow when a mission reaches a hand-driven step.
## Runtime Flow ## Runtime Flow
@@ -16,13 +16,13 @@ The feature is scoped to the debug physics scene rather than production gameplay
4. The backend returns hand data including landmarks, handedness, score, center point, and `isFist`. 4. The backend returns hand data including landmarks, handedness, score, center point, and `isFist`.
5. React stores the latest snapshot in the hand tracking provider. 5. React stores the latest snapshot in the hand tracking provider.
6. `GrabbableObject` reads that snapshot each frame and uses fist state plus raycasting to grab objects. 6. `GrabbableObject` reads that snapshot each frame and uses fist state plus raycasting to grab objects.
7. `HandTrackingGlove` reads the same snapshot and places the rigged `gant_l` and `gant_r` models on the detected hands in the debug physics scene. 7. `HandTrackingGlove` reads the same snapshot and places the rigged `gant_l` and `gant_r` models on the detected hands when hand tracking is active.
## Activation Rules ## Activation Rules
Hand tracking is intentionally gated so the webcam and backend are not used all the time. Hand tracking is intentionally gated so the webcam and backend are not used all the time.
The current activation conditions are: The debug activation conditions are:
- debug mode is active with `?debug` - debug mode is active with `?debug`
- scene mode is `physics` - scene mode is `physics`
@@ -30,6 +30,13 @@ The current activation conditions are:
This keeps hand tracking active while the player is inside an interaction zone, even if the camera is not aimed directly at the object. This keeps hand tracking active while the player is inside an interaction zone, even if the camera is not aimed directly at the object.
The production repair activation conditions are:
- active `mainState` is `bike`, `pylone`, or `ferme`
- the active mission step is `inspected`, `repairing`, or `done`
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands.
## Backend ## Backend
The backend lives in `backend/` and exposes: The backend lives in `backend/` and exposes:
@@ -121,7 +128,7 @@ The glove models are intentionally smaller than the raw SVG overlay so they do n
## Known Limitations ## Known Limitations
- The feature is debug-only and focused on the physics test scene. - Production usage is currently limited to repair mission steps that explicitly need hands.
- MediaPipe depth is relative and can be noisy. - MediaPipe depth is relative and can be noisy.
- The virtual hit zone is an approximation based on multiple raycasts, not a real 3D collider. - The virtual hit zone is an approximation based on multiple raycasts, not a real 3D collider.
- There is no smoothing layer for hand position or depth yet. - There is no smoothing layer for hand position or depth yet.
@@ -8,6 +8,14 @@ import {
} from "@/hooks/handTracking/useHandTrackingSnapshot"; } from "@/hooks/handTracking/useHandTrackingSnapshot";
import { useBrowserHandTracking } from "@/hooks/handTracking/useBrowserHandTracking"; import { useBrowserHandTracking } from "@/hooks/handTracking/useBrowserHandTracking";
import { useRemoteHandTracking } from "@/hooks/handTracking/useRemoteHandTracking"; import { useRemoteHandTracking } from "@/hooks/handTracking/useRemoteHandTracking";
import { useGameStore } from "@/managers/stores/useGameStore";
import type { MissionStep } from "@/managers/stores/useGameStore";
const REPAIR_HAND_TRACKING_STEPS = new Set<MissionStep>([
"inspected",
"repairing",
"done",
]);
export function HandTrackingProvider({ export function HandTrackingProvider({
children, children,
@@ -18,8 +26,23 @@ export function HandTrackingProvider({
const handTrackingSource = useDebugStore((debug) => const handTrackingSource = useDebugStore((debug) =>
debug.getHandTrackingSource(), debug.getHandTrackingSource(),
); );
const repairNeedsHands = useGameStore((state) => {
switch (state.mainState) {
case "bike":
return REPAIR_HAND_TRACKING_STEPS.has(state.bike.currentStep);
case "pylone":
return REPAIR_HAND_TRACKING_STEPS.has(state.pylone.currentStep);
case "ferme":
return REPAIR_HAND_TRACKING_STEPS.has(state.ferme.currentStep);
case "intro":
case "outro":
return false;
}
});
const { nearby, holding, handHolding } = useInteraction(); const { nearby, holding, handHolding } = useInteraction();
const enabled = sceneMode === "physics" && (nearby || holding || handHolding); const enabled =
repairNeedsHands ||
(sceneMode === "physics" && (nearby || holding || handHolding));
const backendSnapshot = useRemoteHandTracking({ const backendSnapshot = useRemoteHandTracking({
enabled: enabled && handTrackingSource === "backend", enabled: enabled && handTrackingSource === "backend",
}); });
+6 -3
View File
@@ -7,6 +7,7 @@ import {
} from "@/data/player/playerConfig"; } from "@/data/player/playerConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls"; import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControls";
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers"; import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove"; import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove";
@@ -21,25 +22,28 @@ import { TestMap } from "@/world/debug/TestMap";
export function World(): React.JSX.Element { export function World(): React.JSX.Element {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const { status, usageStatus } = useHandTrackingSnapshot();
const [octree, setOctree] = useState<Octree | null>(null); const [octree, setOctree] = useState<Octree | null>(null);
const playerSpawnPosition = const playerSpawnPosition =
sceneMode === "game" sceneMode === "game"
? PLAYER_SPAWN_POSITION_GAME ? PLAYER_SPAWN_POSITION_GAME
: PLAYER_SPAWN_POSITION_PHYSICS; : PLAYER_SPAWN_POSITION_PHYSICS;
const showHandTrackingGloves =
sceneMode === "physics" ||
(status !== "idle" && usageStatus !== "inactive");
return ( return (
<> <>
<Environment /> <Environment />
<Lighting /> <Lighting />
<DebugHelpers /> <DebugHelpers />
{sceneMode === "physics" ? ( {showHandTrackingGloves ? (
<> <>
<HandTrackingGlove handedness="left" /> <HandTrackingGlove handedness="left" />
<HandTrackingGlove handedness="right" /> <HandTrackingGlove handedness="right" />
</> </>
) : null} ) : null}
{cameraMode === "debug" ? <DebugCameraControls /> : null} {cameraMode === "debug" ? <DebugCameraControls /> : null}
{sceneMode === "game" ? ( {sceneMode === "game" ? (
<> <>
<GameMusic /> <GameMusic />
@@ -51,7 +55,6 @@ export function World(): React.JSX.Element {
) : ( ) : (
<TestMap onOctreeReady={setOctree} /> <TestMap onOctreeReady={setOctree} />
)} )}
{cameraMode !== "debug" ? ( {cameraMode !== "debug" ? (
<Player octree={octree} spawnPosition={playerSpawnPosition} /> <Player octree={octree} spawnPosition={playerSpawnPosition} />
) : null} ) : null}