can I use multiple texture with drawArraysInstancedANGLE? - webgl

I try to draw multiple icon on screen and I use drawArraysInstancedANGLE method.
I try to use multiple texture like this but some icons draw diffrent geometry, I can not find what draw like that.
I use one big icon map texture and fill icon vertex coord array with this func:
FillIconTextCoordBuffer(data, mapW, mapH, i) {
const ULiconW = data.x / mapW
const ULiconH = data.y / mapH
const LRiconW = (data.x + data.width) / mapW
const LRiconH = (data.y + data.height) / mapH
const { gl } = this.FGlobe
this.IconMapTexCoordArr[i] = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, this.IconMapTexCoordArr[i])
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
ULiconW, ULiconH,
LRiconW, LRiconH,
LRiconW, ULiconH,
LRiconW, LRiconH,
ULiconW, ULiconH,
ULiconW, LRiconH]), gl.STATIC_DRAW)
}
and then my draw func like this:
gl.uniform1f(P2DRotationForLayer, icon.rotDeg)
gl.uniform2fv(P2DScaleLocForLayer, icon.__size)
gl.uniform4fv(P2DOpacityLocForLayer, __iconColor)
ext.vertexAttribDivisorANGLE(P2DoffsetForLayer, 1) // This makes it instanced!
gl.bindBuffer(gl.ARRAY_BUFFER, this.IconMapVertCoordArr)
gl.enableVertexAttribArray(P2DvertexPosForLayer)
gl.vertexAttribPointer(P2DvertexPosForLayer, 3, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, this.IconCoordBuff)
gl.enableVertexAttribArray(P2DoffsetForLayer)
gl.vertexAttribPointer(P2DoffsetForLayer, 2, gl.FLOAT, false, 0, 0)
gl.bindTexture(gl.TEXTURE_2D, IconMap[icon.mapIndex].texture)
gl.disable(gl.BLEND)
for (var j = this.StartCountArr.length; j--;) {
this.DrawInstances(this.StartCountArr[j].start, this.StartCountArr[j].count, j)
}
ext.vertexAttribDivisorANGLE(P2DoffsetForLayer, 0)
and my DrawInstances func like this:
DrawInstances(start, count, j) {
const {
gl, ext,
P2DtextCoordLocForLayer,
} = this.FGlobe
gl.bindBuffer(gl.ARRAY_BUFFER, this.IconMapTexCoordArr[j])
gl.enableVertexAttribArray(P2DtextCoordLocForLayer)
gl.vertexAttribPointer(P2DtextCoordLocForLayer, 2, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
ext.drawArraysInstancedANGLE(gl.TRIANGLES, start, 6, count)
}
Actually some icons drawed right I see 2 different icon but there is one type more look like this:
|\
| \
| \
| /
| /
|/
my icons only two triangle like below, I dont set any shape like above,
______
|\ |
| \ |
| \ |
| \|
------

Here's a sample drawing multiple sprites from a sprite sheet using instanced drawing.
Note if it was me I'd use a matrix for each instance like this example but I thought the code would be simpler using offset and scale here.
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) alert('need ANGLE_instanced_arrays');
const vs = `
attribute vec2 position;
attribute vec2 uv;
attribute vec2 offset; // instanced
attribute vec2 scale; // instanced
attribute vec2 uvOffset; // instanced
attribute vec2 uvScale; // instanced
uniform mat4 matrix;
varying vec2 v_uv;
void main() {
gl_Position = matrix * vec4(position * scale + offset, 0, 1);
v_uv = uv * uvScale + uvOffset;
}
`;
const fs = `
precision highp float;
varying vec2 v_uv;
uniform sampler2D spriteAtlas;
void main() {
gl_FragColor = texture2D(spriteAtlas, v_uv);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const uvLoc = gl.getAttribLocation(program, 'uv');
const offsetLoc = gl.getAttribLocation(program, 'offset');
const scaleLoc = gl.getAttribLocation(program, 'scale');
const uvOffsetLoc = gl.getAttribLocation(program, 'uvOffset');
const uvScaleLoc = gl.getAttribLocation(program, 'uvScale');
const matrixLoc = gl.getUniformLocation(program, 'matrix');
// setup quad positions and uv
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]), gl.STATIC_DRAW);
const uvBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1,
]), gl.STATIC_DRAW);
// create typed array for instanced data
const maxSprites = 1000;
const offsets = new Float32Array(maxSprites * 2);
const scales = new Float32Array(maxSprites * 2);
const uvOffsets = new Float32Array(maxSprites * 2);
const uvScales = new Float32Array(maxSprites * 2);
// create buffers fo instanced data
const offsetBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer);
gl.bufferData(gl.ARRAY_BUFFER, offsets.byteLength, gl.DYNAMIC_DRAW);
const scaleBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer);
gl.bufferData(gl.ARRAY_BUFFER, scales.byteLength, gl.DYNAMIC_DRAW);
const uvOffsetBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvOffsetBuffer);
gl.bufferData(gl.ARRAY_BUFFER, uvOffsets.byteLength, gl.DYNAMIC_DRAW);
const uvScaleBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvScaleBuffer);
gl.bufferData(gl.ARRAY_BUFFER, uvScales.byteLength, gl.DYNAMIC_DRAW);
let spriteNdx = 0;
function addSprite(
spriteAtlasWidth, spriteAtlasHeight,
srcX, srcY, srcWidth, srcHeight,
dstX, dstY, dstWidth, dstHeight) {
const off0 = spriteNdx * 2;
const off1 = off0 + 1;
offsets[off0] = dstX;
offsets[off1] = dstY;
scales[off0] = dstWidth;
scales[off1] = dstHeight;
uvOffsets[off0] = srcX / spriteAtlasWidth;
uvOffsets[off1] = srcY / spriteAtlasHeight;
uvScales[off0] = srcWidth / spriteAtlasWidth;
uvScales[off1] = srcHeight / spriteAtlasHeight;
++spriteNdx;
}
const sprites = [
{msg: 'A', x: 0, y: 0, w: 64, h: 32, bg: 'red', fg: 'yellow'},
{msg: 'B', x: 64, y: 0, w: 64, h: 32, bg: 'blue', fg: 'white' },
{msg: 'C', x: 0, y: 32, w: 40, h: 32, bg: 'green', fg: 'pink' },
{msg: 'D', x: 40, y: 32, w: 48, h: 32, bg: 'purple', fg: 'cyan' },
{msg: 'F', x: 88, y: 32, w: 40, h: 32, bg: 'black', fg: 'red' },
];
// make 5 sprites in an atlas
const spriteAtlasWidth = 128;
const spriteAtlasHeight = 64;
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = spriteAtlasWidth;
ctx.canvas.height = spriteAtlasHeight;
for (const spr of sprites) {
ctx.fillStyle = spr.bg;
ctx.fillRect(spr.x, spr.y, spr.w, spr.h);
ctx.strokeStyle = spr.fg;
ctx.strokeRect(spr.x + .5, spr.y + .5, spr.w - 1, spr.h - 1);
ctx.fillStyle = spr.fg;
ctx.font = 'bold 26px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(spr.msg, spr.x + spr.w / 2, spr.y + spr.h / 2);
}
// show the atlas
document.body.appendChild(ctx.canvas);
// copy the atlas to a texture
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, ctx.canvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
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);
function render(time) {
time *= 0.001; // convert to seconds
spriteNdx = 0;
const numSprites = 10;
for (let i = 0; i < numSprites; ++i) {
const sp = sprites[i % sprites.length];
addSprite(
spriteAtlasWidth, spriteAtlasHeight,
sp.x, sp.y, sp.w, sp.h,
Math.sin(time + i * 15) * gl.canvas.width / 2 + gl.canvas.width / 2,
Math.cos(time + i * 17) * gl.canvas.height / 2 + gl.canvas.height / 2,
sp.w, sp.h,
);
}
// copy the latest sprite instance data
// to their respective buffers and setup
// the attributes.
// NOTE: for the attributes it would be better
// to use a vertex array
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.enableVertexAttribArray(uvLoc);
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, offsets);
gl.enableVertexAttribArray(offsetLoc);
gl.vertexAttribPointer(offsetLoc, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(offsetLoc, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, scaleBuffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, scales);
gl.enableVertexAttribArray(scaleLoc);
gl.vertexAttribPointer(scaleLoc, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(scaleLoc, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, uvOffsetBuffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, uvOffsets);
gl.enableVertexAttribArray(uvOffsetLoc);
gl.vertexAttribPointer(uvOffsetLoc, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(uvOffsetLoc, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, uvScaleBuffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, uvScales);
gl.enableVertexAttribArray(uvScaleLoc);
gl.vertexAttribPointer(uvScaleLoc, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(uvScaleLoc, 1);
gl.useProgram(program);
// pass in a projection matrix that
// converts to pixel space so the top
// left corner is 0,0 and the bottom right corner
// is canvas.width, canvas.height
//
// if you had a 3d math library this would be something like
// m4.ortho(0, gl.canvas.width, gl.canvas.height, 0, -1, 1);
gl.uniformMatrix4fv(matrixLoc, false, [
2 / gl.canvas.width, 0, 0, 0,
0, -2 / gl.canvas.height, 0, 0,
0, 0, 1, 0,
-1, 1, 0, 1,
]);
// note as there as only 1 texture and
// uniforms default to 0 we don't need to
// bind the texture to setup a uniform
// as the defaults happen to work.
ext.drawArraysInstancedANGLE(
gl.TRIANGLES,
0,
6, // verts per instance
spriteNdx, // num instances
);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas { border: 1px solid black; margin: 5px; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
note I'm not skipping instances but if you want to skip instances then you need to set the offset passed to gl.vertexAttribPointer for each instanced attribute. For example in the code above if you wanted to draw instances 7 to 29 it would be
const numInstancesToSkip = 7;
const numInstancesToDraw = 29 - 7 + 1;
const size = 2; // vec2
const sizeOfFloat = 4;
const offset = numInstancesToSkip * sizeOfFloat * size;
gl.bindBuffer(offsetBuffer);
gl.vertexAttribPointer(offsetLoc, size, gl.FLOAT, false, 0, offset);
gl.bindBuffer(scaleBuffer);
gl.vertexAttribPointer(scaleLoc, size, gl.FLOAT, false, 0, offset);
gl.bindBuffer(uvOffsetBuffer);
gl.vertexAttribPointer(uvOffsetLoc, size, gl.FLOAT, false, 0, offset);
gl.bindBuffer(uvScaleBuffer);
gl.vertexAttribPointer(uvScaleLoc, size, gl.FLOAT, false, 0, offset);
and to draw would be
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, mumInstancesToDraw);
note that offset above is the same for each attribute because all the atrributes are the same size (2) and the same type (gl.FLOAT) and they are all in separate buffers so their base offsets are all 0. If they were different sizes or different types or mixed into the same buffer they'd all require different math.

Related

what is the use and role of texSubImage2D?

After a lot of searching, I managed to get the texSubImage2D function to work. Simply what I haven't found is: what is this function for. In the example below I made a nice effect. In short, I know how to make it work but I am still completely unaware of the role of its parameters. And where to find these explanations?
I'm not looking for the syntax,
the example I give shows that I have (it seems to me) understood it well.
https://registry.khronos.org/webgl/specs/latest/1.0/#5.14.8
What I don't understand at all is the semantics...
Anyway, if someone could answer with examples so that I can understand.
"use strict";
let canvas = document.getElementById("canvas");
let gl = canvas.getContext("webgl");
gl.canvas.width = 30;
gl.canvas.height = 30;
let vertex = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 zeroToOne = a_position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_texCoord = a_texCoord;
}
`;
let fragment = `
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
gl_FragColor.rgb *= gl_FragColor.a;
}
`;
let shader = gl.createProgram();
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
gl.attachShader(shader, vertexShader);
gl.attachShader(shader, fragmentShader);
gl.linkProgram(shader);
let image_RGBA = new Image();
image_RGBA.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeBAMAAADJHrORAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAYUExURdUAAKPTdgCN09Aq0w4A09PS0dOoXwD//56WZMcAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAA8SURBVCjPYyAIBJEBNr4SAmDnG8MALr4LBODmh4IAPn5aWhp+fjkBPgH9BOwn4H4C/icQfgTCHx9gYAAArEg8b+0tf+EAAAAASUVORK5CYII=";
image_RGBA.onload = function() {
go(image_RGBA);
};
function go(image) {
let width = image.width;
let height = image.height;
let positionLocation = gl.getAttribLocation(shader, "a_position");
let texcoordLocation = gl.getAttribLocation(shader, "a_texCoord");
let positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
width, 0,
0, height,
0, height,
width, 0,
width, height
]), gl.STATIC_DRAW);
let texcoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
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.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const canvas2D = document.getElementById('canvas2D');
canvas2D.width = 30;
canvas2D.height = 30;
const ctx = canvas2D.getContext('2d');
ctx.drawImage(image, 0, 0);
var imgData = ctx.getImageData(0, 0, width, height).data;
var ArrayBufferView = new Uint8Array(imgData.buffer);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
30,
30,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
ArrayBufferView
);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0,
0,
29,
29,
gl.RGBA,
gl.UNSIGNED_BYTE,
ArrayBufferView
);
let resolutionLocation = gl.getUniformLocation(shader, "u_resolution");
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(100 / 255, 200 / 255, 150 / 255, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(shader);
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
let size = 2;
let type = gl.FLOAT;
let normalize = false;
let stride = 0;
let offset = 0;
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset);
gl.enableVertexAttribArray(texcoordLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
size = 2;
type = gl.FLOAT;
normalize = false;
stride = 0;
offset = 0;
gl.vertexAttribPointer(texcoordLocation, size, type, normalize, stride, offset);
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
#canvas {
width: 150px;
height: 150px;
image-rendering: pixelated;
}
#canvas2D {
width: 150px;
height: 150px;
image-rendering: pixelated;
}
<canvas id="canvas2D"></canvas><canvas id="canvas"></canvas>

