<canvas id="canvas" width="400" height="300"></canvas>
<script type="vertex">
attribute vec2 a_pos;
varying vec2 v_pos;
void main() {
v_pos = vec2(a_pos.xy);
gl_Position = vec4(a_pos, 0, 1);
}
</script>
<script type="fragment">
precision highp float;
varying vec2 v_pos;
void main() {
gl_FragColor = vec4(v_pos, 0.0, 1.0);
}
</script>
<script>
var canvas = document.getElementById("canvas");
var gl = canvas.getContext('experimental-webgl');
if (!gl) throw 'webgl fail';
try {
var program = gl.createProgram();
// vertex shader: pixel mapping set vec4 to gl_Position
var vertShaderObj = gl.createShader(gl.VERTEX_SHADER);
var vertexShaderSrc = document.querySelector('[type="vertex"]').textContent;
gl.shaderSource(vertShaderObj, vertexShaderSrc);
gl.compileShader(vertShaderObj);
gl.attachShader(program, vertShaderObj);
// fragment shader: pixel coloring, set vec4 to gl_FragColor
var fragShaderObj = gl.createShader(gl.FRAGMENT_SHADER);
var fragmentShaderSrc = document.querySelector('[type="fragment"]').textContent;
gl.shaderSource(fragShaderObj, fragmentShaderSrc);
gl.compileShader(fragShaderObj);
gl.attachShader(program, fragShaderObj);
gl.linkProgram(program);
gl.useProgram(program);
if (!gl.getShaderParameter(vertShaderObj, gl.COMPILE_STATUS)) throw new Error("Could not compile shader: " + gl.getShaderInfoLog(vertShaderObj));
if (!gl.getShaderParameter(fragShaderObj, gl.COMPILE_STATUS)) throw new Error("Could not compile shader: " + gl.getShaderInfoLog(fragShaderObj));
} catch(e) {
console.error(e && e.stack || e);
}
var positionLocation = gl.getAttribLocation(program, "a_pos");
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// two rectangles
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
-1.0, 1.0,
1.0, -1.0,
1.0, 1.0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);
</script>
x
and y
position of each pixel as the r
and g
value when determining the color. The colors inside the triangle are extrapolated (we only instruct the corner tips of each triangle). The result is a rectangle with gradient.type
value. The point here is to have any type that's not recognized as "JavaScript" by some browser. This allows you to put invisible multiline string content in the document. We can read out that content later. It looks better than having string concats, or even "multiline JavaScript strings". So these script tags are nothing special at all, just a generic trick to get arbitrary text content into JS.[1.0, 0.5, 1.0, 2.4]
for vec4. It's not actually an array at all though! Just think of vectors as ordered numbers. Their meaning depends on the context. Could be xyz
or rgba
or whatever. They're just numbers. Obviously there are more types, but let's save that for later.precision highp float;
, which is something I had to add to get the shader to compile. Honestly I don't know why, none of the examples I've seen used it, but whatever. I'm happy <ondras> on irc helped me figuring out that one.ctx
or even just c
for 2d canvas, in WebGL it's customary to use gl
or g
. var vertShaderObj = gl.createShader(gl.VERTEX_SHADER);
var vertexShaderSrc = document.querySelector('[type="vertex"]').textContent;
gl.shaderSource(vertShaderObj, vertexShaderSrc);
gl.compileShader(vertShaderObj);
gl.attachShader(program, vertShaderObj);
if (!gl.getShaderParameter(vertShaderObj, gl.COMPILE_STATUS)) console.error("Could not compile shader: " + gl.getShaderInfoLog(vertShaderObj));
to get more information on why a shader did not compile. For me, Firefox was much more helpful by default than Chrome was. And even with the info logs, Chrome would sometimes not report anything while Firefox would. So as far as debugging goes, Firefox wins it right now.main
functions of the shaders have no return value. I guess there's no return
keyword in GLSL, the shader language. The main
function must at some point set a variable which is picked up by WebGL. I would call that the return value for now, though of course there are many more such "live" variables you could set. Each shader has one that's most pertinent though. The vertex shader should set gl_Position
, to tell WebGL which pixel of the canvas you're paiting right now (note that this can be anywhere in the -1.0 ... 1.0
range, regardless of the input). The fragment shader must set gl_FragColor
, which sets the rgba
color of that pixel. Again, this can be anything inside the 0.0 ... 1.0
range. main
functions don't actually return a value but that updating these variables is what makes the program work. I dunno, not very intuitive from where I'm coming from. But once you know ... :)var positionLocation = gl.getAttribLocation(program, "a_pos");
. I interpret this as getting the byte offset for the a_pos
variable in the compiled program. We'll need that to set its value. As I said, I'm not entirely sure yet when you would use an attribute and when you'd use a uniform, but in this case it's an attribute. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
// two rectangles
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
-1.0, 1.0,
1.0, -1.0,
1.0, 1.0]), gl.STATIC_DRAW);
-1
to 1
, top-left to bottom-right (IIRC). So the 12 values above are actually six vertex coordinates (<-1, -1>, <1, -1>, <-1, 1>, <-1, 1>, <1, -1>, <1, 1>
) which make up for two times three corners of a triangle, covering the entire viewport. The STATIC_DRAW
is an optimizer argument, not relevant for us here. gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
a_pos
, which was a vec2
so it fits two floats. The second line tells WebGL to step through the buffer two elements at a time and interpret them as floats.gl.drawArrays(gl.TRIANGLES, 0, 6);
, is where the painting happens. It tells WebGL to paint triangles. This model will consume three vectors per triangle. It should start at index 0
in the buffer and run up to but not including index 6
. There are a few possible types you can do here besides TRIANGLES
. For example, TRIANGLE_FAN
draws triangles but each vector after the first three uses the last two vectors to create another triangle (so you get a fan of them). POINTS
will draw the vectors as single pixels. Etc.drawArrays
method will start running through the buffer we've prepared. It'll take three x,y
coordinates from the buffer and paint a triangle with them. When it does it will execute the main
function of each shader for each of the coordinates. The vertex shader only passes on this coordinate to gl_Position
directly (setting the z
to 0.0
and w
to 1.0
). It also puts the coordinate into a varying
variable so the fragment shader can use it.gl_FragColor
to an rgba
value by using the vec2
coordinates and creating a vec4
from them, which acts as an rgba
value (r=x, g=y, b=0, a=1
).
<canvas id="B"></canvas>
<script>
var w, h = w = Math.pow(2, 9);
var leftCanvas = document.querySelector('#B');
leftCanvas.width = leftCanvas.height = w;
var ctx = leftCanvas.getContext('2d');
var r = 0;
(function yy() {
requestAnimationFrame(yy);
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, leftCanvas.width, leftCanvas.height);
ctx.save();
ctx.beginPath();
ctx.translate(250, 250);
ctx.rotate((++r * 0.05) % 2*Math.PI);
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 75, 75);
ctx.restore();
})();
</script>
uniform sampler2D u_tex;
. The sampler2D
type allows us to put a texture there. In the vertex shader we'll add gl_PointSize = 1.0;
which affects the sample size, and therefor quality. The fragment shader now "outputs" it's result by calling a sampler function: gl_FragColor = texture2D(uSampler, v_pos);
. The rest remains the same so our shaders now look like this:<script type="vertex">
attribute vec2 a_pos;
varying vec2 v_pos;
void main() {
v_pos = vec2(a_pos.xy);
gl_Position = vec4(a_pos.xy, 0.0, 1.0);
gl_PointSize = 1.0;
}
</script>
<script type="fragment">
precision highp float;
varying vec2 v_pos;
uniform sampler2D uSampler;
void main() {
gl_FragColor = texture2D(uSampler, v_pos);
}
</script>
var n = 0;
var arr = [];
for (var i=0; i<512*512*2; i+=2) {
arr[ i] = (Math.floor(++n%512) / 256) - 1;
arr[i+1] = (Math.round(n/512) / 256) - 1;
}
var farr = new Float32Array(arr);
var positionLocation = gl.getAttribLocation(program, 'a_pos');
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, farr, gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
(function xx() {
requestAnimationFrame(xx);
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, leftCanvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.uniform1i(gl.getUniformLocation(program, 'u_tex'), 0);
// draw
gl.drawArrays(gl.POINTS, 0, arr.length/2);
tex.deallocate();
512*512*2
floats. (You should cache that multiplication ;)) For each pixel, create the corresponding x,y
coordinates on the texture (the canvas) normalized to -1.0 ... 1.0
. Stash this array in a Float32Array
for speed and typing.a_pos
to these coordinates like we have before. We tell it each such coordinate consists of two floats. If we were using 3d space we could add another float to the buffer and tell it to use three floats instead. But since we're using a 2d canvas, there's no need.requestAnimationFrame
for creating the animation. var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, leftCanvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.uniform1i(gl.getUniformLocation(program, 'u_tex'), 0);
tex.deallocate();
, after painting, because otherwise you'll blow out your GPU (*I mean crash, nothing permanent*). drawArrays
call now gets a gl.POINTS
parameter. So each coordinate in our buffer is painted as a single pixel.
var n = 0;
var arr = [];
for (var i = 0; i < source.width * source.height * 2; i += 2) {
arr[i ] = (Math.floor(++n % source.width) / source.width) * 2 - 1;
arr[i + 1] = (Math.round(n / source.width) / source.height) * 2 - 1;
}
var farr = new Float32Array(arr);
0 .. 1
range to -1 .. 1
. This means we're also reading that range on the texture. That's causing the quadruple image. So for the texture we'll need to undo that part conversion. We'll just be lazy and do it in the shader...<script type="vertex">
attribute vec2 a_pos;
varying vec2 v_pos;
void main() {
v_pos = (a_pos - 1.0) * 0.5;
gl_Position = vec4(a_pos, 0.0, 1.0);
gl_PointSize = 1.0;
}
</script>
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
to fix that: var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
precision highp float;
uniform float u_barrel_power;
vec2 distort(vec2 p)
{
float theta = atan(p.y, p.x);
float radius = length(p);
radius = pow(radius, u_barrel_power);
p.x = radius * cos(theta);
p.y = radius * sin(theta);
return 0.5 * (p + 1.0);
}
varying vec2 v_pos;
uniform sampler2D u_tex;
void main() {
gl_FragColor = texture2D(u_tex, distort(v_pos));
}
var barrelPowerLocation = gl.getUniformLocation(program, "BarrelPower"); // gl.getAttribLocation(program, "BarrelPower");
gl.uniform1f(barrelPowerLocation, 1.5);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
), but none seem to be able to set a default color in case of an OOB so we'll just do it manually in the shader instead:void main() {
vec2 d = distort(v_pos);
if (d.x > 1.0 || d.x < 0.0 || d.y > 1.0 || d.y < 0.0) gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // white
else gl_FragColor = texture2D(u_tex, d);
}