//--- 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); const worldScale = 20.0; 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, renderer, scene, new THREE.Vector3(0, 1, 0), interactableItems); player._setupVR(renderer); scene.add(player.playerRig); //scene.add(player.camera); setupSocketIO(); /* Load Items/Content */ loadWorldModel("models/demo-world-comp.glb"); 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; } } }); console.log(scene) } async function animate() { const delta = clock.getDelta(); drawDebugRapier(); // (1) Update the player positions player.update(delta, spawnedObjects); 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); // 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); 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 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 static world objects:", error); } try { const dynamicGroups = await modelLoader.loadDynamicGroups(modelUrl, worldScale); const preloadedObjectsMap = new Map(); const dynamicGroupsMap = new Map(); dynamicGroups.forEach(groupData => { const threeGroup = new THREE.Group(); threeGroup.name = groupData.name; scene.add(threeGroup); dynamicGroupsMap.set(groupData.name, threeGroup); groupData.objects.forEach(object => { preloadedObjectsMap.set(object.name, object); }); }); console.log(dynamicGroupsMap); itemManager = new ItemManager( "json/Items.json", preloadedObjectsMap, dynamicGroupsMap, scene, rapierWorld, player, interactableItems); } catch (error) { console.error("Failed to load dynamic world objects:", error); } }