I'm trying to replace all RGB pixels with the value of 0 to 1 (out of the max value of 255).
Here is my code on stackblitz.
You can see that after I'm reassigning the buffer with the new pixels some of the pixles are back to 0. On Firefox it's actually works with this image:
But won't work with the full size image:
It's like the browser won't allow a certain contrast of something like that.
Sharing my code here as well:
const getBase64FromFile = async (file: File): Promise<string> => {
return new Promise((resolve: Function, reject: Function) => {
let reader = new FileReader();
reader.addEventListener(
'load',
(arg) => {
resolve(reader.result);
},
false
);
reader.readAsDataURL(file);
});
};
// Returns the amount of pixels with RGB 0 value
const howManyZeros = async (src: string): Promise<number> => {
return new Promise((resolve: Function, reject: Function) => {
const image = new Image();
image.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
ctx.drawImage(image, 0, 0);
const data = ctx.getImageData(
0,
0,
image.naturalWidth,
image.naturalHeight
).data;
let zeros = 0;
for (var i = 0; i < data.length; i += 4) {
if (data[i] === 0) zeros++;
if (data[i + 1] === 0) zeros++;
if (data[i + 2] === 0) zeros++;
}
resolve(zeros);
};
image.src = src;
});
};
const onFinish = async (src: string) => {
document.querySelector(
'p#after'
).textContent = `nunber of zeros after: ${await howManyZeros(src)}`;
(document.querySelector('img#after-img') as HTMLImageElement).src = src;
const a = document.querySelector('a');
a.setAttribute('href', src);
a.setAttribute('download', 'image.png');
a.style.display = '';
};
const onFileChange = async (e: Event | any) => {
const image = new Image();
image.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
let data = ctx.getImageData(
0,
0,
image.naturalWidth,
image.naturalHeight
).data;
let buffer = new Uint8ClampedArray(
image.naturalWidth * image.naturalHeight * 4
);
// Iterate over all the pixels and increase all RGB 0 values to 1
for (var i = 0; i < data.length; i += 4) {
if (data[i] === 0) buffer[i] = 1;
else buffer[i] = data[i];
if (data[i + 1] === 0) buffer[i + 1] = 1;
else buffer[i + 1] = data[i + 1];
if (data[i + 2] === 0) buffer[i + 2] = 1;
else buffer[i + 2] = data[i + 2];
buffer[i + 3] = data[i + 3];
}
const iData = ctx.createImageData(image.naturalWidth, image.naturalHeight);
iData.data.set(buffer);
ctx.putImageData(iData, 0, 0);
onFinish(canvas.toDataURL('image/png', 1));
};
let src = await getBase64FromFile(e.target.files[0]);
document.querySelector(
'p#before'
).textContent = `nunber of zeros before: ${await howManyZeros(src)}`;
(document.querySelector('img#before-img') as HTMLImageElement).src = src;
image.src = src;
};
const input: HTMLInputElement = document.querySelector('input');
input.addEventListener('change', onFileChange, false);
Appreciate any help with this and praying that's not a browser issue but something with my code.
Due to canvas spec, which doesn't guarantee that the pixels stay the same as you set them, you can't use browser built in image manipulation functions.
Due to the lossy nature of converting between color spaces and converting to and from premultiplied alpha color values, pixels that have just been set using putImageData(), and are not completely opaque, might be returned to an equivalent getImageData() as different values.
In your case pixel values with high transparency get turned to 0 again.
This doesn't happen in WebGL context with the premultipliedAlpha context attribute set to false, but the solution involves a lot of code.
The following code is based on the example from WebGL2 Fundamentals:
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>
Alternatively you could do it with a 3rd party image manupulation library.
Here's an example using the UPNG.js library:
function readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function() {
resolve(this.result);
}
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
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 png = UPNG.decode(png_data);
const png_frames = UPNG.toRGBA8(png);
const png_frame = new Uint8Array(png_frames[0]);
for (var i = 0; i < png_frame.length; i += 4) {
png_frame[i + 0] = png_frame[i + 0] == 0 ? 1 : png_frame[i + 0];
png_frame[i + 1] = png_frame[i + 1] == 0 ? 1 : png_frame[i + 1];
png_frame[i + 2] = png_frame[i + 2] == 0 ? 1 : png_frame[i + 2];
//png_frame[i+3] = 255; // remove transparency
}
const new_png_data = UPNG.encode([png_frame.buffer], png.width, png.height, 0);
const new_png_blob = new Blob([new_png_data], {
type: 'image/png'
});
const new_png_url = URL.createObjectURL(new_png_blob);
/*const new_png_url = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(new_png_data)));*/
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 type="module">
import UPNG from "https://cdn.skypack.dev/upng-js#2.1.0"; window.UPNG = UPNG;
</script>
Related
I'm a newbie to ThreeJS and I have a Shadertoy shader (this one) that I want to embed in my webpage. It makes use of a buffer. I haven't been able to find much useful info on the topic. The main image shader applies a heatmap color scheme, while the buffer shader uses some math to solve the 2D heat equation, which is what the Shadertoy shader does. The buffer shader uses its previous state in these calculations. For reference, I followed the instructions in this Stackoverflow post.
Here's the main image:
out vec4 col;
uniform sampler2D tex;
void main()
{
float t = texelFetch(tex, ivec2(gl_FragCoord.xy), 0).x;
col = vec4(
sqrt(t),
t * t * t,
max(sin(6.283 * t), 0.),
t
);
}
And here's the buffer:
#define R 8.
#define DT 0.1
out vec4 col;
uniform sampler2D tex;
uniform float alpha;
uniform vec2 iMouse;
vec4 laplace(vec2 p)
{
// 5-point stencil Laplacian
vec4 c = texelFetch(tex, ivec2(p), 0);
vec4 lt = texelFetch(tex, ivec2(p + vec2(-1, 0)), 0);
vec4 rt = texelFetch(tex, ivec2(p + vec2(1, 0)), 0);
vec4 up = texelFetch(tex, ivec2(p + vec2(0, -1)), 0);
vec4 dn = texelFetch(tex, ivec2(p + vec2(0, 1)), 0);
return lt + up + rt + dn - (4. * c);
}
void main()
{
vec2 p = gl_FragCoord.xy;
if (distance(p, iMouse) < R)
{
col = vec4(1.0);
} else {
// Euler integration
vec4 T = texelFetch(tex, ivec2(p), 0);
vec4 iT = alpha * laplace(p);
col = T + iT * DT;
}
}
Here's my JS code:
let canvas, renderer, camera, renderTarg;
let size;
let count = 2;
let scene0, plane0, fragment0, uniforms0;
let scene1, plane1, fragment1, uniforms1;
let drawing = false;
let a = 1;
let coord = [200, 200];
document.addEventListener("DOMContentLoaded", function() {
// Set up canvas and renderers
canvas = document.getElementById("canv");
canvas.addEventListener("mousedown", startDraw);
canvas.addEventListener("mousemove", draw);
canvas.addEventListener("mouseup", endDraw);
canvas.addEventListener("mouseout", endDraw);
size = window.innerHeight;
renderer = new THREE.WebGLRenderer({
canvas,
preserveDrawingBuffer: true
});
// Initialize objects and cameras
scene0 = new THREE.Scene();
scene1 = new THREE.Scene();
plane0 = new THREE.PlaneGeometry(2, 2);
plane1 = new THREE.PlaneGeometry(2, 2);
renderTarg = new THREE.WebGLRenderTarget(size, size);
// Load shaders
let loader = new THREE.FileLoader();
function next() {
count--;
if (count == 0)
load();
}
loader.load("static/buffer.frag", (dat) => {fragment0 = dat; next();});
loader.load("static/mainShader.frag", (dat) => {fragment1 = dat; next();});
});
function load() {
// Create meshes and apply shaders
uniforms0 = {tex: {value: new THREE.Texture()}, iMouse: {value: new THREE.Vector2()}, alpha: {value: a}};
uniforms1 = {tex: {value: new THREE.Texture()}};
let mat0 = new THREE.ShaderMaterial({fragmentShader: fragment0, uniforms: uniforms0, glslVersion: THREE.GLSL3});
let mat1 = new THREE.ShaderMaterial({fragmentShader: fragment1, uniforms: uniforms1, glslVersion: THREE.GLSL3});
let planeMesh0 = new THREE.Mesh(plane0, mat0);
let planeMesh1 = new THREE.Mesh(plane1, mat1);
scene0.add(planeMesh0);
scene1.add(planeMesh1);
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1);
renderer.setSize(size, size, false);
animLoop();
}
function animLoop() {
// Render buffer
uniforms0.tex.value = renderTarg.texture;
uniforms0.iMouse.value.set(coord[0], coord[1]);
uniforms0.alpha.value = a;
renderer.render(scene0, camera, renderTarg);
// Render main
uniforms1.tex.value = renderTarg.texture;
renderer.render(scene1, camera);
window.requestAnimationFrame(animLoop);
}
/*
Event handlers for clicking and dragging, to trace path.
*/
const startDraw = (e) => {
drawing = true;
draw(e);
}
const draw = (e) => {
// Draw on canvas
if (drawing) {
let x = e.pageX - canvas.offsetLeft;
let y = size - (e.pageY - canvas.offsetTop);
coord = [x, y];
}
}
const endDraw = (e) => {
drawing = false;
}
When I run it I just get a black screen, no error messages. When I comment out the second call to renderer.render, I get a white circle that follows the cursor.
Thanks in advance.
I'm learning WebGL now.
I made a simple scene with 10 triangles and when I had increased amount of triangles to 1000 scene started to freeze. I use 3 shaders and 2 program (for emulation real environment). I know I should take out of the render cycle body something but I don't know what.
My code is bellow:
function render() {
requestAnimationFrame(render);
context.clear(context.COLOR_BUFFER_BIT);
for (let i = 0; i < 10; i++) {
const currentProgram = i % 2 === 0 ? blueProgram : redProgram;
context.useProgram(currentProgram);
const a_Position = context.getAttribLocation(currentProgram, "a_Position");
const triangleGeometry = getTriangleGeometry(); // returns Float32Array filled with randoms
const buffer = context.createBuffer();
context.bindBuffer(context.ARRAY_BUFFER, buffer);
context.bufferData(context.ARRAY_BUFFER, triangleGeometry, context.STATIC_DRAW);
context.enableVertexAttribArray(a_Position);
context.vertexAttribPointer(
a_Position,
2,
context.FLOAT,
false,
0,
0,
);
context.drawArrays(context.TRIANGLES, 0, 3);
}
}
requestAnimationFrame(render);
Any ideas what I can do for performance optimization?
There are many ways to optimize drawing lots of things but since yoi're just starting the most important is that in general setting up buffers should happen at init time, not render time.
See Draw multiple models in WebGL
The code in the question is looking up locations every triangle. it should look up locations as init time.
The code is also creating a new buffer for every triangle. It would be faster to create one buffer and just update it with the new triangle, and of course eventually it will run out of memory creating new buffers.
const context = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
`;
const redFS = `
precision highp float;
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
`;
const blueFS = `
precision highp float;
void main() {
gl_FragColor = vec4(0, 0, 1, 1);
}
`;
const blueProgram = twgl.createProgram(context, [vs, blueFS]);
const blueProgramInfo = {
program: blueProgram,
a_PositionLocation: context.getAttribLocation(blueProgram, "a_Position"),
};
const redProgram = twgl.createProgram(context, [vs, redFS]);
const redProgramInfo = {
program: redProgram,
a_PositionLocation: context.getAttribLocation(redProgram, "a_Position"),
};
const buffer = context.createBuffer();
function rand(min, max) {
return Math.random() * (max - min) + min;
}
// pre allocate
const triangleData = new Float32Array(6); // 3 vertices, 2 values per
function getTriangleGeometry() {
const x = rand(-1, 1);
const y = rand(-1, 1);
triangleData[0] = x;
triangleData[1] = y;
triangleData[2] = x + rand(-0.1, 0.1);
triangleData[3] = y + rand(-0.1, 0.1);
triangleData[4] = x + rand(-0.1, 0.1);
triangleData[5] = y + rand(-0.1, 0.1);
return triangleData;
}
function render() {
context.clear(context.COLOR_BUFFER_BIT);
for (let i = 0; i < 100; i++) {
const currentProgramInfo = i % 2 === 0 ? blueProgramInfo : redProgramInfo;
context.useProgram(currentProgramInfo.program);
const a_Position = currentProgramInfo.a_PositionLocation;
const triangleGeometry = getTriangleGeometry(); // returns Float32Array filled with randoms
context.bindBuffer(context.ARRAY_BUFFER, buffer);
context.bufferData(context.ARRAY_BUFFER, triangleGeometry, context.STATIC_DRAW);
context.enableVertexAttribArray(a_Position);
context.vertexAttribPointer(
a_Position,
2,
context.FLOAT,
false,
0,
0,
);
context.drawArrays(context.TRIANGLES, 0, 3);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
The code in the question appears to be using 2 programs, one to draw blue and one to draw red. It would probably be faster to have one program with a uniform to choose the color.
const context = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
`;
const fs = `
precision highp float;
uniform vec4 u_Color;
void main() {
gl_FragColor = u_Color;
}
`;
const program = twgl.createProgram(context, [vs, fs]);
const programInfo = {
program: program,
a_PositionLocation: context.getAttribLocation(program, "a_Position"),
u_ColorLocation: context.getUniformLocation(program, "u_Color"),
};
const buffer = context.createBuffer();
function rand(min, max) {
return Math.random() * (max - min) + min;
}
// pre allocate
const triangleData = new Float32Array(6); // 3 vertices, 2 values per
function getTriangleGeometry() {
const x = rand(-1, 1);
const y = rand(-1, 1);
triangleData[0] = x;
triangleData[1] = y;
triangleData[2] = x + rand(-0.1, 0.1);
triangleData[3] = y + rand(-0.1, 0.1);
triangleData[4] = x + rand(-0.1, 0.1);
triangleData[5] = y + rand(-0.1, 0.1);
return triangleData;
}
const blue = [0, 0, 1, 1];
const red = [1, 0, 0, 1];
function render() {
context.clear(context.COLOR_BUFFER_BIT);
context.useProgram(programInfo.program);
const a_Position = programInfo.a_PositionLocation;
context.bindBuffer(context.ARRAY_BUFFER, buffer);
context.enableVertexAttribArray(a_Position);
context.vertexAttribPointer(
a_Position,
2,
context.FLOAT,
false,
0,
0,
);
for (let i = 0; i < 100; i++) {
const color = i % 2 === 0 ? blue : red;
context.uniform4fv(programInfo.u_ColorLocation, color);
const triangleGeometry = getTriangleGeometry(); // returns Float32Array filled with randoms
context.bufferData(context.ARRAY_BUFFER, triangleGeometry, context.STATIC_DRAW);
context.drawArrays(context.TRIANGLES, 0, 3);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
It would be significantly faster if you put all the triangles in a single buffer at init time as well as each triangle's vertex colors in a buffer at init time and just draw them with a single draw call at render time. If you wanted random triangles every frame then it will still be faster to create one buffer at init time, fill out N random triangles in a single buffer, and then draw them all in one draw call.
const context = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
gl_Position = a_Position;
v_Color = a_Color;
}
`;
const fs = `
precision highp float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`;
const program = twgl.createProgram(context, [vs, fs]);
const programInfo = {
program: program,
a_PositionLocation: context.getAttribLocation(program, "a_Position"),
a_ColorLocation: context.getAttribLocation(program, "a_Color"),
u_ColorLocation: context.getUniformLocation(program, "u_Color"),
};
const positionBuffer = context.createBuffer();
const colorBuffer = context.createBuffer();
function rand(min, max) {
return Math.random() * (max - min) + min;
}
const numTriangles = 1000;
const positionData = new Float32Array(numTriangles * 3 * 2);
const colorData = new Float32Array(numTriangles * 3 * 4);
const blue = [0, 0, 1, 1];
const red = [1, 0, 0, 1];
// the color data does not change so fill it out at init time
for (let i = 0; i < numTriangles; ++i) {
const offset = i * 4;
colorData.set(i % 2 === 0 ? blue : red, offset);
}
context.bindBuffer(context.ARRAY_BUFFER, colorBuffer);
context.bufferData(context.ARRAY_BUFFER, colorData, context.STATIC_DRAW);
function getTriangleGeometry() {
for (let i = 0; i < numTriangles; ++i) {
const offset = i * 3 * 2; // 3 verts per tri, 2 values per ver
const x = rand(-1, 1);
const y = rand(-1, 1);
positionData[offset ] = x;
positionData[offset + 1] = y;
positionData[offset + 2] = x + rand(-0.1, 0.1);
positionData[offset + 3] = y + rand(-0.1, 0.1);
positionData[offset + 4] = x + rand(-0.1, 0.1);
positionData[offset + 5] = y + rand(-0.1, 0.1);
}
return positionData;
}
function render() {
context.clear(context.COLOR_BUFFER_BIT);
context.useProgram(programInfo.program);
const a_Position = programInfo.a_PositionLocation;
context.bindBuffer(context.ARRAY_BUFFER, positionBuffer);
const triangleGeometry = getTriangleGeometry(); // returns Float32Array filled with randoms
context.bufferData(context.ARRAY_BUFFER, triangleGeometry, context.DYNAMIC_DRAW);
context.enableVertexAttribArray(a_Position);
context.vertexAttribPointer(
a_Position,
2,
context.FLOAT,
false,
0,
0,
);
const a_Color = programInfo.a_ColorLocation;
context.bindBuffer(context.ARRAY_BUFFER, colorBuffer);
context.enableVertexAttribArray(a_Color);
context.vertexAttribPointer(
a_Color,
4,
context.FLOAT,
false,
0,
0,
);
context.drawArrays(context.TRIANGLES, 0, numTriangles * 3);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
Drawing a bunch of random triangles every frame like you have above is arguably an exception. The majority of WebGL apps draw 3D models created in a modeling package so it's far more common to just put data in a buffer once at init time (like data for a cube, a sphere, a car, a human, a tree) and then draw it at render time.
Also be aware that GPUs can only draw so many pixels so if your triangles are large (like the size of the entire screen) you'll only be able to draw 10 to a few 100). A 1920x1080 screen is about 2 millon pixels. So every fullscreen triangle would also be about 2 million pixels. Drawing 1000 of them is 2000 * 2 million or 4 billion pixels. At 60 frames a second 240 billion pixels. A medium high end GPU can only draw 10billion a second and that's a theoretical max so at best it could do that at ~2 frames a second.
Most 3D apps draw a scene where most of the triangles are far away and small. They also use the depth buffer and draw opaque objects front to back so pixels that are in back don't get drawn.
I am trying to render two objects, with two separate gl.drawArrays call. I want to have transparent parts of the objects not be visible. Also I want to render one object on top of another so the first drawn object is not visible where it overlaps the second one.
I use this setup on my render loop:
gl.clearColor(0, 0, 0, 1);
// https://stackoverflow.com/questions/18439897/webgl-fragment-shader-opacity
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.enable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
I am not sure what the blend functions do but I use them so the transparency is enabled. But that causes two objects to blend and create a yellow colour. (One object is red, other is green). I want to have red if i draw red last, and vice versa, while having transparency enabled.
const fShaderSource2 = `#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
outColor = vec4(0.0, 1.0, 0.0, 1.0);
}
`;
const fShaderSource = `#version 300 es
precision mediump float;
out vec4 outColor;
uniform sampler2D u_texture;
void main() {
outColor = texture(u_texture, vec2(0.0));
}
`;
const vShaderSource = `#version 300 es
precision mediump float;
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
`;
main(document.getElementById('app'));
function main(element) {
const canvas = document.createElement('canvas'),
gl = canvas.getContext('webgl2');
element.append(canvas);
const displayWidth = canvas.clientWidth,
displayHeight = canvas.clientHeight;
canvas.width = displayWidth;
canvas.height = displayHeight;
let graphics = new Graphics({width: displayWidth, height: displayHeight}, gl);
new Loop(() => {
graphics.render();
}).start();
}
function Graphics(state, gl) {
const { width, height } = state;
gl.clearColor(0, 0, 0, 0);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
gl.enable(gl.BLEND);
gl.disable(gl.DEPTH_TEST);
let minibatch = [];
const redText = makeGlQuad(gl, fShaderSource, canvasTexture());
const greenText = makeGlQuad(gl, fShaderSource2);
this.render = () => {
minibatch.push(redText);
minibatch.push(greenText);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT);
minibatch.forEach(({
program,
resUniformLocation,
vao,
glTexture
}) => {
gl.useProgram(program);
gl.uniform2f(resUniformLocation, gl.canvas.width, gl.canvas.height);
if (glTexture) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, glTexture);
}
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 6);
});
minibatch = [];
};
}
function makeGlQuad(gl, fShaderSource, texture) {
let vShader = createShader(gl, gl.VERTEX_SHADER, vShaderSource);
let fShader = createShader(gl, gl.FRAGMENT_SHADER, fShaderSource);
let program = createProgram(gl, vShader, fShader);
let posAttrLocation = gl.getAttribLocation(program, "a_position");
let posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
/*
(-1, 1).( 1, 1)
.
(-1,-1).( 1,-1)
*/
let positions = [
-1, 1,
-1, -1,
1, -1,
-1, 1,
1,-1,
1, 1
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
let vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(posAttrLocation);
let size = 2,
type = gl.FLOAT,
normalize = false,
stride = 0,
offset = 0;
gl.vertexAttribPointer(posAttrLocation,
size,
type,
normalize,
stride,
offset);
let glTexture;
if (texture) {
glTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, glTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture);
//gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
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.LINEAR);
}
let resUniformLocation = gl.getUniformLocation(program, "u_resolution");
let texUniformLocation = gl.getUniformLocation(program, "u_texture");
return {
program,
resUniformLocation,
vao,
glTexture
}
}
function canvasTexture() {
return withCanvasTexture(256, 256, (w, h, canvas, ctx) => {
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, w, h);
ctx.font = '50pt Comic Sans';
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('label', w / 2, 50);
return canvas;
});
function withCanvasTexture(width, height, f) {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
f(width, height, canvas, canvas.getContext('2d'));
const texture = canvas;
//document.body.append(canvas);
return texture;
}
}
function createShader(gl, type, source) {
let shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
};
function createProgram(gl, vShader, fShader) {
let program = gl.createProgram();
gl.attachShader(program, vShader);
gl.attachShader(program, fShader);
gl.linkProgram(program);
let success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
// Loop Library
function Loop(fn) {
const perf = window.performance !== undefined ? window.performance : Date;
const now = () => perf.now();
const raf = window.requestAnimationFrame;
let running = false,
lastUpdate = now(),
frame = 0;
this.start = () => {
if (running) {
return this;
}
running = true;
lastUpdate = now();
frame = raf(tick);
return this;
};
this.stop = () => {
running = false;
if (frame != 0) {
raf.cancel(frame);
}
frame = 0;
return this;
};
const tick = () => {
frame = raf(tick);
const time = now();
const dt = time - lastUpdate;
fn(dt);
lastUpdate = time;
};
}
#app canvas {
position: fixed;
top: 50%;
bottom: 0;
left: 50%;
right: 0;
width: 100vmin;
height: 70vmin;
transform: translate(-50%, -25%);
image-rendering: optimizeSpeed;
cursor: none;
margin: auto;
}
<div id="app">
</div>
You can see here:
minibatch.push(redText);
minibatch.push(greenText);
I render red first and then green second but I get yellow instead.
The issue is that you use the wrong blend function (blendFunc). Blending defines a function which combines the fragment color outputs with the current colors in the color buffers. The fist parameter is the factor for the fragment color output and the 2nd parameter the factor for the color in the color buffer. The colors are summed, because the default blend equation (blendEquation()) is FUNC_ADD.
So the blend function
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
can be expressed by the formula
destColor = srcColor * srcAlpha + destColor * 1
where destColor is the current color in the framebuffer, srcColor is the color which is set to fragment (outColor).
This causes that the color in the current framebuffer is kept (multiplied by 1). The new color is multiplied by the alpha channel and add to the color in the framebuffer. If the color in the framebuffer is red (1, 0, 0) and the new color is green (0, 1, 0), then the result is yellow (if the alpha channel is 1):
(0, 1, 0) * 1 + (1, 0, 0) * 1 == (1, 1, 0)
Use the blend function :
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
which cause that the color in the frambuffer and the new color are "mixed", dependent on the alpha channel of the new color:
destColor = srcColor * srcAlpha + destColor * (1-srcAlpha)
The general principle of Alpha blending is similar as in OpenGL (since WebGL (2.0) conforms closely to the OpenGL ES (3.0)), so further information can be found at the OpenGL wiki page Blending.
Im trying to render simple shapes ( circles, rectangles and triangles , however, they become very pixelated when WebGL Renders them.
Shader code:
<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
// convert the rectangle 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);
}
</script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
Here is my code for rendering the circle:
var WebGLRenderer = (function () {
function WebGLRenderer() {
this.canvas = document.getElementById('canvas')
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
if (!this.gl) {
throw Error('Your browser does not support WebGL')
return
}
// Programs
this.rectangleProgram = webglUtils.createProgramFromScripts(this.gl, ['2d-vertex-shader', '2d-fragment-shader'])
// Locations
this.rectanglePoisitionLocation = this.gl.getAttribLocation(this.rectangleProgram, 'a_position')
// Uniforms
this.rectangleResolutionLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_resolution')
this.rectangleColorLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_color')
// this.positionBuffer = this.gl.createBuffer()
this.rectanglePositionBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
requestAnimationFrame(this.render.bind(this))
}
WebGLRenderer.prototype.clearCanvas = function (color) {
var rgba = color.getColor()
this.gl.clearColor(...rgba)
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
}
WebGLRenderer.prototype.drawCircle = function (x, y, radius, color) {
// Render circle
// For now user rectangleProgram
this.gl.useProgram(this.rectangleProgram)
this.gl.enableVertexAttribArray(this.rectanglePoisitionLocation)
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
this.circleBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.rectanglePositionBuffer)
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.circleBuffer)
// Setup circle
var circleVertices = [x, y]
var numFans = 360
var anglePerFan = (2 * Math.PI) / numFans
for (var i = 0; i <= numFans; i++) {
var angle = anglePerFan * (i + 1)
var angledX = x + Math.cos(angle) * radius
var angledY = y + Math.sin(angle) * radius
circleVertices.push(angledX, angledY)
// circleVertices.push()
}
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(circleVertices), this.gl.DYNAMIC_DRAW)
// this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW)
var size = 2
var type = this.gl.FLOAT
var normalize = false
var stride = 0
var offset = 0
this.gl.vertexAttribPointer(this.rectanglePoisitionLocation, size, type, normalize, stride, offset)
this.gl.uniform2f(this.rectangleResolutionLocation, this.gl.canvas.width, this.gl.canvas.height)
// Color
var colorArray = color.getColor()
this.gl.uniform4fv(this.rectangleColorLocation, colorArray)
// Draw rectangle
var primitiveType = this.gl.TRIANGLE_FAN
// var primitiveType = this.gl.POINTS
var offset = 0
var count = circleVertices.length / size
// var count = positions.length / size
this.gl.drawArrays(primitiveType, offset, count)
}
WebGLRenderer.prototype.render = function (time) {
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
var delta = Math.sin(time / 1000) * 10
this.clearCanvas(new Color(0, 0, 0, 255))
var rectangleColor = new Color(0, 65, 255, 255)
var width = 50
var height = 50
var circleColor = new Color(0, 167, 255, 255)
this.drawCircle(10, 10, 10, circleColor)
requestAnimationFrame(this.render.bind(this))
}
return WebGLRenderer
})()
function Color(r, g, b, a) {
this.r = r
this.g = g
this.b = b
this.a = a
this.getColor = function () {
return [r / 255, g / 255, b / 255, a / 255]
}
}
var renderer = new WebGLRenderer()
Results: blurry circle (everything I render with WebGL is blurry)
See fiddle for results: https://jsfiddle.net/xLwmngav/1/
Expected results: a smooth round circle
Any help is appreciated. Thank you in advance.
As is pointed out in this article canvases have 2 sizes, their resolution (how many pixels are in them) and the size they are displayed.
Generally you want the resolution to match or exceed the size the canvas is displayed. The best way to do that is to to check, just before rendering, if the canvas's resolution matches the size it's displayed and if it's not to resize it with a function like this
function resize(canvas) {
// Lookup the size the browser is displaying the canvas.
const desiredWidth = canvas.clientWidth;
const desiredHeight = canvas.clientHeight;
// Check if the canvas is not the same size.
if (canvas.width !== desiredWidth ||
canvas.height !== desiredHeight) {
// Make the canvas the same size
canvas.width = desiredWidth;
canvas.height = desiredHeight;
}
}
And use it like this
function render() {
resize(canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
... draw here ...
...
example:
function resize(canvas) {
// Lookup the size the browser is displaying the canvas.
const desiredWidth = canvas.clientWidth;
const desiredHeight = canvas.clientHeight;
// Check if the canvas is not the same size.
if (canvas.width !== desiredWidth ||
canvas.height !== desiredHeight) {
// Make the canvas the same size
canvas.width = desiredWidth;
canvas.height = desiredHeight;
}
}
var WebGLRenderer = (function () {
function WebGLRenderer() {
this.canvas = document.getElementById('canvas')
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
if (!this.gl) {
throw Error('Your browser does not support WebGL')
return
}
// Programs
this.rectangleProgram = webglUtils.createProgramFromScripts(this.gl, ['2d-vertex-shader', '2d-fragment-shader'])
// Locations
this.rectanglePoisitionLocation = this.gl.getAttribLocation(this.rectangleProgram, 'a_position')
// Uniforms
this.rectangleResolutionLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_resolution')
this.rectangleColorLocation = this.gl.getUniformLocation(this.rectangleProgram, 'u_color')
// this.positionBuffer = this.gl.createBuffer()
this.rectanglePositionBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
requestAnimationFrame(this.render.bind(this))
}
WebGLRenderer.prototype.clearCanvas = function (color) {
var rgba = color.getColor()
this.gl.clearColor(...rgba)
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
}
WebGLRenderer.prototype.drawCircle = function (x, y, radius, color) {
// Render circle
// For now user rectangleProgram
this.gl.useProgram(this.rectangleProgram)
this.gl.enableVertexAttribArray(this.rectanglePoisitionLocation)
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer)
this.circleBuffer = this.gl.createBuffer()
// this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.rectanglePositionBuffer)
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.circleBuffer)
// Setup circle
var circleVertices = [x, y]
var numFans = 360
var anglePerFan = (2 * Math.PI) / numFans
for (var i = 0; i <= numFans; i++) {
var angle = anglePerFan * (i + 1)
var angledX = x + Math.cos(angle) * radius
var angledY = y + Math.sin(angle) * radius
circleVertices.push(angledX, angledY)
// circleVertices.push()
}
/*var circleVertices = [
x, y,
15, 18,
5, 18,
0, 10,
4, 1,
14, 1,
20, 9,
15, 18
]*/
// three 2d points
// TODO: Research static draw
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(circleVertices), this.gl.DYNAMIC_DRAW)
// this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW)
var size = 2
var type = this.gl.FLOAT
var normalize = false
var stride = 0
var offset = 0
this.gl.vertexAttribPointer(this.rectanglePoisitionLocation, size, type, normalize, stride, offset)
this.gl.uniform2f(this.rectangleResolutionLocation, this.gl.canvas.width, this.gl.canvas.height)
// Color
var colorArray = color.getColor()
this.gl.uniform4fv(this.rectangleColorLocation, colorArray)
// Draw rectangle
var primitiveType = this.gl.TRIANGLE_FAN
// var primitiveType = this.gl.POINTS
var offset = 0
var count = circleVertices.length / size
// var count = positions.length / size
this.gl.drawArrays(primitiveType, offset, count)
}
WebGLRenderer.prototype.render = function (time) {
resize(this.gl.canvas);
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
var delta = Math.sin(time / 1000) * 10
this.clearCanvas(new Color(0, 0, 0, 255))
var rectangleColor = new Color(0, 65, 255, 255)
var width = 50
var height = 50
var circleColor = new Color(0, 167, 255, 255)
this.drawCircle(10, 10, 10, circleColor)
requestAnimationFrame(this.render.bind(this))
}
return WebGLRenderer
})()
function Color(r, g, b, a) {
this.r = r
this.g = g
this.b = b
this.a = a
this.getColor = function () {
return [r / 255, g / 255, b / 255, a / 255]
}
}
var renderer = new WebGLRenderer()
window.WebGLRenderer = WebGLRenderer
body {
margin: 0;
}
#canvas {
display: block; /* prevents scrollbar */
width: 100vw;
height: 100vh;
}
<canvas id="canvas"></canvas>
<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform vec2 u_resolution;
void main() {
// convert the rectangle 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);
}
</script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
</script>
<script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m3.js"></script>
The canvas defaults to a width and height of 300x150 pixels. These are attributes of the <canvas> element, not CSS properties. By scaling to 100vw by 100vh, you're just stretching those 300x150 pixels out to fill the screen.
To actually get a 1:1 mapping from canvas pixels to screen pixels, you need to set the width and height of the canvas to the size of the window:
this.canvas.width = window.innerWidth
this.canvas.height = window.innerHeight
You may also want to listen for the resize event on window and update the canvas size accordingly.
Fiddle: https://jsfiddle.net/kL1a2zpr/
I want to graph about 120k series, each of them having 64 points (down sampled, 128-512 points if using the real sampling rate, which is even larger)
I have attempted to do it with dygraph but it seems to be very slow if i use more than 1000 series.
I have attempted to use vanilla WebGL, it drew really fast, but than my problem was getting the mouse's click and than deciding which series it was - any strategies on this? (I believe its called unprojecting?) - since there are 100k+ series, using a different color for each series and than using the click coordinate's pixel's color to determine the series is impractical. Any other strategies?
My current design draws the graph as a large PNG atlas containing all the graphs, this is fast to load, but on changes on the data i have to redraw the PNG on the server and than show it again, also "unprojecting" is an issue here - any ideas on how to solve it? if possible?
The data is already quite down sampled, further down sampling will probably result in loss of details i would like to show the end-user.
Drawing 120k * 64 things means every single pixel of a 2700x2700 could be covered. In other words you're probably trying to display too much data? It's also a very large number of things and likely to be slow.
In any case drawing and picking via WebGL is relatively easy. You draw your scene using whatever techniques you want. Then, separately, when the user clicks the mouse (or always under the mouse) you draw the entire scene again to an offscreen framebuffer giving every selectable thing a different color. Given there are 32bits of color by default (8bits red, 8bits green, 8bits blue, 8bits alpha) can count 2^32-1 things. Of course with other buffer formats you could count even higher or draw to multiple buffers but storing the data for 2^32 things is probably the larger limit.
In any case here's an example. This one makes 1000 cubes (just used cubes because this sample already existed). You can consider each cube one of of your "series" with 8 points although the code is actually drawing 24 points per cube. (set primType = gl.TRIANGLES) to see the cubes. It put all the cubes in the same buffer so that a single draw call draws all the cubes. This makes it much faster than if we draw each cube with a separate draw call.
The important part is making a series ID per series. In the code below all points of one cube have the same ID.
The code draws the scene twice. Once with each cube's color, again with each cube's ID into an offscreen texture (as a framebuffer attachment). To know which cube is under the mouse we look up the pixel under the mouse, convert its color back into an ID and update that cube's vertex colors to highlight it.
const gl = document.querySelector('canvas').getContext("webgl");
const m4 = twgl.m4;
const v3 = twgl.v3;
// const primType = gl.TRIANGLES;
const primType = gl.POINTS;
const renderVS = `
attribute vec4 position;
attribute vec4 color;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_color;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_color = color;
}
`;
const renderFS = `
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;
const idVS = `
attribute vec4 position;
attribute vec4 id;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_id;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_id = id; // pass the id to the fragment shader
}
`;
const idFS = `
precision mediump float;
varying vec4 v_id;
void main() {
gl_FragColor = v_id;
}
`;
// creates shaders, programs, looks up attribute and uniform locations
const renderProgramInfo = twgl.createProgramInfo(gl, [renderVS, renderFS]);
const idProgramInfo = twgl.createProgramInfo(gl, [idVS, idFS]);
// create one set of geometry with a bunch of cubes
// for each cube give it random color (so every vertex
// that cube will have the same color) and give it an id (so
// every vertex for that cube will have the same id)
const numCubes = 1000;
const positions = [];
const normals = [];
const colors = [];
const timeStamps = [];
const ids = [];
// Save the color of each cube so we can restore it after highlighting
const cubeColors = [];
const radius = 25;
// adapted from http://stackoverflow.com/a/26127012/128511
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
const rnd = 1.;
const offset = 2. / samples;
const increment = Math.PI * (3. - Math.sqrt(5.));
// for i in range(samples):
const y = ((i * offset) - 1.) + (offset / 2.);
const r = Math.sqrt(1. - Math.pow(y ,2.));
const phi = ((i + rnd) % samples) * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return [x, y, z];
}
const addCubeVertexData = (function() {
const CUBE_FACE_INDICES = [
[3, 7, 5, 1], // right
[6, 2, 0, 4], // left
[6, 7, 3, 2], // ??
[0, 1, 5, 4], // ??
[7, 6, 4, 5], // front
[2, 3, 1, 0], // back
];
const cornerVertices = [
[-1, -1, -1],
[+1, -1, -1],
[-1, +1, -1],
[+1, +1, -1],
[-1, -1, +1],
[+1, -1, +1],
[-1, +1, +1],
[+1, +1, +1],
];
const faceNormals = [
[+1, +0, +0],
[-1, +0, +0],
[+0, +1, +0],
[+0, -1, +0],
[+0, +0, +1],
[+0, +0, -1],
];
const quadIndices = [0, 1, 2, 0, 2, 3];
return function addCubeVertexData(id, matrix, color) {
for (let f = 0; f < 6; ++f) {
const faceIndices = CUBE_FACE_INDICES[f];
for (let v = 0; v < 6; ++v) {
const ndx = faceIndices[quadIndices[v]];
const position = cornerVertices[ndx];
const normal = faceNormals[f];
positions.push(...m4.transformPoint(matrix, position));
normals.push(...m4.transformDirection(matrix, normal));
colors.push(color);
ids.push(id);
timeStamps.push(-1000);
}
}
};
}());
for (let i = 0; i < numCubes; ++i) {
const direction = fibonacciSphere(numCubes, i);
const cubePosition = v3.mulScalar(direction, radius);
const target = [0, 0, 0];
const up = [0, 1, 0];
const matrix = m4.lookAt(cubePosition, target, up);
const color = (Math.random() * 0xFFFFFF | 0) + 0xFF000000;
cubeColors.push(color);
addCubeVertexData(i + 1, matrix, color);
}
const colorData = new Uint32Array(colors);
const cubeColorsAsUint32 = new Uint32Array(cubeColors);
const timeStampData = new Float32Array(timeStamps);
// pass color as Uint32. Example 0x0000FFFF; // blue with alpha 0
function setCubeColor(id, color) {
// we know each cube uses 36 vertices. If each model was different
// we need to save the offset and number of vertices for each model
const numVertices = 36;
const offset = (id - 1) * numVertices;
colorData.fill(color, offset, offset + numVertices);
}
function setCubeTimestamp(id, timeStamp) {
const numVertices = 36;
const offset = (id - 1) * numVertices;
timeStampData.fill(timeStamp, offset, offset + numVertices);
}
// calls gl.createBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: positions,
normal: normals,
color: new Uint8Array(colorData.buffer),
// the colors are stored as 32bit unsigned ints
// but we want them as 4 channel 8bit RGBA values
id: {
numComponents: 4,
data: new Uint8Array((new Uint32Array(ids)).buffer),
},
timeStamp: {
numComponents: 1,
data: timeStampData,
},
});
const lightDir = v3.normalize([3, 5, 10]);
// creates an RGBA/UNSIGNED_BYTE texture
// and a depth renderbuffer and attaches them
// to a framebuffer.
const fbi = twgl.createFramebufferInfo(gl);
// current mouse position in canvas relative coords
let mousePos = {x: 0, y: 0};
let lastHighlightedCubeId = 0;
let highlightedCubeId = 0;
let frameCount = 0;
function getIdAtPixel(x, y, projection, view, time) {
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, fbi);
// no reason to render 100000s of pixels when
// we're only going to read one
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, 1, 1);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
drawCubes(idProgramInfo, projection, view, time);
gl.disable(gl.SCISSOR_TEST);
const idPixel = new Uint8Array(4);
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, idPixel);
// convert from RGBA back into ID.
const id = (idPixel[0] << 0) +
(idPixel[1] << 8) +
(idPixel[2] << 16) +
(idPixel[3] << 24);
return id;
}
function drawCubes(programInfo, projection, modelView, time) {
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_projection: projection,
u_modelView: modelView, // drawing at origin so model is identity
});
gl.drawArrays(primType, 0, bufferInfo.numElements);
}
function render(time) {
time *= 0.001;
++frameCount;
if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
// resizes the texture and depth renderbuffer to
// match the new size of the canvas.
twgl.resizeFramebufferInfo(gl, fbi);
}
const fov = Math.PI * .35;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1;
const zFar = 1000;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const radius = 45;
const angle = time * .2;
const eye = [
Math.cos(angle) * radius,
0,
Math.sin(angle) * radius,
];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
if (lastHighlightedCubeId > 0) {
// restore the last highlighted cube's color
setCubeColor(
lastHighlightedCubeId,
cubeColorsAsUint32[lastHighlightedCubeId]);
lastHighlightedCubeId = -1;
}
{
const x = mousePos.x;
const y = gl.canvas.height - mousePos.y - 1;
highlightedCubeId = getIdAtPixel(x, y, projection, view, time);
}
if (highlightedCubeId > 0) {
const color = (frameCount & 0x2) ? 0xFF0000FF : 0xFFFFFFFF;
setCubeColor(highlightedCubeId, color);
setCubeTimestamp(highlightedCubeId, time);
lastHighlightedCubeId = highlightedCubeId;
}
highlightedCubeId = Math.random() * numCubes | 0;
// NOTE: We could use `gl.bufferSubData` and just upload
// the portion that changed.
// upload cube color data.
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.color.buffer);
gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
// upload the timestamp
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.timeStamp.buffer);
gl.bufferData(gl.ARRAY_BUFFER, timeStampData, gl.DYNAMIC_DRAW);
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, null);
gl.enable(gl.DEPTH_TEST);
drawCubes(renderProgramInfo, projection, view, time);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getRelativeMousePosition(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
}
}
// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
target = target || event.target;
const pos = getRelativeMousePosition(event, target);
pos.x = pos.x * target.width / target.clientWidth;
pos.y = pos.y * target.height / target.clientHeight;
return pos;
}
gl.canvas.addEventListener('mousemove', (event, target) => {
mousePos = getRelativeMousePosition(event, target);
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
The code above uses an offscreen framebuffer the same size as the canvas but it uses the scissor test to only draw a single pixel (the one under the mouse). It would still run without the scissor test it would just be slower.
We could also make it work using just a single pixel offscreen framebuffer and using projection math so things work out.
const gl = document.querySelector('canvas').getContext("webgl");
const m4 = twgl.m4;
const v3 = twgl.v3;
// const primType = gl.TRIANGLES;
const primType = gl.POINTS;
const renderVS = `
attribute vec4 position;
attribute vec4 color;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_color;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_color = color;
}
`;
const renderFS = `
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;
const idVS = `
attribute vec4 position;
attribute vec4 id;
uniform mat4 u_projection;
uniform mat4 u_modelView;
varying vec4 v_id;
void main() {
gl_PointSize = 10.0;
gl_Position = u_projection * u_modelView * position;
v_id = id; // pass the id to the fragment shader
}
`;
const idFS = `
precision mediump float;
varying vec4 v_id;
void main() {
gl_FragColor = v_id;
}
`;
// creates shaders, programs, looks up attribute and uniform locations
const renderProgramInfo = twgl.createProgramInfo(gl, [renderVS, renderFS]);
const idProgramInfo = twgl.createProgramInfo(gl, [idVS, idFS]);
// create one set of geometry with a bunch of cubes
// for each cube give it random color (so every vertex
// that cube will have the same color) and give it an id (so
// every vertex for that cube will have the same id)
const numCubes = 1000;
const positions = [];
const normals = [];
const colors = [];
const timeStamps = [];
const ids = [];
// Save the color of each cube so we can restore it after highlighting
const cubeColors = [];
const radius = 25;
// adapted from http://stackoverflow.com/a/26127012/128511
// used to space the cubes around the sphere
function fibonacciSphere(samples, i) {
const rnd = 1.;
const offset = 2. / samples;
const increment = Math.PI * (3. - Math.sqrt(5.));
// for i in range(samples):
const y = ((i * offset) - 1.) + (offset / 2.);
const r = Math.sqrt(1. - Math.pow(y ,2.));
const phi = ((i + rnd) % samples) * increment;
const x = Math.cos(phi) * r;
const z = Math.sin(phi) * r;
return [x, y, z];
}
const addCubeVertexData = (function() {
const CUBE_FACE_INDICES = [
[3, 7, 5, 1], // right
[6, 2, 0, 4], // left
[6, 7, 3, 2], // ??
[0, 1, 5, 4], // ??
[7, 6, 4, 5], // front
[2, 3, 1, 0], // back
];
const cornerVertices = [
[-1, -1, -1],
[+1, -1, -1],
[-1, +1, -1],
[+1, +1, -1],
[-1, -1, +1],
[+1, -1, +1],
[-1, +1, +1],
[+1, +1, +1],
];
const faceNormals = [
[+1, +0, +0],
[-1, +0, +0],
[+0, +1, +0],
[+0, -1, +0],
[+0, +0, +1],
[+0, +0, -1],
];
const quadIndices = [0, 1, 2, 0, 2, 3];
return function addCubeVertexData(id, matrix, color) {
for (let f = 0; f < 6; ++f) {
const faceIndices = CUBE_FACE_INDICES[f];
for (let v = 0; v < 6; ++v) {
const ndx = faceIndices[quadIndices[v]];
const position = cornerVertices[ndx];
const normal = faceNormals[f];
positions.push(...m4.transformPoint(matrix, position));
normals.push(...m4.transformDirection(matrix, normal));
colors.push(color);
ids.push(id);
timeStamps.push(-1000);
}
}
};
}());
for (let i = 0; i < numCubes; ++i) {
const direction = fibonacciSphere(numCubes, i);
const cubePosition = v3.mulScalar(direction, radius);
const target = [0, 0, 0];
const up = [0, 1, 0];
const matrix = m4.lookAt(cubePosition, target, up);
const color = (Math.random() * 0xFFFFFF | 0) + 0xFF000000;
cubeColors.push(color);
addCubeVertexData(i + 1, matrix, color);
}
const colorData = new Uint32Array(colors);
const cubeColorsAsUint32 = new Uint32Array(cubeColors);
const timeStampData = new Float32Array(timeStamps);
// pass color as Uint32. Example 0x0000FFFF; // blue with alpha 0
function setCubeColor(id, color) {
// we know each cube uses 36 vertices. If each model was different
// we need to save the offset and number of vertices for each model
const numVertices = 36;
const offset = (id - 1) * numVertices;
colorData.fill(color, offset, offset + numVertices);
}
function setCubeTimestamp(id, timeStamp) {
const numVertices = 36;
const offset = (id - 1) * numVertices;
timeStampData.fill(timeStamp, offset, offset + numVertices);
}
// calls gl.createBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: positions,
normal: normals,
color: new Uint8Array(colorData.buffer),
// the colors are stored as 32bit unsigned ints
// but we want them as 4 channel 8bit RGBA values
id: {
numComponents: 4,
data: new Uint8Array((new Uint32Array(ids)).buffer),
},
timeStamp: {
numComponents: 1,
data: timeStampData,
},
});
const lightDir = v3.normalize([3, 5, 10]);
// creates an 1x1 pixel RGBA/UNSIGNED_BYTE texture
// and a depth renderbuffer and attaches them
// to a framebuffer.
const fbi = twgl.createFramebufferInfo(gl, [
{ format: gl.RGBA, type: gl.UNSIGNED_BYTE, minMag: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE, },
{ format: gl.DEPTH_STENCIL, },
], 1, 1);
// current mouse position in canvas relative coords
let mousePos = {x: 0, y: 0};
let lastHighlightedCubeId = 0;
let highlightedCubeId = 0;
let frameCount = 0;
function getIdAtPixel(x, y, projectionInfo, view, time) {
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, fbi);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
drawCubes(idProgramInfo, projectionInfo, {
totalWidth: gl.canvas.width,
totalHeight: gl.canvas.height,
partWidth: 1,
partHeight: 1,
partX: x,
partY: y,
}, view, time);
const idPixel = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, idPixel);
// convert from RGBA back into ID.
const id = (idPixel[0] << 0) +
(idPixel[1] << 8) +
(idPixel[2] << 16) +
(idPixel[3] << 24);
return id;
}
function drawCubes(programInfo, projectionInfo, partInfo, modelView, time) {
const projection = projectionForPart(projectionInfo, partInfo);
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_projection: projection,
u_modelView: modelView, // drawing at origin so model is identity
});
gl.drawArrays(primType, 0, bufferInfo.numElements);
}
function projectionForPart(projectionInfo, partInfo) {
const {fov, zNear, zFar} = projectionInfo;
const {
totalWidth,
totalHeight,
partX,
partY,
partWidth,
partHeight,
} = partInfo;
const aspect = totalWidth / totalHeight;
// corners at zNear for total image
const zNearTotalTop = Math.tan(fov) * 0.5 * zNear;
const zNearTotalBottom = -zNearTotalTop;
const zNearTotalLeft = zNearTotalBottom * aspect;
const zNearTotalRight = zNearTotalTop * aspect;
// width, height at zNear for total image
const zNearTotalWidth = zNearTotalRight - zNearTotalLeft;
const zNearTotalHeight = zNearTotalTop - zNearTotalBottom;
const zNearPartLeft = zNearTotalLeft + partX * zNearTotalWidth / totalWidth; const zNearPartRight = zNearTotalLeft + (partX + partWidth) * zNearTotalWidth / totalWidth;
const zNearPartBottom = zNearTotalBottom + partY * zNearTotalHeight / totalHeight;
const zNearPartTop = zNearTotalBottom + (partY + partHeight) * zNearTotalHeight / totalHeight;
return m4.frustum(zNearPartLeft, zNearPartRight, zNearPartBottom, zNearPartTop, zNear, zFar);
}
function render(time) {
time *= 0.001;
++frameCount;
twgl.resizeCanvasToDisplaySize(gl.canvas);
const projectionInfo = {
fov: Math.PI * .35,
zNear: 0.1,
zFar: 1000,
};
const radius = 45;
const angle = time * .2;
const eye = [
Math.cos(angle) * radius,
0,
Math.sin(angle) * radius,
];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
if (lastHighlightedCubeId > 0) {
// restore the last highlighted cube's color
setCubeColor(
lastHighlightedCubeId,
cubeColorsAsUint32[lastHighlightedCubeId]);
lastHighlightedCubeId = -1;
}
{
const x = mousePos.x;
const y = gl.canvas.height - mousePos.y - 1;
highlightedCubeId = getIdAtPixel(x, y, projectionInfo, view, time);
}
if (highlightedCubeId > 0) {
const color = (frameCount & 0x2) ? 0xFF0000FF : 0xFFFFFFFF;
setCubeColor(highlightedCubeId, color);
setCubeTimestamp(highlightedCubeId, time);
lastHighlightedCubeId = highlightedCubeId;
}
highlightedCubeId = Math.random() * numCubes | 0;
// NOTE: We could use `gl.bufferSubData` and just upload
// the portion that changed.
// upload cube color data.
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.color.buffer);
gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.DYNAMIC_DRAW);
// upload the timestamp
gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.timeStamp.buffer);
gl.bufferData(gl.ARRAY_BUFFER, timeStampData, gl.DYNAMIC_DRAW);
// calls gl.bindFramebuffer and gl.viewport
twgl.bindFramebufferInfo(gl, null);
gl.enable(gl.DEPTH_TEST);
drawCubes(renderProgramInfo, projectionInfo, {
totalWidth: gl.canvas.width,
totalHeight: gl.canvas.height,
partWidth: gl.canvas.width,
partHeight: gl.canvas.height,
partX: 0,
partY: 0,
}, view, time);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function getRelativeMousePosition(event, target) {
target = target || event.target;
const rect = target.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
}
}
// assumes target or event.target is canvas
function getNoPaddingNoBorderCanvasRelativeMousePosition(event, target) {
target = target || event.target;
const pos = getRelativeMousePosition(event, target);
pos.x = pos.x * target.width / target.clientWidth;
pos.y = pos.y * target.height / target.clientHeight;
return pos;
}
gl.canvas.addEventListener('mousemove', (event, target) => {
mousePos = getRelativeMousePosition(event, target);
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
note that drawing POINTS in WebGL is generally slower than drawing 2 TRIANGLES of the same size. If I set the number of cubes to 100k and set primType to TRIANGLES it draws 100k cubes. On my integrated GPU the snippet window it runs at about 10-20fps. Of course with that many cubes it's impossible to pick one. If I set the radius to 250 I can at least see the picking is still working.