Related
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'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
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.
I'm drawing a wind-turbine and I'm trying to figure out how I should draw the blades so they rotate around in a circle. Initially, I'm trying to draw one blade that rotates in a circle but I'm having a hard time understanding how to do it. All mine does is go till a certain angle and then comes back...here's my code :
var t = model_transform;
model_transform = t;
var temp = Math.PI/180;
var degreeOfRot = 45;
var time = Math.sin(graphics_state.animation_time/2000); //Represents the graphics time
model_transform = mult(model_transform, translation(0,0,0));
model_transform = mult(model_transform, rotation(degreeOfRot*time, 0.0,0.0,1.0));
model_transform = mult(model_transform, scale(0.3,0.9,0));
this.shapes.box.draw(graphics_state,model_transform,this.red);
I've been looking at other answers on stack and still haven't figured it out...I feel like its something to do with the math but not sure
I think you want something like (note: I'm using a unit cube centered around the origin so there's an extra scale to make it long and an extra translation to move the origin)
// spread the blades round the circle
const rotationOffset = i / settings.numBlades * Math.PI * 2;
// rotate around Z
world = m4.multiply(world, m4.rotationZ(rotationOffset + time));
// move it away from center
world = m4.multiply(world, m4.translation([settings.xoff, settings.yoff, 0]));
// scale the unit cube to be long in Y
world = m4.multiply(world, m4.scaling([1, 10, 1]));
// the unit cube is centered around the origin. It's 1 unit big
// so this tranlation will move it's origin to the bottom left edge
world = m4.multiply(world, m4.translation([-.5, .5, 0]));
You might find this article helpful and maybe this one or this one
const vs = `
uniform mat4 u_worldViewProjection;
attribute vec4 position;
void main() {
gl_Position = u_worldViewProjection * position;
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
"use strict";
const m4 = twgl.m4;
const v3 = twgl.v3;
const gl = document.getElementById("c").getContext("webgl");
const settings = {
xoff: 0,
yoff: 0,
numBlades: 1,
};
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData for positions, texcoords
const centerBufferInfo = twgl.primitives.createSphereBufferInfo(gl, 1, 24, 12);
const bladeBufferInfo = twgl.primitives.createCubeBufferInfo(gl);
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 = 45 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.5;
const zFar = 100;
const projection = m4.perspective(fov, aspect, zNear, zFar);
const eye = [0, 0, -40];
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);
gl.useProgram(programInfo.program);
// draw center
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, centerBufferInfo);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_color: [1, 0.5, 1, 1],
u_worldViewProjection: m4.translate(viewProjection, [0, 0, 1]),
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, centerBufferInfo);
// draw blades
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bladeBufferInfo);
for (let i = 0; i < settings.numBlades; ++i) {
let world = m4.identity();
// spread the blades round the circle
const rotationOffset = i / settings.numBlades * Math.PI * 2;
// rotate around Z
world = m4.multiply(world, m4.rotationZ(rotationOffset + time));
// move it away from center
world = m4.multiply(world, m4.translation([settings.xoff, settings.yoff, 0]));
// scale the unit cube to be long in Y
world = m4.multiply(world, m4.scaling([1, 10, 1]));
// the unit cube is centered around the origin. It's 1 unit big
// so this tranlation will move it's origin to the bottom left edge
world = m4.multiply(world, m4.translation([-.5, .5, 0]));
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_color: [1, 0, 0, 1],
u_worldViewProjection: m4.multiply(viewProjection, world),
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bladeBufferInfo);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
setupSlider("#xoffSlider", "#xoff", "xoff", 10);
setupSlider("#yoffSlider", "#yoff", "yoff", 10);
setupSlider("#bladesSlider", "#blades", "numBlades", 1);
function setupSlider(sliderId, labelId, property, divisor) {
const slider = document.querySelector(sliderId);
const label = document.querySelector(labelId);
function updateLabel() {
label.textContent = settings[property];
}
slider.addEventListener('input', e => {
settings[property] = parseInt(slider.value) / divisor;
updateLabel();
});
updateLabel();
slider.value = settings[property];
}
body { margin: 0; }
canvas { display: block; width: 100vw; height: 100vh; }
#ui {
position: absolute;
left: 10px;
top: 10px;
z-index: 2;
background: rgba(255, 255, 255, 0.9);
padding: .5em;
}
<script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
<canvas id="c"></canvas>
<div id="ui">
<div><input id="xoffSlider" type="range" min="0" max="100"/><label>xoff: <span id="xoff"></span></label></div>
<div><input id="yoffSlider" type="range" min="0" max="100"/><label>yoff: <span id="yoff"></span></label></div>
<div><input id="bladesSlider" type="range" min="1" max="20"/><label>blades: <span id="blades"></span></label></div>
</div>