¿Qué es Three.js?
Three.js es una biblioteca de código abierto escrita en JavaScript que simplifica la creación de gráficos 3D interactivos directamente en el navegador web. Internamente utiliza la API WebGL del navegador, pero abstrae la complejidad de escribir shaders y código matemático de computación gráfica de bajo nivel, permitiendo a los desarrolladores construir escenas 3D con mucha menos curva de aprendizaje.
Sitio oficial: https://threejs.org/
Los tres pilares fundamentales
Toda aplicación Three.js requiere tres componentes esenciales:
- Escena (Scene): Actúa como contenedor donde se colocan todos los objetos 3D, luces y cámaras.
- Cámara (Camera): Define el punto de observación, determinando qué porción de la escena es visible y cómo se proyecta en pantalla.
- Renderizador (Renderer): Toma la escena y la cámara, calcula la perspectiva y dibuja el resultado final en un elemento canvas del DOM.
Configuración inicial del entorno
El siguiente ejemplo muestra cómo inicializar una escena básica con un color de fondo personalizado:
// Importar la biblioteca principal
import * as THREE from 'three'
// Crear la escena como contenedor principal
const worldScene = new THREE.Scene();
worldScene.background = new THREE.Color(0x1a1a2e);
// Configurar la cámara perspectiva
const viewCamera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
2000
);
// Inicializar el renderizador con antialiasing
const canvasRenderer = new THREE.WebGLRenderer({ antialias: true });
canvasRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(canvasRenderer.domElement);
// Posicionar la cámara y renderizar
viewCamera.position.z = 8;
canvasRenderer.render(worldScene, viewCamera);
Cosntrucción de un cubo tridimensional
Para crear un objeto visible en Three.js se necesitan dos componentes: una geometría que define la forma y un material que define el aspecto visual. Ambos se combinan en una malla (Mesh):
import * as THREE from 'three'
const mainScene = new THREE.Scene();
const cam = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const glRenderer = new THREE.WebGLRenderer();
glRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(glRenderer.domElement);
// Definir la geometría del cubo (ancho, alto, profundidad en unidades Three.js)
const boxShape = new THREE.BoxGeometry(1.5, 1.5, 1.5);
// Crear un material con color sólido
const surfaceMat = new THREE.MeshBasicMaterial({ color: 0x00cc88 });
// Ensamblar la malla combinando geometría y material
const boxObject = new THREE.Mesh(boxShape, surfaceMat);
// Insertar el objeto en la escena
mainScene.add(boxObject);
// Alejar la cámara para poder ver el cubo
cam.position.z = 5;
glRenderer.render(mainScene, cam);
Comprendiendo la cámara perspectiva
La cámara perspectiva simula la visión humana, donde los objetos más lejanos aparecen más pequeños. Sus parámetros principales son:
- fov (campo de visión): Ángulo vertical de apertura. Valores más pequeños producen mayor zoom y objetos más grandes; valores más amplios muestran mayor área.
- aspect (relación de aspecto): Debe coincidir con la proporción del canvas para evitar distorsiones.
- near (plano cercano): Distancia mínima desde la cámara donde los objetos son visibles.
- far (plano lejano): Distancia máxima de renderizado. Los objetos más allá de este plano no se dibujan.
const viewCamera = new THREE.PerspectiveCamera(
60, // Ángulo de visión vertical
window.innerWidth / window.innerHeight, // Relación de aspecto
0.1, // Plano cercano
1500 // Plano lejano
);
Mostrar ejes de coordenadas
El helper de ejes es una herramienta de depuración muy útil que dibuja tres líneas representando los ejes X (rojo), Y (verde) y Z (azul):
function attachAxes(sceneRef, length) {
const axesGuide = new THREE.AxesHelper(length);
sceneRef.add(axesGuide);
}
// Agregar ejes de longitud 4 a la escena
attachAxes(mainScene, 4);
Se pueden agregar ejes tanto a la escena (coordenadas del mundo) como a objetos individuales (coordenadas locales del objeto).
Controles orbitales interactivos
Los controles orbitales permiten al usuario manipular la cámara mediante el ratón: arrastrar con el botón izquierdo para rotar, usar la rueda para acercar/alejar, y arrastrar con el botón derecho para desplazar.
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let orbitCtrl;
function setupOrbitControls(cameraRef, rendererElement) {
orbitCtrl = new OrbitControls(cameraRef, rendererElement);
orbitCtrl.enableDamping = true;
orbitCtrl.dampingFactor = 0.1;
orbitCtrl.minDistance = 2;
orbitCtrl.maxDistance = 20;
}
function startRenderLoop(sceneRef, cameraRef, rendererRef) {
function tick() {
requestAnimationFrame(tick);
orbitCtrl.update();
rendererRef.render(sceneRef, cameraRef);
}
tick();
}
Es fundamental invocar orbitCtrl.update() en cada fotograma para que el efecto de amortiguación funcione correctamente.
Adaptación responsiva de la ventana
Para que la escena se redimensione correctamente cuando el usuario cambia el tamaño del navegador:
function handleWindowResize(cameraRef, rendererRef) {
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
cameraRef.aspect = width / height;
cameraRef.updateProjectionMatrix();
rendererRef.setSize(width, height);
});
}
Geometrías y materiales
Three.js proporciona geometrías incorporadas como BoxGeometry, SphereGeometry, PlaneGeometry, entre otras. Para formas irregulares, se pueden importar modelos 3D creados con software de modelado.
Los materiales controlan la apariencia visual. MeshBasicMaterial no responde a la iluminación, mientras que MeshStandardMaterial sí interactúa con las luces de la escena.
Múltiples cubos con colores aleatorios
Se pueden generar varios objetos con propiedades aleatorias para visualizaciones dinámicas:
const objectPool = [];
function generateRandomBoxes(quantity) {
for (let idx = 0; idx < quantity; idx++) {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
const dims = {
w: Math.random() * 2 + 0.5,
h: Math.random() * 2 + 0.5,
d: Math.random() * 2 + 0.5
};
const geo = new THREE.BoxGeometry(dims.w, dims.h, dims.d);
const mat = new THREE.MeshBasicMaterial({
color: `rgb(${r},${g},${b})`
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(
(Math.random() - 0.5) * 12,
(Math.random() - 0.5) * 12,
(Math.random() - 0.5) * 12
);
mainScene.add(mesh);
objectPool.push(mesh);
}
}
generateRandomBoxes(8);
Transformaciones: traslación, rotación y escala
function applyTransforms(targetMesh) {
// Traslación en el sistema de coordenadas del padre
targetMesh.position.set(3, 1, -2);
// Rotación en radianes alrededor de los ejes locales
targetMesh.rotation.x = Math.PI / 6;
targetMesh.rotation.y = Math.PI / 4;
// Escala a lo largo de los ejes locales (el centro permanece fijo)
targetMesh.scale.set(1.5, 0.8, 2);
}
function animateRotation(targetMesh) {
targetMesh.rotation.x += 0.008;
targetMesh.rotation.y += 0.012;
}
Monitoreo de rendimiento con Stats
Stats.js muestra métricas de rendimiento en tiempo real (FPS, tiempo de fotograma, uso de memoria):
import Stats from 'three/addons/libs/stats.module.js';
function initPerformanceMonitor() {
const monitor = new Stats();
monitor.setMode(0); // 0=FPS, 1=ms por fotograma, 2=memoria
monitor.domElement.style.position = 'fixed';
monitor.domElement.style.left = '0px';
monitor.domElement.style.top = '0px';
document.body.appendChild(monitor.domElement);
return monitor;
}
Cubos multicolor con seis caras
Asignando un array de materiales, cada cara del cubo puede tener un color diferente:
const colors = ['#e74c3c', '#2ecc71', '#3498db', '#f39c12', '#9b59b6', '#1abc9c'];
const faceMaterials = colors.map(c => new THREE.MeshBasicMaterial({ color: c }));
const multiFaceBox = new THREE.Mesh(
new THREE.BoxGeometry(2, 2, 2),
faceMaterials
);
mainScene.add(multiFaceBox);
Geometría esférica
function buildSphere() {
const sphereGeo = new THREE.SphereGeometry(2, 64, 32);
const sphereMat = new THREE.MeshStandardMaterial({
color: 0x2196f3,
roughness: 0.3,
metalness: 0.7
});
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
mainScene.add(sphereMesh);
return sphereMesh;
}
Eliminación de objetos y liberación de memoria
Al eliminar un objeto de la escena, es importante liberar también la memoria de la geometría y el material:
function disposeObject(mesh, sceneRef) {
if (mesh.geometry) mesh.geometry.dispose();
if (mesh.material) {
if (Array.isArray(mesh.material)) {
mesh.material.forEach(mat => mat.dispose());
} else {
mesh.material.dispose();
}
}
sceneRef.remove(mesh);
}
Raycasting para interacción con el ratón
El raycasting proyecta un rayo invisible desde la posición del ratón en pantalla hacia la escena 3D, detectando qué objetos son intersectados. Las coordenadas de pantalla deben normalizarse al rango [-1, +1]:
function setupClickInteraction(cameraRef, targets, sceneRef) {
const ray = new THREE.Raycaster();
const cursor = new THREE.Vector2();
window.addEventListener('dblclick', (event) => {
// Convertir coordenadas de pantalla a coordenadas normalizadas
cursor.x = (event.clientX / window.innerWidth) * 2 - 1;
cursor.y = -(event.clientY / window.innerHeight) * 2 + 1;
ray.setFromCamera(cursor, cameraRef);
const hits = ray.intersectObjects(targets);
if (hits.length === 0) return;
const nearestHit = hits[0].object;
disposeObject(nearestHit, sceneRef);
const index = targets.indexOf(nearestHit);
if (index > -1) targets.splice(index, 1);
});
}
Tipos de iluminación
Luz ambiental
Ilumina uniformemente todas las superficies de la escena sin dirección ni sombras. Actúa como relleno base:
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
mainScene.add(ambientLight);
Luz direccional
Simula una fuente de luz distante como el sol, con rayos paralelos. Produce sombras definidas:
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
sunLight.position.set(5, 10, 7);
mainScene.add(sunLight);
// Helper para visualizar la dirección de la luz
const sunHelper = new THREE.DirectionalLightHelper(sunLight, 2);
mainScene.add(sunHelper);
Luz puntual
Emite luz en todas las direcciones desde un punto específico, como una bombilla:
const bulbLight = new THREE.PointLight(0xffaa00, 8, 50);
bulbLight.position.set(-3, 4, 2);
mainScene.add(bulbLight);
const bulbHelper = new THREE.PointLightHelper(bulbLight, 0.5);
mainScene.add(bulbHelper);
Foco (SpotLight)
Proyecta luz cónica desde un punto hacia una dirección específica. Permite configurar el ángulo del cono y la difuminación del borde:
const spotlight = new THREE.SpotLight(0xffffff, 15, 80, Math.PI / 5, 0.6, 1);
spotlight.position.set(4, 6, 4);
spotlight.castShadow = true;
mainScene.add(spotlight);
const spotHelper = new THREE.SpotLightHelper(spotlight);
mainScene.add(spotHelper);
Sombras en Three.js
Para activar el sistema de sombras se requiere configuración en tres niveles:
function enableShadowSystem(rendererRef, lightRef, casterMesh, receiverMesh) {
// 1. Habilitar sombras en el renderizador
rendererRef.shadowMap.enabled = true;
// 2. La luz debe calcular sombras
lightRef.castShadow = true;
// 3. El objeto que proyecta sombra
casterMesh.castShadow = true;
// 4. El objeto que recibe sombra
receiverMesh.receiveShadow = true;
}
Renderizadores CSS: 2D y 3D
Three.js permite integrar elementos HTML/CSS dentro de la escena 3D mediante renderizadores CSS especiales:
- CSS3DRenderer: Los elementos DOM se transforman en objetos 3D con posición, rotación y escala completas. No siempre miran hacia la cámara.
- CSS2DRenderer: Los elementos siempre se orientan hacia la cámara y mantienen tamaño constante independiente de la distancia.
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
function setupCSSRenderers() {
// Renderizador 3D para DOM
const css3d = new CSS3DRenderer();
css3d.setSize(window.innerWidth, window.innerHeight);
css3d.domElement.style.position = 'fixed';
css3d.domElement.style.pointerEvents = 'none';
document.body.appendChild(css3d.domElement);
// Renderizador 2D para DOM
const css2d = new CSS2DRenderer();
css2d.setSize(window.innerWidth, window.innerHeight);
css2d.domElement.style.position = 'fixed';
css2d.domElement.style.pointerEvents = 'none';
document.body.appendChild(css2d.domElement);
return { css3d, css2d };
}
function createLabel3D(text, position) {
const label = document.createElement('div');
label.textContent = text;
label.style.color = '#ff6b6b';
label.style.fontSize = '18px';
label.style.fontWeight = 'bold';
const obj3d = new CSS3DObject(label);
obj3d.position.copy(position);
obj3d.scale.set(1/16, 1/16, 1/16);
return obj3d;
}
Objetos sprite (sprites)
Los sprites son planos 2D que siempre miran hacia la cámara. Son ideales para iconos, etiquetas o efectos de partículas:
function createSprite(texturePath, position) {
const loader = new THREE.TextureLoader();
const texture = loader.load(texturePath);
const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true });
const spriteObj = new THREE.Sprite(spriteMat);
spriteObj.position.copy(position);
spriteObj.scale.set(2, 2, 1);
return spriteObj;
}
const markerSprite = createSprite('/icon-marker.png', new THREE.Vector3(0, 3, 0));
mainScene.add(markerSprite);
Ejemplo integrado: escena completa
El siguiente código combina todos los conceptos anteriores en una escena funcional:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Stats from 'three/addons/libs/stats.module.js';
// Variables globales
let sceneWorld, mainCamera, glRenderer, orbitControls, perfMonitor;
let rotatingBoxes = [];
function bootstrapApplication() {
// Escena
sceneWorld = new THREE.Scene();
sceneWorld.background = new THREE.Color(0x0d1117);
// Cámara
mainCamera = new THREE.PerspectiveCamera(
65,
window.innerWidth / window.innerHeight,
0.1,
5000
);
mainCamera.position.set(8, 6, 12);
// Renderizador
glRenderer = new THREE.WebGLRenderer({ antialias: true });
glRenderer.setSize(window.innerWidth, window.innerHeight);
glRenderer.shadowMap.enabled = true;
document.body.appendChild(glRenderer.domElement);
// Controles orbitales
orbitControls = new OrbitControls(mainCamera, glRenderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.08;
// Monitor de rendimiento
perfMonitor = new Stats();
perfMonitor.setMode(0);
document.body.appendChild(perfMonitor.domElement);
// Ejes de referencia
sceneWorld.add(new THREE.AxesHelper(6));
// Iluminación
setupLights();
// Suelo
createGroundPlane();
// Objetos
spawnRotatingBoxes(6);
// Responsividad
registerResizeHandler();
// Iniciar bucle de renderizado
requestAnimationFrame(renderCycle);
}
function setupLights() {
const hemispherical = new THREE.HemisphereLight(0x87ceeb, 0x362d1b, 0.6);
sceneWorld.add(hemispherical);
const directional = new THREE.DirectionalLight(0xfff4e6, 1.2);
directional.position.set(6, 10, 8);
directional.castShadow = true;
sceneWorld.add(directional);
}
function createGroundPlane() {
const groundGeo = new THREE.PlaneGeometry(30, 30);
const groundMat = new THREE.MeshStandardMaterial({
color: 0x2d3436,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.5;
ground.receiveShadow = true;
sceneWorld.add(ground);
}
function spawnRotatingBoxes(count) {
const palette = [0xe17055, 0x00b894, 0x0984e3, 0xfdcb6e, 0x6c5ce7, 0xe84393];
for (let i = 0; i < count; i++) {
const size = Math.random() * 1.5 + 0.5;
const geo = new THREE.BoxGeometry(size, size, size);
const mat = new THREE.MeshStandardMaterial({
color: palette[i % palette.length],
roughness: 0.4,
metalness: 0.6
});
const mesh = new THREE.Mesh(geo, mat);
mesh.castShadow = true;
mesh.position.set(
(Math.random() - 0.5) * 10,
size / 2,
(Math.random() - 0.5) * 10
);
sceneWorld.add(mesh);
rotatingBoxes.push(mesh);
}
}
function registerResizeHandler() {
window.addEventListener('resize', () => {
mainCamera.aspect = window.innerWidth / window.innerHeight;
mainCamera.updateProjectionMatrix();
glRenderer.setSize(window.innerWidth, window.innerHeight);
});
}
function renderCycle() {
requestAnimationFrame(renderCycle);
orbitControls.update();
perfMonitor.update();
// Animar cada cubo con velocidad de rotación aleatoria
rotatingBoxes.forEach((box, idx) => {
box.rotation.x += 0.005 + idx * 0.002;
box.rotation.y += 0.008 + idx * 0.001;
});
glRenderer.render(sceneWorld, mainCamera);
}
bootstrapApplication();
Carga de modelos GLTF/GLB
Three.js permite cargar modelos 3D exportados en formato GLTF (JSON) o GLB (binario) mediante el cargador GLTFLoader:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
function importModel(filePath, sceneRef, callback) {
const modelLoader = new GLTFLoader();
modelLoader.load(
filePath,
(gltfData) => {
const modelRoot = gltfData.scene;
// Ajustar escala y posición según sea necesario
modelRoot.scale.set(1, 1, 1);
modelRoot.position.set(0, 0, 0);
sceneRef.add(modelRoot);
// Procesar animaciones si existen
if (gltfData.animations.length > 0) {
const mixer = new THREE.AnimationMixer(modelRoot);
gltfData.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
}
if (callback) callback(modelRoot);
},
(progress) => {
const percent = (progress.loaded / progress.total) * 100;
console.log(`Carga: ${percent.toFixed(1)}%`);
},
(error) => {
console.error('Error al cargar el modelo:', error);
}
);
}
Los archivos de modelo deben ubicarse en la carpeta pública del servidor para ser accesibles como recursos estáticos. En proyectos con Vite, se pueden importar directamente configurando assetsInclude en la configuración.
Aplicación de texturas a superficies
Las texturas se cargan como imágenes y se asignan a los matreiales para dar apariencia realista a las superficies:
function applyTextureToObject(mesh, imagePath) {
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(imagePath);
// Configurar el espacio de color sRGB para colores correctos
texture.colorSpace = THREE.SRGBColorSpace;
// Repetir la textura en ambas direcciones
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(4, 4);
mesh.material = new THREE.MeshStandardMaterial({
map: texture,
roughness: 0.7
});
}
Recursos y proyectos de práctica recomendados
Para consolidar los conocimientos de Three.js, se recomiendan los siguientes recursos:
- Documentación oficial: threejs.org/manual - Guías paso a paso.
- Three.js Fundamentals: threejsfundamentals.org - Tutorial estructurado desde cero.
- Discover Three.js: discoverthreejs.com - Libro en línea gratuito.
- Ejemplos oficiales: threejs.org/examples - Demos interactivas con código fuente.
- Three.js Editor: threejs.org/editor - Editor visual en línea para prototipado rápido.
Entre los proyectos prácticos sugeridos se incluyen: escenas con iluminación y sombras, carga y manipulación de modelos 3D, visualización de datos geoespaciales en globo terráqueo, entornos interactivos de interior, y juegos simples con físicas básicas.