WebGL mix instanced sprite rendering

I want to render some instanced sprites using drawArraysInstancedANGLE. The problem is that when i set vertexAttribDivisorANGLE for the buffer to be instanced, it clears the whole screen, erasing anything i drawn earlier. I based this example on https://stackoverflow.com/a/56066386/1227852
Example with the vertexAttribDivisorANGLE issue (only draws the instanced boxes):
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
alert('need ANGLE_instanced_arrays');
}
// create a simple background shader
const backgroundVs = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const backgroundFs = `
precision mediump float;
uniform vec2 resolution;
void main() {
vec2 uv = gl_FragCoord.xy/resolution.xy;
vec3 color = uv.xyx;
gl_FragColor = vec4(color, 1.0);
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
gl_FragColor = vec4(color,1.0);
}
`;
const backgroundProgramInfo = twgl.createProgram(gl, [backgroundVs, backgroundFs]);
const backgroundProgram = twgl.createProgram(gl, [backgroundVs, backgroundFs]);
const backgroundPositionLoc = gl.getAttribLocation(backgroundProgram, 'position');
const backgroundResolutionLoc = gl.getUniformLocation(backgroundProgram, 'resolution');
const backgroundQuad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, backgroundQuad);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1, 1, // 1 +-----+ 2
-1, 1, // | |
1, -1, // | |
1, -1, // | |
-1, 1, // | |
-1, -1, // 3 +-----+ 0
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(backgroundPositionLoc);
gl.vertexAttribPointer(backgroundPositionLoc, 2, gl.FLOAT, false, 0, 0);
//Create an instanced point renderer (based on: )
const vs = `
attribute vec4 position; // center point
attribute vec2 cornerPosition; // the corners (-0.5 to 0.5)
uniform vec2 resolution;
uniform mat4 matrix;
varying vec3 pointCoord; // only if you need gl_PointCoord substitute
void main() {
// do the normal thing (can mult by matrix or whatever here
gl_Position = matrix * position;
float pointSize = 20.0 / gl_Position.w;
// -- point emulation
gl_Position.xy += cornerPosition * (pointSize * 2.0 - 1.0) /
resolution * gl_Position.w;
// only if you need gl_PointCoord substitute
pointCoord = vec3(cornerPosition * 0.5, gl_Position.z);
}
`;
const fs = `
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
`;
const programInfo = twgl.createProgram(gl, [vs, fs]);
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const cornerPositionLoc = gl.getAttribLocation(program, 'cornerPosition');
const resolutionLoc = gl.getUniformLocation(program, 'resolution');
const matrixLoc = gl.getUniformLocation(program, 'matrix');
const bufSprites = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufSprites);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.001, -1.001,
1.001, -1.001,
-1.001, 1.001,
1.001, 1.001,
0, 0,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
const bufCorners = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufCorners);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, 0.5,
0.5, -0.5,
0.5, 0.5,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(cornerPositionLoc);
gl.vertexAttribPointer(cornerPositionLoc, 2, gl.FLOAT, false, 0, 0);
function render(ms) {
const secs = ms * 0.001;
gl.useProgram(backgroundProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, backgroundQuad);
gl.enableVertexAttribArray(backgroundPositionLoc);
gl.vertexAttribPointer(backgroundPositionLoc, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(backgroundResolutionLoc, gl.canvas.width, gl.canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 6);
const mat = m4.perspective(
60 * Math.PI / 180,
gl.canvas.clientWidth / gl.canvas.clientHeight,
0.1,
100);
m4.translate(mat, [0, 0, -2.11 + Math.sin(secs)], mat);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, bufSprites);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(positionLoc, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, bufCorners);
gl.enableVertexAttribArray(cornerPositionLoc);
gl.vertexAttribPointer(cornerPositionLoc, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(resolutionLoc, gl.canvas.width, gl.canvas.height);
gl.uniformMatrix4fv(matrixLoc, false, mat);
// 6 verts per point
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, 5);
requestAnimationFrame(render);
}
render(0);
<html>
<head>
<style>canvas { border: 1px solid black; width: 100%; height: 100%;}</style>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
</head>
<body>
<canvas></canvas>
</body>
</html>
Now when i disable the ext.vertexAttribDivisorANGLE(positionLoc, 1); call, the background renders like expected and the boxes are drawn over the background, but obviously the mesh is incorrect since the instancing / divisor is not set correctly:
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
alert('need ANGLE_instanced_arrays');
}
// create a simple background shader
const backgroundVs = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const backgroundFs = `
precision mediump float;
uniform vec2 resolution;
void main() {
vec2 uv = gl_FragCoord.xy/resolution.xy;
vec3 color = uv.xyx;
gl_FragColor = vec4(color, 1.0);
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
gl_FragColor = vec4(color,1.0);
}
`;
const backgroundProgramInfo = twgl.createProgram(gl, [backgroundVs, backgroundFs]);
const backgroundProgram = twgl.createProgram(gl, [backgroundVs, backgroundFs]);
const backgroundPositionLoc = gl.getAttribLocation(backgroundProgram, 'position');
const backgroundResolutionLoc = gl.getUniformLocation(backgroundProgram, 'resolution');
const backgroundQuad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, backgroundQuad);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1, 1, // 1 +-----+ 2
-1, 1, // | |
1, -1, // | |
1, -1, // | |
-1, 1, // | |
-1, -1, // 3 +-----+ 0
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(backgroundPositionLoc);
gl.vertexAttribPointer(backgroundPositionLoc, 2, gl.FLOAT, false, 0, 0);
//Create an instanced point renderer (based on: )
const vs = `
attribute vec4 position; // center point
attribute vec2 cornerPosition; // the corners (-0.5 to 0.5)
uniform vec2 resolution;
uniform mat4 matrix;
varying vec3 pointCoord; // only if you need gl_PointCoord substitute
void main() {
// do the normal thing (can mult by matrix or whatever here
gl_Position = matrix * position;
float pointSize = 20.0 / gl_Position.w;
// -- point emulation
gl_Position.xy += cornerPosition * (pointSize * 2.0 - 1.0) /
resolution * gl_Position.w;
// only if you need gl_PointCoord substitute
pointCoord = vec3(cornerPosition * 0.5, gl_Position.z);
}
`;
const fs = `
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
`;
const programInfo = twgl.createProgram(gl, [vs, fs]);
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const cornerPositionLoc = gl.getAttribLocation(program, 'cornerPosition');
const resolutionLoc = gl.getUniformLocation(program, 'resolution');
const matrixLoc = gl.getUniformLocation(program, 'matrix');
const bufSprites = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufSprites);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.001, -1.001,
1.001, -1.001,
-1.001, 1.001,
1.001, 1.001,
0, 0,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
const bufCorners = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufCorners);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, 0.5,
0.5, -0.5,
0.5, 0.5,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(cornerPositionLoc);
gl.vertexAttribPointer(cornerPositionLoc, 2, gl.FLOAT, false, 0, 0);
function render(ms) {
const secs = ms * 0.001;
gl.useProgram(backgroundProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, backgroundQuad);
gl.enableVertexAttribArray(backgroundPositionLoc);
gl.vertexAttribPointer(backgroundPositionLoc, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(backgroundResolutionLoc, gl.canvas.width, gl.canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 6);
const mat = m4.perspective(
60 * Math.PI / 180,
gl.canvas.clientWidth / gl.canvas.clientHeight,
0.1,
100);
m4.translate(mat, [0, 0, -2.11 + Math.sin(secs)], mat);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, bufSprites);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
//ext.vertexAttribDivisorANGLE(positionLoc, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, bufCorners);
gl.enableVertexAttribArray(cornerPositionLoc);
gl.vertexAttribPointer(cornerPositionLoc, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(resolutionLoc, gl.canvas.width, gl.canvas.height);
gl.uniformMatrix4fv(matrixLoc, false, mat);
// 6 verts per point
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, 5);
requestAnimationFrame(render);
}
render(0);
<html>
<head>
<style>canvas { border: 1px solid black; width: 100%; height: 100%;}</style>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
</head>
<body>
<canvas></canvas>
</body>
</html>
How can i render the instanced boxes correctly over the background without the background being erased?
edit: removed some unnecessary commented lines
edit 2: as pointed out by user253751, i needed to turn off the divisor using ext.vertexAttribDivisorANGLE(positionLoc, 0); right after the drawArraysInstancedANGLE call
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const ext = gl.getExtension('ANGLE_instanced_arrays');
if (!ext) {
alert('need ANGLE_instanced_arrays');
}
// create a simple background shader
const backgroundVs = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const backgroundFs = `
precision mediump float;
uniform vec2 resolution;
void main() {
vec2 uv = gl_FragCoord.xy/resolution.xy;
vec3 color = uv.xyx;
gl_FragColor = vec4(color, 1.0);
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
gl_FragColor = vec4(color,1.0);
}
`;
const backgroundProgramInfo = twgl.createProgram(gl, [backgroundVs, backgroundFs]);
const backgroundProgram = twgl.createProgram(gl, [backgroundVs, backgroundFs]);
const backgroundPositionLoc = gl.getAttribLocation(backgroundProgram, 'position');
const backgroundResolutionLoc = gl.getUniformLocation(backgroundProgram, 'resolution');
const backgroundQuad = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, backgroundQuad);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1, 1, // 1 +-----+ 2
-1, 1, // | |
1, -1, // | |
1, -1, // | |
-1, 1, // | |
-1, -1, // 3 +-----+ 0
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(backgroundPositionLoc);
gl.vertexAttribPointer(backgroundPositionLoc, 2, gl.FLOAT, false, 0, 0);
//Create an instanced point renderer (based on: )
const vs = `
attribute vec4 position; // center point
attribute vec2 cornerPosition; // the corners (-0.5 to 0.5)
uniform vec2 resolution;
uniform mat4 matrix;
varying vec3 pointCoord; // only if you need gl_PointCoord substitute
void main() {
// do the normal thing (can mult by matrix or whatever here
gl_Position = matrix * position;
float pointSize = 20.0 / gl_Position.w;
// -- point emulation
gl_Position.xy += cornerPosition * (pointSize * 2.0 - 1.0) /
resolution * gl_Position.w;
// only if you need gl_PointCoord substitute
pointCoord = vec3(cornerPosition * 0.5, gl_Position.z);
}
`;
const fs = `
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
`;
const programInfo = twgl.createProgram(gl, [vs, fs]);
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const cornerPositionLoc = gl.getAttribLocation(program, 'cornerPosition');
const resolutionLoc = gl.getUniformLocation(program, 'resolution');
const matrixLoc = gl.getUniformLocation(program, 'matrix');
const bufSprites = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufSprites);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.001, -1.001,
1.001, -1.001,
-1.001, 1.001,
1.001, 1.001,
0, 0,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
const bufCorners = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufCorners);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, 0.5,
0.5, -0.5,
0.5, 0.5,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(cornerPositionLoc);
gl.vertexAttribPointer(cornerPositionLoc, 2, gl.FLOAT, false, 0, 0);
function render(ms) {
const secs = ms * 0.001;
gl.useProgram(backgroundProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, backgroundQuad);
gl.enableVertexAttribArray(backgroundPositionLoc);
gl.vertexAttribPointer(backgroundPositionLoc, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(backgroundResolutionLoc, gl.canvas.width, gl.canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 6);
const mat = m4.perspective(
60 * Math.PI / 180,
gl.canvas.clientWidth / gl.canvas.clientHeight,
0.1,
100);
m4.translate(mat, [0, 0, -2.11 + Math.sin(secs)], mat);
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, bufSprites);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(positionLoc, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, bufCorners);
gl.enableVertexAttribArray(cornerPositionLoc);
gl.vertexAttribPointer(cornerPositionLoc, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(resolutionLoc, gl.canvas.width, gl.canvas.height);
gl.uniformMatrix4fv(matrixLoc, false, mat);
// 6 verts per point
ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, 5);
ext.vertexAttribDivisorANGLE(positionLoc, 0);
requestAnimationFrame(render);
}
render(0);
<html>
<head>
<style>canvas { border: 1px solid black; width: 100%; height: 100%;}</style>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
</head>
<body>
<canvas></canvas>
</body>
</html>
You need to turn the divisor off by setting it to 0 before drawing the background. Otherwise, it is used for the background as well, and the background is drawn incorrectly.

