Basics parte I

Bienvenidos y bienvenidas!

La idea de este blog es ir volcando resúmenes de lo que voy aprendiendo en el curso de Three.js de Bruno Simon para hacerlo un poco más accesible para todos y todas y que puedan aprender sobre animaciones. Mientras tanto, también pueden ver contenido teórico sobre animaciones en el canal de youtube de la materia Técnicas de Gráficos por Computadora de la UTN FRBA donde soy ayudante 😄

Ahora si, vamos a lo que nos incumbe

Introducción

Three.js es una librería de javascript que se encuentra por encima de WebGL y es open source! Pueden ver el código acá

También se puede usar con CSS y SVG pero no es lo que nos interesa por ahora

Acá se pueden ver paginas hechas con Three.js😍:

Bueno pero, que es WebGL exactamente?

Es una API de javascript que renderiza triángulos en un <canvas> de una manera increíblemente veloz, dado que usa la placa de video (la GPU) y hace operaciones en paralelo

La GPU dibuja triángulos, y para hacerlo, necesita saber dónde se encuentran posicionados. Esta información se encuentra en los shaders, que tienen la posición de los vertices y el color de los triángulos.

Manos a la obra!

Lo primero que vamos a hacer es crear una escena bien básica con un cubo rojo en el centro.

Antes que nada, necesitamos descargarnos Three.js en nuestra compu: entran acá https://threejs.org/ y van a donde dice download. Se les va a descargar un zip, lo extraen y el archivo que vamos a usar es el que se llama three.min.js

Necesitamos crear 2 archivos: index.html y script.js. En el primero lo único que hacemos es lo siguiente:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>03 - Basic Scene</title>
</head>
<body>
    <canvas class="webgl"></canvas>
    <script src="three.min.js"></script>
    <script src="script.js"></script>
</body>
</html>

Por ahora nada raro, simplemente ponemos en el body el canvas que les mencioné anteriormente y cargamos Three.js y el código javascript.

Ahora en script.js podemos usar el objeto THREE y acceder a los métodos y clases que nos provee la librería.

Si no me creen, escriban en script.js

console.log(THREE);

Y pueden ver en la consola del navegador todo lo que tiene THREE

Para crear una escena necesitamos 4 elementos:

  • Objetos
  • Una escena que contiene los objetos
  • Una cámara
  • Un renderer

Escena

Una escena es como un container. Ahí es donde vamos a poner todos los objetos que queremos mostrar en el browser, para luego renderizarlo.

Para instanciar una escena, el código es:

const scene = new THREE.Scene();

Objetos

En este ejemplo nuestro objeto va a ser un cubo rojo.

Para crear el cubo necesitamos un mesh. Un Mesh es una combinación de una geometría (figura) y un material (cómo se ve)
Como queremos hacer un cubo rojo, vamos a usar BoxGeometry para la geometría y MeshBasicMaterial para el material.

const geometry = new THREE.BoxGeometry(1, 1, 1);

Y para instanciar el material le pasamos como parámetro un objeto que tiene el color que queremos que tenga el material (se pueden pasar más pero en este caso solo nos interesa el color)

const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });

Como pueden observar, le pasamos 0x seguido del color en hexadecimal. El color se puede setear de muchas maneras: https://threejs.org/docs/?q=mesh#api/en/math/Color

Ahora sí podemos crear nuestro mesh:

const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);

Y lo agregamos a nuestra escena (muy importante! Si no no se va a ver)

scene.add(mesh);

Cámara

La cámara no es algo que nosotros vemos, sino que es el punto de vista desde donde se ve la escena. Podemos usar varias cámaras pero en general se usa una

Para setear la cámara, instanciamos una clase de PerspectiveCamera a la que le pasamos dos parámetros: el FOV (field of view) que es el ángulo de visión en grados, en este caso vamos a usar 75. El segundo parámetro es el aspect ratio que es lo que el ancho del canvas / el alto.
Recuerden al final agregar la cámara a la escena.

const sizes = {
    width: 800,
    height: 600
};

const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);
scene.add(camera);

Renderer

El renderer se encarga de hacer el render. Vamos a instanciar un WebGLRenderer y le pasaremos como parámetro un objeto que tiene el canvas que pusimos en el HTML más arriba.
También tenemos que setearle el tamaño que va a ser el mismo que usamos para el aspect ratio 😉

const canvas = document.querySelector('canvas.webgl');

const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);

Ahora llegó el momento de renderizar. Para eso, hacemos:

renderer.render(scene, camera);

