VueJs + ThreeJs Primera parte. Template
Published: 2023-01-25 19:49:13

## Introducción

Esta será la primera de una serie de post sobre algunas técnicas para usar vueJs + threeJs. Aunque hay algunas librerías que nos facilitan el trabajo conocer las bases nos ayuda a tener conocimientos más sólidos

### Recursos

- [https://threejs.org/docs/](https://threejs.org/docs/)
- [https://vuejs.org/](https://vuejs.org/guide/quick-start.html#creating-a-vue-application)
- [https://vueuse.org/](https://vueuse.org/)
- [https://greensock.com/](https://greensock.com/)
- [https://threejs-journey.xyz/](https://threejs-journey.xyz/)

En esta serie de post crearemos algunas cosas interesantes como:

- Como hacer un FPS (first person shooter camera)
- Parallax effect sobre una escena con threeJs que sigue al mouse
- Shadders
- usar 3D models
- y algunas cosillas más…

Usaremos algunos conceptos avanzados por lo que es recomendable un conocimiento básico de cómo funciona VueJs y js. Pero no es necesario conocimiento previo con ThreeJs

Crearemos un template que usaremos como base para las siguientes ediciones, como este:

[![Edit Vue+threejs-template](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-threejs-template-cf3vcw?fontsize=14&hidenavigation=1&theme=dark&view=preview)

## Setup

Lo primero será crear un nuevo proyecto con Vue e instalar las dependencias
```jsx
> npm init vue@latest

> cd <your-project-name>

> npm install

> npm i @vueuse/core three

> npm run dev

```

Para efectos de este tutorial eliminaremos todos los archivos y nos quedaremos solo con nuestros archivos App.vue y main.js dentro de /src

## Nuestra escena

Para iniciar nuestra primera escena 3D necesitamos comprender y configurar algunos componentes así que vamos por partes, lo primero que necesitaremos será un canvas, (algunos autores lo crean directamente desde JS) yo prefiero crearlo en nuestro template.

Le asignaremos un ref para tener acceso dentro del lifecycle onMouted

```jsx
<template>
    <canvas ref="canvas" />
</template>

<script setup>
import { ref, onMounted } from "vue";

const canvas = ref(null);

onMounted(() => {

    console.log(canvas.value);

});
</script>

<style>
...
</style>
```

Ahora necesitamos varias cosas, como la **escena**, la **cámara**, una función **render**, un **Mesh** (objeto que mostrar) y algunas pequeñas configuraciones, empecemos por la escena.

```jsx
import { Scene} from "three"
// NOTA: Aunque muchos autores usan una notación * as THREE
// Yo personalmente prefiero importar sólo lo que necesito por temas de rendimiento

const scene = new Scene();
```

Y así de simple creamos nuestra escena (nada muy emocionante aún) pero vamos a ir poco a poco añadiendo todo lo que necesitamos a ella

## Renderer

Sigamos con el render, que no es más que una función de ThreeJs que realizará el renderizado de nuestra escena usando la WebGL API

Crearemos un archivo renderer.js donde exportamos dos funciones un initRenderer y un updateRenderer, por ahora creemos el initRenderer.

```jsx
// renderer.js
import { WebGLRenderer } from "three";

export const initRenderer = (canvas) => {

    const renderer = new WebGLRenderer({

        canvas: canvas,

    });

    return renderer;

};
```

Le indicamos donde renderizar (nuestro canvas), aunque podemos tener múltiples configuraciones, tales como color de la escena, manejo de sombras , etc. Nos quedaremos con lo básico, ahora usemos initRenderer en nuestro App.vue

```jsx
// <script setup>
import { ref, onMounted} from "vue";
import { Scene } from "three";
import { initRenderer } from "./renderer";

const canvas = ref(null);

const scene = new Scene();

let renderer = null;

onMounted(() => {

    renderer = initRenderer(canvas.value);

});
// </script>
```

Aún no podemos ver nada pero vamos avanzando.

- *Nota**: Puede parecer extraño declarar una variable let renderer null al principio, no te preocupes esto se hace porque próximamente nuestro updateRender se dispara antes de que el obj renderer pueda ejecutarse.

## Cámara

Para poder visualizar algo, necesitamos una cámara, hay muchos tipos de cámaras disponibles en esta maravillosa librería de ThreeJs, elegiremos PerspectiveCamera

Para esto también vamos a crear dos funciones initCamera y updateCamera empecemos por el init en un archivo separado camera.js

```jsx
//camera.js
import { PerspectiveCamera } from "three";

export const initCamera = () => {

    const camera = new PerspectiveCamera(
        75, window.width / window.height, 0.1, 100
    );

return camera;

// la position de la cámara por defecto es x:0, y:0 z:0
// recordemos que trabajamos con 3 dimensiones, el orden por defecto es XYZ
};
```

Para poder visualizar nuestra escena vamos a renderizarla con la función render() que recibe dos parámetros, la escena y la cámara (y añadirla a nuestra escena)

```jsx
//<script setup>
import { ref, onMounted } from "vue";
import { Scene } from "three";
import { initRenderer } from "./renderer";
import { initCamera } from "./camera";

const canvas = ref(null);

const scene = new Scene();

let renderer = null;

let camera = null;

camera = initCamera();

scene.add(camera);

onMounted(() => {

    renderer = initRenderer(canvas.value);

    renderer.render(scene, camera)

});
// </script>
```

Y si todo ha salido de manera correcta deberíamos visualizar algo como:

![](https://assets.codepen.io/4698468/Primera_scena.png)

- Lo sé, lo sé. No es lo más emocionante del mundo pero pasos cortos.*

Tenemos dos detalles a corregir:

- El tamaño de la escena no corresponde a toda la pantalla
- Además el tamaño de la escena es fijo y no se actualiza (acá vamos a usar la reactividad de Vue para ayudarnos)

### Update functions

Crearemos dos funciones updateRenderer y updateCamera en sus correspondientes archivos

```jsx
//renderer.js
export function updateRenderer(renderer, width, height) {

if (!renderer) return;

renderer.setSize(width, height);
// configura nuestra escena con el tamaño enviado

renderer.setPixelRatio(window.devicePixelRatio);
// configura nuestra escena con el pixel Ratio del dispositivo

}
```

```jsx

//camera.js

export const updateCamera = (camera, aspectRatio) => {

if (!camera) return;

camera.aspect = aspectRatio;

// configura nuestra escena con el aspectRatio enviado

camera.updateProjectionMatrix();

// actualiza la projection de la cámara
};
```

Necesitamos algunos parámetros extras para usar nuestras update functions, usaremos la reactividad de VueJs para que escuche los cambios y automáticamente llame a nuestra update function. Usemos un watchEffect.

```jsx
// <script setup>
import { ref, onMounted, watchEffect } from "vue";
import { Scene } from "three";
import { initRenderer } from "./renderer";
import { initCamera, updateCamera, updateRenderer } from "./camera";

const canvas = ref(null);

const scene = new Scene();

let renderer = null;

let camera = null;

camera = initCamera();

scene.add(camera);

watchEffect(() => {

    updateRenderer(renderer, width, height);

    updateCamera(camera, aspectRatio);
});

onMounted(() => {

    renderer = initRenderer(canvas.value);

    renderer.render(scene, camera)

    updateRenderer(renderer, width, height);

    updateCamera(camera, aspectRatio);
});
// </script>
```

También necesitamos hacer un llamado inicial de las funciones update en el onMounted.

Usemos vueUse para obtener los tamaños reactivos de la pantalla

```jsx
const { width, height } = useWindowSize();

const aspectRatio = computed(() => width.value / height.value);
```

Como extra añadamos unos estilos en el css global que nos van a ayudar

```css
// <style>
* {
margin: 0;
padding: 0;
}

canvas {
position: absolute;
touch-action: none;
}
// </style>
```

Y nuestro App quedaría de esta forma

```jsx
// <script setup>
import { ref, onMounted, computed, watchEffect } from "vue";
import { Scene } from "three";
import { useWindowSize } from "@vueuse/core";
import { initRenderer, updateRenderer } from "./renderer";
import { initCamera, updateCamera } from "./camera";

const canvas = ref(null);

const scene = new Scene();

let renderer = null;

let camera = null;

const { width, height } = useWindowSize();

const aspectRatio = computed(() => width.value / height.value);

camera = initCamera();

scene.add(camera);

watchEffect(() => {
    updateRenderer(renderer, width.value, height.value);
    updateCamera(camera, aspectRatio.value);
});

onMounted(() => {
    renderer = initRenderer(canvas.value);
    renderer.render(scene, camera);
    updateRenderer(renderer, width.value, height.value);
    updateCamera(camera, aspectRatio.value);
});
// </script>
```

- Es ok si perdemos visibilidad de la escena a este punto, esto es porque el updateRenderer y updateCamera se ejecutan antes del mounted

### Function tick

La función tick() (el maestro Bruno Simon la llama de esta forma), es una función versátil, que nos permitirá animar los objetos que se encuentren dentro de nuestra escena, le dará vida a nuestra app, además como usa el  requestAnimationFrame API tiene buen rendimiento

```jsx
//App..vue dentro del mounted
const tick = () => {
//render
    renderer.render(scene, camera); // moveremos nuestro renderer dentro de tick()
    window.requestAnimationFrame(tick); // cada frame ejecutara un tick()
};
tick(); // inicializamos tick()
```

### Ya casi…

uff hasta ahora hay bastante código y solo una escena totalmente negra (color por default)

Pero con todo configurado, ahora si intentamos hacer resize a la pantalla, veremos que nuestra escena se actualiza de manera automática, al igual que nuestra cámara.

Hasta ahora nuestro script va de esta forma

```jsx
import { ref, onMounted, computed, watchEffect } from "vue";
import { Scene } from "three";
import { useWindowSize } from "@vueuse/core";
import { initRenderer, updateRenderer } from "./renderer";
import { initCamera, updateCamera } from "./camera";

const canvas = ref(null);

const scene = new Scene();

let renderer = null;

let camera = null;

const { width, height } = useWindowSize();

const aspectRatio = computed(() => width.value / height.value);

camera = initCamera();

scene.add(camera);

watchEffect(() => {
    updateRenderer(renderer, width.value, height.value);
    updateCamera(camera, aspectRatio.value);
});

onMounted(() => {
    renderer = initRenderer(canvas.value);
    updateRenderer(renderer, width.value, height.value);
    updateCamera(camera, aspectRatio.value);

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

    tick();
});
```

### Mesh

Aunque para los Mesh hay infinidades de combinaciones: materiales, geometrías, texturas y propiedades, que poco a poco indagaremos más en ellas, por el momento crearemos la forma más simple posible un cubo, yo recomendaría tratar de dejar lo más despejado el App.vue y crear un archivo aparte algo como cube.js (que luego reemplazamos por otros modelos o sencillamente lo eliminaremos)

```jsx

// cube.js

import { BoxGeometry, MeshBasicMaterial, Mesh } from "three";

export const createCube = () => {
    const geometry = new BoxGeometry(1, 1, 1);
// la BoxGeometry necesita los parámetros de width, height y depth

    const material = new MeshBasicMaterial({ color: 0x00ff00 });
// el MeshBasicMaterial es el material más básico, no necesita luz

    return new Mesh(geometry, material);
};

```

Para crear un mesh, necesitamos una geometría y un material.

Hay muchas geometrías e incluso podemos crear las nuestras propias, y también hay muchos materiales cada uno con diferentes propiedades, algunos reflejan sombra, otros tienen refracción de luz. Muchos de estos temas los estaremos viendo más adelante.

Por ahora importamos y añadamos a nuestra escena

```jsx

// App.vue

import { createCube } from "./cube";

const cube = createCube()

scene.add(cube);

onMouted(() => {
...
})
```

Y te darás cuenta que…. nada se renderiza 😟. ¿Por qué sucede esto?

Realmente si se esta renderizando pero no lo podemos ver, (podemos checarlo haciendo un console.log(scene) y revisando si tiene un children, y lo que sucede es muy simple.

Nuestra cámara está en la posición (0,0,0) al igual que nuestro cubo. es la posición por defecto y no la hemos cambiado. (recordemos que el orden es X,Y,Z)

La cámara así como nuestro Mesh son lo que denominamos 3d Object y comparten propiedades como, scale, rotation y posición. Todas en tres dimensiones.

Para solucionarlo solo movamos la cámara un poco para atrás, nos vamos a nuestro camera.js y justo antes del return añadimos:

```jsx

camera.position.z = 5;

// si te preguntas 5 qué? la mejor forma de verlo son 5 unidades, una buena práctica

// es definir si son 5 metros, 5 centímetros depende de tu scena

```

Y ahí está! un cuadrado en el medio de nuestra escena, verde, inmóvil y que parece en 2D 😟

Ah! pero no es en 2D, lo que sucede es que nuestra cámara está exactamente alineada, de frente a él

Agreguemos una pequeña animación, verán que es muy fácil

Para esto en nuestra función tick() vamos a añadir un estas dos líneas de código

```jsx

cube.rotation.y += 0.01

cube.rotation.x += 0.01

```

Y listo.

uff fue bastante trabajo no? varias cosas que configurar, nuevos conceptos pero al final tenemos un template robusto que podemos usar como base para infinita variedad de proyectos

Así se ve nuestro código hasta el momento:

```jsx

<template>
    <canvas ref="canvas" />
</template>

<script setup>
import { ref, onMounted, computed, watchEffect } from "vue";
import { Scene } from "three";
import { useWindowSize } from "@vueuse/core";
import { initRenderer, updateRenderer } from "./renderer";
import { initCamera, updateCamera } from "./camera";
import { createCube } from "./cube";

const canvas = ref(null);
const scene = new Scene();
let renderer = null;
let camera = null;

const cube = createCube();
camera = initCamera();

const { width, height } = useWindowSize();
const aspectRatio = computed(() => width.value / height.value);

scene.add(camera);
scene.add(cube);

watchEffect(() => {
    updateRenderer(renderer, width.value, height.value);
    updateCamera(camera, aspectRatio.value);
});

onMounted(() => {
    renderer = initRenderer(canvas.value);
    updateRenderer(renderer, width.value, height.value);
    updateCamera(camera, aspectRatio.value);
    const tick = () => {
        //render
        renderer.render(scene, camera);
        window.requestAnimationFrame(tick);
        //animation
        cube.rotation.y += 0.01;
        cube.rotation.x += 0.01;
    };
    tick();
});

</script>

<style>
* {
    margin: 0;
    padding: 0;
}

canvas {
    position: absolute;
    touch-action: none;
}

</style>

```

## Conclusión

Hasta ahora puede parecer que tengamos solo un cubo verde dando vueltas, pero la verdad tenemos un template que usaremos más adelante sirve como base para ya no preocuparnos de pequeños detalles.

Que hemos aprendido:

1. Crear una escena básica y renderizarla

2. Crear un Mesh, añadirlo a la escena y agregarle animación

3. Cómo usar la reactividad de VueJs, para añadir reactividad a la escena