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>