Browse Source

updated json, code for loading and sorting dynamic objects, group and inter group linking

master
Cailean Finn 5 days ago
parent
commit
632aeb95e6
  1. 25
      js/Item.js
  2. 265
      js/ItemManager.js
  3. 46
      js/ModelLoader.js
  4. 39
      js/main.js
  5. 40
      public/json/Items.json

25
js/Item.js

@ -43,6 +43,8 @@ export class Item {
this.links = []; this.links = [];
this._init(); this._init();
this.isVisible = true;
} }
_init() { _init() {
@ -148,8 +150,9 @@ export class Item {
} }
_drawBoundingBox() { _drawBoundingBox() {
if (this.isCollider) { if (this.isCollider && this.isVisible) {
// 1. Remove the previous frame's box // 1. Remove the previous frame's box
if (this.debugBox) { if (this.debugBox) {
this.scene.remove(this.debugBox); this.scene.remove(this.debugBox);
this.debugBox.geometry.dispose(); this.debugBox.geometry.dispose();
@ -188,6 +191,26 @@ export class Item {
} }
} }
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) { async _moveActiveObject(delta) {
if (this.isActive) { if (this.isActive) {
if (this.rb && !this.rb.isKinematic()) { if (this.rb && !this.rb.isKinematic()) {

265
js/ItemManager.js

@ -3,9 +3,13 @@ import { Player } from './Player';
import { Item } from './Item'; import { Item } from './Item';
import { TextContent, ImageContent, VideoContent, AudioContent } from './Content'; import { TextContent, ImageContent, VideoContent, AudioContent } from './Content';
import { GeometryUtils } from 'three/examples/jsm/Addons.js'; import { GeometryUtils } from 'three/examples/jsm/Addons.js';
import { Line2 } from 'three/examples/jsm/lines/Line2.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
export class ItemManager { export class ItemManager {
constructor(filePath, scene, rapierWorld, player, interactableItems) { constructor(filePath, preloadedObjects, dynamicGroups, scene, rapierWorld, player, interactableItems) {
this.filePath = filePath; this.filePath = filePath;
this.scene = scene; this.scene = scene;
this.rapierWorld = rapierWorld; this.rapierWorld = rapierWorld;
@ -17,6 +21,11 @@ export class ItemManager {
this.unloadDistance = 300; this.unloadDistance = 300;
this.linkLines = new Map(); this.linkLines = new Map();
this.drawnLinks = new Set(); this.drawnLinks = new Set();
this.preloadedObjects = preloadedObjects;
this.dynamicGroups = dynamicGroups;
this.groupCentres = new Map();
this.groupCenterLinks = new Map();
this.interGroupLinks = new Map();
this._init(); this._init();
} }
@ -33,7 +42,9 @@ export class ItemManager {
const data = await resp.json(); const data = await resp.json();
await this._processItemData(data.items); await this._processItemData(data.items);
this._createLinkLines(); this._createGroupCenterLinks();
if (data.groupLinks) this._createInterGroupLinks(data.groupLinks);
} catch (error) { } catch (error) {
console.error("Could not load or process content JSON:", error); console.error("Could not load or process content JSON:", error);
@ -44,6 +55,20 @@ export class ItemManager {
for (const itemDef of itemArray) { for (const itemDef of itemArray) {
let contentObject = null; let contentObject = null;
const preloadedObject = this.preloadedObjects.get(itemDef.name);
if (!preloadedObject) {
console.warn(`Could not find a preloaded 3D object for item name: "${itemDef.name}"`);
continue;
}
// Find the correct THREE.Group for this item
const parentGroup = this.dynamicGroups.get(itemDef.groupId);
if (!parentGroup) {
console.warn(`Could not find a group for groupId: "${itemDef.groupId}"`);
continue;
}
// Create content object based on contentType and file // Create content object based on contentType and file
if (itemDef.contentType && itemDef.file) { if (itemDef.contentType && itemDef.file) {
switch (itemDef.contentType) { switch (itemDef.contentType) {
@ -77,97 +102,140 @@ export class ItemManager {
} }
} }
// Create the Item instance
const position = new THREE.Vector3(itemDef.position.x, itemDef.position.y, itemDef.position.z);
// The Item constructor expects a single number for scale, which matches your JSON.
const scale = itemDef.scale;
const newItem = new Item( const newItem = new Item(
this.rapierWorld, this.rapierWorld,
this.scene, parentGroup,
this.player, this.player,
itemDef.isCollider, true,
position, preloadedObject.position,
scale, 1,
itemDef.name, itemDef.name,
itemDef.model, null,
itemDef.texture, null,
[], // spawnedObjects placeholder [], // spawnedObjects placeholder
contentObject contentObject,
preloadedObject
); );
// Set item id and group id // Set item id and group id
newItem.id = itemDef.id; newItem.id = itemDef.id;
newItem.groupId = itemDef.groupId; newItem.groupId = parentGroup.name;
// Set links // Set links
if (itemDef.links) { if (itemDef.links) {
newItem.links = itemDef.links; newItem.links = itemDef.links;
} }
if (itemDef.isCollider) { // Item is already loaded from world glb, but this func creates collider etc.
this.interactableItems.push(newItem); await newItem.loadModel();
} newItem.show();
this.interactableItems.push(newItem);
// Setting a key and value pair in the Map (id, item) // Setting a key and value pair in the Map (id, item)
this.itemData.set(itemDef.id, newItem); this.itemData.set(itemDef.id, newItem);
} }
} }
_createLinkLines() { _createGroupCenterLinks() {
this.itemData.forEach(item => { this.itemData.forEach(item => {
if (!item.links) return; const groupCenter = this._getGroupCentre(item.groupId);
if (!groupCenter) return;
item.links.forEach(linkedItemId => { const material = new THREE.LineBasicMaterial({
// Goes through each connection; links = [0, 1, 2..] color: 0xFF0000,
const linkedItem = this.itemData.get(linkedItemId); transparent: false,
if (!linkedItem) return; opacity: 1
});
if (item.groupId !== linkedItem.groupId) return;
// Creates a key for the Set; a Set can only have unique values, so used for no overlap const points = [groupCenter.clone(), item.spawnPosition.clone()];
const key = [item.id, linkedItemId].sort().join('-'); const geometry = new THREE.BufferGeometry().setFromPoints(points);
if (this.drawnLinks.has(key)) return; const line = new THREE.Line(geometry, material);
line.visible = false; // Initially hidden
const material = new THREE.LineDashedMaterial({ this.scene.add(line);
color: 0x0000ff,
dashSize: 3,
gapSize: 1,
});
const sourcePos = item.spawnPosition.clone(); // Store the line and the group it belongs to, keyed by the item's ID
const terminalPos = linkedItem.spawnPosition.clone(); this.groupCenterLinks.set(item.id, { line, groupId: item.groupId });
sourcePos.y = 1; terminalPos.y = 1; });
}
const points = [item.spawnPosition.clone(), linkedItem.spawnPosition.clone()]; _createInterGroupLinks(groupLinks) {
const geometry = new THREE.BufferGeometry().setFromPoints(points); groupLinks.forEach(linkPair => {
const [groupA_ID, groupB_ID] = linkPair;
const line = new THREE.Line(geometry, material); const centerA = this._getGroupCentre(groupA_ID);
line.computeLineDistances(); const centerB = this._getGroupCentre(groupB_ID);
this.scene.add(line); if (!centerA || !centerB) {
console.warn(`Could not find centers for group link: ${groupA_ID} to ${groupB_ID}`);
return;
}
// The Set is used as the key "0-1", then the item and the linkedItem are set as values in the Map? const lineGeometry = new LineGeometry();
this.linkLines.set(key, { line, item1: item, item2: linkedItem }); lineGeometry.setPositions([
// Make sure it is not drawn again, adding the unique pair here centerA.x, centerA.y, centerA.z,
this.drawnLinks.add(key); centerB.x, centerB.y, centerB.z
]);
const lineMaterial = new LineMaterial({
color: 0xffa500, // Orange color for distinction
linewidth: 2, // in pixels
resolution: new THREE.Vector2(window.innerWidth, window.innerHeight),
fog:true
}); });
const line = new Line2(lineGeometry, lineMaterial);
this.scene.add(line);
const linkKey = `${groupA_ID}-${groupB_ID}`;
this.interGroupLinks.set(linkKey, { line, groupA_ID, groupB_ID });
});
}
_getGroupCentre(groupId) {
const group = this.dynamicGroups.get(groupId);
if (!group) {
console.warn(`Group with id ${groupId} not found`);
return null;
}
const bbox = new THREE.Box3().setFromObject(group);
const centre = new THREE.Vector3();
bbox.getCenter(centre);
return centre;
}
_updateGroupCentres() {
this.dynamicGroups.forEach((group, groupId) => {
const center = this._getGroupCentre(groupId);
if (center) {
let helper = this.groupCentres.get(groupId);
if (!helper) {
helper = new THREE.AxesHelper(3); // The number defines the size of the helper
this.scene.add(helper);
this.groupCentres.set(groupId, helper);
}
helper.position.copy(center);
}
}); });
} }
_updateLinkLines() { _updateGroupCenterLinks() {
this.linkLines.forEach(link => { this.groupCenterLinks.forEach((linkData, itemId) => {
const { line, item1, item2 } = link; const { line, groupId } = linkData;
const item = this.itemData.get(itemId);
if (item && item.isVisible) {
const groupCenter = this._getGroupCentre(groupId);
if (!groupCenter) return;
// Only update/show the line if both items are loaded
if (item1.loadState === 'loaded' && item2.loadState === 'loaded') {
line.visible = true; line.visible = true;
const positions = line.geometry.attributes.position; const positions = line.geometry.attributes.position;
positions.setXYZ(0, item1.object.position.x, 1, item1.object.position.z); positions.setXYZ(0, groupCenter.x, groupCenter.y, groupCenter.z);
positions.setXYZ(1, item2.object.position.x, 1, item2.object.position.z); positions.setXYZ(1, item.object.position.x, item.object.position.y, item.object.position.z);
positions.needsUpdate = true; positions.needsUpdate = true;
} else { } else {
line.visible = false; line.visible = false;
@ -175,6 +243,20 @@ export class ItemManager {
}); });
} }
_updateInterGroupLinks() {
this.interGroupLinks.forEach(link => {
const centerA = this._getGroupCentre(link.groupA_ID);
const centerB = this._getGroupCentre(link.groupB_ID);
if (centerA && centerB) {
link.line.geometry.setPositions([
centerA.x, centerA.y, centerA.z,
centerB.x, centerB.y, centerB.z
]);
}
});
}
update() { update() {
const playerPosition = this.player.camera.position; const playerPosition = this.player.camera.position;
this.itemData.forEach(item => { this.itemData.forEach(item => {
@ -188,15 +270,78 @@ export class ItemManager {
} }
// Check if item should be loaded // Check if item should be loaded
if (distance < this.loadDistance && item.loadState === 'unloaded') { if (distance < this.loadDistance && !item.isVisible) {
item.loadModel(); item.show();
} }
// Check if item should be unloaded // Check if item should be unloaded
else if (distance > this.unloadDistance && item.loadState === 'loaded') { else if (distance > this.unloadDistance && item.isVisible) {
item.unloadModel(); item.hide();
} }
}); });
this._updateLinkLines(); this._updateGroupCenterLinks();
this._updateGroupCentres();
this._updateInterGroupLinks();
} }
// _createLinkLines() {
// this.itemData.forEach(item => {
// if (!item.links) return;
// item.links.forEach(linkedItemId => {
// // Goes through each connection; links = [0, 1, 2..]
// const linkedItem = this.itemData.get(linkedItemId);
// if (!linkedItem) return;
// if (item.groupId !== linkedItem.groupId) return;
// // Creates a key for the Set; a Set can only have unique values, so used for no overlap
// const key = [item.id, linkedItemId].sort().join('-');
// if (this.drawnLinks.has(key)) return;
// const material = new THREE.LineDashedMaterial({
// color: 0xffa500,
// dashSize: 10,
// gapSize: 1
// });
// const sourcePos = item.spawnPosition.clone();
// const terminalPos = linkedItem.spawnPosition.clone();
// sourcePos.y = 1; terminalPos.y = 1;
// const points = [item.spawnPosition.clone(), linkedItem.spawnPosition.clone()];
// const geometry = new THREE.BufferGeometry().setFromPoints(points);
// const line = new THREE.Line(geometry, material);
// line.computeLineDistances();
// this.scene.add(line);
// // The Set is used as the key "0-1", then the item and the linkedItem are set as values in the Map?
// this.linkLines.set(key, { line, item1: item, item2: linkedItem });
// // Make sure it is not drawn again, adding the unique pair here
// this.drawnLinks.add(key);
// });
// });
// }
// _updateLinkLines() {
// this.linkLines.forEach(link => {
// const { line, item1, item2 } = link;
// // Only update/show the line if both items are loaded
// if ((item1.loadState === 'loaded' && item2.loadState === 'loaded')
// && (item1.isVisible && item2.isVisible)) {
// line.visible = true;
// const positions = line.geometry.attributes.position;
// positions.setXYZ(0, item1.object.position.x, 1, item1.object.position.z);
// positions.setXYZ(1, item2.object.position.x, 1, item2.object.position.z);
// positions.needsUpdate = true;
// } else {
// line.visible = false;
// }
// });
// }
} }

46
js/ModelLoader.js

@ -77,6 +77,52 @@ export class ModelLoader {
}); });
} }
loadDynamicGroups(modelURL, worldScale) {
return new Promise((resolve, reject) => {
this.gltfLoader.load(modelURL, (gltf) => {
const dynamicGroup = gltf.scene.getObjectByName('Dynamic');
const groups = [];
if (dynamicGroup) {
// Iterate over children of 'dynamic', which are the subgroups (e.g., 'reddit')
const subgroups = [...dynamicGroup.children];
subgroups.forEach(subgroup => {
if (subgroup.isGroup || subgroup.isObject3D) { // Treat empties as groups
const groupData = {
name: subgroup.name,
objects: []
};
// Iterate over the actual mesh objects within the subgroup
const objects = [...subgroup.children];
objects.forEach(object => {
if (object.isMesh) {
// 1. Apply world scale
object.position.multiplyScalar(worldScale);
object.scale.multiplyScalar(worldScale);
// 2. Detach from original parent and apply material
gltf.scene.attach(object);
this._applyCustomMaterial(object);
groupData.objects.push(object);
}
});
groups.push(groupData);
}
});
resolve(groups);
} else {
console.warn("Could not find 'dynamic' group in the loaded model.");
resolve([]);
}
}, undefined, (error) => {
console.error('An error happened while loading the dynamic groups:', error);
reject(error);
});
});
}
loadDRACOModelURL(modelURL, textureURL, scale) { loadDRACOModelURL(modelURL, textureURL, scale) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const applyMaterial = (object, texture) => { const applyMaterial = (object, texture) => {

39
js/main.js

@ -27,6 +27,7 @@ let rapierWorld, debugLines, itemManager;
let debugRapier = false; let debugRapier = false;
let isPaused = false; let isPaused = false;
let floorGridSize = new THREE.Vector2(10, 200); let floorGridSize = new THREE.Vector2(10, 200);
const worldScale = 20.0;
let socket; let socket;
const currentPlayers = {}; const currentPlayers = {};
@ -89,7 +90,7 @@ async function init() {
/* Load Items/Content */ /* Load Items/Content */
loadWorldModel("models/demo-world-comp.glb"); loadWorldModel("models/demo-world-comp.glb");
itemManager = new ItemManager("json/Items.json", scene, rapierWorld, player, interactableItems); //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); 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(); await item.loadModel();
@ -134,6 +135,8 @@ async function init() {
} }
} }
}); });
console.log(scene)
} }
async function animate() { async function animate() {
@ -352,7 +355,6 @@ function createTiledFloor(gridSize = 20, tileSize = 50) {
async function loadWorldModel(modelUrl) { async function loadWorldModel(modelUrl) {
const modelLoader = new ModelLoader(); const modelLoader = new ModelLoader();
const worldScale = 20.0;
const staticWorldGroup = new THREE.Group(); const staticWorldGroup = new THREE.Group();
scene.add(staticWorldGroup); scene.add(staticWorldGroup);
@ -379,6 +381,37 @@ async function loadWorldModel(modelUrl) {
console.log("Finished loading world"); console.log("Finished loading world");
} catch (error) { } catch (error) {
console.error("Failed to load world model:", 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);
} }
} }

40
public/json/Items.json

@ -2,31 +2,39 @@
"items": [ "items": [
{ {
"id": 0, "id": 0,
"groupId": "reddit1", "groupId": "group_Reddit01",
"name": "cone", "name": "dyCone1",
"model": "./models/cone.glb",
"texture": null,
"contentType": "text", "contentType": "text",
"file": "./texts/example.md", "file": "./texts/example.md",
"isCollider": true,
"position": { "x": 100, "y": 1, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": 5,
"links": [1] "links": [1]
}, },
{ {
"id": 1, "id": 1,
"groupId": "reddit", "groupId": "group_Reddit01",
"name": "cone2", "name": "dyCone3",
"model": "./models/cone.glb",
"texture": null,
"contentType": "image", "contentType": "image",
"file": "./images/test.webp", "file": "./images/test.webp",
"isCollider": true,
"position": { "x": -100, "y": 1, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": 10,
"links": [0] "links": [0]
},
{
"id": 2,
"groupId": "group_Reddit",
"name": "dyCone2",
"contentType": "text",
"file": "./texts/example.md",
"links": [3]
},
{
"id": 3,
"groupId": "group_Reddit",
"name": "dyCone4",
"contentType": "image",
"file": "./images/test.webp",
"links": [2]
} }
],
"groupLinks": [
["group_Reddit", "group_Reddit01"]
] ]
} }
Loading…
Cancel
Save