How to use packed pixel array in WebGL Texture? [duplicate] - webgl
Currently, I'm using 2D canvas context to draw an image generated (from pixel to pixel, but refreshed as a whole buffer in once after a generated frame) from JavaScript at about a 25fps rate. The generated image is always one byte (integer / typed array) per pixel and a fixed palette is used to generate RGB final result. Scaling is also needed to adopt to the size of the canvas (ie: going to fullscreen) and/or at user request (zoom in/out buttons).
The 2D context of canvas is OK for this purpose, however I'm curious if WebGL can provide better result and/or better performance. Please note: I don't want to put pixels via webGL, I want to put pixels into my buffer (which is basically Uint8Array), and use that buffer (in once) to refresh the context. I don't know too much about WebGL, but using the needed generated image as some kind of texture would work somehow for example? Then I would need to refresh the texture at about 25fps rate, I guess.
It would be really fantastic, if WebGL support the colour space conversion somehow. With 2D context, I need to convert 1 byte / pixel buffer into RGBA for the imagedata in JavaScript for every pixel ... Scaling (for 2D context) is done now by altering the height/width style of the canvas, so browsers scales the image then. However I guess it can be slower than what WebGL can do with hw support, and also (I hope) WebGL can give greater flexibility to control the scaling, eg with the 2D context, browsers will do antialiasing even if I don't want to do (eg: integer zooming factor), and maybe that's a reason it can be quite slow sometimes.
I've already tried to learn several WebGL tutorials but all of them starts with objects, shapes, 3D cubes, etc, I don't need any - classical - object to render only what 2D context can do as well - in the hope that WebGL can be a faster solution for the very same task! Of course if there is no win here with WebGL at all, I would continue to use 2D context.
To be clear: this is some kind of computer hardware emulator done in JavaScript, and its output (what would be seen on a PAL TV connected to it) is rendered via a canvas context. The machine has fixed palette with 256 elements, internally it only needs one byte for a pixel to define its colour.
You can use a texture as your palette and a different texture as your image. You then get a value from the image texture and use it too look up a color from the palette texture.
The palette texture is 256x1 RGBA pixels. Your image texture is any size you want but just a single channel ALPHA texture. You can then look up a value from the image
float index = texture2D(u_image, v_texcoord).a * 255.0;
And use that value to look up a color in the palette
gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
Your shaders might be something like this
Vertex Shader
attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
// assuming a unit quad for position we
// can just use that for texcoords. Flip Y though so we get the top at 0
v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
}
Fragment shader
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
void main() {
float index = texture2D(u_image, v_texcoord).a * 255.0;
gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
Then you just need a palette texture.
// Setup a palette.
var palette = new Uint8Array(256 * 4);
// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
palette[index * 4 + 0] = r;
palette[index * 4 + 1] = g;
palette[index * 4 + 2] = b;
palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue
// upload palette
...
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA,
gl.UNSIGNED_BYTE, palette);
And your image. It's an alpha only image so just 1 channel.
// Make image. Just going to make something 8x8
var image = new Uint8Array([
0,0,1,1,1,1,0,0,
0,1,0,0,0,0,1,0,
1,0,0,0,0,0,0,1,
1,0,2,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,0,3,3,3,3,0,1,
0,1,0,0,0,0,1,0,
0,0,1,1,1,1,0,0,
]);
// upload image
....
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, 8, 8, 0, gl.ALPHA,
gl.UNSIGNED_BYTE, image);
You also need to make sure both textures are using gl.NEAREST for filtering since one represents indices and the other a palette and filtering between values in those cases makes no sense.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
Here's a working example:
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = twgl.createProgramFromScripts(
gl,
["vshader", "fshader"],
["a_position", "a_textureIndex"]);
gl.useProgram(program);
var imageLoc = gl.getUniformLocation(program, "u_image");
var paletteLoc = gl.getUniformLocation(program, "u_palette");
// tell it to use texture units 0 and 1 for the image and palette
gl.uniform1i(imageLoc, 0);
gl.uniform1i(paletteLoc, 1);
// Setup a unit quad
var positions = [
1, 1,
-1, 1,
-1, -1,
1, 1,
-1, -1,
1, -1,
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
// Setup a palette.
var palette = new Uint8Array(256 * 4);
// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
palette[index * 4 + 0] = r;
palette[index * 4 + 1] = g;
palette[index * 4 + 2] = b;
palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue
// make palette texture and upload palette
gl.activeTexture(gl.TEXTURE1);
var paletteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTex);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, palette);
// Make image. Just going to make something 8x8
var image = new Uint8Array([
0,0,1,1,1,1,0,0,
0,1,0,0,0,0,1,0,
1,0,0,0,0,0,0,1,
1,0,2,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,0,3,3,3,3,0,1,
0,1,0,0,0,0,1,0,
0,0,1,1,1,1,0,0,
]);
// make image textures and upload image
gl.activeTexture(gl.TEXTURE0);
var imageTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTex);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, 8, 8, 0, gl.ALPHA, gl.UNSIGNED_BYTE, image);
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl.min.js"></script>
<script id="vshader" type="whatever">
attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
// assuming a unit quad for position we
// can just use that for texcoords. Flip Y though so we get the top at 0
v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
}
</script>
<script id="fshader" type="whatever">
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
void main() {
float index = texture2D(u_image, v_texcoord).a * 255.0;
gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
</script>
<canvas id="c" width="256" height="256"></canvas>
To animate just update the image and then re-upload it into the texture
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, 8, 8, 0, gl.ALPHA,
gl.UNSIGNED_BYTE, image);
Example:
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = twgl.createProgramFromScripts(
gl,
["vshader", "fshader"],
["a_position", "a_textureIndex"]);
gl.useProgram(program);
var imageLoc = gl.getUniformLocation(program, "u_image");
var paletteLoc = gl.getUniformLocation(program, "u_palette");
// tell it to use texture units 0 and 1 for the image and palette
gl.uniform1i(imageLoc, 0);
gl.uniform1i(paletteLoc, 1);
// Setup a unit quad
var positions = [
1, 1,
-1, 1,
-1, -1,
1, 1,
-1, -1,
1, -1,
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
// Setup a palette.
var palette = new Uint8Array(256 * 4);
// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
palette[index * 4 + 0] = r;
palette[index * 4 + 1] = g;
palette[index * 4 + 2] = b;
palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue
// make palette texture and upload palette
gl.activeTexture(gl.TEXTURE1);
var paletteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTex);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, palette);
// Make image. Just going to make something 8x8
var width = 8;
var height = 8;
var image = new Uint8Array([
0,0,1,1,1,1,0,0,
0,1,0,0,0,0,1,0,
1,0,0,0,0,0,0,1,
1,0,2,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,0,3,3,3,3,0,1,
0,1,0,0,0,0,1,0,
0,0,1,1,1,1,0,0,
]);
// make image textures and upload image
gl.activeTexture(gl.TEXTURE0);
var imageTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTex);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, image);
var frameCounter = 0;
function render() {
++frameCounter;
// skip 3 of 4 frames so the animation is not too fast
if ((frameCounter & 3) == 0) {
// rotate the image left
for (var y = 0; y < height; ++y) {
var temp = image[y * width];
for (var x = 0; x < width - 1; ++x) {
image[y * width + x] = image[y * width + x + 1];
}
image[y * width + width - 1] = temp;
}
// re-upload image
gl.activeTexture(gl.TEXTURE0);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA,
gl.UNSIGNED_BYTE, image);
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
}
requestAnimationFrame(render);
}
render();
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl.min.js"></script>
<script id="vshader" type="whatever">
attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
// assuming a unit quad for position we
// can just use that for texcoords. Flip Y though so we get the top at 0
v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
}
</script>
<script id="fshader" type="whatever">
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
void main() {
float index = texture2D(u_image, v_texcoord).a * 255.0;
gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
</script>
<canvas id="c" width="256" height="256"></canvas>
Of course that assumes your goal is to do the animation on the CPU by manipulating pixels. Otherwise you can use any normal webgl techniques to manipulate texture coordinates or whatever.
You can also update the palette similarly for palette animation. Just modify the palette and re-upload it
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA,
gl.UNSIGNED_BYTE, palette);
Example:
var canvas = document.getElementById("c");
var gl = canvas.getContext("webgl");
// Note: createProgramFromScripts will call bindAttribLocation
// based on the index of the attibute names we pass to it.
var program = twgl.createProgramFromScripts(
gl,
["vshader", "fshader"],
["a_position", "a_textureIndex"]);
gl.useProgram(program);
var imageLoc = gl.getUniformLocation(program, "u_image");
var paletteLoc = gl.getUniformLocation(program, "u_palette");
// tell it to use texture units 0 and 1 for the image and palette
gl.uniform1i(imageLoc, 0);
gl.uniform1i(paletteLoc, 1);
// Setup a unit quad
var positions = [
1, 1,
-1, 1,
-1, -1,
1, 1,
-1, -1,
1, -1,
];
var vertBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
// Setup a palette.
var palette = new Uint8Array(256 * 4);
// I'm lazy so just setting 4 colors in palette
function setPalette(index, r, g, b, a) {
palette[index * 4 + 0] = r;
palette[index * 4 + 1] = g;
palette[index * 4 + 2] = b;
palette[index * 4 + 3] = a;
}
setPalette(1, 255, 0, 0, 255); // red
setPalette(2, 0, 255, 0, 255); // green
setPalette(3, 0, 0, 255, 255); // blue
// make palette texture and upload palette
gl.activeTexture(gl.TEXTURE1);
var paletteTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, paletteTex);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, palette);
// Make image. Just going to make something 8x8
var width = 8;
var height = 8;
var image = new Uint8Array([
0,0,1,1,1,1,0,0,
0,1,0,0,0,0,1,0,
1,0,0,0,0,0,0,1,
1,0,2,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,0,3,3,3,3,0,1,
0,1,0,0,0,0,1,0,
0,0,1,1,1,1,0,0,
]);
// make image textures and upload image
gl.activeTexture(gl.TEXTURE0);
var imageTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTex);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, image);
var frameCounter = 0;
function render() {
++frameCounter;
// skip 3 of 4 frames so the animation is not too fast
if ((frameCounter & 3) == 0) {
// rotate the 3 palette colors
var tempR = palette[4 + 0];
var tempG = palette[4 + 1];
var tempB = palette[4 + 2];
var tempA = palette[4 + 3];
setPalette(1, palette[2 * 4 + 0], palette[2 * 4 + 1], palette[2 * 4 + 2], palette[2 * 4 + 3]);
setPalette(2, palette[3 * 4 + 0], palette[3 * 4 + 1], palette[3 * 4 + 2], palette[3 * 4 + 3]);
setPalette(3, tempR, tempG, tempB, tempA);
// re-upload palette
gl.activeTexture(gl.TEXTURE1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA,
gl.UNSIGNED_BYTE, palette);
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
}
requestAnimationFrame(render);
}
render();
canvas { border: 1px solid black; }
<script src="https://twgljs.org/dist/twgl.min.js"></script>
<script id="vshader" type="whatever">
attribute vec4 a_position;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
// assuming a unit quad for position we
// can just use that for texcoords. Flip Y though so we get the top at 0
v_texcoord = a_position.xy * vec2(0.5, -0.5) + 0.5;
}
</script>
<script id="fshader" type="whatever">
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D u_image;
uniform sampler2D u_palette;
void main() {
float index = texture2D(u_image, v_texcoord).a * 255.0;
gl_FragColor = texture2D(u_palette, vec2((index + 0.5) / 256.0, 0.5));
}
</script>
<canvas id="c" width="256" height="256"></canvas>
Slightly related is this tile shader example
http://blog.tojicode.com/2012/07/sprite-tile-maps-on-gpu.html
Presumably you're building up a javascript array that's around 512 x 512 (PAL size)...
A WebGL fragment shader could definitely do your palette conversion pretty nicely. The recipe would go something like this:
Set up WebGL with a "geometry" of just two triangles that span your viewport. (GL is all triangles.) This is the biggest bother, if you're not already GL fluent. But it's not that bad. Spend some quality time with http://learningwebgl.com/blog/?page_id=1217 . But it will be ~100 lines of stuff. Price of admission.
Build your in-memory frame buffer 4 times bigger. (I think textures always have to be RGBA?) And populate every fourth byte, the R component, with your pixel values. Use new Float32Array to allocate it. You can use values 0-255, or divide it down to 0.0 to 1.0. We'll pass this to webgl as a texture. This one changes every frame.
Build a second texture that's 256 x 1 pixels, which is your palette lookup table. This one never changes (unless the palette can be modified?).
In your fragment shader, use your emulated frame buffer texture as a lookup into your palette. The first pixel in the palette is accessed at location (0.5/256.0, 0.5), middle of the pixel.
On each frame, resubmit the emulated frame buffer texture and redraw. Pushing pixels to the GPU is expensive... but a PAL-sized image is pretty small by modern standards.
Bonus step: You could enhance the fragment shader to imitate scanlines, interlace video, or other cute emulation artifacts (phosphor dots?) on modern high-resolution displays, all at no cost to your javascript!
This is just a sketch. But it will work. WebGL is a pretty low-level API, and quite flexible, but well worth the effort (if you like that kind of thing, which I do. :-) ).
Again, http://learningwebgl.com/blog/?page_id=1217 is well-recommended for overall WebGL guidance.
Related
How can I fix the display of this 16-bit RGBA PNG using WebGL2?
I am trying to work with 16-bit per channel RGBA data (and later RGB data) in WebGL2. I am having trouble properly displaying one of the reference images from PngSuite and I'd be eternally grateful if someone could take a look. I am loading a 3x16 bits rgb color + 16 bit alpha-channel PNG file using pngtoy.js or UPNG.js (both give the same values which I believe are correct). Here is what I am seeing: My WebGL2 code was based on gman's past answers which have been incredibly helpful. I don't know where to focus to investigate where I went wrong. I have spent an entire day looking at this so any advice or pointers where to look is greatly appreciated!!! https://jsfiddle.net/mortac8/yq2tfe97/13/ (apologies for the messy jsfiddle with inline resources at the top) // https://stackoverflow.com/a/57704283/1469613 function addWebgl(canvas, gl, img, w, h) { var program = gl.createProgram(); // texture var tex = gl.createTexture(); // create empty texture gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D( gl.TEXTURE_2D, // target 0, // mip level gl.RGBA16UI, // internal format -> gl.RGBA16UI w, h, // width and height 0, // border gl.RGBA_INTEGER, //format -> gm.RGBA_INTEGER gl.UNSIGNED_SHORT, // type -> gl.UNSIGNED_SHORT img // texture data ); // buffer var buffer = gl.createBuffer(); var bufferData = new Float32Array([ -1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1 ]); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, bufferData, gl.STATIC_DRAW); // shaders program.vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(program.vs, `#version 300 es in vec4 vertex; // incoming pixel input? out vec2 pixelCoordinate; // variable used to pass position to fragment shader void main(){ gl_Position = vertex; // set pixel output position to incoming position (pass through) pixelCoordinate = vertex.xy*0.5+0.5; // set coordinate for fragment shader pixelCoordinate.y = 1.0 - pixelCoordinate.y; //flip } `); program.fs = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(program.fs, `#version 300 es precision highp float; // ? uniform highp usampler2D tex; // ? in vec2 pixelCoordinate; // receive pixel position from vertex shader out vec4 fooColor; void main() { uvec4 unsignedIntValues = texture(tex, pixelCoordinate); vec4 floatValues0To65535 = vec4(unsignedIntValues); vec4 colorValues0To1 = floatValues0To65535 / 65535.0; fooColor = colorValues0To1; } `); gl.compileShader(program.vs); checkCompileError(program.vs); gl.compileShader(program.fs); checkCompileError(program.fs); function checkCompileError(s) { if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(s)); } } gl.attachShader(program,program.vs); gl.attachShader(program,program.fs); gl.deleteShader(program.vs); gl.deleteShader(program.fs); // program gl.bindAttribLocation(program, 0, "vertex"); gl.linkProgram(program); gl.useProgram(program); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, 6); // execute program }
Per default the webgl context uses premultiplied alpha, disabling it fixes your issue. var myCtx = myCv.getContext('webgl2', { premultipliedAlpha: false });
RENDER WARNING: texture bound to texture unit 0 is not renderable. It might be non-power-of-2 or have incompatible texture filtering (maybe)?
I'm trying to render the 2d image in WebGL with no animation. I'm getting the issue. Since i have passed the proper image data. Here is the code.What am i missing? Or how to render the text or image. Without matrix can't we render the image or text? When i say image or text it means it is purely 2d not 3d. const vsSource = ` attribute vec4 a_position; attribute vec2 a_texcoord; uniform mat4 u_matrix; varying vec2 v_texcoord; void main() { gl_Position = u_matrix * a_position; v_texcoord = a_texcoord; } `; // Fragment shader program const fsSource = ` precision mediump float; varying vec2 v_texcoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texcoord); } `; function loadTexture(gl, url) { var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); var textureInfo = { width: 1, // we don't know the size until it loads height: 1, texture: texture, }; var img = new Image(); img.src = url; //img.crossOrigin="*" img.addEventListener('load', function() { textureInfo.width = img.width; textureInfo.height = img.height; gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); 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); }); return textureInfo; } drawImage(texture); function drawImage(tex) { // gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.clear(gl.COLOR_BUFFER_BIT); gl.clearColor(1, 1, 0.0, 1.0); // Clear to black, fully opaque // gl.clearDepth(1.0); // Clear everything // gl.enable(gl.DEPTH_TEST); // Enable depth testing // gl.depthFunc(gl.LEQUAL); var positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); // Put a unit quad in the buffer var positions = [ 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); var texcoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); // Put texcoords in the buffer var texcoords = [ 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texcoords), gl.STATIC_DRAW); gl.bindTexture(gl.TEXTURE_2D, tex.texture); // Tell WebGL to use our shader program pair gl.useProgram(programInfo.program); // Setup the attributes to pull data from our buffers gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, 2, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord); gl.vertexAttribPointer(programInfo.attribLocations.textureCoord, 2, gl.FLOAT, false, 0, 0); gl.uniform1i(programInfo.uniformLocations.textureLocation, 0); > Here getting issue gl.drawArrays(gl.TRIANGLES, 0, 6); }
Your code never creates a texture. It never calls loadImage. If it did call loadImage the code doesn't draw after the image has loaded. I'd suggest you call loadImage and inside the load handler you call drawImage. I'd also suggest you initialize the texture with a single pixel so it's usable immediately if you happen to want to draw before the image has loaded. That is what this article does
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");
WebGL render to texture with framebuffer
I am new in webgl and trying to learn it. I am trying to learn framebuffer and rendering to texture but I am stuck. What I am trying to do is to copy the colors (pixel data) of a texture to another texture by using framebuffer. I have some other things in mind like doing some calculations before rendering to texture etc. but will do that afterwards. I have created two 2x2 textures, put some random colors in one of them and bind other to the framebuffer. But I am not getting expected output. Vertex shader precision mediump float; attribute vec2 a_texcoord; varying vec2 v_texcoord; void main() { // Convert texture coordinate to render coordinate gl_Position = vec4(2.0 * a_texcoord.x - 1.0, 1.0 - 2.0 * a_texcoord.y, 0, 1); gl_PointSize = 1.0; v_texcoord = a_texcoord; } Fragment shader precision mediump float; uniform sampler2D u_texture; varying vec2 v_texcoord; void main() { vec4 data = texture2D(u_texture, v_texcoord); gl_FragColor = data; } Javascript var canvas = document.getElementById("canvas"); // WebGL context var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (!gl) { console.log("WebGL not supported"); } // Set canvas dimensions canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Create program with shaders var program = glUtils.createProgram(gl, 'vshader', 'fshader'); // Texture dimensions var textureWidth = 2, textureHeight = 2; // Texture coordinates var coords = []; for (var i = 0; i < textureWidth; ++i) { for (var j = 0; j < textureHeight; ++j) { coords.push(i / textureWidth, j / textureHeight); } } // Random colors for texture var d = []; for (var i = 0; i < textureWidth * textureHeight; ++i) { d.push( Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256) ); } // Texture with random colors var data = new Uint8Array(d); var texture0 = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture0); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureWidth, textureHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); 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.bindTexture(gl.TEXTURE_2D, null); // Texture to render to var texture1 = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture1); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureWidth, textureHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 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.bindTexture(gl.TEXTURE_2D, null); // Framebuffer var fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture1, 0); // Program gl.useProgram(program); // Bind texture0 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture0); gl.uniform1i(program.uniforms.u_texture, 0); // Bind framebuffer gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture1, 0); // Set coordinate array sendCoords(program); // Set WebGL viewport setupViewport(gl, textureWidth, textureHeight); // Clear gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Draw gl.drawArrays(gl.POINTS, 0, textureWidth * textureHeight); // Read pixels var pixels = new Uint8Array(textureWidth * textureHeight * 4); gl.readPixels(0, 0, textureWidth, textureHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); gl.deleteFramebuffer(fb); console.log(pixels); // Enable and bind data to attribute for the texture coordinates function sendCoords(program) { var coordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, coordBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(coords), gl.STATIC_DRAW); gl.vertexAttribPointer( program.attribs.a_texcoord, 2, gl.FLOAT, gl.FALSE, 0, 0 ); gl.enableVertexAttribArray(program.attribs.a_texcoord); } // Viewport setup function setupViewport(gl, width, height) { gl.viewport(0, 0, width, height); } I am expecting exact same data that I have in texture0 but not getting that in gl.readPixels(). Maybe I am doing wrong calculations. Update: Here is sample data to make my problem clear. // Texture coordinates [0, 0, 0, 0.5, 0.5, 0, 0.5, 0.5] // Random colors to copy from [197, 103, 13, 0, 199, 17, 0, 18, 61, 177, 5, 14, 15, 72, 18, 10] // Data getting by gl.readPixels() [61, 177, 5, 14, 15, 72, 18, 10, 197, 103, 13, 0, 199, 17, 0, 18] As you can see, the order is not same. I would like to know why. Any help would be much appreciated. Thanks.
How to use the OES_texture_float extension? and create a texture as a floating point one for that?
How to use the OES_texture_float extension? and create a texture as a floating point one for that? As webGL 1 extension lists: var ext = gl.getExtension("OES_texture_float"); var linear = gl.getExtension("OES_texture_float_linear"); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_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); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.FLOAT, image); How to make a float-point image as a texture input?
First off you're not actually checking you got the extension Your code should be something like var ext = gl.getExtension("OES_texture_float"); if (!ext) { alert("this machine or browser does not support OES_texture_float"); } var linear = gl.getExtension("OES_texture_float_linear"); if (!linear) { alert("this machine or browser does not support OES_texture_float_linear"); } Otherwise you didn't show enough code to see what else might be wrong. Have you read any webgl tutorials? Where are you creating and binding the texture? What do your shaders look like? What kind of attributes are you using if any? function main() { var gl = document.querySelector("canvas").getContext("webgl"); var ext = gl.getExtension("OES_texture_float"); if (!ext) { alert("this machine or browser does not support OES_texture_float"); return; } var linear = gl.getExtension("OES_texture_float_linear"); if (!linear) { alert("this machine or browser does not support OES_texture_float_linear"); return; } var vs = ` void main() { gl_PointSize = 100.0; gl_Position = vec4(0, 0, 0, 1); } `; var fs = ` precision mediump float; uniform sampler2D u_tex; void main () { gl_FragColor = texture2D(u_tex, gl_PointCoord); } `; var program = twgl.createProgramFromSources(gl, [vs, fs]); // let's use a canvas instead of an image. It should be the same var image = document.createElement("canvas"); var ctx = image.getContext("2d"); for (var i = 20; i > 0; --i) { ctx.fillStyle = i % 2 ? "red" : "yellow"; ctx.beginPath(); ctx.arc(ctx.canvas.width / 2, ctx.canvas.height / 2, i * 20, 0, Math.PI * 2, false); ctx.fill(); } var tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_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); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.FLOAT, image); gl.useProgram(program); gl.drawArrays(gl.POINTS, 0, 1); } main(); canvas { border: 1px solid black; } <script src="https://twgljs.org/dist/twgl.min.js"></script> <canvas></canvas> Also it's not clear what you mean by "create a texture as a floating point one for that". If the features are supported then uploading the image it will get converted to floating point (which we see in the example above) but the input image is an 8bit image at best. If you really want floating point data you'll have to use binary data rather than an image. function main() { var gl = document.querySelector("canvas").getContext("webgl"); var ext = gl.getExtension("OES_texture_float"); if (!ext) { alert("this machine or browser does not support OES_texture_float"); return; } var linear = gl.getExtension("OES_texture_float_linear"); if (!linear) { alert("this machine or browser does not support OES_texture_float_linear"); return; } var vs = ` void main() { gl_PointSize = 100.0; gl_Position = vec4(0, 0, 0, 1); } `; var fs = ` precision mediump float; uniform sampler2D u_tex; void main () { gl_FragColor = texture2D(u_tex, gl_PointCoord) / vec4(32, 16, 32 + 16, 1); } `; var program = twgl.createProgramFromSources(gl, [vs, fs]); // create floating point data directly var width = 32; var height = 16; var data = new Float32Array(width * height * 4); // RGBA for (y = 0; y < height; ++y) { for (x = 0; x < width; ++x) { var off = (y * width + x) * 4; data[off + 0] = x; data[off + 1] = y; data[off + 2] = x + y; data[off + 3] = 1; } } var tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_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); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, data); gl.useProgram(program); gl.drawArrays(gl.POINTS, 0, 1); } main(); canvas { border: 1px solid black; } <script src="https://twgljs.org/dist/twgl.min.js"></script> <canvas></canvas>