Files
GDevelop/GDJS/Runtime/Model3DManager.ts
ViktorVovk 5c71a4da56 Allow to unload scene resources when a scene is exited (#7381)
* This adds two new settings, available in the Project Properties and in the Scene Properties dialog, to allow to specify the strategy for preloading resources of the scenes and unloading them. By default, a game will preload in background the resources of all scenes. It will never unload these resources (so scene switching is fast).
* You can now choose to unload the resources of a scene when the scene is left using the "Resources unloading" field. If the scene is launched again later, it will load its resources again.
* You can also choose to change the preloading to disable it for all scenes (by modifying the setting in the project properties) or enable it/disable it on a scene by scene basis. This can be useful for large or modular games where you anticipate the player to only play some scenes, or if you want to reduce the resources that needs to be loaded on a web game.
2025-07-02 16:09:52 +02:00

191 lines
5.7 KiB
TypeScript

/*
* GDevelop JS Platform
* Copyright 2013-present Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
const logger = new gdjs.Logger('Model3DManager');
const resourceKinds: Array<ResourceKind> = ['model3D'];
/**
* Load GLB files (using `Three.js`), using the "model3D" resources
* registered in the game resources.
*/
export class Model3DManager implements gdjs.ResourceManager {
/**
* Map associating a resource name to the loaded Three.js model.
*/
private _loadedThreeModels = new gdjs.ResourceCache<THREE_ADDONS.GLTF>();
private _downloadedArrayBuffers = new gdjs.ResourceCache<ArrayBuffer>();
_resourceLoader: gdjs.ResourceLoader;
_loader: THREE_ADDONS.GLTFLoader | null = null;
_dracoLoader: THREE_ADDONS.DRACOLoader | null = null;
//@ts-ignore Can only be null if THREE is not loaded.
_invalidModel: THREE_ADDONS.GLTF;
/**
* @param resourceLoader The resources loader of the game.
*/
constructor(resourceLoader: gdjs.ResourceLoader) {
this._resourceLoader = resourceLoader;
if (typeof THREE !== 'undefined') {
this._loader = new THREE_ADDONS.GLTFLoader();
this._dracoLoader = new THREE_ADDONS.DRACOLoader();
this._dracoLoader.setDecoderPath('./pixi-renderers/draco/gltf/');
this._loader.setDRACOLoader(this._dracoLoader);
/**
* The invalid model is a box with magenta (#ff00ff) faces, to be
* easily spotted if rendered on screen.
*/
const group = new THREE.Group();
group.add(
new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: '#ff00ff' })
)
);
this._invalidModel = {
scene: group,
animations: [],
cameras: [],
scenes: [],
asset: {},
userData: {},
//@ts-ignore
parser: null,
};
}
}
getResourceKinds(): ResourceKind[] {
return resourceKinds;
}
async processResource(resourceName: string): Promise<void> {
const resource = this._resourceLoader.getResource(resourceName);
if (!resource) {
logger.warn(
'Unable to find texture for resource "' + resourceName + '".'
);
return;
}
const loader = this._loader;
if (!loader) {
return;
}
const data = this._downloadedArrayBuffers.get(resource);
if (!data) {
return;
}
this._downloadedArrayBuffers.delete(resource);
try {
const gltf: THREE_ADDONS.GLTF = await loader.parseAsync(data, '');
this._loadedThreeModels.set(resource, gltf);
} catch (error) {
logger.error(
"Can't fetch the 3D model file " + resource.file + ', error: ' + error
);
}
}
async loadResource(resourceName: string): Promise<void> {
const resource = this._resourceLoader.getResource(resourceName);
if (!resource) {
logger.warn(
'Unable to find texture for resource "' + resourceName + '".'
);
return;
}
const loader = this._loader;
if (!loader) {
return;
}
if (this._loadedThreeModels.get(resource)) {
return;
}
const url = this._resourceLoader.getFullUrl(resource.file);
try {
const response = await fetch(url, {
credentials: this._resourceLoader.checkIfCredentialsRequired(url)
? 'include'
: 'omit',
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.arrayBuffer();
this._downloadedArrayBuffers.set(resource, data);
} catch (error) {
logger.error(
"Can't fetch the 3D model file " + resource.file + ', error: ' + error
);
}
}
/**
* Return a 3D model.
*
* Caller should not modify the object but clone it.
*
* @param resourceName The name of the json resource.
* @returns a 3D model if it exists.
*/
getModel(resourceName: string): THREE_ADDONS.GLTF {
return (
this._loadedThreeModels.getFromName(resourceName) || this._invalidModel
);
}
/**
* To be called when the game is disposed.
* Clear the models, resources loaded and destroy 3D models loaders in this manager.
*/
dispose(): void {
this._loadedThreeModels.clear();
this._downloadedArrayBuffers.clear();
this._loader = null;
this._dracoLoader = null;
if (this._invalidModel) {
this._invalidModel.cameras = [];
this._invalidModel.animations = [];
this._invalidModel.scenes = [];
this._invalidModel.userData = {};
this._invalidModel.asset = {};
this._invalidModel.scene.clear();
}
}
/**
* Unload the specified list of resources:
* this clears the models, resources loaded and destroy 3D models loaders in this manager.
*
* Usually called when scene resoures are unloaded.
*
* @param resourcesList The list of specific resources
*/
unloadResourcesList(resourcesList: ResourceData[]): void {
resourcesList.forEach((resourceData) => {
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
if (loadedThreeModel) {
loadedThreeModel.scene.clear();
this._loadedThreeModels.delete(resourceData);
}
const downloadedArrayBuffer =
this._downloadedArrayBuffers.get(resourceData);
if (downloadedArrayBuffer) {
this._downloadedArrayBuffers.delete(resourceData);
}
});
}
}
}