Multiplayer WebXR Project with Vite/Node/Three.js
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.
 
 
 

300 lines
10 KiB

import * as THREE from 'three';
import { RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat';
import { ModelLoader } from './ModelLoader';
import { AudioContent, Content, TextContent } from './Content';
export class Item {
constructor(rapierWorld, scene, player, isCollider = false, spawnPosition = new THREE.Vector3(0, 1, 0), scale, name, model, texture = null, spawnedObjects, content = null) {
this.name = name;
this.id = null;
this.model = model;
this.texture = texture;
this.object = null
this.spawnedObjects = spawnedObjects;
this.scene = scene;
this.spawnPosition = spawnPosition.clone();
this.spawnRotation = new THREE.Euler(0, 0, 0, 'YXZ');
this.scale = new THREE.Vector3(1 * scale, 1 * scale, 1 * scale);
this.rotationSpeed = new THREE.Vector3();
this.initialPositon = null;
this.targetPosition = null;
this.centerOffset = new THREE.Vector3();
this.rapierWorld = rapierWorld;
this.rb = null;
this.coll = null;
this.isCollider = isCollider;
this.debugBox = null;
this.isActive = false;
this.modelLoader = new ModelLoader();
this.player = player;
this.content = content;
this.contentDisplay = null;
this.loadState = "unloaded";
this.lastPosition = null;
this.links = [];
this._init();
}
_init() {
}
async loadModel() {
if (this.loadState !== 'unloaded') return;
this.loadState = 'loading';
try {
this.object = await this.modelLoader.loadDRACOModelURL(this.model, this.texture, this.scale);
this.object.traverse((child) => {
if (child.isMesh) {
this.spawnedObjects.push( { mesh: child, body: null} );
}
});
this._addObject();
if(!this.isCollider) {
this.loadState = 'loaded';
return;
}
const boundingBox = new THREE.Box3().setFromObject(this.object);
const size = new THREE.Vector3();
boundingBox.getSize(size);
// Calculate the world center of the bounding box
const worldCenter = new THREE.Vector3();
boundingBox.getCenter(worldCenter);
// Calculate the local center by subtracting the object's world position
this.centerOffset.subVectors(worldCenter, this.object.position);
let rbDesc;
if (this.content === null) {
rbDesc = RigidBodyDesc.kinematicPositionBased();
} else {
rbDesc = RigidBodyDesc.dynamic();
}
rbDesc.setTranslation(this.object.position.x, this.object.position.y, this.object.position.z);
this.rb = this.rapierWorld.createRigidBody(rbDesc);
const colDesc = ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2)
.setTranslation(this.centerOffset.x, this.centerOffset.y, this.centerOffset.z); // Use the calculated local center offset
this.coll = this.rapierWorld.createCollider(colDesc, this.rb);
this.loadState = 'loaded';
} catch (error) {
console.error(`Failed to load model for tiem: ${this.name}`);
this.loadState = 'unloaded';
}
}
unloadModel() {
if (this.loadState !== 'loaded') return;
// Remove physics body
if (this.rb) {
this.rapierWorld.removeRigidBody(this.rb);
this.rb = null;
this.coll = null;
}
// Remove visual object
if (this.object) {
this.scene.remove(this.object);
// Dispose of geometries and materials to free memory
this.object.traverse(child => {
if (child.isMesh) {
child.geometry.dispose();
child.material.dispose();
}
});
this.object = null;
}
// Clean up content display
this._removeContentDisplay();
this.loadState = 'unloaded';
}
_addObject() {
if (this.object != null) {
if (this.lastPosition != null) {
this.object.position.copy(this.lastPosition);
} else {
this.object.position.copy(this.spawnPosition);
}
this.scene.add(this.object);
}
}
_drawBoundingBox() {
if (this.isCollider) {
// 1. Remove the previous frame's box
if (this.debugBox) {
this.scene.remove(this.debugBox);
this.debugBox.geometry.dispose();
this.debugBox.material.dispose();
}
if (this.loadState !== 'loaded') return;
// 2. Calculate the new world-space bounding box
const worldAABB = new THREE.Box3().setFromObject(this.object);
const size = new THREE.Vector3();
worldAABB.getSize(size);
const center = new THREE.Vector3();
worldAABB.getCenter(center);
const scaleFactor = 1.2;
size.multiplyScalar(scaleFactor);
// 3. Create a new 2D box with the updated dimensions
const w = size.x / 2;
const h = size.y / 2;
const points = [
new THREE.Vector3(-w, -h, 0),
new THREE.Vector3(w, -h, 0),
new THREE.Vector3(w, h, 0),
new THREE.Vector3(-w, h, 0)
];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ linewidth: 10, color: 'red' });
this.debugBox = new THREE.LineLoop(geometry, material);
// 4. Position and orient the new box
this.debugBox.position.copy(center);
this.debugBox.quaternion.copy(this.player.camera.quaternion);
this.scene.add(this.debugBox);
}
}
async _moveActiveObject(delta) {
if (this.isActive) {
if (this.rb && !this.rb.isKinematic()) {
this.rb.setBodyType(1); // 1 is kinematic
}
if (!this.initialPositon) {
this.initialPositon = this.object.position.clone();
this.targetPosition = this.initialPositon.clone().add(new THREE.Vector3(0, 10, 0));
this.rotationSpeed.set(
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5) * 0.5
);
await this._createContentDisplay();
}
// Directly update the object's position for smooth animation
this.object.position.lerp(this.targetPosition, 0.01);
this.rb.setNextKinematicTranslation(this.object.position);
this.object.rotation.x += this.rotationSpeed.x * delta;
this.object.rotation.y += this.rotationSpeed.y * delta;
this.object.rotation.z += this.rotationSpeed.z * delta;
this.rb.setNextKinematicRotation(this.object.quaternion);
if (this.content) this.content.update();
} else {
if (this.rb && !this.rb.isDynamic()) {
// Set the rigid body's position to the animated object's final position
// before switching back to dynamic.
this.rb.setTranslation(this.object.position, true);
this.rb.setRotation(this.object.quaternion, true);
this.rb.setBodyType(0); // 0 is dynamic
this.initialPositon = null;
this.targetPosition = null;
this._removeContentDisplay();
}
// When not active, the physics engine controls the object
if (this.rb) {
const pos = this.rb.translation();
const rot = this.rb.rotation();
this.object.position.set(pos.x, pos.y, pos.z);
this.object.quaternion.set(rot.x, rot.y, rot.z, rot.w);
}
}
}
async _createContentDisplay() {
//console.log(this.contentDisplay)
// Prevent creating a new display if the old one is still fading out.
if (this.content instanceof AudioContent && this.content.isFadingOut) {
return;
}
if (this.content && !this.contentDisplay) {
// Check if content needs to receieve a promise (async loading)
const meshOrPromise = this.content.createDisplayMesh();
if (meshOrPromise instanceof Promise) {
this.contentDisplay = await meshOrPromise;
} else {
this.contentDisplay = meshOrPromise;
}
// Only add the resolved mesh, attach to either object or player
if (!(this.content instanceof AudioContent)) {
this.player.camera.add(this.contentDisplay);
this.contentDisplay.position.set(0, 0, -5);
this.contentDisplay.rotation.set(0, 0, 0);
console.log("Added content display to screen");
} else {
this.object.add(this.contentDisplay);
this.contentDisplay.position.set(0, 0, 0);
this.contentDisplay.rotation.set(0, 0, 0);
console.log("Added content display to Object");
}
}
}
_removeContentDisplay() {
if (this.content && this.contentDisplay) {
if (!(this.content instanceof AudioContent)) {
this.player.camera.remove(this.contentDisplay);
this.content.dispose();
this.contentDisplay = null;
console.log("Removed content display");
} else {
// If audio fade out
this.content.fadeOutAndDispose(3000, this.object, this.contentDisplay);
this.contentDisplay = null;
}
}
}
async update(delta) {
this._drawBoundingBox();
if (this.loadState !== 'loaded') return;
this.lastPosition = this.object.position.clone();
await this._moveActiveObject(delta);
this.content.update();
}
}