@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.
1,251 lines (1,250 loc) • 61.7 kB
JavaScript
import { EngineStore } from "./engineStore.js";
import { Logger } from "../Misc/logger.js";
import { Effect } from "../Materials/effect.js";
import { PerformanceConfigurator } from "./performanceConfigurator.js";
import { PrecisionDate } from "../Misc/precisionDate.js";
import { DepthCullingState } from "../States/depthCullingState.js";
import { StencilStateComposer } from "../States/stencilStateComposer.js";
import { StencilState } from "../States/stencilState.js";
import { AlphaState } from "../States/alphaCullingState.js";
import { _WarnImport } from "../Misc/devTools.js";
import { InternalTexture } from "../Materials/Textures/internalTexture.js";
import { IsDocumentAvailable, IsNavigatorAvailable, IsWindowObjectExist } from "../Misc/domManagement.js";
import { Observable } from "../Misc/observable.js";
import { EngineFunctionContext, _loadFile } from "./abstractEngine.functions.js";
import { _GetCompatibleTextureLoader } from "../Materials/Textures/Loaders/textureLoaderManager.js";
/**
* Queue a new function into the requested animation frame pool (ie. this function will be executed by the browser (or the javascript engine) for the next frame)
* @param func - the function to be called
* @param requester - the object that will request the next frame. Falls back to window.
* @returns frame number
*/
export function QueueNewFrame(func, requester) {
// Note that there is kind of a typing issue here, as `setTimeout` might return something else than a number (NodeJs returns a NodeJS.Timeout object).
// Also if the global `requestAnimationFrame`'s returnType is number, `requester.requestPostAnimationFrame` and `requester.requestAnimationFrame` types
// are `any`.
if (!IsWindowObjectExist()) {
if (typeof requestAnimationFrame === "function") {
return requestAnimationFrame(func);
}
}
else {
const { requestAnimationFrame } = requester || window;
if (typeof requestAnimationFrame === "function") {
return requestAnimationFrame(func);
}
}
// fallback to the global `setTimeout`.
// In most cases (aka in the browser), `window` is the global object, so instead of calling `window.setTimeout` we could call the global `setTimeout`.
return setTimeout(func, 16);
}
/**
* The parent class for specialized engines (WebGL, WebGPU)
*/
export class AbstractEngine {
/**
* Gets the current frame id
*/
get frameId() {
return this._frameId;
}
/**
* Gets a boolean indicating if the engine runs in WebGPU or not.
*/
get isWebGPU() {
return this._isWebGPU;
}
/**
* @internal
*/
_getShaderProcessor(shaderLanguage) {
return this._shaderProcessor;
}
/**
* Gets the shader platform name used by the effects.
*/
get shaderPlatformName() {
return this._shaderPlatformName;
}
_clearEmptyResources() {
this._emptyTexture = null;
this._emptyCubeTexture = null;
this._emptyTexture3D = null;
this._emptyTexture2DArray = null;
}
/**
* Gets or sets a boolean indicating if depth buffer should be reverse, going from far to near.
* This can provide greater z depth for distant objects.
*/
get useReverseDepthBuffer() {
return this._useReverseDepthBuffer;
}
set useReverseDepthBuffer(useReverse) {
if (useReverse === this._useReverseDepthBuffer) {
return;
}
this._useReverseDepthBuffer = useReverse;
if (useReverse) {
this._depthCullingState.depthFunc = 518;
}
else {
this._depthCullingState.depthFunc = 515;
}
}
/**
* Enable or disable color writing
* @param enable defines the state to set
*/
setColorWrite(enable) {
if (enable !== this._colorWrite) {
this._colorWriteChanged = true;
this._colorWrite = enable;
}
}
/**
* Gets a boolean indicating if color writing is enabled
* @returns the current color writing state
*/
getColorWrite() {
return this._colorWrite;
}
/**
* Gets the depth culling state manager
*/
get depthCullingState() {
return this._depthCullingState;
}
/**
* Gets the alpha state manager
*/
get alphaState() {
return this._alphaState;
}
/**
* Gets the stencil state manager
*/
get stencilState() {
return this._stencilState;
}
/**
* Gets the stencil state composer
*/
get stencilStateComposer() {
return this._stencilStateComposer;
}
/** @internal */
_getGlobalDefines(defines) {
if (defines) {
if (this.isNDCHalfZRange) {
defines["IS_NDC_HALF_ZRANGE"] = "";
}
else {
delete defines["IS_NDC_HALF_ZRANGE"];
}
if (this.useReverseDepthBuffer) {
defines["USE_REVERSE_DEPTHBUFFER"] = "";
}
else {
delete defines["USE_REVERSE_DEPTHBUFFER"];
}
if (this.useExactSrgbConversions) {
defines["USE_EXACT_SRGB_CONVERSIONS"] = "";
}
else {
delete defines["USE_EXACT_SRGB_CONVERSIONS"];
}
return;
}
else {
let s = "";
if (this.isNDCHalfZRange) {
s += "#define IS_NDC_HALF_ZRANGE";
}
if (this.useReverseDepthBuffer) {
if (s) {
s += "\n";
}
s += "#define USE_REVERSE_DEPTHBUFFER";
}
if (this.useExactSrgbConversions) {
if (s) {
s += "\n";
}
s += "#define USE_EXACT_SRGB_CONVERSIONS";
}
return s;
}
}
_rebuildInternalTextures() {
const currentState = this._internalTexturesCache.slice(); // Do a copy because the rebuild will add proxies
for (const internalTexture of currentState) {
internalTexture._rebuild();
}
}
_rebuildRenderTargetWrappers() {
const currentState = this._renderTargetWrapperCache.slice(); // Do a copy because the rebuild will add proxies
for (const renderTargetWrapper of currentState) {
renderTargetWrapper._rebuild();
}
}
_rebuildEffects() {
for (const key in this._compiledEffects) {
const effect = this._compiledEffects[key];
effect._pipelineContext = null; // because _prepareEffect will try to dispose this pipeline before recreating it and that would lead to webgl errors
effect._prepareEffect();
}
Effect.ResetCache();
}
_rebuildGraphicsResources() {
// Ensure webgl and engine states are matching
this.wipeCaches(true);
// Rebuild effects
this._rebuildEffects();
this._rebuildComputeEffects?.();
// Note:
// The call to _rebuildBuffers must be made before the call to _rebuildInternalTextures because in the process of _rebuildBuffers the buffers used by the post process managers will be rebuilt
// and we may need to use the post process manager of the scene during _rebuildInternalTextures (in WebGL1, non-POT textures are rescaled using a post process + post process manager of the scene)
// Rebuild buffers
this._rebuildBuffers();
// Rebuild textures
this._rebuildInternalTextures();
// Rebuild textures
this._rebuildTextures();
// Rebuild textures
this._rebuildRenderTargetWrappers();
// Reset engine states after all the buffer/textures/... have been rebuilt
this.wipeCaches(true);
}
_flagContextRestored() {
Logger.Warn(this.name + " context successfully restored.");
this.onContextRestoredObservable.notifyObservers(this);
this._contextWasLost = false;
}
_restoreEngineAfterContextLost(initEngine) {
// Adding a timeout to avoid race condition at browser level
setTimeout(async () => {
this._clearEmptyResources();
const depthTest = this._depthCullingState.depthTest; // backup those values because the call to initEngine / wipeCaches will reset them
const depthFunc = this._depthCullingState.depthFunc;
const depthMask = this._depthCullingState.depthMask;
const stencilTest = this._stencilState.stencilTest;
// Rebuild context
await initEngine();
this._rebuildGraphicsResources();
this._depthCullingState.depthTest = depthTest;
this._depthCullingState.depthFunc = depthFunc;
this._depthCullingState.depthMask = depthMask;
this._stencilState.stencilTest = stencilTest;
this._flagContextRestored();
}, 0);
}
/** Gets a boolean indicating if the engine was disposed */
get isDisposed() {
return this._isDisposed;
}
/**
* Enables or disables the snapshot rendering mode
* Note that the WebGL engine does not support snapshot rendering so setting the value won't have any effect for this engine
*/
get snapshotRendering() {
return false;
}
set snapshotRendering(activate) {
// Do nothing
}
/**
* Gets or sets the snapshot rendering mode
*/
get snapshotRenderingMode() {
return 0;
}
set snapshotRenderingMode(mode) { }
/**
* Returns the string "AbstractEngine"
* @returns "AbstractEngine"
*/
getClassName() {
return "AbstractEngine";
}
/**
* Gets the default empty texture
*/
get emptyTexture() {
if (!this._emptyTexture) {
this._emptyTexture = this.createRawTexture(new Uint8Array(4), 1, 1, 5, false, false, 1);
}
return this._emptyTexture;
}
/**
* Gets the default empty 3D texture
*/
get emptyTexture3D() {
if (!this._emptyTexture3D) {
this._emptyTexture3D = this.createRawTexture3D(new Uint8Array(4), 1, 1, 1, 5, false, false, 1);
}
return this._emptyTexture3D;
}
/**
* Gets the default empty 2D array texture
*/
get emptyTexture2DArray() {
if (!this._emptyTexture2DArray) {
this._emptyTexture2DArray = this.createRawTexture2DArray(new Uint8Array(4), 1, 1, 1, 5, false, false, 1);
}
return this._emptyTexture2DArray;
}
/**
* Gets the default empty cube texture
*/
get emptyCubeTexture() {
if (!this._emptyCubeTexture) {
const faceData = new Uint8Array(4);
const cubeData = [faceData, faceData, faceData, faceData, faceData, faceData];
this._emptyCubeTexture = this.createRawCubeTexture(cubeData, 1, 5, 0, false, false, 1);
}
return this._emptyCubeTexture;
}
/**
* Gets the list of current active render loop functions
* @returns a read only array with the current render loop functions
*/
get activeRenderLoops() {
return this._activeRenderLoops;
}
/**
* stop executing a render loop function and remove it from the execution array
* @param renderFunction defines the function to be removed. If not provided all functions will be removed.
*/
stopRenderLoop(renderFunction) {
if (!renderFunction) {
this._activeRenderLoops.length = 0;
this._cancelFrame();
return;
}
const index = this._activeRenderLoops.indexOf(renderFunction);
if (index >= 0) {
this._activeRenderLoops.splice(index, 1);
if (this._activeRenderLoops.length == 0) {
this._cancelFrame();
}
}
}
_cancelFrame() {
if (this._frameHandler !== 0) {
const handlerToCancel = this._frameHandler;
this._frameHandler = 0;
if (!IsWindowObjectExist()) {
if (typeof cancelAnimationFrame === "function") {
return cancelAnimationFrame(handlerToCancel);
}
}
else {
const { cancelAnimationFrame } = this.getHostWindow() || window;
if (typeof cancelAnimationFrame === "function") {
return cancelAnimationFrame(handlerToCancel);
}
}
return clearTimeout(handlerToCancel);
}
}
/**
* Begin a new frame
*/
beginFrame() {
this.onBeginFrameObservable.notifyObservers(this);
}
/**
* End the current frame
*/
endFrame() {
this._frameId++;
this.onEndFrameObservable.notifyObservers(this);
}
/** Gets or sets max frame per second allowed. Will return undefined if not capped */
get maxFPS() {
return this._maxFPS;
}
set maxFPS(value) {
this._maxFPS = value;
if (value === undefined) {
return;
}
if (value <= 0) {
this._minFrameTime = Number.MAX_VALUE;
return;
}
this._minFrameTime = 1000 / value;
}
_isOverFrameTime(timestamp) {
if (!timestamp || this._maxFPS === undefined) {
return false;
}
const elapsedTime = timestamp - this._lastFrameTime;
this._lastFrameTime = timestamp;
this._renderAccumulator += elapsedTime;
if (this._renderAccumulator < this._minFrameTime) {
return true;
}
this._renderAccumulator -= this._minFrameTime;
if (this._renderAccumulator > this._minFrameTime) {
this._renderAccumulator = this._minFrameTime;
}
return false;
}
_processFrame(timestamp) {
this._frameHandler = 0;
if (!this._contextWasLost && !this._isOverFrameTime(timestamp)) {
let shouldRender = true;
if (this.isDisposed || (!this.renderEvenInBackground && this._windowIsBackground)) {
shouldRender = false;
}
if (shouldRender) {
// Start new frame
this.beginFrame();
// Child canvases
if (!this.skipFrameRender && !this._renderViews()) {
// Main frame
this._renderFrame();
}
// Present
this.endFrame();
}
}
}
/** @internal */
_renderLoop(timestamp) {
this._processFrame(timestamp);
// The first condition prevents queuing another frame if we no longer have active render loops (e.g., if
// `stopRenderLoop` is called mid frame). The second condition prevents queuing another frame if one has
// already been queued (e.g., if `stopRenderLoop` and `runRenderLoop` is called mid frame).
if (this._activeRenderLoops.length > 0 && this._frameHandler === 0) {
this._frameHandler = this._queueNewFrame(this._boundRenderFunction, this.getHostWindow());
}
}
/** @internal */
_renderFrame() {
for (let index = 0; index < this._activeRenderLoops.length; index++) {
const renderFunction = this._activeRenderLoops[index];
renderFunction();
}
}
/** @internal */
_renderViews() {
return false;
}
/**
* Can be used to override the current requestAnimationFrame requester.
* @internal
*/
_queueNewFrame(bindedRenderFunction, requester) {
return QueueNewFrame(bindedRenderFunction, requester);
}
/**
* Register and execute a render loop. The engine can have more than one render function
* @param renderFunction defines the function to continuously execute
*/
runRenderLoop(renderFunction) {
if (this._activeRenderLoops.indexOf(renderFunction) !== -1) {
return;
}
this._activeRenderLoops.push(renderFunction);
// On the first added function, start the render loop.
if (this._activeRenderLoops.length === 1 && this._frameHandler === 0) {
this._frameHandler = this._queueNewFrame(this._boundRenderFunction, this.getHostWindow());
}
}
/**
* Gets a boolean indicating if depth testing is enabled
* @returns the current state
*/
getDepthBuffer() {
return this._depthCullingState.depthTest;
}
/**
* Enable or disable depth buffering
* @param enable defines the state to set
*/
setDepthBuffer(enable) {
this._depthCullingState.depthTest = enable;
}
/**
* Set the z offset Factor to apply to current rendering
* @param value defines the offset to apply
*/
setZOffset(value) {
this._depthCullingState.zOffset = this.useReverseDepthBuffer ? -value : value;
}
/**
* Gets the current value of the zOffset Factor
* @returns the current zOffset Factor state
*/
getZOffset() {
const zOffset = this._depthCullingState.zOffset;
return this.useReverseDepthBuffer ? -zOffset : zOffset;
}
/**
* Set the z offset Units to apply to current rendering
* @param value defines the offset to apply
*/
setZOffsetUnits(value) {
this._depthCullingState.zOffsetUnits = this.useReverseDepthBuffer ? -value : value;
}
/**
* Gets the current value of the zOffset Units
* @returns the current zOffset Units state
*/
getZOffsetUnits() {
const zOffsetUnits = this._depthCullingState.zOffsetUnits;
return this.useReverseDepthBuffer ? -zOffsetUnits : zOffsetUnits;
}
/**
* Gets host window
* @returns the host window object
*/
getHostWindow() {
if (!IsWindowObjectExist()) {
return null;
}
if (this._renderingCanvas && this._renderingCanvas.ownerDocument && this._renderingCanvas.ownerDocument.defaultView) {
return this._renderingCanvas.ownerDocument.defaultView;
}
return window;
}
/**
* (WebGPU only) True (default) to be in compatibility mode, meaning rendering all existing scenes without artifacts (same rendering than WebGL).
* Setting the property to false will improve performances but may not work in some scenes if some precautions are not taken.
* See https://doc.babylonjs.com/setup/support/webGPU/webGPUOptimization/webGPUNonCompatibilityMode for more details
*/
get compatibilityMode() {
return this._compatibilityMode;
}
set compatibilityMode(mode) {
// not supported in WebGL
this._compatibilityMode = true;
}
_rebuildTextures() {
for (const scene of this.scenes) {
scene._rebuildTextures();
}
for (const scene of this._virtualScenes) {
scene._rebuildTextures();
}
}
/**
* @internal
*/
_releaseRenderTargetWrapper(rtWrapper) {
const index = this._renderTargetWrapperCache.indexOf(rtWrapper);
if (index !== -1) {
this._renderTargetWrapperCache.splice(index, 1);
}
}
/**
* Gets the current viewport
*/
get currentViewport() {
return this._cachedViewport;
}
/**
* Set the WebGL's viewport
* @param viewport defines the viewport element to be used
* @param requiredWidth defines the width required for rendering. If not provided the rendering canvas' width is used
* @param requiredHeight defines the height required for rendering. If not provided the rendering canvas' height is used
*/
setViewport(viewport, requiredWidth, requiredHeight) {
const width = requiredWidth || this.getRenderWidth();
const height = requiredHeight || this.getRenderHeight();
const x = viewport.x || 0;
const y = viewport.y || 0;
this._cachedViewport = viewport;
this._viewport(x * width, y * height, width * viewport.width, height * viewport.height);
}
/**
* Create an image to use with canvas
* @returns IImage interface
*/
createCanvasImage() {
return document.createElement("img");
}
/**
* Create a 2D path to use with canvas
* @returns IPath2D interface
* @param d SVG path string
*/
createCanvasPath2D(d) {
return new Path2D(d);
}
/**
* Returns a string describing the current engine
*/
get description() {
let description = this.name + this.version;
if (this._caps.parallelShaderCompile) {
description += " - Parallel shader compilation";
}
return description;
}
_createTextureBase(url, noMipmap, invertY, scene, samplingMode = 3, onLoad = null, onError = null, prepareTexture, prepareTextureProcess, buffer = null, fallback = null, format = null, forcedExtension = null, mimeType, loaderOptions, useSRGBBuffer) {
url = url || "";
const fromData = url.substr(0, 5) === "data:";
const fromBlob = url.substr(0, 5) === "blob:";
const isBase64 = fromData && url.indexOf(";base64,") !== -1;
const texture = fallback ? fallback : new InternalTexture(this, 1 /* InternalTextureSource.Url */);
if (texture !== fallback) {
texture.label = url.substring(0, 60); // default label, can be overriden by the caller
}
const originalUrl = url;
if (this._transformTextureUrl && !isBase64 && !fallback && !buffer) {
url = this._transformTextureUrl(url);
}
if (originalUrl !== url) {
texture._originalUrl = originalUrl;
}
// establish the file extension, if possible
const lastDot = url.lastIndexOf(".");
let extension = forcedExtension ? forcedExtension : lastDot > -1 ? url.substring(lastDot).toLowerCase() : "";
// Remove query string
const queryStringIndex = extension.indexOf("?");
if (queryStringIndex > -1) {
extension = extension.split("?")[0];
}
const loaderPromise = _GetCompatibleTextureLoader(extension, mimeType);
if (scene) {
scene.addPendingData(texture);
}
texture.url = url;
texture.generateMipMaps = !noMipmap;
texture.samplingMode = samplingMode;
texture.invertY = invertY;
texture._useSRGBBuffer = this._getUseSRGBBuffer(!!useSRGBBuffer, noMipmap);
if (!this._doNotHandleContextLost) {
// Keep a link to the buffer only if we plan to handle context lost
texture._buffer = buffer;
}
let onLoadObserver = null;
if (onLoad && !fallback) {
onLoadObserver = texture.onLoadedObservable.add(onLoad);
}
if (!fallback) {
this._internalTexturesCache.push(texture);
}
const onInternalError = (message, exception) => {
if (scene) {
scene.removePendingData(texture);
}
if (url === originalUrl) {
if (onLoadObserver) {
texture.onLoadedObservable.remove(onLoadObserver);
}
if (EngineStore.UseFallbackTexture && url !== EngineStore.FallbackTexture) {
this._createTextureBase(EngineStore.FallbackTexture, noMipmap, texture.invertY, scene, samplingMode, null, onError, prepareTexture, prepareTextureProcess, buffer, texture);
}
message = (message || "Unknown error") + (EngineStore.UseFallbackTexture ? " - Fallback texture was used" : "");
texture.onErrorObservable.notifyObservers({ message, exception });
if (onError) {
onError(message, exception);
}
}
else {
// fall back to the original url if the transformed url fails to load
Logger.Warn(`Failed to load ${url}, falling back to ${originalUrl}`);
this._createTextureBase(originalUrl, noMipmap, texture.invertY, scene, samplingMode, onLoad, onError, prepareTexture, prepareTextureProcess, buffer, texture, format, forcedExtension, mimeType, loaderOptions, useSRGBBuffer);
}
};
// processing for non-image formats
if (loaderPromise) {
const callback = async (data) => {
const loader = await loaderPromise;
loader.loadData(data, texture, (width, height, loadMipmap, isCompressed, done, loadFailed) => {
if (loadFailed) {
onInternalError("TextureLoader failed to load data");
}
else {
prepareTexture(texture, extension, scene, { width, height }, texture.invertY, !loadMipmap, isCompressed, () => {
done();
return false;
}, samplingMode);
}
}, loaderOptions);
};
if (!buffer) {
this._loadFile(url, (data) => callback(new Uint8Array(data)), undefined, scene ? scene.offlineProvider : undefined, true, (request, exception) => {
onInternalError("Unable to load " + (request ? request.responseURL : url, exception));
});
}
else {
if (buffer instanceof ArrayBuffer) {
callback(new Uint8Array(buffer));
}
else if (ArrayBuffer.isView(buffer)) {
callback(buffer);
}
else {
if (onError) {
onError("Unable to load: only ArrayBuffer or ArrayBufferView is supported", null);
}
}
}
}
else {
const onload = (img) => {
if (fromBlob && !this._doNotHandleContextLost) {
// We need to store the image if we need to rebuild the texture
// in case of a webgl context lost
texture._buffer = img;
}
prepareTexture(texture, extension, scene, img, texture.invertY, noMipmap, false, prepareTextureProcess, samplingMode);
};
// According to the WebGL spec section 6.10, ImageBitmaps must be inverted on creation.
// So, we pass imageOrientation to _FileToolsLoadImage() as it may create an ImageBitmap.
if (!fromData || isBase64) {
if (buffer && (typeof buffer.decoding === "string" || buffer.close)) {
onload(buffer);
}
else {
AbstractEngine._FileToolsLoadImage(url || "", onload, onInternalError, scene ? scene.offlineProvider : null, mimeType, texture.invertY && this._features.needsInvertingBitmap ? { imageOrientation: "flipY" } : undefined, this);
}
}
else if (typeof buffer === "string" || buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer) || buffer instanceof Blob) {
AbstractEngine._FileToolsLoadImage(buffer, onload, onInternalError, scene ? scene.offlineProvider : null, mimeType, texture.invertY && this._features.needsInvertingBitmap ? { imageOrientation: "flipY" } : undefined, this);
}
else if (buffer) {
onload(buffer);
}
}
return texture;
}
_rebuildBuffers() {
// Uniforms
for (const uniformBuffer of this._uniformBuffers) {
uniformBuffer._rebuildAfterContextLost();
}
}
/** @internal */
get _shouldUseHighPrecisionShader() {
return !!(this._caps.highPrecisionShaderSupported && this._highPrecisionShadersAllowed);
}
/**
* Gets host document
* @returns the host document object
*/
getHostDocument() {
if (this._renderingCanvas && this._renderingCanvas.ownerDocument) {
return this._renderingCanvas.ownerDocument;
}
return IsDocumentAvailable() ? document : null;
}
/**
* Gets the list of loaded textures
* @returns an array containing all loaded textures
*/
getLoadedTexturesCache() {
return this._internalTexturesCache;
}
/**
* Clears the list of texture accessible through engine.
* This can help preventing texture load conflict due to name collision.
*/
clearInternalTexturesCache() {
this._internalTexturesCache.length = 0;
}
/**
* Gets the object containing all engine capabilities
* @returns the EngineCapabilities object
*/
getCaps() {
return this._caps;
}
/**
* Reset the texture cache to empty state
*/
resetTextureCache() {
for (const key in this._boundTexturesCache) {
if (!Object.prototype.hasOwnProperty.call(this._boundTexturesCache, key)) {
continue;
}
this._boundTexturesCache[key] = null;
}
this._currentTextureChannel = -1;
}
/**
* Gets or sets the name of the engine
*/
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
/**
* Returns the current npm package of the sdk
*/
// Not mixed with Version for tooling purpose.
static get NpmPackage() {
return "babylonjs@8.4.0";
}
/**
* Returns the current version of the framework
*/
static get Version() {
return "8.4.0";
}
/**
* Gets the HTML canvas attached with the current webGL context
* @returns a HTML canvas
*/
getRenderingCanvas() {
return this._renderingCanvas;
}
/**
* Gets the audio context specified in engine initialization options
* @deprecated please use AudioEngineV2 instead
* @returns an Audio Context
*/
getAudioContext() {
return this._audioContext;
}
/**
* Gets the audio destination specified in engine initialization options
* @deprecated please use AudioEngineV2 instead
* @returns an audio destination node
*/
getAudioDestination() {
return this._audioDestination;
}
/**
* Defines the hardware scaling level.
* By default the hardware scaling level is computed from the window device ratio.
* if level = 1 then the engine will render at the exact resolution of the canvas. If level = 0.5 then the engine will render at twice the size of the canvas.
* @param level defines the level to use
*/
setHardwareScalingLevel(level) {
this._hardwareScalingLevel = level;
this.resize();
}
/**
* Gets the current hardware scaling level.
* By default the hardware scaling level is computed from the window device ratio.
* if level = 1 then the engine will render at the exact resolution of the canvas. If level = 0.5 then the engine will render at twice the size of the canvas.
* @returns a number indicating the current hardware scaling level
*/
getHardwareScalingLevel() {
return this._hardwareScalingLevel;
}
/**
* Gets or sets a boolean indicating if resources should be retained to be able to handle context lost events
* @see https://doc.babylonjs.com/features/featuresDeepDive/scene/optimize_your_scene#handling-webgl-context-lost
*/
get doNotHandleContextLost() {
return this._doNotHandleContextLost;
}
set doNotHandleContextLost(value) {
this._doNotHandleContextLost = value;
}
/**
* Returns true if the stencil buffer has been enabled through the creation option of the context.
*/
get isStencilEnable() {
return this._isStencilEnable;
}
/**
* Gets the options used for engine creation
* @returns EngineOptions object
*/
getCreationOptions() {
return this._creationOptions;
}
/**
* Creates a new engine
* @param antialias defines whether anti-aliasing should be enabled. If undefined, it means that the underlying engine is free to enable it or not
* @param options defines further options to be sent to the creation context
* @param adaptToDeviceRatio defines whether to adapt to the device's viewport characteristics (default: false)
*/
constructor(antialias, options, adaptToDeviceRatio) {
// States
/** @internal */
this._colorWrite = true;
/** @internal */
this._colorWriteChanged = true;
/** @internal */
this._depthCullingState = new DepthCullingState();
/** @internal */
this._stencilStateComposer = new StencilStateComposer();
/** @internal */
this._stencilState = new StencilState();
/** @internal */
this._alphaState = new AlphaState();
/** @internal */
this._alphaMode = 1;
/** @internal */
this._alphaEquation = 0;
this._activeRequests = [];
/** @internal */
this._badOS = false;
/** @internal */
this._badDesktopOS = false;
this._compatibilityMode = true;
/** @internal */
this._internalTexturesCache = new Array();
/** @internal */
this._currentRenderTarget = null;
/** @internal */
this._boundTexturesCache = {};
/** @internal */
this._activeChannel = 0;
/** @internal */
this._currentTextureChannel = -1;
/** @internal */
this._viewportCached = { x: 0, y: 0, z: 0, w: 0 };
/** @internal */
this._isWebGPU = false;
/**
* Observable event triggered each time the canvas loses focus
*/
this.onCanvasBlurObservable = new Observable();
/**
* Observable event triggered each time the canvas gains focus
*/
this.onCanvasFocusObservable = new Observable();
/**
* Event raised when a new scene is created
*/
this.onNewSceneAddedObservable = new Observable();
/**
* Observable event triggered each time the rendering canvas is resized
*/
this.onResizeObservable = new Observable();
/**
* Observable event triggered each time the canvas receives pointerout event
*/
this.onCanvasPointerOutObservable = new Observable();
/**
* Observable event triggered each time an effect compilation fails
*/
this.onEffectErrorObservable = new Observable();
/**
* Turn this value on if you want to pause FPS computation when in background
*/
this.disablePerformanceMonitorInBackground = false;
/**
* Gets or sets a boolean indicating that vertex array object must be disabled even if they are supported
*/
this.disableVertexArrayObjects = false;
/** @internal */
this._frameId = 0;
/**
* Gets information about the current host
*/
this.hostInformation = {
isMobile: false,
};
/**
* Gets a boolean indicating if the engine is currently rendering in fullscreen mode
*/
this.isFullscreen = false;
/**
* Gets or sets a boolean to enable/disable IndexedDB support and avoid XHR on .manifest
**/
this.enableOfflineSupport = false;
/**
* Gets or sets a boolean to enable/disable checking manifest if IndexedDB support is enabled (js will always consider the database is up to date)
**/
this.disableManifestCheck = false;
/**
* Gets or sets a boolean to enable/disable the context menu (right-click) from appearing on the main canvas
*/
this.disableContextMenu = true;
/**
* Gets or sets the current render pass id
*/
this.currentRenderPassId = 0;
/**
* Gets a boolean indicating if the pointer is currently locked
*/
this.isPointerLock = false;
/**
* Gets the list of created postprocesses
*/
this.postProcesses = [];
/** Gets or sets the tab index to set to the rendering canvas. 1 is the minimum value to set to be able to capture keyboard events */
this.canvasTabIndex = 1;
/** @internal */
this._contextWasLost = false;
this._useReverseDepthBuffer = false;
/**
* Indicates if the z range in NDC space is 0..1 (value: true) or -1..1 (value: false)
*/
this.isNDCHalfZRange = false;
/**
* Indicates that the origin of the texture/framebuffer space is the bottom left corner. If false, the origin is top left
*/
this.hasOriginBottomLeft = true;
/** @internal */
this._renderTargetWrapperCache = new Array();
/** @internal */
this._compiledEffects = {};
/** @internal */
this._isDisposed = false;
/**
* Gets the list of created scenes
*/
this.scenes = [];
/** @internal */
this._virtualScenes = new Array();
/**
* Observable event triggered before each texture is initialized
*/
this.onBeforeTextureInitObservable = new Observable();
/**
* Gets or sets a boolean indicating if the engine must keep rendering even if the window is not in foreground
*/
this.renderEvenInBackground = true;
/**
* Gets or sets a boolean indicating that cache can be kept between frames
*/
this.preventCacheWipeBetweenFrames = false;
/** @internal */
this._frameHandler = 0;
/** @internal */
this._activeRenderLoops = new Array();
/** @internal */
this._windowIsBackground = false;
/** @internal */
this._boundRenderFunction = (timestamp) => this._renderLoop(timestamp);
this._lastFrameTime = 0;
this._renderAccumulator = 0;
/**
* Skip frame rendering but keep the frame heartbeat (begin/end frame).
* This is useful if you need all the plumbing but not the rendering work.
* (for instance when capturing a screenshot where you do not want to mix rendering to the screen and to the screenshot)
*/
this.skipFrameRender = false;
/**
* Observable raised when the engine is about to compile a shader
*/
this.onBeforeShaderCompilationObservable = new Observable();
/**
* Observable raised when the engine has just compiled a shader
*/
this.onAfterShaderCompilationObservable = new Observable();
/**
* Observable raised when the engine begins a new frame
*/
this.onBeginFrameObservable = new Observable();
/**
* Observable raised when the engine ends the current frame
*/
this.onEndFrameObservable = new Observable();
/** @internal */
this._transformTextureUrl = null;
/** @internal */
this._uniformBuffers = new Array();
/** @internal */
this._storageBuffers = new Array();
this._highPrecisionShadersAllowed = true;
// Lost context
/**
* Observable signaled when a context lost event is raised
*/
this.onContextLostObservable = new Observable();
/**
* Observable signaled when a context restored event is raised
*/
this.onContextRestoredObservable = new Observable();
/** @internal */
this._name = "";
/**
* Defines whether the engine has been created with the premultipliedAlpha option on or not.
*/
this.premultipliedAlpha = true;
/**
* If set to true zooming in and out in the browser will rescale the hardware-scaling correctly.
*/
this.adaptToDeviceRatio = false;
/** @internal */
this._lastDevicePixelRatio = 1.0;
/** @internal */
this._doNotHandleContextLost = false;
/**
* Gets or sets a boolean indicating if back faces must be culled. If false, front faces are culled instead (true by default)
* If non null, this takes precedence over the value from the material
*/
this.cullBackFaces = null;
/** @internal */
this._renderPassNames = ["main"];
// FPS
this._fps = 60;
this._deltaTime = 0;
// Deterministic lockstepMaxSteps
/** @internal */
this._deterministicLockstep = false;
/** @internal */
this._lockstepMaxSteps = 4;
/** @internal */
this._timeStep = 1 / 60;
/**
* An event triggered when the engine is disposed.
*/
this.onDisposeObservable = new Observable();
/**
* An event triggered when a global cleanup of all effects is required
*/
this.onReleaseEffectsObservable = new Observable();
EngineStore.Instances.push(this);
this.startTime = PrecisionDate.Now;
this._stencilStateComposer.stencilGlobal = this._stencilState;
PerformanceConfigurator.SetMatrixPrecision(!!options.useHighPrecisionMatrix);
if (IsNavigatorAvailable() && navigator.userAgent) {
// Detect if we are running on a faulty buggy OS.
this._badOS = /iPad/i.test(navigator.userAgent) || /iPhone/i.test(navigator.userAgent);
// Detect if we are running on a faulty buggy desktop OS.
this._badDesktopOS = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
// Save this off for use in resize().
this.adaptToDeviceRatio = adaptToDeviceRatio ?? false;
options.antialias = antialias ?? options.antialias;
options.deterministicLockstep = options.deterministicLockstep ?? false;
options.lockstepMaxSteps = options.lockstepMaxSteps ?? 4;
options.timeStep = options.timeStep ?? 1 / 60;
options.stencil = options.stencil ?? true;
this._audioContext = options.audioEngineOptions?.audioContext ?? null;
this._audioDestination = options.audioEngineOptions?.audioDestination ?? null;
this.premultipliedAlpha = options.premultipliedAlpha ?? true;
this._doNotHandleContextLost = !!options.doNotHandleContextLost;
this._isStencilEnable = options.stencil ? true : false;
this.useExactSrgbConversions = options.useExactSrgbConversions ?? false;
const devicePixelRatio = IsWindowObjectExist() ? window.devicePixelRatio || 1.0 : 1.0;
const limitDeviceRatio = options.limitDeviceRatio || devicePixelRatio;
// Viewport
adaptToDeviceRatio = adaptToDeviceRatio || options.adaptToDeviceRatio || false;
this._hardwareScalingLevel = adaptToDeviceRatio ? 1.0 / Math.min(limitDeviceRatio, devicePixelRatio) : 1.0;
this._lastDevicePixelRatio = devicePixelRatio;
this._creationOptions = options;
}
/**
* Resize the view according to the canvas' size
* @param forceSetSize true to force setting the sizes of the underlying canvas
*/
resize(forceSetSize = false) {
let width;
let height;
// Re-query hardware scaling level to handle zoomed-in resizing.
if (this.adaptToDeviceRatio) {
const devicePixelRatio = IsWindowObjectExist() ? window.devicePixelRatio || 1.0 : 1.0;
const changeRatio = this._lastDevicePixelRatio / devicePixelRatio;
this._lastDevicePixelRatio = devicePixelRatio;
this._hardwareScalingLevel *= changeRatio;
}
if (IsWindowObjectExist() && IsDocumentAvailable()) {
// make sure it is a Node object, and is a part of the document.
if (this._renderingCanvas) {
const boundingRect = this._renderingCanvas.getBoundingClientRect?.();
width = this._renderingCanvas.clientWidth || boundingRect?.width || this._renderingCanvas.width * this._hardwareScalingLevel || 100;
height = this._renderingCanvas.clientHeight || boundingRect?.height || this._renderingCanvas.height * this._hardwareScalingLevel || 100;
}
else {
width = window.innerWidth;
height = window.innerHeight;
}
}
else {
width = this._renderingCanvas ? this._renderingCanvas.width : 100;
height = this._renderingCanvas ? this._renderingCanvas.height : 100;
}
this.setSize(width / this._hardwareScalingLevel, height / this._hardwareScalingLevel, forceSetSize);
}
/**
* Force a specific size of the canvas
* @param width defines the new canvas' width
* @param height defines the new canvas' height
* @param forceSetSize true to force setting the sizes of the underlying canvas
* @returns true if the size was changed
*/
setSize(width, height, forceSetSize = false) {
if (!this._renderingCanvas) {
return false;
}
width = width | 0;
height = height | 0;
if (!forceSetSize && this._renderingCanvas.width === width && this._renderingCanvas.height === height) {
return false;
}
this._renderingCanvas.width = width;
this._renderingCanvas.height = height;
if (this.scenes) {
for (let index = 0; index < this.scenes.length; index++) {
const scene = this.scenes[index];
for (let camIndex = 0; camIndex < scene.cameras.length; camIndex++) {
const cam = scene.cameras[camIndex];
cam._currentRenderId = 0;
}
}
if (this.onResizeObservable.hasObservers()) {
this.onResizeObservable.notifyObservers(this);
}
}
return true;
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Creates a raw texture
* @param data defines the data to store in the texture
* @param width defines the width of the texture
* @param height defines the height of the texture
* @param format defines the format of the data
* @param generateMipMaps defines if the engine should generate the mip levels
* @param invertY defines if data must be stored with Y axis inverted
* @param samplingMode defines the required sampling mode (Texture.NEAREST_SAMPLINGMODE by default)
* @param compression defines the compression used (null by default)
* @param type defines the type fo the data (Engine.TEXTURETYPE_UNSIGNED_BYTE by default)
* @param creationFlags specific flags to use when creating the texture (1 for storage textures, for eg)
* @param useSRGBBuffer defines if the texture must be loaded in a sRGB GPU buffer (if supported by the GPU).
* @returns the raw texture inside an InternalTexture
*/
createRawTexture(data, width, height, format, generateMipMaps, invertY, samplingMode, compression, type, creationFlags, useSRGBBuffer) {
throw _WarnImport("engine.rawTexture");
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Creates a new raw cube texture
* @param data defines the array of data to use to create each face
* @param size defines the size of the textures
* @param format defines the format of the data
* @param type defines the type of the data (like Engine.TEXTURETYPE_UNSIGNED_BYTE)
* @param generateMipMaps defines if the engine should generate the mip levels
* @param invertY defines if data must be stored with Y axis inverted
* @param samplingMode defines the required sampling mode (like Texture.NEAREST_SAMPLINGMODE)
* @param compression defines the compression used (null by default)
* @returns the cube texture as an InternalTexture
*/
createRawCubeTexture(data, size, format, type, generateMipMaps, invertY, samplingMode, compression) {
throw _WarnImport("engine.rawTexture");
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Creates a new raw 3D texture
* @param data defines the data used to create the texture
* @param width defines the width of the texture
* @param height defines the height of the texture
* @param depth defines the depth of the texture
* @param format defines the format of the texture
* @param generateMipMaps defines if the engine must generate mip levels
* @param invertY defines if data must be stored with Y axis inverted
* @param samplingMode defines the required sampling mode (like Texture.NEAREST_SAMPLINGMODE)
* @param compression defines the compressed used (can be null)
* @param textureType defines the compressed used (can be null)
* @param creationFlags specific flags to use when creating the texture (1 for storage textures, for eg)
* @returns a new raw 3D texture (stored in an InternalTexture)
*/
createRawTexture3D(data, width, height, depth, format, generateMipMaps, invertY, samplingMode, compression, textureType, creationFlags) {
throw _WarnImport("engine.rawTexture");
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Creates a new raw 2D array texture
* @param data defines the data used to create the texture
* @param width defines the width of the texture
* @param height defines the height of the texture
* @param depth defines the number of layers of the texture
* @param format defines the format of the texture
* @para