在使用 Plotly.js 绘制 WebGL 图表 (如 scattergl) 时,如果同时显示图表过多,就有可能观看到如下错误:
WARNING: Too many active WebGL contexts. Oldest context will be lost.
这是什么意思?如何解决?本文将进行全面分析,帮助您快速实现回退或跨越这个应用性限制。
问题说明
WebGL 是一种基于 GPU 的绘图 API,每一个 WebGL 图表就需要创建一个 WebGL 上下文 (WebGL context)。因为 GPU 资源有限,大部分浏览器会限制 WebGL context 的数量,通常在 8 ~ 16 个之间。
Plotly.js 里的 WebGL 图表,如 type: 'scattergl'
,同样会创建 WebGL context,而且当您使用 trellisBy n*n 分面布局显示多个图表时,很容易超过这个上限。
解决方案
1. 更换为 SVG 模式
如果每个图表点量不大,可以改用 SVG 模式(type: 'scatter'
):
{
type: 'scatter', // SVG 模式
mode: 'markers',
...
}
2. 分页/懒加载显示图表
分页显示 6~8 个图表,切换时重新渲染,当前页的图表没有显示时重置或释放上下文:
Plotly.purge(document.getElementById('chartId'));
3. 使用 Virtual WebGL (实际解决方案)
Plotly.js 官网提供一个性能量解决方案:使用 Virtual WebGL 实现多 WebGL 上下文虚拟在一个实际上下文上:
使用步骤
- 在页面中引入开源代码:(或作为
.js
静态资源文件放在项目assets
文件夹下手动引入)
<script src="https://unpkg.com/virtual-webgl@1.0.6/src/virtual-webgl.js"></script>
-
它会动态 patch 原本 HTMLCanvasElement.prototype.getContext,在您调用
getContext('webgl')
时代理为虚拟 WebGL 上下文,将多个 context 虚拟到同一 GPU context 上。 -
无需修改 Plotly 图表代码,如果您本来就是 scattergl,就会被自动虚拟化,保持结构不变。
注意事项
- Virtual WebGL 性能有所降低,适合图表量大但每图点量较小场景
- 如果您有大量点分布需求,还是要遵守 6~8 个图表上限
小结
Plotly.js 中 scattergl/热力图等 WebGL 模式图表有突出性能优势,但也带来了 WebGL context 限制的突点。通过分页、SVG fallback 或 virtual-webgl 虚拟技术,可以有效解决多图表同时渲染时的失效问题。
建议在 BI 项目中对 WebGL context 使用进行监控或 fallback 策略,以增强程序的应急性和稳定性。
virtual-webgl.js
/*
* Copyright 2018, Gregg Tavares.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Gregg Tavares. nor the names of his
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function() {
const settings = {
disableWebGL2: false,
compositorCreator() {
},
};
const canvasToVirtualContextMap = new Map();
const extensionInfo = {
webgl_draw_buffers: {
wrapperFnMakerFn: makeWEBGL_draw_buffersWrapper,
saveRestoreMakerFn: makeWEBGL_drawBuffersSaveRestoreHelper,
},
oes_vertex_array_object: {
wrapperFnMakerFn: makeOES_vertex_array_objectWrapper,
},
angle_instanced_arrays: {
wrapperFnMakerFn: makeANGLE_instanced_arraysWrapper,
},
};
const extensionSaveRestoreHelpersArray = [];
const extensionSaveRestoreHelpers = {};
let currentVirtualContext = null;
let someContextsNeedRendering;
const sharedWebGLContext = document.createElement('canvas').getContext('webgl');
const sharedVAOExtension = sharedWebGLContext.getExtension("OES_vertex_array_object");
const sharedInstanceExtension = sharedWebGLContext.getExtension("ANGLE_instanced_arrays");
const numAttributes = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_VERTEX_ATTRIBS);
const numTextureUnits = sharedWebGLContext.getParameter(sharedWebGLContext.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
let numDrawBuffers;
const baseState = makeDefaultState(300, 150);
const vs = `
attribute vec4 position;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = position.xy * .5 + .5;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_tex;
void main() {
gl_FragColor = texture2D(u_tex, v_texcoord);
}
`;
const fs2 = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_tex;
void main() {
gl_FragColor = texture2D(u_tex, v_texcoord);
gl_FragColor.rgb *= gl_FragColor.a;
}
`;
const premultplyAlphaTrueProgram = createProgram(sharedWebGLContext, [vs, fs]);
const premultplyAlphaFalseProgram = createProgram(sharedWebGLContext, [vs, fs2]);
if (sharedVAOExtension) {
const vao = sharedVAOExtension.createVertexArrayOES();
sharedVAOExtension.bindVertexArrayOES(vao);
baseState.vertexArray = vao;
}
{
const gl = sharedWebGLContext;
const positionLoc = 0; // hard coded in createProgram
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
}
saveAllState(baseState);
HTMLCanvasElement.prototype.getContext = (function(origFn) {
return function(type, contextAttributes) {
if (type === 'webgl' || type === 'experimental-webgl') {
return createOrGetVirtualWebGLContext(this, type, contextAttributes);
} else if (type === 'webgl2' && settings.disableWebGL2) {
return null;
}
return origFn.call(this, type, contextAttributes);
};
}(HTMLCanvasElement.prototype.getContext));
function valueOrDefault(value, defaultValue) {
return value === undefined ? defaultValue : value;
}
function errorDisposedContext(fnName) {
return function() {
throw new Error(`tried to call ${fnName} on disposed context`);
};
}
class DefaultCompositor {
constructor(canvas) {
this._ctx = canvas.getContext('2d');
}
composite(gl, texture, canvas, contextAttributes) {
// note: not entirely sure what to do here. We need this canvas to be at least as large
// as the canvas we're drawing to. Resizing a canvas is slow so I think just making
// sure we never get smaller than the largest canvas. At the moment though I'm too lazy
// to make it smaller.
const ctx = this._ctx;
const width = canvas.width;
const height = canvas.height;
const maxWidth = Math.max(gl.canvas.width, width);
const maxHeight = Math.max(gl.canvas.height, height);
if (gl.canvas.width !== maxWidth || gl.canvas.height !== maxHeight) {
gl.canvas.width = maxWidth;
gl.canvas.height = maxHeight;
}
gl.viewport(0, 0, width, height);
gl.useProgram(contextAttributes.premultipliedAlpha ? premultplyAlphaTrueProgram : premultplyAlphaFalseProgram);
// draw the drawingbuffer's texture to the offscreen canvas
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// copy it to target canvas
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(
gl.canvas,
0, maxHeight - height, width, height, // src rect
0, 0, width, height); // dest rect
}
dispose() {
}
}
class VirtualWebGLContext {
constructor(canvas, contextAttributes = {}, compositor, disposeHelper) {
const gl = sharedWebGLContext;
this.canvas = canvas;
// Should use Symbols or someting to hide these variables from the outside.
this._compositor = compositor;
this._disposeHelper = disposeHelper;
this._extensions = {};
// based on context attributes and canvas.width, canvas.height
// create a texture and framebuffer
this._drawingbufferTexture = gl.createTexture();
this._drawingbufferFramebuffer = gl.createFramebuffer();
this._contextAttributes = {
alpha: valueOrDefault(contextAttributes.alpha, true),
antialias: false,
depth: valueOrDefault(contextAttributes.depth, true),
failIfMajorPerformanceCaveat: false,
premultipliedAlpha: valueOrDefault(contextAttributes.premultipliedAlpha, true),
stencil: valueOrDefault(contextAttributes.stencil, false),
};
this._preserveDrawingbuffer = valueOrDefault(contextAttributes.preserveDrawingBuffer, false);
const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
const oldFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
gl.bindTexture(gl.TEXTURE_2D, this._drawingbufferTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// this._drawingbufferTexture.id = canvas.id;
// this._drawingbufferFramebuffer.id = canvas.id;
gl.bindFramebuffer(gl.FRAMEBUFFER, this._drawingbufferFramebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._drawingbufferTexture, 0);
if (this._contextAttributes.depth) {
const oldRenderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
this._depthRenderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, this._depthRenderbuffer);
const attachmentPoint = this._contextAttributes.stencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, attachmentPoint, gl.RENDERBUFFER, this._depthRenderbuffer);
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
}
gl.bindTexture(gl.TEXTURE_2D, oldTexture);
gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
// remember all WebGL state (default bindings, default texture units,
// default attributes and/or vertex shade object, default program,
// default blend, stencil, zbuffer, culling, viewport etc... state
this._state = makeDefaultState(canvas.width, canvas.height);
this._state.framebuffer = this._drawingbufferFramebuffer;
if (sharedVAOExtension) {
this._state.vertexArray = sharedVAOExtension.createVertexArrayOES();
this._defaultVertexArray = this._state.vertexArray;
}
}
dispose() {
this._disposeHelper();
const gl = sharedWebGLContext;
gl.deleteFramebuffer(this._drawingbufferFramebuffer);
gl.deleteTexture(this._drawingbufferTexture);
if (this._depthRenderbuffer) {
gl.deleteRenderbuffer(this._depthRenderbuffer);
}
if (this._compositor.dispose) {
this._compositor.dispose();
}
for (const [key, value] of Object.entries(this)) {
if (typeof value === 'function') {
this[key] = errorDisposedContext(key);
}
}
for (const [key, value] of Object.entries(VirtualWebGLContext.prototype)) {
if (typeof value === 'function') {
this[key] = errorDisposedContext(key);
}
}
}
get drawingBufferWidth() {
return this.canvas.width;
}
get drawingBufferHeight() {
return this.canvas.height;
}
composite(gl) {
this._compositor.composite(gl, this._drawingbufferTexture, this.canvas, this._contextAttributes);
if (!this._preserveDrawingbuffer) {
this._needClear = true;
}
}
}
function makeDefaultState(width, height) {
const gl = WebGLRenderingContext;
const state ={
arrayBuffer: null,
renderbuffer: null,
framebuffer: null,
blend: false,
cullFace: false,
depthTest: false,
dither: false,
polygonOffsetFill: false,
sampleAlphaToCoverage: false,
sampleCoverage: false,
scissorTest: false,
stencilTest: false,
activeTexture: gl.TEXTURE0,
packAlignment: 4,
unpackAlignment: 4,
unpackColorspaceConversion: gl.BROWSER_DEFAULT_WEBGL,
unpackFlipY: 0,
unpackPremultiplyAlpha: 0,
currentProgram: null,
viewport: [0, 0, width, height],
scissor: [0, 0, 0, 0],
blendSrcRgb: gl.ONE,
blendDstRgb: gl.ZERO,
blendSrcAlpha: gl.ONE,
blendDstAlpha: gl.ZERO,
blendEquationRgb: gl.FUNC_ADD,
blendEquationAlpha: gl.FUNC_ADD,
blendColor: [0, 0, 0, 0],
colorClearValue: [0, 0, 0, 0],
colorMask: [true, true, true, true],
cullFaceMode: gl.BACK,
depthClearValue: 1,
depthFunc: gl.LESS,
depthRange: [0, 1],
depthMask: true,
frontFace: gl.CCW,
generateMipmapHint: gl.DONT_CARE,
lineWidth: 1,
polygonOffsetFactor: 0,
polygonOffsetUnits: 0,
sampleCoverageValue: 1,
sampleCoverageUnits: false,
stencilBackFail: gl.KEEP,
stencilBackFunc: gl.ALWAYS,
stencilBackPassDepthFail: gl.KEEP,
stencilBackPassDepthPass: gl.KEEP,
stencilBackRef: 0,
stencilBackValueMask: 0xFFFFFFFF,
stencilBackWriteMask: 0xFFFFFFFF,
stencilClearValue: 0,
stencilFail: gl.KEEP,
stencilFunc: gl.ALWAYS,
stencilPassDepthFail: gl.KEEP,
stencilPassDepthPass: gl.KEEP,
stencilRef: 0,
stencilValueMask: 0xFFFFFFFF,
stencilWriteMask: 0xFFFFFFFF,
textureUnits: [],
};
if (sharedVAOExtension) {
state.vertexArray = null;
} else {
state.elementArrayBuffer = null;
state.attributes = [];
for (let i = 0; i < numAttributes; ++i) {
const attrib = {
buffer: null,
enabled: false,
size: 4,
stride: 0,
type: gl.FLOAT,
normalized: false,
value: [0, 0, 0, 1],
};
if (sharedInstanceExtension) {
attrib.divisor = 0;
}
state.attributes.push(attrib);
}
}
for (let i = 0; i < numTextureUnits; ++i) {
state.textureUnits.push({
texture2D: null,
textureCubemap: null,
});
}
return state;
}
// copy all WebGL constants and functions to the prototype of
// VirtualWebGLContext
for (let key in WebGLRenderingContext.prototype) {
const propDesc = Object.getOwnPropertyDescriptor(WebGLRenderingContext.prototype, key);
if (propDesc.get) {
// it's a getter/setter ?
const virtualPropDesc = Object.getOwnPropertyDescriptor(VirtualWebGLContext.prototype, key);
if (!virtualPropDesc) {
console.warn(`WebGLRenderingContext.${key} is not supported`);
}
continue;
}
switch (key) {
default: {
const value = WebGLRenderingContext.prototype[key];
let newValue = value;
switch (key) {
case 'getContextAttributes':
newValue = virtualGetContextAttributes;
break;
case 'getExtension':
newValue = createGetExtensionWrapper(value);
break;
case 'bindFramebuffer':
newValue = virtualBindFramebuffer;
break;
case 'getParameter':
newValue = virtualGetParameter;
break;
case 'readPixels':
newValue = virtualReadPixels;
break;
case 'clear':
case 'drawArrays':
case 'drawElements':
newValue = createDrawWrapper(value);
break;
default:
if (typeof value === 'function') {
newValue = createWrapper(value);
}
break;
}
VirtualWebGLContext.prototype[key] = newValue;
break;
}
}
}
function createGetExtensionWrapper(origFn) {
return function(extensionName) {
extensionName = extensionName.toLowerCase();
// just like the real context each extension needs a virtual class because each use
// of the extension might be modified (as in people adding properties to it)
const existingExt = this._extensions[extensionName];
if (existingExt) {
return existingExt;
}
const ext = origFn.call(sharedWebGLContext, extensionName);
const wrapperInfo = extensionInfo[extensionName] || {};
const wrapperFnMakerFn = wrapperInfo.wrapperFnMakerFn || (() => { console.log("trying to get extension:", extensionName); });
const saveRestoreHelper = extensionSaveRestoreHelpers[extensionName];
if (!saveRestoreHelper) {
const saveRestoreMakerFn = wrapperInfo.saveRestoreMakerFn;
if (saveRestoreMakerFn) {
const saveRestore = saveRestoreMakerFn(ext);
extensionSaveRestoreHelpers[extensionName] = saveRestore;
extensionSaveRestoreHelpersArray.push(saveRestore);
}
}
const wrapper = {
_context: this,
};
for (let key in ext) {
let value = ext[key];
if (typeof value === 'function') {
value = wrapperFnMakerFn(ext, value, extensionName);
}
wrapper[key] = value;
}
this._extensions[extensionName] = wrapper;
return wrapper;
};
}
function isFramebufferBindingNull(vctx) {
return vctx._state.framebuffer === vctx._drawingbufferFramebuffer;
}
function makeOES_vertex_array_objectWrapper(ext, origFn, name) {
switch (name) {
case 'bindVertexArrayOES':
return virutalbindVertexArrayOES;
}
return function(...args) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
return origFn.call(ext, ...args);
}
}
function virutalbindVertexArrayOES(vao) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
if (!vao) {
vao = this._context._defaultVertexArray;
}
sharedVAOExtension.bindVertexArrayOES(vao);
}
function makeANGLE_instanced_arraysWrapper(ext, origFn, name) {
switch (name) {
case 'drawArraysInstancedANGLE':
case 'drawElementsInstancedANGLE':
return createExtDrawWrapper(ext, origFn);
break;
}
return function(...args) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
return origFn.call(ext, ...args);
}
}
function createExtDrawWrapper(ext, origFn) {
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
beforeDraw(this._context);
const result = origFn.call(exts, ...args);
afterDraw(this._context);
return result;
};
}
function makeWEBGL_draw_buffersWrapper(ext, origFn, name) {
const gl = sharedWebGLContext;
const backBuffer = [gl.COLOR_ATTACHMENT0];
return function(drawingBuffers) {
makeCurrentContext(this._context);
resizeCanvasIfChanged(this._context);
// if the virtual context is bound to canvas then fake it
if (isFramebufferBindingNull(this._context)) {
// this really isn't checking everything
// for example if the user passed in array.length != 1
// then we are supposed to generate an error
if (drawingBuffers[0] === gl.BACK) {
drawingBuffers = backBuffer;
}
}
origFn.call(ext, drawingBuffers);
}
}
function makeWEBGL_drawBuffersSaveRestoreHelper(ext) {
const gl = sharedWebGLContext;
const maxDrawBuffers = gl.getParameter(ext.MAX_DRAW_BUFFERS_WEBGL);
const defaultBackDrawBuffers = [gl.BACK];
const defaultFBDrawBuffers = [gl.COLOR_ATTACHMENT0];
for (let i = 1; i < maxDrawBuffers; ++i) {
defaultFBDrawBuffers.push(gl.NONE);
}
function initDrawBufferState(state) {
state.drawBuffers = state.framebuffer ? defaultFBDrawBuffers.slice() : defaultBackDrawBuffers.slice();
}
function save(state) {
if (!state.drawBuffers) {
initDrawBufferState(state);
}
// remember state.framebuffer is only null for the offscreen canvas
const drawBuffers = state.drawBuffers;
for (let i = 0; i < drawBuffers.length; ++i) {
drawBuffers[i] = gl.getParameter(ext.DRAW_BUFFER0_WEBGL + i);
}
}
function restore(state) {
if (!state.drawBuffers) {
initDrawBufferState(state);
}
ext.drawBuffersWEBGL(state.drawBuffers);
}
return {
save,
restore,
};
}
function virtualGetContextAttributes() {
return this._contextAttributes;
}
function virtualReadPixels(...args) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
clearIfNeeded(this);
const gl = sharedWebGLContext;
return gl.readPixels(...args);
}
function virtualGetParameter(pname) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
const gl = sharedWebGLContext;
const value = gl.getParameter(pname);
switch (pname) {
case gl.FRAMEBUFFER_BINDING:
if (value === this._drawingbufferFramebuffer) {
return null;
}
break;
case 0x8825: // DRAW_BUFFER0_WEBGL
if (isFramebufferBindingNull(this)) {
if (value === gl.COLOR_ATTACHMENT0) {
return gl.BACK;
}
}
break;
case 0x85B5: // VERTEX_ARRAY_BINDING_OES
if (value === this._defaultVertexArray) {
return null;
}
break;
}
return value;
}
function virtualBindFramebuffer(bindpoint, framebuffer) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
const gl = sharedWebGLContext;
if (bindpoint === WebGLRenderingContext.FRAMEBUFFER) {
if (framebuffer === null) {
// bind our drawingBuffer
framebuffer = this._drawingbufferFramebuffer;
}
}
gl.bindFramebuffer(bindpoint, framebuffer);
this._state.framebuffer = framebuffer;
}
function createWrapper(origFn) {
// lots of optimization could happen here depending on specific functions
return function(...args) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
return origFn.call(sharedWebGLContext, ...args);
};
}
function clearIfNeeded(vctx) {
if (vctx._needClear) {
vctx._needClear = false;
const gl = sharedWebGLContext;
gl.bindFramebuffer(gl.FRAMEBUFFER, vctx._drawingbufferFramebuffer);
gl.disable(gl.SCISSOR_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
enableDisable(gl, gl.SCISSOR_TEST, vctx._state.scissorTest);
gl.bindFramebuffer(gl.FRAMEBUFFER, vctx._state.framebuffer);
}
}
function beforeDraw(vctx) {
makeCurrentContext(vctx);
resizeCanvasIfChanged(vctx);
clearIfNeeded(vctx);
}
function afterDraw(vctx) {
if (isFramebufferBindingNull(vctx)) {
vctx._needComposite = true;
if (!someContextsNeedRendering) {
someContextsNeedRendering = true;
setTimeout(renderAllDirtyVirtualCanvases, 0);
}
}
}
function createDrawWrapper(origFn) {
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
beforeDraw(this);
const result = origFn.call(sharedWebGLContext, ...args);
afterDraw(this);
return result;
};
}
function makeCurrentContext(vctx) {
if (currentVirtualContext === vctx) {
return;
}
// save all current WebGL state on the previous current virtual context
if (currentVirtualContext) {
saveAllState(currentVirtualContext._state, currentVirtualContext);
}
// restore all state for the new context
restoreAllState(vctx._state, vctx);
// check if the current state is supposed to be rendering to the canvas.
// if so bind vctx._drawingbuffer
currentVirtualContext = vctx;
}
function resizeCanvasIfChanged(vctx) {
const width = vctx.canvas.width;
const height = vctx.canvas.height;
if (width !== vctx._width || height !== vctx._height) {
vctx._width = width;
vctx._height = height;
const gl = sharedWebGLContext;
const oldTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
const format = vctx._contextAttributes.alpha ? gl.RGBA : gl.RGB;
gl.bindTexture(gl.TEXTURE_2D, vctx._drawingbufferTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, null);
gl.bindTexture(gl.TEXTURE_2D, oldTexture);
if (vctx._depthRenderbuffer) {
const oldRenderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
const internalFormat = vctx._contextAttributes.stencil ? gl.DEPTH_STENCIL : gl.DEPTH_COMPONENT16;
gl.bindRenderbuffer(gl.RENDERBUFFER, vctx._depthRenderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, internalFormat, width, height);
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
}
}
}
function createOrGetVirtualWebGLContext(canvas, type, contextAttributes) {
// check if this canvas already has a context
const existingVirtualCtx = canvasToVirtualContextMap.get(canvas);
if (existingVirtualCtx) {
return existingVirtualCtx;
}
const compositor = settings.compositorCreator(canvas, type, contextAttributes) || new DefaultCompositor(canvas, type, contextAttributes);
const newVirtualCtx = new VirtualWebGLContext(canvas, contextAttributes, compositor, () => {
canvasToVirtualContextMap.delete(canvas);
});
canvasToVirtualContextMap.set(canvas, newVirtualCtx);
return newVirtualCtx;
}
function createProgram(gl, shaderSources) {
const program = gl.createProgram();
[gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, shaderSources[ndx]);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader)); // eslint-disable-line
}
gl.attachShader(program, shader);
});
gl.bindAttribLocation(program, 0, 'position');
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program)); // eslint-disable-line
}
return program;
}
function saveAllState(state, vctx) {
// save all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
const gl = sharedWebGLContext;
state.activeTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
// save texture units
for (let i = 0; i < numTextureUnits; ++i) {
gl.activeTexture(gl.TEXTURE0 + i);
const unit = state.textureUnits[i];
unit.texture2D = gl.getParameter(gl.TEXTURE_BINDING_2D);
unit.textureCubemap = gl.getParameter(gl.TEXTURE_BINDING_CUBE_MAP);
}
// bindings
state.arrayBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
state.renderbuffer = gl.getParameter(gl.RENDERBUFFER_BINDING);
state.framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
// save attributes
if (sharedVAOExtension) {
state.vertexArray = gl.getParameter(sharedVAOExtension.VERTEX_ARRAY_BINDING_OES);
} else {
for (let i = 0; i < numAttributes; ++i) {
const attrib = state.attributes[i];
attrib.buffer = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING);
attrib.enabled = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED);
attrib.size = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE);
attrib.stride = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE);
attrib.type = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE);
attrib.normalized = gl.getVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED);
attrib.value = gl.getVertexAttrib(i, gl.CURRENT_VERTEX_ATTRIB);
attrib.offset = gl.getVertexAttribOffset(i, gl.VERTEX_ATTRIB_ARRAY_POINTER);
if (sharedInstanceExtension) {
attrib.divisor = gl.getVertexAttrib(i, sharedInstanceExtension.VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE);
}
}
state.elementArrayBuffer = gl.getParameter(gl.ELEMENT_ARRAY_BUFFER_BINDING);
}
state.blend = gl.getParameter(gl.BLEND);
state.cullFace = gl.getParameter(gl.CULL_FACE);
state.depthTest = gl.getParameter(gl.DEPTH_TEST);
state.dither = gl.getParameter(gl.DITHER);
state.polygonOffsetFill = gl.getParameter(gl.POLYGON_OFFSET_FILL);
state.sampleAlphaToCoverage = gl.getParameter(gl.SAMPLE_ALPHA_TO_COVERAGE);
state.sampleCoverage = gl.getParameter(gl.SAMPLE_COVERAGE);
state.scissorTest = gl.getParameter(gl.SCISSOR_TEST);
state.stencilTest = gl.getParameter(gl.STENCIL_TEST);
state.packAlignment = gl.getParameter(gl.PACK_ALIGNMENT);
state.unpackAlignment = gl.getParameter(gl.UNPACK_ALIGNMENT);
state.unpackColorspaceConversion = gl.getParameter(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL);
state.unpackFlipY = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL);
state.unpackPremultiplyAlpha = gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL);
state.currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
state.viewport = gl.getParameter(gl.VIEWPORT);
state.scissor = gl.getParameter(gl.SCISSOR_BOX);
state.blendSrcRgb = gl.getParameter(gl.BLEND_SRC_RGB);
state.blendDstRgb = gl.getParameter(gl.BLEND_DST_RGB);
state.blendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA);
state.blendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA);
state.blendEquationRgb = gl.getParameter(gl.BLEND_EQUATION_RGB);
state.blendEquationAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA);
state.blendColor = gl.getParameter(gl.BLEND_COLOR);
state.colorClearValue = gl.getParameter(gl.COLOR_CLEAR_VALUE);
state.colorMask = gl.getParameter(gl.COLOR_WRITEMASK);
state.cullFaceMode = gl.getParameter(gl.CULL_FACE_MODE);
state.depthClearValue = gl.getParameter(gl.DEPTH_CLEAR_VALUE);
state.depthFunc = gl.getParameter(gl.DEPTH_FUNC);
state.depthRange = gl.getParameter(gl.DEPTH_RANGE);
state.depthMask = gl.getParameter(gl.DEPTH_WRITEMASK);
state.frontFace = gl.getParameter(gl.FRONT_FACE);
state.generateMipmapHint = gl.getParameter(gl.GENERATE_MIPMAP_HINT);
state.lineWidth = gl.getParameter(gl.LINE_WIDTH);
state.polygonOffsetFactor = gl.getParameter(gl.POLYGON_OFFSET_FACTOR);
state.polygonOffsetUnits = gl.getParameter(gl.POLYGON_OFFSET_UNITS);
state.sampleCoverageValue = gl.getParameter(gl.SAMPLE_COVERAGE_VALUE);
state.sampleCoverageUnits = gl.getParameter(gl.SAMPLE_COVERAGE_INVERT);
state.stencilBackFail = gl.getParameter(gl.STENCIL_BACK_FAIL);
state.stencilBackFunc = gl.getParameter(gl.STENCIL_BACK_FUNC);
state.stencilBackPassDepthFail = gl.getParameter(gl.STENCIL_BACK_PASS_DEPTH_FAIL);
state.stencilBackPassDepthPass = gl.getParameter(gl.STENCIL_BACK_PASS_DEPTH_PASS);
state.stencilBackRef = gl.getParameter(gl.STENCIL_BACK_REF);
state.stencilBackValueMask = gl.getParameter(gl.STENCIL_BACK_VALUE_MASK);
state.stencilBackWriteMask = gl.getParameter(gl.STENCIL_BACK_WRITEMASK);
state.stencilClearValue = gl.getParameter(gl.STENCIL_CLEAR_VALUE);
state.stencilFail = gl.getParameter(gl.STENCIL_FAIL);
state.stencilFunc = gl.getParameter(gl.STENCIL_FUNC);
state.stencilPassDepthFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL);
state.stencilPassDepthPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS);
state.stencilRef = gl.getParameter(gl.STENCIL_REF);
state.stencilValueMask = gl.getParameter(gl.STENCIL_VALUE_MASK);
state.stencilWriteMask = gl.getParameter(gl.STENCIL_WRITEMASK);
for (const fns of extensionSaveRestoreHelpersArray) {
fns.save(state, vctx);
}
}
function restoreAllState(state, vctx) {
// restore all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
// save all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
const gl = sharedWebGLContext;
// restore texture units
for (let i = 0; i < numTextureUnits; ++i) {
gl.activeTexture(gl.TEXTURE0 + i);
const unit = state.textureUnits[i];
gl.bindTexture(gl.TEXTURE_2D, unit.texture2D);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, unit.textureCubemap);
}
gl.activeTexture(state.activeTexture);
// restore attributes
if (sharedVAOExtension) {
sharedVAOExtension.bindVertexArrayOES(state.vertexArray);
} else {
for (let i = 0; i < numAttributes; ++i) {
const attrib = state.attributes[i];
if (attrib.enabled) {
gl.enableVertexAttribArray(i);
} else {
gl.disableVertexAttribArray(i);
}
gl.vertexAttrib4fv(i, attrib.value);
gl.bindBuffer(gl.ARRAY_BUFFER, attrib.buffer);
gl.vertexAttribPointer(i, attrib.size, attrib.type, attrib.normalized, attrib.stride, attrib.offset);
if (sharedInstanceExtension) {
sharedInstanceExtension.vertexAttribDivisorANGLE(i, attrib.divisor);
}
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, state.elementArrayBuffer);
}
// bindings'
gl.bindBuffer(gl.ARRAY_BUFFER, state.arrayBuffer);
gl.bindRenderbuffer(gl.RENDERBUFFER, state.renderbuffer);
gl.bindFramebuffer(gl.FRAMEBUFFER, state.framebuffer);
enableDisable(gl, gl.BLEND, state.blend);
enableDisable(gl, gl.CULL_FACE, state.cullFace);
enableDisable(gl, gl.DEPTH_TEST, state.depthTest);
enableDisable(gl, gl.DITHER, state.dither);
enableDisable(gl, gl.POLYGON_OFFSET_FILL, state.polygonOffsetFill);
enableDisable(gl, gl.SAMPLE_ALPHA_TO_COVERAGE, state.sampleAlphaToCoverage);
enableDisable(gl, gl.SAMPLE_COVERAGE, state.sampleCoverage);
enableDisable(gl, gl.SCISSOR_TEST, state.scissorTest);
enableDisable(gl, gl.STENCIL_TEST, state.stencilTest);
gl.pixelStorei(gl.PACK_ALIGNMENT, state.packAlignment);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, state.unpackAlignment);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, state.unpackColorspaceConversion);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, state.unpackFlipY);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, state.unpackPremultiplyAlpha);
gl.useProgram(state.currentProgram);
gl.viewport(...state.viewport);
gl.scissor(...state.scissor);
gl.blendFuncSeparate(state.blendSrcRgb, state.blendDstRgb, state.blendSrcAlpha, state.blendDstAlpha);
gl.blendEquationSeparate(state.blendEquationRgb, state.blendEquationAlpha);
gl.blendColor(...state.blendColor);
gl.clearColor(...state.colorClearValue);
gl.colorMask(...state.colorMask);
gl.cullFace(state.cullFaceMode);
gl.clearDepth(state.depthClearValue);
gl.depthFunc(state.depthFunc);
gl.depthRange(...state.depthRange);
gl.depthMask(state.depthMask);
gl.frontFace(state.frontFace);
gl.hint(gl.GENERATE_MIPMAP_HINT, state.generateMipmapHint);
gl.lineWidth(state.lineWidth);
gl.polygonOffset(state.polygonOffsetFactor, state.polygonOffsetUnits);
gl.sampleCoverage(state.sampleCoverageValue, state.sampleCoverageUnits);
gl.stencilFuncSeparate(gl.BACK, state.stencilBackFunc, state.stencilBackRef, state.stencilBackValueMask);
gl.stencilFuncSeparate(gl.FRONT, state.stencilFunc, state.stencilRef, state.stencilValueMask);
gl.stencilOpSeparate(gl.BACK, state.stencilBackFail, state.stencilBackPassDepthFail, state.stencilBackPassDepthPass);
gl.stencilOpSeparate(gl.FRONT, state.stencilFail, state.stencilPassDepthFail, state.stencilPassDepthPass);
gl.stencilMaskSeparate(gl.BACK, state.stencilBackWriteMask);
gl.stencilMaskSeparate(gl.FRONT, state.stencilWriteMask);
gl.clearStencil(state.stencilClearValue);
for (const fns of extensionSaveRestoreHelpersArray) {
fns.restore(state, vctx);
}
}
function enableDisable(gl, feature, enable) {
if (enable) {
gl.enable(feature);
} else {
gl.disable(feature);
}
}
function renderAllDirtyVirtualCanvases() {
if (!someContextsNeedRendering) {
return;
}
someContextsNeedRendering = false;
// save all current WebGL state on the previous current virtual context
if (currentVirtualContext) {
saveAllState(currentVirtualContext._state, currentVirtualContext);
currentVirtualContext = null;
}
// set the state back to the one for drawing the canvas
restoreAllState(baseState);
for (const vctx of canvasToVirtualContextMap.values()) {
if (!vctx._needComposite) {
continue;
}
vctx._needComposite = false;
vctx.composite(sharedWebGLContext);
}
}
window.requestAnimationFrame = (function(origFn) {
return function(callback) {
return origFn.call(window, (time) => {
const result = callback(time);
renderAllDirtyVirtualCanvases();
return result;
});
};
}(window.requestAnimationFrame))
function setup(options) {
Object.assign(settings, options);
}
window.virtualWebGL = {
setup,
};
}());
Comments 51 条评论
你好,求教一下次元口袋阿里云盘里面的资源怎么下载呀?我点击之后是浏览器自动下载速度很慢,也没法添加到阿里云盘里。
@aiden 很久没维护了,你想要哪个文件我给你分享网盘链接吧
@QiuYeDx 07.超级马里奥 RPG(Super Mario RPG )整合版v1.0.1 谢谢您!
@aiden 【快传】我给你发了 07….0.1, 快来看看 https://www.alipan.com/t/qmnIakLoEWAUCBxB9Qwy 点击链接即可保存。「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
@aiden 这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论
这是一条私密评论