Si abren index.html, van a notar que solo se ve un cuadrado negro. Esto es porque la cámara está adentro del cubo. Para poder verlo, necesitamos alejar la cámara de este y para eso hacemos:

camera.position.z = 3;

AxesHelper

Algo que nos puede ayudar mucho a saber dónde están los ejes es la clase AxesHelper
Cuando lo agregamos a la escena, vamos a ver en color verde el eje y positivo, en color rojo el x positivo, y en color azul el eje z positivo

const axesHelper = new THREE.AxesHelper(2);
scene.add(axesHelper);

El parámetro que se manda al instanciar esta clase, es el largo de los ejes (si queremos que se vean más grandes o más chicos)

Transformaciones

Los objetos que usamos en las escenas tienen 4 atributos:

  • position (para moverlo)
  • scale (para cambiar el tamaño)
  • rotation (para rotarlo)
  • quaternion (para rotarlo - luego vamos a profundizar sobre esto)

position

Tiene 3 propiedades esenciales, que son x, y y z.

Para mover un objeto:

  • a la derecha => asignamos un valor > 0 en x
  • a la izquierda => asignamos un valor < 0 en x
  • hacia arriba => asignamos un valor > 0 en y
  • hacia abajo => asignamos un valor < 0 en y
  • hacia adelante (o sea hacia nosotros) => asignamos un valor > 0 en z
  • hacia atrás (o sea alejado de nosotros) => asignamos un valor < 0 en z

Tengan en cuenta que tienen que mover al objeto antes de renderizarlo

Por ejemplo:

mesh.position.x = 0.9;
mesh.position.y = -1;
mesh.position.z = 2;

position es una instancia de Vector3, por lo tanto tiene los siguientes métodos heredados:

console.log(mesh.position.length());
console.log(mesh.position.distanceTo(camera.position)); // distancia a otro Vector3
console.log(mesh.position.normalize()); // normaliza un vector, o sea, hace que su módulo (tamaño) sea 1
mesh.position.set(0.9, -1, 2); // setea los atributos x, y, z

scale

Al igual que position, scale es una instancia de Vector3. Se usa para setear en cuántas veces querés setear el tamaño del objeto. Por ejemplo, si ponemos 0.5, va a ser la mitad de chico; y si ponemos 2, va a ser el doble de grande.

mesh.scale.x = 2;
mesh.scale.y = 0.25;
mesh.scale.z = 0.5;

PD: ojo! no usen números negativos.

rotation

A diferencia de position y scale, rotation no es un Vector3, sino que es un Euler. Según el eje que cambiemos, es sobre el eje que va a rotar el objeto. Por ejemplo, si cambiamos el y, va a rotar como una calesita.

Los valores de x, y y z están expresados en radianes, esto implica que si queremos que rote media vuelta, tenemos que asignarle π. Esto en javascript se logra usando la constante Math.PI

Peeeero tenemos un problemilla: Esta clase de rotaciones nos puede traer problemas dependiendo en qué orden se aplican las rotaciones. Este problema se llama gimbal lock y nos saca un grado de libertad. Si quieren leer más sobre eso pueden leer una explicación matemática. Para ayudarnos con este problema, vienen los quaterniones al rescate!

quaternion

Como dijimos antes, los quaterniones se usan para las rotaciones. En esta parte del curso no profundiza sobre quaterniones así que no vamos a ahondar sobre el tema.

Tengan en cuenta que al actualizar el atributo de rotation, se actualizan los valores de quaternion

Combinando transformaciones

Se pueden combinar la posición, la rotación (ya sea de Euler o quaternión) y la escala en cualquier tipo de orden. El resultado será el mismo

Hi, I'm Mr. Meeseeks, look at me!

Mr. Meeseeks

Todas las instancias de Object3D tienen un método que se llama lookAt() que recibe como argumento un Vector3. Podemos hacer, por ejemplo

camera.lookAt(new THREE.Vector3(0, 1, -1));

Grupos

Muchas veces vamos a querer mover un conjunto de objetos de la misma manera. Por ejemplo, estamos armando un auto, con las ruedas y puertas y queremos que sea más chico. Si quisiéramos achicar el auto, tendriámos que achicar cada parte por separado, o sea, un bajón 😔
Una solución a esto son los Group.

Para usarlo, lo instanciamos y lo agregamos a la escena para luego agregarle los objetos que querramos.

const group = new THREE.Group();
group.scale.y = 2;
group.rotation.y = 0.2;
scene.add(group);

