Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -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,43 +31,31 @@ 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) {
|
|
||||||
// Prefer left for locomotion; if no left stick, use right
|
|
||||||
if (src.handedness === 'left') {
|
if (src.handedness === 'left') {
|
||||||
stickX = gp.axes[2] ?? 0;
|
stickX = gp.axes[2] ?? 0;
|
||||||
stickY = gp.axes[3] ?? 0;
|
stickY = gp.axes[3] ?? 0;
|
||||||
} else if (src.handedness === 'right' && stickX === 0 && stickY === 0) {
|
break;
|
||||||
|
}
|
||||||
|
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) ──────────────────────────────────────
|
||||||
if (Math.abs(stickX) > DEAD) {
|
if (Math.abs(stickX) > DEAD) {
|
||||||
@@ -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,41 +72,24 @@ 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;
|
||||||
|
if (headDir.lengthSq() > 1e-6) {
|
||||||
|
headDir.normalize();
|
||||||
rig.position.addScaledVector(headDir, -STEP_BACK);
|
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 refSpaceObj = (frame.session as XRSession).requestReferenceSpace; // not used; we use camera world
|
|
||||||
void refSpaceObj;
|
|
||||||
let targetWorld: THREE.Vector3 | null = null;
|
|
||||||
if (rightSource && rightSource.targetRaySpace) {
|
|
||||||
const pose = frame.getPose(rightSource.targetRaySpace, (camera as unknown as { parent: THREE.Object3D }).parent
|
|
||||||
? (frame as XRFrame).session as unknown as XRReferenceSpace
|
|
||||||
: null as unknown as XRReferenceSpace);
|
|
||||||
// Fallback path: build ray from controller via WebXR reference
|
|
||||||
void pose;
|
|
||||||
}
|
|
||||||
// Simpler & robust: get controller pose from its object via three's renderer
|
|
||||||
const gl = (rigRef.current as unknown as { __gl?: unknown }).__gl as unknown;
|
|
||||||
void gl;
|
|
||||||
|
|
||||||
// We can't get the controller's three.js Object3D here without the XR
|
|
||||||
// input wrapper — use the camera as a fallback ray origin: shoot from
|
|
||||||
// camera forward until it hits something.
|
|
||||||
if (!targetWorld) {
|
|
||||||
const origin = new THREE.Vector3();
|
const origin = new THREE.Vector3();
|
||||||
camera.getWorldPosition(origin);
|
camera.getWorldPosition(origin);
|
||||||
const dir = new THREE.Vector3();
|
const dir = new THREE.Vector3();
|
||||||
@@ -134,23 +103,13 @@ export function ControllerLocomotion({ rigRef }: Props) {
|
|||||||
if (o.userData.__edgeLine) return false;
|
if (o.userData.__edgeLine) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (hit) {
|
const target = hit ? hit.point.clone() : origin.clone().add(dir.multiplyScalar(STEP_FWD_NO_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();
|
const camWorld = new THREE.Vector3();
|
||||||
camera.getWorldPosition(camWorld);
|
camera.getWorldPosition(camWorld);
|
||||||
const delta = new THREE.Vector3().subVectors(targetWorld, camWorld);
|
const delta = new THREE.Vector3().subVectors(target, camWorld);
|
||||||
delta.y = 0; // keep user height
|
delta.y = 0; // preserva altura do usuário
|
||||||
rig.position.add(delta);
|
rig.position.add(delta);
|
||||||
}
|
}
|
||||||
fwdArmed.current = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user