Is WebGL capable of using the GPU's full power? - webgl

I tried running a GPU intensive WebGL shader and couldnt get my GPU to peak over ~30% usage in task manager, even when visiting pages that benchmark wild WebGL simulations like this one which renders 30,000 fish in a complex environment. Perhaps this is a WebGL security feature? Is there any way programatically, even if it involves disabling security settings in the browser (any browser), to force WebGL to utilize 100% of the GPU?

What did you try? It's trival to use 100% of your GPUs power. Just give it something to draw that takes a long time. The aquarium you linked to is not designed to do that.
Here's a trival one
const gl = document.createElement('canvas').getContext('webgl');
gl.canvas.width = 2048;
gl.canvas.height = 2048;
const vs = `
attribute vec4 position;
void main() {
gl_Position = position;
}
`;
const fs = `
precision mediump float;
void main() {
gl_FragColor = vec4(1);
}
`;
const quad = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
];
const maxQuads = 50000;
const quads = [];
for (let i = 0; i < maxQuads; ++i) {
quads.push(...quad);
}
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
data: quads,
numComponents: 2,
},
});
let count = 10;
function render() {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
gl.drawArrays(gl.TRIANGLES, 0, 6 * count);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
document.querySelector('input').addEventListener('input', (e) => {
count = Math.min(parseInt(e.target.value), maxQuads);
});
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<p>increase number to increase GPU usage. Large numbers will get the browser or OS to reset the GPU.</p>
<input type="number" value="10">
For me a value of 30 saturated the GPU and also made everything slow (the OS needs the GPU too but we're hogging it)
At 30 each draw call is drawing 2048x2048x30 pixels. That's 125.8 million pixels per draw call.

Related

Can an layout(location=n) out skip an index for drawBuffers in WebGL?

I'm working on MRT in my graphics engine.
An interesting point i'm at (and aim to fix) has my generated fragment shader spitting out:
layout(location = 0) out vec4 thing1;
layout(location = 2) out vec4 thing2;
The drawBuffers call on the application side calls something like this:
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.NONE, gl.COLOR_ATTACHMENT1]);
However, I'm getting an error:
WebGL: INVALID_OPERATION: drawBuffers: COLOR_ATTACHMENTi_EXT or NONE
So obviously, this would appear to not be allowed. From the documentation I've read from a wikipedia article discussing it:
https://www.khronos.org/opengl/wiki/Fragment_Shader
It states along the lines that the layout location specified refers to the array index specified from the drawBuffers call. So, in theory I would have thought this shader to configuration would be valid.
What am I missing from my understanding that makes this not work?
I ask for understanding mostly and not to fix my program, my generator will correct the indices when I'm done to be 'correct' with no location index skipping.
Update: As noted below, you CAN skip layout locations in the shader. My issue was the improper formatting of the drawBuffers call where I had COLOR_ATTACHMENT1 in the index where ONLY COLOR_ATTACHMENT2 is valid.
This is wrong
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.NONE, gl.COLOR_ATTACHMENT1]);
the i-th attachment must be gl.NONE or gl.COLOR_ATTACHMENTi
so it has to be this
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.NONE, gl.COLOR_ATTACHMENT2]);
function main() {
const gl = document.querySelector('canvas').getContext('webgl2');
const vs = `#version 300 es
void main() {
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 100.0;
}
`;
const fs = `#version 300 es
precision highp float;
layout(location = 0) out vec4 thing1;
layout(location = 2) out vec4 thing2;
void main () {
thing1 = vec4(1, 0, 0, 1);
thing2 = vec4(0, 0, 1, 1);
}
`;
const prg = twgl.createProgram(gl, [vs, fs]);
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
createTextureAndAttach(gl, gl.COLOR_ATTACHMENT0);
createTextureAndAttach(gl, gl.COLOR_ATTACHMENT2);
gl.drawBuffers([
gl.COLOR_ATTACHMENT0,
gl.NONE,
gl.COLOR_ATTACHMENT2,
]);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
console.error("can't render to this framebuffer combo");
return;
}
gl.useProgram(prg);
gl.viewport(0, 0, 1, 1);
gl.drawArrays(gl.POINTS, 0, 1);
checkError();
read(gl.COLOR_ATTACHMENT0);
read(gl.COLOR_ATTACHMENT2);
checkError();
function checkError() {
const err = gl.getError();
if (err) {
console.error(twgl.glEnumToString(gl, err));
}
}
function read(attachmentPoint) {
gl.readBuffer(attachmentPoint);
const pixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
console.log(Array.from(pixel).join(','));
}
function createTextureAndAttach(gl, attachmentPoint) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, tex, 0);
}
}
main();
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
note: Referencing the OpenGL docs for WebGL are often wrong and/or misleading for WebGL. You need to reference the OpenGL 3.0 ES spec for WebGL2

