You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

384 lines
11 KiB

//--- Imports ---//
import * as THREE from 'three';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import { VRButton } from 'three/addons/webxr/VRButton.js';
import RAPIER from '@dimforge/rapier3d-compat';
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
import { DRACOLoader } from 'three/examples/jsm/Addons.js';
import { createCustomJitterMaterial } from './materials/CustomJitterMaterial';
import { TextureLoader } from 'three';
import { io } from "socket.io-client";
import { Player } from './Player';
import { Item } from './Item';
import { TextContent, ImageContent, VideoContent, AudioContent } from './Content';
import { ItemManager } from './ItemManager';
import { ModelLoader } from './ModelLoader';
//-- Variables ---//
let scene, renderer;
let player;
let rapierWorld, debugLines, itemManager;
let debugRapier = false;
let isPaused = false;
let floorGridSize = new THREE.Vector2(10, 200);
let socket;
const currentPlayers = {};
let lastPlayerCount = 0;
const spawnedObjects = [];
const interactableItems = [];
const clock = new THREE.Clock();
const vertex = new THREE.Vector3();
const color = new THREE.Color();
const stats = new Stats();
init();
async function init() {
await RAPIER.init();
const gravity = { x: 0, y: -9.81, z: 0 };
rapierWorld = new RAPIER.World(gravity);
scene = new THREE.Scene();
scene.background = new THREE.Color( 'white' );
scene.fog = new THREE.FogExp2( new THREE.Color( 'white' ), 0.02 );
const light = new THREE.HemisphereLight( 0xeeeeff, 0x777788, 2.5 );
light.position.set( 0.5, 1, 0.75 );
scene.add( light );
// --- Debug Renderer Setup ---
const material = new THREE.LineBasicMaterial({
color: 0xffffff,
vertexColors: true,
});
const geometry = new THREE.BufferGeometry();
debugLines = new THREE.LineSegments(geometry, material);
scene.add(debugLines);
// --- End Debug Renderer Setup ---
const blocker = document.getElementById( 'blocker' );
const instructions = document.getElementById('instructions');
fragmentedFloor(floorGridSize.x * floorGridSize.y);
createTiledFloor(floorGridSize.x, floorGridSize.y);
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.xr.enabled = true; // Enable XR
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );
document.body.appendChild( VRButton.createButton( renderer ) );
player = new Player(rapierWorld, new THREE.Vector3(0, 1, 0), interactableItems);
scene.add(player.camera);
setupSocketIO();
/* Load Items/Content */
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.addEventListener( 'click', function () {
document.body.requestPointerLock();
} );
document.body.appendChild( stats.dom );
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === document.body) {
// Pointer is locked: Unpause the game
isPaused = false;
player.enableInput = true;
blocker.style.opacity = '0';
blocker.addEventListener('transitionend', () => {
blocker.style.display = 'none';
}, { once: true });
} else {
// Pointer is unlocked: Pause the game
isPaused = true;
player.enableInput = false;
blocker.style.display = 'flex';
// Use a short timeout to ensure 'display' is set before starting the transition
setTimeout(() => {
blocker.style.opacity = '1';
}, 10);
}
});
window.addEventListener( 'resize', onWindowResize );
window.addEventListener('keydown', function(event) {
if(event.key === 'Escape') {
if (!isPaused) {
renderer.setAnimationLoop(null);
isPaused = true;
} else {
renderer.setAnimationLoop(animate);
isPaused = false;
}
}
});
}
async function animate() {
const delta = clock.getDelta();
drawDebugRapier();
// (1) Update the player positions
player.update(delta);
for (const item of interactableItems) {
await item.update(delta);
}
// Update managers
if (itemManager) itemManager.update();
// (2) Run a step of the physics sim
rapierWorld.step();
// // (3) Update the camera position, after physics step has run.
const newPosition = player.rigibody.translation();
player.position.set(newPosition.x, newPosition.y, newPosition.z);
player.camera.position.copy(player.position);
player.draw(spawnedObjects);
sendPlayerDataToServer();
renderer.render(scene, player.camera); // No post-processing in XR
const numberOfPlayers = Object.keys(currentPlayers).length;
if( lastPlayerCount != numberOfPlayers) {
lastPlayerCount = numberOfPlayers;
console.log(lastPlayerCount);
}
stats.update();
}
//-- Other functions --//
function fragmentedFloor(size) {
// floor
let floorGeometry = new THREE.PlaneGeometry( size, size, 100, 100 );
floorGeometry.rotateX( - Math.PI / 2 );
// vertex displacement
let position = floorGeometry.attributes.position;
for ( let i = 0, l = position.count; i < l; i ++ ) {
vertex.fromBufferAttribute( position, i );
vertex.x += Math.random() * 20 - 10;
vertex.y += Math.random() * 2;
vertex.z += Math.random() * 20 - 10;
position.setXYZ( i, vertex.x, vertex.y, vertex.z );
}
floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices
position = floorGeometry.attributes.position;
const colorsFloor = [];
for ( let i = 0, l = position.count; i < l; i ++ ) {
color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace );
colorsFloor.push( color.r, color.g, color.b );
}
floorGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colorsFloor, 3 ) );
const material = new THREE.MeshBasicMaterial({ color: 0x888888, wireframe: true });
const floor = new THREE.Mesh( floorGeometry, material );
scene.add( floor );
}
function onWindowResize() {
player.camera.aspect = window.innerWidth / window.innerHeight;
player.camera.updateProjectionMatrix();
// Only set size if not in XR
if (!renderer.xr.isPresenting) {
renderer.setSize(window.innerWidth, window.innerHeight);
}
}
function setupSocketIO() {
socket = io();
socket.on('connect', () => {
console.log('Connected to server with ID:', socket.id);
});
socket.on('currentPlayers', (players) => {
Object.keys(players).forEach((id) => {
if (id !== socket.id) {
const playerData = players[id];
addOtherPlayer(playerData);
}
});
});
socket.on('newPlayer', (playerData) => {
addOtherPlayer(playerData);
});
socket.on('playerDisconnected', (id) => {
if (currentPlayers[id]) {
scene.remove(currentPlayers[id].mesh);
delete currentPlayers[id];
}
});
socket.on('playerMoved', (playerData) => {
if (currentPlayers[playerData.id]) {
const playerMesh = currentPlayers[playerData.id].mesh;
playerMesh.position.set(playerData.position.x, playerData.position.y, playerData.position.z);
playerMesh.rotation.set(playerData.rotation.x, playerData.rotation.y, playerData.rotation.z);
}
});
}
function addOtherPlayer(playerData) {
const geometry = new THREE.BoxGeometry(1, 2, 1);
const material = new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff });
const playerMesh = new THREE.Mesh(geometry, material);
playerMesh.position.set(playerData.position.x, playerData.position.y, playerData.position.z);
scene.add(playerMesh);
currentPlayers[playerData.id] = { mesh: playerMesh };
}
function sendPlayerDataToServer() {
// Send player data to the server
if (socket && socket.connected) {
socket.emit('playerMovement', {
position: player.camera.position,
rotation: {
x: player.camera.rotation.x,
y: player.camera.rotation.y,
z: player.camera.rotation.z
}
});
}
}
function drawDebugRapier() {
const buffers = rapierWorld.debugRender();
if (debugRapier) {
debugLines.geometry.setAttribute(
"color",
new THREE.BufferAttribute(buffers.colors, 4)
);
debugLines.geometry.setAttribute(
"position",
new THREE.BufferAttribute(buffers.vertices, 3)
);
}
}
function createTiledFloor(gridSize = 20, tileSize = 50) {
const textureResolution = 1024;
for (let i = -gridSize / 2; i < gridSize / 2; i++) {
for (let j = -gridSize / 2; j < gridSize / 2; j++) {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = textureResolution;
const context = canvas.getContext('2d');
context.fillStyle = '#000000ff';
context.fillRect(0, 0, textureResolution, textureResolution);
const canvasTexture = new THREE.CanvasTexture(canvas);
canvasTexture.flipY = false;
canvasTexture.needsUpdate = true;
const floorGeometry = new THREE.PlaneGeometry(tileSize, tileSize);
floorGeometry.rotateX(-Math.PI / 2);
const floorMaterial = new THREE.MeshStandardMaterial({
map: canvasTexture,
color: 0xffffff,
transparent: false
});
const floorTile = new THREE.Mesh(floorGeometry, floorMaterial);
floorTile.position.set(i * tileSize + tileSize / 2, 1, j * tileSize + tileSize / 2);
scene.add(floorTile);
// Make it drawable
spawnedObjects.push({ mesh: floorTile, body: null });
// --- Rapier Physics Body ---
const rbDesc = RAPIER.RigidBodyDesc.fixed().setTranslation(
floorTile.position.x,
floorTile.position.y,
floorTile.position.z
);
const floorBody = rapierWorld.createRigidBody(rbDesc);
const floorCollider = RAPIER.ColliderDesc.cuboid(tileSize / 2, 0.1, tileSize / 2);
rapierWorld.createCollider(floorCollider, floorBody);
}
}
}
async function loadWorldModel(modelUrl) {
const modelLoader = new ModelLoader();
const worldScale = 20.0;
const staticWorldGroup = new THREE.Group();
scene.add(staticWorldGroup);
try {
const staticObjects = await modelLoader.loadStaticWorld(modelUrl, worldScale);
staticObjects.forEach(object => {
const item = new Item(
rapierWorld,
staticWorldGroup,
player,
true, // isCollider
object.position,
1, // Scale is already applied to the object
object.name || 'static-world-object',
null, // model URL (not needed)
null, // texture URL (not needed)
spawnedObjects,
null, // content
object // preloadedObject
);
item.loadModel();
});
console.log("Finished loading world");
} catch (error) {
console.error("Failed to load world model:", error);
}
}