Curso de ThreeJS con React

Enlaces de interés

Puedes consultar la página oficial de la documentación de ThreeJS con React.

En esta otra url tienes una guía / tutorial que recorre los conceptos básicos de ThreeJS con React.

También tienes la documentación de la página oficial de ThreeJS.

Instalación de las dependencias necesarias

npm install three @types/three @react-three/fiber @react-three/drei

Tu primera escena

La escena tendrá el siguiente código que explicaré más abajo. A continuación tienes el código que escribiremos usando react-three/fiber y el código equivalente que escribiríamos si lo hiciesemos directamente con three.js.

import { Canvas } from "@react-three/fiber";

function App() {
  return (
    <Canvas>
      <mesh>
        <boxGeometry />
        <meshBasicMaterial color={0xff0000} />
      </mesh>
    </Canvas>
  );
}

export default App;
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

const mesh = new THREE.Mesh()
mesh.geometry = new THREE.BoxGeometry()
mesh.material = new THREE.MeshStandardMaterial()

scene.add(mesh)

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}

animate()

Canvas

Este componente:

  • Configura la cámara y la escena, que es lo mínimo que necesitamos para renderizar.
  • Además renderiza la escena en cada frame, por lo que no tenemos que preocuparnos de React.

Podemos cambiar el punto de visión de la cámara con la prop camera.

<Canvas camera={{ position: [3, 3, 3] }}>

Estilos

Para que todo se vea un poco mejor, vamos a añadir la siguiente hoja de estilos al componente App.

html,
body,
#root {
  height: 100%;
  margin: 0;
  background-color: #000000;
}

Algunos componentes básicos

OrbitControls

Permite mirar la escena desde diferentes ángulos usando el ratón

import { OrbitControls } from "@react-three/drei";
<OrbitControls />

Grid

Una rejilla nos permitirá ubicarnos más fácilmente en la escena

<mesh>
  <Grid sectionSize={3} cellSize={1} infiniteGrid />
  <boxGeometry />
</mesh>

mesh

Puedes consultar las geometrías disponibles en three.js documentation.

Una malla es un objeto básico de Three.js . Es un contenedor de la geometría (BoxGeometry por ejemplo).

box

Parámetros: ancho, alto y profundo.

<mesh>
    <boxGeometry args={[5, 1, 0.2]} />
</mesh>

sphere

Parámetros: el radio y la cantidad de segmentos utilizados a lo ancho y alto.

<mesh>
    <sphereGeometry args={[2, 10, 10]} />
    <meshBasicMaterial color="brown" wireframe />
</mesh>

cylinder

Parámetros: radio de una de las caras, radio de la otra cara, altura, cantidad de segmentos longitudinales.

<mesh>
    <cylinderGeometry args={[1, 1, 2, 32]} />
    <meshBasicMaterial color="brown" wireframe />
</mesh>

plane

El plano sólo se ve por una de las dos caras.

Parámetros: ancho y alto.

<mesh rotation-x={-Math.PI / 2} position-y={-3}>
    <planeGeometry args={[10, 10]} />
</mesh>

Materiales

MeshBasicMaterial: Usa un color. No es afectado por la luz.

MeshStandardMaterial: Usa un color, textura, etc. Es afectado por la luz. Si no hay luz, se ve negro.

text

<Text font={"fonts/Runtoe.ttf"}>
    Love is {"\n"}in the air
    <meshBasicMaterial color="brown" />
</Text>

models

Podemos generar un fichero .jsx a partir de un fichero glb utilizando el siguiente comando. El fichero trees.glb será el fichero a partir del cual queremos generar el modelo y el fichero Trees,jsx será el fichero generado.

npx gltfjsx public/models/trees.glb -o src/app/glb/Trees.jsx -k -K -r public
<Trees />
<Environment preset="sunset" />

Cámara

Hay tres tipos de cámara:

Perspective Camera

Es la cámara por defecto.

Es la cámara más habitual en aplicaciones 3D y simula como el ojo humano ve el mundo.

