[Three.js] Basics parte I
Daiu Szwimer
Posted on June 24, 2021
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😍:
- https://bruno-simon.com
- https://go.pioneer.com/cornrevolution
- https://richardmattka.com
- https://lusion.co
- https://www.oculus.com/medal-of-honor/
- http://letsplay.ouigo.com
- https://zen.ly
- https://prior.co.jp/discover
- https://www.midwam.com
- https://heraclosgame.com
- https://chartogne-taillet.com
- https://live.vanmoof.com/site
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!
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! 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
Hasta la próxima!!
Posted on June 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.