Is there a difference between Webgl and Canvas or is it just a branch of the same?
The canvas
is an element responsible for drawing on the page. Like this drawing is made depends on the context (context) used. Currently there are two contexts available: 2d
and webgl
. Each of them is an object that exposes a different API that can be used to draw:
var ctx = document.getElementById("meucanvas").getContext("2d");
or:
var gl = document.getElementById("meucanvas").getContext("webgl");
From these returned objects, you can call methods according to the corresponding API. More details on the context 2d
and the context webgl
.
How is the development of 3D graphics using the API?
The process is fairly complex, even a "hello world" usually has several steps that need to be done to display something on the screen. I will summarize here what I know, that is not much but should serve to guide you in the search for more information:
- An element
canvas
should be created, and a webgl
should be created for it (as shown in the above code);
Some basic properties should be assigned, such as the size of the viewport, background color, and various flags which control how to render. Example:
gl.viewport( 0, 0, canvas.width, canvas.height ); // Desenha no canvas inteiro
gl.clearColor( 1.0, 1.0, 1.0, 1.0 ); // A cor de fundo é branca, opaca
gl.enable(gl.DEPTH_TEST); // Os objetos têm profundidade (i.e. é 3D, não 2D)
You need to create a "program", which is composed of code to be executed by the GPU - and not by the CPU, which is where Javascript runs. These codes ("Vertex Shader" and "Fragment Shader") are not written in Javascript, but in a language called GLSL ES, very similar to C. Thus, the browser accurate compile and then link these codes before you can use them:
var vs = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource(vs, codigoFonteDoVertexShader_emString);
gl.compileShader(vs);
var fs = gl.createShader( gl.FRAGMENT_SHADER );
gl.shaderSource(fs, codigoFonteDoFragmentShader_emString);
gl.compileShader(fs);
var program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram( program ); // Eventualmente; pode usar mais de um se quiser
The basic code (no error checking) is this. How to write these shaders, there is already more complex (I will give an example at the end of a couple of shaders super-simple, but not doing anything interesting), I suggest looking for some tutorial or course on the subject (if you want to program in "raw" Webgl, instead of using some framework that does most of the work for you).
Before you draw anything, you have to send the content you want to draw from the CPU to GPU. In Webgl this is done through special objects, called buffers. Those buffers is to store information on the vertices of their geometries, such as position, colour (if applicable), normal (if applicable), texture indices (if applicable), etc.
var triangulo = [[1,0,0], [0,1,0], [-1,0,0]];
var floats = new Float32Array(3*3);
// Na prática você vai querer fazer isso num loop
floats[0] = triangulo[0][0];
floats[1] = triangulo[0][1];
floats[2] = triangulo[0][2];
floats[3] = triangulo[1][0];
// ...
// Cria o buffer e manda os dados pra GPU
var pBuffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, pBuffer );
gl.bufferData( gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW );
// Associa esse buffer com uma variável do seu vertex shader
var vPos = gl.getAttribLocation( program, "vPosition" );
gl.vertexAttribPointer( vPos, 3, gl.FLOAT, false, 0, 0 );
gl.enableVertexAttribArray( vPos );
Finally, there is the actual drawing! You can create a function render
that will be called each time you want to draw (it may be only when something changes, if it is an interactive application, or then in a regular interval, if it is an animation or game). In it you say whatever is drawn, among what is already in the GPU:
var render = function(){
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render); // Chama de novo após um tempo X
}
render();
That’s the basic thing. As for shaders, their function is to first transform the vertices you sent to the GPU (for example, transformations such as scaling, rotating and translating are usually done in the Vertex Shader, as well as the 3D projection for 2D) and then the pixels that the GPU created from the transformed geometry (determining the color and transparency of each pixel is usually done in the Fragment Shader).
Example of shaders [almost] "trivial" placed inline in HTML itself:
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec3 vPosition; // Recebido do JavaScript
varying float z; // Enviado ao fragment shader
void main() {
z = vPosition.z;
gl_Position = vec4(vPosition, 1.0); // A coordenada w normalmente é 1.0
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying float z; // Recebido do vertex shader; interpolado pela GPU
void main() {
gl_FragColor = vec4( z, 0.0, 0.0, 1.0 );
}
</script>
(the first uses the position of the received vertices, without turning them into anything, just puts the component w
missing; the second uses the color red for all vertices of the figure, whose intensity is given by its component z
)
If on the one hand nothing comes ready, you have to do everything, on the other there is immense flexibility for you to organize your code as you want and implement the visual effects as you want. On the website shadertoy.with There are several examples of shaders that can be used for study (Beware: if your GPU is bad like mine, there’s a good chance that page will crash your screen! Here you go a simple example that I hope will not lock anyone’s browser).
Full example:
// Cria o contexto a partir do elemento canvas
var canvas = document.getElementById("meucanvas");
var gl = canvas.getContext("webgl");
// Estabelece as propriedades básicas
gl.viewport( 0, 0, canvas.width, canvas.height );
gl.clearColor( 1.0, 1.0, 1.0, 1.0 );
gl.enable(gl.DEPTH_TEST);
// Pega o texto dos shaders, como string
var vertex = document.getElementById("vertex-shader").textContent;
var fragment = document.getElementById("fragment-shader").textContent;
// Cria o programa, compilando e linkando os shaders
var vs = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource(vs, vertex);
gl.compileShader(vs);
var fs = gl.createShader( gl.FRAGMENT_SHADER );
gl.shaderSource(fs, fragment);
gl.compileShader(fs);
var program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// Usa o programa (pode ter mais de um, e usar ora um ora outros)
gl.useProgram( program );
// Cria a geometria; nesse exemplo, dois triângulos
var triangulos = [[1,0,0], [0,1,0], [-1,0,0],
[-1,0.5,-1], [-0.5,1,-1], [0.8,0.5,1]];
var floats = new Float32Array(triangulos.length*3);
for ( var i = 0 ; i < triangulos.length ; i++ )
for ( var t = 0 ; t < 3 ; t++ )
floats[3*i+t] = triangulos[i][t];
// Cria o buffer e manda os dados pra GPU
var pBuffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, pBuffer );
gl.bufferData( gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW );
// Associa esse buffer com uma variável do seu vertex shader
var vPos = gl.getAttribLocation( program, "vPosition" );
gl.vertexAttribPointer( vPos, 3, gl.FLOAT, false, 0, 0 );
gl.enableVertexAttribArray( vPos );
var render = function(){
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, triangulos.length);
//requestAnimationFrame(render); // Chama de novo após um tempo X
}
render();
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec3 vPosition;
varying float z;
void main() {
z = vPosition.z; // Salva a posição z e usa como cor
gl_Position = vec4(vPosition, 1.0);
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying float z;
void main() {
gl_FragColor = vec4( (z+1.0)/2.0, 0.0, 0.0, 1.0 );
}
</script>
<canvas id="meucanvas" width="300" height="300"></canvas>
From there, there are several paths to follow, not necessarily in a specific order:
Create more attributes for your geometry, such as different colors for each vertex. For this you would need code similar to item 4, i.e. create a new buffer with the same number of vertices of its geometry (this is important!) and associate it with a new variable in its Vertex Shader; this variable in turn would have to be passed to Fragment Shader, so that he can use it instead of a single color, fixed (in the same way as in the above example I created a variable varying z
in both shaders);
Transform the position of each arriving vertex into Vertex Shader somehow; you can start by experiencing doing simple operations with the vPosition
, but eventually you’ll want to create matrices and use them to make the 3D transformations (creating variables uniform
and setting them in render
, just before doing the draw
);
Use some more complex calculation to determine the color of each vertex, for example using some lighting model and/or textures that change colour, normal, etc;
Etc..
Here is another example, a little more elaborate (demonstrates colors, simple rotation and projection in perspective):
// Cria o contexto a partir do elemento canvas
var canvas = document.getElementById("meucanvas");
var gl = canvas.getContext("webgl");
// Estabelece as propriedades básicas
gl.viewport( 0, 0, canvas.width, canvas.height );
gl.clearColor( 1.0, 1.0, 1.0, 1.0 );
gl.enable(gl.DEPTH_TEST);
// Pega o texto dos shaders, como string
var vertex = document.getElementById("vertex-shader").textContent;
var fragment = document.getElementById("fragment-shader").textContent;
// Cria o programa, compilando e linkando os shaders
var vs = gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource(vs, vertex);
gl.compileShader(vs);
var fs = gl.createShader( gl.FRAGMENT_SHADER );
gl.shaderSource(fs, fragment);
gl.compileShader(fs);
var program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// Usa o programa (pode ter mais de um, e usar ora um ora outros)
gl.useProgram( program );
// Cria a geometria e suas cores; nesse exemplo, dois triângulos
var triangulos = [[1,0,0], [0,1,0], [-1,0,0],
[1,-1,0.2], [0,0,0.3], [-1,-1,0.2]];
var cores = [[1,0,0],[0,1,0],[0,0,1],
[1,1,0],[0,1,1],[1,0,1]];
var floats = new Float32Array(triangulos.length*3);
var floats2 = new Float32Array(triangulos.length*3);
for ( var i = 0 ; i < triangulos.length ; i++ )
for ( var t = 0 ; t < 3 ; t++ ) {
floats[3*i+t] = triangulos[i][t];
floats2[3*i+t] = cores[i][t];
}
// Cria o buffer e manda os dados pra GPU
var pBuffer = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, pBuffer );
gl.bufferData( gl.ARRAY_BUFFER, floats, gl.STATIC_DRAW );
// Associa esse buffer com uma variável do seu vertex shader
var vPos = gl.getAttribLocation( program, "vPosition" );
gl.vertexAttribPointer( vPos, 3, gl.FLOAT, false, 0, 0 );
gl.enableVertexAttribArray( vPos );
// Idem, para as cores dos vértices
var pBuffer2 = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, pBuffer2 );
gl.bufferData( gl.ARRAY_BUFFER, floats2, gl.STATIC_DRAW );
var vCor = gl.getAttribLocation( program, "vColor" );
gl.vertexAttribPointer( vCor, 3, gl.FLOAT, false, 0, 0 );
gl.enableVertexAttribArray( vCor );
// Para girar o triângulo em torno do eixo Y
var rotacaoLoc = gl.getUniformLocation( program, "rotacao" );
// Projeção em perspectiva (fonte: http://stackoverflow.com/a/30429728/520779)
function perspective(fieldOfViewYInRadians, aspect, zNear, zFar, dst) {
dst = dst || new Float32Array(16);
var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewYInRadians);
var rangeInv = 1.0 / (zNear - zFar);
dst[0] = f / aspect; dst[1] = 0; dst[2] = 0; dst[3] = 0;
dst[4] = 0; dst[5] = f; dst[6] = 0; dst[7] = 0;
dst[8] = 0; dst[9] = 0; dst[10] = (zNear + zFar) * rangeInv; dst[11] = -1;
dst[12] = 0; dst[13] = 0; dst[14] = zNear * zFar * rangeInv * 2; dst[15] = 0;
return dst;
}
var perspectivaLoc = gl.getUniformLocation( program, "u_matrix" );
// Desenha
var inicio = Date.now();
var render = function(){
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.uniformMatrix4fv(perspectivaLoc, gl.FALSE, perspective(Math.PI/8, 1, 0.1, 10));
gl.uniform1f(rotacaoLoc, -(Date.now()-inicio)/500); // Gira conforme a data
gl.drawArrays(gl.TRIANGLES, 0, triangulos.length/2); // 1º triângulo
gl.uniform1f(rotacaoLoc, -(Date.now()-inicio)/900); // Gira num ritmo diferente
gl.drawArrays(gl.TRIANGLES, triangulos.length/2, triangulos.length/2); // 2º triângulo
requestAnimationFrame(render); // Chama de novo após um tempo X
}
render();
<script id="vertex-shader" type="x-shader/x-vertex">
// uniform é o mesmo pra figura inteira
uniform float rotacao;
uniform mat4 u_matrix;
// attribute é um pra cada vértice
attribute vec3 vPosition;
attribute vec3 vColor;
// varying é um pra cada pixel (fragmento)
varying vec3 fColor;
varying float z;
void main() {
// Gira os vértices
float novoX = cos(rotacao)*vPosition.x + sin(rotacao)*vPosition.z;
float novoZ = sin(rotacao)*vPosition.x + cos(rotacao)*vPosition.z;
// Translada e projeta em 2D (recebe a projeção em perspectiva do JS)
vec4 trans = vec4(0.0, 0.0, 6.0, 0.0);
gl_Position = u_matrix * (vec4(novoX, vPosition.y, novoZ, 1.0) - trans);
// Envia dados para o fragment shader (serão interpolados)
z = novoZ*abs(novoZ);
fColor = vColor;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec3 fColor;
varying float z;
void main() {
// Quanto mais próximo da câmera, mais claro
float r = fColor.r/2.0 + z/2.0;
float g = fColor.g/2.0 + z/2.0;
float b = fColor.b/2.0 + z/2.0;
gl_FragColor = vec4(r, g, b, 1.0 );
}
</script>
<canvas id="meucanvas" width="150" height="150"></canvas>
Notice how complex it gets really fast... So in practice it may be worth using some framework like Threejs, Babylonjs, Scenejs, etc., even because you will rarely create your geometries "by hand" but rather import them from a modeling tool. These frameworks of Scene Graph help you assemble your screen, animate it, and implement key lighting, texture algorithms as well as assist in 3D transformations - at the expense of less flexibility for more advanced applications (that if you don’t know if you need it or not, you probably don’t need it).
Congratulations on the excellent content @mgibsonbr! I found the API very interesting and I want to start studying further. Do you happen to have any material you recommend for me to start studying? type published works, repositories or even tutorials, just to have as a basis... Thank you very much for the answer.
– Rafael Kendrik
Unfortunately not... If your English is good, I recently took a free course at Coursera - "Interactive Computer Graphics with Webgl" - where I learned most of what I wrote in that reply. I found it very good, but even so most of the details I [and the other students] had to run after, because it explains very well the fundamentals but not so much the API (for example, this way of creating buffers is still kind of mysterious to me). Even in English, finding a good reference that explains the API is difficult, but I keep searching rsrs!
– mgibsonbr