WebGL rendering outside of browser paint time

We are building a WebGL application that has some high render-load objects. Is there a way we can render those object outside of browser-paint time, i.e. in the background? We don't want our FPS going down, and breaking up our rendering process is possible (to split between frames).
Three ideas come to mind.
You can render to a texture via a framebuffer over many frames, when you're done you render that texture to the canvas.
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
uniform sampler2D tex;
varying vec2 v_texcoord;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
// compile shader, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
],
},
texcoord: {
numComponents: 2,
data: [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
],
},
});
// create a framebuffer with a texture and depth buffer
// same size as canvas
// gl.createTexture, gl.texImage2D, gl.createFramebuffer
// gl.framebufferTexture2D
const framebufferInfo = twgl.createFramebufferInfo(gl);
const infoElem = document.querySelector('#info');
const numDrawSteps = 16;
let drawStep = 0;
let time = 0;
// draw over several frames. Return true when ready
function draw() {
// draw to texture
// gl.bindFrambuffer, gl.viewport
twgl.bindFramebufferInfo(gl, framebufferInfo);
if (drawStep == 0) {
// on the first step clear and record time
gl.disable(gl.SCISSOR_TEST);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
time = performance.now() * 0.001;
}
// this represents drawing something.
gl.enable(gl.SCISSOR_TEST);
const halfWidth = framebufferInfo.width / 2;
const halfHeight = framebufferInfo.height / 2;
const a = time * 0.1 + drawStep
const x = Math.cos(a ) * halfWidth + halfWidth;
const y = Math.sin(a * 1.3) * halfHeight + halfHeight;
gl.scissor(x, y, 16, 16);
gl.clearColor(
drawStep / 16,
drawStep / 6 % 1,
drawStep / 3 % 1,
1);
gl.clear(gl.COLOR_BUFFER_BIT);
drawStep = (drawStep + 1) % numDrawSteps;
return drawStep === 0;
}
let frameCount = 0;
function render() {
++frameCount;
infoElem.textContent = frameCount;
if (draw()) {
// draw to canvas
// gl.bindFramebuffer, gl.viewport
twgl.bindFramebufferInfo(gl, null);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
gl.disable(gl.SCISSOR_TEST);
gl.useProgram(programInfo.program);
// gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// gl.uniform...
twgl.setUniformsAndBindTextures(programInfo, {
tex: framebufferInfo.attachments[0],
});
// draw the quad
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<canvas></canvas>
<div id="info"></div>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
You can make 2 canvases. A webgl canvas that is not in the DOM. You render to it over many frames and when you're done you draw it to a 2D canvas with ctx.drawImage(webglCanvas, ...) This is basically the same as #1 except you're letting the browser "render that texture to a canvas" part
const ctx = document.querySelector('canvas').getContext('2d');
const gl = document.createElement('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
uniform sampler2D tex;
varying vec2 v_texcoord;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
// compile shader, link program, look up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const infoElem = document.querySelector('#info');
const numDrawSteps = 16;
let drawStep = 0;
let time = 0;
// draw over several frames. Return true when ready
function draw() {
if (drawStep == 0) {
// on the first step clear and record time
gl.disable(gl.SCISSOR_TEST);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
time = performance.now() * 0.001;
}
// this represents drawing something.
gl.enable(gl.SCISSOR_TEST);
const halfWidth = gl.canvas.width / 2;
const halfHeight = gl.canvas.height / 2;
const a = time * 0.1 + drawStep
const x = Math.cos(a ) * halfWidth + halfWidth;
const y = Math.sin(a * 1.3) * halfHeight + halfHeight;
gl.scissor(x, y, 16, 16);
gl.clearColor(
drawStep / 16,
drawStep / 6 % 1,
drawStep / 3 % 1,
1);
gl.clear(gl.COLOR_BUFFER_BIT);
drawStep = (drawStep + 1) % numDrawSteps;
return drawStep === 0;
}
let frameCount = 0;
function render() {
++frameCount;
infoElem.textContent = frameCount;
if (draw()) {
// draw to canvas
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(gl.canvas, 0, 0);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<canvas></canvas>
<div id="info"></div>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
You can use OffscreenCanvas and render in a worker. This has only shipped in Chrome though.
Note that if you DOS the GPU (give the GPU too much work) you can still affect the responsiveness of the main thread because most GPUs do not support pre-emptive multitasking. So, if you have a lot of really heavy work then split it up into smaller tasks.
As an example if you took one of the heaviest shaders from shadertoy.com that runs at say 0.5 fps when rendered at 1920x1080, even offscreen it will force the entire machine to run at 0.5 fps. To fix you'd need to render smaller portions over several frames. If it's running at 0.5 fps that suggests you need to split it up into at least 120 smaller parts, maybe more, to keep the main thread responsive and at 120 smaller parts you'd only see the results every 2 seconds.
In fact trying it out shows some issues. Here's Iq's Happy Jumping Example drawn over 960 frames. It still can't keep 60fps on my late 2018 Macbook Air even though it's rendering only 2160 pixels a frame (2 columns of a 1920x1080 canvas). The issue is likely some parts of the scene have to recurse deeply and there is no way knowing before hand which parts of the scene that will be. One reason why shadertoy style shaders using signed distance fields are more of a toy (hence shaderTOY) and not actually a production style technique.
Anyway, the point of that is if you give the GPU too much work you'll still get an unresponsive machine.

Why unbind gl.UNIFORM_BUFFER before calling bindBufferRange()?

From looking at some different examples in the wild, it seems that uploading data to a buffer, for use as a uniform buffer, does the following sequence:
bindBuffer()
bufferData()
bindBuffer() - with null, i.e. "unbinding"
bindBufferRange()
What is the purpose of step 3?
You don’t need to do it in that order.
Simplest example:
'use strict';
const vs = `#version 300 es
void main() {
gl_PointSize = 128.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision mediump float;
uniform Color {
vec4 u_color;
};
out vec4 outColor;
void main() {
outColor = u_color;
}
`;
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) alert('need webgl2');
const program = twgl.createProgram(gl, [vs, fs]);
const color = new Float32Array([1, 0.5, 0.7, 1]);
const buffer = gl.createBuffer();
// there's only 1 so I believe it's safe to guess index 0
const uniformBlockIndex = 0;
const uniformBlockBinding = 0;
gl.uniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
// at render time
gl.useProgram(program);
// for each block
{
const uniformBlockBufferOffset = 0;
const uniformBlockBufferOffsetByteLength = 16; // 4 floats
gl.bindBufferRange(gl.UNIFORM_BUFFER, uniformBlockBinding, buffer, uniformBlockBufferOffset, uniformBlockBufferOffsetByteLength);
// set the data
gl.bufferData(gl.UNIFORM_BUFFER, color, gl.DYNAMIC_DRAW);
}
gl.drawArrays(gl.POINTS, 0, 1);
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
If you’d like to see a complex example you can dig through this example. It queries all the data about uniform buffers when the program is created. How many there are, what their names are, which uniforms they use, what the types of those uniforms are. This happens when you call twgl.createProgramInfo which you can look inside and see that info is created in createUniformBlockSpecFromProgram
Then later, using the block spec, you can create a typedarray with premade views into that array for all the uniforms by calling twgl.createUniformBlockInfo
const ubi = twgl.createUniformBlockInfo(...)
You could set the uniform values in the typedarray through the views directly using
ubi.uniforms.nameOfUniform.set(newValue)
but that would be brittle since blocks may get optimized out while debugging so instead you can use the less brittle
twgl.setBlockUniforms(ubi, {nameOfUniform: newValue});
When you actually want the data in the typedarray to get uploaded to the GPU you call
twgl.setUniformBlock(...);
Which both binds the uniform block to its assigned binding AND uploads the data to the GPU.
If you just want to bind an existing block (no need to upload new data) then
twgl.bindUniformBlock(gl, programInfo, ubi);
The pattern though is as you see in the example
bindBufferRange
bufferData
bindBufferRange already binds the buffer so we can just use that binding to upload the data.
Test (non twgl)
'use strict';
const vs = `#version 300 es
void main() {
gl_PointSize = 128.0;
gl_Position = vec4(0, 0, 0, 1);
}
`;
const fs = `#version 300 es
precision mediump float;
uniform Color1 {
vec4 u_color1;
};
uniform Color2 {
vec4 u_color2;
};
out vec4 outColor;
void main() {
outColor = u_color1 + u_color2;
}
`;
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) alert('need webgl2');
const program = twgl.createProgram(gl, [vs, fs]);
const color1 = new Float32Array([1, 0, 0, 1]);
const buffer1 = gl.createBuffer();
const color2 = new Float32Array([0, 0, 1, 1]);
const buffer2 = gl.createBuffer();
// there's only 2 and they are the same format so we don't really
// care which is which to see the results.
const uniformBlockIndex = 0;
const uniformBlockBinding = 0;
gl.uniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
gl.uniformBlockBinding(program, uniformBlockIndex + 1, uniformBlockBinding + 1);
// at render time
gl.useProgram(program);
{
const uniformBlockBufferOffset = 0;
const uniformBlockBufferOffsetByteLength = 16; // 4 floats
gl.bindBufferRange(gl.UNIFORM_BUFFER, uniformBlockBinding, buffer1, uniformBlockBufferOffset, uniformBlockBufferOffsetByteLength);
// set the data
gl.bufferData(gl.UNIFORM_BUFFER, color1, gl.DYNAMIC_DRAW);
gl.bindBufferRange(gl.UNIFORM_BUFFER, uniformBlockBinding + 1, buffer2, uniformBlockBufferOffset, uniformBlockBufferOffsetByteLength);
// set the data
gl.bufferData(gl.UNIFORM_BUFFER, color2, gl.DYNAMIC_DRAW);
}
gl.drawArrays(gl.POINTS, 0, 1);
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
The example above shows bindBufferRange does 2 things.
it binds the buffer to the UNIFORM_BUFFER bind point
it binds a portion of the buffer to the uniform buffer index.
We know it worked because the result is purple. If it didn’t work it would either be red or blue
From the OpenGL ES 3.0 spec section 2.10.1.1 in relation to bindBufferRange
Each target represents an indexed array of buffer object binding points, as well
as a single general binding point that can be used by other buffer object manipulation functions

glFramebufferTexture2D on webgl2 with mipmaps levels

With webGL2 derived from ES3.0 I thought that we can use mipmap levels as the last parameter of:
void glFramebufferTexture2D(GLenum target,
GLenum attachment,
GLenum textarget,
GLuint texture,
GLint level);
Now from Khronos ES3.0 official documentation states that mipmap levels are supposed to work:
level:
Specifies the mipmap level of texture to attach.
From Khronos ES2.0 instead it says it must be 0
level:
Specifies the mipmap level of the texture image to be attached, which must be 0.
Now, the I cannot find any docs from WebGL2.0 context about glFramebufferTexture2D, but the mozilla docs states that mipmap layer must be 0, as in ES2.0, here:
Mozilla WebGL doc
level:
A GLint specifying the mipmap level of the texture image to be attached. Must be 0.
That page I think refers to WebGL1 context but it has mentions of WebGL2 features in it, and I cannot find glFramebufferTexture2D on WebGL2 docs.
So to wrap it up, is there a way to use mipmap levels on framebuffer targets on WebGL2.0?
(I've looked into layered images but AFAIK layered rendering is not available for WebGL2.0)
is there a way to use mipmap levels on framebuffer targets on WebGL2.0
Yes
I'd close the answer there but I guess I wonder did you actually try something and have it not work? You have to create a WebGL2 context to use mipmap levels as framebuffer attachments but otherwise yes, it works. On WebGL1 it will not work.
function main() {
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) {
return alert('need webgl2');
}
const vs = `#version 300 es
void main() {
// just draw an 8x8 pixel point in the center of the target
// this shader needs/uses no attributes
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 8.0;
}
`;
const fsColor = `#version 300 es
precision mediump float;
uniform vec4 color;
out vec4 outColor;
void main() {
outColor = color;
}
`;
const fsTexture = `#version 300 es
precision mediump float;
uniform sampler2D tex;
out vec4 outColor;
void main() {
// this shader needs no texcoords since we just
// use gl_PoitnCoord provided by rendering a point with gl.POINTS
// bias lets select the mip level so no need for
// some fancier shader just to show that it's working.
float bias = gl_PointCoord.x * gl_PointCoord.y * 4.0;
outColor = texture(tex, gl_PointCoord.xy, bias);
}
`;
// compile shaders, link into programs, look up attrib/uniform locations
const colorProgramInfo = twgl.createProgramInfo(gl, [vs, fsColor]);
const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fsTexture]);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
const levels = 4;
const width = 8;
const height = 8;
gl.texStorage2D(gl.TEXTURE_2D, levels, gl.RGBA8, width, height);
// make a framebuffer for each mip level
const fbs = [];
for (let level = 0; level < levels; ++level) {
const fb = gl.createFramebuffer();
fbs.push(fb);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D, tex, level);
}
// render a different color to each level
const colors = [
[1, 0, 0, 1], // red
[0, 1, 0, 1], // green
[0, 0, 1, 1], // blue
[1, 1, 0, 1], // yellow
];
gl.useProgram(colorProgramInfo.program);
for (let level = 0; level < levels; ++level) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbs[level]);
const size = width >> level;
gl.viewport(0, 0, size, size);
twgl.setUniforms(colorProgramInfo, { color: colors[level] });
const offset = 0;
const count = 1;
gl.drawArrays(gl.POINTS, offset, count); // draw 1 point
}
// draw the texture's mips to the canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(textureProgramInfo.program);
// no need to bind the texture it's already bound
// no need to set the uniform it defaults to 0
gl.drawArrays(gl.POINT, 0, 1); // draw 1 point
}
main();
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas width="8" height="8" style="width: 128px; height: 128px;"></canvas>
You can also render to layers of TEXTURE_2D_ARRAY texture.
function main() {
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) {
return alert('need webgl2');
}
const vs = `#version 300 es
void main() {
// just draw an 8x8 pixel point in the center of the target
// this shader needs/uses no attributes
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 8.0;
}
`;
const fsColor = `#version 300 es
precision mediump float;
uniform vec4 color;
out vec4 outColor;
void main() {
outColor = color;
}
`;
const fsTexture = `#version 300 es
precision mediump float;
uniform mediump sampler2DArray tex;
out vec4 outColor;
void main() {
// this shader needs no texcoords since we just
// use gl_PoitnCoord provided by rendering a point with gl.POINTS
float layer = gl_PointCoord.x * gl_PointCoord.y * 4.0;
outColor = texture(tex, vec3(gl_PointCoord.xy, layer));
}
`;
// compile shaders, link into programs, look up attrib/uniform locations
const colorProgramInfo = twgl.createProgramInfo(gl, [vs, fsColor]);
const textureProgramInfo = twgl.createProgramInfo(gl, [vs, fsTexture]);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);
const levels = 1;
const width = 8;
const height = 8;
const layers = 4;
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, levels, gl.RGBA8, width, height, layers);
// only use level 0 (of course we could render to levels in layers as well)
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// make a framebuffer for each layer
const fbs = [];
for (let layer = 0; layer < layers; ++layer) {
const fb = gl.createFramebuffer();
fbs.push(fb);
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
const level = 0;
gl.framebufferTextureLayer(
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
tex, level, layer);
}
// render a different color to each layer
const colors = [
[1, 0, 0, 1], // red
[0, 1, 0, 1], // green
[0, 0, 1, 1], // blue
[1, 1, 0, 1], // yellow
];
gl.useProgram(colorProgramInfo.program);
for (let layer = 0; layer < layers; ++layer) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbs[layer]);
gl.viewport(0, 0, width, height);
twgl.setUniforms(colorProgramInfo, { color: colors[layer] });
const offset = 0;
const count = 1;
gl.drawArrays(gl.POINTS, offset, count); // draw 1 point
}
// draw the texture's mips to the canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(textureProgramInfo.program);
// no need to bind the texture it's already bound
// no need to set the uniform it defaults to 0
gl.drawArrays(gl.POINT, 0, 1); // draw 1 point
}
main();
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas width="8" height="8" style="width: 128px; height: 128px; image-rendering: pixelated;"></canvas>

