Fix Meta Quest 3 VR locomotion and physics drift

This commit is contained in:
Hermes
2026-08-05 22:19:16 +00:00
parent e74e835d6b
commit 32946b64f7
5 changed files with 203 additions and 255 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ export default function App() {
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}
> >
<XR store={store}> <XR store={store}>
<XRScene onEnterVR={handleEnterVR} onEnterAR={handleEnterAR} /> <XRScene />
</XR> </XR>
</Canvas> </Canvas>
</div> </div>
+102 -134
View File
@@ -24,149 +24,123 @@ export function InteractiveCube({
locked, locked,
onFaceSelect, onFaceSelect,
resetSignal, resetSignal,
tableY = 0.625, // Height of table top tableY = 0.625,
tableBounds = { minX: -1, maxX: 1, minZ: -1.5, maxZ: -0.5 }, tableBounds = { minX: -1, maxX: 1, minZ: -1.5, maxZ: -0.5 },
}: InteractiveCubeProps) { }: InteractiveCubeProps) {
const meshRef = useRef<THREE.Group>(null); const meshRef = useRef<THREE.Group>(null);
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
// Cube state & transform // Physics state
const pos = useRef(new THREE.Vector3(...initialPosition)); const pos = useRef(new THREE.Vector3(...initialPosition));
const vel = useRef(new THREE.Vector3(0, 0, 0)); const vel = useRef(new THREE.Vector3(0, 0, 0));
const rot = useRef(new THREE.Euler(0, 0, 0)); const rot = useRef(new THREE.Quaternion().identity());
const angVel = useRef(new THREE.Vector3(0, 0, 0)); const angVel = useRef(new THREE.Vector3(0, 0, 0));
const scale = useRef(1); const scale = useRef(1);
// Interaction tracking // Hover & selection
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [hoveredFace, setHoveredFace] = useState<FaceId | null>(null); const [hoveredFace, setHoveredFace] = useState<FaceId | null>(null);
const [selectedFace, setSelectedFace] = useState<FaceId | null>(null); const [selectedFace, setSelectedFace] = useState<FaceId | null>(null);
// Grab state // Grab state - using ray to prevent feedback loop drift
const isGrabbed = useRef(false); const isGrabbed = useRef(false);
const grabControllerId = useRef<any>(null); const grabPointerId = useRef<number | null>(null);
const prevControllerPos = useRef(new THREE.Vector3()); const grabDistance = useRef<number>(0);
const grabOffsetPos = useRef(new THREE.Vector3());
// Two-hand scale/rotate state const previousRayPos = useRef(new THREE.Vector3());
const secondGrabControllerId = useRef<any>(null);
const initialHandDist = useRef<number>(1);
const initialCubeScale = useRef<number>(1);
// Reset physics signal // Reset signal listener
const lastResetSignal = useRef(resetSignal); const lastResetSignal = useRef(resetSignal);
if (resetSignal !== lastResetSignal.current) { if (resetSignal !== lastResetSignal.current) {
lastResetSignal.current = resetSignal; lastResetSignal.current = resetSignal;
pos.current.set(...initialPosition); pos.current.set(...initialPosition);
vel.current.set(0, 0, 0); vel.current.set(0, 0, 0);
angVel.current.set(0, 0, 0); angVel.current.set(0, 0, 0);
rot.current.set(0, 0, 0); rot.current.identity();
scale.current = 1; scale.current = 1;
isGrabbed.current = false;
setSelectedFace(null); setSelectedFace(null);
if (meshRef.current) {
meshRef.current.position.copy(pos.current);
meshRef.current.rotation.copy(rot.current);
meshRef.current.scale.setScalar(1);
}
} }
// Handle pointer hover / grab events (works natively with XR rays and hands) const onPointerDown = (e: ThreeEvent<PointerEvent>) => {
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => { if (locked || isGrabbed.current) return;
if (locked) return;
e.stopPropagation(); e.stopPropagation();
// Capture pointer to track dragging outside object bounds
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
isGrabbed.current = true;
grabPointerId.current = e.pointerId;
// Save the distance from the controller ray origin to the intersection point
grabDistance.current = e.distance;
// Track velocity
previousRayPos.current.copy(e.point);
vel.current.set(0, 0, 0);
angVel.current.set(0, 0, 0);
soundFx.playGrab(); soundFx.playGrab();
triggerHaptic(session, 'any', 0.8, 60); triggerHaptic(session, 'any', 0.8, 60);
const controller = (e as any).pointerId ?? 'primary';
if (!isGrabbed.current) {
isGrabbed.current = true;
grabControllerId.current = controller;
if (e.point) {
grabOffsetPos.current.copy(pos.current).sub(e.point);
} else {
grabOffsetPos.current.set(0, 0, 0);
}
prevControllerPos.current.copy(e.point || pos.current);
vel.current.set(0, 0, 0);
angVel.current.set(0, 0, 0);
} else if (!secondGrabControllerId.current && controller !== grabControllerId.current) {
// Second hand grab -> Pinch scale / rotate mode
secondGrabControllerId.current = controller;
initialCubeScale.current = scale.current;
if (e.point) {
initialHandDist.current = prevControllerPos.current.distanceTo(e.point) || 0.3;
}
}
}; };
const handlePointerUp = (e: ThreeEvent<PointerEvent>) => { const onPointerMove = (e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
const controller = (e as any).pointerId ?? 'primary';
if (secondGrabControllerId.current === controller) {
secondGrabControllerId.current = null;
} else if (grabControllerId.current === controller) {
if (secondGrabControllerId.current) {
grabControllerId.current = secondGrabControllerId.current;
secondGrabControllerId.current = null;
} else {
isGrabbed.current = false;
grabControllerId.current = null;
soundFx.playRelease();
triggerHaptic(session, 'any', 0.4, 40);
// Apply throw inertia boost
vel.current.multiplyScalar(1.2);
angVel.current.set(
(Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 4
);
}
}
};
const handleFaceClick = (face: FaceId, e: ThreeEvent<MouseEvent>) => {
e.stopPropagation();
if (locked) return; if (locked) return;
setSelectedFace(face); if (isGrabbed.current && e.pointerId === grabPointerId.current) {
soundFx.playSnap(); e.stopPropagation();
triggerHaptic(session, 'any', 0.6, 50);
if (onFaceSelect) onFaceSelect(face); // FIX: Use e.ray to calculate new position instead of e.point to avoid feedback drift
const ray = e.ray;
const targetPos = ray.origin.clone().add(ray.direction.clone().multiplyScalar(grabDistance.current));
// Calculate velocity for throwing
const dt = 0.016; // approximate frame time for velocity calc
vel.current.copy(targetPos).sub(pos.current).divideScalar(dt);
pos.current.copy(targetPos);
}
};
const onPointerUp = (e: ThreeEvent<PointerEvent>) => {
if (isGrabbed.current && e.pointerId === grabPointerId.current) {
e.stopPropagation();
(e.target as HTMLElement).releasePointerCapture?.(e.pointerId);
isGrabbed.current = false;
grabPointerId.current = null;
// Throw physics applied automatically via vel.current calculated in onPointerMove
soundFx.playRelease();
triggerHaptic(session, 'any', 0.4, 40);
}
}; };
// Main frame update for Physics & Grabbing
useFrame((_, delta) => { useFrame((_, delta) => {
if (!meshRef.current) return; if (!meshRef.current) return;
const dt = Math.min(delta, 0.05);
const clampedDelta = Math.min(delta, 0.05); if (!isGrabbed.current && !locked) {
// Physics Simulation
if (isGrabbed.current) {
// Calculate controller movement velocity
// Mesh is updated via event points or target tracking
} else if (!locked) {
// PHYSICS SIMULATION
const gravity = -9.81; const gravity = -9.81;
const currentScale = scale.current * size; const currentScale = scale.current * size;
const halfSize = currentScale / 2; const halfSize = currentScale / 2;
// Apply Gravity & Damping vel.current.y += gravity * dt;
vel.current.y += gravity * clampedDelta;
vel.current.x *= 1 - 0.5 * clampedDelta;
vel.current.z *= 1 - 0.5 * clampedDelta;
angVel.current.multiplyScalar(1 - 1.2 * clampedDelta);
// Integrate Position & Rotation // Air resistance
pos.current.addScaledVector(vel.current, clampedDelta); vel.current.x *= 1 - 0.5 * dt;
rot.current.x += angVel.current.x * clampedDelta; vel.current.z *= 1 - 0.5 * dt;
rot.current.y += angVel.current.y * clampedDelta; angVel.current.multiplyScalar(1 - 1.2 * dt);
rot.current.z += angVel.current.z * clampedDelta;
// Table Collision Check // Integrate Position
pos.current.addScaledVector(vel.current, dt);
// Integrate Rotation via quaternions
const w = angVel.current.clone().multiplyScalar(dt);
const q = new THREE.Quaternion().setFromEuler(new THREE.Euler(w.x, w.y, w.z));
rot.current.multiply(q);
// Collision detection (Table and Floor)
const isOnTable = const isOnTable =
pos.current.x >= tableBounds.minX && pos.current.x >= tableBounds.minX &&
pos.current.x <= tableBounds.maxX && pos.current.x <= tableBounds.maxX &&
@@ -178,25 +152,24 @@ export function InteractiveCube({
if (pos.current.y <= surfaceY) { if (pos.current.y <= surfaceY) {
pos.current.y = surfaceY; pos.current.y = surfaceY;
// Bounce / Restitution
if (Math.abs(vel.current.y) > 0.4) { if (Math.abs(vel.current.y) > 0.4) {
soundFx.playImpact(Math.abs(vel.current.y)); soundFx.playImpact(Math.abs(vel.current.y));
triggerHaptic(session, 'any', Math.min(Math.abs(vel.current.y) * 0.2, 0.8), 30); triggerHaptic(session, 'any', Math.min(Math.abs(vel.current.y) * 0.2, 0.8), 30);
vel.current.y = -vel.current.y * 0.35; // 35% restitution vel.current.y = -vel.current.y * 0.35; // bounce
} else { } else {
vel.current.y = 0; vel.current.y = 0;
} }
// Friction on surface // Friction
vel.current.x *= 0.82; vel.current.x *= 0.82;
vel.current.z *= 0.82; vel.current.z *= 0.82;
angVel.current.multiplyScalar(0.75); angVel.current.multiplyScalar(0.75);
} }
} }
// Apply transform to 3D object // Apply transform
meshRef.current.position.copy(pos.current); meshRef.current.position.copy(pos.current);
meshRef.current.rotation.copy(rot.current); meshRef.current.setRotationFromQuaternion(rot.current);
meshRef.current.scale.setScalar(scale.current); meshRef.current.scale.setScalar(scale.current);
}); });
@@ -214,49 +187,53 @@ export function InteractiveCube({
ref={meshRef} ref={meshRef}
onPointerOver={(e) => { onPointerOver={(e) => {
e.stopPropagation(); e.stopPropagation();
setHovered(true); if (!isGrabbed.current) {
soundFx.playHover(); setHovered(true);
triggerHaptic(session, 'any', 0.2, 20); soundFx.playHover();
}} triggerHaptic(session, 'any', 0.2, 20);
onPointerOut={() => {
setHovered(false);
setHoveredFace(null);
}}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onPointerMove={(e) => {
if (isGrabbed.current && e.point) {
const newPos = e.point.clone().add(grabOffsetPos.current);
const dt = 0.016;
vel.current.copy(newPos).sub(pos.current).divideScalar(dt);
pos.current.copy(newPos);
} }
}} }}
onPointerOut={() => {
if (!isGrabbed.current) {
setHovered(false);
setHoveredFace(null);
}
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
onPointerMissed={onPointerUp}
> >
{/* Base Cube Body */}
<mesh castShadow receiveShadow> <mesh castShadow receiveShadow>
<boxGeometry args={[size, size, size]} /> <boxGeometry args={[size, size, size]} />
<meshStandardMaterial <meshStandardMaterial
color={hovered ? '#fbbf24' : '#334155'} color={hovered || isGrabbed.current ? '#fbbf24' : '#334155'}
metalness={0.4} metalness={0.4}
roughness={0.3} roughness={0.3}
wireframe={locked} wireframe={locked}
/> />
</mesh> </mesh>
{/* Faces with colors, labels and snapping feedback */}
{faces.map((f) => { {faces.map((f) => {
const isSelected = selectedFace === f.id; const isSelected = selectedFace === f.id;
const isFaceHovered = hoveredFace === f.id; const isFaceHovered = hoveredFace === f.id && !isGrabbed.current;
return ( return (
<group key={f.id} position={f.pos} rotation={f.rot}> <group key={f.id} position={f.pos} rotation={f.rot}>
<mesh <mesh
onPointerOver={(e) => { onPointerOver={(e) => {
e.stopPropagation(); e.stopPropagation();
setHoveredFace(f.id); if (!isGrabbed.current) setHoveredFace(f.id);
}}
onClick={(e) => {
e.stopPropagation();
if (locked || isGrabbed.current) return;
setSelectedFace(f.id);
soundFx.playSnap();
triggerHaptic(session, 'any', 0.6, 50);
if (onFaceSelect) onFaceSelect(f.id);
}} }}
onClick={(e) => handleFaceClick(f.id, e)}
> >
<planeGeometry args={[size * 0.94, size * 0.94]} /> <planeGeometry args={[size * 0.94, size * 0.94]} />
<meshStandardMaterial <meshStandardMaterial
@@ -268,7 +245,6 @@ export function InteractiveCube({
opacity={0.9} opacity={0.9}
/> />
</mesh> </mesh>
{/* Face Label Text */}
<Text <Text
position={[0, 0, 0.002]} position={[0, 0, 0.002]}
fontSize={size * 0.28} fontSize={size * 0.28}
@@ -281,14 +257,6 @@ export function InteractiveCube({
</group> </group>
); );
})} })}
{/* Selection Glow Box Indicator */}
{selectedFace && (
<mesh>
<boxGeometry args={[size * 1.05, size * 1.05, size * 1.05]} />
<meshBasicMaterial color="#38bdf8" wireframe transparent opacity={0.6} />
</mesh>
)}
</group> </group>
); );
} }
+30 -17
View File
@@ -1,4 +1,4 @@
import { useRef } from 'react'; import { useRef, type MutableRefObject } from 'react';
import { useFrame, useThree } from '@react-three/fiber'; import { useFrame, useThree } from '@react-three/fiber';
import { useXR } from '@react-three/xr'; import { useXR } from '@react-three/xr';
import * as THREE from 'three'; import * as THREE from 'three';
@@ -21,7 +21,11 @@ export function triggerHaptic(session: any, hand: 'left' | 'right' | 'any' = 'an
} catch {} } catch {}
} }
export function QuestXRLocomotion() { interface LocomotionProps {
worldRef: MutableRefObject<THREE.Group | null>;
}
export function QuestXRLocomotion({ worldRef }: LocomotionProps) {
const { camera } = useThree(); const { camera } = useThree();
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
const snapCooldownRef = useRef(0); const snapCooldownRef = useRef(0);
@@ -31,9 +35,8 @@ export function QuestXRLocomotion() {
const sideVector = useRef(new THREE.Vector3()); const sideVector = useRef(new THREE.Vector3());
useFrame((_, delta) => { useFrame((_, delta) => {
if (!session) return; if (!session || !worldRef.current) return;
// Get input sources from WebXR session
const inputSources = session.inputSources; const inputSources = session.inputSources;
if (!inputSources) return; if (!inputSources) return;
@@ -41,18 +44,22 @@ export function QuestXRLocomotion() {
for (const source of inputSources as any) { for (const source of inputSources as any) {
const gamepad = source.gamepad; const gamepad = source.gamepad;
if (!gamepad || !gamepad.axes || gamepad.axes.length < 2) continue; if (!gamepad || !gamepad.axes) continue;
const handedness = source.handedness; const handedness = source.handedness;
// Axis 0 = Thumbstick X, Axis 1 = Thumbstick Y (Standard WebXR Gamepad mapping) // Quest 3 axes mapping:
const axisX = Math.abs(gamepad.axes[2] ?? gamepad.axes[0]) > STICK_DEADZONE ? (gamepad.axes[2] ?? gamepad.axes[0]) : 0; // axes[0], axes[1] or axes[2], axes[3] depending on the browser version
const axisY = Math.abs(gamepad.axes[3] ?? gamepad.axes[1]) > STICK_DEADZONE ? (gamepad.axes[3] ?? gamepad.axes[1]) : 0; const axisX = Math.abs(gamepad.axes[2]) > STICK_DEADZONE ? gamepad.axes[2]
: Math.abs(gamepad.axes[0]) > STICK_DEADZONE ? gamepad.axes[0] : 0;
const axisY = Math.abs(gamepad.axes[3]) > STICK_DEADZONE ? gamepad.axes[3]
: Math.abs(gamepad.axes[1]) > STICK_DEADZONE ? gamepad.axes[1] : 0;
if (handedness === 'left') { if (handedness === 'left') {
// Left Controller -> Smooth Locomotion // Left Controller -> Smooth Locomotion
if (axisX !== 0 || axisY !== 0) { if (axisX !== 0 || axisY !== 0) {
// Get camera yaw angle // Calculate movement relative to headset orientation
camera.getWorldDirection(forwardVector.current); camera.getWorldDirection(forwardVector.current);
forwardVector.current.y = 0; forwardVector.current.y = 0;
forwardVector.current.normalize(); forwardVector.current.normalize();
@@ -60,20 +67,26 @@ export function QuestXRLocomotion() {
sideVector.current.crossVectors(camera.up, forwardVector.current).normalize(); sideVector.current.crossVectors(camera.up, forwardVector.current).normalize();
moveVector.current.set(0, 0, 0); moveVector.current.set(0, 0, 0);
moveVector.current.addScaledVector(forwardVector.current, -axisY * MOVE_SPEED * delta);
moveVector.current.addScaledVector(sideVector.current, -axisX * MOVE_SPEED * delta);
camera.position.add(moveVector.current); // Move the WORLD in the OPPOSITE direction of the joystick
moveVector.current.addScaledVector(forwardVector.current, axisY * MOVE_SPEED * delta);
moveVector.current.addScaledVector(sideVector.current, axisX * MOVE_SPEED * delta);
worldRef.current.position.add(moveVector.current);
} }
} else if (handedness === 'right') { } else if (handedness === 'right') {
// Right Controller -> Snap Turning // Right Controller -> Snap Turning
if (now > snapCooldownRef.current && Math.abs(axisX) > 0.6) { if (now > snapCooldownRef.current && Math.abs(axisX) > 0.6) {
const turnDirection = axisX > 0 ? -1 : 1; // Right stick right = turn right const turnDirection = axisX > 0 ? -1 : 1;
// Rotate camera around Y axis at current camera position // To turn the player right, we rotate the world LEFT around the player's current position
const rotationMatrix = new THREE.Matrix4().makeRotationY(turnDirection * SNAP_TURN_ANGLE); const playerPos = camera.position.clone();
camera.position.applyMatrix4(rotationMatrix);
camera.rotation.y += turnDirection * SNAP_TURN_ANGLE; worldRef.current.position.sub(playerPos);
worldRef.current.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), turnDirection * SNAP_TURN_ANGLE);
worldRef.current.position.add(playerPos);
worldRef.current.rotation.y += turnDirection * SNAP_TURN_ANGLE;
triggerHaptic(session, 'right', 0.4, 40); triggerHaptic(session, 'right', 0.4, 40);
snapCooldownRef.current = now + SNAP_COOLDOWN_MS; snapCooldownRef.current = now + SNAP_COOLDOWN_MS;
+6 -36
View File
@@ -6,8 +6,6 @@ export interface SpatialHUDProps {
locked: boolean; locked: boolean;
onToggleLock: () => void; onToggleLock: () => void;
onReset: () => void; onReset: () => void;
onEnterVR: () => void;
onEnterAR: () => void;
alignmentStep: string; alignmentStep: string;
isAR: boolean; isAR: boolean;
} }
@@ -17,14 +15,11 @@ export function SpatialHUD({
locked, locked,
onToggleLock, onToggleLock,
onReset, onReset,
onEnterVR,
onEnterAR,
alignmentStep, alignmentStep,
isAR, isAR,
}: SpatialHUDProps) { }: SpatialHUDProps) {
return ( return (
<group position={position}> <group position={position}>
{/* HUD Header Title */}
<Text <Text
position={[0, 0.45, 0]} position={[0, 0.45, 0]}
fontSize={0.14} fontSize={0.14}
@@ -37,7 +32,6 @@ export function SpatialHUD({
LABORATÓRIO XR META QUEST 3 LABORATÓRIO XR META QUEST 3
</Text> </Text>
{/* Alignment Instructions Message */}
{alignmentStep && ( {alignmentStep && (
<group position={[0, 0.25, 0]}> <group position={[0, 0.25, 0]}>
<mesh position={[0, 0, -0.005]}> <mesh position={[0, 0, -0.005]}>
@@ -56,7 +50,6 @@ export function SpatialHUD({
</group> </group>
)} )}
{/* Main Spatial Control Card Panel */}
<mesh position={[0, -0.1, -0.01]}> <mesh position={[0, -0.1, -0.01]}>
<planeGeometry args={[1.8, 0.5]} /> <planeGeometry args={[1.8, 0.5]} />
<meshStandardMaterial <meshStandardMaterial
@@ -68,10 +61,8 @@ export function SpatialHUD({
/> />
</mesh> </mesh>
{/* Action Buttons Row */}
{/* 1. Lock/Unlock Physics Button */}
<group <group
position={[-0.55, -0.1, 0.01]} position={[-0.45, -0.1, 0.01]}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
soundFx.playHover(); soundFx.playHover();
@@ -79,7 +70,7 @@ export function SpatialHUD({
}} }}
> >
<mesh> <mesh>
<boxGeometry args={[0.45, 0.14, 0.02]} /> <boxGeometry args={[0.55, 0.14, 0.02]} />
<meshStandardMaterial color={locked ? '#ef4444' : '#22c55e'} metalness={0.5} roughness={0.3} /> <meshStandardMaterial color={locked ? '#ef4444' : '#22c55e'} metalness={0.5} roughness={0.3} />
</mesh> </mesh>
<Text position={[0, 0, 0.015]} fontSize={0.065} color="white" anchorX="center" anchorY="middle"> <Text position={[0, 0, 0.015]} fontSize={0.065} color="white" anchorX="center" anchorY="middle">
@@ -87,9 +78,8 @@ export function SpatialHUD({
</Text> </Text>
</group> </group>
{/* 2. Reset Position Button */}
<group <group
position={[0, -0.1, 0.01]} position={[0.45, -0.1, 0.01]}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
soundFx.playSnap(); soundFx.playSnap();
@@ -97,34 +87,14 @@ export function SpatialHUD({
}} }}
> >
<mesh> <mesh>
<boxGeometry args={[0.45, 0.14, 0.02]} /> <boxGeometry args={[0.55, 0.14, 0.02]} />
<meshStandardMaterial color="#3b82f6" metalness={0.5} roughness={0.3} /> <meshStandardMaterial color="#3b82f6" metalness={0.5} roughness={0.3} />
</mesh> </mesh>
<Text position={[0, 0, 0.015]} fontSize={0.065} color="white" anchorX="center" anchorY="middle"> <Text position={[0, 0, 0.015]} fontSize={0.065} color="white" anchorX="center" anchorY="middle">
🔄 RESETAR CUBO 🔄 RESETAR MUNDO
</Text> </Text>
</group> </group>
{/* 3. AR / VR Switcher Button */}
<group
position={[0.55, -0.1, 0.01]}
onClick={(e) => {
e.stopPropagation();
soundFx.playHover();
if (isAR) onEnterVR();
else onEnterAR();
}}
>
<mesh>
<boxGeometry args={[0.45, 0.14, 0.02]} />
<meshStandardMaterial color="#8b5cf6" metalness={0.5} roughness={0.3} />
</mesh>
<Text position={[0, 0, 0.015]} fontSize={0.065} color="white" anchorX="center" anchorY="middle">
{isAR ? '🥽 MODO VR (3D)' : '📷 PASSTHROUGH AR'}
</Text>
</group>
{/* Status Footer info */}
<Text <Text
position={[0, -0.28, 0.01]} position={[0, -0.28, 0.01]}
fontSize={0.045} fontSize={0.045}
@@ -132,7 +102,7 @@ export function SpatialHUD({
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
Controles: Gatilho/Grip (Agarrar/Arremessar) Joystick Esq (Mover) Joystick Dir (Girar 45°) {isAR ? 'MODO: PASSTHROUGH AR ATIVO' : 'MODO: VR IMERSIVO'} Joysticks Movem e Giram
</Text> </Text>
</group> </group>
); );
+63 -66
View File
@@ -1,23 +1,21 @@
import { useState, Suspense } from 'react'; import { useState, Suspense, useRef } from 'react';
import { useXR } from '@react-three/xr'; import { useXR } from '@react-three/xr';
import { Environment, Grid } from '@react-three/drei'; import { Environment, Grid } from '@react-three/drei';
import * as THREE from 'three';
import { WorkbenchTable } from './WorkbenchTable'; import { WorkbenchTable } from './WorkbenchTable';
import { InteractiveCube, type FaceId } from './InteractiveCube'; import { InteractiveCube, type FaceId } from './InteractiveCube';
import { SpatialHUD } from './SpatialHUD'; import { SpatialHUD } from './SpatialHUD';
import { QuestXRLocomotion } from './QuestXRLocomotion'; import { QuestXRLocomotion } from './QuestXRLocomotion';
export interface XRSceneProps { export function XRScene() {
onEnterVR: () => void;
onEnterAR: () => void;
}
export function XRScene({ onEnterVR, onEnterAR }: XRSceneProps) {
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
const isAR = !!(session && (session as any).environmentBlendMode === 'additive'); const isAR = !!(session && (session as any).environmentBlendMode === 'additive');
const [locked, setLocked] = useState(false); const [locked, setLocked] = useState(false);
const [resetSignal, setResetSignal] = useState(0); const [resetSignal, setResetSignal] = useState(0);
const worldRef = useRef<THREE.Group>(null);
// Alignment tracking // Alignment tracking
const [selectedCubeFace, setSelectedCubeFace] = useState<FaceId | null>(null); const [selectedCubeFace, setSelectedCubeFace] = useState<FaceId | null>(null);
const [alignmentStep, setAlignmentStep] = useState<string>( const [alignmentStep, setAlignmentStep] = useState<string>(
@@ -41,10 +39,8 @@ export function XRScene({ onEnterVR, onEnterAR }: XRSceneProps) {
return ( return (
<> <>
{/* VR Joystick Locomotion & Snap Turn */} <QuestXRLocomotion worldRef={worldRef} />
<QuestXRLocomotion />
{/* Dynamic Background Fog & Color for VR Mode */}
{!isAR && ( {!isAR && (
<> <>
<color attach="background" args={['#090d16']} /> <color attach="background" args={['#090d16']} />
@@ -52,67 +48,68 @@ export function XRScene({ onEnterVR, onEnterAR }: XRSceneProps) {
</> </>
)} )}
{/* Lighting Setup */} {/* World Group that moves inversely to simulate player movement */}
<ambientLight intensity={isAR ? 1.4 : 0.6} /> <group ref={worldRef}>
<directionalLight <ambientLight intensity={isAR ? 1.4 : 0.6} />
position={[4, 6, 4]} <directionalLight
intensity={isAR ? 1.6 : 1.4} position={[4, 6, 4]}
castShadow intensity={isAR ? 1.6 : 1.4}
shadow-mapSize={[2048, 2048]} castShadow
shadow-bias={-0.0001} shadow-mapSize={[2048, 2048]}
/> shadow-bias={-0.0001}
<pointLight position={[-3, 4, -2]} intensity={0.5} color="#38bdf8" /> />
<pointLight position={[3, 4, -2]} intensity={0.5} color="#f59e0b" /> <pointLight position={[-3, 4, -2]} intensity={0.5} color="#38bdf8" />
<pointLight position={[3, 4, -2]} intensity={0.5} color="#f59e0b" />
{/* Grid Floor */} <Grid
<Grid position={[0, -0.01, 0]}
position={[0, -0.01, 0]} args={[20, 20]}
args={[20, 20]} cellSize={0.25}
cellSize={0.25} cellThickness={0.6}
cellThickness={0.6} cellColor="#334155"
cellColor="#334155" sectionSize={1}
sectionSize={1} sectionThickness={1.2}
sectionThickness={1.2} sectionColor="#0284c7"
sectionColor="#0284c7" fadeDistance={10}
fadeDistance={10} fadeStrength={1.5}
fadeStrength={1.5} infiniteGrid
infiniteGrid />
/>
{/* Environment Reflections */} <Suspense fallback={null}>
<Suspense fallback={null}> <Environment preset="city" />
<Environment preset="city" /> </Suspense>
</Suspense>
{/* Workbench Table */} <WorkbenchTable
<WorkbenchTable position={[0, 0, -1]}
position={[0, 0, -1]} onSurfaceClick={handleSurfaceSelect}
onSurfaceClick={handleSurfaceSelect} onEdgeClick={(edge) => setAlignmentStep(`Aresta selecionada: ${edge}`)}
onEdgeClick={(edge) => setAlignmentStep(`Aresta selecionada: ${edge}`)} />
/>
{/* Interactive Physics Cube */} <InteractiveCube
<InteractiveCube position={[0, 1.2, -1]}
position={[0, 1.2, -1]} size={0.3}
size={0.3} locked={locked}
locked={locked} onFaceSelect={handleFaceSelect}
onFaceSelect={handleFaceSelect} resetSignal={resetSignal}
resetSignal={resetSignal} tableY={0.625}
tableY={0.625} tableBounds={{ minX: -1, maxX: 1, minZ: -1.5, maxZ: -0.5 }}
tableBounds={{ minX: -1, maxX: 1, minZ: -1.5, maxZ: -0.5 }} />
/>
{/* Floating Spatial HUD */} <SpatialHUD
<SpatialHUD position={[0, 1.85, -1.7]}
position={[0, 1.85, -1.7]} locked={locked}
locked={locked} onToggleLock={() => setLocked((l) => !l)}
onToggleLock={() => setLocked((l) => !l)} onReset={() => {
onReset={() => setResetSignal((s) => s + 1)} setResetSignal((s) => s + 1);
onEnterVR={onEnterVR} if (worldRef.current) {
onEnterAR={onEnterAR} worldRef.current.position.set(0, 0, 0);
alignmentStep={alignmentStep} worldRef.current.rotation.set(0, 0, 0);
isAR={isAR} }
/> }}
alignmentStep={alignmentStep}
isAR={isAR}
/>
</group>
</> </>
); );
} }