Related
Is there a way to rewrite this code in order to control rotation of the model by user input?
This is the WebGL code I use:
function render(time) {
time *= 0.001;
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
const fieldOfViewRadians = degToRad(60);
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const projection = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
const up = [0, 1, 0];
const camera = m4.lookAt(cameraPosition, cameraTarget, up);
const view = m4.inverse(camera);
const sharedUniforms = {
u_lightDirection: m4.normalize([-1, 3, 5]),
u_view: view,
u_projection: projection,
u_viewWorldPosition: cameraPosition,
};
gl.useProgram(meshProgramInfo.program);
webglUtils.setUniforms(meshProgramInfo, sharedUniforms);
let u_world = m4.yRotation(time*0.3);
u_world = m4.translate(u_world, ...objOffset);
for (const { bufferInfo, material } of parts) {
webglUtils.setBuffersAndAttributes(gl, meshProgramInfo, bufferInfo);
webglUtils.setUniforms(meshProgramInfo, {
u_world,
}, material);
webglUtils.drawBufferInfo(gl, bufferInfo);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
I tried this code but nothing works, the model just disappears.
var rect = gl.canvas.getBoundingClientRect();
var x = (ev.clientX - rect.left) / canvas.width * 2 - 1;
var y = (ev.clientY - rect.top) / canvas.height * -2 + 1;
So i just need to replace time variable with another one that catches position of user's mouse when drag the model.
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.
Since there is suprisingly almost no information on webGL (or I just don't know how to search for it), I have a question about how to transform a mouse coordinates to 3D coordinates, so to see where exactly on the screen I am clicking.
So my case is that I have a very simple skybox, the camera is positioned at [0, 0, 0] and I can look around it by clicking and dragging. What I want to do is be able to click somewhere on that skybox and know where I have clicked as I need to put an annotation (some text, or html element) on that position. And that html element must move and go out of view with me turning to another side. So what I need is a way to get a mouse click and find out which side of the cube I am clicking on and at what coordinates, so I can place the annotations correctly.
I am using a plain WebGL, I don't use THREE.js or anything like that. Since its just one cube, I can only assume finding the intersection won't be that hard and won't require extra libraries.
Well you're certainly right that it's hard to find an example ðŸ˜
A common webgl shader projects in 3D using code like either
gl_Position = matrix * position;
or
gl_Position = projection * modelView * position;
or
gl_Position = projection * view * world * position;
which are all the same thing basically. They take position and multiply it by a matrix to convert to clip space. You need to do the opposite to go the other way, take a position in clip space and covert back to position space which is
inverse (projection * view * world) * clipSpacePosition
So, take your 3D library and compute the inverse of the matrix you're passing to WebGL. For exmaple here is some code that is computing matrices to draw something using twgl's math library
const fov = 30 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.5;
const zFar = 10;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [1, 4, -6];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
const world = m4.rotationY(time);
For a shader that is effectively doing this
gl_Position = viewProjection * world * position
So we need the inverse
const invMat = m4.inverse(m4.multiply(viewProjection, world));
Then we need a clip space ray. We're going from 2D to 3D so we'll make a ray that cuts through the frustum starting at zNear and ending at zFar by using -1 and +1 as our Z value
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const clipX = x / rect.width * 2 - 1;
const clipY = y / rect.height * -2 + 1;
const start = m4.transformPoint(invMat, [clipX, clipY, -1]);
const end = m4.transformPoint(invMat, [clipX, clipY, 1]);
... do something with start/end
});
start and end are now relative to position (the data in your geometry) so you now have to use some ray to triangle code in JavaScript to walk through all your triangles and see if the ray from start to end intersecs one or more of your triangles.
Note if all you want is a ray in world space, not position space then you'd use
const invMat = m4.inverse(viewProjection);
"use strict";
const vs = `
uniform mat4 u_world;
uniform mat4 u_viewProjection;
attribute vec4 position;
attribute vec2 texcoord;
attribute vec4 color;
varying vec4 v_position;
varying vec2 v_texcoord;
varying vec4 v_color;
void main() {
v_texcoord = texcoord;
v_color = color;
gl_Position = u_viewProjection * u_world * position;
}
`;
const fs = `
precision mediump float;
varying vec2 v_texcoord;
varying vec4 v_color;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord) * v_color;
}
`;
const m4 = twgl.m4;
const gl = document.querySelector("#c").getContext("webgl");
// compiles shaders, links, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const cubeArrays = twgl.primitives.createCubeVertices(1);
cubeArrays.color = {value: [0.2, 0.3, 1, 1]};
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
// for each array
const cubeBufferInfo = twgl.createBufferInfoFromArrays(gl, cubeArrays);
const numLines = 50;
const positions = new Float32Array(numLines * 3 * 2);
const colors = new Float32Array(numLines * 4 * 2);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
// for each array
const linesBufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: positions,
color: colors,
texcoord: { value: [0, 0], },
});
const tex = twgl.createTexture(gl, {
minMag: gl.NEAREST,
format: gl.LUMINANCE,
src: [
255, 192,
192, 255,
],
});
let clipX = 0;
let clipY = 0;
let lineNdx = 0;
function render(time) {
time *= 0.001;
twgl.resizeCanvasToDisplaySize(gl.canvas);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const fov = 30 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 1;
const zFar = 10;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [Math.cos(time), Math.sin(time), 6];
const target = [0, 0, 0];
const up = [0, 1, 0];
const camera = m4.lookAt(eye, target, up);
const view = m4.inverse(camera);
const viewProjection = m4.multiply(projection, view);
const world = m4.rotateX(m4.rotationY(1), 1);
gl.useProgram(programInfo.program);
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, cubeBufferInfo);
twgl.setUniformsAndBindTextures(programInfo, {
tex,
u_world: world,
u_viewProjection: viewProjection,
color: [0.2, 0.3, 1, 1],
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, cubeBufferInfo);
// add a line in world space
const invMat = m4.inverse(viewProjection);
const start = m4.transformPoint(invMat, [clipX, clipY, -1]);
const end = m4.transformPoint(invMat, [clipX, clipY, 1]);
const poffset = lineNdx * 3 * 2;
const coffset = lineNdx * 4 * 2;
const color = [Math.random(), Math.random(), Math.random(), 1];
positions.set(start, poffset);
positions.set(end, poffset + 3);
colors.set(color, coffset);
colors.set(color, coffset + 4);
gl.bindBuffer(gl.ARRAY_BUFFER, linesBufferInfo.attribs.position.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, positions);
gl.bindBuffer(gl.ARRAY_BUFFER, linesBufferInfo.attribs.color.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);
lineNdx = (lineNdx + 1) % numLines;
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, linesBufferInfo);
twgl.setUniformsAndBindTextures(programInfo, {
tex,
u_world: m4.identity(),
u_viewProjection: viewProjection,
color: [1, 0, 0, 1],
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, linesBufferInfo, gl.LINES);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
gl.canvas.addEventListener('mousemove', (e) => {
const canvas = gl.canvas;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
clipX = x / rect.width * 2 - 1;
clipY = y / rect.height * -2 + 1;
});
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas id="c"></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
As for WebGL info there is some here
I'm currently building a 2D drawing app in WebGL. I want to implement zoom to point under mouse cursor similar to example in here. But I can't figure out how to apply the solution from that answer in my case.
I have done basic zoom by scaling camera matrix. But it zooms to the top-left corner of the canvas, due to that being the origin (0,0) set by the projection (as far as I understand).
Basic pan & zoom implemented:
My draw function (including matrix computations) looks like this:
var projection = null;
var view = null;
var viewProjection = null;
function draw(gl, camera, sceneTree){
// projection matrix
projection = new Float32Array(9);
mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight);
// camera matrix
view = new Float32Array(9);
mat3.fromTranslation(view, camera.translation);
mat3.rotate(view, view, toRadians(camera.rotation));
mat3.scale(view, view, camera.scale);
// view matrix
mat3.invert(view, view)
// VP matrix
viewProjection = new Float32Array(9);
mat3.multiply(viewProjection, projection, view);
// go through scene tree:
// - build final matrix for each object
// e.g: u_matrix = VP x Model (translate x rotate x scale)
// draw each object in scene tree
// ...
}
Vertex shader:
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
Zoom function:
function screenToWorld(screenPos){
// normalized screen position
let nsp = [
2.0 * screenPos[0] / this.gl.canvas.width - 1,
- 2.0 * screenPos[1] / this.gl.canvas.height + 1
];
let inverseVP = new Float32Array(9);
mat3.invert(inverseVP, viewProjection);
let worldPos = [0, 0];
return vec2.transformMat3(worldPos, nsp, inverseVP);
}
var zoomRange = [0.01, 2];
canvas.addEventListener('wheel', (e) => {
let oldZoom = camera.scale[0];
let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]);
camera.scale = [zoom, zoom];
let zoomPoint = screenToWorld([e.clientX, e.clientY]);
// totally breaks if enable this line
//vec2.copy(camera.translation, zoomPoint);
// call draw function again
draw();
}, false);
If I apply zoomPoint to camera translation, the values of zoomPoint (and the camera position accordingly) start to raise up uncontrollably with every zoom event (no mater if I zoom in or out) and the objects drawn in the scene go immediately out of view.
Would greatly appreciate any insights or suggestions about what am I doing wrong here. Thanks.
Since you didn't post a minimal reproducible example in the question itself I couldn't test with your math library. Using my own though I was able to zoom like this
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
Note that zoom is the opposite of scale. If zoom = 2 then I want everything to appear 2x larger. To do that requires shrinking the camera space so we scale that space by 1 / zoom
Example:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const vs = `
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
a_position: {
numComponents: 2,
data: [
0, 0, // 0----1
40, 0, // | |
40, 10, // | 3--2
10, 10, // | |
10, 20, // | 4-5
30, 20, // | |
30, 30, // | 7-6
10, 30, // | |
10, 50, // 9-8
0, 50,
],
},
indices: [
0, 1, 2,
0, 2, 3,
0, 3, 8,
0, 8, 9,
4, 5, 6,
4, 6, 7,
],
});
const camera = {
x: 0,
y: 0,
rotation: 0,
zoom: 1,
};
const scene = [
{ x: 20, y: 20, rotation: 0, scale: 1, color: [1, 0, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: Math.PI, scale: 0.5, color: [0, 0.5, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: 0, scale: 2, color: [0, 0, 1, 1], bufferInfo},
{ x: 200, y: 100, rotation: 0.7, scale: 1, color: [1, 0, 1, 1], bufferInfo},
];
let viewProjectionMat;
function makeCameraMatrix() {
const zoomScale = 1 / camera.zoom;
let cameraMat = m3.identity();
cameraMat = m3.translate(cameraMat, camera.x, camera.y);
cameraMat = m3.rotate(cameraMat, camera.rotation);
cameraMat = m3.scale(cameraMat, zoomScale, zoomScale);
return cameraMat;
}
function updateViewProjection() {
// same as ortho(0, width, height, 0, -1, 1)
const projectionMat = m3.projection(gl.canvas.width, gl.canvas.height);
const cameraMat = makeCameraMatrix();
let viewMat = m3.inverse(cameraMat);
viewProjectionMat = m3.multiply(projectionMat, viewMat);
}
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
updateViewProjection();
gl.useProgram(programInfo.program);
for (const {x, y, rotation, scale, color, bufferInfo} of scene) {
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
let mat = m3.identity();
mat = m3.translate(mat, x, y);
mat = m3.rotate(mat, rotation);
mat = m3.scale(mat, scale, scale);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_matrix: m3.multiply(viewProjectionMat, mat),
u_color: color,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
}
}
draw();
function getClipSpaceMousePosition(e) {
// get canvas relative css position
const rect = canvas.getBoundingClientRect();
const cssX = e.clientX - rect.left;
const cssY = e.clientY - rect.top;
// get normalized 0 to 1 position across and down canvas
const normalizedX = cssX / canvas.clientWidth;
const normalizedY = cssY / canvas.clientHeight;
// convert to clip space
const clipX = normalizedX * 2 - 1;
const clipY = normalizedY * -2 + 1;
return [clipX, clipY];
}
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
draw();
});
canvas { border: 1px solid black; display: block; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m3.js"></script>
note that I included camera.rotation just to make sure things worked if rotated. They seem to. Here's one with zoom, pan, and rotate
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.