WebGL FBO (real-time drawing)

I'm trying to translate this example from Three.js - https://codepen.io/tutsplus/pen/PZmpEM
to pure WebGL. I experimented a lot with the code, I think there is an error in texture baking, but my attempts are unsuccessful, if not difficult, please!
WebGL example
let a_Position, u_Mouse, u_Sampler, u_Resolution;
const position = {
screenRect: null,
xyz: [0.0, 0.0, 0.0],
mouseDown: false,
};
function main() {
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const program = webglUtils.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
gl.useProgram(program);
const tick = function() {
render(gl, canvas, fbo, plane);
window.requestAnimationFrame(tick, canvas);
};
a_Position = gl.getAttribLocation(program, 'a_position');
u_Mouse = gl.getUniformLocation(program, 'u_mouse');
u_Resolution = gl.getUniformLocation(program, 'u_resolution');
u_Sampler = gl.getUniformLocation(program, 'u_sampler');
const fbo = [initFramebufferObject(gl), initFramebufferObject(gl)];
const plane = initVertexBuffersForPlane(gl);
tick();
}
let src = 0, dst = 1, t;
function render(gl, canvas, fbo, plane) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[dst]);
gl.viewport(0, 0, 1, 1);
drawTexture(gl, gl.program, plane, fbo[src].texture);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
drawTexture(gl, gl.program, plane, fbo[dst].texture);
t = src;
src = dst;
dst = t;
}
function drawTexture(gl, program, o, texture) {
gl.uniform3f(u_Mouse, ...position.xyz);
gl.uniform2f(u_Resolution, canvas.width, canvas.height);
initAttributeVariable(gl, a_Position, o.vertexBuffer);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0);
}
function initAttributeVariable(gl, a_attribute, buffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}
function initFramebufferObject(gl) {
const framebuffer = gl.createFramebuffer(), texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
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);
framebuffer.texture = texture;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
return framebuffer;
}
function initVertexBuffersForPlane(gl) {
const vertices = new Float32Array([1.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0,-1.0, 0.0, 1.0,-1.0, 0.0]);
const texCoords = new Float32Array([1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0]);
const indices = new Uint8Array([0, 1, 2, 0, 2, 3]);
const o = {};
o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
o.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
o.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return o;
}
function initArrayBufferForLaterUse(gl, data, num, type) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.num = num;
buffer.type = type;
return buffer;
}
function initElementArrayBufferForLaterUse(gl, data, type) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.type = type;
return buffer;
}
function mouseHandlers() {
function getPosition(e) {
const x = e.clientX, y = window.innerHeight - e.clientY, z = 0.05;
position.xyz = [x, y, z];
}
function getRect() {
position.screnRect = canvas.getBoundingClientRect();
}
function mouseDown(e) {
position.mouseDown = true;
getPosition(e);
}
function move(e) {
if (position.mouseDown) getPosition(e);
else return;
}
function up() {
position.mouseDown = false;
}
getRect();
canvas.addEventListener('mousedown', mouseDown);
canvas.addEventListener('mousemove', move);
canvas.addEventListener('mouseup', up);
}
mouseHandlers();
main();
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="canvas"></canvas>
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
</script>
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_sampler;
uniform vec2 u_resolution;
uniform vec3 u_mouse;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = texture2D(u_sampler, uv);
float dist = distance(u_mouse.xy, gl_FragCoord.xy);
gl_FragColor.rgb += u_mouse.z * max(15.0-dist,0.0);
//gl_FragColor.gb += 0.01; /* testing FBO */
}
</script>
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
So I have a result after moving the mouse but something is wrong:
It should be:
There is an obvious mistake when you create the texture objects for the frambuffer.
If you do not generate mipmaps (by gl.generateMipmap), then it is important to set gl.TEXTURE_MIN_FILTER. Since the default filter is gl.NEAREST_MIPMAP_LINEAR the texture would be mipmap incomplete, if you don't change the minifying function to gl.NEAREST or gl.LINEAR:
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
See further OpenGL ES 2.0 Full Specification - 3.7.10 Texture Completeness.
I recommend to check the completeness of the framebuffer:
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
// [...]
}
The size of the frame buffer textures has to be a power of 2 (WebGL 1.0). Create framebuffers with a fixed size (e.g. 1024x1024):
framebuffer.size = [1024, 1024];
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ...framebuffer.size, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(tblack));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
framebuffer.texture = texture;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
alert("incomplete frambuffer");
}
Ensure that the uniforms are set correctly. Set the resolution (u_resolution) dependent on the size of the framebuffer. The position of the mouse (u_mouse) has to be relative to the size of the framebuffer:
function drawTexture(gl, program, o, texture, resolution) {
const mx = position.xyz[0] * resolution[0] / canvas.width;
const my = position.xyz[1] * resolution[1] / canvas.height;
gl.uniform3f(u_Mouse, mx, my, position.xyz[2]);
gl.uniform2f(u_Resolution, resolution[0], resolution[1]);
initAttributeVariable(gl, a_Position, o.vertexBuffer);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0);
}
Set the viewport rectangle when you switch the current frame buffer
let src = 0, dst = 1, t;
function render(gl, canvas, fbo, plane) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[dst]);
gl.viewport(0, 0, ...fbo[dst].size);
drawTexture(gl, gl.program, plane, fbo[src].texture, fbo[src].size);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
drawTexture(gl, gl.program, plane, fbo[dst].texture, [canvas.width, canvas.height]);
t = src;
src = dst;
dst = t;
}
See the example, where the computation of the distance to the mouse is scaled, by the ration of the canvas resoultuion and the framebuffer:
let a_Position, u_Mouse, u_Sampler;
const position = {
screenRect: null,
xyz: [0.0, 0.0, 0.0],
mouseDown: false,
};
function main() {
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const program = webglUtils.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
gl.useProgram(program);
const tick = function() {
render(gl, canvas, fbo, plane);
window.requestAnimationFrame(tick, canvas);
};
a_Position = gl.getAttribLocation(program, 'a_position');
u_Mouse = gl.getUniformLocation(program, 'u_mouse');
u_Sampler = gl.getUniformLocation(program, 'u_sampler');
u_Resolution = gl.getUniformLocation(program, 'u_resolution');
u_CanvasSize = gl.getUniformLocation(program, 'u_canvasSize');
const fbo = [initFramebufferObject(gl), initFramebufferObject(gl)];
const plane = initVertexBuffersForPlane(gl);
tick();
}
let src = 0, dst = 1, t;
function render(gl, canvas, fbo, plane) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo[dst]);
gl.viewport(0, 0, ...fbo[dst].size);
drawTexture(gl, gl.program, plane, fbo[src].texture, fbo[src].size);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
drawTexture(gl, gl.program, plane, fbo[dst].texture, [canvas.width, canvas.height]);
t = src;
src = dst;
dst = t;
}
function drawTexture(gl, program, o, texture, resolution) {
const mx = position.xyz[0] * resolution[0] / canvas.width;
const my = position.xyz[1] * resolution[1] / canvas.height;
gl.uniform3f(u_Mouse, mx, my, position.xyz[2]);
gl.uniform2f(u_Resolution, resolution[0], resolution[1]);
gl.uniform2f(u_CanvasSize, canvas.width, canvas.height);
initAttributeVariable(gl, a_Position, o.vertexBuffer);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
gl.drawElements(gl.TRIANGLES, o.numIndices, o.indexBuffer.type, 0);
}
function initAttributeVariable(gl, a_attribute, buffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}
function initFramebufferObject(gl) {
let framebuffer = gl.createFramebuffer(), texture = gl.createTexture();
framebuffer.size = [1024, 1024];
let tblack = []
for (let i= 0; i < framebuffer.size[0]*framebuffer.size[1]; i ++) tblack.push(0, 0, 0, 255);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ...framebuffer.size, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(tblack));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
framebuffer.texture = texture;
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
alert("incomplete frambuffer");
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
return framebuffer;
}
function initVertexBuffersForPlane(gl) {
const vertices = new Float32Array([1.0, 1.0, 0.0, -1.0, 1.0, 0.0, -1.0,-1.0, 0.0, 1.0,-1.0, 0.0]);
const texCoords = new Float32Array([1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0]);
const indices = new Uint8Array([0, 1, 2, 0, 2, 3]);
const o = {};
o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
o.indexBuffer = initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
o.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return o;
}
function initArrayBufferForLaterUse(gl, data, num, type) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.num = num;
buffer.type = type;
return buffer;
}
function initElementArrayBufferForLaterUse(gl, data, type) {
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.type = type;
return buffer;
}
function mouseHandlers() {
function getPosition(e) {
const x = e.clientX, y = window.innerHeight - e.clientY, z = 0.05;
position.xyz = [x, y, z];
}
function getRect() {
position.screnRect = canvas.getBoundingClientRect();
}
function mouseDown(e) {
position.mouseDown = true;
getPosition(e);
}
function move(e) {
if (position.mouseDown) getPosition(e);
else return;
}
function up() {
position.mouseDown = false;
}
getRect();
canvas.addEventListener('mousedown', mouseDown);
canvas.addEventListener('mousemove', move);
canvas.addEventListener('mouseup', up);
}
mouseHandlers();
main();
<style>
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
</style>
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
</script>
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_sampler;
uniform vec2 u_resolution;
uniform vec2 u_canvasSize;
uniform vec3 u_mouse;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec4 texColor = texture2D(u_sampler, uv);
vec2 scale = u_canvasSize / u_resolution;
float dist = distance(u_mouse.xy * scale, gl_FragCoord.xy * scale);
float intensity = u_mouse.z * max(15.0-dist,0.0);
gl_FragColor = texColor + vec4(vec3(intensity), 0.0);
}
</script>
<canvas id="canvas"></canvas>
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>