WebGL: fade drawing buffer

I've set preserveDrawingBuffer to true.
Doing this results in everything drawn on the buffer to be seen all at once, however,
I was wondering if there is a way to somehow fade the buffer as time goes on so that the old elements drawn disappear over time, and the newest drawn elements appear with a relatively high opacity until they also fade away.
Is there a better way to achieve such an effect?
I've tried to render previous elements again by lowering their opacity until it reaches 0 but it didn't seem like an efficient way of fading as once something is drawn I don't plan on changing it.
Thanks!
It's actually common it just redraw stuff which I went over here
WebGL: smoothly fade lines out of canvas
Redrawing stuff means you can keep some things from not fading out. For example if you're making a space shooting game and you only want explosions and missile trails to fade out but you don't want the spaceships and asteroids to fade out then you need to do it by redrawing everything and manually fading stuff out by drawn them while decreasing their alpha
If you just want everything to fade out then you can use a post processing type effect.
You make 2 textures and attach them to 2 framebuffers. You blend/fade the first framebuffer fadeFb1 into the second one fadeFb2 with a fadeColor using
gl_FragColor = mix(textureColor, fadeColor, mixAmount);
You then draw any new stuff to fadeFb2
Then finally draw fadeFb2 to the canvas so you can see the result.
The next frame you do the same thing except swap which buffer you're drawing to and which one you're fading to.
frame 0: mix(fadeFb1,fadeColor)->fadeFb2, draw->fadeFb2, fadeFB2->canvas
frame 1: mix(fadeFb2,fadeColor)->fadeFb1, draw->fadeFb1, fadeFB1->canvas
frame 2: mix(fadeFb1,fadeColor)->fadeFb2, draw->fadeFb2, fadeFB2->canvas
...
Note you don't clear when you draw since you need the result to be left behind
As for setting up framebuffers there's a tutorial here that might be useful
http://webglfundamentals.org/webgl/lessons/webgl-image-processing-continued.html
Here's an example using twgl since I'm too lazy for straight WebGL
var vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
var fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
var vsQuad = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = position;
v_texcoord = texcoord;
}
`;
var fsFade = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform float u_mixAmount;
uniform vec4 u_fadeColor;
void main() {
vec4 color = texture2D(u_texture, v_texcoord);
gl_FragColor = mix(color, u_fadeColor, u_mixAmount);
}
`;
var fsCopy = `
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_texcoord);
}
`;
var $ = document.querySelector.bind(document);
var mixAmount = 0.05;
var mixElem = $("#mix");
var mixValueElem = $("#mixValue");
mixElem.addEventListener('input', function(e) {
setMixAmount(e.target.value / 100);
});
function setMixAmount(value) {
mixAmount = value;
mixValueElem.innerHTML = mixAmount;
}
setMixAmount(mixAmount);
var gl = $("canvas").getContext("webgl");
var m4 = twgl.m4;
var programInfo = twgl.createProgramInfo(gl, [vs, fs]);
var fadeProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsFade]);
var copyProgramInfo = twgl.createProgramInfo(gl, [vsQuad, fsCopy]);
// Creates a -1 to +1 quad
var quadBufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);
// Creates an RGBA/UNSIGNED_BYTE texture and depth buffer framebuffer
var imgFbi = twgl.createFramebufferInfo(gl);
// Creates 2 RGBA texture + depth framebuffers
var fadeAttachments = [
{ format: gl.RGBA, min: gl.NEAREST, max: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE, },
{ format: gl.DEPTH_STENCIL },
];
var fadeFbi1 = twgl.createFramebufferInfo(gl, fadeAttachments);
var fadeFbi2 = twgl.createFramebufferInfo(gl, fadeAttachments);
function drawThing(gl, x, y, rotation, scale, color) {
var matrix = m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
matrix = m4.translate(matrix, [x, y, 0]);
matrix = m4.rotateZ(matrix, rotation);
matrix = m4.scale(matrix, [scale, scale, 1]);
gl.useProgram(programInfo.program);
twgl.setBuffersAndAttributes(gl, programInfo, quadBufferInfo);
twgl.setUniforms(programInfo, {
u_matrix: matrix,
u_color: color,
});
twgl.drawBufferInfo(gl, gl.TRIANGLES, quadBufferInfo);
}
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.random() * (max - min);
}
function render(time) {
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
twgl.resizeFramebufferInfo(gl, fadeFbi1, fadeAttachments);
twgl.resizeFramebufferInfo(gl, fadeFbi2, fadeAttachments);
}
// fade by copying from fadeFbi1 into fabeFbi2 using mixAmount.
// fadeFbi2 will contain mix(fadeFb1, u_fadeColor, u_mixAmount)
twgl.bindFramebufferInfo(gl, fadeFbi2);
gl.useProgram(fadeProgramInfo.program);
twgl.setBuffersAndAttributes(gl, fadeProgramInfo, quadBufferInfo);
twgl.setUniforms(fadeProgramInfo, {
u_texture: fadeFbi1.attachments[0],
u_mixAmount: mixAmount,
u_fadeColor: [0, 0, 0, 0],
});
twgl.drawBufferInfo(gl, gl.TRIANGLES, quadBufferInfo);
// now draw new stuff to fadeFb2. Notice we don't clear!
twgl.bindFramebufferInfo(gl, fadeFbi2);
var x = rand(gl.canvas.width);
var y = rand(gl.canvas.height);
var rotation = rand(Math.PI);
var scale = rand(10, 20);
var color = [rand(1), rand(1), rand(1), 1];
drawThing(gl, x, y, rotation, scale, color);
// now copy fadeFbi2 to the canvas so we can see the result
twgl.bindFramebufferInfo(gl, null);
gl.useProgram(copyProgramInfo.program);
twgl.setBuffersAndAttributes(gl, copyProgramInfo, quadBufferInfo);
twgl.setUniforms(copyProgramInfo, {
u_texture: fadeFbi2.attachments[0],
});
twgl.drawBufferInfo(gl, gl.TRIANGLES, quadBufferInfo);
// swap the variables so we render to the opposite textures next time
var temp = fadeFbi1;
fadeFbi1 = fadeFbi2;
fadeFbi2 = temp;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
#ui { position: absolute; top: 0 }
<script src="https://twgljs.org/dist/twgl-full.min.js"></script>
<canvas></canvas>
<div id="ui">
<span>mix:</span><input id="mix" type="range" min="0" max="100" value="5" /><span id="mixValue"></span>
</div>
The preserveDrawingBuffer flag is useful on a device with limited memory (mobile phones) as it allows those devices to reuse that chunk of memory.
The fading/ghosting effect is done in a different manner: you allocate a texture with the same size as the viewport and do the darkening on this texture instead. Every frame you re-render the contents of this texture to itself while multiplying the color value with a fading factor (say 0.9). Afterwards, on the same texture you render your new elements and finally you render the texture to the viewport (a simple "copy-render").

Resources