UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

1,036 lines (1,034 loc) 102 kB
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++){