feat: grab objects with closed fist raycast

This commit is contained in:
Tom Boullay
2026-04-29 10:40:48 +02:00
parent cc4c11f934
commit a14ff9d913
+62 -31
View File
@@ -20,6 +20,7 @@ import {
GRAB_THROW_BOOST_MIN, GRAB_THROW_BOOST_MIN,
GRAB_THROW_BOOST_STEP, GRAB_THROW_BOOST_STEP,
} from "@/data/interaction/grabConfig"; } from "@/data/interaction/grabConfig";
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot"; import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot";
import type { ColliderShape, Vector3Tuple } from "@/types/three"; import type { ColliderShape, Vector3Tuple } from "@/types/three";
@@ -46,6 +47,9 @@ const _currentPos = new THREE.Vector3();
const _velocity = new THREE.Vector3(); const _velocity = new THREE.Vector3();
const _handNdc = new THREE.Vector3(); const _handNdc = new THREE.Vector3();
const _handDirection = new THREE.Vector3(); const _handDirection = new THREE.Vector3();
const _cameraPos = new THREE.Vector3();
const _objectPos = new THREE.Vector3();
const _handRaycaster = new THREE.Raycaster();
export function GrabbableObject({ export function GrabbableObject({
position, position,
@@ -56,8 +60,10 @@ export function GrabbableObject({
}: GrabbableObjectProps): React.JSX.Element { }: GrabbableObjectProps): React.JSX.Element {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const { hands } = useHandTrackingSnapshot(); const { hands } = useHandTrackingSnapshot();
const groupRef = useRef<THREE.Group>(null);
const rbRef = useRef<RapierRigidBody>(null); const rbRef = useRef<RapierRigidBody>(null);
const isHolding = useRef(false); const isHolding = useRef(false);
const isHandHolding = useRef(false);
useDebugFolder("GrabbableObject", (folder) => { useDebugFolder("GrabbableObject", (folder) => {
folder folder
@@ -96,23 +102,43 @@ export function GrabbableObject({
? hands.find((hand) => hand.isFist) ? hands.find((hand) => hand.isFist)
: undefined; : undefined;
if (!isHolding.current && !fistHand) return; const t = rbRef.current.translation();
_currentPos.set(t.x, t.y, t.z);
if (fistHand) { if (fistHand) {
_handNdc.set((1 - fistHand.x) * 2 - 1, -fistHand.y * 2 + 1, 0.5); _handNdc.set((1 - fistHand.x) * 2 - 1, -fistHand.y * 2 + 1, 0.5);
_handNdc.unproject(camera); _handNdc.unproject(camera);
_handDirection.subVectors(_handNdc, camera.position).normalize(); camera.getWorldPosition(_cameraPos);
_handDirection.subVectors(_handNdc, _cameraPos).normalize();
if (!isHandHolding.current) {
_objectPos.copy(_currentPos);
_handRaycaster.set(_cameraPos, _handDirection);
_handRaycaster.far = INTERACTION_RADIUS;
const isObjectInRange =
_cameraPos.distanceTo(_objectPos) <= INTERACTION_RADIUS;
const hits = groupRef.current
? _handRaycaster.intersectObject(groupRef.current, true)
: [];
isHandHolding.current = isObjectInRange && hits.length > 0;
}
} else {
isHandHolding.current = false;
}
if (!isHolding.current && !isHandHolding.current) return;
if (fistHand && isHandHolding.current) {
_holdTarget _holdTarget
.copy(camera.position) .copy(_cameraPos)
.addScaledVector(_handDirection, params.holdDistance); .addScaledVector(_handDirection, params.holdDistance);
} else { } else {
camera.getWorldDirection(_holdTarget); camera.getWorldDirection(_holdTarget);
_holdTarget.multiplyScalar(params.holdDistance).add(camera.position); _holdTarget.multiplyScalar(params.holdDistance).add(camera.position);
} }
const t = rbRef.current.translation();
_currentPos.set(t.x, t.y, t.z);
_velocity _velocity
.subVectors(_holdTarget, _currentPos) .subVectors(_holdTarget, _currentPos)
.multiplyScalar(params.stiffness); .multiplyScalar(params.stiffness);
@@ -131,31 +157,36 @@ export function GrabbableObject({
colliders={colliders} colliders={colliders}
position={position} position={position}
> >
<InteractableObject <group ref={groupRef}>
kind="grab" <InteractableObject
label={label} kind="grab"
position={position} label={label}
bodyRef={rbRef} position={position}
onPress={() => { bodyRef={rbRef}
isHolding.current = true; onPress={() => {
}} isHolding.current = true;
onRelease={() => { }}
isHolding.current = false; onRelease={() => {
if (!rbRef.current || params.throwBoost === GRAB_THROW_BOOST_DEFAULT) isHolding.current = false;
return; if (
const v = rbRef.current.linvel(); !rbRef.current ||
rbRef.current.setLinvel( params.throwBoost === GRAB_THROW_BOOST_DEFAULT
{ )
x: v.x * params.throwBoost, return;
y: v.y * params.throwBoost, const v = rbRef.current.linvel();
z: v.z * params.throwBoost, rbRef.current.setLinvel(
}, {
true, x: v.x * params.throwBoost,
); y: v.y * params.throwBoost,
}} z: v.z * params.throwBoost,
> },
{children} true,
</InteractableObject> );
}}
>
{children}
</InteractableObject>
</group>
</RigidBody> </RigidBody>
); );
} }