Can adding a WebGL readPixels call change the output of shaders?

Is it possible to create a sequence of WebGL commands that gives a different output if a spurious readPixels command (writing to an otherwise unused buffer) is inserted into the middle of the program?
Context
I've run into a situation where I'm applying a series of shaders in WebGL, and they sometimes compute the wrong thing. Isolating the bug has proved extremely difficult because it seems to depend on random details of the shaders and the presence of certain calls that should be redundant. As far as I can tell, I am not doing anything wrong or even particularly out of the ordinary. I suspect a GPU driver bug, with some sort of race condition since it is not consistent how many iterations it takes to catch a bad result, but I could be a lot more confident in that inference if I knew that behavior when a readPixels line was present should match the behavior when it is not present.
For reference, this is code that reproduces the bug on my desktop (Windows, NVidia GeForce RTX 2060, AMD Ryzen 7 2700X). It does not reproduce on other machines I own:
const gl = document.createElement('canvas').getContext('webgl');
const GL = WebGLRenderingContext;
{
gl.getExtension('OES_texture_float');
gl.getExtension('WEBGL_color_buffer_float');
let positionBuffer = gl.createBuffer();
let positions = new Float32Array([-1, +1, +1, +1, -1, -1, +1, -1]);
gl.bindBuffer(GL.ARRAY_BUFFER, positionBuffer);
gl.bufferData(GL.ARRAY_BUFFER, positions, GL.STATIC_DRAW);
let indexBuffer = gl.createBuffer();
let indices = new Uint16Array([0, 2, 1, 2, 3, 1]);
gl.bindBuffer(GL.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(GL.ELEMENT_ARRAY_BUFFER, indices, GL.STATIC_DRAW);
gl.viewport(0, 0, 4, 2);
}
function shader(fragmentShaderSource) {
let glVertexShader = gl.createShader(GL.VERTEX_SHADER);
gl.shaderSource(glVertexShader, `
precision highp float;
precision highp int;
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0, 1);
}`);
gl.compileShader(glVertexShader);
let glFragmentShader = gl.createShader(GL.FRAGMENT_SHADER);
gl.shaderSource(glFragmentShader, `
precision highp float;
precision highp int;
${fragmentShaderSource}`);
gl.compileShader(glFragmentShader);
let program = gl.createProgram();
gl.attachShader(program, glVertexShader);
gl.attachShader(program, glFragmentShader);
gl.linkProgram(program);
gl.deleteShader(glVertexShader);
gl.deleteShader(glFragmentShader);
return program;
}
function tex() {
let texture = gl.createTexture();
let framebuffer = gl.createFramebuffer();
gl.bindTexture(GL.TEXTURE_2D, texture);
gl.bindFramebuffer(GL.FRAMEBUFFER, framebuffer);
gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.NEAREST);
gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.NEAREST);
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.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, 4, 2, 0, GL.RGBA, GL.FLOAT, null);
gl.framebufferTexture2D(GL.FRAMEBUFFER, GL.COLOR_ATTACHMENT0, GL.TEXTURE_2D, texture, 0);
return {texture, framebuffer};
}
let shader_less_than = shader(`
uniform float lim;
void main() {
gl_FragColor = vec4(float(gl_FragCoord.y < lim), 0.0, 0.0, 0.0);
}`);
let shader_zero = shader(`
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
}`);
let shader_tricky = shader(`
uniform sampler2D tex_unused_dep;
uniform sampler2D tex_c;
void main() {
float c = texture2D(tex_c, gl_FragCoord.xy / vec2(4.0, 2.0)).x;
vec2 unused = texture2D(tex_unused_dep, vec2(0.0, c)).xy; // Without this line, test passes.
gl_FragColor = vec4(0.0, c, 0.0, 0.0);
}`);
let tex_unrelated = tex();
let tex_lim = tex();
let tex_unused_dep = tex();
let tex_out = tex();
let out_buf = new Float32Array(32);
for (let k = 0; k < 1000; k++) {
let flag = k % 2 === 0;
gl.useProgram(shader_zero);
gl.bindFramebuffer(GL.FRAMEBUFFER, tex_unused_dep.framebuffer);
gl.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
gl.useProgram(shader_less_than);
gl.uniform1f(gl.getUniformLocation(shader_less_than, 'lim'), flag ? 1 : 2);
gl.bindFramebuffer(GL.FRAMEBUFFER, tex_unrelated.framebuffer);
gl.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
gl.uniform1f(gl.getUniformLocation(shader_less_than, 'lim'), flag ? 1 : 2); // Commenting this line makes a pass more likely, but not guaranteed.
gl.bindFramebuffer(GL.FRAMEBUFFER, tex_lim.framebuffer);
gl.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
// gl.readPixels(0, 0, 4, 2, GL.RGBA, GL.FLOAT, out_buf); // Uncommenting this line seems to guarantee a pass.
gl.useProgram(shader_tricky);
gl.uniform1i(gl.getUniformLocation(shader_tricky, 'tex_unused_dep'), 0);
gl.uniform1i(gl.getUniformLocation(shader_tricky, 'tex_c'), 1);
gl.activeTexture(GL.TEXTURE0 + 0);
gl.bindTexture(GL.TEXTURE_2D, tex_unused_dep.texture);
gl.activeTexture(GL.TEXTURE0 + 1);
gl.bindTexture(GL.TEXTURE_2D, tex_lim.texture);
gl.enableVertexAttribArray(gl.getAttribLocation(shader_tricky, 'position'));
gl.vertexAttribPointer(gl.getAttribLocation(shader_tricky, 'position'), 2, GL.FLOAT, false, 0, 0);
gl.bindFramebuffer(GL.FRAMEBUFFER, tex_out.framebuffer);
gl.drawElements(GL.TRIANGLES, 6, GL.UNSIGNED_SHORT, 0);
gl.readPixels(0, 0, 4, 2, GL.RGBA, GL.FLOAT, out_buf);
if (out_buf[17] !== (flag ? 0 : 1)) {
throw new Error("Bad output.")
}
}
console.log("PASS");

