Skip to content

Commit

Permalink
Bundles Refactor (#5675)
Browse files Browse the repository at this point in the history
* wip

* Bundles refactor

* fix jsdoc

* @internal > @ignore

* PR comments

* engine-only example for bundle loading

* update bundle example to new examples format

* edits based on PR review comments

* lint, fix

* events docs

* lazy initialize TextDecoder

* example update

* linter

---------

Co-authored-by: KPal <48248865+kpal81xd@users.noreply.github.com>
  • Loading branch information
Maksims and kpal81xd committed Mar 7, 2024
1 parent ea671d1 commit 9d17f0d
Show file tree
Hide file tree
Showing 24 changed files with 998 additions and 837 deletions.
Binary file added examples/assets/bundles/bundle.tar
Binary file not shown.
Binary file added examples/bundle.tar
Binary file not shown.
6 changes: 6 additions & 0 deletions examples/src/examples/loaders/bundle/config.mjs
@@ -0,0 +1,6 @@
/**
* @type {import('../../../../types.mjs').ExampleConfig}
*/
export default {
WEBGPU_ENABLED: true
};
116 changes: 116 additions & 0 deletions examples/src/examples/loaders/bundle/example.mjs
@@ -0,0 +1,116 @@
import * as pc from 'playcanvas';
import { deviceType, rootPath } from '@examples/utils';

// The example demonstrates loading multiple assets from a single bundle file

// This tar file has been created by a command line:
// : cd engine/examples/
// : tar cvf bundle.tar assets/models/geometry-camera-light.glb assets/models/torus.png

const canvas = document.getElementById('application-canvas');
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error('No canvas found');
}

const assets = {
bundle: new pc.Asset('bundle', 'bundle', { url: '/static/assets/bundles/bundle.tar' }),
scene: new pc.Asset('scene', 'container', { url: 'assets/models/geometry-camera-light.glb' }),
torus: new pc.Asset('torus', 'container', { url: 'assets/models/torus.glb' })
};

// Bundle should list asset IDs in its data
assets.bundle.data = { assets: [assets.scene.id, assets.torus.id] };

const gfxOptions = {
deviceTypes: [deviceType],
glslangUrl: rootPath + '/static/lib/glslang/glslang.js',
twgslUrl: rootPath + '/static/lib/twgsl/twgsl.js'
};

const device = await pc.createGraphicsDevice(canvas, gfxOptions);
const createOptions = new pc.AppOptions();
createOptions.graphicsDevice = device;

createOptions.componentSystems = [pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem];
createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler];

const app = new pc.AppBase(canvas);
app.init(createOptions);

// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

// Ensure canvas is resized when window changes size
const resize = () => app.resizeCanvas();
window.addEventListener('resize', resize);
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});

// load assets
// notice that scene and torus are loaded as blob's and only tar file is downloaded
const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {

app.start();

/**
* the array will store loaded cameras
* @type {pc.CameraComponent[]}
*/
let camerasComponents = null;

// glb lights use physical units
app.scene.physicalUnits = true;

// create an instance using render component
const entity = assets.scene.resource.instantiateRenderEntity();
app.root.addChild(entity);

// create an instance using render component
const entityTorus = assets.torus.resource.instantiateRenderEntity();
app.root.addChild(entityTorus);
entityTorus.setLocalPosition(0, 0, 2);

// find all cameras - by default they are disabled
camerasComponents = entity.findComponents("camera");
camerasComponents.forEach((component) => {

// set the aspect ratio to automatic to work with any window size
component.aspectRatioMode = pc.ASPECT_AUTO;

// set up exposure for physical units
component.aperture = 4;
component.shutter = 1 / 100;
component.sensitivity = 500;
});

/** @type {pc.LightComponent[]} */
const lightComponents = entity.findComponents("light");
lightComponents.forEach((component) => {
component.enabled = true;
});

let time = 0;
let activeCamera = 0;
app.on("update", function (dt) {
time -= dt;

entityTorus.rotateLocal(360 * dt, 0, 0);

// change the camera every few seconds
if (time <= 0) {
time = 2;

// disable current camera
camerasComponents[activeCamera].enabled = false;

// activate next camera
activeCamera = (activeCamera + 1) % camerasComponents.length;
camerasComponents[activeCamera].enabled = true;
}
});
});

