Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I set the color of a pixel on a canvas using WebGL?

I am very new to WebGL, but not Javascript or Canvas. Essentially, I need a very fast way to be able to change the color of a pixel on a canvas at any coordinate. I feel like this could be done using WebGL fragment shaders. Is this possible, and is there a better way? How would I go about doing this?

like image 752
nondefault Avatar asked Jun 17 '12 20:06

nondefault


2 Answers

This isn't an exhaustive guide to rendering because it doesn't include the bits about shader management; instead it focuses on how drawing of WebGL primitives is actually performed, and how to use orthographic projection to precisely position vertices on the screen.

For more information, I highly recommend these sources:

  • Learning WebGL - tutorials
  • The WebGL specification
  • The Typed Arrays specification

WebGL does all of its drawing with Vertex Buffer Objects (VBOs), so you need to compile all your data into 3D vertices and send them down to the video card in as few VBOs as possible (to maximize performance).

A VBO can be created in WebGL like so:

var vbo = gl.createBuffer();

Then you need to send the data down to the buffer, usually in the form of a Float32Array. If you already have your data in the form of a regular JavaScript array, then you can use new Float32Array(jsArray) to initialize the Float32Array from the jsArray's contents. Try to do this rarely though. Typed arrays are very fast, but initialize very slowly. It's best to create them once and reuse them whenever possible.

Once you have a Float32Array, you can pass the data down to the buffer like so:

gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, float32Data, gl.DYNAMIC_DRAW);

You'll need to perform a bufferData or bufferSubData call every time the data changes.

By this time the data is on the graphics card, so you only need to actually draw it:

// bind the VBO to be drawn if you haven't already
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// send the vertex data to the shader program
gl.vertexAttribPointer(vertexAttributeLocation, 3, gl.FLOAT, false, 0, 0);
// draw the buffer
gl.drawArrays(gl.POINTS, 0, float32Data.length / 3);

Note that although the graphics card can hold quite a few VBOs, you should use as few of them as possible, because the more drawArrays calls you have to perform, the slower things are going to get. Indeed, if you render only a single pixel at a time in a given VBO, it's going to be too slow to run.

The reason the length of the Float32Array is divided by 3 is because each single data element is a 3D (X, Y, Z) coordinate, so each data element consists of 3 float components. Note that the first argument to gl.drawArrays was gl.POINTS. This instructs the graphics card to draw a single point (by default, a single pixel in size) for each item in the array. There are other ways to draw, and if you need to fill a group of pixels, one of the other draw modes (e.g. gl.TRIANGLES) may be more to your liking.

As for lighting up specific pixels, it depends on how the shader is written, but most likely you're making use of a modelview matrix and a projection matrix. The modelview matrix represents the orientation of the camera relative to the points being drawn, and the projection matrix represents the camera dimensions (width and height, field of view, nearest and furthest visible ranges, etc). So if you want to light specific pixels, your best bet is to apply an orthographic projection matrix with a width and height equal to the width and height of the canvas; and a modelview matrix set to the identity (no transformation). In an orthographic projection, objects don't shrink as they get further away from the camera, so they're very helpful for specifying exact positions relative to the screen. Also, if you give them the proper dimensions, you can position vertices very precisely -- at specific pixels, if you wish.

Different matrix libraries work in different ways, but for example, to set up an orthographic matrix in gl-matrix, you can do so like this:

var ortho = mat4.ortho(left, right, bottom, top, near, far);

The exact numbers depend on your preference; if you'd like to place the origin (0, 0) at the bottom-left of the canvas, you'd do so like this:

var ortho = mat4.ortho(0, canvas.width, 0, canvas.height, 0.01, 200);

Note that the Z value of each point still has to lie between near and far in order to be rendered, however, and the near value cannot be set to 0.

Let me know if any of this needs clarifying.

like image 145
sinisterchipmunk Avatar answered Oct 22 '22 02:10

sinisterchipmunk


If you just want to draw single pixels you're probably better off using canvas 2d.

Otherwise you can probably figure it out from this tutorial which draws rectangles in pixel units so set the width and height to 1x1 and you'll get single pixels.

http://games.greggman.com/game/webgl-fundamentals/

like image 31
gman Avatar answered Oct 22 '22 03:10

gman