const vertexShaderSource = `#version 300 es
// an attribute is an input (in) to a vertex shader.
// It will receive data from a buffer
in vec2 a_position;
in vec2 a_texCoord;
// Used to pass in the resolution of the canvas
uniform vec2 u_resolution;
// Used to pass the texture coordinates to the fragment shader
out vec2 v_texCoord;
// all shaders have a main function
void main() {
// convert the position 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);
// pass the texCoord to the fragment shader
// The GPU will interpolate this value between points.
v_texCoord = a_texCoord;
}`;
const fragmentShaderSource = `#version 300 es
// fragment shaders don't have a default precision so we need
// to pick one. highp is a good default. It means "high precision"
precision highp float;
// our texture
uniform sampler2D u_image;
// the texCoords passed in from the vertex shader.
in vec2 v_texCoord;
// we need to declare an output for the fragment shader
out vec4 outColor;
void main() {
vec4 inColor = texture(u_image, v_texCoord);
outColor = vec4(
inColor.r != 0.0 ? inColor.r : 1.0/255.0,
inColor.g != 0.0 ? inColor.g : 1.0/255.0,
inColor.b != 0.0 ? inColor.b : 1.0/255.0,
inColor.a
);
}`;
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function() {
resolve(this.result);
}
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
function loadImage(url) {
return new Promise(async(resolve, reject) => {
const image = new Image();
image.onload = function() {
resolve(this);
}
image.onerror = reject;
image.src = url;
})
}
function canvasToBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob(blob => blob ? resolve(blob) : reject(canvas), "image/png");
});
}
function howManyZeros(gl) {
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
let data = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
let zeros = 0;
for (let i=0; i<data.length; i++) {
if (i % 4 == 3) continue; // ignore alpha
if (data[i] == 0) zeros++;
}
return zeros;
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
function render(image) {
const canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const gl = canvas.getContext("webgl2", {
premultipliedAlpha: false
});
if (!gl) {
console.error("No WebGL2");
return;
}
canvas.gl = gl;
// setup GLSL program
const program = webglUtils.createProgramFromSources(gl, [vertexShaderSource, fragmentShaderSource]);
// look up where the vertex data needs to go.
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
// lookup uniforms
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const imageLocation = gl.getUniformLocation(program, "u_image");
// Create a vertex array object (attribute state)
const vao = gl.createVertexArray();
// and make it the one we're currently working with
gl.bindVertexArray(vao);
// Create a buffer and put a single pixel space rectangle in
// it (2 triangles)
const positionBuffer = gl.createBuffer();
// Turn on the attribute
gl.enableVertexAttribArray(positionAttributeLocation);
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
let size = 2; // 2 components per iteration
let type = gl.FLOAT; // the data is 32bit floats
let normalize = false; // don't normalize the data
let stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
let offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset);
// provide texture coordinates for the rectangle.
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
// Turn on the attribute
gl.enableVertexAttribArray(texCoordAttributeLocation);
// Tell the attribute how to get data out of texCoordBuffer (ARRAY_BUFFER)
size = 2; // 2 components per iteration
type = gl.FLOAT; // the data is 32bit floats
normalize = false; // don't normalize the data
stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
texCoordAttributeLocation, size, type, normalize, stride, offset);
// Create a texture.
const texture = gl.createTexture();
// make unit 0 the active texture uint
// (ie, the unit all other texture commands will affect
gl.activeTexture(gl.TEXTURE0 + 0);
// Bind it to texture unit 0' 2D bind point
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we don't need mips and so we're not filtering
// and we don't repeat
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);
// Upload the image into the texture.
const mipLevel = 0; // the largest mip
const internalFormat = gl.RGBA; // format we want in the texture
const srcFormat = gl.RGBA; // format of data we are supplying
const srcType = gl.UNSIGNED_BYTE; // type of data we are supplying
gl.texImage2D(gl.TEXTURE_2D,
mipLevel,
internalFormat,
srcFormat,
srcType,
image);
// 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 | gl.DEPTH_BUFFER_BIT);
// Tell it to use our program (pair of shaders)
gl.useProgram(program);
// Bind the attribute/buffer set we want.
gl.bindVertexArray(vao);
// Pass in the canvas resolution so we can convert from
// pixels to clipspace in the shader
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// Tell the shader to get the texture from texture unit 0
gl.uniform1i(imageLocation, 0);
// Bind the position buffer so gl.bufferData that will be called
// in setRectangle puts data in the position buffer
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Set a rectangle the same size as the image.
setRectangle(gl, 0, 0, image.width, image.height);
// Draw the rectangle.
const primitiveType = gl.TRIANGLES;
offset = 0;
const count = 6;
gl.drawArrays(primitiveType, offset, count);
return canvas;
}
async function onFileChange(e) {
const png_data = await readFile(e.target.files[0]);
const png_blob = new Blob([png_data], {
type: 'image/png'
});
const png_url = URL.createObjectURL(png_blob);
document.querySelector("#before-img").src = png_url;
const image = await loadImage(png_url);
let canvas = render(image);
document.querySelector("#after").textContent = `nunber of zeros after: ${howManyZeros(canvas.gl)}`
const new_png_blob = await canvasToBlob(canvas);
const new_png_url = URL.createObjectURL(new_png_blob);
const dl_link = document.querySelector("a");
dl_link.href = new_png_url;
dl_link.style.display = "";
document.querySelector("#after-img").src = new_png_url;
};
const input = document.querySelector('input[type=file]');
input.addEventListener('change', onFileChange, false);
<input type="file" />
<div>
<a style="display: none" download="image.png">download</a>
</div>
<div>
<p id="before"></p>
<img id="before-img" src="" />
</div>
<div>
<p id="after"></p>
<img id="after-img" src="" />
</div>
<script src="https://webgl2fundamentals.org/webgl/resources/webgl-utils.js"></script>