Al ser la cámara por defecto, no es necesario añadirla a la escena, ya viene de serie. Podemos controlar su ubicación desde el propio Canvas:

<Canvas camera={{ position: [3, 3, 3] }}>
Propiedades de las cámaras

Podemos añadir una cámara a la escena y convertirla en principal utilizando la propiedad makeDefault.

<Canvas>
  <PerspectiveCamera position={[0, 8, 0]} makeDefault />
</Canvas>
fov

La cámara tiene una propiedad llamada fov (field of view) que cuanto de la escena vamos a ver. Cuanto menor es su valor, menos vemos. El efecto es similar a hacer zoom.

<PerspectiveCamera fov={30} />
near y far

Con las propiedades near y far podemos hacer que lo que esté más cerca de near y más lejos de far, no será renderizado.

<PerspectiveCamera near={5} far={8} />

Orthographic Camera

Los objetos no se hacen pequeños cuando se alejan de la cámara. Esta cámara no tiene la propiedad fov, que veremos más adelante.

Para hacer algo más grande, en esta cámara, podemos usar:

zoom

<OrthographicCamera makeDefault zoom={100} />

left, right, top, bottom

<OrthographicCamera
  makeDefault
  position={[1, 1, 1]}
  left={-2}
  right={2}
  top={2}
  bottom={-2}
/>

Cube Camera

Renderiza la escena 6 veces, una por cada lado de un cubo. Es útil para crear efectos de luz, etc.

Transforms

position

<mesh position={[-1, 0, 0]}>
    <boxGeometry />
<meshBasicMaterial color="red" />
    </mesh>
<mesh position={[0, 0, 0]}>
    <boxGeometry />
<meshBasicMaterial color="green" />
    </mesh>
<mesh position={[1, 0, 0]}>
    <boxGeometry />
    <meshBasicMaterial color="blue" />
</mesh>

scale

Podemos asignar todos los valores de scale con una sola propiedad…

<mesh position={[-1, 0, 0]} scale={[0.5, 0.5, 0.5]}>
        <boxGeometry />
        <meshBasicMaterial color="red" />
      </mesh>
      <mesh position={[0, 0, 0]} scale={[1, 1, 1]}>
        <boxGeometry />
        <meshBasicMaterial color="green" />
      </mesh>
      <mesh position={[1, 0, 0]} scale={[2, 2, 2]}>
        <boxGeometry />
        <meshBasicMaterial color="blue" />
      </mesh>

O con una propiedad para cada coordenada:

 <mesh position={[-1, 0, 0]} scale={[1, 0.5, 0.5]}>
        <boxGeometry />
        <meshBasicMaterial color="red" />
      </mesh>
      <mesh position={[0, 0, 0]} scale-y={4}>
        <boxGeometry />
        <meshBasicMaterial color="green" />
      </mesh>
      <mesh position={[1, 0, 0]} scale-z={3}>
        <boxGeometry />
        <meshBasicMaterial color="blue" />
      </mesh>

rotation

<mesh rotation-y={Math.PI / 4} rotation-z={THREE.MathUtils.degToRad(30)}>
  <boxGeometry />
  <meshBasicMaterial color="green" />
</mesh>

Grupos

<group position={[-2, -2, 0]} >
  <mesh position={[-1, 0, 0]}>
    <boxGeometry />
    <meshStandardMaterial color="red" />
  </mesh>
  <mesh position={[0, 0, 0]}>
    <boxGeometry />
    <meshStandardMaterial color="green" />
  </mesh>
  <mesh position={[1, 0, 0]}>
    <boxGeometry />
    <meshStandardMaterial color="blue" />
  </mesh>
</group>

Luces

Para que las luces afecten a los objetos, no pueden tener meshBasicMaterial. Usaremos meshStandardMaterial en su lugar.

AmbientLight

Es la luz más sencilla. Ilumina todos los objetos de la escena por igual, independientemente de su posición u orientación.

