@animech-public/playcanvas
Version:
PlayCanvas WebGL game engine
908 lines (862 loc) • 27.1 kB
JavaScript
import { extends as _extends } from '../../../_virtual/_rollupPluginBabelHelpers.js';
import { Debug } from '../../core/debug.js';
import { EventHandler } from '../../core/event-handler.js';
import { platform } from '../../core/platform.js';
import { now } from '../../core/time.js';
import { Vec2 } from '../../core/math/vec2.js';
import { Tracing } from '../../core/tracing.js';
import { Color } from '../../core/math/color.js';
import { TRACEID_TEXTURES } from '../../core/constants.js';
import { CLEARFLAG_COLOR, CLEARFLAG_DEPTH, PRIMITIVE_TRIFAN, SEMANTIC_POSITION, TYPE_FLOAT32, CULLFACE_BACK, PIXELFORMAT_RGBA32F, PIXELFORMAT_RGBA16F, PIXELFORMAT_111110F, PRIMITIVE_POINTS } from './constants.js';
import { BlendState } from './blend-state.js';
import { DepthState } from './depth-state.js';
import { ScopeSpace } from './scope-space.js';
import { VertexBuffer } from './vertex-buffer.js';
import { VertexFormat } from './vertex-format.js';
import { StencilParameters } from './stencil-parameters.js';
/**
* The graphics device manages the underlying graphics context. It is responsible for submitting
* render state changes and graphics primitives to the hardware. A graphics device is tied to a
* specific canvas HTML element. It is valid to have more than one canvas element per page and
* create a new graphics device against each.
*
* @category Graphics
*/
class GraphicsDevice extends EventHandler {
constructor(canvas, options) {
var _this$initOptions, _this$initOptions$dep, _this$initOptions2, _this$initOptions2$st, _this$initOptions3, _this$initOptions3$an, _this$initOptions4, _this$initOptions4$po;
super();
/**
* Fired when the canvas is resized. The handler is passed the new width and height as number
* parameters.
*
* @event
* @example
* graphicsDevice.on('resizecanvas', (width, height) => {
* console.log(`The canvas was resized to ${width}x${height}`);
* });
*/
/**
* The canvas DOM element that provides the underlying WebGL context used by the graphics device.
*
* @type {HTMLCanvasElement}
* @readonly
*/
this.canvas = void 0;
/**
* The render target representing the main back-buffer.
*
* @type {import('./render-target.js').RenderTarget|null}
* @ignore
*/
this.backBuffer = null;
/**
* The dimensions of the back buffer.
*
* @ignore
*/
this.backBufferSize = new Vec2();
/**
* The pixel format of the back buffer. Typically PIXELFORMAT_RGBA8, PIXELFORMAT_BGRA8 or
* PIXELFORMAT_RGB8.
*
* @ignore
*/
this.backBufferFormat = void 0;
/**
* True if the back buffer should use anti-aliasing.
*
* @type {boolean}
*/
this.backBufferAntialias = false;
/**
* True if the deviceType is WebGPU
*
* @type {boolean}
* @readonly
*/
this.isWebGPU = false;
/**
* True if the deviceType is WebGL1
*
* @type {boolean}
* @readonly
*/
this.isWebGL1 = false;
/**
* True if the deviceType is WebGL2
*
* @type {boolean}
* @readonly
*/
this.isWebGL2 = false;
/**
* The scope namespace for shader attributes and variables.
*
* @type {ScopeSpace}
* @readonly
*/
this.scope = void 0;
/**
* The maximum number of supported bones using uniform buffers.
*
* @type {number}
* @readonly
*/
this.boneLimit = void 0;
/**
* The maximum supported texture anisotropy setting.
*
* @type {number}
* @readonly
*/
this.maxAnisotropy = void 0;
/**
* The maximum supported dimension of a cube map.
*
* @type {number}
* @readonly
*/
this.maxCubeMapSize = void 0;
/**
* The maximum supported dimension of a texture.
*
* @type {number}
* @readonly
*/
this.maxTextureSize = void 0;
/**
* The maximum supported dimension of a 3D texture (any axis).
*
* @type {number}
* @readonly
*/
this.maxVolumeSize = void 0;
/**
* The maximum supported number of color buffers attached to a render target.
*
* @type {number}
* @readonly
*/
this.maxColorAttachments = 1;
/**
* The highest shader precision supported by this graphics device. Can be 'hiphp', 'mediump' or
* 'lowp'.
*
* @type {string}
* @readonly
*/
this.precision = void 0;
/**
* The number of hardware anti-aliasing samples used by the frame buffer.
*
* @readonly
* @type {number}
*/
this.samples = void 0;
/**
* True if the main framebuffer contains stencil attachment.
*
* @ignore
* @type {boolean}
*/
this.supportsStencil = void 0;
/**
* True if Multiple Render Targets feature is supported. This refers to the ability to render to
* multiple color textures with a single draw call.
*
* @readonly
* @type {boolean}
*/
this.supportsMrt = false;
/**
* True if the device supports volume textures.
*
* @readonly
* @type {boolean}
*/
this.supportsVolumeTextures = false;
/**
* True if the device supports compute shaders.
*
* @readonly
* @type {boolean}
*/
this.supportsCompute = false;
/**
* True if the device can read from StorageTexture in the compute shader. By default, the
* storage texture can be only used with the write operation.
* When a shader uses this feature, it's recommended to use a `requires` directive to signal the
* potential for non-portability at the top of the WGSL shader code:
* ```javascript
* requires readonly_and_readwrite_storage_textures;
* ```
*
* @readonly
* @type {boolean}
*/
this.supportsStorageTextureRead = false;
/**
* Currently active render target.
*
* @type {import('./render-target.js').RenderTarget|null}
* @ignore
*/
this.renderTarget = null;
/**
* Array of objects that need to be re-initialized after a context restore event
*
* @type {import('./shader.js').Shader[]}
* @ignore
*/
this.shaders = [];
/**
* An array of currently created textures.
*
* @type {import('./texture.js').Texture[]}
* @ignore
*/
this.textures = [];
/**
* A set of currently created render targets.
*
* @type {Set<import('./render-target.js').RenderTarget>}
* @ignore
*/
this.targets = new Set();
/**
* A version number that is incremented every frame. This is used to detect if some object were
* invalidated.
*
* @type {number}
* @ignore
*/
this.renderVersion = 0;
/**
* Index of the currently active render pass.
*
* @type {number}
* @ignore
*/
this.renderPassIndex = void 0;
/** @type {boolean} */
this.insideRenderPass = false;
/**
* True if hardware instancing is supported.
*
* @type {boolean}
* @readonly
*/
this.supportsInstancing = void 0;
/**
* True if the device supports uniform buffers.
*
* @type {boolean}
* @ignore
*/
this.supportsUniformBuffers = false;
/**
* True if 32-bit floating-point textures can be used as a frame buffer.
*
* @type {boolean}
* @readonly
*/
this.textureFloatRenderable = void 0;
/**
* True if 16-bit floating-point textures can be used as a frame buffer.
*
* @type {boolean}
* @readonly
*/
this.textureHalfFloatRenderable = void 0;
/**
* True if filtering can be applied when sampling float textures.
*
* @type {boolean}
* @readonly
*/
this.textureFloatFilterable = false;
/**
* True if filtering can be applied when sampling 16-bit float textures.
*
* @type {boolean}
* @readonly
*/
this.textureHalfFloatFilterable = false;
/**
* A vertex buffer representing a quad.
*
* @type {VertexBuffer}
* @ignore
*/
this.quadVertexBuffer = void 0;
/**
* An object representing current blend state
*
* @ignore
*/
this.blendState = new BlendState();
/**
* The current depth state.
*
* @ignore
*/
this.depthState = new DepthState();
/**
* True if stencil is enabled and stencilFront and stencilBack are used
*
* @ignore
*/
this.stencilEnabled = false;
/**
* The current front stencil parameters.
*
* @ignore
*/
this.stencilFront = new StencilParameters();
/**
* The current back stencil parameters.
*
* @ignore
*/
this.stencilBack = new StencilParameters();
/**
* The dynamic buffer manager.
*
* @type {import('./dynamic-buffers.js').DynamicBuffers}
* @ignore
*/
this.dynamicBuffers = void 0;
/**
* The GPU profiler.
*
* @type {import('./gpu-profiler.js').GpuProfiler}
*/
this.gpuProfiler = void 0;
this.defaultClearOptions = {
color: [0, 0, 0, 1],
depth: 1,
stencil: 0,
flags: CLEARFLAG_COLOR | CLEARFLAG_DEPTH
};
/**
* The current client rect.
*
* @type {{ width: number, height: number }}
* @ignore
*/
this.clientRect = {
width: 0,
height: 0
};
this.canvas = canvas;
// copy options and handle defaults
this.initOptions = _extends({}, options);
(_this$initOptions$dep = (_this$initOptions = this.initOptions).depth) != null ? _this$initOptions$dep : _this$initOptions.depth = true;
(_this$initOptions2$st = (_this$initOptions2 = this.initOptions).stencil) != null ? _this$initOptions2$st : _this$initOptions2.stencil = true;
(_this$initOptions3$an = (_this$initOptions3 = this.initOptions).antialias) != null ? _this$initOptions3$an : _this$initOptions3.antialias = true;
(_this$initOptions4$po = (_this$initOptions4 = this.initOptions).powerPreference) != null ? _this$initOptions4$po : _this$initOptions4.powerPreference = 'high-performance';
// Some devices window.devicePixelRatio can be less than one
// eg Oculus Quest 1 which returns a window.devicePixelRatio of 0.8
this._maxPixelRatio = platform.browser ? Math.min(1, window.devicePixelRatio) : 1;
this.buffers = [];
this._vram = {
texShadow: 0,
texAsset: 0,
texLightmap: 0,
tex: 0,
vb: 0,
ib: 0,
ub: 0,
sb: 0
};
this._shaderStats = {
vsCompiled: 0,
fsCompiled: 0,
linked: 0,
materialShaders: 0,
compileTime: 0
};
this.initializeContextCaches();
// Profiler stats
this._drawCallsPerFrame = 0;
this._shaderSwitchesPerFrame = 0;
this._primsPerFrame = [];
for (let i = PRIMITIVE_POINTS; i <= PRIMITIVE_TRIFAN; i++) {
this._primsPerFrame[i] = 0;
}
this._renderTargetCreationTime = 0;
// Create the ScopeNamespace for shader attributes and variables
this.scope = new ScopeSpace('Device');
this.textureBias = this.scope.resolve('textureBias');
this.textureBias.setValue(0.0);
}
/**
* Function that executes after the device has been created.
*/
postInit() {
// create quad vertex buffer
const vertexFormat = new VertexFormat(this, [{
semantic: SEMANTIC_POSITION,
components: 2,
type: TYPE_FLOAT32
}]);
const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
this.quadVertexBuffer = new VertexBuffer(this, vertexFormat, 4, {
data: positions
});
}
/**
* Destroy the graphics device.
*/
destroy() {
var _this$quadVertexBuffe, _this$dynamicBuffers, _this$gpuProfiler;
// fire the destroy event.
// textures and other device resources may destroy themselves in response.
this.fire('destroy');
(_this$quadVertexBuffe = this.quadVertexBuffer) == null || _this$quadVertexBuffe.destroy();
this.quadVertexBuffer = null;
(_this$dynamicBuffers = this.dynamicBuffers) == null || _this$dynamicBuffers.destroy();
this.dynamicBuffers = null;
(_this$gpuProfiler = this.gpuProfiler) == null || _this$gpuProfiler.destroy();
this.gpuProfiler = null;
}
onDestroyShader(shader) {
this.fire('destroy:shader', shader);
const idx = this.shaders.indexOf(shader);
if (idx !== -1) {
this.shaders.splice(idx, 1);
}
}
// executes after the extended classes have executed their destroy function
postDestroy() {
this.scope = null;
this.canvas = null;
}
/**
* Called when the device context was lost. It releases all context related resources.
*
* @ignore
*/
loseContext() {
var _this$gpuProfiler2;
this.contextLost = true;
// force the back-buffer to be recreated on restore
this.backBufferSize.set(-1, -1);
// release textures
for (const texture of this.textures) {
texture.loseContext();
}
// release vertex and index buffers
for (const buffer of this.buffers) {
buffer.loseContext();
}
// Reset all render targets so they'll be recreated as required.
// TODO: a solution for the case where a render target contains something
// that was previously generated that needs to be re-rendered.
for (const target of this.targets) {
target.loseContext();
}
(_this$gpuProfiler2 = this.gpuProfiler) == null || _this$gpuProfiler2.loseContext();
}
/**
* Called when the device context is restored. It reinitializes all context related resources.
*
* @ignore
*/
restoreContext() {
var _this$gpuProfiler3;
this.contextLost = false;
this.initializeRenderState();
this.initializeContextCaches();
// Recreate buffer objects and reupload buffer data to the GPU
for (const buffer of this.buffers) {
buffer.unlock();
}
(_this$gpuProfiler3 = this.gpuProfiler) == null || _this$gpuProfiler3.restoreContext == null || _this$gpuProfiler3.restoreContext();
}
// don't stringify GraphicsDevice to JSON by JSON.stringify
toJSON(key) {
return undefined;
}
initializeContextCaches() {
this.indexBuffer = null;
this.vertexBuffers = [];
this.shader = null;
this.shaderValid = undefined;
this.shaderAsyncCompile = false;
this.renderTarget = null;
}
initializeRenderState() {
this.blendState = new BlendState();
this.depthState = new DepthState();
this.cullMode = CULLFACE_BACK;
// Cached viewport and scissor dimensions
this.vx = this.vy = this.vw = this.vh = 0;
this.sx = this.sy = this.sw = this.sh = 0;
this.blendColor = new Color(0, 0, 0, 0);
}
/**
* Sets the specified stencil state. If both stencilFront and stencilBack are null, stencil
* operation is disabled.
*
* @param {StencilParameters} [stencilFront] - The front stencil parameters. Defaults to
* {@link StencilParameters.DEFAULT} if not specified.
* @param {StencilParameters} [stencilBack] - The back stencil parameters. Defaults to
* {@link StencilParameters.DEFAULT} if not specified.
*/
setStencilState(stencilFront, stencilBack) {
Debug.assert(false);
}
/**
* Sets the specified blend state.
*
* @param {BlendState} blendState - New blend state.
*/
setBlendState(blendState) {
Debug.assert(false);
}
/**
* Sets the constant blend color and alpha values used with {@link BLENDMODE_CONSTANT} and
* {@link BLENDMODE_ONE_MINUS_CONSTANT} factors specified in {@link BlendState}. Defaults to
* [0, 0, 0, 0].
*
* @param {number} r - The value for red.
* @param {number} g - The value for green.
* @param {number} b - The value for blue.
* @param {number} a - The value for alpha.
*/
setBlendColor(r, g, b, a) {
Debug.assert(false);
}
/**
* Sets the specified depth state.
*
* @param {DepthState} depthState - New depth state.
*/
setDepthState(depthState) {
Debug.assert(false);
}
/**
* Controls how triangles are culled based on their face direction. The default cull mode is
* {@link CULLFACE_BACK}.
*
* @param {number} cullMode - The cull mode to set. Can be:
*
* - {@link CULLFACE_NONE}
* - {@link CULLFACE_BACK}
* - {@link CULLFACE_FRONT}
*/
setCullMode(cullMode) {
Debug.assert(false);
}
/**
* Sets the specified render target on the device. If null is passed as a parameter, the back
* buffer becomes the current target for all rendering operations.
*
* @param {import('./render-target.js').RenderTarget|null} renderTarget - The render target to
* activate.
* @example
* // Set a render target to receive all rendering output
* device.setRenderTarget(renderTarget);
*
* // Set the back buffer to receive all rendering output
* device.setRenderTarget(null);
*/
setRenderTarget(renderTarget) {
this.renderTarget = renderTarget;
}
/**
* Sets the current index buffer on the graphics device. On subsequent calls to
* {@link GraphicsDevice#draw}, the specified index buffer will be used to provide index data
* for any indexed primitives.
*
* @param {import('./index-buffer.js').IndexBuffer|null} indexBuffer - The index buffer to assign to
* the device.
*/
setIndexBuffer(indexBuffer) {
// Store the index buffer
this.indexBuffer = indexBuffer;
}
/**
* Sets the current vertex buffer on the graphics device. On subsequent calls to
* {@link GraphicsDevice#draw}, the specified vertex buffer(s) will be used to provide vertex
* data for any primitives.
*
* @param {import('./vertex-buffer.js').VertexBuffer} vertexBuffer - The vertex buffer to
* assign to the device.
*/
setVertexBuffer(vertexBuffer) {
if (vertexBuffer) {
this.vertexBuffers.push(vertexBuffer);
}
}
/**
* Clears the vertex buffer set on the graphics device. This is called automatically by the
* renderer.
* @ignore
*/
clearVertexBuffer() {
this.vertexBuffers.length = 0;
}
/**
* Queries the currently set render target on the device.
*
* @returns {import('./render-target.js').RenderTarget} The current render target.
* @example
* // Get the current render target
* const renderTarget = device.getRenderTarget();
*/
getRenderTarget() {
return this.renderTarget;
}
/**
* Initialize render target before it can be used.
*
* @param {import('./render-target.js').RenderTarget} target - The render target to be
* initialized.
* @ignore
*/
initRenderTarget(target) {
if (target.initialized) return;
const startTime = now();
this.fire('fbo:create', {
timestamp: startTime,
target: this
});
target.init();
this.targets.add(target);
this._renderTargetCreationTime += now() - startTime;
}
/**
* Reports whether a texture source is a canvas, image, video or ImageBitmap.
*
* @param {*} texture - Texture source data.
* @returns {boolean} True if the texture is a canvas, image, video or ImageBitmap and false
* otherwise.
* @ignore
*/
_isBrowserInterface(texture) {
return this._isImageBrowserInterface(texture) || this._isImageCanvasInterface(texture) || this._isImageVideoInterface(texture);
}
_isImageBrowserInterface(texture) {
return typeof ImageBitmap !== 'undefined' && texture instanceof ImageBitmap || typeof HTMLImageElement !== 'undefined' && texture instanceof HTMLImageElement;
}
_isImageCanvasInterface(texture) {
return typeof HTMLCanvasElement !== 'undefined' && texture instanceof HTMLCanvasElement;
}
_isImageVideoInterface(texture) {
return typeof HTMLVideoElement !== 'undefined' && texture instanceof HTMLVideoElement;
}
/**
* Sets the width and height of the canvas, then fires the `resizecanvas` event. Note that the
* specified width and height values will be multiplied by the value of
* {@link GraphicsDevice#maxPixelRatio} to give the final resultant width and height for the
* canvas.
*
* @param {number} width - The new width of the canvas.
* @param {number} height - The new height of the canvas.
* @ignore
*/
resizeCanvas(width, height) {
const pixelRatio = Math.min(this._maxPixelRatio, platform.browser ? window.devicePixelRatio : 1);
const w = Math.floor(width * pixelRatio);
const h = Math.floor(height * pixelRatio);
if (w !== this.canvas.width || h !== this.canvas.height) {
this.setResolution(w, h);
}
}
/**
* Sets the width and height of the canvas, then fires the `resizecanvas` event. Note that the
* value of {@link GraphicsDevice#maxPixelRatio} is ignored.
*
* @param {number} width - The new width of the canvas.
* @param {number} height - The new height of the canvas.
* @ignore
*/
setResolution(width, height) {
this.canvas.width = width;
this.canvas.height = height;
this.fire(GraphicsDevice.EVENT_RESIZE, width, height);
}
updateClientRect() {
if (platform.worker) {
// Web Workers don't do page layout, so getBoundingClientRect is not available
this.clientRect.width = this.canvas.width;
this.clientRect.height = this.canvas.height;
} else {
const rect = this.canvas.getBoundingClientRect();
this.clientRect.width = rect.width;
this.clientRect.height = rect.height;
}
}
/**
* Width of the back buffer in pixels.
*
* @type {number}
*/
get width() {
return this.canvas.width;
}
/**
* Height of the back buffer in pixels.
*
* @type {number}
*/
get height() {
return this.canvas.height;
}
/**
* Sets whether the device is currently in fullscreen mode.
*
* @type {boolean}
*/
set fullscreen(fullscreen) {
Debug.error('GraphicsDevice.fullscreen is not implemented on current device.');
}
/**
* Gets whether the device is currently in fullscreen mode.
*
* @type {boolean}
*/
get fullscreen() {
Debug.error('GraphicsDevice.fullscreen is not implemented on current device.');
return false;
}
/**
* Sets the maximum pixel ratio.
*
* @type {number}
*/
set maxPixelRatio(ratio) {
this._maxPixelRatio = ratio;
}
/**
* Gets the maximum pixel ratio.
*
* @type {number}
*/
get maxPixelRatio() {
return this._maxPixelRatio;
}
/**
* Gets the type of the device. Can be:
*
* - {@link DEVICETYPE_WEBGL1}
* - {@link DEVICETYPE_WEBGL2}
* - {@link DEVICETYPE_WEBGPU}
*
* @type {import('./constants.js').DEVICETYPE_WEBGL1 | import('./constants.js').DEVICETYPE_WEBGL2 | import('./constants.js').DEVICETYPE_WEBGPU}
*/
get deviceType() {
return this._deviceType;
}
/**
* Queries the maximum number of bones that can be referenced by a shader. The shader
* generators (programlib) use this number to specify the matrix array size of the uniform
* 'matrix_pose[0]'. The value is calculated based on the number of available uniform vectors
* available after subtracting the number taken by a typical heavyweight shader. If a different
* number is required, it can be tuned via {@link GraphicsDevice#setBoneLimit}.
*
* @returns {number} The maximum number of bones that can be supported by the host hardware.
* @ignore
*/
getBoneLimit() {
return this.boneLimit;
}
/**
* Specifies the maximum number of bones that the device can support on the current hardware.
* This function allows the default calculated value based on available vector uniforms to be
* overridden.
*
* @param {number} maxBones - The maximum number of bones supported by the host hardware.
* @ignore
*/
setBoneLimit(maxBones) {
this.boneLimit = maxBones;
}
startRenderPass(renderPass) {}
endRenderPass(renderPass) {}
startComputePass() {}
endComputePass() {}
/**
* Function which executes at the start of the frame. This should not be called manually, as
* it is handled by the AppBase instance.
*
* @ignore
*/
frameStart() {
this.renderPassIndex = 0;
this.renderVersion++;
Debug.call(() => {
// log out all loaded textures, sorted by gpu memory size
if (Tracing.get(TRACEID_TEXTURES)) {
const textures = this.textures.slice();
textures.sort((a, b) => b.gpuSize - a.gpuSize);
Debug.log(`Textures: ${textures.length}`);
let textureTotal = 0;
textures.forEach((texture, index) => {
const textureSize = texture.gpuSize;
textureTotal += textureSize;
Debug.log(`${index}. ${texture.name} ${texture.width}x${texture.height} VRAM: ${(textureSize / 1024 / 1024).toFixed(2)} MB`);
});
Debug.log(`Total: ${(textureTotal / 1024 / 1024).toFixed(2)}MB`);
}
});
}
/**
* Function which executes at the end of the frame. This should not be called manually, as it is
* handled by the AppBase instance.
*
* @ignore
*/
frameEnd() {}
/**
* Dispatch multiple compute shaders inside a single compute shader pass.
*
* @param {Array<import('./compute.js').Compute>} computes - An array of compute shaders to
* dispatch.
*/
computeDispatch(computes) {}
/**
* Get a renderable HDR pixel format supported by the graphics device.
*
* @param {number[]} [formats] - An array of pixel formats to check for support. Can contain:
*
* - {@link PIXELFORMAT_111110F}
* - {@link PIXELFORMAT_RGBA16F}
* - {@link PIXELFORMAT_RGBA32F}
*
* @param {boolean} [filterable] - If true, the format also needs to be filterable. Defaults to
* true.
* @returns {number|undefined} The first supported renderable HDR format or undefined if none is
* supported.
*/
getRenderableHdrFormat(formats = [PIXELFORMAT_111110F, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F], filterable = true) {
for (let i = 0; i < formats.length; i++) {
const format = formats[i];
switch (format) {
case PIXELFORMAT_111110F:
{
if (this.textureRG11B10Renderable) {
return format;
}
break;
}
case PIXELFORMAT_RGBA16F:
if (this.textureHalfFloatRenderable && (!filterable || this.textureHalfFloatFilterable)) {
return format;
}
break;
case PIXELFORMAT_RGBA32F:
if (this.textureFloatRenderable && (!filterable || this.textureFloatFilterable)) {
return format;
}
break;
}
}
return undefined;
}
}
GraphicsDevice.EVENT_RESIZE = 'resizecanvas';
export { GraphicsDevice };