How to properly check webgl output

You can find the original question below about LUMINANCE_ALPHA but I realized I was wrong about my problem.
The real question should have been :
How can we efficiently check the output value done on a canvas drawn using webgl ?
Is using the webgl canvas as an image to draw it in a 2D canvas and get the values using getImageData() a good idea ?
const webglCanvas = ...;
const offCanvas = document.createElement('canvas');
offCanvas.style.background = 'black';
offCanvas.width = canvas.width;
offCanvas.height = canvas.height;
const context = offCanvas.getContext('2d');
context.drawImage(webglCanvas, 0, 0);
console.log( context.getImageData(0, 0, canvas.width, canvas.height).data );
Original Question :
I don't understand how gl.LUMINANCE_ALPHA works, from my understand it's supposed to get bytes 2 by 2 and assign the first value to rgb and the second value to alpha.
However when I do that with webgl :
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE_ALPHA, 1, 1, 0, gl.LUMINANCE_ALPHA, gl.UNSIGNED_BYTE, new Uint8Array([1, 30]));
I'm getting a color of (8, 8, 8, 30) while I'm expecting (1, 1, 1, 30).
I got that definition from those specs :
Each element is an luminance/alpha double. The GL converts each component to floating point, clamps to the range [0,1], and assembles them into an RGBA element by placing the luminance value in the red, green and blue channels.
Not sure how this apply to webgl since there is no double. Maybe I'm missing what converts each component to floating point means or missing some pack/unpack configuration.
Here's a snippet replicating the issue:
const vertShaderStr = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
`;
const fragShaderStr = `
precision mediump float;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, vec2(0, 0));
}
`
var canvas = document.getElementById('canvas');
canvas.width = 1;
canvas.height = 1;
const gl = canvas.getContext('webgl');
const program = gl.createProgram();
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertShaderStr);
gl.compileShader(vertexShader);
if ( !gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) )
throw new Error('Vertex shader error', gl.getShaderInfoLog(vertexShader));
gl.attachShader(program, vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragShaderStr);
gl.compileShader(fragmentShader);
if ( !gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) )
throw new Error('Fragment shader error', gl.getShaderInfoLog(fragmentShader));
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if ( !gl.getProgramParameter(program, gl.LINK_STATUS) )
throw new Error(gl.getProgramInfoLog(program));
gl.useProgram(program);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1, 1,
-1, 1,
1, -1,
-1, -1
]), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
/*** Interresting part here ***/
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 2);
gl.pixelStorei(gl.PACK_ALIGNMENT, 2);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE_ALPHA, 1, 1, 0, gl.LUMINANCE_ALPHA, gl.UNSIGNED_BYTE,
new Uint8Array([1, 30]));
gl.activeTexture(gl.TEXTURE0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
const offCanvas = document.createElement('canvas');
offCanvas.style.background = 'black';
offCanvas.width = canvas.width;
offCanvas.height = canvas.height;
const context = offCanvas.getContext('2d');
context.drawImage(canvas, 0, 0);
console.log( context.getImageData(0, 0, canvas.width, canvas.height).data );
<canvas id="canvas"></canvas>
update
Just found out that the alpha value (30) will affect the resulting rgb. But I can't find out what's doing exactly, if it's using alpha to compute rgb or if it's reading the wrong bytes from the buffer.
When drawing a webgl canvas to another 2d canvas conversion, filtering and blending operations are being applied which may lead to a skewed result. While you can disable blending by setting the globalCompositeOperation on the 2d context to copy you're still running through a conversion and filtering process which is not standardized and is not guaranteed to provide a precise result.
Using readPixels returns correct results and is the only way to get guaranteed accurate readings from the current color framebuffer. If you need that data to be available to a 2D context you may use ImageData in conjunction with putImageData.
const vertShaderStr = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
`;
const fragShaderStr = `
precision mediump float;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, vec2(0, 0));
}
`
var canvas = document.getElementById('canvas');
canvas.width = 1;
canvas.height = 1;
const gl = canvas.getContext('webgl');
const program = gl.createProgram();
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertShaderStr);
gl.compileShader(vertexShader);
if ( !gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) )
throw new Error('Vertex shader error', gl.getShaderInfoLog(vertexShader));
gl.attachShader(program, vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragShaderStr);
gl.compileShader(fragmentShader);
if ( !gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) )
throw new Error('Fragment shader error', gl.getShaderInfoLog(fragmentShader));
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if ( !gl.getProgramParameter(program, gl.LINK_STATUS) )
throw new Error(gl.getProgramInfoLog(program));
gl.useProgram(program);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1, 1,
-1, 1,
1, -1,
-1, -1
]), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
/*** Interresting part here ***/
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 2);
gl.pixelStorei(gl.PACK_ALIGNMENT, 2);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE_ALPHA, 1, 1, 0, gl.LUMINANCE_ALPHA, gl.UNSIGNED_BYTE,
new Uint8Array([1, 30]));
gl.activeTexture(gl.TEXTURE0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
var readback = new Uint8Array(4);
gl.readPixels(0,0,1,1,gl.RGBA,gl.UNSIGNED_BYTE,readback);
const offCanvas = document.createElement('canvas');
offCanvas.style.background = 'black';
offCanvas.width = canvas.width;
offCanvas.height = canvas.height;
const context = offCanvas.getContext('2d');
context.globalCompositeOperation = 'copy';
context.drawImage(canvas, 0, 0,1,1);
console.log("Canvas",context.getImageData(0, 0, canvas.width, canvas.height).data);
console.log("readPixels", readback );
<canvas id="canvas"></canvas>

Resources