playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,325 lines • 61 kB
JavaScript
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 { version, revision } from "../core/core.js";
import { platform } from "../core/platform.js";
import { now } from "../core/time.js";
import { path } from "../core/path.js";
import { TRACEID_RENDER_FRAME, TRACEID_RENDER_FRAME_TIME } from "../core/constants.js";
import { Debug } from "../core/debug.js";
import { EventHandler } from "../core/event-handler.js";
import { Color } from "../core/math/color.js";
import { Mat4 } from "../core/math/mat4.js";
import { math } from "../core/math/math.js";
import { Quat } from "../core/math/quat.js";
import { Vec3 } from "../core/math/vec3.js";
import {
CULLFACE_NONE,
SHADERLANGUAGE_GLSL,
SHADERLANGUAGE_WGSL
} from "../platform/graphics/constants.js";
import { DebugGraphics } from "../platform/graphics/debug-graphics.js";
import { http } from "../platform/net/http.js";
import {
LAYERID_DEPTH,
LAYERID_IMMEDIATE,
LAYERID_SKYBOX,
LAYERID_UI,
LAYERID_WORLD,
SORTMODE_NONE,
SORTMODE_MANUAL
} from "../scene/constants.js";
import { setProgramLibrary } from "../scene/shader-lib/get-program-library.js";
import { ProgramLibrary } from "../scene/shader-lib/program-library.js";
import { ForwardRenderer } from "../scene/renderer/forward-renderer.js";
import { FrameGraph } from "../scene/frame-graph.js";
import { AreaLightLuts } from "../scene/area-light-luts.js";
import { Layer } from "../scene/layer.js";
import { LayerComposition } from "../scene/composition/layer-composition.js";
import { Scene } from "../scene/scene.js";
import { ShaderMaterial } from "../scene/materials/shader-material.js";
import { StandardMaterial } from "../scene/materials/standard-material.js";
import { setDefaultMaterial } from "../scene/materials/default-material.js";
import {
FILLMODE_FILL_WINDOW,
FILLMODE_KEEP_ASPECT,
RESOLUTION_AUTO,
RESOLUTION_FIXED
} from "./constants.js";
import { Asset } from "./asset/asset.js";
import { AssetRegistry } from "./asset/asset-registry.js";
import { BundleRegistry } from "./bundle/bundle-registry.js";
import { ComponentSystemRegistry } from "./components/registry.js";
import { BundleHandler } from "./handlers/bundle.js";
import { ResourceLoader } from "./handlers/loader.js";
import { I18n } from "./i18n/i18n.js";
import { ScriptRegistry } from "./script/script-registry.js";
import { Entity } from "./entity.js";
import { SceneRegistry } from "./scene-registry.js";
import { script } from "./script.js";
import { ApplicationStats } from "./stats.js";
import { getApplication, setApplication } from "./globals.js";
import { shaderChunksGLSL } from "../scene/shader-lib/glsl/collections/shader-chunks-glsl.js";
import { shaderChunksWGSL } from "../scene/shader-lib/wgsl/collections/shader-chunks-wgsl.js";
import { ShaderChunks } from "../scene/shader-lib/shader-chunks.js";
let app = null;
const _AppBase = class _AppBase extends EventHandler {
/**
* Create a new AppBase instance.
*
* @param {HTMLCanvasElement | OffscreenCanvas} canvas - The canvas element.
* @example
* const app = new pc.AppBase(canvas);
*
* const options = new AppOptions();
* app.init(options);
*
* // Start the application's main loop
* app.start();
*/
constructor(canvas) {
super();
/**
* The application's batch manager.
*
* @type {BatchManager|null}
* @private
*/
__publicField(this, "_batcher", null);
/** @private */
__publicField(this, "_destroyRequested", false);
/** @private */
__publicField(this, "_inFrameUpdate", false);
/** @private */
__publicField(this, "_librariesLoaded", false);
/** @private */
__publicField(this, "_fillMode", FILLMODE_KEEP_ASPECT);
/** @private */
__publicField(this, "_resolutionMode", RESOLUTION_FIXED);
/** @private */
__publicField(this, "_allowResize", true);
/**
* @type {Asset|null}
* @private
*/
__publicField(this, "_skyboxAsset", null);
/**
* @type {SoundManager}
* @private
*/
__publicField(this, "_soundManager");
/** @private */
__publicField(this, "_visibilityChangeHandler");
/**
* Stores all entities that have been created for this app by guid.
*
* @type {Object<string, Entity>}
* @ignore
*/
__publicField(this, "_entityIndex", {});
/** @ignore */
__publicField(this, "_inTools", false);
/** @ignore */
__publicField(this, "_scriptPrefix", "");
/** @ignore */
__publicField(this, "_time", 0);
/**
* Set this to false if you want to run without using bundles. We set it to true only if
* TextDecoder is available because we currently rely on it for untarring.
*
* @ignore
*/
__publicField(this, "enableBundles", typeof TextDecoder !== "undefined");
/**
* A request id returned by requestAnimationFrame, allowing us to cancel it.
*
* @ignore
*/
__publicField(this, "frameRequestId");
/**
* Main loop tick, invoked by `requestAnimationFrame` each frame. Bound to this instance so
* it can be passed directly to `requestAnimationFrame`. Subclasses may replace this with
* their own tick function.
*
* @param {number} [timestamp] - The timestamp supplied by requestAnimationFrame.
* @param {XRFrame} [xrFrame] - XRFrame from requestAnimationFrame callback.
* @ignore
*/
__publicField(this, "tick", (timestamp, xrFrame) => {
if (!this.graphicsDevice) {
return;
}
if (this.frameRequestId) {
this.xr?.session?.cancelAnimationFrame(this.frameRequestId);
cancelAnimationFrame(this.frameRequestId);
this.frameRequestId = null;
}
this._inFrameUpdate = true;
setApplication(this);
app = this;
const currentTime = this._processTimestamp(timestamp) || now();
const ms = currentTime - (this._time || currentTime);
let dt = ms / 1e3;
dt = math.clamp(dt, 0, this.maxDeltaTime);
dt *= this.timeScale;
this._time = currentTime;
this.requestAnimationFrame();
if (this.graphicsDevice.contextLost) {
return;
}
this.stats.updateBasic(currentTime, dt, ms, this.renderer, this.graphicsDevice);
this.stats.updateDetailed(this.renderer, this.graphicsDevice);
this.fire("frameupdate", ms);
let skipUpdate = false;
if (xrFrame) {
skipUpdate = !this.xr?.update(xrFrame);
}
if (!skipUpdate) {
Debug.trace(TRACEID_RENDER_FRAME, `---- Frame ${this.frame}`);
Debug.trace(TRACEID_RENDER_FRAME_TIME, `-- UpdateStart ${now().toFixed(2)}ms`);
this.update(dt);
this.fire("framerender");
if (this.autoRender || this.renderNextFrame) {
Debug.trace(TRACEID_RENDER_FRAME_TIME, `-- RenderStart ${now().toFixed(2)}ms`);
this.render();
this.renderNextFrame = false;
Debug.trace(TRACEID_RENDER_FRAME_TIME, `-- RenderEnd ${now().toFixed(2)}ms`);
}
this.fire("frameend");
this.stats.frameEnd();
}
this._inFrameUpdate = false;
if (this._destroyRequested) {
this.destroy();
}
});
/**
* Scales the global time delta. Defaults to 1.
*
* @example
* // Set the app to run at half speed
* this.app.timeScale = 0.5;
*/
__publicField(this, "timeScale", 1);
/**
* Clamps per-frame delta time to an upper bound. Useful since returning from a tab
* deactivation can generate huge values for dt, which can adversely affect game state.
* Defaults to 0.1 (seconds).
*
* @type {number}
* @example
* // Don't clamp inter-frame times of 200ms or less
* this.app.maxDeltaTime = 0.2;
*/
__publicField(this, "maxDeltaTime", 0.1);
// Maximum delta is 0.1s or 10 fps.
/**
* The total number of frames the application has updated since start() was called.
*
* @ignore
*/
__publicField(this, "frame", 0);
/**
* The frame graph.
*
* @type {FrameGraph}
* @ignore
*/
__publicField(this, "frameGraph", new FrameGraph());
/**
* The forward renderer.
*
* @type {ForwardRenderer}
* @ignore
*/
__publicField(this, "renderer");
/**
* Scripts in order of loading first.
*
* @type {string[]}
*/
__publicField(this, "scriptsOrder", []);
/**
* The application's performance stats.
*
* @type {ApplicationStats}
* @ignore
*/
__publicField(this, "stats");
/**
* When true, the application's render function is called every frame. Setting autoRender to
* false is useful to applications where the rendered image may often be unchanged over time.
* This can heavily reduce the application's load on the CPU and GPU. Defaults to true.
*
* @example
* // Disable rendering every frame and only render on a keydown event
* this.app.autoRender = false;
* this.app.keyboard.on('keydown', (event) => {
* this.app.renderNextFrame = true;
* });
*/
__publicField(this, "autoRender", true);
/**
* Set to true to render the scene on the next iteration of the main loop. This only has an
* effect if {@link autoRender} is set to false. The value of renderNextFrame is set back to
* false again as soon as the scene has been rendered.
*
* @example
* // Render the scene only while space key is pressed
* if (this.app.keyboard.isPressed(pc.KEY_SPACE)) {
* this.app.renderNextFrame = true;
* }
*/
__publicField(this, "renderNextFrame", false);
/**
* The graphics device used by the application.
*
* @type {GraphicsDevice}
*/
__publicField(this, "graphicsDevice");
/**
* The root entity of the application.
*
* @type {Entity}
* @example
* // Return the first entity called 'Camera' in a depth-first search of the scene hierarchy
* const camera = this.app.root.findByName('Camera');
*/
__publicField(this, "root");
/**
* The scene managed by the application.
*
* @type {Scene}
* @example
* // Set the fog type property of the application's scene
* this.app.scene.fog.type = pc.FOG_LINEAR;
*/
__publicField(this, "scene");
/**
* The run-time lightmapper.
*
* @type {Lightmapper|null}
*/
__publicField(this, "lightmapper", null);
/**
* The resource loader.
*
* @type {ResourceLoader}
*/
__publicField(this, "loader", new ResourceLoader(this));
/**
* The asset registry managed by the application.
*
* @type {AssetRegistry}
* @example
* // Search the asset registry for all assets with the tag 'vehicle'
* const vehicleAssets = this.app.assets.findByTag('vehicle');
*/
__publicField(this, "assets");
/**
* The bundle registry managed by the application.
*
* @type {BundleRegistry}
* @ignore
*/
__publicField(this, "bundles");
/**
* The scene registry managed by the application.
*
* @type {SceneRegistry}
* @example
* // Search the scene registry for a item with the name 'racetrack1'
* const sceneItem = this.app.scenes.find('racetrack1');
*
* // Load the scene using the item's url
* this.app.scenes.loadScene(sceneItem.url);
*/
__publicField(this, "scenes", new SceneRegistry(this));
/**
* The application's script registry.
*
* @type {ScriptRegistry}
*/
__publicField(this, "scripts", new ScriptRegistry(this));
/**
* The application's component system registry.
*
* @type {ComponentSystemRegistry}
* @example
* // Set global gravity to zero
* this.app.systems.rigidbody.gravity.set(0, 0, 0);
* @example
* // Set the global sound volume to 50%
* this.app.systems.sound.volume = 0.5;
*/
__publicField(this, "systems", new ComponentSystemRegistry());
/**
* Handles localization.
*
* @type {I18n}
*/
__publicField(this, "i18n", new I18n(this));
/**
* The keyboard device.
*
* @type {Keyboard|null}
*/
__publicField(this, "keyboard", null);
/**
* The mouse device.
*
* @type {Mouse|null}
*/
__publicField(this, "mouse", null);
/**
* Used to get touch events input.
*
* @type {TouchDevice|null}
*/
__publicField(this, "touch", null);
/**
* Used to access GamePad input.
*
* @type {GamePads|null}
*/
__publicField(this, "gamepads", null);
/**
* Used to handle input for {@link ElementComponent}s.
*
* @type {ElementInput|null}
*/
__publicField(this, "elementInput", null);
/**
* The XR Manager that provides ability to start VR/AR sessions.
*
* @type {XrManager|null}
* @example
* // check if VR is available
* if (app.xr.isAvailable(pc.XRTYPE_VR)) {
* // VR is available
* }
*/
__publicField(this, "xr", null);
if (version?.indexOf("$") < 0) {
Debug.log(`Powered by PlayCanvas ${version} ${revision}`);
}
_AppBase._applications[canvas.id] = this;
setApplication(this);
app = this;
this.root = new Entity();
this.root._enabledInHierarchy = true;
}
/**
* Initialize the app.
*
* @param {AppOptions} appOptions - Options specifying the init parameters for the app.
*/
init(appOptions) {
const {
assetPrefix,
batchManager,
componentSystems,
elementInput,
gamepads,
graphicsDevice,
keyboard,
lightmapper,
mouse,
resourceHandlers,
scriptsOrder,
scriptPrefix,
soundManager,
touch,
xr
} = appOptions;
Debug.assert(graphicsDevice, "The application cannot be created without a valid GraphicsDevice");
this.graphicsDevice = graphicsDevice;
ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_GLSL).add(shaderChunksGLSL);
ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_WGSL).add(shaderChunksWGSL);
this._initDefaultMaterial();
this._initProgramLibrary();
this.stats = new ApplicationStats(graphicsDevice);
this._soundManager = soundManager;
this.scene = new Scene(graphicsDevice);
this._registerSceneImmediate(this.scene);
this.assets = new AssetRegistry(this.loader);
if (assetPrefix) this.assets.prefix = assetPrefix;
this.bundles = new BundleRegistry(this.assets);
this.scriptsOrder = scriptsOrder || [];
this.defaultLayerWorld = new Layer({ name: "World", id: LAYERID_WORLD });
this.defaultLayerDepth = new Layer({ name: "Depth", id: LAYERID_DEPTH, enabled: false, opaqueSortMode: SORTMODE_NONE });
this.defaultLayerSkybox = new Layer({ name: "Skybox", id: LAYERID_SKYBOX, opaqueSortMode: SORTMODE_NONE });
this.defaultLayerUi = new Layer({ name: "UI", id: LAYERID_UI, transparentSortMode: SORTMODE_MANUAL });
this.defaultLayerImmediate = new Layer({ name: "Immediate", id: LAYERID_IMMEDIATE, opaqueSortMode: SORTMODE_NONE });
const defaultLayerComposition = new LayerComposition("default");
defaultLayerComposition.pushOpaque(this.defaultLayerWorld);
defaultLayerComposition.pushOpaque(this.defaultLayerDepth);
defaultLayerComposition.pushOpaque(this.defaultLayerSkybox);
defaultLayerComposition.pushTransparent(this.defaultLayerWorld);
defaultLayerComposition.pushOpaque(this.defaultLayerImmediate);
defaultLayerComposition.pushTransparent(this.defaultLayerImmediate);
defaultLayerComposition.pushTransparent(this.defaultLayerUi);
this.scene.layers = defaultLayerComposition;
AreaLightLuts.createPlaceholder(graphicsDevice);
this.renderer = new ForwardRenderer(graphicsDevice, this.scene);
if (lightmapper) {
this.lightmapper = new lightmapper(graphicsDevice, this.root, this.scene, this.renderer, this.assets);
this.once("prerender", this._firstBake, this);
}
if (batchManager) {
this._batcher = new batchManager(graphicsDevice, this.root, this.scene);
this.once("prerender", this._firstBatch, this);
}
this.keyboard = keyboard || null;
this.mouse = mouse || null;
this.touch = touch || null;
this.gamepads = gamepads || null;
if (elementInput) {
this.elementInput = elementInput;
this.elementInput.app = this;
}
this.xr = xr ? new xr(this) : null;
if (this.elementInput) this.elementInput.attachSelectEvents();
this._scriptPrefix = scriptPrefix || "";
if (this.enableBundles) {
this.loader.addHandler("bundle", new BundleHandler(this));
}
resourceHandlers.forEach((resourceHandler) => {
const handler = new resourceHandler(this);
this.loader.addHandler(handler.handlerType, handler);
});
this.loader.enableRetry();
componentSystems.forEach((componentSystem) => {
this.systems.add(new componentSystem(this));
});
this._visibilityChangeHandler = this.onVisibilityChange.bind(this);
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", this._visibilityChangeHandler, false);
}
}
/**
* Get the current application. In the case where there are multiple running applications, the
* function can get an application based on a supplied canvas id. This function is particularly
* useful when the current Application is not readily available. For example, in the JavaScript
* console of the browser's developer tools.
*
* @param {string} [id] - If defined, the returned application should use the canvas which has
* this id. Otherwise current application will be returned.
* @returns {AppBase|undefined} The running application, if any.
* @example
* const app = pc.AppBase.getApplication();
*/
static getApplication(id) {
return id ? _AppBase._applications[id] : getApplication();
}
/** @private */
_initDefaultMaterial() {
const material = new StandardMaterial();
material.name = "Default Material";
setDefaultMaterial(this.graphicsDevice, material);
}
/** @private */
_initProgramLibrary() {
const library = new ProgramLibrary(this.graphicsDevice, new StandardMaterial());
setProgramLibrary(this.graphicsDevice, library);
}
/**
* @type {SoundManager}
* @ignore
*/
get soundManager() {
return this._soundManager;
}
/**
* The application's batch manager. The batch manager is used to merge mesh instances in
* the scene, which reduces the overall number of draw calls, thereby boosting performance.
*
* @type {BatchManager}
*/
get batcher() {
Debug.assert(this._batcher, "BatchManager has not been created and is required for correct functionality.");
return this._batcher;
}
/**
* The current fill mode of the canvas. Can be:
*
* - {@link FILLMODE_NONE}: the canvas will always match the size provided.
* - {@link FILLMODE_FILL_WINDOW}: the canvas will simply fill the window, changing aspect ratio.
* - {@link FILLMODE_KEEP_ASPECT}: the canvas will grow to fill the window as best it can while
* maintaining the aspect ratio.
*
* @type {string}
*/
get fillMode() {
return this._fillMode;
}
/**
* The current resolution mode of the canvas, Can be:
*
* - {@link RESOLUTION_AUTO}: if width and height are not provided, canvas will be resized to
* match canvas client size.
* - {@link RESOLUTION_FIXED}: resolution of canvas will be fixed.
*
* @type {string}
*/
get resolutionMode() {
return this._resolutionMode;
}
/**
* Load the application configuration file and apply application properties and fill the asset
* registry.
*
* @param {string} url - The URL of the configuration file to load.
* @param {ConfigureAppCallback} callback - The Function called when the configuration file is
* loaded and parsed (or an error occurs).
*/
configure(url, callback) {
http.get(url, (err, response) => {
if (err) {
callback(err);
return;
}
const props = response.application_properties;
const scenes = response.scenes;
const assets = response.assets;
this._parseApplicationProperties(props, (err2) => {
this._parseScenes(scenes);
this._parseAssets(assets);
if (!err2) {
callback(null);
} else {
callback(err2);
}
});
});
}
/**
* Load all assets in the asset registry that are marked as 'preload'.
*
* @param {PreloadAppCallback} callback - Function called when all assets are loaded.
*/
preload(callback) {
this.fire("preload:start");
const assets = this.assets.list({
preload: true
});
if (assets.length === 0) {
this.fire("preload:end");
callback();
return;
}
let loadedCount = 0;
const onAssetLoadOrError = () => {
loadedCount++;
this.fire("preload:progress", loadedCount / assets.length);
if (loadedCount === assets.length) {
this.fire("preload:end");
callback();
}
};
assets.forEach((asset) => {
if (!asset.loaded) {
asset.once("load", onAssetLoadOrError);
asset.once("error", onAssetLoadOrError);
this.assets.load(asset);
} else {
onAssetLoadOrError();
}
});
}
_preloadScripts(sceneData, callback) {
callback();
}
// set application properties from data file
_parseApplicationProperties(props, callback) {
if (typeof props.maxAssetRetries === "number" && props.maxAssetRetries > 0) {
this.loader.enableRetry(props.maxAssetRetries);
}
if (!props.useDevicePixelRatio) {
props.useDevicePixelRatio = props.use_device_pixel_ratio;
}
if (!props.resolutionMode) {
props.resolutionMode = props.resolution_mode;
}
if (!props.fillMode) {
props.fillMode = props.fill_mode;
}
this._width = props.width;
this._height = props.height;
if (props.useDevicePixelRatio) {
this.graphicsDevice.maxPixelRatio = window.devicePixelRatio;
}
this.setCanvasResolution(props.resolutionMode, this._width, this._height);
this.setCanvasFillMode(props.fillMode, this._width, this._height);
if (props.layers && props.layerOrder) {
const composition = new LayerComposition("application");
const layers = {};
for (const key in props.layers) {
const data = props.layers[key];
data.id = parseInt(key, 10);
data.enabled = data.id !== LAYERID_DEPTH;
layers[key] = new Layer(data);
}
for (let i = 0, len = props.layerOrder.length; i < len; i++) {
const sublayer = props.layerOrder[i];
const layer = layers[sublayer.layer];
if (!layer) continue;
if (sublayer.transparent) {
composition.pushTransparent(layer);
} else {
composition.pushOpaque(layer);
}
composition.subLayerEnabled[i] = sublayer.enabled;
}
this.scene.layers = composition;
}
if (props.batchGroups) {
const batcher = this.batcher;
if (batcher) {
for (let i = 0, len = props.batchGroups.length; i < len; i++) {
const grp = props.batchGroups[i];
batcher.addGroup(grp.name, grp.dynamic, grp.maxAabbSize, grp.id, grp.layers);
}
}
}
if (props.i18nAssets) {
this.i18n.assets = props.i18nAssets;
}
this._loadLibraries(props.libraries, callback);
}
/**
* @param {string[]} urls - List of URLs to load.
* @param {Function} callback - Callback function.
* @private
*/
_loadLibraries(urls, callback) {
const len = urls.length;
let count = len;
const regex = /^https?:\/\//;
if (len) {
const onLoad = (err, script2) => {
count--;
if (err) {
callback(err);
} else if (count === 0) {
this.onLibrariesLoaded();
callback(null);
}
};
for (let i = 0; i < len; ++i) {
let url = urls[i];
if (!regex.test(url.toLowerCase()) && this._scriptPrefix) {
url = path.join(this._scriptPrefix, url);
}
this.loader.load(url, "script", onLoad);
}
} else {
this.onLibrariesLoaded();
callback(null);
}
}
/**
* Insert scene name/urls into the registry.
*
* @param {*} scenes - Scenes to add to the scene registry.
* @private
*/
_parseScenes(scenes) {
if (!scenes) return;
for (let i = 0; i < scenes.length; i++) {
this.scenes.add(scenes[i].name, scenes[i].url);
}
}
/**
* Insert assets into registry.
*
* @param {*} assets - Assets to insert.
* @private
*/
_parseAssets(assets) {
const list = [];
const scriptsIndex = {};
const bundlesIndex = {};
for (let i = 0; i < this.scriptsOrder.length; i++) {
const id = this.scriptsOrder[i];
if (!assets[id]) {
continue;
}
scriptsIndex[id] = true;
list.push(assets[id]);
}
if (this.enableBundles) {
for (const id in assets) {
if (assets[id].type === "bundle") {
bundlesIndex[id] = true;
list.push(assets[id]);
}
}
}
for (const id in assets) {
if (scriptsIndex[id] || bundlesIndex[id]) {
continue;
}
list.push(assets[id]);
}
for (let i = 0; i < list.length; i++) {
const data = list[i];
const asset = new Asset(data.name, data.type, data.file, data.data);
asset.id = parseInt(data.id, 10);
asset.preload = data.preload ? data.preload : false;
asset.loaded = data.type === "script" && data.data && data.data.loadingType > 0;
asset.tags.add(data.tags);
if (data.i18n) {
for (const locale in data.i18n) {
asset.addLocalizedAssetId(locale, data.i18n[locale]);
}
}
this.assets.add(asset);
}
}
/**
* Start the application. This function does the following:
*
* 1. Fires an event on the application named 'start'
* 2. Calls initialize for all components on entities in the hierarchy
* 3. Fires an event on the application named 'initialize'
* 4. Calls postInitialize for all components on entities in the hierarchy
* 5. Fires an event on the application named 'postinitialize'
* 6. Starts executing the main loop of the application
*
* This function is called internally by PlayCanvas applications made in the Editor but you
* will need to call start yourself if you are using the engine stand-alone.
*
* @example
* app.start();
*/
start() {
Debug.call(() => {
Debug.assert(!this._alreadyStarted, "The application can be started only one time.");
this._alreadyStarted = true;
});
this.frame = 0;
this.fire("start", {
timestamp: now(),
target: this
});
if (!this._librariesLoaded) {
this.onLibrariesLoaded();
}
this.systems.fire("initialize", this.root);
this.fire("initialize");
this.systems.fire("postInitialize", this.root);
this.systems.fire("postPostInitialize", this.root);
this.fire("postinitialize");
this.requestAnimationFrame();
}
/**
* Request the next animation frame tick.
*
* @ignore
*/
requestAnimationFrame() {
if (this.xr?.session) {
this.frameRequestId = this.xr.session.requestAnimationFrame(this.tick);
} else {
this.frameRequestId = platform.browser || platform.worker ? requestAnimationFrame(this.tick) : null;
}
}
/**
* Update all input devices managed by the application.
*
* @param {number} dt - The time in seconds since the last update.
* @private
*/
inputUpdate(dt) {
if (this.mouse) {
this.mouse.update();
}
if (this.keyboard) {
this.keyboard.update();
}
if (this.gamepads) {
this.gamepads.update();
}
}
/**
* Update the application. This function will call the update functions and then the postUpdate
* functions of all enabled components. It will then update the current state of all connected
* input devices. This function is called internally in the application's main loop and does
* not need to be called explicitly.
*
* @param {number} dt - The time delta in seconds since the last frame.
*/
update(dt) {
this.frame++;
Debug.call(() => {
this.assets.log();
});
this.graphicsDevice.update();
this.stats.frame.updateStart = now();
this.stats.frame.scriptUpdateStart = now();
this.systems.fire(this._inTools ? "toolsUpdate" : "update", dt);
this.stats.frame.scriptUpdate = now() - this.stats.frame.scriptUpdateStart;
this.stats.frame.animUpdateStart = now();
this.systems.fire("animationUpdate", dt);
this.stats.frame.animUpdate = now() - this.stats.frame.animUpdateStart;
this.stats.frame.scriptPostUpdateStart = now();
this.systems.fire("postUpdate", dt);
this.stats.frame.scriptPostUpdate = now() - this.stats.frame.scriptPostUpdateStart;
this.fire("update", dt);
this.inputUpdate(dt);
this.stats.frame.updateTime = now() - this.stats.frame.updateStart;
}
/**
* Render the application's scene. More specifically, the scene's {@link LayerComposition} is
* rendered. This function is called internally in the application's main loop and does not
* need to be called explicitly.
*
* @ignore
*/
render() {
this.updateCanvasSize();
this.graphicsDevice.frameStart();
this.stats.frame.renderStart = now();
this.fire("prerender");
this.root.syncHierarchy();
if (this._batcher) {
this._batcher.updateAll();
}
ForwardRenderer._skipRenderCounter = 0;
this.renderComposition(this.scene.layers);
this.fire("postrender");
this.stats.frame.renderTime = now() - this.stats.frame.renderStart;
this.graphicsDevice.frameEnd();
}
// render a layer composition
renderComposition(layerComposition) {
DebugGraphics.clearGpuMarkers();
this.renderer.update(layerComposition);
this.renderer.buildFrameGraph(this.frameGraph, layerComposition);
this.frameGraph.render(this.graphicsDevice);
}
/**
* Controls how the canvas fills the window and resizes when the window changes.
*
* @param {string} mode - The mode to use when setting the size of the canvas. Can be:
*
* - {@link FILLMODE_NONE}: the canvas will always match the size provided.
* - {@link FILLMODE_FILL_WINDOW}: the canvas will simply fill the window, changing aspect ratio.
* - {@link FILLMODE_KEEP_ASPECT}: the canvas will grow to fill the window as best it can while
* maintaining the aspect ratio.
*
* @param {number} [width] - The width of the canvas (only used when mode is {@link FILLMODE_NONE}).
* @param {number} [height] - The height of the canvas (only used when mode is {@link FILLMODE_NONE}).
*/
setCanvasFillMode(mode, width, height) {
this._fillMode = mode;
this.resizeCanvas(width, height);
}
/**
* Change the resolution of the canvas, and set the way it behaves when the window is resized.
*
* @param {string} mode - The mode to use when setting the resolution. Can be:
*
* - {@link RESOLUTION_AUTO}: if width and height are not provided, canvas will be resized to
* match canvas client size.
* - {@link RESOLUTION_FIXED}: resolution of canvas will be fixed.
*
* @param {number} [width] - The horizontal resolution, optional in AUTO mode, if not provided
* canvas clientWidth is used.
* @param {number} [height] - The vertical resolution, optional in AUTO mode, if not provided
* canvas clientHeight is used.
*/
setCanvasResolution(mode, width, height) {
this._resolutionMode = mode;
if (mode === RESOLUTION_AUTO && width === void 0) {
width = this.graphicsDevice.canvas.clientWidth;
height = this.graphicsDevice.canvas.clientHeight;
}
this.graphicsDevice.resizeCanvas(width, height);
}
/**
* Queries the visibility of the window or tab in which the application is running.
*
* @returns {boolean} True if the application is not visible and false otherwise.
*/
isHidden() {
return document.hidden;
}
/**
* Called when the visibility state of the current tab/window changes.
*
* @private
*/
onVisibilityChange() {
if (this.isHidden()) {
if (this._soundManager) {
this._soundManager.suspend();
}
} else {
if (this._soundManager) {
this._soundManager.resume();
}
}
}
/**
* Resize the application's canvas element in line with the current fill mode.
*
* - In {@link FILLMODE_KEEP_ASPECT} mode, the canvas will grow to fill the window as best it
* can while maintaining the aspect ratio.
* - In {@link FILLMODE_FILL_WINDOW} mode, the canvas will simply fill the window, changing
* aspect ratio.
* - In {@link FILLMODE_NONE} mode, the canvas will always match the size provided.
*
* @param {number} [width] - The width of the canvas. Only used if current fill mode is {@link FILLMODE_NONE}.
* @param {number} [height] - The height of the canvas. Only used if current fill mode is {@link FILLMODE_NONE}.
* @returns {{width: number, height: number}|undefined} An object containing the values
* calculated to use as width and height, or `undefined` if resizing is not allowed or an XR
* session is active.
*/
resizeCanvas(width, height) {
if (!this._allowResize) return void 0;
if (this.xr && this.xr.session) {
return void 0;
}
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (this._fillMode === FILLMODE_KEEP_ASPECT) {
const r = this.graphicsDevice.canvas.width / this.graphicsDevice.canvas.height;
const winR = windowWidth / windowHeight;
if (r > winR) {
width = windowWidth;
height = width / r;
} else {
height = windowHeight;
width = height * r;
}
} else if (this._fillMode === FILLMODE_FILL_WINDOW) {
width = windowWidth;
height = windowHeight;
}
this.graphicsDevice.canvas.style.width = `${width}px`;
this.graphicsDevice.canvas.style.height = `${height}px`;
this.updateCanvasSize();
return {
width,
height
};
}
/**
* Updates the {@link GraphicsDevice} canvas size to match the canvas size on the document
* page. It is recommended to call this function when the canvas size changes (e.g on window
* resize and orientation change events) so that the canvas resolution is immediately updated.
*/
updateCanvasSize() {
if (!this._allowResize || this.xr?.active) {
return;
}
if (this._resolutionMode === RESOLUTION_AUTO) {
const canvas = this.graphicsDevice.canvas;
this.graphicsDevice.resizeCanvas(canvas.clientWidth, canvas.clientHeight);
}
}
/**
* Event handler called when all code libraries have been loaded. Code libraries are passed
* into the constructor of the Application and the application won't start running or load
* packs until all libraries have been loaded.
*
* @private
*/
onLibrariesLoaded() {
this._librariesLoaded = true;
if (this.systems.rigidbody) {
this.systems.rigidbody.onLibraryLoaded();
}
}
/**
* Apply scene settings to the current scene. Useful when your scene settings are parsed or
* generated from a non-URL source.
*
* @param {object} settings - The scene settings to be applied.
* @param {object} settings.physics - The physics settings to be applied.
* @param {number[]} settings.physics.gravity - The world space vector representing global
* gravity in the physics simulation. Must be a fixed size array with three number elements,
* corresponding to each axis [ X, Y, Z ].
* @param {object} settings.render - The rendering settings to be applied.
* @param {number[]} settings.render.global_ambient - The color of the scene's ambient light.
* Must be a fixed size array with three number elements, corresponding to each color channel
* [ R, G, B ].
* @param {string} settings.render.fog - The type of fog used by the scene. Can be:
*
* - {@link FOG_NONE}
* - {@link FOG_LINEAR}
* - {@link FOG_EXP}
* - {@link FOG_EXP2}
*
* @param {number[]} settings.render.fog_color - The color of the fog (if enabled). Must be a
* fixed size array with three number elements, corresponding to each color channel [ R, G, B ].
* @param {number} settings.render.fog_density - The density of the fog (if enabled). This
* property is only valid if the fog property is set to {@link FOG_EXP} or {@link FOG_EXP2}.
* @param {number} settings.render.fog_start - The distance from the viewpoint where linear fog
* begins. This property is only valid if the fog property is set to {@link FOG_LINEAR}.
* @param {number} settings.render.fog_end - The distance from the viewpoint where linear fog
* reaches its maximum. This property is only valid if the fog property is set to {@link FOG_LINEAR}.
* @param {number} settings.render.gamma_correction - The gamma correction to apply when
* rendering the scene. Can be:
*
* - {@link GAMMA_NONE}
* - {@link GAMMA_SRGB}
*
* @param {number} settings.render.tonemapping - The tonemapping transform to apply when
* writing fragments to the frame buffer. Can be:
*
* - {@link TONEMAP_LINEAR}
* - {@link TONEMAP_FILMIC}
* - {@link TONEMAP_HEJL}
* - {@link TONEMAP_ACES}
* - {@link TONEMAP_ACES2}
* - {@link TONEMAP_NEUTRAL}
*
* @param {number} settings.render.exposure - The exposure value tweaks the overall brightness
* of the scene.
* @param {number|null} [settings.render.skybox] - The asset ID of the cube map texture to be
* used as the scene's skybox. Defaults to null.
* @param {number} [settings.render.skyboxIntensity] - Multiplier for skybox intensity. Defaults to 1.
* @param {number} [settings.render.skyboxLuminance] - Lux (lm/m^2) value for skybox intensity when physical light units are enabled. Defaults to 20000.
* @param {number} [settings.render.skyboxMip] - The mip level of the skybox to be displayed. Defaults to 0.
* Only valid for prefiltered cubemap skyboxes.
* @param {number[]} [settings.render.skyboxRotation] - Rotation of skybox. Defaults to [0, 0, 0].
*
* @param {string} [settings.render.skyType] - The type of the sky. One of the SKYTYPE_* constants. Defaults to {@link SKYTYPE_INFINITE}.
* @param {number[]} [settings.render.skyMeshPosition] - The position of sky mesh. Ignored for {@link SKYTYPE_INFINITE}. Defaults to [0, 0, 0].
* @param {number[]} [settings.render.skyMeshRotation] - The rotation of sky mesh. Ignored for {@link SKYTYPE_INFINITE}. Defaults to [0, 0, 0].
* @param {number[]} [settings.render.skyMeshScale] - The scale of sky mesh. Ignored for {@link SKYTYPE_INFINITE}. Defaults to [1, 1, 1].
* @param {number[]} [settings.render.skyCenter] - The center of the sky. Ignored for {@link SKYTYPE_INFINITE}. Defaults to [0, 1, 0].
*
* @param {number} settings.render.lightmapSizeMultiplier - The lightmap resolution multiplier.
* @param {number} settings.render.lightmapMaxResolution - The maximum lightmap resolution.
* @param {number} settings.render.lightmapMode - The lightmap baking mode. Can be:
*
* - {@link BAKE_COLOR}: single color lightmap
* - {@link BAKE_COLORDIR}: single color lightmap + dominant light direction (used for bump/specular)
*
* @param {boolean} [settings.render.lightmapFilterEnabled] - Enables bilateral filter on runtime baked color lightmaps. Defaults to false.
* @param {number} [settings.render.lightmapFilterRange] - Sets the range parameter of the bilateral filter. Defaults to 10.
* @param {number} [settings.render.lightmapFilterSmoothness] - Sets the spatial parameter of the bilateral filter. Defaults to 0.2.
*
* @param {boolean} [settings.render.ambientBake] - Enable baking ambient light into lightmaps. Defaults to false.
* @param {number} [settings.render.ambientBakeNumSamples] - Number of samples to use when baking ambient light. Defaults to 1.
* @param {number} [settings.render.ambientBakeSpherePart] - How much of the sphere to include when baking ambient light. Defaults to 0.4.
* @param {number} [settings.render.ambientBakeOcclusionBrightness] - Brightness of the baked ambient occlusion. Defaults to 0.
* @param {number} [settings.render.ambientBakeOcclusionContrast] - Contrast of the baked ambient occlusion. Defaults to 0.
* @param {number} settings.render.ambientLuminance - Lux (lm/m^2) value for ambient light intensity.
*
* @param {boolean} [settings.render.clusteredLightingEnabled] - Enable clustered lighting. Defaults to false.
* @param {boolean} [settings.render.lightingShadowsEnabled] - If set to true, the clustered lighting will support shadows. Defaults to true.
* @param {boolean} [settings.render.lightingCookiesEnabled] - If set to true, the clustered lighting will support cookie textures. Defaults to false.
* @param {boolean} [settings.render.lightingAreaLightsEnabled] - If set to true, the clustered lighting will support area lights. Defaults to false.
* @param {number} [settings.render.lightingShadowAtlasResolution] - Resolution of the atlas texture storing all non-directional shadow textures. Defaults to 2048.
* @param {number} [settings.render.lightingCookieAtlasResolution] - Resolution of the atlas texture storing all non-directional cookie textures. Defaults to 2048.
* @param {number} [settings.render.lightingMaxLightsPerCell] - Maximum number of lights a cell can store. Defaults to 255.
* @param {number} [settings.render.lightingShadowType] - The type of shadow filtering used by all shadows. Can be:
*
* - {@link SHADOW_PCF1_32F}
* - {@link SHADOW_PCF3_32F}
* - {@link SHADOW_PCF5_32F}
* - {@link SHADOW_PCF1_16F}
* - {@link SHADOW_PCF3_16F}
* - {@link SHADOW_PCF5_16F}
*
* Defaults to {@link SHADOW_PCF3_32F}.
* @param {number[]} [settings.render.lightingCells] - Number of cells along each world space axis the space containing lights
* is subdivided into. Defaults to [10, 3, 10].
*
* Only lights with bakeDir=true will be used for generating the dominant light direction.
* @example
*
* const settings = {
* physics: {
* gravity: [0, -9.8, 0]
* },
* render: {
* fog_end: 1000,
* tonemapping: 0,
* skybox: null,
* fog_density: 0.01,
* gamma_correction: 1,
* exposure: 1,
* fog_start: 1,
* global_ambient: [0, 0, 0],
* skyboxIntensity: 1,
* skyboxRotation: [0, 0, 0],
* fog_color: [0, 0, 0],
* lightmapMode: 1,
* fog: 'none',
* lightmapMaxResolution: 2048,
* skyboxMip: 2,
* lightmapSizeMultiplier: 16
* }
* };
* app.applySceneSettings(settings);
*/
applySceneSettings(settings) {
let asset;
if (this.systems.rigidbody && typeof Ammo !== "undefined") {
const [x, y, z] = settings.physics.gravity;
this.systems.rigidbody.gravity.set(x, y, z);
}
this.scene.applySettings(settings);
if (settings.render.hasOwnProperty("skybox")) {
if (settings.render.skybox) {
asset = this.assets.get(settings.render.skybox);
if (asset) {
this.setSkybox(asset);
} else {
this.assets.once(`add:${settings.render.skybox}`, this.setSkybox, this);
}
} else {
this.setSkybox(null);
}
}
}
/**
* Sets the area light LUT tables for this app.
*
* @param {number[]} ltcMat1 - LUT table of type `array` to be set.
* @param {number[]} ltcMat2 - LUT table of type `array` to be set.
*/
setAreaLightLuts(ltcMat1, ltcMat2) {
if (ltcMat1 && ltcMat2) {
AreaLightLuts.set(this.graphicsDevice, ltcMat1, ltcMat2);
} else {
Debug.warn("setAreaLightLuts: LUTs for area light are not valid");
}
}
/**
* Sets the skybox asset to current scene, and subscribes to asset load/change events.
*
* @param {Asset} asset - Asset of type `skybox` to be set to, or null to remove skybox.
*/
setSkybox(asset) {
if (asset !== this._skyboxAsset) {
const onSkyboxRemoved = () => {
this.setSkybox(null);
};
const onSkyboxChanged = () => {
this.scene.setSkybox(this._skyboxAsset ? this._skyboxAsset.resources : null);
};
if (this._skyboxAsset) {
this.assets.off(`load:${this._skyboxAsset.id}`, onSkyboxChanged, this);
this.assets.off(`remove:${this._skyboxAsset.id}`, onSkyboxRemoved, this);
this._skyboxAsset.off("change", onSkyboxChanged, this);
}
this._skyboxAsset = asset;
if (this._skyboxAsset) {
this.assets.on(`load:${this._skyboxAsset.id}`, onSkyboxChanged, this);
this.assets.once(`remove:${this._skyboxAsset.id}`, onSkyboxRemoved, this);
this._skyboxAsset.on("change", onSkyboxChanged, this);
if (this.scene.skyboxMip === 0 && !this._skyboxAsset.loadFaces) {
this._skyboxAsset.loadFaces = true;
}
this.assets.load(this._skyboxAsset);
}
onSkyboxChanged();
}
}
/** @private */
_firstBake() {
this.lightmapper?.bake(null, this.scene.lightmapMode);
}
/** @private */
_firstBatch() {
this.batcher?.generate();
}
/**
* Provide an opportunity to modify the timestamp supplied by requestAnimationFrame.
*
* @param {number} [timestamp] - The timestamp supplied by requestAnimationFrame.
* @returns {number|undefined} The modified timestamp.
* @ignore
*/
_processTimestamp(timestamp) {
return timestamp;
}
/**
* Draws a single line. Line start and end coordinates are specified in world space. The line
* will be flat-shaded with the specified color.
*
* @param {Vec3} start - The start world space coordinate of the line.
* @param {Vec3} end - The end world space coordinate of the line.
* @param {Color} [color] - The color of the line. It defaults to white if not specified.
* @param {boolean} [depthTest] - Specifies if the line is depth tested against the depth
* buffer. Defaults to true.
* @param {Layer} [layer] - The layer to render the line into. Defaults to {@link LAYERID_IMMEDIATE}.
* @example
* // Render a 1-unit long white line
* const start = new pc.Vec3(0, 0, 0);
* const end = new pc.Vec3(1, 0, 0);
* app.drawLine(start, end);
* @example
* // Render a 1-unit long red line which is not depth tested and renders on top of other geometry
* const start = new pc.Vec3(0, 0, 0);
* const end = new pc.Vec3(1, 0, 0);
* app.drawLine(start, end, pc.Color.RED, false);
* @example
* // Render a 1-unit long white line into the world layer
* const start = new pc.Vec3(0, 0, 0);
* const end = new pc.Vec3(1, 0, 0);
* const worldLayer = app.scene.layers.getLayerById(pc.LAYERID_WORLD);
* app.drawLine(start, end, pc.Color.WHITE, true, worldLayer);
*/
drawLine(start, end, color, depthTest, layer) {
this.scene.drawLine(start, end, color, depthTest, layer);
}
/**
* Renders an arbitrary number of discrete line segments. The lines are not connected by each
* subsequent point in the array. Instead, they are individual segments specified by two
* points. Therefore, the lengths of the supplied position and color arrays must be the same
* and also must be a multiple of 2. The colors of the ends of each line segment will be
* interpolated along the length of each line.
*
* @param {Vec3[]} positions - An array of points to draw lines between. The length of the
* array must be a multiple of 2.
* @param {Color[] | Color} colors - An array of colors or a single color. If an array is
* specified, this must be the same length as the position array. The length of the array
* must also be a multiple of 2.
* @param {boolean} [depthTest] - Specifies if the lines are depth tested against the depth
* buffer. Defaults to true.
* @param {Layer} [layer] - The layer to render the lines into. Defaults to {@link LAYERID_IMMEDIATE}.
* @example
* // Render a single line, with unique colors for each point
* const start = new pc.Vec3(0, 0, 0);
* const end = new pc.Vec3(1, 0, 0);
* app.drawLines([start, end], [pc.Color.RED, pc.Color.WHITE]);
* @example
* // Render 2 discrete line segments
* const points = [
* // Line 1
* new pc.Vec3(0, 0, 0),
* new pc.Vec3(1, 0, 0),
* // Line 2
* new pc.Vec3(1, 1, 0),
* new pc.Vec3(1, 1, 1)
* ];
* const colors = [
* // Line 1
* pc.Color.RED,
* pc.Color.YELLOW,
* // Line 2
* pc.Color.CYAN,
* pc.Color.BLUE
* ];
* app.drawLines(points, colors);
*/
drawLines(positions, colors, depthTest = true, layer = this.scene.defaultDrawLayer) {
this.scene.drawLines(positions, colors, depthTest, layer);
}
/**
* Renders an arbitrary number of discrete line segments. The lines are not connected by each
* subsequent point in the array. Instead, they are individual segments specified by two
* points.
*
* @param {number[]} positions - An array of points to draw lines between. Each point is
* represented by 3 numbers - x, y and z coordinate.
* @param {number[]|Color} colors - A single color for all lines, or an array of colors to color
* the lines. If an array is specifie