Sobre un objeto blanco, la luz azul mostrará un objeto azul, pero si el objeto es de color, mostrará un objeto negro.

<ambientLight intensity={1} color={"blue"} />

<mesh position={[5, 0, 0]}>
        <boxGeometry />
        <meshStandardMaterial color="white" />
</mesh>

DirectionalLight

<directionalLight intensity={0.5} color="red" />

<mesh>
        <boxGeometry />
        <meshStandardMaterial color="white" />
</mesh>

SpotLight

<spotLight intensity={0.5} color="red" />

<mesh>
        <boxGeometry />
        <meshStandardMaterial color="white" />
</mesh>

Componentes y props

Podemos encapsular parte de la lógica anterior en un componente para poder reutilizarlo.

function App() {
  return (
    <Canvas>
      <Box position={[-0.75, 0, 0]} />
      <Box position={[0.75, 0, 0]} />
    </Canvas>
  );
}
export const Box = (props) => (
  <mesh {...props}>
    <boxGeometry />
    <meshBasicMaterial color={0xff0000} />
  </mesh>
);

useRef y useFrame

export const Box = (props) => {
  const boxRef = useRef();
  useFrame((_, delta) => {
    boxRef.current.rotation.x += 2 * delta;
    boxRef.current.position.y += 2 * delta;
  });
  return (
    <mesh {...props} ref={boxRef}>
      <boxGeometry />
      <meshBasicMaterial color={0xff0000} />
    </mesh>
  );
};

Events

export const Box = (props) => {
  const boxRef = useRef();
  useFrame((_, delta) => {
    boxRef.current.rotation.x += 2 * delta;
    boxRef.current.position.y += 2 * delta;
  });
  return (
    <mesh
      {...props}
      ref={boxRef}
      onPointerDown={(e) => console.log("down")}
      onPointerUp={(e) => console.log("up")}
      onPointerOver={(e) => console.log("over")}
      onPointerOut={(e) => console.log("out")}
    >
      <boxGeometry />
      <meshBasicMaterial color={0xff0000} />
    </mesh>
  );
};

Eventos del teclado

import { Canvas } from "@react-three/fiber";
import { KeyboardControls } from "@react-three/drei";
import "./style.css";
import Box from "./Box";

function App() {
  return (
    <KeyboardControls
      map={[
        { name: "left", keys: ["ArrowLeft", "a", "A"] },
        { name: "right", keys: ["ArrowRight", "d", "D"] },
      ]}
    >
      <Canvas>
        <Box position={[-0.75, 0, 0]} />
      </Canvas>
    </KeyboardControls>
  );
}

export default App;
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { useKeyboardControls } from "@react-three/drei";

const Box = (props) => {
  const boxRef = useRef();
  const [, get] = useKeyboardControls();

  useFrame((_, delta) => {
    const { left, right } = get();
    if (left) boxRef.current.position.x -= 2 * delta;
    if (right) boxRef.current.position.x += 2 * delta;
  });
  return (
    <mesh {...props} ref={boxRef}>
      <boxGeometry />
      <meshBasicMaterial color={0xffff00} />
    </mesh>
  );
};

export default Box;

HTML

Físicas

Debemos instalar la siguiente dependencia:

npm i @react-three/rapier

Todos los objetos afectados por la física deben estar dentro de la etiqueta Physics.

Un RigidBody hace que un cuerpo sea afectado por las físicas del entorno (gravedad, colisiones, etc).

Todos los objetos que rengan RigidBody, deben estar dentro de la etiqueta RigidBody.

<Physics>
  <RigidBody>
    <mesh>
      <boxGeometry />
      <meshBasicMaterial color="green" />
    </mesh> 
  </RigidBody>
</Physics>

Podemos añadir un suelo con la prop fixed para que no se caiga.

<RigidBody type="fixed">
  <mesh position-y={-5}>
    <boxGeometry args={[20, 0.5, 20]} />
    <meshBasicMaterial color="mediumpurple" />
  </mesh>
</RigidBody>