Hello,
Iâm working on a website mainly focused on mobile use (its working already) but the main scene is too heavy and I need to add more assets.
I tried to load external assets with the Additive Scene methods
https://forum.needle.tools/t/additive-scenes
https://forum.needle.tools/t/load-multiple-scene
For now itâs kinda working, but I have a big issue : the interactions are not working anymore.
- the external loaded scene is linked with some elements of the initial scene (ex: main camera).
Iâve tried some tests for the external loaded scene structure
- Add the full interactive structure (with camera, Timelines, GameObjects with linked components, and the concerned meshs)
â The assets success to load but the timelines didnât play because of a PlayableDirector issue, and the content isnât interactive - Add the mesh only without any other components
â the asset is loaded and interactive with inside elements but it doesnât link with the rest of the scene
Iâm not sure how to handle this kind of problem, and progressive/lazy loading with external asset would be key for a mobile use.
What would be a way to make it work?
Here is the code I use below
import { Behaviour, serializable, GameObject, AssetReference, InstantiateOptions } from "@needle-tools/engine";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import { ChunkConfiguration, ChunkConfig } from "./ChunkConfiguration";
export class ModularChunkLoader extends Behaviour {
@serializable()
enableDebugLogs: boolean = true;
@serializable()
loadOnStart: boolean = false;
@serializable()
preloadHighPriorityChunks: boolean = true;
@serializable(GameObject)
configurationObject?: GameObject;
private chunkConfig?: ChunkConfiguration;
private loadedChunks = new Map<string, GameObject>();
private loadingPromises = new Map<string, Promise<boolean>>();
awake(): void {
if (this.enableDebugLogs) {
console.log('đ ModularChunkLoader: Initialisation (version AssetReference.instantiate)');
}
}
start(): void {
this.initializeConfiguration();
if (this.loadOnStart) {
this.loadAutoLoadChunks();
}
if (this.preloadHighPriorityChunks) {
setTimeout(() => {
this.preloadPriorityChunks();
}, 2000);
}
setTimeout(() => {
this.preloadDelayedMobileChunks();
}, 3000);
}
private initializeConfiguration(): void {
if (this.configurationObject) {
this.chunkConfig = this.configurationObject.getComponent(ChunkConfiguration) || undefined;
}
if (!this.chunkConfig) {
this.chunkConfig = this.context.scene.getComponentInChildren(ChunkConfiguration) || undefined;
}
if (!this.chunkConfig) {
console.error("ModularChunkLoader: Aucune ChunkConfiguration trouvée !");
return;
}
if (this.enableDebugLogs) {
const availableChunks = this.chunkConfig.getAvailableChunkNames();
console.log(`ModularChunkLoader: ${availableChunks.length} chunks disponibles:`, availableChunks);
}
}
public async loadAutoLoadChunks(): Promise<void> {
if (!this.chunkConfig) return;
const autoLoadChunks = this.chunkConfig.getAutoLoadChunks();
if (autoLoadChunks.length === 0) return;
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Chargement automatique de ${autoLoadChunks.length} chunks`);
}
for (const chunk of autoLoadChunks) {
try {
await this.loadChunkByName(chunk.name);
} catch (error) {
console.error(`ModularChunkLoader: Erreur chargement auto '${chunk.name}':`, error);
}
}
}
public async preloadPriorityChunks(): Promise<void> {
if (!this.chunkConfig) return;
const priorityChunks = this.chunkConfig.getChunksByPriority(1);
if (priorityChunks.length === 0) return;
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Préchargement de ${priorityChunks.length} chunks prioritaires`);
}
for (const chunk of priorityChunks) {
try {
await this.loadChunkByName(chunk.name);
} catch (error) {
console.error(`ModularChunkLoader: Erreur préchargement '${chunk.name}':`, error);
}
}
}
public async loadChunkByName(chunkName: string): Promise<boolean> {
if (!this.chunkConfig) {
console.error("ModularChunkLoader: Configuration non initialisée");
return false;
}
if (this.loadingPromises.has(chunkName)) {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: '${chunkName}' déjà en cours de chargement`);
}
return await this.loadingPromises.get(chunkName)!;
}
if (this.loadedChunks.has(chunkName)) {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: '${chunkName}' déjà chargé`);
}
return true;
}
const config = this.chunkConfig.getChunkConfig(chunkName);
if (!config) {
console.error(`â ModularChunkLoader: Configuration non trouvĂ©e pour '${chunkName}'`);
return false;
}
const platformChunks = this.chunkConfig.getPlatformCompatibleChunks();
const isCompatible = platformChunks.some(c => c.name === chunkName);
if (!isCompatible) {
if (this.enableDebugLogs) {
console.log(`đ± ModularChunkLoader: Chunk '${chunkName}' non compatible avec cette plateforme`);
}
return false;
}
const delay = this.chunkConfig.getLoadDelay(chunkName);
if (delay > 0) {
if (this.enableDebugLogs) {
console.log(`đ± ModularChunkLoader: DĂ©lai mobile de ${delay}ms pour '${chunkName}'`);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
const loadPromise = this.loadChunk(config);
this.loadingPromises.set(chunkName, loadPromise);
try {
const success = await loadPromise;
this.loadingPromises.delete(chunkName);
return success;
} catch (error) {
this.loadingPromises.delete(chunkName);
throw error;
}
}
private async loadChunk(config: ChunkConfig): Promise<boolean> {
const startTime = performance.now();
try {
if (config.remoteUrl) {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Chargement distant de '${config.name}' depuis '${config.remoteUrl}'`);
}
const success = await this.loadSceneFromUrl(config.remoteUrl, config.name);
if (success) {
const loadTime = performance.now() - startTime;
console.log(`â
ModularChunkLoader: Chunk distant '${config.name}' chargé en ${loadTime.toFixed(2)}ms`);
return true;
}
}
if (config.localFallbackPath) {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Fallback local pour '${config.name}' depuis '${config.localFallbackPath}'`);
}
const success = await this.loadSceneFromUrl(config.localFallbackPath, config.name);
if (success) {
const loadTime = performance.now() - startTime;
console.log(`â
ModularChunkLoader: Chunk local '${config.name}' chargé en ${loadTime.toFixed(2)}ms`);
return true;
}
}
console.error(`â ModularChunkLoader: Ăchec chargement '${config.name}' (distant et local)`);
return false;
} catch (error) {
console.error(`â ModularChunkLoader: Erreur chargement '${config.name}':`, error);
return false;
}
}
private async loadSceneFromUrl(url: string, chunkName: string): Promise<boolean> {
try {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Utilisation du systĂšme Needle Engine pour ${url}`);
}
const assetRef = new AssetReference(url);
const scene = await assetRef.loadAssetAsync();
if (scene) {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Chargement Needle Engine réussi pour ${url}`);
}
this.gameObject.add(scene);
if (this.enableDebugLogs) {
console.log(`đŻ ModularChunkLoader: Chunk '${chunkName}' ajoutĂ© Ă la scĂšne principale`);
}
this.loadedChunks.set(chunkName, scene);
await this.postProcessChunk(scene, chunkName);
return true;
}
return false;
} catch (error) {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Erreur AssetReference.loadAssetAsync() pour ${url}:`, error);
}
return false;
}
}
private async postProcessChunk(scene: GameObject, chunkName: string): Promise<void> {
try {
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Post-traitement du chunk '${chunkName}'`);
}
if (scene.getComponentsInChildren) {
const allComponents = scene.getComponentsInChildren(Behaviour);
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: ${allComponents.length} composants Needle trouvés dans '${chunkName}'`);
if (allComponents.length > 0) {
const componentTypes = [...new Set(allComponents.map(c => c.constructor.name))];
console.log(`ModularChunkLoader: Types de composants: ${componentTypes.join(', ')}`);
}
}
for (const component of allComponents) {
const componentName = component.constructor.name;
const problematicComponents = [
'SpatialAudioUI',
'Fog',
'RemoteSkybox',
'SceneLightSettings',
'WebXR',
'NeedleMenu',
'Canvas'
];
if (problematicComponents.includes(componentName)) {
component.enabled = false;
if (this.enableDebugLogs) {
console.log(`ModularChunkLoader: Désactivé ${componentName} dans '${chunkName}'`);
}
}
}
}
if (this.enableDebugLogs) {
console.log(`â
ModularChunkLoader: Post-traitement terminé pour '${chunkName}'`);
}
} catch (error) {
if (this.enableDebugLogs) {
console.error(`â ModularChunkLoader: Erreur post-traitement '${chunkName}':`, error);
}
}
}
public async preloadDelayedMobileChunks(): Promise<void> {
if (!this.chunkConfig) return;
const delayedChunks = this.chunkConfig.getDelayedMobileChunks();
if (delayedChunks.length === 0) return;
if (this.enableDebugLogs) {
console.log(`đ± ModularChunkLoader: PrĂ©chargement diffĂ©rĂ© de ${delayedChunks.length} chunks mobiles`);
}
for (const chunk of delayedChunks) {
try {
await this.loadChunkByName(chunk.name);
} catch (error) {
console.error(`â ModularChunkLoader: Erreur prĂ©chargement diffĂ©rĂ© '${chunk.name}':`, error);
}
}
}
public async loadChunkOnDemand(chunkName: string): Promise<boolean> {
if (!this.chunkConfig) {
console.error("ModularChunkLoader: Configuration non initialisée");
return false;
}
const config = this.chunkConfig.getChunkConfig(chunkName);
if (!config) {
console.error(`â ModularChunkLoader: Configuration non trouvĂ©e pour '${chunkName}'`);
return false;
}
const platformChunks = this.chunkConfig.getPlatformCompatibleChunks();
const isCompatible = platformChunks.some(c => c.name === chunkName);
if (!isCompatible) {
if (this.enableDebugLogs) {
console.log(`đ± ModularChunkLoader: Chunk '${chunkName}' non compatible avec cette plateforme`);
}
return false;
}
return await this.loadChunkByName(chunkName);
}
public isChunkLoaded(chunkName: string): boolean {
return this.loadedChunks.has(chunkName);
}
public getLoadedChunk(chunkName: string): GameObject | undefined {
return this.loadedChunks.get(chunkName);
}
public unloadChunk(chunkName: string): boolean {
if (!this.loadedChunks.has(chunkName)) {
return false;
}
const scene = this.loadedChunks.get(chunkName);
if (scene && scene.parent) {
scene.parent.remove(scene);
}
this.loadedChunks.delete(chunkName);
if (this.enableDebugLogs) {
console.log(`đïž ModularChunkLoader: Chunk '${chunkName}' dĂ©chargĂ©`);
}
return true;
}
public getLoadedChunkNames(): string[] {
return Array.from(this.loadedChunks.keys());
}
public unloadAllChunks(): void {
const chunkNames = this.getLoadedChunkNames();
for (const chunkName of chunkNames) {
this.unloadChunk(chunkName);
}
if (this.enableDebugLogs) {
console.log(`đïž ModularChunkLoader: Tous les chunks dĂ©chargĂ©s (${chunkNames.length})`);
}
}
onDestroy(): void {
this.unloadAllChunks();
this.loadingPromises.clear();
if (this.enableDebugLogs) {
console.log("đïž ModularChunkLoader: Composant dĂ©truit, nettoyage terminĂ©");
}
}
}
Thank you for your help