const cube1 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
cube1.position.x = -1.5;
group.add(cube1);

const cube2 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
cube2.position.x = 0;
group.add(cube2);

const cube3 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
cube3.position.x = 1.5;
group.add(cube3);

Tengan en cuenta que group hereda de Object3D, por lo tanto, tiene los atributos y clases que mencioné más arriba.

Animaciones

Cada vez que hacemos renderer.render(...) es como sacarle una "foto" a la escena. Las animaciones no son nada más ni nada menos que muchas fotos consecutivas de la escena, como si fuera un stop-motion.

La pantalla que uno ve corre a determinados FPS (frames per second), que en general suele ser 60FPS.

requestAnimationFrame

window.requestAnimationFrame() es un método que recibe como argumento una función que ejecutará en el próximo render. En el 99% de los casos vamos a usar esta función de manera recursiva ya que queremos que nuestra función que anima a los objetos se ejecute todo el tiempo

const loop = () => {
    mesh.rotation.y += 0.01;
    renderer.render(scene, camera);
    window.requestAnimationFrame(loop);
};

loop();

El código de arriba crea una función y la guarda en la variable loop. Adentro de la función se invoca a window.requestAnimationFrame que hace justamente lo que queremos: ejecuta loop en el próximo render. No se olviden de llamar loop por primera vez porque sino no se ejecuta nuestra función y no podremos ver nuestras bellas animaciones.

Pero tenemos un problema:

Si yo corro este código en una computadora con mejor GPU y en otra con peor, vamos a ver las animaciones a distintas velocidades porque tendremos distintos FPS.

La solución? Que la animación (en este caso, la rotación) dependa del tiempo entre cada render.

let time = Date.now();

const loop = () => {
    const currentTime = Date.now();
    const deltaTime = currentTime - time;
    time = currentTime;

    mesh.rotation.y += 0.01 * deltaTime;

    renderer.render(scene, camera);
    window.requestAnimationFrame(loop);
}

loop();

Solución de Three.js

Three.js tiene una clase que se llama Clock que hace por nosotros el código escrito anteriormente mediante la función getElapsedTime(), que nos retorna cuántos segundos pasaron desde que se instanció la clase Clock.

const clock = new THREE.Clock()

const loop = () => {
    const elapsedTime = clock.getElapsedTime();

    mesh.rotation.y = elapsedTime;

    renderer.render(scene, camera);
    window.requestAnimationFrame(loop);
}

loop();

Volviendo a las animaciones

Acá es donde uno se pone creativo: podemos usar, por ejemplo, la función seno que oscila entre el -1 y 1 para hacer que nuestro cubo se mueva en el eje y entre -1 y 1

const clock = new THREE.Clock()

const loop = () => {
    const elapsedTime = clock.getElapsedTime();

    mesh.rotation.y = Math.sin(elapsedTime);

    renderer.render(scene, camera);
    window.requestAnimationFrame(loop);
}

loop();

Y si queremos que el cubo haga la trayectoria de un círculo

const clock = new THREE.Clock()

const loop = () => {
    const elapsedTime = clock.getElapsedTime();

    mesh.rotation.y = Math.sin(elapsedTime);
    mesh.position.x = Math.cos(elapsedTime);

    renderer.render(scene, camera);
    window.requestAnimationFrame(loop);
}

loop();

Ta-da!

Usando GSAP

GSAP es una librería, por lo tanto tenemos que agregarla a nuestro proyecto. Al igual que la mayoría de las librerías, su objetivo es simplificarnos las cosas, con ella es más simple generar animaciones

gsap.to(mesh.position, { duration: 1, delay: 1, x: 2 });

const loop = () => {
    renderer.render(scene, camera);
    window.requestAnimationFrame(loop);
}

loop();

Acá le decimos a gsap que cambie la posición del mesh, que se mueva 2 unidades en el eje x, que tarde un segundo en empezar y que dure un segundo

PD: ojo! A gsap le pasamos la position, no todo el mesh

That's all folks
That's all folks! Si llegaron hasta acá, gracias por leer🥰
A medida que vaya viendo los otros módulos voy a ir subiendo los resúmenes acá.
Mientras tanto, pueden verme en Twitch o leerme en Twitter

Si quieren recibir notificaciones cada vez que subo un posteo nuevo, pueden suscribirse https://listed.to/@daiuszw/subscribe y si les sirvió/gusta esto, también me pueden dejar un mensajito https://listed.to/authors/15023/guestbook

Hasta la próxima!!


You'll only receive email when they publish something new.

More from Daiu
All posts