fix: flickering hands

This commit is contained in:
Tom Boullay
2026-05-06 23:16:58 +01:00
parent 74a901a48b
commit 9a1849b0f8
3 changed files with 56 additions and 19 deletions
@@ -18,23 +18,22 @@ const GLOVE_CONFIGS: Record<
{ {
modelPath: string; modelPath: string;
rootNodeName: string; rootNodeName: string;
scale: number;
} }
> = { > = {
left: { left: {
modelPath: "/models/gant_l/model.gltf", modelPath: "/models/gant_l/model.gltf",
rootNodeName: "Armature", rootNodeName: "Armature",
scale: 0.17,
}, },
right: { right: {
modelPath: "/models/gant_r/model.gltf", modelPath: "/models/gant_r/model.gltf",
rootNodeName: "Hand_r", rootNodeName: "Hand_r",
scale: 0.04,
}, },
}; };
const HAND_SPACE_DISTANCE = 2.4; const GLOVE_MODEL_SCALE = 0.33;
const HAND_DEPTH_SCALE = 0.45; const HAND_SPACE_DISTANCE = 0.5;
const HAND_DEPTH_SCALE = 0.5;
const HAND_TRACKING_HIDE_DELAY_MS = 250;
const FINGER_LANDMARK_CHAINS = [ const FINGER_LANDMARK_CHAINS = [
[0, 1, 2, 3, 4], [0, 1, 2, 3, 4],
@@ -104,7 +103,7 @@ class HandTrackingGloveErrorBoundary extends Component<
{ {
modelPath: this.props.modelPath, modelPath: this.props.modelPath,
scope: `HandTrackingGlove.${this.props.handedness}`, scope: `HandTrackingGlove.${this.props.handedness}`,
scale: GLOVE_CONFIGS[this.props.handedness].scale, scale: GLOVE_MODEL_SCALE,
}, },
error, error,
); );
@@ -252,8 +251,9 @@ function HandTrackingGloveModel({
const modelPath = config.modelPath; const modelPath = config.modelPath;
const gltf = useLoggedGLTF(modelPath, { const gltf = useLoggedGLTF(modelPath, {
scope: `HandTrackingGlove.${handedness}`, scope: `HandTrackingGlove.${handedness}`,
scale: config.scale, scale: GLOVE_MODEL_SCALE,
}); });
const lastTrackedAtRef = useRef<number | null>(null);
const gloveScene = useMemo(() => { const gloveScene = useMemo(() => {
const rootNode = gltf.scene.getObjectByName(config.rootNodeName); const rootNode = gltf.scene.getObjectByName(config.rootNodeName);
@@ -261,17 +261,16 @@ function HandTrackingGloveModel({
throw new Error(`Missing glove root node ${config.rootNodeName}`); 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]); }, [config.rootNodeName, gltf.scene]);
const fingerPoseChains = useMemo( const fingerPoseChains = useMemo(
() => createFingerPoseChains(gloveScene), () => createFingerPoseChains(gloveScene),
[gloveScene], [gloveScene],
); );
const hand = hands.find((candidate) =>
matchesHandedness(candidate.handedness, handedness),
);
useEffect(() => { useEffect(() => {
setGloveStatus(handedness, "loaded"); setGloveStatus(handedness, "loaded");
}, [handedness, setGloveStatus]); }, [handedness, setGloveStatus]);
@@ -282,12 +281,23 @@ function HandTrackingGloveModel({
matchesHandedness(candidate.handedness, handedness), matchesHandedness(candidate.handedness, handedness),
); );
if (!group || !trackedHand || trackedHand.landmarks.length < 21) { if (!group) return;
if (group) group.visible = false;
resetFingerPose(fingerPoseChains); 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; return;
} }
lastTrackedAtRef.current = performance.now();
group.visible = true; group.visible = true;
const wrist = trackedHand.landmarks[0]; const wrist = trackedHand.landmarks[0];
@@ -335,14 +345,12 @@ function HandTrackingGloveModel({
group.quaternion.slerp(_targetQuaternion, Math.min(1, delta * 18)); group.quaternion.slerp(_targetQuaternion, Math.min(1, delta * 18));
const palmLength = _wristPosition.distanceTo(_middlePosition); const palmLength = _wristPosition.distanceTo(_middlePosition);
const scale = palmLength * config.scale; const scale = palmLength * GLOVE_MODEL_SCALE;
group.scale.setScalar(scale); group.scale.setScalar(scale);
group.updateMatrixWorld(true); group.updateMatrixWorld(true);
applyFingerPose(fingerPoseChains, trackedHand.landmarks, camera); applyFingerPose(fingerPoseChains, trackedHand.landmarks, camera);
}); });
if (!hand) return null;
return <primitive ref={groupRef} object={gloveScene} />; return <primitive ref={groupRef} object={gloveScene} />;
} }
+9 -1
View File
@@ -1,5 +1,6 @@
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot"; import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import { useHandTrackingGloveStatus } from "@/hooks/handTracking/useHandTrackingGloveStatus"; import { useHandTrackingGloveStatus } from "@/hooks/handTracking/useHandTrackingGloveStatus";
import { useDebugStore } from "@/hooks/debug/useDebugStore";
const HAND_CONNECTIONS: Array<[number, number]> = [ const HAND_CONNECTIONS: Array<[number, number]> = [
[0, 1], [0, 1],
@@ -27,12 +28,19 @@ const HAND_CONNECTIONS: Array<[number, number]> = [
export function HandTrackingVisualizer(): React.JSX.Element | null { export function HandTrackingVisualizer(): React.JSX.Element | null {
const { hands, status } = useHandTrackingSnapshot(); const { hands, status } = useHandTrackingSnapshot();
const showHandTrackingSvg = useDebugStore((debug) =>
debug.getShowHandTrackingSvg(),
);
const gloves = useHandTrackingGloveStatus((state) => state.gloves); const gloves = useHandTrackingGloveStatus((state) => state.gloves);
const hasLoadedGlove = Object.values(gloves).some( const hasLoadedGlove = Object.values(gloves).some(
(gloveStatus) => gloveStatus === "loaded", (gloveStatus) => gloveStatus === "loaded",
); );
if (status === "idle" || hands.length === 0 || hasLoadedGlove) { if (
status === "idle" ||
hands.length === 0 ||
(hasLoadedGlove && !showHandTrackingSvg)
) {
return null; return null;
} }
+21
View File
@@ -53,6 +53,7 @@ export class Debug {
private readonly controls: { private readonly controls: {
cameraMode: CameraMode; cameraMode: CameraMode;
showDebugOverlay: boolean; showDebugOverlay: boolean;
showHandTrackingSvg: boolean;
showInteractionSpheres: boolean; showInteractionSpheres: boolean;
showPerf: boolean; showPerf: boolean;
sceneMode: SceneMode; sceneMode: SceneMode;
@@ -73,6 +74,7 @@ export class Debug {
this.controls = { this.controls = {
cameraMode: storedControls.cameraMode ?? "player", cameraMode: storedControls.cameraMode ?? "player",
showDebugOverlay: true, showDebugOverlay: true,
showHandTrackingSvg: false,
showInteractionSpheres: false, showInteractionSpheres: false,
showPerf: true, showPerf: true,
sceneMode: storedControls.sceneMode ?? "game", sceneMode: storedControls.sceneMode ?? "game",
@@ -116,6 +118,16 @@ export class Debug {
this.controls.showDebugOverlay = value; this.controls.showDebugOverlay = value;
this.emit(); 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; return this.controls.showInteractionSpheres;
} }
getShowHandTrackingSvg(): boolean {
return this.controls.showHandTrackingSvg;
}
setShowHandTrackingSvg(value: boolean): void {
this.controls.showHandTrackingSvg = value;
this.emit();
}
setShowInteractionSpheres(value: boolean): void { setShowInteractionSpheres(value: boolean): void {
this.controls.showInteractionSpheres = value; this.controls.showInteractionSpheres = value;
this.emit(); this.emit();