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.
 
 
 

480 lines
17 KiB

import * as THREE from 'three';
import { RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat';
import { ModelLoader } from './ModelLoader';
import { AudioContent, Content, TextContent } from './Content';
import { Noise } from 'noisejs';
export class Item {
constructor(rapierWorld, scene, player, isCollider = false, spawnPosition = new THREE.Vector3(0, 1, 0), scale, name, model, texture = null, spawnedObjects, content = null, object = null) {
this.name = name;
this.id = null;
this.groupId = null;
this.model = model;
this.texture = texture;
this.object = object
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();
this.isVisible = true;
this.outlineMesh = null;
this.pulseTime = 0;
this.noise = new Noise(Math.random());
this.noiseTime = Math.random() * 1000;
}
_init() {
}
async loadModel() {
if (this.loadState !== 'unloaded') return;
this.loadState = 'loading';
try {
if (!this.object) {
this.object = await this.modelLoader.loadDRACOModelURL(this.model, this.texture, this.scale);
}
this.object.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
this.spawnedObjects.push( { mesh: child, body: null} );
}
});
this.spawnRotation.copy(this.object.rotation);
this._addObject();
if(!this.isCollider || this.object.name.startsWith('TRIGGER') || this.object.name.startsWith('stContainer') || this.object.name.startsWith('stGrass')) {
this.loadState = 'loaded';
return;
}
if (this.object.name.startsWith('stCollider')) {
this.object.visible = false;
this.object.layers.disableAll();
//console.log(this.object);
}
// --- Simplified Convex Hull Generation ---
const vertices = [];
// Decimation rate: 1 = use all vertices, 10 = use 1 in every 10. Higher is faster.
const decimation = 1;
let vertexCount = 0;
this.object.updateWorldMatrix(true, true);
// gets vertices
this.object.traverse((child) => {
if (child.isMesh) {
const geometry = child.geometry;
if (geometry.attributes.position) {
const positionAttribute = geometry.attributes.position;
for (let i = 0; i < positionAttribute.count; i++) {
// Only process every Nth vertex to simplify the mesh
if (vertexCount % decimation === 0) {
const vertex = new THREE.Vector3();
vertex.fromBufferAttribute(positionAttribute, i);
// Transform vertex to world space
vertex.applyMatrix4(child.matrixWorld);
// Then to parent's local space (unscaled)
vertex.sub(this.object.position);
vertex.applyQuaternion(this.object.quaternion.clone().invert());
vertices.push(vertex.x, vertex.y, vertex.z);
}
vertexCount++;
}
}
}
});
const verticesFloat32Array = new Float32Array(vertices);
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);
let colDesc;
if (verticesFloat32Array.length > 0) {
colDesc = ColliderDesc.convexHull(verticesFloat32Array);
} else {
// Fallback to a cuboid if simplification results in no vertices
console.warn(`Could not generate convex hull for ${this.name}, falling back to cuboid.`);
const boundingBox = new THREE.Box3().setFromObject(this.object);
const size = new THREE.Vector3();
boundingBox.getSize(size);
const center = new THREE.Vector3();
boundingBox.getCenter(center).sub(this.object.position);
colDesc = ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2)
.setTranslation(center.x, center.y, center.z);
}
this.coll = this.rapierWorld.createCollider(colDesc, this.rb);
this.loadState = 'loaded';
} catch (error) {
console.error(`Failed to load model for item: ${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.object.rotation.copy(this.spawnRotation);
this.scene.add(this.object);
}
}
_drawBoundingBox() {
if (this.isCollider && this.isVisible) {
// 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: 2, 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.playerRig.quaternion);
this.scene.add(this.debugBox);
}
}
show() {
if (this.object) {
this.object.visible = true;
}
if (this.rb) {
this.rb.setEnabled(true);
}
this.isVisible = true;
}
hide() {
if (this.object) {
this.object.visible = false;
}
if (this.rb) {
this.rb.setEnabled(false);
}
this.isVisible = false;
}
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();
this.rotationSpeed.set(
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5) * 0.5,
(Math.random() - 0.5) * 0.5
);
await this._createContentDisplay();
}
// --- Perlin noise movement ---
this.noiseTime += delta * 0.05; // Adjust speed as needed
const noiseX = this.noise.perlin2(this.noiseTime, 0);
const noiseY = this.noise.perlin2(0, this.noiseTime);
// Scale the movement
const moveScale = 50.0; // Adjust for desired movement range
this.targetPosition.x = this.initialPositon.x + noiseX * moveScale;
this.targetPosition.y = this.initialPositon.y + (noiseY * moveScale) + 20;
// 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;
}
}
}
showOutline() {
if (this.hoverIndicator || !this.object) return;
function getRandomColor() {
const hexChars = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += hexChars[Math.floor(Math.random() * 16)];
}
return color;
}
// Create a circular texture using a canvas
const canvas = document.createElement('canvas');
const size = 128;
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d');
const centerX = size / 2;
const centerY = size / 2;
const radius = size / 2;
// Create a radial gradient
// The gradient goes from the center to the outer edge of the canvas
const gradient = context.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
// Add color stops to control opacity
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); // Center: transparent
gradient.addColorStop(0.75, getRandomColor()); // 75% of the way out: fully opaque
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); // Edge: transparent
// Apply the gradient and draw the circle
context.fillStyle = gradient;
context.fillRect(0, 0, size, size);
const texture = new THREE.CanvasTexture(canvas);
// Create a sprite material with the new texture
const material = new THREE.SpriteMaterial({
map: texture,
blending: THREE.AdditiveBlending,
transparent: true
});
this.hoverIndicator = new THREE.Sprite(material);
// Calculate the size and position for the indicator
const boundingBox = new THREE.Box3().setFromObject(this.object);
const boundingSphere = new THREE.Sphere();
boundingBox.getBoundingSphere(boundingSphere);
// Set the desired world scale for the indicator based on the object's size
const desiredScale = boundingSphere.radius * 2.5;
this.hoverIndicator.scale.set(desiredScale, desiredScale, desiredScale);
// Counteract the parent's scale by dividing the indicator's scale by the parent's scale.
if (this.object.scale.x !== 0 && this.object.scale.y !== 0 && this.object.scale.z !== 0) {
this.hoverIndicator.scale.divide(this.object.scale);
}
// Position the sprite at the center of the object, in local space.
this.hoverIndicator.position.copy(boundingSphere.center).sub(this.object.position);
this.object.add(this.hoverIndicator);
}
hideOutline() {
if (!this.hoverIndicator) return;
this.object.remove(this.hoverIndicator);
this.hoverIndicator.material.map.dispose();
this.hoverIndicator.material.dispose();
this.hoverIndicator = null;
}
async update(delta) {
//this._drawBoundingBox();
if (this.loadState !== 'loaded') return;
if (this.hoverIndicator) {
this.pulseTime += delta * 5; // Adjust the multiplier to change pulse speed
const minOpacity = 0.1;
const maxOpacity = 0.8;
// Create a sine wave that oscillates between 0 and 1
const oscillation = (Math.sin(this.pulseTime) + 1) / 4;
// Apply the oscillation to the desired opacity range
this.hoverIndicator.material.opacity = minOpacity + oscillation * (maxOpacity - minOpacity);
}
this.lastPosition = this.object.position.clone();
await this._moveActiveObject(delta);
if (this.content) {
this.content.update();
}
}
}