WebGL Read pixels from floating point render target - webgl

There is some confusion e.g. in terms of support levels for rendering to floating point textures in WebGL. The OES_texture_float extension does not seem to mandate it per se, as per Optional support for FLOAT textures as FBO attachments (deprecated), but it looks like some vendors went ahead and implement it. Therefore my basic understanding is that rendering to floating point textures actually works in non-ES desktop environments. I have not been able to read from the floating point render target directly though.
My question is whether there is a way to read from a floating point texture using a WebGLContext::readPixels() call and a Float32Array destination? Thanks in advance.
Attached is a script that succeeds reading from a byte texture, but fails for a float texture:
<html>
<head>
<script>
function run_test(use_float) {
// Create canvas and context
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var gl = canvas.getContext("experimental-webgl");
// Decide on types to user for texture
var texType, bufferFmt;
if (use_float) {
texType = gl.FLOAT;
bufferFmt = Float32Array;
} else {
texType = gl.UNSIGNED_BYTE;
bufferFmt = Uint8Array;
}
// Query extension
var OES_texture_float = gl.getExtension('OES_texture_float');
if (!OES_texture_float) {
throw new Error("No support for OES_texture_float");
}
// Clear
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Create texture
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 512, 512, 0, gl.RGBA, texType, null);
// Create and attach frame buffer
var fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.bindTexture(gl.TEXTURE_2D, null);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
throw new Error("gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE");
}
// Clear
gl.viewport(0, 0, 512, 512);
gl.clear(gl.COLOR_BUFFER_BIT);
var pixels = new bufferFmt(4 * 512 * 512);
gl.readPixels(0, 0, 512, 512, gl.RGBA, texType, pixels);
if (pixels[0] !== (use_float ? 1.0 : 255)) {
throw new Error("pixels[0] === " + pixels[0].toString());
}
}
function main() {
run_test(false);
console.log('Test passed using GL_UNSIGNED_BYTE');
run_test(true);
console.log('Test passed using GL_FLOAT');
}
</script>
</head>
<body onload='main()'>
</body>
</html>

Unfortunately it still seems that reading out RGBA components as bytes is the only way for WebGL. If you need to encode a float into a pixel value you can use the following:
In your fractal shader (GLSL/HLSL):
float shift_right (float v, float amt) {
v = floor(v) + 0.5;
return floor(v / exp2(amt));
}
float shift_left (float v, float amt) {
return floor(v * exp2(amt) + 0.5);
}
float mask_last (float v, float bits) {
return mod(v, shift_left(1.0, bits));
}
float extract_bits (float num, float from, float to) {
from = floor(from + 0.5); to = floor(to + 0.5);
return mask_last(shift_right(num, from), to - from);
}
vec4 encode_float (float val) {
if (val == 0.0) return vec4(0, 0, 0, 0);
float sign = val > 0.0 ? 0.0 : 1.0;
val = abs(val);
float exponent = floor(log2(val));
float biased_exponent = exponent + 127.0;
float fraction = ((val / exp2(exponent)) - 1.0) * 8388608.0;
float t = biased_exponent / 2.0;
float last_bit_of_biased_exponent = fract(t) * 2.0;
float remaining_bits_of_biased_exponent = floor(t);
float byte4 = extract_bits(fraction, 0.0, 8.0) / 255.0;
float byte3 = extract_bits(fraction, 8.0, 16.0) / 255.0;
float byte2 = (last_bit_of_biased_exponent * 128.0 + extract_bits(fraction, 16.0, 23.0)) / 255.0;
float byte1 = (sign * 128.0 + remaining_bits_of_biased_exponent) / 255.0;
return vec4(byte4, byte3, byte2, byte1);
}
// (the following inside main(){}) return your float as the fragment color
float myFloat = 420.420;
gl_FragColor = encode_float(myFloat);
Then back on the JavaScript side, after your draw call has been made you can extract the encoded float value of each pixel with the following:
var pixels = new Uint8Array(CANVAS.width * CANVAS.height * 4);
gl.readPixels(0, 0, CANVAS.width, CANVAS.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
pixels = new Float32Array(pixels.buffer);
// pixels now contains an array of floats, 1 float for each pixel

The readPixels is limited to the RGBA format and the UNSIGNED_BYTE type (WebGL specification). However there are some methods for "packing" floats into RGBA/UNSIGNED_BYTE described here:
http://concord-consortium.github.io/lab/experiments/webgl-gpgpu/webgl.html

I'm adding my recent discoveries: Chrome will let you read floats, as part of the implementation defined format (search for "readPixels" in the spec), Firefox implements the WEBGL_color_buffer_float extension, so you can just load the extension and read your floats, I have not been able to read floats with Safari.

Things have changed since WebGL shipped. Basically WebGL requires that you can call readPixels with format = RGBA and type = UNSIGNED_BYTE. Otherwise, the implementation is allowed one other implementation defined format/type combo per framebuffer attachment type.
You can query what that format/type combo is like this
gl.bindFramebuffer(gl.FRAMEBUFFER, someFramebuffer);
const format = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
const type = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
Unfortunately, it's implementation defined. So for example, checking my personal devices at least one of them, my Nvidia Macbook Pro, reports RGBA/UNSIGNED_BYTE, in Chrome. The other browsers/devices report RGBA/FLOAT
WebGL2 requires being able to read as RGBA/FLOAT for floating point texture, if EXT_color_buffer_float extension is enabled.
A workaround in WebGL1 could include writing to RGBA/UNSIGNED_BYTE as FLOAT. See this. You could either change your shader. Or you could add another pass that reads your floating point result texture and writes to a RGBA/UNSIGNED_BYTE texture, maybe 4x as large to get all RGBA values.

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 });

