//--- 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'; //-- Variables ---// let scene, renderer; let player; let rapierWorld, debugLines, itemManager; let debugRapier = false; let isPaused = false; 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(); 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 */ itemManager = new ItemManager("json/Items.json", scene, rapierWorld, player, interactableItems); // const textContent = new TextContent("This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object.This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object. This is an example of dynamic text rendered onto a texture in Three.js. It can be used to display information about an object."); // const imageContent = new ImageContent("images/test.webp"); // const videoContent = new VideoContent("videos/sintel.mp4"); // const audioContent = new AudioContent("sounds/Violet.mp3", player.audioListener); // const item = new Item(rapierWorld, scene, player, false, new THREE.Vector3(0, 1, 0), 20, "test", '/models/init.glb', null, spawnedObjects, null); // await item.loadModel(); // const intItem = new Item(rapierWorld, scene, player, true, new THREE.Vector3(50, 1, 0), 5, "intTest", './models/cone.glb', null, spawnedObjects, audioContent); // await intItem.loadModel(); // interactableItems.push(intItem); 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() { // floor let floorGeometry = new THREE.PlaneGeometry( 2000, 2000, 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 ); const rbDesc = RAPIER.RigidBodyDesc.fixed(); const floorBody = rapierWorld.createRigidBody(rbDesc); let floorCollider = RAPIER.ColliderDesc.cuboid(1000, 1, 1000).setFriction(0.7).setFrictionCombineRule(RAPIER.CoefficientCombineRule.Min);; rapierWorld.createCollider(floorCollider, floorBody); } 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) ); } }