@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
553 lines • 23.2 kB
JavaScript
import { Texture } from "../Materials/Textures/texture.pure.js";
import { CubeTexture } from "../Materials/Textures/cubeTexture.pure.js";
import { HDRCubeTexture } from "../Materials/Textures/hdrCubeTexture.pure.js";
import { Observable } from "../Misc/observable.js";
import { Logger } from "../Misc/logger.js";
import { LoadAssetContainerAsync } from "../Loading/sceneLoader.js";
import { GetExtensionFromUrl } from "../Misc/urlTools.js";
import { DeserializeSmartAssetMap, IsAbsoluteOrSpecialUrl, MakeRelative, ResolveAssetUrl, ReadJsonSourceAsync, } from "./smartAssetSerializer.js";
// Keep this a Symbol (not a string): scene.metadata is a publicly enumerable
// object, and routing SAM through a symbol key keeps it out of Object.values /
// JSON.stringify so the inspector's metadata viewer doesn't recurse into the
// manager (which holds a back-reference to the scene, creating a cycle).
// eslint-disable-next-line @typescript-eslint/naming-convention
const SMART_ASSET_MANAGER_KEY = /*#__PURE__*/ Symbol("babylonjs:smartAssetManager");
// Lazy-initialised module state. Wrapped in accessors so the module has
// no top-level side effects and is trivially tree-shakeable: nothing
// allocates until a SmartAssetManager is actually created.
let _InternalsMap;
function GetInternalsMap() {
return (_InternalsMap ?? (_InternalsMap = new WeakMap()));
}
let _OnCreatedObservable;
function GetOnCreatedObservable() {
return (_OnCreatedObservable ?? (_OnCreatedObservable = new Observable()));
}
/**
* Creates a new SmartAssetManager state object and attaches it to the scene.
*
* Internal: callers should use {@link GetSmartAssetManager} which returns the
* existing manager when one is already attached.
* @param scene - The scene this manager operates on.
* @returns The created smart asset manager state.
*/
function CreateSmartAssetManager(scene) {
const manager = {
scene,
onChangedObservable: new Observable(),
onAssetNotFound: null,
};
const internal = {
urls: new Map(),
options: new Map(),
containers: new Map(),
objectToKeyMap: new WeakMap(),
textureKeys: new Set(),
reloadSources: new Map(),
blobUrls: new Map(),
sceneDisposeObserver: null,
};
GetInternalsMap().set(manager, internal);
if (!scene.metadata) {
scene.metadata = {};
}
scene.metadata[SMART_ASSET_MANAGER_KEY] = manager;
// Auto-dispose when the scene is disposed so the manager doesn't outlive it.
internal.sceneDisposeObserver = scene.onDisposeObservable.add(() => DisposeSmartAssetManager(manager));
GetOnCreatedObservable().notifyObservers(manager);
return manager;
}
/**
* Returns the SmartAssetManager attached to the given scene, creating and
* attaching one if none exists.
* @param scene - The scene to look up or attach a manager to.
* @returns The existing or newly created SmartAssetManager.
*/
export function GetSmartAssetManager(scene) {
const existing = scene.metadata?.[SMART_ASSET_MANAGER_KEY];
if (existing) {
return existing;
}
return CreateSmartAssetManager(scene);
}
/**
* Adds an observer that is notified whenever a SmartAssetManager is created.
* @param callback - The callback to invoke with each newly created manager.
* @returns The observer registration.
*/
export function AddSmartAssetManagerCreatedObserver(callback) {
// Wrap so the EventState second-arg from Observable.add isn't passed through to the caller.
return GetOnCreatedObservable().add((manager) => callback(manager));
}
/**
* Registers a smart asset entry mapping a key to a URL.
* @param scene - The scene whose smart asset registry to update.
* @param key - Unique string identifier for this asset.
* @param url - URL or path to the asset file.
* @param options - Optional loader hints and metadata for this asset.
*/
export function RegisterSmartAsset(scene, key, url, options) {
const manager = GetSmartAssetManager(scene);
const internal = GetSmartAssetInternals(manager);
RevokeManagedBlobUrl(internal, key, url);
internal.urls.set(key, url);
TrackManagedBlobUrl(internal, key, url);
if (options) {
const existingOptions = internal.options.get(key);
internal.options.set(key, { ...existingOptions, ...options });
if (options.type === "texture") {
internal.textureKeys.add(key);
}
else if (options.type !== undefined) {
internal.textureKeys.delete(key);
}
}
manager.onChangedObservable.notifyObservers();
}
/**
* Removes a key from the registry. If the asset is loaded, it is unloaded first.
* @param scene - The scene that owns the smart asset.
* @param key - The key to remove.
* @returns A promise that resolves when the asset has been unloaded and removed.
*/
export async function RemoveSmartAssetAsync(scene, key) {
const manager = GetSmartAssetManager(scene);
const internal = GetSmartAssetInternals(manager);
if (internal.containers.has(key)) {
await UnloadSmartAssetAsync(scene, key);
}
for (const tex of [...scene.textures]) {
if (internal.objectToKeyMap.get(tex) === key) {
internal.objectToKeyMap.delete(tex);
tex.dispose();
}
}
internal.urls.delete(key);
internal.options.delete(key);
internal.textureKeys.delete(key);
internal.reloadSources.delete(key);
RevokeManagedBlobUrl(internal, key);
manager.onChangedObservable.notifyObservers();
}
/**
* Returns all registered key-to-URL mappings.
* @param scene - The scene whose smart asset registry to read.
* @returns A read-only map of keys to URLs.
*/
export function GetAllSmartAssets(scene) {
return GetSmartAssetInternals(GetSmartAssetManager(scene)).urls;
}
/**
* Loads a scene-file asset by key.
* @param scene - The scene to load the asset into.
* @param key - The key to load.
* @param url - Optional URL. If provided, the key is registered first.
* @param options - Optional loader hints and metadata for this asset.
* @returns A promise resolving to the loaded AssetContainer.
*/
export async function LoadSmartAssetAsync(scene, key, url, options) {
const manager = GetSmartAssetManager(scene);
const internal = GetSmartAssetInternals(manager);
const previousUrl = internal.urls.get(key);
const { reloadSource, ...registrationOptions } = options ?? {};
if (url) {
RegisterSmartAsset(scene, key, url, registrationOptions);
}
if (reloadSource) {
internal.reloadSources.set(key, reloadSource);
}
const resolvedUrl = internal.urls.get(key);
if (!resolvedUrl) {
throw new Error(`SmartAssetManager: Key "${key}" is not registered. Provide a URL to auto-register.`);
}
const existing = internal.containers.get(key);
if (existing) {
if (url && url !== previousUrl) {
// URL changed — drop the stale container before fetching the new one
// so callers don't get a surprise cached return for an updated URL.
await UnloadSmartAssetAsync(scene, key);
}
else {
return existing;
}
}
return await LoadSmartAssetSceneFileAsync(manager, key, resolvedUrl, internal.options.get(key)?.extension);
}
/**
* Loads all registered assets concurrently.
* @param scene - The scene whose registered assets to load.
* @returns A promise resolving to loaded scene-file containers.
*/
export async function LoadAllSmartAssetsAsync(scene) {
const manager = GetSmartAssetManager(scene);
const internal = GetSmartAssetInternals(manager);
const scenePromises = [];
const texturePromises = [];
for (const [key, url] of Array.from(internal.urls)) {
if (internal.containers.has(key)) {
continue;
}
const options = internal.options.get(key);
if (internal.textureKeys.has(key) || options?.type === "texture" || IsTextureUrl(url) || IsTextureExtension(options?.extension)) {
const textureLoadAsync = async () => {
try {
await LoadSmartAssetTextureAsync(scene, key);
}
catch {
Logger.Warn(`SmartAssetManager: Texture "${key}" could not be loaded — skipping.`);
}
};
texturePromises.push(textureLoadAsync());
}
else {
const sceneLoadAsync = async () => {
try {
return await LoadSmartAssetAsync(scene, key);
}
catch {
Logger.Warn(`SmartAssetManager: Asset "${key}" could not be loaded — skipping.`);
return null;
}
};
scenePromises.push(sceneLoadAsync());
}
}
await Promise.all(texturePromises);
const results = await Promise.all(scenePromises);
return results.filter((r) => r !== null);
}
/**
* Loads a standalone texture by key.
* @param scene - The scene to load the texture into.
* @param key - The key to load.
* @param url - Optional URL. If provided, the key is registered first.
* @param options - Optional loader hints and metadata for this asset.
* @returns A promise resolving to the loaded texture.
*/
export async function LoadSmartAssetTextureAsync(scene, key, url, options) {
const manager = GetSmartAssetManager(scene);
const internal = GetSmartAssetInternals(manager);
const previousUrl = internal.urls.get(key);
const { reloadSource, ...registrationOptions } = options ?? {};
if (url) {
RegisterSmartAsset(scene, key, url, { ...registrationOptions, type: registrationOptions.type ?? "texture" });
}
if (reloadSource) {
internal.reloadSources.set(key, reloadSource);
}
internal.textureKeys.add(key);
// Mirror LoadSmartAssetAsync: if a tracked texture already exists for this key,
// return it on a same-URL call (cache hit) or dispose it before reload on URL change.
// Without this guard, calling LoadSmartAssetTextureAsync twice with the same URL
// would create duplicate Texture objects in scene.textures.
const existingTextures = [];
for (const tex of scene.textures) {
if (internal.objectToKeyMap.get(tex) === key) {
existingTextures.push(tex);
}
}
if (existingTextures.length > 0) {
if (url && previousUrl !== undefined && url !== previousUrl) {
for (const tex of existingTextures) {
internal.objectToKeyMap.delete(tex);
tex.dispose();
}
}
else {
return existingTextures[0];
}
}
const resolvedUrl = internal.urls.get(key);
if (!resolvedUrl) {
throw new Error(`SmartAssetManager: Key "${key}" is not registered. Provide a URL to auto-register.`);
}
const extensionHint = internal.options.get(key)?.extension;
let texture;
try {
texture = await CreateAndLoadTextureAsync(manager, resolvedUrl, extensionHint);
}
catch (error) {
const fallback = await ResolveNotFoundAsync(manager, key, resolvedUrl);
if (!fallback) {
throw error;
}
texture = await CreateAndLoadTextureAsync(manager, fallback.url, fallback.extensionHint ?? extensionHint);
}
internal.objectToKeyMap.set(texture, key);
// Surface the registry key as the texture's display name when the texture
// doesn't already have one. For blob/data-URL textures (e.g. user-uploaded
// files) the underlying `texture.name` is an opaque blob URL, so without
// this any UI that shows texture names (Scene Explorer, material slot
// pickers, override summaries) would render the blob URL instead of the
// user-chosen key. We never overwrite a display name the caller already set.
if (!texture.displayName) {
texture.displayName = key;
}
manager.onChangedObservable.notifyObservers();
return texture;
}
/**
* Unloads a loaded asset while keeping the key registered.
* @param scene - The scene whose smart asset to unload.
* @param key - The key to unload.
* @returns A promise that resolves once the asset has been unloaded.
*/
export async function UnloadSmartAssetAsync(scene, key) {
const manager = GetSmartAssetManager(scene);
const internal = GetSmartAssetInternals(manager);
const container = internal.containers.get(key);
if (container) {
container.removeAllFromScene();
container.dispose();
internal.containers.delete(key);
manager.onChangedObservable.notifyObservers();
return;
}
for (const tex of [...scene.textures]) {
if (internal.objectToKeyMap.get(tex) === key) {
internal.objectToKeyMap.delete(tex);
tex.dispose();
}
}
manager.onChangedObservable.notifyObservers();
}
/**
* Unloads and re-loads an asset.
* @param scene - The scene whose smart asset to reload.
* @param key - The key to reload.
* @returns A promise resolving to the newly loaded AssetContainer or BaseTexture.
*/
export async function ReloadSmartAssetAsync(scene, key) {
const internal = GetSmartAssetInternals(GetSmartAssetManager(scene));
const reloadSource = internal.reloadSources.get(key);
if (reloadSource) {
try {
const freshFile = await reloadSource();
const blobUrl = URL.createObjectURL(freshFile);
RegisterSmartAsset(scene, key, blobUrl, { extension: GetExtensionFromUrl(freshFile.name) || internal.options.get(key)?.extension });
}
catch (e) {
Logger.Warn(`SmartAssetManager: reloadSource callback failed for "${key}": ${e}`);
}
}
await UnloadSmartAssetAsync(scene, key);
if (internal.textureKeys.has(key)) {
return await LoadSmartAssetTextureAsync(scene, key);
}
return await LoadSmartAssetAsync(scene, key);
}
/**
* Finds which smart asset key owns a scene object.
* @param scene - The scene whose registry to search.
* @param object - A scene object.
* @returns The key, or undefined if the object is not tracked.
*/
export function FindSmartAssetKeyForObject(scene, object) {
return GetSmartAssetInternals(GetSmartAssetManager(scene)).objectToKeyMap.get(object);
}
/**
* Serializes the registry to a JSON-compatible document.
* If a baseUrl is provided, asset URLs are stored relative to it for portability.
* @param scene - The scene whose registry to serialize.
* @param baseUrl - Optional base URL for making asset paths relative.
* @returns A serialized asset map document.
*/
export function SerializeSmartAssetManagerMap(scene, baseUrl) {
const internal = GetSmartAssetInternals(GetSmartAssetManager(scene));
const assets = {};
for (const [key, registeredUrl] of Array.from(internal.urls)) {
let url = registeredUrl;
if (baseUrl && !IsAbsoluteOrSpecialUrl(url)) {
url = MakeRelative(url, baseUrl);
}
const options = internal.options.get(key);
assets[key] = { url, ...options, ...(internal.textureKeys.has(key) ? { type: "texture" } : {}) };
}
return { version: 1, assets };
}
/**
* Loads an asset map from a URL, File, or pre-parsed JSON object.
* @param scene - The scene to load assets into.
* @param source - A URL string, File object, or pre-parsed ISerializedSmartAssetMap.
* @param rootUrl - Optional root URL for resolving relative asset paths.
* @returns A promise that resolves after the map has been loaded and all registered assets have been attempted.
*/
export async function LoadSmartAssetMapAsync(scene, source, rootUrl) {
let resolvedRootUrl = rootUrl ?? "";
if (typeof source === "string" && !rootUrl) {
const { Tools } = await import("../Misc/tools.js");
resolvedRootUrl = Tools.GetFolderPath(source);
}
const raw = await ReadJsonSourceAsync(source);
const doc = DeserializeSmartAssetMap(raw);
for (const [key, entry] of Object.entries(doc.assets)) {
const resolved = resolvedRootUrl ? ResolveAssetUrl(entry.url, resolvedRootUrl) : entry.url;
RegisterSmartAsset(scene, key, resolved, { type: entry.type, extension: entry.extension, metadata: entry.metadata });
}
await LoadAllSmartAssetsAsync(scene);
}
/**
* Registers an externally loaded AssetContainer under a key.
* @param manager - The smart asset manager state.
* @param key - The key to associate with the container.
* @param container - The loaded AssetContainer.
*/
function TrackLoadedSmartAssetContainer(manager, key, container) {
const internal = GetSmartAssetInternals(manager);
internal.containers.set(key, container);
TrackSmartAssetContainerObjects(manager, key, container);
manager.onChangedObservable.notifyObservers();
}
/**
* Disposes the manager, unloading all assets and detaching it from its scene.
* Safe to call multiple times; subsequent calls are no-ops. Automatically invoked when the
* owning scene is disposed.
* @param manager - The smart asset manager state.
*/
export function DisposeSmartAssetManager(manager) {
const internal = GetInternalsMap().get(manager);
if (!internal) {
return;
}
GetInternalsMap().delete(manager);
if (internal.sceneDisposeObserver) {
manager.scene.onDisposeObservable.remove(internal.sceneDisposeObserver);
internal.sceneDisposeObserver = null;
}
for (const container of Array.from(internal.containers.values())) {
container.removeAllFromScene();
container.dispose();
}
for (const tex of [...manager.scene.textures]) {
if (internal.objectToKeyMap.get(tex) !== undefined) {
tex.dispose();
}
}
internal.urls.clear();
internal.options.clear();
internal.textureKeys.clear();
internal.reloadSources.clear();
internal.containers.clear();
for (const blobUrl of Array.from(internal.blobUrls.values())) {
URL.revokeObjectURL(blobUrl);
}
internal.blobUrls.clear();
manager.onChangedObservable.clear();
if (manager.scene.metadata) {
delete manager.scene.metadata[SMART_ASSET_MANAGER_KEY];
}
}
function GetSmartAssetInternals(manager) {
const internal = GetInternalsMap().get(manager);
if (!internal) {
throw new Error("SmartAssetManager: Unknown manager state.");
}
return internal;
}
function RevokeManagedBlobUrl(internal, key, replacementUrl) {
const blobUrl = internal.blobUrls.get(key);
if (!blobUrl || blobUrl === replacementUrl) {
return;
}
URL.revokeObjectURL(blobUrl);
internal.blobUrls.delete(key);
}
function TrackManagedBlobUrl(internal, key, url) {
if (url.startsWith("blob:")) {
internal.blobUrls.set(key, url);
}
}
async function CreateAndLoadTextureAsync(manager, url, extensionHint) {
return await new Promise((resolve, reject) => {
const ext = (extensionHint || GetExtensionFromUrl(url)).toLowerCase();
const onError = (message, exception) => {
const err = exception instanceof Error ? exception : new Error(message ?? `SmartAssetManager: failed to load texture from "${url}".`);
reject(err);
};
let texture;
const onLoad = () => resolve(texture);
if (ext === ".hdr") {
// HDR equirectangular files require HDRCubeTexture — CubeTexture's .hdr
// loader explicitly throws ".hdr not supported in Cube." so we can't
// route HDRs through the generic CubeTexture path.
texture = new HDRCubeTexture(url, manager.scene, 256, false, true, false, false, onLoad, onError);
}
else if (ext === ".env" || ext === ".dds") {
texture = new CubeTexture(url, manager.scene, null, false, null, onLoad, onError, undefined, ext === ".env");
}
else {
texture = new Texture(url, manager.scene, undefined, undefined, undefined, onLoad, onError);
}
});
}
async function ResolveNotFoundAsync(manager, key, expectedUrl) {
if (!manager.onAssetNotFound) {
return null;
}
const resolution = await manager.onAssetNotFound(key, expectedUrl);
if (resolution === null || resolution === undefined) {
return null;
}
if (typeof resolution === "string") {
RegisterSmartAsset(manager.scene, key, resolution);
return { url: resolution, extensionHint: GetSmartAssetInternals(manager).options.get(key)?.extension };
}
const blobUrl = URL.createObjectURL(resolution);
const extensionHint = GetExtensionFromUrl(resolution.name) || undefined;
RegisterSmartAsset(manager.scene, key, blobUrl, { extension: extensionHint });
return { url: blobUrl, extensionHint };
}
async function LoadSmartAssetSceneFileAsync(manager, key, url, extensionHint) {
const loadAsync = async (loadUrl, extensionHint) => {
const container = await LoadAssetContainerAsync(loadUrl, manager.scene, { pluginExtension: extensionHint });
container.addAllToScene();
TrackLoadedSmartAssetContainer(manager, key, container);
return container;
};
try {
return await loadAsync(url, extensionHint);
}
catch (error) {
const fallback = await ResolveNotFoundAsync(manager, key, url);
if (fallback) {
try {
return await loadAsync(fallback.url, fallback.extensionHint);
}
catch (retryError) {
Logger.Warn(`SmartAssetManager: Asset "${key}" could not be loaded from fallback "${fallback.url}".`);
throw retryError;
}
}
Logger.Warn(`SmartAssetManager: Asset "${key}" could not be loaded from "${url}".`);
throw error;
}
}
function TrackSmartAssetContainerObjects(manager, key, container) {
const internal = GetSmartAssetInternals(manager);
for (const collection of [container.meshes, container.materials, container.textures, container.animationGroups, container.lights, container.cameras]) {
for (const obj of collection) {
internal.objectToKeyMap.set(obj, key);
}
}
}
let _TextureExtensions;
/**
* Returns the set of file extensions (including the leading dot) that {@link LoadAllSmartAssetsAsync}
* treats as standalone textures.
* @returns A read-only set of texture file extensions.
*/
export function GetSmartAssetTextureExtensions() {
return (_TextureExtensions ?? (_TextureExtensions = new Set([".png", ".jpg", ".jpeg", ".bmp", ".tga", ".gif", ".webp", ".env", ".hdr", ".dds", ".ktx", ".ktx2", ".basis"])));
}
/**
* Returns true if the URL points to a standalone texture file.
* @param url - The URL to check.
* @returns True if the URL has a texture file extension.
*/
function IsTextureUrl(url) {
return GetSmartAssetTextureExtensions().has(GetExtensionFromUrl(url));
}
function IsTextureExtension(extension) {
return extension !== undefined && GetSmartAssetTextureExtensions().has(extension.toLowerCase());
}
//# sourceMappingURL=smartAssetManager.pure.js.map