From 553dc6eb0ae082a2218a847d886ee1f73800dc90 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Wed, 6 May 2026 23:16:58 +0100 Subject: [PATCH] fix: flickering hands --- .../three/handTracking/HandTrackingGlove.tsx | 44 +++++++++++-------- src/components/ui/HandTrackingVisualizer.tsx | 10 ++++- src/utils/debug/Debug.ts | 21 +++++++++ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/components/three/handTracking/HandTrackingGlove.tsx b/src/components/three/handTracking/HandTrackingGlove.tsx index 064758e..1158b6d 100644 --- a/src/components/three/handTracking/HandTrackingGlove.tsx +++ b/src/components/three/handTracking/HandTrackingGlove.tsx @@ -18,23 +18,22 @@ const GLOVE_CONFIGS: Record< { modelPath: string; rootNodeName: string; - scale: number; } > = { left: { modelPath: "/models/gant_l/model.gltf", rootNodeName: "Armature", - scale: 0.17, }, right: { modelPath: "/models/gant_r/model.gltf", rootNodeName: "Hand_r", - scale: 0.04, }, }; -const HAND_SPACE_DISTANCE = 2.4; -const HAND_DEPTH_SCALE = 0.45; +const GLOVE_MODEL_SCALE = 0.33; +const HAND_SPACE_DISTANCE = 0.5; +const HAND_DEPTH_SCALE = 0.5; +const HAND_TRACKING_HIDE_DELAY_MS = 250; const FINGER_LANDMARK_CHAINS = [ [0, 1, 2, 3, 4], @@ -104,7 +103,7 @@ class HandTrackingGloveErrorBoundary extends Component< { modelPath: this.props.modelPath, scope: `HandTrackingGlove.${this.props.handedness}`, - scale: GLOVE_CONFIGS[this.props.handedness].scale, + scale: GLOVE_MODEL_SCALE, }, error, ); @@ -252,8 +251,9 @@ function HandTrackingGloveModel({ const modelPath = config.modelPath; const gltf = useLoggedGLTF(modelPath, { scope: `HandTrackingGlove.${handedness}`, - scale: config.scale, + scale: GLOVE_MODEL_SCALE, }); + const lastTrackedAtRef = useRef(null); const gloveScene = useMemo(() => { const rootNode = gltf.scene.getObjectByName(config.rootNodeName); @@ -261,17 +261,16 @@ function HandTrackingGloveModel({ throw new Error(`Missing glove root node ${config.rootNodeName}`); } - return clone(rootNode); + const clonedRootNode = clone(rootNode); + clonedRootNode.visible = false; + + return clonedRootNode; }, [config.rootNodeName, gltf.scene]); const fingerPoseChains = useMemo( () => createFingerPoseChains(gloveScene), [gloveScene], ); - const hand = hands.find((candidate) => - matchesHandedness(candidate.handedness, handedness), - ); - useEffect(() => { setGloveStatus(handedness, "loaded"); }, [handedness, setGloveStatus]); @@ -282,12 +281,23 @@ function HandTrackingGloveModel({ matchesHandedness(candidate.handedness, handedness), ); - if (!group || !trackedHand || trackedHand.landmarks.length < 21) { - if (group) group.visible = false; - resetFingerPose(fingerPoseChains); + if (!group) return; + + if (!trackedHand || trackedHand.landmarks.length < 21) { + const lastTrackedAt = lastTrackedAtRef.current; + const shouldHide = + lastTrackedAt === null || + performance.now() - lastTrackedAt > HAND_TRACKING_HIDE_DELAY_MS; + + if (shouldHide) { + group.visible = false; + resetFingerPose(fingerPoseChains); + } + return; } + lastTrackedAtRef.current = performance.now(); group.visible = true; const wrist = trackedHand.landmarks[0]; @@ -335,14 +345,12 @@ function HandTrackingGloveModel({ group.quaternion.slerp(_targetQuaternion, Math.min(1, delta * 18)); const palmLength = _wristPosition.distanceTo(_middlePosition); - const scale = palmLength * config.scale; + const scale = palmLength * GLOVE_MODEL_SCALE; group.scale.setScalar(scale); group.updateMatrixWorld(true); applyFingerPose(fingerPoseChains, trackedHand.landmarks, camera); }); - if (!hand) return null; - return ; } diff --git a/src/components/ui/HandTrackingVisualizer.tsx b/src/components/ui/HandTrackingVisualizer.tsx index 203b729..98c6e1b 100644 --- a/src/components/ui/HandTrackingVisualizer.tsx +++ b/src/components/ui/HandTrackingVisualizer.tsx @@ -1,5 +1,6 @@ import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useHandTrackingGloveStatus } from "@/hooks/handTracking/useHandTrackingGloveStatus"; +import { useDebugStore } from "@/hooks/debug/useDebugStore"; const HAND_CONNECTIONS: Array<[number, number]> = [ [0, 1], @@ -27,12 +28,19 @@ const HAND_CONNECTIONS: Array<[number, number]> = [ export function HandTrackingVisualizer(): React.JSX.Element | null { const { hands, status } = useHandTrackingSnapshot(); + const showHandTrackingSvg = useDebugStore((debug) => + debug.getShowHandTrackingSvg(), + ); const gloves = useHandTrackingGloveStatus((state) => state.gloves); const hasLoadedGlove = Object.values(gloves).some( (gloveStatus) => gloveStatus === "loaded", ); - if (status === "idle" || hands.length === 0 || hasLoadedGlove) { + if ( + status === "idle" || + hands.length === 0 || + (hasLoadedGlove && !showHandTrackingSvg) + ) { return null; } diff --git a/src/utils/debug/Debug.ts b/src/utils/debug/Debug.ts index d45c15a..e73c723 100644 --- a/src/utils/debug/Debug.ts +++ b/src/utils/debug/Debug.ts @@ -53,6 +53,7 @@ export class Debug { private readonly controls: { cameraMode: CameraMode; showDebugOverlay: boolean; + showHandTrackingSvg: boolean; showInteractionSpheres: boolean; showPerf: boolean; sceneMode: SceneMode; @@ -73,6 +74,7 @@ export class Debug { this.controls = { cameraMode: storedControls.cameraMode ?? "player", showDebugOverlay: true, + showHandTrackingSvg: false, showInteractionSpheres: false, showPerf: true, sceneMode: storedControls.sceneMode ?? "game", @@ -116,6 +118,16 @@ export class Debug { this.controls.showDebugOverlay = value; this.emit(); }); + + const handTrackingFolder = this.createFolder("Hand Tracking"); + + handTrackingFolder + ?.add(this.controls, "showHandTrackingSvg") + .name("Afficher SVG") + .onChange((value: boolean) => { + this.controls.showHandTrackingSvg = value; + this.emit(); + }); } } @@ -179,6 +191,15 @@ export class Debug { return this.controls.showInteractionSpheres; } + getShowHandTrackingSvg(): boolean { + return this.controls.showHandTrackingSvg; + } + + setShowHandTrackingSvg(value: boolean): void { + this.controls.showHandTrackingSvg = value; + this.emit(); + } + setShowInteractionSpheres(value: boolean): void { this.controls.showInteractionSpheres = value; this.emit();