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.
348 lines
10 KiB
348 lines
10 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';
|
|
|
|
//-- 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 */
|
|
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 = '#ffd500ff';
|
|
// 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: true
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|