Browse Source

VR supported added, and teleport done

master
Cailean Finn 4 days ago
parent
commit
e2ee2b94b3
  1. 284
      js/Player.js
  2. 20
      js/main.js
  3. 2
      server.js

284
js/Player.js

@ -3,7 +3,7 @@ import { RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat';
import { AudioContent, VideoContent } from './Content'; import { AudioContent, VideoContent } from './Content';
export class Player { export class Player {
constructor(rapierWorld, spawnPosition = new THREE.Vector3(0, 1, 0), itemList) { constructor(rapierWorld, renderer, scene, spawnPosition = new THREE.Vector3(0, 1, 0), itemList) {
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.position = spawnPosition.clone(); this.position = spawnPosition.clone();
this.rotation = new THREE.Euler(0, 0, 0, 'YXZ'); this.rotation = new THREE.Euler(0, 0, 0, 'YXZ');
@ -13,6 +13,9 @@ export class Player {
this.name = null; this.name = null;
this.audioListener = new THREE.AudioListener(); this.audioListener = new THREE.AudioListener();
this.renderer = renderer;
this.scene = scene;
this.rapierWorld = rapierWorld; this.rapierWorld = rapierWorld;
this.rigibody = null; this.rigibody = null;
this.collider = null; this.collider = null;
@ -41,6 +44,19 @@ export class Player {
this.enableInput = false; this.enableInput = false;
/* VR Stuff */
this.vrControllers = [];
this.vrGamepads = [null, null];
this.teleArc = null;
this.teleMarker = null;
this.teleporting = false;
this.teleportTarget = new THREE.Vector3();
this.playerRig = new THREE.Group();
this.teleportDistanceFactor = 1.0; // Default distance multiplier
this.minTeleportDistanceFactor = 0.1; // Minimum distance multiplier
this.maxTeleportDistanceFactor = 1.5; // Maximum distance multiplier
this._init(); this._init();
this._setupInput(); this._setupInput();
this._bindEvents(); this._bindEvents();
@ -113,6 +129,8 @@ export class Player {
// Attach audio listener to the camera/player // Attach audio listener to the camera/player
this.camera.add(this.audioListener); this.camera.add(this.audioListener);
this.playerRig.add(this.camera);
} }
_setupInput() { _setupInput() {
@ -155,6 +173,205 @@ export class Player {
this.isDrawing = false; this.isDrawing = false;
} }
_setupVR() {
const markerGeometry = new THREE.CircleGeometry(0.5, 32);
markerGeometry.rotateX(-Math.PI / 2);
const markerMat = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: false, opacity: 0.5});
this.teleMarker = new THREE.Mesh(markerGeometry, markerMat);
this.teleMarker.visible = false;
this.playerRig.add(this.teleMarker);
// Setup teleport arc
const arcMaterial = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 2 });
const arcGeometry = new THREE.BufferGeometry();
this.teleArc = new THREE.Line(arcGeometry, arcMaterial);
this.teleArc.visible = false;
this.playerRig.add(this.teleArc);
// Controller Setup
for (let i = 0; i < 2; i++) {
const controller = this.renderer.xr.getController(i);
this.playerRig.add(controller);
this.vrControllers.push(controller);
// --- NEW: Add the 'connected' listener to get the Gamepad object ---
controller.addEventListener('connected', (event) => {
// The Gamepad object is in event.data.gamepad
this.vrGamepads[i] = event.data.gamepad;
console.log(`Controller ${i} connected, Gamepad stored.`);
});
// --- OPTIONAL: Handle disconnection ---
controller.addEventListener('disconnected', () => {
this.vrGamepads[i] = null;
console.log(`Controller ${i} disconnected.`);
});
// Add a debug sphere to the controller
const sphereGeometry = new THREE.SphereGeometry(0.05, 8, 8);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: (i === 0 ? 0xff0000 : 0x0000ff) }); // Red for left, Blue for right
const debugSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
controller.add(debugSphere);
const lineGeometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1)]);
const line = new THREE.Line(lineGeometry);
line.scale.z = 5;
controller.add(line);
controller.addEventListener('selectstart', () => this._OnVRSelectStart(i));
controller.addEventListener('selectend', () => this._OnVRSelectEnd(i));
controller.addEventListener('squeezestart', () => this._OnVRSqueezeStart(i));
}
}
_OnVRSelectStart(controllerIndex) {
const controller = this.vrControllers[controllerIndex];
// Right controller (index 1) for drawing/interaction
console.log(`Select Started: ${controllerIndex}`);
if (controllerIndex === 0) {
if (this.currentIntItem && !this.attachedItem) {
this.attachedItem = this.currentIntItem;
this.attachedItem.isActive = true;
} else {
this.isDrawing = true;
}
}
// Left controller (index 0) for teleporting
if (controllerIndex === 1) {
this.teleporting = true;
this.teleArc.visible = true;
}
}
_OnVRSelectEnd(controllerIndex) {
// Right controller
console.log(`Select End: ${controllerIndex}`);
if (controllerIndex === 0) {
this.isDrawing = false;
}
// Left controller
if (controllerIndex === 1) {
this.teleporting = false;
this.teleArc.visible = false;
if (this.teleMarker.visible) {
const newPosition = this.teleportTarget.clone();
newPosition.y = 10; // Maintain height
this.playerRig.position.copy(newPosition);
this.rigibody.setNextKinematicTranslation({ x: newPosition.x, y: newPosition.y, z: newPosition.z });
this.position.copy(newPosition);
}
this.teleMarker.visible = false;
}
}
_OnVRSqueezeStart(controllerIndex) {
console.log(`Squeeze Started: ${controllerIndex}`);
// Use squeeze on right controller to detach item
if (controllerIndex === 0 && this.attachedItem) {
this.attachedItem.isActive = false;
this.attachedItem._removeContentDisplay();
this.attachedItem = null;
}
}
_handleVRJoystick() {
// Get the gamepad for the left controller (index 1 in your setup)
const gamepad = this.vrGamepads[1];
// You no longer need to check this.vrControllers[1] since the gamepad is null if disconnected.
if (!gamepad) return;
// The axes array for a thumbstick is often at index 2 (X) and 3 (Y) for the primary stick.
// If the left stick is the primary for movement/teleport, these are the typical indices.
// Always check for undefined or use a safe index, just in case.
const joystickVertical = gamepad.axes[3];
if (joystickVertical !== undefined) {
// joystickVertical is -1 (forward) to 1 (backward). We want forward to be max distance.
// We'll map the [-1, 1] range to our [min, max] distance factor range.
// 1. Convert [-1, 1] to [0, 1] (Mapping: -1 -> 1, 0 -> 0.5, 1 -> 0)
// Since Y is typically -1 forward, using (-Y + 1) / 2 makes full forward (Y=-1) equal to 1.
const normalizedValue = (-joystickVertical + 1) / 2;
// 2. Linearly interpolate between min and max factors
this.teleportDistanceFactor = this.minTeleportDistanceFactor + normalizedValue * (this.maxTeleportDistanceFactor - this.minTeleportDistanceFactor);
// Optional: Apply a small deadzone to prevent accidental changes when the stick is centered
const deadzone = 0.05;
if (Math.abs(joystickVertical) < deadzone) {
// If centered, reset to the default factor (e.g., the midpoint of your min/max range)
this.teleportDistanceFactor = (this.minTeleportDistanceFactor + this.maxTeleportDistanceFactor) / 2;
}
// console.log(`Normalized: ${normalizedValue.toFixed(2)}, Factor: ${this.teleportDistanceFactor.toFixed(2)}`);
}
}
_handleVRTeleport(floorObjects) {
if (!this.teleporting) {
this.teleMarker.visible = false;
this.teleArc.visible = false;
return;
}
const controller = this.vrControllers[1]; // Left controller for teleporting
const controllerMatrix = controller.matrixWorld;
const initialVelocity = 50 * this.teleportDistanceFactor;;
const gravity = -9.8;
const timeStep = 0.03;
const numSegments = 100;
const points = [];
const startPoint = new THREE.Vector3().setFromMatrixPosition(controllerMatrix);
points.push(startPoint.clone());
const launchDirection = new THREE.Vector3(0, 0, -1).applyMatrix4(new THREE.Matrix4().extractRotation(controllerMatrix));
let lastPoint = startPoint.clone();
let hit = false;
for (let i = 1; i < numSegments; i++) {
const t = i * timeStep;
const currentPoint = new THREE.Vector3(
startPoint.x + launchDirection.x * initialVelocity * t,
startPoint.y + launchDirection.y * initialVelocity * t + 0.5 * gravity * t * t,
startPoint.z + launchDirection.z * initialVelocity * t
);
const ray = new THREE.Raycaster(lastPoint, currentPoint.clone().sub(lastPoint).normalize());
ray.far = lastPoint.distanceTo(currentPoint);
const floorMeshes = floorObjects.map(obj => obj.mesh);
const intersects = ray.intersectObjects(floorMeshes);
if (intersects.length > 0) {
const intersectPoint = intersects[0].point;
points.push(intersectPoint);
this.teleportTarget.copy(intersectPoint);
this.teleMarker.position.copy(this.playerRig.worldToLocal(this.teleportTarget.clone()));
this.teleMarker.position.y += 0.01; // Avoid z-fighting
this.teleMarker.visible = true;
hit = true;
break;
}
points.push(currentPoint);
lastPoint = currentPoint;
}
if (!hit) {
this.teleMarker.visible = false;
}
// Convert world-space points to local-space for the rig
const localPoints = points.map(p => this.playerRig.worldToLocal(p.clone()));
this.teleArc.geometry.setFromPoints(localPoints);
this.teleArc.geometry.computeBoundingSphere(); // Important for visibility
this.teleArc.visible = true;
}
_drawOnTexture(intersect, color = 'red') { _drawOnTexture(intersect, color = 'red') {
const object = intersect.object; const object = intersect.object;
const uv = intersect.uv; const uv = intersect.uv;
@ -199,20 +416,36 @@ export class Player {
} }
draw(drawableObjects) { draw(drawableObjects) {
if (!this.isDrawing) return;
this.pointer.x = 0;
this.pointer.y = 0;
this.raycast.setFromCamera(this.pointer, this.camera);
const meshesToIntersect = drawableObjects.map(obj => obj.mesh); const meshesToIntersect = drawableObjects.map(obj => obj.mesh);
const intersections = this.raycast.intersectObjects(meshesToIntersect);
if( intersections.length > 0) { // Desktop drawing
const intersect = intersections[0]; if (this.isDrawing && !this.vrControllers[0]?.userData.isDrawing) {
if( intersect.object.material.map && intersect.object.material.map.isCanvasTexture ) { this.pointer.x = 0;
this._drawOnTexture(intersect); this.pointer.y = 0;
this.raycast.setFromCamera(this.pointer, this.camera);
const intersections = this.raycast.intersectObjects(meshesToIntersect);
if (intersections.length > 0) {
const intersect = intersections[0];
if (intersect.object.material.map && intersect.object.material.map.isCanvasTexture) {
this._drawOnTexture(intersect);
}
}
}
// VR drawing (right controller)
const vrController = this.vrControllers[0];
if (this.isDrawing && vrController) {
const controllerMatrix = vrController.matrixWorld;
const ray = new THREE.Raycaster();
ray.ray.origin.setFromMatrixPosition(controllerMatrix);
ray.ray.direction.set(0, 0, -1).applyMatrix4(new THREE.Matrix4().extractRotation(controllerMatrix));
const intersections = ray.intersectObjects(meshesToIntersect);
if (intersections.length > 0) {
const intersect = intersections[0];
if (intersect.object.material.map && intersect.object.material.map.isCanvasTexture) {
this._drawOnTexture(intersect);
}
} }
} }
} }
@ -220,7 +453,18 @@ export class Player {
_checkForInteractableItems() { _checkForInteractableItems() {
const ray = new THREE.Raycaster(); const ray = new THREE.Raycaster();
ray.set(this.camera.position, this.camera.getWorldDirection(new THREE.Vector3())); let isVR = this.vrControllers[0] && this.vrControllers[0].visible;
if (isVR) {
// Use right controller for interaction ray
const controller = this.vrControllers[0];
const controllerMatrix = controller.matrixWorld;
ray.ray.origin.setFromMatrixPosition(controllerMatrix);
ray.ray.direction.set(0, 0, -1).applyMatrix4(new THREE.Matrix4().extractRotation(controllerMatrix));
} else {
// Use camera for desktop interaction ray
ray.set(this.camera.position, this.camera.getWorldDirection(new THREE.Vector3()));
}
const nearbyItems = this.itemList.filter(item => item.object && this.position.distanceTo(item.object.position) < this.maxInteractionDistance); const nearbyItems = this.itemList.filter(item => item.object && this.position.distanceTo(item.object.position) < this.maxInteractionDistance);
@ -295,11 +539,17 @@ export class Player {
this.rigibody.setNextKinematicTranslation({ x: newPosition.x, y: newPosition.y, z: newPosition.z }); this.rigibody.setNextKinematicTranslation({ x: newPosition.x, y: newPosition.y, z: newPosition.z });
} }
update(delta) { update(delta, spawnedObjects) {
//console.log(`Number of Controllers: ${this.vrControllers.length}`);
if (this.renderer.xr.isPresenting) {
this._handleVRJoystick();
this._handleVRTeleport(spawnedObjects);
}
if (this.enableInput) { if (this.enableInput) {
if (this.attachedItem) { if (this.attachedItem) {
this._lockCameraForAttachedItem(); this._lockCameraForAttachedItem();
} else { } else if (!this.renderer.xr.isPresenting) { // Only update movement if not in VR
this._updatePlayerMovement(delta); this._updatePlayerMovement(delta);
} }
this._checkForInteractableItems(); this._checkForInteractableItems();

20
js/main.js

@ -81,21 +81,17 @@ async function init() {
document.body.appendChild( renderer.domElement ); document.body.appendChild( renderer.domElement );
document.body.appendChild( VRButton.createButton( renderer ) ); document.body.appendChild( VRButton.createButton( renderer ) );
player = new Player(rapierWorld, new THREE.Vector3(0, 1, 0), interactableItems); player = new Player(rapierWorld, renderer, scene, new THREE.Vector3(0, 1, 0), interactableItems);
player._setupVR(renderer);
scene.add(player.playerRig);
scene.add(player.camera); //scene.add(player.camera);
setupSocketIO(); setupSocketIO();
/* Load Items/Content */ /* Load Items/Content */
loadWorldModel("models/demo-world-comp.glb"); loadWorldModel("models/demo-world-comp.glb");
//itemManager = new ItemManager("json/Items.json", scene, rapierWorld, player, interactableItems);
const item = new Item(rapierWorld, scene, player, true, new THREE.Vector3(0, 20, 0), 20, "test", '/models/init.glb', null, spawnedObjects, null);
await item.loadModel();
instructions.innerHTML = "Click to play"; instructions.innerHTML = "Click to play";
instructions.addEventListener( 'click', function () { instructions.addEventListener( 'click', function () {
document.body.requestPointerLock(); document.body.requestPointerLock();
@ -146,7 +142,7 @@ async function animate() {
drawDebugRapier(); drawDebugRapier();
// (1) Update the player positions // (1) Update the player positions
player.update(delta); player.update(delta, spawnedObjects);
for (const item of interactableItems) { for (const item of interactableItems) {
await item.update(delta); await item.update(delta);
@ -162,7 +158,11 @@ async function animate() {
// // (3) Update the camera position, after physics step has run. // // (3) Update the camera position, after physics step has run.
const newPosition = player.rigibody.translation(); const newPosition = player.rigibody.translation();
player.position.set(newPosition.x, newPosition.y, newPosition.z); player.position.set(newPosition.x, newPosition.y, newPosition.z);
player.camera.position.copy(player.position);
// Only copy the rigidbody position to the camera when not in VR
if (!renderer.xr.isPresenting) {
player.playerRig.position.copy(player.position);
}
player.draw(spawnedObjects); player.draw(spawnedObjects);

2
server.js

@ -18,7 +18,7 @@ io.on('connection', (socket) => {
players[socket.id] = { players[socket.id] = {
rotation: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 },
position: { x: 0, y: 1, z: 0 }, position: { x: 0, y: 1, z: 0 },
id: socket.id socketId: socket.id
}; };
// Send the new player the list of all other players // Send the new player the list of all other players

Loading…
Cancel
Save