export { app };
2 changes: 1 addition & 1 deletion src/core/event-handle.js
Expand Up @@ -97,7 +97,7 @@ class EventHandle {
/**
* Mark if event has been removed.
* @type {boolean}
* @internal
* @ignore
*/
set removed(value) {
if (!value) return;
Expand Down
84 changes: 76 additions & 8 deletions src/framework/asset/asset-registry.js
Expand Up @@ -26,6 +26,14 @@ import { Asset } from './asset.js';
* @param {Asset} [asset] - The loaded asset if no errors were encountered.
*/

/**
* Callback used by {@link ResourceLoader#load} and called when an asset is choosing a bundle
* to load from. Return a single bundle to ensure asset is loaded from it.
*
* @callback BundlesFilterCallback
* @param {import('../bundle/bundle.js').Bundle[]} bundles - List of bundles which contain the asset.
*/

/**
* Container for all assets that are available to this application. Note that PlayCanvas scripts
* are provided with an AssetRegistry instance as `app.assets`.
Expand Down Expand Up @@ -189,6 +197,13 @@ class AssetRegistry extends EventHandler {
*/
prefix = null;

/**
* BundleRegistry
*
* @type {import('../bundle/bundle-registry.js').BundleRegistry|null}
*/
bundles = null;

/**
* Create an instance of an AssetRegistry.
*
Expand Down Expand Up @@ -335,6 +350,15 @@ class AssetRegistry extends EventHandler {
* 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
* smallest filesize bundle is choosen.
* @example
* // load some assets
* const assetsToLoad = [
Expand All @@ -352,16 +376,24 @@ class AssetRegistry extends EventHandler {
* app.assets.load(assetToLoad);
* });
*/
load(asset) {
load(asset, options) {
// do nothing if asset is already loaded
// note: lots of code calls assets.load() assuming this check is present
// don't remove it without updating calls to assets.load() with checks for the asset.loaded state
if (asset.loading || asset.loaded) {
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);
};

// open has completed on the resource
const _opened = (resource) => {
if (resource instanceof Array) {
Expand All @@ -373,11 +405,28 @@ class AssetRegistry extends EventHandler {
// let handler patch the resource
this._loader.patch(asset, this);

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);
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();
}
};

// load has completed on the resource
Expand Down Expand Up @@ -409,7 +458,26 @@ class AssetRegistry extends EventHandler {
this.fire('load:' + asset.id + ':start', asset);

asset.loading = true;
this._loader.load(asset.getFileUrl(), asset.type, _loaded, asset);

const fileUrl = asset.getFileUrl();

// mark bundle assets as loading
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 {
// asset has no file to load, open it directly
const resource = this._loader.open(asset.type, asset.data);
Expand Down
9 changes: 8 additions & 1 deletion src/framework/asset/asset.js
Expand Up @@ -133,7 +133,7 @@ class Asset extends EventHandler {
*
* @param {string} name - A non-unique but human-readable name which can be later used to
* retrieve the asset.
* @param {string} type - Type of asset. One of ["animation", "audio", "binary", "container",
* @param {string} type - Type of asset. One of ["animation", "audio", "binary", "bundle", "container",
* "cubemap", "css", "font", "json", "html", "material", "model", "script", "shader", "sprite",
* "template", text", "texture", "textureatlas"]
* @param {object} [file] - Details about the file the asset is made from. At the least must
Expand Down Expand Up @@ -197,6 +197,8 @@ class Asset extends EventHandler {
// This is where the loaded resource(s) will be
this._resources = [];

this.urlObject = null;

// a string-assetId dictionary that maps
// locale to asset id
this._i18n = {};
Expand Down Expand Up @@ -533,6 +535,11 @@ class Asset extends EventHandler {

const old = this._resources;

if (this.urlObject) {
URL.revokeObjectURL(this.urlObject);
this.urlObject = null;
}

// clear resources on the asset
this.resources = [];
this.loaded = false;
Expand Down

0 comments on commit 9d17f0d

Please sign in to comment.