Circle not rendering to canvas

I'm trying to render a circle using WebGL. I only need to translate the circle by a dynamic pixel value, so I used some translation logic from a tutorial converting pixel space to clipspace and put that in the vertex shader. I'm also using the common TRIANGLE_FAN technique to make the circle.
I currently can't see anything on the canvas; it renders as a white screen and there is no circle anywhere. I only want the circle to have a radius of 1px.
//shaders
const glsl = (x) => x;
const vertex = glsl`
attribute vec2 a_position;
uniform vec2 u_resolution;
uniform vec2 u_translation;
void main() {
//add in the translation
vec2 position = a_position + u_translation;
// convert the circle points from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
}
`;
const fragment = glsl`
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
function main() {
// Get A WebGL context
var canvas = document.querySelector("#c");
var gl = canvas.getContext("webgl");
if (!gl) {
return;
}
const opacity = 0.5; //opacity will be dynamic
const color = [0, 0, 0, opacity];
const translation = [50, 50]; //this translation value with be dynamic but using [50,50] for demo purposes
// Use our boilerplate utils to compile the shaders and link into a program
var program = webglUtils.createProgramFromScripts(gl, [vertex, fragment]);
// look up where the vertex data needs to go.
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
// look up uniform locations
var resolutionUniformLocation = gl.getUniformLocation(program,"u_resolution");
var translationUniformLocation = gl.getUniformLocation(program, "u_translation");
var colorUniformLocation = gl.getUniformLocation(program, "u_color");
// Create a buffer to put three 2d clip space points in
var positionBuffer = gl.createBuffer();
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
0.0, 0.0 //circle center vertex
];
const stops = 100;
for (i = 0; i < stops; i++){
positions.push(Math.cos(i * 2 * Math.PI/stops)); // x coord
positions.push(Math.sin(i * 2 * Math.PI/stops)); // y coord
}
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
//sets canvas width and height to current size of canvas as specified in css
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
// Tell WebGL how to convert from clip space to pixels
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Tell it to use our program (pair of shaders)
gl.useProgram(program);
// Turn on the attribute
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind the position buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per stop
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionAttributeLocation,
size,
type,
normalize,
stride,
offset
);
// set the resolution
gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
//set the translation
gl.uniform2fv(translationUniformLocation, translation);
//set the color
gl.uniform4fv(colorUniformLocation, color);
// draw
var primitiveType = gl.TRIANGLE_FAN;
var offset = 0;
const count = stops + 1; //adding one for center of circle
gl.drawArrays(primitiveType, offset, count);
}
main();
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
<canvas id="c"></canvas>
There are 3 problems with the code above
It's calling createProgramFromScripts instead of createProgramFromSources
The shader doesn't use u_translation
Here's the first 2 lines
//add in the translation
vec2 position = a_position + u_translation;
// convert the circle points from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
that second line is using a_position instead of position
The fan is missing the last triangle.
You probably want <= stops in your for loop
I'd strongly encourge you to follow the tutorials there and get comfortable using matrices. They start with shader code like the code above uses but progress into replacing it with matrices. Even for pixels matrices enable many things that will be hard without.

How to create a histogram in WebGL2 using 16 bit data?

var gl, utils, pseudoImg, vertices;
var img = null;
document.addEventListener('DOMContentLoaded', () => {
utils = new WebGLUtils();
vertices = utils.prepareVec2({x1 : -1.0, y1 : -1.0, x2 : 1.0, y2 : 1.0});
gl = utils.getGLContext(document.getElementById('canvas'));
var program = utils.getProgram(gl, 'render-vs', 'render16bit-fs');
var histogramProgram = utils.getProgram(gl, 'histogram-vs', 'histogram-fs');
var sortProgram = utils.getProgram(gl, 'sorting-vs', 'sorting-fs');
var showProgram = utils.getProgram(gl, 'showhistogram-vs', 'showhistogram-fs');
utils.activateTextureByIndex(gl, showProgram, 'histTex', 3);
utils.activateTextureByIndex(gl, showProgram, 'maxTex', 4);
utils.activateTextureByIndex(gl, sortProgram, 'tex3', 2);
utils.activateTextureByIndex(gl, histogramProgram, 'tex2', 1);
utils.activateTextureByIndex(gl, program, 'u_texture', 0);
var vertexBuffer = utils.createAndBindBuffer(gl, vertices);
var imageTexture;
computeHistogram = (AR, myFB) => {
gl.useProgram(histogramProgram);
var width = AR.width;
var height = AR.height;
var numOfPixels = width * height;
var pixelIds = new Float32Array(numOfPixels);
for (var i = 0; i < numOfPixels; i++) {
pixelIds[i] = i;
}
var histogramFbObj = utils.createTextureAndFramebuffer(gl, {
format : gl.RED,
internalFormat : gl.R32F,
filter : gl.NEAREST,
dataType : gl.FLOAT,
mipMapST : gl.CLAMP_TO_EDGE,
width : 256,
height : 256
});
gl.bindFramebuffer(gl.FRAMEBUFFER, histogramFbObj.fb);
gl.viewport(0, 0, 256, 256);
var pixelBuffer = utils.createAndBindBuffer(gl, pixelIds, true);
gl.blendFunc(gl.ONE, gl.ONE);
gl.enable(gl.BLEND);
utils.linkAndSendDataToGPU(gl, histogramProgram, 'pixelIds', pixelBuffer, 1);
gl.uniform2fv(gl.getUniformLocation(histogramProgram, 'imageDimensions'), [width, height]);
utils.sendTextureToGPU(gl, myFB.tex, 1);
gl.drawArrays(gl.POINTS, 0, numOfPixels);
gl.blendFunc(gl.ONE, gl.ZERO);
gl.disable(gl.BLEND);
return histogramFbObj;
};
sortHistogram = (histogramFbObj) => {
gl.useProgram(sortProgram);
utils.linkAndSendDataToGPU(gl, sortProgram, 'vertices', vertexBuffer, 2);
var sortFbObj = utils.createTextureAndFramebuffer(gl, {
format : gl.RED,
internalFormat : gl.R32F,
filter : gl.NEAREST,
dataType : gl.FLOAT,
mipMapST : gl.CLAMP_TO_EDGE,
width : 1,
height : 1
});
gl.bindFramebuffer(gl.FRAMEBUFFER, sortFbObj.fb);
gl.viewport(0, 0, 1, 1);
utils.sendTextureToGPU(gl, histogramFbObj.tex, 2);
gl.drawArrays(gl.TRIANGLES, 0, 6);
return sortFbObj;
};
showHistogram = (histFb, sortFb) => {
gl.useProgram(showProgram);
utils.linkAndSendDataToGPU(gl, showProgram, 'vertices', vertexBuffer, 2);
utils.sendTextureToGPU(gl, histFb.tex, 3);
utils.sendTextureToGPU(gl, sortFb.tex, 4);
gl.uniform2fv(gl.getUniformLocation(showProgram, 'imageDimensions'), [gl.canvas.width, gl.canvas.height]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
};
showTexture = (AR) => {
imageTexture = utils.createAndBindTexture(gl, {
filter : gl.NEAREST,
mipMapST : gl.CLAMP_TO_EDGE,
dataType : gl.UNSIGNED_SHORT,
format : gl.RGBA_INTEGER,
internalFormat : gl.RGBA16UI,
img : AR.img,
width : AR.width,
height : AR.height
});
gl.useProgram(program);
var myFB = utils.createTextureAndFramebuffer(gl, {
filter : gl.NEAREST,
mipMapST : gl.CLAMP_TO_EDGE,
dataType : gl.UNSIGNED_BYTE,
format : gl.RGBA,
internalFormat : gl.RGBA,
width : AR.width,
height : AR.height,
});
gl.bindFramebuffer(gl.FRAMEBUFFER, myFB.fb);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
utils.linkAndSendDataToGPU(gl, program, 'vertices', vertexBuffer, 2);
gl.uniform1f(gl.getUniformLocation(program, 'flipY'), 1.0);
utils.sendTextureToGPU(gl, imageTexture, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
var fb1 = computeHistogram(AR, myFB);
var fb2 = sortHistogram(fb1);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
showHistogram(fb1, fb2);
};
var w = 128;
var h = 128;
var size = w * h * 4;
var img = new Uint16Array(size); // need Uint16Array
for (var i = 0; i < img.length; i += 4) {
img[i + 0] = 65535; // r
img[i + 1] = i/64 * 256; // g
img[i + 2] = 0; // b
img[i + 3] = 65535; // a
}
showTexture({
img : img,
width : w,
height : h
});
});
<script id="render16bit-fs" type="not-js">
#version 300 es
precision highp float;
uniform highp usampler2D tex;
in vec2 texcoord; // receive pixel position from vertex shader
out vec4 fooColor;
void main() {
uvec4 unsignedIntValues = texture(tex, texcoord);
vec4 floatValues0To65535 = vec4(unsignedIntValues);
vec4 colorValues0To1 = floatValues0To65535;
fooColor = colorValues0To1;
}
</script>
<script type="not-js" id="render-vs">
#version 300 es
in vec2 vertices;
out vec2 texcoord;
uniform float flipY;
void main() {
texcoord = vertices.xy * 0.5 + 0.5;
gl_Position = vec4(vertices.x, vertices.y * flipY, 0.0, 1.0);
}
</script>
<script type="not-js" id="histogram-vs">
#version 300 es
in float pixelIds; //0,1,2,3,4,.......width*height
uniform sampler2D tex2;
uniform vec2 imageDimensions;
void main () {
vec2 pixel = vec2(mod(pixelIds, imageDimensions.x), floor(pixelIds / imageDimensions.x));
vec2 xy = pixel/imageDimensions;
float pixelValue = texture(tex2, xy).r;//Pick Pixel value from GPU texture ranges from 0-65535
float xDim = mod(pixelValue, 255.0)/256.0;
float yDim = floor(pixelValue / 255.0)/256.0;
float xVertex = (xDim*2.0) - 1.0;//convert 0.0 to 1.0 -> -1.0 -> 1.0, it will increment because we have used gl.blendFunc
float yVertex = 1.0 - (yDim*2.0);
gl_Position = vec4(xVertex, yVertex, 0.0, 1.0);
gl_PointSize = 1.0;
}
</script>
<script type="not-js" id="histogram-fs">
#version 300 es
precision mediump float;
out vec4 fcolor;
void main() {
fcolor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
<script type="not-js" id="sorting-vs">
#version 300 es
in vec2 vertices;
void main () {
gl_Position = vec4(vertices, 0.0, 1.0);
}
</script>
<script type="not-js" id="sorting-fs">
#version 300 es
precision mediump float;
out vec4 fcolor;
uniform sampler2D tex3;
const int MAX_WIDTH = 65536;
void main() {
vec4 maxColor = vec4(0.0);
for (int i = 0; i < MAX_WIDTH; i++) {
float xDim = mod(float(i), 256.0)/256.0;
float yDim = floor(float(i) / 256.0)/256.0;
vec2 xy = vec2(xDim, yDim);
vec4 currPixel = texture(tex3, xy).rrra;
maxColor = max(maxColor, currPixel);
}
fcolor = vec4(maxColor);
}
</script>
<script type="not-js" id="showhistogram-vs">
#version 300 es
in vec2 vertices;
void main () {
gl_Position = vec4(vertices, 0.0, 1.0);
}
</script>
<script type="not-js" id="showhistogram-fs">
#version 300 es
precision mediump float;
uniform sampler2D histTex, maxTex;
uniform vec2 imageDimensions;
out vec4 fcolor;
void main () {
// get the max color constants
vec4 maxColor = texture(maxTex, vec2(0));
// compute our current UV position
vec2 uv = gl_FragCoord.xy / imageDimensions;
vec2 uv2 = gl_FragCoord.xy / vec2(256.0, 256.0);
// Get the history for this color
vec4 hist = texture(histTex, uv2);
// scale by maxColor so scaled goes from 0 to 1 with 1 = maxColor
vec4 scaled = hist / maxColor;
// 1 > maxColor, 0 otherwise
vec4 color = step(uv2.yyyy, scaled);
fcolor = vec4(color.rgb, 1);
}
</script>
<canvas id="canvas"></canvas>
<script type="text/javascript">
class WebGLUtils {
getGLContext = (canvas, version) => {
canvas.width = window.innerWidth * 0.99;
canvas.height = window.innerHeight * 0.85;
var gl = canvas.getContext(version ? 'webgl' : 'webgl2');
const ext = gl.getExtension("EXT_color_buffer_float");
if (!ext) {
console.log("sorry, can't render to floating point textures");
}
gl.clearColor(0, 0, 0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.lineWidth(0.5);
return gl;
};
clear = (gl) => {
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
};
getShader = (gl, type, shaderText) => {
var vfs = gl.createShader(type);
gl.shaderSource(vfs, shaderText);
gl.compileShader(vfs);
if (!gl.getShaderParameter(vfs, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(vfs));
}
return vfs;
};
getProgram = (gl, vertexShader, fragmentShader) => {
var program = gl.createProgram();
gl.attachShader(program, this.getShader(gl, gl.VERTEX_SHADER, document.getElementById(vertexShader).text.trim()));
gl.attachShader(program, this.getShader(gl, gl.FRAGMENT_SHADER, document.getElementById(fragmentShader).text.trim()));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
}
return program;
};
createAndBindBuffer = (gl, relatedVertices, isNotJSArray) => {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, isNotJSArray ? relatedVertices : new Float32Array(relatedVertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return buffer;
};
createAndBindTexture = (gl, _) => {
var texBuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texBuffer);
if (_.img.width) {
gl.texImage2D(gl.TEXTURE_2D, 0, _.internalFormat, _.format, _.dataType, _.img);
} else {
gl.texImage2D(gl.TEXTURE_2D, 0, _.internalFormat, _.width, _.height, 0, _.format, _.dataType, _.img);
}
// set the filtering so we don't need mips
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, _.filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, _.filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, _.mipMapST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, _.mipMapST);
gl.bindTexture(gl.TEXTURE_2D, null);
return texBuffer;
};
createTextureAndFramebuffer = (gl, _) => {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, _.internalFormat, _.width, _.height, 0, _.format, _.dataType, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, _.filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, _.filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, _.mipMapST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, _.mipMapST);
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
console.log(`can ${status === gl.FRAMEBUFFER_COMPLETE ? "" : "NOT "}render to R32`);
return {tex: tex, fb: fb};
};
linkAndSendDataToGPU = (gl, program, linkedVariable, buffer, dimensions) => {
var vertices = gl.getAttribLocation(program, linkedVariable);
gl.enableVertexAttribArray(vertices);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(vertices, dimensions, gl.FLOAT, gl.FALSE, 0, 0);
return vertices;
};
sendDataToGPU = (gl, buffer, vertices, dimensions) => {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(vertices, dimensions, gl.FLOAT, gl.FALSE, 0, 0);
};
sendTextureToGPU = (gl, tex, index) => {
gl.activeTexture(gl.TEXTURE0 + index);
gl.bindTexture(gl.TEXTURE_2D, tex);
};
calculateAspectRatio = (img, gl) => {
var cols = img.width;
var rows = img.height;
var imageAspectRatio = cols / rows;
var ele = gl.canvas;
var windowWidth = ele.width;
var windowHeight = ele.height;
var canvasAspectRatio = windowWidth / windowHeight;
var renderableHeight, renderableWidth;
var xStart, yStart;
/// If image's aspect ratio is less than canvas's we fit on height
/// and place the image centrally along width
if(imageAspectRatio < canvasAspectRatio) {
renderableHeight = windowHeight;
renderableWidth = cols * (renderableHeight / rows);
xStart = (windowWidth - renderableWidth) / 2;
yStart = 0;
}
/// If image's aspect ratio is greater than canvas's we fit on width
/// and place the image centrally along height
else if(imageAspectRatio > canvasAspectRatio) {
renderableWidth = windowWidth;
renderableHeight = rows * (renderableWidth / cols);
xStart = 0;
yStart = ( windowHeight - renderableHeight) / 2;
}
///keep aspect ratio
else {
renderableHeight = windowHeight ;
renderableWidth = windowWidth;
xStart = 0;
yStart = 0;
}
return {
y2 : yStart + renderableHeight,
x2 : xStart + renderableWidth,
x1 : xStart,
y1 : yStart
};
};
convertCanvasCoordsToGPUCoords = (canvas, AR) => {
//GPU -> -1, -1, 1, 1
//convert to 0 -> 1
var _0To1 = {
y2 : AR.y2/canvas.height,
x2 : AR.x2/canvas.width,
x1 : AR.x1/canvas.width,
y1 : AR.y1/canvas.height
};
//Convert -1 -> 1
return {
y2 : -1 + _0To1.y2 * 2.0,
x2 : -1 + _0To1.x2 * 2.0,
x1 : -1 + _0To1.x1 * 2.0,
y1 : -1 + _0To1.y1 * 2.0
};
};
//convert -1->+1 to 0.0->1.0
convertVertexToTexCoords = (x1, y1, x2, y2) => {
return {
y2 : (y2 + 1.0)/2.0,
x2 : (x2 + 1.0)/2.0,
x1 : (x1 + 1.0)/2.0,
y1 : (y1 + 1.0)/2.0
};
};
activateTextureByIndex = (gl, program, gpuRef, gpuTextureIndex) => {
gl.useProgram(program);
gl.uniform1i(gl.getUniformLocation(program, gpuRef), gpuTextureIndex);
};
prepareVec4 = (_) => {
return [_.x1, _.y1, 0.0, 1.0,
_.x2, _.y1, 0.0, 1.0,
_.x1, _.y2, 0.0, 1.0,
_.x2, _.y1, 0.0, 1.0,
_.x1, _.y2, 0.0, 1.0,
_.x2, _.y2, 0.0, 1.0];
};
prepareVec2 = (_) => {
return [_.x1, _.y1,
_.x2, _.y1,
_.x1, _.y2,
_.x2, _.y1,
_.x1, _.y2,
_.x2, _.y2];
};
};
</script>
I am able to render a 8 bit histogram in both WebGL1 and WebGL2 using this code. But I need to generate a 16 bit histogram using 16 bit texture.
Here's how am sending the texture to GPU:
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 -> gl.RGBA_INTEGER
gl.UNSIGNED_SHORT, // type -> gl.UNSIGNED_SHORT
img // texture data
);
So, take the working example in mind, I am stuck with few things :
1) How to create a 65536 X 1 framebuffer/texture to keep the 16 bit histogram as WebGL clearly says : WebGL: INVALID_VALUE: texImage2D: width or height out of range. Can we try a 256 x 256 framebuffer? I tried but stuck in point no. 2 below.
2) How to read through the pixels inside the vertex shader in case of 16 bit, below code is for 8-bit data, will it work for 16 bit as well? As I can't debug, so can't say whether it works or not:
<script id="hist-vs" type="not-js">
attribute float pixelId;
uniform vec2 u_resolution;
uniform sampler2D u_texture;
uniform vec4 u_colorMult;
void main() {
// based on an id (0, 1, 2, 3 ...) compute the pixel x, y for the source image
vec2 pixel = vec2(mod(pixelId, u_resolution.x), floor(pixelId / u_resolution.x));
// compute corresponding uv center of that pixel
vec2 uv = (pixel + 0.5) / u_resolution;
// get the pixels but 0 out channels we don't want
vec4 color = texture2D(u_texture, uv) * u_colorMult;
// add up all the channels. Since 3 are zeroed out we'll get just one channel
float colorSum = color.r + color.g + color.b + color.a;
// set the position to be over a single pixel in the 256x1 destination texture
gl_Position = vec4((colorSum * 255.0 + 0.5) / 256.0 * 2.0 - 1.0, 0.5, 0, 1);
gl_PointSize = 1.0;
}
</script>
If you just want answers to your 2 questions then
1) How to create a 65536 X 1 framebuffer/texture to keep the 16 bit histogram as WebGL clearly says : WebGL: INVALID_VALUE: texImage2D: width or height out of range. Can we try a 256 x 256 framebuffer?
yes, you'd make 256x256 texture if you want to know the totals for each of the 65536 possible values
2) How to read through the pixels inside the vertex shader in case of 16 bit, below code is for 8-bit data, will it work for 16 bit as well? As I can't debug, so can't say whether it works or not:
Of course you can debug. You try it and see if the results are correct. If they aren't you look at your code and or the error messages and try to figure out why. That's called debugging. Make a 1x1 texture, call your function, check if the histogram has the correct count of 1 for that 1x1 pixel input by calling gl.readPixels. Then try 2x1 or 2x2.
In any case you can't read gl.RGBA16UI textures with GLSL 1.0 es. You have to use version 300 es so if you actually want to create a separate bucket for all 65536 values then
Here's some a WebGL2 GLSL 3.00 ES shader that will fill out the totals for values from 0 to 65535 in a 256x256 texture
#version 300 es
uniform usampler2D u_texture;
uniform uvec4 u_colorMult;
void main() {
const int mipLevel = 0;
ivec2 size = textureSize(u_texture, mipLevel);
// based on an id (0, 1, 2, 3 ...) compute the pixel x, y for the source image
vec2 pixel = vec2(
gl_VertexID % size.x,
gl_VertexID / size.x);
// get the pixels but 0 out channels we don't want
uvec4 color = texelFetch(u_texture, pixel, mipLevel) * u_colorMult;
// add up all the channels. Since 3 are zeroed out we'll get just one channel
uint colorSum = color.r + color.g + color.b + color.a;
vec2 outputPixel = vec2(
colorSum % 256u,
colorSum / 256u);
// set the position to be over a single pixel in the 256x256 destination texture
gl_Position = vec4((outputPixel + 0.5) / 256.0 * 2.0 - 1.0, 0, 1);
gl_PointSize = 1.0;
}
Example
Notes:
In WebGL2 you don't need pixelID, you can use gl_VertexID so no need to setup any buffers or attributes. Just call
const numPixels = img.width * img.height;
gl.drawArrays(gl.POINTS, 0, numPixels);
You can use textureSize to get the size of a texture so no need to pass it in.
You can use texelFetch to get a single texel(pixel) from a texture. It takes integer pixel coordinates.
To read an unsigned integer texture format like RGBA16UI you have to use a usampler2D otherwise you'll get an error at draw time drawing to use an RGBA16UI texture on a sampler2D (this is how I know you weren't actually using a RGBA16UI texture because you would have gotten an error in the JavaScript console telling you and leading you to change your shader.
You still need to use a floating point texture as the target because the technique used requires blending but blending doesn' work with integer textures (just in case you got the idea to try to use an integer based texture in the framebuffer)

Unable to generate mipmap for half_float texture

I am using webgl2 and loading my texture data as half floats. I can render the image correctly when using LINEAR MIN_FILTER. However, I want to use a mipmap filter. When I use a mipmap filter and attempt to generate mipmaps it fails. The webgl documentation https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D indicates R16F textures are filterable and doesn't indicate it is limited to LINEAR filters. Is there a step I am missing or is this an undocumented limitation of webgl2?
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
const tex = gl.createTexture();
const unit = 1; // Pick some texture unit
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, tex);
const numPixels = this.width * this.height;
const level = 0;
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.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); //Works
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST); //Does NOT work
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// Upload the image into the texture
const pixel = new Uint16Array(this.binaryImage);
gl.texImage2D(gl.TEXTURE_2D, level, gl.R16F, this.width, this.height, 0, gl.RED, gl.HALF_FLOAT, pixel);
gl.generateMipmap(gl.TEXTURE_2D); //FAILS
const sampler2DLoc = gl.getUniformLocation(program, "u_image");
gl.uniform1i(sampler2DLoc, unit);
WebGL2's spec says WebGL2 is OpenGL ES 3.0 with the differences listed in the WebGL2 spec. Otherwise the WebGL2 spec says to read the OpenGL ES 3.0 spec for the details.
From the OpenGL ES 3.0 spec section 3.8.10.5
3.8.10.5 Manual Mipmap Generation
Mipmaps can be generated manually with the command
void GenerateMipmap(enumtarget);
...
If the level base array was not specified with an unsized internal format from table 3.3 or a sized internal format that is both color-renderable and texture-filterable according to table 3.13, an INVALID_OPERATION error is generated
R16F is texture-filterable but it is not color-renderable
You'd need to check for and enable the EXT_color_buffer_float extension to be able to generate mips for half float formats.
'use strict';
function main() {
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) {
return alert('need webgl2');
}
const ext = gl.getExtension('EXT_color_buffer_float');
if (!ext){
return alert('need EXT_color_buffer_float');
}
const vs = `#version 300 es
void main() {
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 120.0;
}
`;
const fs = `#version 300 es
precision mediump float;
uniform sampler2D tex;
out vec4 outColor;
void main() {
outColor = vec4(texture(tex, gl_PointCoord.xy).r, 0, 0, 1);
}
`;
// setup GLSL program
const program = twgl.createProgram(gl, [vs, fs]);
// a 2x2 pixel data
const h0 = 0x0000;
const h1 = 0x3c00;
const pixels = new Uint16Array([
h0, h1,
h1, h0,
]);
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D,
0, // level
gl.R16F, // internal format
2, // width
2, // height
0, // border
gl.RED, // format
gl.HALF_FLOAT, // type
pixels, // data
);
gl.generateMipmap(gl.TEXTURE_2D);
gl.useProgram(program);
const offset = 0;
const count = 1;
gl.drawArrays(gl.POINTS, offset, count);
console.log('gl.getError should be 0 was:', gl.getError());
}
main();
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>

How to use packed pixel array in WebGL Texture? [duplicate]

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.

Resources