import * as THREE from 'three'; 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'; export class ModelLoader { constructor() { this.gltfLoader = new GLTFLoader(); this.textureLoader = new TextureLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('/draco/'); this.gltfLoader.setDRACOLoader(dracoLoader) } _applyCustomMaterial(object) { let texture = null; if (object.material && object.material.map) { texture = object.material.map; } const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (texture && texture.image) { canvas.width = texture.image.width; canvas.height = texture.image.height; context.drawImage(texture.image, 0, 0); } else { // Fallback if no texture is found canvas.width = 1024; canvas.height = 1024; context.fillStyle = '#0044ffff'; context.fillRect(0, 0, 1024, 1024); } const drawableTexture = new THREE.CanvasTexture(canvas); drawableTexture.flipY = false; drawableTexture.needsUpdate = true; drawableTexture.encoding = THREE.sRGBEncoding; object.material = createCustomJitterMaterial(100, drawableTexture); } loadStaticWorld(modelURL, worldScale) { return new Promise((resolve, reject) => { this.gltfLoader.load(modelURL, (gltf) => { const staticGroup = gltf.scene.getObjectByName('Static'); const processedObjects = []; if (staticGroup) { // We need to handle children carefully as we might be reparenting them const children = [...staticGroup.children]; children.forEach(object => { if (object.isMesh) { // 1. Apply world scale to position and object scale object.position.multiplyScalar(worldScale); object.scale.multiplyScalar(worldScale); // Detach from the original parent ('Static' group) // so it can be added directly to the scene later. gltf.scene.attach(object); this._applyCustomMaterial(object); processedObjects.push(object); } }); resolve(processedObjects); } else { console.warn("Could not find 'Static' group in the loaded model."); resolve([]); // Resolve with an empty array if group not found } }, undefined, (error) => { console.error('An error happened while loading the world model:', error); reject(error); }); }); } 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) { return new Promise((resolve, reject) => { const applyMaterial = (object, texture) => { object.traverse((child) => { if (child.isMesh) { texture.encoding = THREE.sRGBEncoding; texture.needsUpdate = true; child.material = createCustomJitterMaterial(100, texture); } }); }; const processObject = (object, texture) => { object.scale.copy(scale); const box = new THREE.Box3().setFromObject(object); const height = box.max.y - box.min.y; object.position.y = height / 2; object.position.x = 0; applyMaterial(object, texture); resolve(object); }; if (textureURL) { this.textureLoader.load(textureURL, function (baseTexture) { baseTexture.flipY = false; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = baseTexture.image.width; canvas.height = baseTexture.image.height; context.drawImage(baseTexture.image, 0, 0); const drawableTexture = new THREE.CanvasTexture(canvas); drawableTexture.flipY = false; this.gltfLoader.load(modelURL, function (gltf) { processObject(gltf.scene, drawableTexture); }, undefined, function (error) { reject(error); }); }, undefined, function (error) { reject(error); }); } else { this.gltfLoader.load(modelURL, function (gltf) { let extractedTexture = null; gltf.scene.traverse((child) => { if (child.isMesh && child.material && child.material.map) { if (!extractedTexture) { extractedTexture = child.material.map; } } }); let finalTexture; if (extractedTexture && extractedTexture.image) { // The texture from the GLTF is valid, make it a drawable CanvasTexture const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = extractedTexture.image.width; canvas.height = extractedTexture.image.height; context.drawImage(extractedTexture.image, 0, 0); finalTexture = new THREE.CanvasTexture(canvas); } else { // fallback: create a white texture const canvas = document.createElement('canvas'); canvas.width = 1024; canvas.height = 1024; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#0044ffff'; ctx.fillRect(0, 0, 1024, 1024); finalTexture = new THREE.CanvasTexture(canvas); } finalTexture.flipY = false; processObject(gltf.scene, finalTexture); }, undefined, function (error) { reject(error); }); } }); } }