Moisés Gabriel Cachay Tello
Creator, destructor.
@xpktro - LimaJS
Hoy avanzaremos un paso más en nuestro trayecto como programadores de gráficos...
Haciendo un cuadrado.
Y aún más allá transformándolo.
Primero vamos a revisitar nuestro código y lo prepararemos para este y los siguientes talleres:
class WebGLGraph {
constructor(canvas) { ... }
createAndCompileShaders() { ... }
createAndLinkProgram() { ... }
setVAO() { ... }
setArrayBuffers() { ... }
draw() { ... }
}
constructor(canvas) {
if(canvas) {
this.canvas = canvas;
} else {
this.canvas = document.createElement('canvas');
document.body.appendChild(this.canvas);
}
this.gl = this.canvas.getContext('webgl2');
if(!this.gl) {
alert('Webgl2 Unsupported');
return;
}
this.createAndCompileShaders();
this.createAndLinkProgram();
this.setVAO();
this.setArrayBuffers();
this.draw();
}
createAndCompileShaders() {
this.VERTEX_SHADER_SOURCE = `...`;
this.FRAGMENT_SHADER_SOURCE = `...`;
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
this.gl.shaderSource(this.vertexShader, this.VERTEX_SHADER_SOURCE);
this.gl.shaderSource(this.fragmentShader, this.FRAGMENT_SHADER_SOURCE);
this.gl.compileShader(this.vertexShader);
this.gl.compileShader(this.fragmentShader);
}
createAndLinkProgram() {
this.program = this.gl.createProgram();
this.gl.attachShader(this.program, this.vertexShader);
this.gl.attachShader(this.program, this.fragmentShader);
this.gl.linkProgram(this.program);
}
setVAO() {
this.vao = this.gl.createVertexArray();
this.gl.bindVertexArray(this.vao);
}
setArrayBuffer(attribute, size, contents) {
const buffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array(contents),
this.gl.STATIC_DRAW
);
const attributeLocation = this.gl.getAttribLocation(this.program, attribute);
this.gl.vertexAttribPointer(attributeLocation, size, this.gl.FLOAT, false, 0, 0);
this.gl.enableVertexAttribArray(attributeLocation);
return attributeLocation;
}
setArrayBuffers() {
this.positionLocation = this.setArrayBuffer('position', 2, [
0, 0.5,
0.5, -0.5,
-0.5, -0.5
]);
this.colorLocation = this.setArrayBuffer('color', 3, [
0, 0, 1,
0, 1, 0,
1, 0, 0
]);
}
draw() {
const cssToRealPixels = window.devicePixelRatio || 1;
this.canvas.width = Math.floor(this.canvas.clientWidth * cssToRealPixels);
this.canvas.height = Math.floor(this.canvas.clientHeight * cssToRealPixels);
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
this.gl.clearColor(0, 0, 0, 1.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
this.gl.useProgram(this.program);
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
}
const graph = new WebGLGraph();
const graph = new WebGLGraph();
setArrayBuffers() {
this.positionLocation = this.setArrayBuffer('position', 2, [
-0.5, 0.5,
0.5, 0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, -0.5,
0.5, -0.5
]);
this.colorLocation = this.setArrayBuffer('color', 3, [
0, 0, 1,
0, 1, 0,
1, 0, 0,
0, 0, 1,
1, 1, 1,
1, 0, 0
]);
}
Y ahora: Cómo dibujar un cuadrado
+
=
El siguiente paso en nuestra aventura consiste en aprender el significado de geometría vs transformaciones:
Geometría: descripción de la forma del objeto a dibujar
Transformaciones: operaciones que alteran la geometría
La transformación más básica es la de la traslación.
this.VERTEX_SHADER_SOURCE = `#version 300 es
in vec4 position;
in vec4 color;
out vec4 v_color;
uniform vec4 translation;
void main() {
gl_Position = position + translation;
v_color = color;
}
`;
¿Qué es un uniform?
constructor() {
...
this.transX = 0.0;
this.transY = 0.0;
this.draw();
}
set4fvUniform(attribute, contents) {
const uniformLocation = this.gl.getUniformLocation(this.program, attribute);
this.gl.uniform4fv(uniformLocation, contents);
return uniformLocation;
}
setUniforms() {
this.set4fvUniform('translation', [this.transX, this.transY, 0, 0]);
}
draw() {
...
this.gl.useProgram(this.program);
this.setUniforms();
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}
gl.uniform1f (floatUniformLoc, v); // for float
gl.uniform1fv(floatUniformLoc, [v]); // for float or float array
gl.uniform2f (vec2UniformLoc, v0, v1); // for vec2
gl.uniform2fv(vec2UniformLoc, [v0, v1]); // for vec2 or vec2 array
gl.uniform3f (vec3UniformLoc, v0, v1, v2); // for vec3
gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // for vec3 or vec3 array
gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // for vec4
gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // for vec4 or vec4 array
gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // for mat2 or mat2 array
gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // for mat3 or mat3 array
gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ]) // for mat4 or mat4 array
gl.uniform1i (intUniformLoc, v); // for int
gl.uniform1iv(intUniformLoc, [v]); // for int or int array
gl.uniform2i (ivec2UniformLoc, v0, v1); // for ivec2
gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // for ivec2 or ivec2 array
gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // for ivec3
gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // for ivec3 or ivec3 array
gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // for ivec4
gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // for ivec4 or ivec4 array
gl.uniform1u (intUniformLoc, v); // for uint
gl.uniform1uv(intUniformLoc, [v]); // for uint or uint array
gl.uniform2u (ivec2UniformLoc, v0, v1); // for uvec2
gl.uniform2uv(ivec2UniformLoc, [v0, v1]); // for uvec2 or uvec2 array
gl.uniform3u (ivec3UniformLoc, v0, v1, v2); // for uvec3
gl.uniform3uv(ivec3UniformLoc, [v0, v1, v2]); // for uvec3 or uvec3 array
gl.uniform4u (ivec4UniformLoc, v0, v1, v2, v4); // for uvec4
gl.uniform4uv(ivec4UniformLoc, [v0, v1, v2, v4]); // for uvec4 or uvec4 array
// for sampler2D, sampler3D, samplerCube, samplerCubeShader, sampler2DShadow,
// sampler2DArray, sampler2DArrayShadow
gl.uniform1i (samplerUniformLoc, v);
gl.uniform1iv(samplerUniformLoc, [v]);
La segunda transformación que veremos será la rotación.
this.VERTEX_SHADER_SOURCE = `#version 300 es
in vec4 position;
in vec4 color;
out vec4 v_color;
uniform vec4 translation;
uniform float rotation;
void main() {
float rotation_rad = rotation * 3.141592 / 180.0;
vec4 rotatedPosition = vec4(
position.x * cos(rotation_rad) + position.y * sin(rotation_rad),
position.x * -sin(rotation_rad) + position.y * cos(rotation_rad),
0,
1
);
gl_Position = rotatedPosition + translation;
v_color = color;
}
`;
@_@
constructor() {
...
this.rotation = 0.0;
this.draw();
}
set1fUniform(attribute, contents) {
const uniformLocation = this.gl.getUniformLocation(this.program, attribute);
this.gl.uniform1f(uniformLocation, contents);
return uniformLocation;
}
setUniforms() {
this.set4fvUniform('translation', [this.transX, this.transY, 0, 0]);
this.set1fUniform('rotation', this.rotation);
}
El último tipo de transformación de hoy será el escalamiento.
this.VERTEX_SHADER_SOURCE = `#version 300 es
in vec4 position;
in vec4 color;
out vec4 v_color;
uniform vec4 translation;
uniform float rotation;
uniform vec4 scale;
void main() {
float rotation_rad = rotation * 3.141592 / 180.0;
vec4 rotated_position = vec4(...);
gl_Position = (rotated_position + translation) * scale;
v_color = color;
}
`;
constructor() {
...
this.scaleX = 1.0;
this.scaleY = 1.0;
this.draw();
}
setUniforms() {
this.set4fvUniform('translation', [this.transX, this.transY, 0, 0]);
this.set1fUniform('rotation', this.rotation);
this.set4fvUniform('scale', [this.scaleX, this.scaleY, 1, 1]);
}
El orden de las operaciones importa, y modificar los shaders antes de cada draw() es demasiado costoso.
Qué tal si te digo que no necesitamos tocar los shaders para efectuar transformaciones complejas
Traslación matricial
Rotación matricial
Escalamiento matricial
¿Cómo esto es mejor que hacerlo linealmente?
gl_Position = \(\begin{bmatrix}x & y & 1\end{bmatrix} \times transfmat\)
this.VERTEX_SHADER_SOURCE = `#version 300 es
in vec2 position;
in vec4 color;
out vec4 v_color;
uniform mat3 matrix;
void main() {
gl_Position = vec4((matrix * vec3(position, 1)).xy, 0, 1);
v_color = color;
}
`;
draw() {
...
this.setUniforms();
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}
setUniforms() {
this.setMatrix3Uniform('matrix', this.getTransformationMatrix());
}
getTransformationMatrix() {
let translationMatrix = mat3.fromTranslation(mat3.create(), [this.transX, this.transY]);
let rotationMatrix = mat3.fromRotation(mat3.create(), glMatrix.toRadian(this.rotation));
let scaleMatrix = mat3.fromScaling(mat3.create(), [this.scaleX, this.scaleY]);
let transformationMatrix = mat3.multiply(mat3.create(), translationMatrix, rotationMatrix);
transformationMatrix = mat3.multiply(mat3.create(), transformationMatrix, scaleMatrix)
return transformationMatrix;
}
He decidido detenerme a explicar un poco más de fundamentos para dar una dimensión más técnica a los fundamentos de la programación de gráficos.
Ejercicio propuesto: animar posición, rotación y escala automáticamente (lo solucionamos en la siguiente charla)
Un laboratorio de Google cuyo video relacionado se puede ver aquí.
ShaderToy - Sandbox de creación de fragment shaders muy completo.
Vertex Shader Art - Sandbox de creación de vertex shaders.
GLSL Shader - Sandbox de creación de fragment shaders básico.
By Moisés Gabriel Cachay Tello