fix: flickering hands

This commit is contained in:
Tom Boullay
2026-05-06 23:16:58 +01:00
parent 7ae45d4cfa
commit 553dc6eb0a
3 changed files with 56 additions and 19 deletions
@@ -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<number | null>(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 <primitive ref={groupRef} object={gloveScene} />;
}
+9 -1
View File
@@ -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;
}
+21
View File
@@ -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();