playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,036 lines (1,034 loc) • 102 kB
JavaScript
import { math } from '../../../core/math/math.js';
import { Debug } from '../../../core/debug.js';
import { platform } from '../../../core/platform.js';
import { Color } from '../../../core/math/color.js';
import { PIXELFORMAT_RGBA8, PIXELFORMAT_RGB8, FUNC_ALWAYS, STENCILOP_KEEP, TEXPROPERTY_MIN_FILTER, TEXPROPERTY_MAG_FILTER, TEXPROPERTY_ADDRESS_U, TEXPROPERTY_ADDRESS_V, TEXPROPERTY_ADDRESS_W, TEXPROPERTY_COMPARE_ON_READ, TEXPROPERTY_COMPARE_FUNC, TEXPROPERTY_ANISOTROPY, semanticToLocation, CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL, getPixelFormatArrayType, CULLFACE_NONE, DEVICETYPE_WEBGL2, UNIFORMTYPE_BOOL, UNIFORMTYPE_INT, UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC2, UNIFORMTYPE_VEC3, UNIFORMTYPE_VEC4, UNIFORMTYPE_IVEC2, UNIFORMTYPE_IVEC3, UNIFORMTYPE_IVEC4, UNIFORMTYPE_BVEC2, UNIFORMTYPE_BVEC3, UNIFORMTYPE_BVEC4, UNIFORMTYPE_MAT2, UNIFORMTYPE_MAT3, UNIFORMTYPE_MAT4, UNIFORMTYPE_TEXTURE2D, UNIFORMTYPE_TEXTURECUBE, UNIFORMTYPE_UINT, UNIFORMTYPE_UVEC2, UNIFORMTYPE_UVEC3, UNIFORMTYPE_UVEC4, UNIFORMTYPE_TEXTURE2D_SHADOW, UNIFORMTYPE_TEXTURECUBE_SHADOW, UNIFORMTYPE_TEXTURE2D_ARRAY, UNIFORMTYPE_TEXTURE3D, UNIFORMTYPE_ITEXTURE2D, UNIFORMTYPE_UTEXTURE2D, UNIFORMTYPE_ITEXTURECUBE, UNIFORMTYPE_UTEXTURECUBE, UNIFORMTYPE_ITEXTURE3D, UNIFORMTYPE_UTEXTURE3D, UNIFORMTYPE_ITEXTURE2D_ARRAY, UNIFORMTYPE_UTEXTURE2D_ARRAY, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY, UNIFORMTYPE_INTARRAY, UNIFORMTYPE_UINTARRAY, UNIFORMTYPE_BOOLARRAY, UNIFORMTYPE_IVEC2ARRAY, UNIFORMTYPE_UVEC2ARRAY, UNIFORMTYPE_BVEC2ARRAY, UNIFORMTYPE_IVEC3ARRAY, UNIFORMTYPE_UVEC3ARRAY, UNIFORMTYPE_BVEC3ARRAY, UNIFORMTYPE_IVEC4ARRAY, UNIFORMTYPE_UVEC4ARRAY, UNIFORMTYPE_BVEC4ARRAY, UNIFORMTYPE_MAT4ARRAY, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, FILTER_NEAREST, FILTER_LINEAR_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_LINEAR, FILTER_LINEAR, PIXELFORMAT_RG8, PIXELFORMAT_R8 } from '../constants.js';
import { GraphicsDevice } from '../graphics-device.js';
import { RenderTarget } from '../render-target.js';
import { Texture } from '../texture.js';
import { DebugGraphics } from '../debug-graphics.js';
import { WebglVertexBuffer } from './webgl-vertex-buffer.js';
import { WebglIndexBuffer } from './webgl-index-buffer.js';
import { WebglShader } from './webgl-shader.js';
import { WebglDrawCommands } from './webgl-draw-commands.js';
import { WebglTexture } from './webgl-texture.js';
import { WebglRenderTarget } from './webgl-render-target.js';
import { WebglUploadStream } from './webgl-upload-stream.js';
import { BlendState } from '../blend-state.js';
import { DepthState } from '../depth-state.js';
import { StencilParameters } from '../stencil-parameters.js';
import { WebglGpuProfiler } from './webgl-gpu-profiler.js';
import { TextureUtils } from '../texture-utils.js';
import { getBuiltInTexture } from '../built-in-textures.js';
/**
* @import { RenderPass } from '../render-pass.js'
* @import { Shader } from '../shader.js'
* @import { VertexBuffer } from '../vertex-buffer.js'
*/ /**
* Returns the number of channels for 8-bit normalized formats that require RGBA readback.
* WebGL2's readPixels only guarantees RGBA/UNSIGNED_BYTE support, so these formats
* need to be read as RGBA and have their channels extracted.
*
* @param {number} format - The pixel format constant.
* @returns {number} Number of channels (1, 2, or 3), or 0 if format doesn't require RGBA readback.
* @ignore
*/ const getPixelFormatChannelsForRgbaReadback = (format)=>{
switch(format){
case PIXELFORMAT_R8:
return 1;
case PIXELFORMAT_RG8:
return 2;
default:
return 0;
}
};
const invalidateAttachments = [];
/**
* WebglGraphicsDevice extends the base {@link GraphicsDevice} to provide rendering capabilities
* utilizing the WebGL 2.0 specification.
*
* @category Graphics
*/ class WebglGraphicsDevice extends GraphicsDevice {
postInit() {
super.postInit();
this.gpuProfiler = new WebglGpuProfiler(this);
}
/**
* Destroy the graphics device.
*/ destroy() {
super.destroy();
const gl = this.gl;
if (this.feedback) {
gl.deleteTransformFeedback(this.feedback);
}
this.clearVertexArrayObjectCache();
this.canvas.removeEventListener('webglcontextlost', this._contextLostHandler, false);
this.canvas.removeEventListener('webglcontextrestored', this._contextRestoredHandler, false);
this._contextLostHandler = null;
this._contextRestoredHandler = null;
this.gl = null;
super.postDestroy();
}
createBackbuffer(frameBuffer) {
this.supportsStencil = this.initOptions.stencil;
this.backBuffer = new RenderTarget({
name: 'WebglFramebuffer',
graphicsDevice: this,
depth: this.initOptions.depth,
stencil: this.supportsStencil,
samples: this.samples
});
// use the default WebGL framebuffer for rendering
this.backBuffer.impl.suppliedColorFramebuffer = frameBuffer;
}
// Update framebuffer format based on the current framebuffer, as this is use to create matching multi-sampled framebuffer
updateBackbufferFormat(framebuffer) {
const gl = this.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
const alphaBits = this.gl.getParameter(this.gl.ALPHA_BITS);
this.backBufferFormat = alphaBits ? PIXELFORMAT_RGBA8 : PIXELFORMAT_RGB8;
}
updateBackbuffer() {
const resolutionChanged = this.canvas.width !== this.backBufferSize.x || this.canvas.height !== this.backBufferSize.y;
if (this._defaultFramebufferChanged || resolutionChanged) {
// if the default framebuffer changes (entering or exiting XR for example)
if (this._defaultFramebufferChanged) {
this.updateBackbufferFormat(this._defaultFramebuffer);
}
this._defaultFramebufferChanged = false;
this.backBufferSize.set(this.canvas.width, this.canvas.height);
// recreate the backbuffer with newly supplied framebuffer
this.backBuffer.destroy();
this.createBackbuffer(this._defaultFramebuffer);
}
}
// provide webgl implementation for the vertex buffer
createVertexBufferImpl(vertexBuffer, format) {
return new WebglVertexBuffer();
}
// provide webgl implementation for the index buffer
createIndexBufferImpl(indexBuffer) {
return new WebglIndexBuffer(indexBuffer);
}
createShaderImpl(shader) {
return new WebglShader(shader);
}
createDrawCommandImpl(drawCommands) {
return new WebglDrawCommands(drawCommands.indexSizeBytes);
}
createTextureImpl(texture) {
this.textures.add(texture);
return new WebglTexture(texture);
}
createRenderTargetImpl(renderTarget) {
return new WebglRenderTarget();
}
createUploadStreamImpl(uploadStream) {
return new WebglUploadStream(uploadStream);
}
pushMarker(name) {
if (platform.browser && window.spector) {
const label = DebugGraphics.toString();
window.spector.setMarker(`${label} #`);
}
}
popMarker() {
if (platform.browser && window.spector) {
const label = DebugGraphics.toString();
if (label.length) {
window.spector.setMarker(`${label} #`);
} else {
window.spector.clearMarker();
}
}
}
/**
* Query the precision supported by ints and floats in vertex and fragment shaders. Note that
* getShaderPrecisionFormat is not guaranteed to be present (such as some instances of the
* default Android browser). In this case, assume highp is available.
*
* @returns {"highp"|"mediump"|"lowp"} The highest precision supported by the WebGL context.
* @ignore
*/ getPrecision() {
const gl = this.gl;
let precision = 'highp';
if (gl.getShaderPrecisionFormat) {
const vertexShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT);
const vertexShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT);
const fragmentShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
const fragmentShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT);
if (vertexShaderPrecisionHighpFloat && vertexShaderPrecisionMediumpFloat && fragmentShaderPrecisionHighpFloat && fragmentShaderPrecisionMediumpFloat) {
const highpAvailable = vertexShaderPrecisionHighpFloat.precision > 0 && fragmentShaderPrecisionHighpFloat.precision > 0;
const mediumpAvailable = vertexShaderPrecisionMediumpFloat.precision > 0 && fragmentShaderPrecisionMediumpFloat.precision > 0;
if (!highpAvailable) {
if (mediumpAvailable) {
precision = 'mediump';
Debug.warn('WARNING: highp not supported, using mediump');
} else {
precision = 'lowp';
Debug.warn('WARNING: highp and mediump not supported, using lowp');
}
}
}
}
return precision;
}
getExtension() {
for(let i = 0; i < arguments.length; i++){
if (this.supportedExtensions.indexOf(arguments[i]) !== -1) {
return this.gl.getExtension(arguments[i]);
}
}
return null;
}
get extDisjointTimerQuery() {
// lazy evaluation as this is not typically used
if (!this._extDisjointTimerQuery) {
// Note that Firefox exposes EXT_disjoint_timer_query under WebGL2 rather than EXT_disjoint_timer_query_webgl2
this._extDisjointTimerQuery = this.getExtension('EXT_disjoint_timer_query_webgl2', 'EXT_disjoint_timer_query');
}
return this._extDisjointTimerQuery;
}
/**
* Initialize the extensions provided by the WebGL context.
*
* @ignore
*/ initializeExtensions() {
const gl = this.gl;
this.supportedExtensions = gl.getSupportedExtensions() ?? [];
this._extDisjointTimerQuery = null;
this.textureRG11B10Renderable = true;
// In WebGL2 float texture renderability is dictated by the EXT_color_buffer_float extension
this.extColorBufferFloat = this.getExtension('EXT_color_buffer_float');
this.textureFloatRenderable = !!this.extColorBufferFloat;
// iOS exposes this for half precision render targets on WebGL2 from iOS v 14.5beta
this.extColorBufferHalfFloat = this.getExtension('EXT_color_buffer_half_float');
// render to half float buffers support - either of these two extensions
this.textureHalfFloatRenderable = !!this.extColorBufferHalfFloat || !!this.extColorBufferFloat;
this.extDebugRendererInfo = this.getExtension('WEBGL_debug_renderer_info');
this.extTextureFloatLinear = this.getExtension('OES_texture_float_linear');
this.textureFloatFilterable = !!this.extTextureFloatLinear;
this.extFloatBlend = this.getExtension('EXT_float_blend');
this.extTextureFilterAnisotropic = this.getExtension('EXT_texture_filter_anisotropic', 'WEBKIT_EXT_texture_filter_anisotropic');
this.extParallelShaderCompile = this.getExtension('KHR_parallel_shader_compile');
this.extMultiDraw = this.getExtension('WEBGL_multi_draw');
this.supportsMultiDraw = !!this.extMultiDraw;
// compressed textures
this.extCompressedTextureETC1 = this.getExtension('WEBGL_compressed_texture_etc1');
this.extCompressedTextureETC = this.getExtension('WEBGL_compressed_texture_etc');
this.extCompressedTexturePVRTC = this.getExtension('WEBGL_compressed_texture_pvrtc', 'WEBKIT_WEBGL_compressed_texture_pvrtc');
this.extCompressedTextureS3TC = this.getExtension('WEBGL_compressed_texture_s3tc', 'WEBKIT_WEBGL_compressed_texture_s3tc');
this.extCompressedTextureS3TC_SRGB = this.getExtension('WEBGL_compressed_texture_s3tc_srgb');
this.extCompressedTextureATC = this.getExtension('WEBGL_compressed_texture_atc');
this.extCompressedTextureASTC = this.getExtension('WEBGL_compressed_texture_astc');
this.extTextureCompressionBPTC = this.getExtension('EXT_texture_compression_bptc');
// HTML-in-Canvas support (texElementImage2D)
this.supportsHtmlTextures = typeof gl.texElementImage2D === 'function';
}
/**
* Query the capabilities of the WebGL context.
*
* @ignore
*/ initializeCapabilities() {
const gl = this.gl;
let ext;
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
this.maxPrecision = this.precision = this.getPrecision();
const contextAttribs = gl.getContextAttributes();
this.supportsMsaa = contextAttribs?.antialias ?? false;
this.supportsStencil = contextAttribs?.stencil ?? false;
// Query parameter values from the WebGL context
this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
this.maxCubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
this.maxRenderBufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
this.maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
this.maxCombinedTextures = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
this.maxVertexTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
this.vertexUniformsCount = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
this.fragmentUniformsCount = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
this.maxColorAttachments = gl.getParameter(gl.MAX_COLOR_ATTACHMENTS);
this.maxVolumeSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE);
ext = this.extDebugRendererInfo;
this.unmaskedRenderer = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : '';
this.unmaskedVendor = ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : '';
// Mali-G52 has rendering issues with GPU particles including
// SM-A225M, M2003J15SC and KFRAWI (Amazon Fire HD 8 2022)
const maliRendererRegex = /\bMali-G52+/;
// Samsung devices with Exynos (ARM) either crash or render incorrectly when using GPU for particles. See:
// https://github.com/playcanvas/engine/issues/3967
// https://github.com/playcanvas/engine/issues/3415
// https://github.com/playcanvas/engine/issues/4514
// Example UA matches: Starting 'SM' and any combination of letters or numbers:
// Mozilla/5.0 (Linux, Android 12; SM-G970F Build/SP1A.210812.016; wv)
const samsungModelRegex = /SM-[a-zA-Z0-9]+/;
this.supportsGpuParticles = !(this.unmaskedVendor === 'ARM' && userAgent.match(samsungModelRegex)) && !this.unmaskedRenderer.match(maliRendererRegex);
ext = this.extTextureFilterAnisotropic;
this.maxAnisotropy = ext ? gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) : 1;
const antialiasSupported = !this.forceDisableMultisampling;
this.maxSamples = antialiasSupported ? gl.getParameter(gl.MAX_SAMPLES) : 1;
// some devices incorrectly report max samples larger than 4
this.maxSamples = Math.min(this.maxSamples, 4);
// we handle anti-aliasing internally by allocating multi-sampled backbuffer
this.samples = antialiasSupported && this.backBufferAntialias ? this.maxSamples : 1;
// Don't allow area lights on old android devices, they often fail to compile the shader, run it incorrectly or are very slow.
this.supportsAreaLights = !platform.android;
// Also do not allow them when we only have small number of texture units
if (this.maxTextures <= 8) {
this.supportsAreaLights = false;
}
this.initCapsDefines();
}
/**
* Set the initial render state on the WebGL context.
*
* @ignore
*/ initializeRenderState() {
super.initializeRenderState();
const gl = this.gl;
// Initialize render state to a known start state
// default blend state
gl.disable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ZERO);
gl.blendEquation(gl.FUNC_ADD);
gl.colorMask(true, true, true, true);
gl.blendColor(0, 0, 0, 0);
gl.enable(gl.CULL_FACE);
this.cullFace = gl.BACK;
gl.cullFace(gl.BACK);
// default depth state
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.depthMask(true);
this.stencil = false;
gl.disable(gl.STENCIL_TEST);
this.stencilFuncFront = this.stencilFuncBack = FUNC_ALWAYS;
this.stencilRefFront = this.stencilRefBack = 0;
this.stencilMaskFront = this.stencilMaskBack = 0xFF;
gl.stencilFunc(gl.ALWAYS, 0, 0xFF);
this.stencilFailFront = this.stencilFailBack = STENCILOP_KEEP;
this.stencilZfailFront = this.stencilZfailBack = STENCILOP_KEEP;
this.stencilZpassFront = this.stencilZpassBack = STENCILOP_KEEP;
this.stencilWriteMaskFront = 0xFF;
this.stencilWriteMaskBack = 0xFF;
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
gl.stencilMask(0xFF);
this.alphaToCoverage = false;
this.raster = true;
gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
gl.disable(gl.RASTERIZER_DISCARD);
this.depthBiasEnabled = false;
gl.disable(gl.POLYGON_OFFSET_FILL);
this.clearDepth = 1;
gl.clearDepth(1);
this.clearColor = new Color(0, 0, 0, 0);
gl.clearColor(0, 0, 0, 0);
this.clearStencil = 0;
gl.clearStencil(0);
gl.hint(gl.FRAGMENT_SHADER_DERIVATIVE_HINT, gl.NICEST);
gl.enable(gl.SCISSOR_TEST);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
this.unpackFlipY = false;
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
this.unpackPremultiplyAlpha = false;
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
this.unpackAlignment = 1;
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
}
initTextureUnits(count = 16) {
this.textureUnits = [];
for(let i = 0; i < count; i++){
this.textureUnits.push([
null,
null,
null
]);
}
}
initializeContextCaches() {
super.initializeContextCaches();
// cache of VAOs
this._vaoMap = new Map();
this.boundVao = null;
this.activeFramebuffer = null;
this.feedback = null;
this.transformFeedbackBuffer = null;
this.textureUnit = 0;
this.initTextureUnits(this.maxCombinedTextures);
}
/**
* Called when the WebGL context was lost. It releases all context related resources.
*
* @ignore
*/ loseContext() {
super.loseContext();
// release shaders
for (const shader of this.shaders){
shader.loseContext();
}
}
/**
* Called when the WebGL context is restored. It reinitializes all context related resources.
*
* @ignore
*/ restoreContext() {
this.initializeExtensions();
this.initializeCapabilities();
super.restoreContext();
// Recompile all shaders
for (const shader of this.shaders){
shader.restoreContext();
}
}
/**
* Set the active rectangle for rendering on the specified device.
*
* @param {number} x - The pixel space x-coordinate of the bottom left corner of the viewport.
* @param {number} y - The pixel space y-coordinate of the bottom left corner of the viewport.
* @param {number} w - The width of the viewport in pixels.
* @param {number} h - The height of the viewport in pixels.
*/ setViewport(x, y, w, h) {
if (this.vx !== x || this.vy !== y || this.vw !== w || this.vh !== h) {
this.gl.viewport(x, y, w, h);
this.vx = x;
this.vy = y;
this.vw = w;
this.vh = h;
}
}
/**
* Set the active scissor rectangle on the specified device.
*
* @param {number} x - The pixel space x-coordinate of the bottom left corner of the scissor rectangle.
* @param {number} y - The pixel space y-coordinate of the bottom left corner of the scissor rectangle.
* @param {number} w - The width of the scissor rectangle in pixels.
* @param {number} h - The height of the scissor rectangle in pixels.
*/ setScissor(x, y, w, h) {
if (this.sx !== x || this.sy !== y || this.sw !== w || this.sh !== h) {
this.gl.scissor(x, y, w, h);
this.sx = x;
this.sy = y;
this.sw = w;
this.sh = h;
}
}
/**
* Binds the specified framebuffer object.
*
* @param {WebGLFramebuffer | null} fb - The framebuffer to bind.
* @ignore
*/ setFramebuffer(fb) {
if (this.activeFramebuffer !== fb) {
const gl = this.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
this.activeFramebuffer = fb;
}
}
/**
* Copies source render target into destination render target. Mostly used by post-effects.
*
* @param {RenderTarget} [source] - The source render target. Defaults to frame buffer.
* @param {RenderTarget} [dest] - The destination render target. Defaults to frame buffer.
* @param {boolean} [color] - If true, will copy the color buffer. Defaults to false.
* @param {boolean} [depth] - If true, will copy the depth buffer. Defaults to false.
* @returns {boolean} True if the copy was successful, false otherwise.
*/ copyRenderTarget(source, dest, color, depth) {
const gl = this.gl;
// if copying from the backbuffer
if (source === this.backBuffer) {
source = null;
}
if (color) {
if (!dest) {
// copying to backbuffer
if (!source._colorBuffer) {
Debug.error('Can\'t copy empty color buffer to backbuffer');
return false;
}
} else if (source) {
// copying to render target
if (!source._colorBuffer || !dest._colorBuffer) {
Debug.error('Can\'t copy color buffer, because one of the render targets doesn\'t have it');
return false;
}
if (source._colorBuffer._format !== dest._colorBuffer._format) {
Debug.error('Can\'t copy render targets of different color formats');
return false;
}
}
}
if (depth && source) {
if (!source._depth) {
if (!source._depthBuffer || !dest._depthBuffer) {
Debug.error('Can\'t copy depth buffer, because one of the render targets doesn\'t have it');
return false;
}
if (source._depthBuffer._format !== dest._depthBuffer._format) {
Debug.error('Can\'t copy render targets of different depth formats');
return false;
}
}
}
DebugGraphics.pushGpuMarker(this, 'COPY-RT');
const prevRt = this.renderTarget;
this.renderTarget = dest;
this.updateBegin();
// copy from single sampled framebuffer
const src = source ? source.impl._glFrameBuffer : this.backBuffer?.impl._glFrameBuffer;
const dst = dest ? dest.impl._glFrameBuffer : this.backBuffer?.impl._glFrameBuffer;
Debug.assert(src !== dst, 'Source and destination framebuffers must be different when blitting.');
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, src);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst);
const w = source ? source.width : dest ? dest.width : this.width;
const h = source ? source.height : dest ? dest.height : this.height;
gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, (color ? gl.COLOR_BUFFER_BIT : 0) | (depth ? gl.DEPTH_BUFFER_BIT : 0), gl.NEAREST);
// TODO: not sure we need to restore the prev target, as this only should run in-between render passes
this.renderTarget = prevRt;
gl.bindFramebuffer(gl.FRAMEBUFFER, prevRt ? prevRt.impl._glFrameBuffer : null);
DebugGraphics.popGpuMarker(this);
return true;
}
frameStart() {
super.frameStart();
this.updateBackbuffer();
this.gpuProfiler.frameStart();
}
frameEnd() {
super.frameEnd();
this.gpuProfiler.frameEnd();
this.gpuProfiler.request();
}
/**
* Start a render pass.
*
* @param {RenderPass} renderPass - The render pass to start.
* @ignore
*/ startRenderPass(renderPass) {
// set up render target
const rt = renderPass.renderTarget ?? this.backBuffer;
this.renderTarget = rt;
Debug.assert(rt);
DebugGraphics.pushGpuMarker(this, `Pass:${renderPass.name} RT:${rt.name}`);
DebugGraphics.pushGpuMarker(this, 'START-PASS');
this.updateBegin();
// the pass always start using full size of the target
const { width, height } = rt;
this.setViewport(0, 0, width, height);
this.setScissor(0, 0, width, height);
// clear the render target
const colorOps = renderPass.colorOps;
const depthStencilOps = renderPass.depthStencilOps;
if (colorOps?.clear || depthStencilOps.clearDepth || depthStencilOps.clearStencil) {
let clearFlags = 0;
const clearOptions = {};
if (colorOps?.clear) {
clearFlags |= CLEARFLAG_COLOR;
clearOptions.color = [
colorOps.clearValue.r,
colorOps.clearValue.g,
colorOps.clearValue.b,
colorOps.clearValue.a
];
}
if (depthStencilOps.clearDepth) {
clearFlags |= CLEARFLAG_DEPTH;
clearOptions.depth = depthStencilOps.clearDepthValue;
}
if (depthStencilOps.clearStencil) {
clearFlags |= CLEARFLAG_STENCIL;
clearOptions.stencil = depthStencilOps.clearStencilValue;
}
// clear it
clearOptions.flags = clearFlags;
this.clear(clearOptions);
}
Debug.call(()=>{
if (this.insideRenderPass) {
Debug.errorOnce('RenderPass cannot be started while inside another render pass.');
}
});
this.insideRenderPass = true;
DebugGraphics.popGpuMarker(this);
}
/**
* End a render pass.
*
* @param {RenderPass} renderPass - The render pass to end.
* @ignore
*/ endRenderPass(renderPass) {
DebugGraphics.pushGpuMarker(this, 'END-PASS');
this.unbindVertexArray();
const target = this.renderTarget;
const colorBufferCount = renderPass.colorArrayOps.length;
if (target) {
// invalidate buffers to stop them being written to on tiled architectures
invalidateAttachments.length = 0;
const gl = this.gl;
// color buffers
for(let i = 0; i < colorBufferCount; i++){
const colorOps = renderPass.colorArrayOps[i];
// invalidate color only if we don't need to resolve it
if (!(colorOps.store || colorOps.resolve)) {
invalidateAttachments.push(gl.COLOR_ATTACHMENT0 + i);
}
}
// we cannot invalidate depth/stencil buffers of the backbuffer
if (target !== this.backBuffer) {
if (!renderPass.depthStencilOps.storeDepth) {
invalidateAttachments.push(gl.DEPTH_ATTACHMENT);
}
if (!renderPass.depthStencilOps.storeStencil) {
invalidateAttachments.push(gl.STENCIL_ATTACHMENT);
}
}
if (invalidateAttachments.length > 0) {
// invalidate the whole buffer
// TODO: we could handle viewport invalidation as well
if (renderPass.fullSizeClearRect) {
gl.invalidateFramebuffer(gl.DRAW_FRAMEBUFFER, invalidateAttachments);
}
}
// resolve the color buffer (this resolves all MRT color buffers at once)
if (colorBufferCount && renderPass.colorOps?.resolve) {
if (renderPass.samples > 1 && target.autoResolve) {
target.resolve(true, false);
}
}
// resolve depth/stencil buffer
if (target.depthBuffer && renderPass.depthStencilOps.resolveDepth) {
if (renderPass.samples > 1 && target.autoResolve) {
target.resolve(false, true);
}
}
// generate mipmaps
for(let i = 0; i < colorBufferCount; i++){
const colorOps = renderPass.colorArrayOps[i];
if (colorOps.genMipmaps) {
const colorBuffer = target._colorBuffers[i];
if (colorBuffer && colorBuffer.impl._glTexture && colorBuffer.mipmaps) {
DebugGraphics.pushGpuMarker(this, `MIPS${i}`);
this.activeTexture(this.maxCombinedTextures - 1);
this.bindTexture(colorBuffer);
this.gl.generateMipmap(colorBuffer.impl._glTarget);
DebugGraphics.popGpuMarker(this);
}
}
}
}
this.insideRenderPass = false;
DebugGraphics.popGpuMarker(this);
DebugGraphics.popGpuMarker(this); // pop the pass-start marker
}
set defaultFramebuffer(value) {
if (this._defaultFramebuffer !== value) {
this._defaultFramebuffer = value;
this._defaultFramebufferChanged = true;
}
}
get defaultFramebuffer() {
return this._defaultFramebuffer;
}
/**
* Marks the beginning of a block of rendering. Internally, this function binds the render
* target currently set on the device. This function should be matched with a call to
* {@link GraphicsDevice#updateEnd}. Calls to {@link GraphicsDevice#updateBegin} and
* {@link GraphicsDevice#updateEnd} must not be nested.
*
* @ignore
*/ updateBegin() {
DebugGraphics.pushGpuMarker(this, 'UPDATE-BEGIN');
this.boundVao = null;
// clear texture units once a frame on desktop safari
if (this._tempEnableSafariTextureUnitWorkaround) {
for(let unit = 0; unit < this.textureUnits.length; ++unit){
for(let slot = 0; slot < 3; ++slot){
this.textureUnits[unit][slot] = null;
}
}
}
// Set the render target
const target = this.renderTarget ?? this.backBuffer;
Debug.assert(target);
// Initialize the framebuffer
const targetImpl = target.impl;
if (!targetImpl.initialized) {
this.initRenderTarget(target);
}
// Bind the framebuffer
this.setFramebuffer(targetImpl._glFrameBuffer);
DebugGraphics.popGpuMarker(this);
}
/**
* Marks the end of a block of rendering. This function should be called after a matching call
* to {@link GraphicsDevice#updateBegin}. Calls to {@link GraphicsDevice#updateBegin} and
* {@link GraphicsDevice#updateEnd} must not be nested.
*
* @ignore
*/ updateEnd() {
DebugGraphics.pushGpuMarker(this, 'UPDATE-END');
this.unbindVertexArray();
// Unset the render target
const target = this.renderTarget;
if (target && target !== this.backBuffer) {
// Resolve MSAA if needed
if (target._samples > 1 && target.autoResolve) {
target.resolve();
}
// If the active render target is auto-mipmapped, generate its mip chain
const colorBuffer = target._colorBuffer;
if (colorBuffer && colorBuffer.impl._glTexture && colorBuffer.mipmaps) {
// FIXME: if colorBuffer is a cubemap currently we're re-generating mipmaps after
// updating each face!
this.activeTexture(this.maxCombinedTextures - 1);
this.bindTexture(colorBuffer);
this.gl.generateMipmap(colorBuffer.impl._glTarget);
}
}
DebugGraphics.popGpuMarker(this);
}
/**
* Updates a texture's vertical flip.
*
* @param {boolean} flipY - True to flip the texture vertically.
* @ignore
*/ setUnpackFlipY(flipY) {
if (this.unpackFlipY !== flipY) {
this.unpackFlipY = flipY;
// Note: the WebGL spec states that UNPACK_FLIP_Y_WEBGL only affects
// texImage2D and texSubImage2D, not compressedTexImage2D
const gl = this.gl;
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);
}
}
/**
* Updates a texture to have its RGB channels premultiplied by its alpha channel or not.
*
* @param {boolean} premultiplyAlpha - True to premultiply the alpha channel against the RGB
* channels.
* @ignore
*/ setUnpackPremultiplyAlpha(premultiplyAlpha) {
if (this.unpackPremultiplyAlpha !== premultiplyAlpha) {
this.unpackPremultiplyAlpha = premultiplyAlpha;
// Note: the WebGL spec states that UNPACK_PREMULTIPLY_ALPHA_WEBGL only affects
// texImage2D and texSubImage2D, not compressedTexImage2D
const gl = this.gl;
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiplyAlpha);
}
}
/**
* Sets the byte alignment for unpacking pixel data during texture uploads.
*
* @param {number} alignment - The alignment in bytes. Must be 1, 2, 4, or 8.
* @ignore
*/ setUnpackAlignment(alignment) {
if (this.unpackAlignment !== alignment) {
this.unpackAlignment = alignment;
this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, alignment);
}
}
/**
* Activate the specified texture unit.
*
* @param {number} textureUnit - The texture unit to activate.
* @ignore
*/ activeTexture(textureUnit) {
if (this.textureUnit !== textureUnit) {
this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit);
this.textureUnit = textureUnit;
}
}
/**
* If the texture is not already bound on the currently active texture unit, bind it.
*
* @param {Texture} texture - The texture to bind.
* @ignore
*/ bindTexture(texture) {
const impl = texture.impl;
const textureTarget = impl._glTarget;
const textureObject = impl._glTexture;
const textureUnit = this.textureUnit;
const slot = this.targetToSlot[textureTarget];
if (this.textureUnits[textureUnit][slot] !== textureObject) {
this.gl.bindTexture(textureTarget, textureObject);
this.textureUnits[textureUnit][slot] = textureObject;
}
}
/**
* If the texture is not bound on the specified texture unit, active the texture unit and bind
* the texture to it.
*
* @param {Texture} texture - The texture to bind.
* @param {number} textureUnit - The texture unit to activate and bind the texture to.
* @ignore
*/ bindTextureOnUnit(texture, textureUnit) {
const impl = texture.impl;
const textureTarget = impl._glTarget;
const textureObject = impl._glTexture;
const slot = this.targetToSlot[textureTarget];
if (this.textureUnits[textureUnit][slot] !== textureObject) {
this.activeTexture(textureUnit);
this.gl.bindTexture(textureTarget, textureObject);
this.textureUnits[textureUnit][slot] = textureObject;
}
}
/**
* Update the texture parameters for a given texture if they have changed.
*
* @param {Texture} texture - The texture to update.
* @ignore
*/ setTextureParameters(texture) {
const gl = this.gl;
const flags = texture.impl.dirtyParameterFlags;
const target = texture.impl._glTarget;
if (flags & TEXPROPERTY_MIN_FILTER) {
let filter = texture._minFilter;
if (!texture._mipmaps || texture._compressed && texture._levels.length === 1) {
if (filter === FILTER_NEAREST_MIPMAP_NEAREST || filter === FILTER_NEAREST_MIPMAP_LINEAR) {
filter = FILTER_NEAREST;
} else if (filter === FILTER_LINEAR_MIPMAP_NEAREST || filter === FILTER_LINEAR_MIPMAP_LINEAR) {
filter = FILTER_LINEAR;
}
}
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, this.glFilter[filter]);
}
if (flags & TEXPROPERTY_MAG_FILTER) {
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, this.glFilter[texture._magFilter]);
}
if (flags & TEXPROPERTY_ADDRESS_U) {
gl.texParameteri(target, gl.TEXTURE_WRAP_S, this.glAddress[texture._addressU]);
}
if (flags & TEXPROPERTY_ADDRESS_V) {
gl.texParameteri(target, gl.TEXTURE_WRAP_T, this.glAddress[texture._addressV]);
}
if (flags & TEXPROPERTY_ADDRESS_W) {
gl.texParameteri(target, gl.TEXTURE_WRAP_R, this.glAddress[texture._addressW]);
}
if (flags & TEXPROPERTY_COMPARE_ON_READ) {
gl.texParameteri(target, gl.TEXTURE_COMPARE_MODE, texture._compareOnRead ? gl.COMPARE_REF_TO_TEXTURE : gl.NONE);
}
if (flags & TEXPROPERTY_COMPARE_FUNC) {
gl.texParameteri(target, gl.TEXTURE_COMPARE_FUNC, this.glComparison[texture._compareFunc]);
}
if (flags & TEXPROPERTY_ANISOTROPY) {
const ext = this.extTextureFilterAnisotropic;
if (ext) {
gl.texParameterf(target, ext.TEXTURE_MAX_ANISOTROPY_EXT, math.clamp(Math.round(texture._anisotropy), 1, this.maxAnisotropy));
}
}
}
/**
* Sets the specified texture on the specified texture unit.
*
* @param {Texture} texture - The texture to set.
* @param {number} textureUnit - The texture unit to set the texture on.
* @ignore
*/ setTexture(texture, textureUnit) {
const impl = texture.impl;
if (!impl._glTexture) {
impl.initialize(this, texture);
}
if (impl.dirtyParameterFlags > 0 || texture._needsUpload || texture._needsMipmapsUpload) {
// Ensure the specified texture unit is active
this.activeTexture(textureUnit);
// Ensure the texture is bound on correct target of the specified texture unit
this.bindTexture(texture);
if (impl.dirtyParameterFlags) {
this.setTextureParameters(texture);
impl.dirtyParameterFlags = 0;
}
if (texture._needsUpload || texture._needsMipmapsUpload) {
impl.upload(this, texture);
texture._needsUpload = false;
texture._needsMipmapsUpload = false;
}
} else {
// Ensure the texture is currently bound to the correct target on the specified texture unit.
// If the texture is already bound to the correct target on the specified unit, there's no need
// to actually make the specified texture unit active because the texture itself does not need
// to be updated.
this.bindTextureOnUnit(texture, textureUnit);
}
}
// function creates VertexArrayObject from list of vertex buffers
createVertexArray(vertexBuffers) {
let key, vao;
// only use cache when more than 1 vertex buffer, otherwise it's unique
const useCache = vertexBuffers.length > 1;
if (useCache) {
// generate unique key for the vertex buffers
key = '';
for(let i = 0; i < vertexBuffers.length; i++){
const vertexBuffer = vertexBuffers[i];
key += vertexBuffer.id + vertexBuffer.format.renderingHash;
}
// try to get VAO from cache
vao = this._vaoMap.get(key);
}
// need to create new vao
if (!vao) {
// create VA object
const gl = this.gl;
vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// don't capture index buffer in VAO
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
let locZero = false;
for(let i = 0; i < vertexBuffers.length; i++){
// bind buffer
const vertexBuffer = vertexBuffers[i];
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer.impl.bufferId);
// for each attribute
const elements = vertexBuffer.format.elements;
for(let j = 0; j < elements.length; j++){
const e = elements[j];
const loc = semanticToLocation[e.name];
if (loc === 0) {
locZero = true;
}
if (e.asInt) {
gl.vertexAttribIPointer(loc, e.numComponents, this.glType[e.dataType], e.stride, e.offset);
} else {
gl.vertexAttribPointer(loc, e.numComponents, this.glType[e.dataType], e.normalize, e.stride, e.offset);
}
gl.enableVertexAttribArray(loc);
if (vertexBuffer.format.instancing) {
gl.vertexAttribDivisor(loc, 1);
}
}
}
// end of VA object
gl.bindVertexArray(null);
// unbind any array buffer
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// add it to cache
if (useCache) {
this._vaoMap.set(key, vao);
}
if (!locZero) {
Debug.warn('No vertex attribute is mapped to location 0, which might cause compatibility issues on Safari on MacOS - please use attribute SEMANTIC_POSITION or SEMANTIC_ATTR15');
}
}
return vao;
}
unbindVertexArray() {
// unbind VAO from device to protect it from being changed
if (this.boundVao) {
this.boundVao = null;
this.gl.bindVertexArray(null);
}
}
setBuffers(indexBuffer) {
const gl = this.gl;
let vao;
// create VAO for specified vertex buffers
if (this.vertexBuffers.length === 1) {
// single VB keeps its VAO
const vertexBuffer = this.vertexBuffers[0];
Debug.assert(vertexBuffer.device === this, 'The VertexBuffer was not created using current GraphicsDevice');
if (!vertexBuffer.impl.vao) {
vertexBuffer.impl.vao = this.createVertexArray(this.vertexBuffers);
}
vao = vertexBuffer.impl.vao;
} else {
// obtain temporary VAO for multiple vertex buffers
vao = this.createVertexArray(this.vertexBuffers);
}
// set active VAO
if (this.boundVao !== vao) {
this.boundVao = vao;
gl.bindVertexArray(vao);
}
// Set the active index buffer object
// Note: we don't cache this state and set it only when it changes, as VAO captures last bind buffer in it
// and so we don't know what VAO sets it to.
const bufferId = indexBuffer ? indexBuffer.impl.bufferId : null;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferId);
}
_multiDrawLoopFallback(mode, primitive, indexBuffer, numInstances, drawCommands) {
const gl = this.gl;
if (primitive.indexed) {
const format = indexBuffer.impl.glFormat;
const { glCounts, glOffsetsBytes, glInstanceCounts, count } = drawCommands.impl;
if (numInstances > 0) {
for(let i = 0; i < count; i++){
gl.drawElementsInstanced(mode, glCounts[i], format, glOffsetsBytes[i], glInstanceCounts[i]);
}
} else {
for(let i = 0; i < count; i++){
gl.drawElements(mode, glCounts[i], format, glOffsetsBytes[i]);
}
}
} else {
const { glCounts, glOffsetsBytes, glInstanceCounts, count } = drawCommands.impl;
if (numInstances > 0) {
for(let i = 0; i < count; i++){
gl.drawArraysInstanced(mode, glOffsetsBytes[i], glCounts[i], glInstanceCounts[i]);
}
} else {
for(let i = 0; i < count; i++){
gl.drawArrays(mode, glOffsetsBytes[i], glCounts[i]);
}
}
}
}
draw(primitive, indexBuffer, numInstances, drawCommands, first = true, last = true) {
const shader = this.shader;
if (shader) {
this.activateShader();
if (this.shaderValid) {
const gl = this.gl;
// vertex buffers
if (first) {
Debug.call(()=>this.validateAttributes(this.shader, this.vertexBuffers[0]?.format, this.vertexBuffers[1]?.format));
this.setBuffers(indexBuffer);
}
// Commit the shader program variables
let textureUnit = 0;
const samplers = shader.impl.samplers;
for(let i = 0, len = samplers.length; i < len; i++){
const sampler = samplers[i];
let samplerValue = sampler.scopeId.value;
if (!samplerValue) {
const samplerName = sampler.scopeId.name;
Debug.assert(samplerName !== 'texture_grabPass', 'Engine provided texture with sampler name \'texture_grabPass\' is not longer supported, use \'uSceneColorMap\' instead');
Debug.assert(samplerName !== 'uDepthMap', 'Engine provided texture with sampler name \'uDepthMap\' is not longer supported, use \'uSceneDepthMap\' instead');
if (samplerName === 'uSceneDepthMap') {
Debug.errorOnce(`A uSceneDepthMap texture is used by the shader but a scene depth texture is not available. Use CameraComponent.requestSceneDepthMap / enable Depth Grabpass on the Camera Component / CameraFrame.rendering.sceneDepthMap to enable it. Rendering [${DebugGraphics.toString()}]`);
samplerValue = getBuiltInTexture(this, 'white');
}
if (samplerName === 'uSceneColorMap') {
Debug.errorOnce(`A uSceneColorMap texture is used by the shader but a scene color texture is not available. Use CameraComponent.requestSceneColorMap / enable Color Grabpass on the Camera Component / CameraFrame.rendering.sceneColorMap to enable it. Rendering [${DebugGraphics.toString()}]`);
samplerValue = getBuiltInTexture(this, 'pink');
}
// missing generic texture
if (!samplerValue) {
Debug.errorOnce(`Shader ${shader.name} requires ${samplerName} texture which was not set. Rendering [${DebugGraphics.toString()}]`, shader);
samplerValue = getBuiltInTexture(this, 'pink');
}
}
if (samplerValue instanceof Texture) {
const texture = samplerValue;
this.setTexture(texture, textureUnit);
if (this.renderTarget) {
// Set breakpoint here to debug "Source and destination textures of the draw are the same" errors
if (this.renderTarget._samples < 2) {
if (this.renderTarget.colorBuffer && this.renderTarget.colorBuffer === texture) {
Debug.error('Trying to bind current color buffer as a texture', {
renderTarget: this.renderTarget,
texture
});
} else if (this.renderTarget.depthBuffer && this.renderTarget.depthBuffer === texture) {
Debug.error('Trying to bind current depth buffer as a texture', {
texture
});
}
}
}
if (sampler.slot !== textureUnit) {
gl.uniform1i(sampler.locationId, textureUnit);
sampler.slot = textureUnit;
}
textureUnit++;
} else {
sampler.array.length = 0;
const numTextures = samplerValue.length;
for(let j = 0; j < numTextures; j++){