Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-21 21:07:52 +00:00
parent b1beb90c32
commit 705d7b171b
+54 -95
View File
@@ -6,25 +6,23 @@ import * as THREE from 'three';
/** /**
* Quest 3 thumbstick locomotion (rig-relative). * Quest 3 thumbstick locomotion (rig-relative).
* *
* Mapeamento: * Mapeamento (thumbstick = axes[2]/axes[3] em xr-standard):
* • Joystick para frente (axes[1] < -0.7) e soltar (volta a 0) * • Para frente (axes[1] < -0.7), libera (axis volta a ~0) → teletransporta
* → teletransporta para o ponto apontado pelo controle direito * para o ponto onde o usuário está olhando (raycast da câmera). Se não
* (raycast contra `scene.children`). Sem alvo → passo de 0.5 m * houver geometria atingida, dá um passo de 0.5 m na direção do olhar.
* na direção do controle. * • Para trás (axes[1] > 0.7) → passo de 0.3 m oposto ao olhar.
* • Joystick para trás (axes[1] > 0.7) → passo de 0.3 m para trás * • Lateral (|axes[0]| > 0.7) → snap-rotate ±30° no eixo Y do rig.
* (em relação à orientação do XROrigin / yaw da câmera).
* • Joystick lateral (|axes[0]| > 0.7) → snap-rotate ±30° em torno
* do eixo Y do XROrigin.
* *
* Lê apenas o thumbstick (axes[2]/axes[3] em xr-standard) — não toca em * NÃO lê grip ou trigger — esses são responsabilidade exclusiva de
* grip nem trigger. * XRGrabbable e XRControllerMeasure, respectivamente.
*/ */
interface Props { interface Props {
/** Ref para o <group> que envolve <XROrigin/>; sua posição/rotação move o usuário. */ /** Ref para o <group> que envolve <XROrigin/>; sua pose move o usuário. */
rigRef: RefObject<THREE.Group>; rigRef: RefObject<THREE.Group>;
} }
const DEAD = 0.7; const DEAD = 0.7;
const RELEASE = 0.3;
const SNAP_DEG = 30; const SNAP_DEG = 30;
const STEP_BACK = 0.3; const STEP_BACK = 0.3;
const STEP_FWD_NO_HIT = 0.5; const STEP_FWD_NO_HIT = 0.5;
@@ -33,42 +31,30 @@ export function ControllerLocomotion({ rigRef }: Props) {
const { scene, camera } = useThree(); const { scene, camera } = useThree();
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
// Edge states per direction (need to release to fire again) const fwdPushed = useRef(false);
const fwdArmed = useRef(false);
const fwdLatched = useRef(false); // armed once axis exceeded threshold; fires on release
const backLatched = useRef(false); const backLatched = useRef(false);
const rotLatched = useRef<0 | -1 | 1>(0); const rotLatched = useRef<0 | -1 | 1>(0);
const raycaster = useRef(new THREE.Raycaster()); const raycaster = useRef(new THREE.Raycaster());
useFrame((_state, _dt, frame: XRFrame | undefined) => { useFrame(() => {
const rig = rigRef.current; const rig = rigRef.current;
if (!rig || !session || !frame) return; if (!rig || !session) return;
const refSpace = (camera as unknown as { matrixWorldInverse: THREE.Matrix4 }) && (frame as XRFrame).session
? (frame as XRFrame).session
: null;
if (!refSpace) return;
// Find thumbstick + right controller pose
let stickX = 0; let stickX = 0;
let stickY = 0; let stickY = 0;
let rightSource: XRInputSource | null = null;
for (const src of session.inputSources) { for (const src of session.inputSources) {
const gp = src.gamepad; const gp = src.gamepad;
if (!gp) continue; if (!gp || gp.axes.length < 4) continue;
// xr-standard: axes[2]=thumbstick X, axes[3]=thumbstick Y // Prefer left stick for locomotion
if (gp.axes.length >= 4) { if (src.handedness === 'left') {
// Prefer left for locomotion; if no left stick, use right stickX = gp.axes[2] ?? 0;
if (src.handedness === 'left') { stickY = gp.axes[3] ?? 0;
stickX = gp.axes[2] ?? 0; break;
stickY = gp.axes[3] ?? 0; }
} else if (src.handedness === 'right' && stickX === 0 && stickY === 0) { if (src.handedness === 'right' && stickX === 0 && stickY === 0) {
stickX = gp.axes[2] ?? 0; stickX = gp.axes[2] ?? 0;
stickY = gp.axes[3] ?? 0; stickY = gp.axes[3] ?? 0;
}
} }
if (src.handedness === 'right') rightSource = src;
} }
// ── Snap-rotate (lateral) ────────────────────────────────────── // ── Snap-rotate (lateral) ──────────────────────────────────────
@@ -78,7 +64,7 @@ export function ControllerLocomotion({ rigRef }: Props) {
rotLatched.current = dir; rotLatched.current = dir;
rig.rotation.y -= (dir * SNAP_DEG * Math.PI) / 180; rig.rotation.y -= (dir * SNAP_DEG * Math.PI) / 180;
} }
} else if (Math.abs(stickX) < 0.3) { } else if (Math.abs(stickX) < RELEASE) {
rotLatched.current = 0; rotLatched.current = 0;
} }
@@ -86,70 +72,43 @@ export function ControllerLocomotion({ rigRef }: Props) {
if (stickY > DEAD) { if (stickY > DEAD) {
if (!backLatched.current) { if (!backLatched.current) {
backLatched.current = true; backLatched.current = true;
// Move oposto à direção em que o usuário olha (xz)
const headDir = new THREE.Vector3(); const headDir = new THREE.Vector3();
camera.getWorldDirection(headDir); camera.getWorldDirection(headDir);
headDir.y = 0; headDir.normalize(); headDir.y = 0;
rig.position.addScaledVector(headDir, -STEP_BACK); if (headDir.lengthSq() > 1e-6) {
headDir.normalize();
rig.position.addScaledVector(headDir, -STEP_BACK);
}
} }
} else if (stickY < 0.3) { } else if (stickY < RELEASE) {
backLatched.current = false; backLatched.current = false;
} }
// ── Forward → teleport (arm on push, fire on release) ───────── // ── Forward → teleport on release ─────────────────────────────
if (stickY < -DEAD) { if (stickY < -DEAD) {
fwdArmed.current = true; fwdPushed.current = true;
fwdLatched.current = true; } else if (fwdPushed.current && stickY > -RELEASE) {
} else if (fwdLatched.current && stickY > -0.2) { fwdPushed.current = false;
fwdLatched.current = false; // Cast from camera forward to find target on real/virtual geometry
// Fire teleport const origin = new THREE.Vector3();
const refSpaceObj = (frame.session as XRSession).requestReferenceSpace; // not used; we use camera world camera.getWorldPosition(origin);
void refSpaceObj; const dir = new THREE.Vector3();
let targetWorld: THREE.Vector3 | null = null; camera.getWorldDirection(dir);
if (rightSource && rightSource.targetRaySpace) { raycaster.current.set(origin, dir);
const pose = frame.getPose(rightSource.targetRaySpace, (camera as unknown as { parent: THREE.Object3D }).parent raycaster.current.far = 30;
? (frame as XRFrame).session as unknown as XRReferenceSpace const hits = raycaster.current.intersectObjects(scene.children, true);
: null as unknown as XRReferenceSpace); const hit = hits.find((h) => {
// Fallback path: build ray from controller via WebXR reference const o = h.object;
void pose; if (!(o instanceof THREE.Mesh)) return false;
} if (o.userData.__edgeLine) return false;
// Simpler & robust: get controller pose from its object via three's renderer return true;
const gl = (rigRef.current as unknown as { __gl?: unknown }).__gl as unknown; });
void gl; const target = hit ? hit.point.clone() : origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_HIT));
const camWorld = new THREE.Vector3();
// We can't get the controller's three.js Object3D here without the XR camera.getWorldPosition(camWorld);
// input wrapper — use the camera as a fallback ray origin: shoot from const delta = new THREE.Vector3().subVectors(target, camWorld);
// camera forward until it hits something. delta.y = 0; // preserva altura do usuário
if (!targetWorld) { rig.position.add(delta);
const origin = new THREE.Vector3();
camera.getWorldPosition(origin);
const dir = new THREE.Vector3();
camera.getWorldDirection(dir);
raycaster.current.set(origin, dir);
raycaster.current.far = 30;
const hits = raycaster.current.intersectObjects(scene.children, true);
const hit = hits.find((h) => {
const o = h.object;
if (!(o instanceof THREE.Mesh)) return false;
if (o.userData.__edgeLine) return false;
return true;
});
if (hit) {
targetWorld = hit.point.clone();
} else {
// No hit: step forward STEP_FWD_NO_HIT m on the floor plane
targetWorld = origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_HIT));
}
}
if (targetWorld) {
// Move the rig so the camera ends up at targetWorld (keep camera Y).
const camWorld = new THREE.Vector3();
camera.getWorldPosition(camWorld);
const delta = new THREE.Vector3().subVectors(targetWorld, camWorld);
delta.y = 0; // keep user height
rig.position.add(delta);
}
fwdArmed.current = false;
} }
}); });