UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

683 lines (682 loc) 23.3 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { path } from "../../core/path.js"; import { Debug } from "../../core/debug.js"; import { Tracing } from "../../core/tracing.js"; import { TRACEID_ASSETS } from "../../core/constants.js"; import { EventHandler } from "../../core/event-handler.js"; import { TagsCache } from "../../core/tags-cache.js"; import { standardMaterialTextureParameters } from "../../scene/materials/standard-material-parameters.js"; import { Asset } from "./asset.js"; class AssetRegistry extends EventHandler { /** * Create an instance of an AssetRegistry. * * @param {ResourceLoader} loader - The ResourceLoader used to load the asset files. */ constructor(loader) { super(); /** * @type {Set<Asset>} * @private */ __publicField(this, "_assets", /* @__PURE__ */ new Set()); /** * @type {ResourceLoader} * @private */ __publicField(this, "_loader"); /** * @type {Map<number, Asset>} * @private */ __publicField(this, "_idToAsset", /* @__PURE__ */ new Map()); /** * @type {Map<string, Asset>} * @private */ __publicField(this, "_urlToAsset", /* @__PURE__ */ new Map()); /** * @type {Map<string, Set<Asset>>} * @private */ __publicField(this, "_nameToAsset", /* @__PURE__ */ new Map()); /** * Index for looking up by tags. * * @private */ __publicField(this, "_tags", new TagsCache("id")); /** * A URL prefix that will be added to all asset loading requests. * * @type {string|null} */ __publicField(this, "prefix", null); /** * BundleRegistry * * @type {BundleRegistry|null} */ __publicField(this, "bundles", null); this._loader = loader; } /** * The ResourceLoader used to load asset files. * * @type {ResourceLoader} * @ignore */ get loader() { return this._loader; } /** * Create a filtered list of assets from the registry. * * @param {object} [filters] - Filter options. * @param {boolean} [filters.preload] - Filter by preload setting. * @returns {Asset[]} The filtered list of assets. */ list(filters = {}) { const assets = Array.from(this._assets); if (filters.preload !== void 0) { return assets.filter((asset) => asset.preload === filters.preload); } return assets; } /** * Add an asset to the registry. If {@link Asset#preload} is `true`, it will also get loaded. * * @param {Asset} asset - The asset to add. * @example * const asset = new pc.Asset("My Asset", "texture", { * url: "../path/to/image.jpg" * }); * app.assets.add(asset); */ add(asset) { if (this._assets.has(asset)) return; this._assets.add(asset); this._idToAsset.set(asset.id, asset); if (asset.file?.url) { this._urlToAsset.set(asset.file.url, asset); } if (!this._nameToAsset.has(asset.name)) { this._nameToAsset.set(asset.name, /* @__PURE__ */ new Set()); } this._nameToAsset.get(asset.name).add(asset); asset.on("name", this._onNameChange, this); asset.registry = this; this._tags.addItem(asset); asset.tags.on("add", this._onTagAdd, this); asset.tags.on("remove", this._onTagRemove, this); this.fire("add", asset); this.fire(`add:${asset.id}`, asset); if (asset.file?.url) { this.fire(`add:url:${asset.file.url}`, asset); } if (asset.preload) { this.load(asset); } } /** * Remove an asset from the registry. * * @param {Asset} asset - The asset to remove. * @returns {boolean} True if the asset was successfully removed and false otherwise. * @example * const asset = app.assets.get(100); * app.assets.remove(asset); */ remove(asset) { if (!this._assets.has(asset)) return false; this._assets.delete(asset); this._idToAsset.delete(asset.id); if (asset.file?.url) { this._urlToAsset.delete(asset.file.url); } asset.off("name", this._onNameChange, this); if (this._nameToAsset.has(asset.name)) { const items = this._nameToAsset.get(asset.name); items.delete(asset); if (items.size === 0) { this._nameToAsset.delete(asset.name); } } this._tags.removeItem(asset); asset.tags.off("add", this._onTagAdd, this); asset.tags.off("remove", this._onTagRemove, this); asset.fire("remove", asset); this.fire("remove", asset); this.fire(`remove:${asset.id}`, asset); if (asset.file?.url) { this.fire(`remove:url:${asset.file.url}`, asset); } return true; } /** * Retrieve an asset from the registry by its id field. * * @param {number} id - The id of the asset to get. * @returns {Asset|undefined} The asset. * @example * const asset = app.assets.get(100); */ get(id) { return this._idToAsset.get(Number(id)); } /** * Retrieve an asset from the registry by its file's URL field. * * @param {string} url - The url of the asset to get. * @returns {Asset|undefined} The asset. * @example * const asset = app.assets.getByUrl("../path/to/image.jpg"); */ getByUrl(url) { return this._urlToAsset.get(url); } /** * Load the asset's file from a remote source. Listen for `load` events on the asset to find * out when it is loaded. * * @param {Asset} asset - The asset to load. * @param {object} [options] - Options for asset loading. * @param {boolean} [options.bundlesIgnore] - If set to true, then asset will not try to load * from a bundle. Defaults to false. * @param {boolean} [options.force] - If set to true, then the check of asset being loaded or * is already loaded is bypassed, which forces loading of asset regardless. * @param {BundlesFilterCallback} [options.bundlesFilter] - A callback that will be called * when loading an asset that is contained in any of the bundles. It provides an array of * bundles and will ensure asset is loaded from bundle returned from a callback. By default, * the smallest filesize bundle is chosen. * @example * // load some assets * const assetsToLoad = [ * app.assets.find("My Asset"), * app.assets.find("Another Asset") * ]; * let count = 0; * assetsToLoad.forEach((assetToLoad) => { * assetToLoad.ready((asset) => { * count++; * if (count === assetsToLoad.length) { * // done * } * }); * app.assets.load(assetToLoad); * }); */ load(asset, options) { if ((asset.loading || asset.loaded) && !options?.force) { return; } const file = asset.file; const _fireLoad = () => { this.fire("load", asset); this.fire(`load:${asset.id}`, asset); if (file && file.url) { this.fire(`load:url:${file.url}`, asset); } asset.fire("load", asset); }; const _opened = (resource) => { if (resource instanceof Array) { asset.resources = resource; } else { asset.resource = resource; } this._loader.patch(asset, this); if (asset.type === "bundle") { const assetIds = asset.data.assets; for (let i = 0; i < assetIds.length; i++) { const assetInBundle = this._idToAsset.get(assetIds[i]); if (assetInBundle && !assetInBundle.loaded) { this.load(assetInBundle, { force: true }); } } if (asset.resource.loaded) { _fireLoad(); } else { this.fire("load:start", asset); this.fire(`load:start:${asset.id}`, asset); if (file && file.url) { this.fire(`load:start:url:${file.url}`, asset); } asset.fire("load:start", asset); asset.resource.on("load", _fireLoad); } } else { _fireLoad(); } }; const _loaded = (err, resource, extra) => { asset.loaded = true; asset.loading = false; if (err) { this.fire("error", err, asset); this.fire(`error:${asset.id}`, err, asset); asset.fire("error", err, asset); } else { if (asset.type === "script") { const handler = this._loader.getHandler("script"); if (handler._cache[asset.id] && handler._cache[asset.id].parentNode === document.head) { document.head.removeChild(handler._cache[asset.id]); } if (extra) { handler._cache[asset.id] = extra; } } _opened(resource); } }; if (file || asset.type === "cubemap") { this.fire("load:start", asset); this.fire(`load:${asset.id}:start`, asset); asset.loading = true; const fileUrl = asset.getFileUrl(); if (asset.type === "bundle") { const assetIds = asset.data.assets; for (let i = 0; i < assetIds.length; i++) { const assetInBundle = this._idToAsset.get(assetIds[i]); if (!assetInBundle) { continue; } if (assetInBundle.loaded || assetInBundle.resource || assetInBundle.loading) { continue; } assetInBundle.loading = true; } } this._loader.load(fileUrl, asset.type, _loaded, asset, options); } else { const resource = this._loader.open(asset.type, asset.data); asset.loaded = true; _opened(resource); } } /** * Use this to load and create an asset if you don't have assets created. Usually you would * only use this if you are not integrated with the PlayCanvas Editor. * * @param {string} url - The url to load. * @param {string} type - The type of asset to load. * @param {LoadAssetCallback} callback - Function called when asset is loaded, passed (err, * asset), where err is null if no errors were encountered. * @example * app.assets.loadFromUrl("../path/to/texture.jpg", "texture", function (err, asset) { * const texture = asset.resource; * }); */ loadFromUrl(url, type, callback) { this.loadFromUrlAndFilename(url, null, type, callback); } /** * Use this to load and create an asset when both the URL and filename are required. For * example, use this function when loading BLOB assets, where the URL does not adequately * identify the file. * * @param {string} url - The url to load. * @param {string} filename - The filename of the asset to load. * @param {string} type - The type of asset to load. * @param {LoadAssetCallback} callback - Function called when asset is loaded, passed (err, * asset), where err is null if no errors were encountered. * @example * const file = magicallyObtainAFile(); * app.assets.loadFromUrlAndFilename(URL.createObjectURL(file), "texture.png", "texture", function (err, asset) { * const texture = asset.resource; * }); */ loadFromUrlAndFilename(url, filename, type, callback) { const name = path.getBasename(filename || url); const file = { filename: filename || name, url }; let asset = this.getByUrl(url); if (!asset) { asset = new Asset(name, type, file); this.add(asset); } else if (asset.loaded) { callback(asset.loadFromUrlError || null, asset); return; } const startLoad = (asset2) => { asset2.once("load", (loadedAsset) => { if (type === "material") { this._loadTextures(loadedAsset, (err, textures) => { callback(err, loadedAsset); }); } else { callback(null, loadedAsset); } }); asset2.once("error", (err) => { if (err) { this.loadFromUrlError = err; } callback(err, asset2); }); this.load(asset2); }; if (asset.resource) { callback(null, asset); } else if (type === "model") { this._loadModel(asset, startLoad); } else { startLoad(asset); } } // private method used for engine-only loading of model data _loadModel(modelAsset, continuation) { const url = modelAsset.getFileUrl(); const ext = path.getExtension(url); if (ext === ".json" || ext === ".glb") { const dir = path.getDirectory(url); const basename = path.getBasename(url); const mappingUrl = path.join(dir, basename.replace(ext, ".mapping.json")); this._loader.load(mappingUrl, "json", (err, data) => { if (err) { modelAsset.data = { mapping: [] }; continuation(modelAsset); } else { this._loadMaterials(modelAsset, data, (e, materials) => { modelAsset.data = data; continuation(modelAsset); }); } }); } else { continuation(modelAsset); } } // private method used for engine-only loading of model materials _loadMaterials(modelAsset, mapping, callback) { const materials = []; let count = 0; const onMaterialLoaded = (err, materialAsset) => { this._loadTextures(materialAsset, (err2, textures) => { materials.push(materialAsset); if (materials.length === count) { callback(null, materials); } }); }; for (let i = 0; i < mapping.mapping.length; i++) { const path2 = mapping.mapping[i].path; if (path2) { count++; const url = modelAsset.getAbsoluteUrl(path2); this.loadFromUrl(url, "material", onMaterialLoaded); } } if (count === 0) { callback(null, materials); } } // private method used for engine-only loading of the textures referenced by // the material asset _loadTextures(materialAsset, callback) { const textures = []; let count = 0; const data = materialAsset.data; if (data.mappingFormat !== "path") { Debug.warn(`Skipping: ${materialAsset.name}, material files must be mappingFormat: "path" to be loaded from URL`); callback(null, textures); return; } const onTextureLoaded = (err, texture) => { if (err) console.error(`Failed to load material texture for "${materialAsset.name}": ${err?.message ?? err}`, err); textures.push(texture); if (textures.length === count) { callback(null, textures); } }; const texParams = standardMaterialTextureParameters; for (let i = 0; i < texParams.length; i++) { const path2 = data[texParams[i]]; if (path2 && typeof path2 === "string") { count++; const url = materialAsset.getAbsoluteUrl(path2); this.loadFromUrl(url, "texture", onTextureLoaded); } } if (count === 0) { callback(null, textures); } } _onTagAdd(tag, asset) { this._tags.add(tag, asset); } _onTagRemove(tag, asset) { this._tags.remove(tag, asset); } _onNameChange(asset, name, nameOld) { if (this._nameToAsset.has(nameOld)) { const items = this._nameToAsset.get(nameOld); items.delete(asset); if (items.size === 0) { this._nameToAsset.delete(nameOld); } } if (!this._nameToAsset.has(asset.name)) { this._nameToAsset.set(asset.name, /* @__PURE__ */ new Set()); } this._nameToAsset.get(asset.name).add(asset); } /** * Return all Assets that satisfy the search query. Query can be simply a string, or comma * separated strings, to have inclusive results of assets that match at least one query. A * query that consists of an array of tags can be used to match assets that have each tag of * array. * * @param {...*} query - Name of a tag or array of tags. * @returns {Asset[]} A list of all Assets matched query. * @example * const assets = app.assets.findByTag("level-1"); * // returns all assets that tagged by `level-1` * @example * const assets = app.assets.findByTag("level-1", "level-2"); * // returns all assets that tagged by `level-1` OR `level-2` * @example * const assets = app.assets.findByTag(["level-1", "monster"]); * // returns all assets that tagged by `level-1` AND `monster` * @example * const assets = app.assets.findByTag(["level-1", "monster"], ["level-2", "monster"]); * // returns all assets that tagged by (`level-1` AND `monster`) OR (`level-2` AND `monster`) */ findByTag(...query) { return this._tags.find(query); } /** * Return all Assets that satisfy a filter callback. * * @param {FilterAssetCallback} callback - The callback function that is used to filter assets. * Return `true` to include an asset in the returned array. * @returns {Asset[]} A list of all Assets found. * @example * const assets = app.assets.filter(asset => asset.name.includes('monster')); * console.log(`Found ${assets.length} assets with a name containing 'monster'`); */ filter(callback) { return Array.from(this._assets).filter((asset) => callback(asset)); } /** * Return the first Asset with the specified name and type found in the registry. * * @param {string} name - The name of the Asset to find. * @param {string} [type] - The type of the Asset to find. * @returns {Asset|null} A single Asset or null if no Asset is found. * @example * const asset = app.assets.find("myTextureAsset", "texture"); */ find(name, type) { const items = this._nameToAsset.get(name); if (!items) return null; for (const asset of items) { if (!type || asset.type === type) { return asset; } } return null; } /** * Return all Assets with the specified name and type found in the registry. * * @param {string} name - The name of the Assets to find. * @param {string} [type] - The type of the Assets to find. * @returns {Asset[]} A list of all Assets found. * @example * const assets = app.assets.findAll('brick', 'texture'); * console.log(`Found ${assets.length} texture assets named 'brick'`); */ findAll(name, type) { const items = this._nameToAsset.get(name); if (!items) return []; const results = Array.from(items); if (!type) return results; return results.filter((asset) => asset.type === type); } /** * Logs all assets in the registry to the console. Used for debugging with TRACEID_ASSETS. * * @ignore */ log() { if (!Tracing.get(TRACEID_ASSETS)) return; const assets = this.list(); Debug.trace(TRACEID_ASSETS, `Assets: ${assets.length}`); const byType = {}; let loadedCount = 0; let loadingCount = 0; assets.forEach((asset, index) => { byType[asset.type] = (byType[asset.type] || 0) + 1; if (asset.loaded) loadedCount++; else if (asset.loading) loadingCount++; const status = asset.loaded ? "loaded" : asset.loading ? "loading" : "pending"; const url = asset.file?.url; const urlPart = url && url !== asset.name ? ` ${url}` : ""; Debug.trace(TRACEID_ASSETS, `${index}. ID:${asset.id} [${asset.type}] "${asset.name}" ${status}${urlPart}`); }); const pendingCount = assets.length - loadedCount - loadingCount; Debug.trace(TRACEID_ASSETS, `Status: ${loadedCount} loaded, ${loadingCount} loading, ${pendingCount} pending`); Debug.trace(TRACEID_ASSETS, `Types: ${Object.entries(byType).map(([type, count]) => `${type}:${count}`).join(", ")}`); } } /** * Fired when an asset completes loading. This event is available in three forms. They are as * follows: * * 1. `load` - Fired when any asset finishes loading. * 2. `load:[id]` - Fired when a specific asset has finished loading, where `[id]` is the * unique id of the asset. * 3. `load:url:[url]` - Fired when an asset finishes loading whose URL matches `[url]`, where * `[url]` is the URL of the asset. * * @event * @example * app.assets.on('load', (asset) => { * console.log(`Asset loaded: ${asset.name}`); * }); * @example * const id = 123456; * const asset = app.assets.get(id); * app.assets.on('load:' + id, (asset) => { * console.log(`Asset loaded: ${asset.name}`); * }); * app.assets.load(asset); * @example * const id = 123456; * const asset = app.assets.get(id); * app.assets.on('load:url:' + asset.file.url, (asset) => { * console.log(`Asset loaded: ${asset.name}`); * }); * app.assets.load(asset); */ __publicField(AssetRegistry, "EVENT_LOAD", "load"); /** * Fired when an asset is added to the registry. This event is available in three forms. They * are as follows: * * 1. `add` - Fired when any asset is added to the registry. * 2. `add:[id]` - Fired when an asset is added to the registry, where `[id]` is the unique id * of the asset. * 3. `add:url:[url]` - Fired when an asset is added to the registry and matches the URL * `[url]`, where `[url]` is the URL of the asset. * * @event * @example * app.assets.on('add', (asset) => { * console.log(`Asset added: ${asset.name}`); * }); * @example * const id = 123456; * app.assets.on('add:' + id, (asset) => { * console.log(`Asset added: ${asset.name}`); * }); * @example * const id = 123456; * const asset = app.assets.get(id); * app.assets.on('add:url:' + asset.file.url, (asset) => { * console.log(`Asset added: ${asset.name}`); * }); */ __publicField(AssetRegistry, "EVENT_ADD", "add"); /** * Fired when an asset is removed from the registry. This event is available in three forms. * They are as follows: * * 1. `remove` - Fired when any asset is removed from the registry. * 2. `remove:[id]` - Fired when an asset is removed from the registry, where `[id]` is the * unique id of the asset. * 3. `remove:url:[url]` - Fired when an asset is removed from the registry and matches the * URL `[url]`, where `[url]` is the URL of the asset. * * @event * @param {Asset} asset - The asset that was removed. * @example * app.assets.on('remove', (asset) => { * console.log(`Asset removed: ${asset.name}`); * }); * @example * const id = 123456; * app.assets.on('remove:' + id, (asset) => { * console.log(`Asset removed: ${asset.name}`); * }); * @example * const id = 123456; * const asset = app.assets.get(id); * app.assets.on('remove:url:' + asset.file.url, (asset) => { * console.log(`Asset removed: ${asset.name}`); * }); */ __publicField(AssetRegistry, "EVENT_REMOVE", "remove"); /** * Fired when an error occurs during asset loading. This event is available in two forms. They * are as follows: * * 1. `error` - Fired when any asset reports an error in loading. * 2. `error:[id]` - Fired when an asset reports an error in loading, where `[id]` is the * unique id of the asset. * * @event * @example * const id = 123456; * const asset = app.assets.get(id); * app.assets.on('error', (err, asset) => { * console.error(err); * }); * app.assets.load(asset); * @example * const id = 123456; * const asset = app.assets.get(id); * app.assets.on('error:' + id, (err, asset) => { * console.error(err); * }); * app.assets.load(asset); */ __publicField(AssetRegistry, "EVENT_ERROR", "error"); export { AssetRegistry };