En esta entrada, vamos a dar los primeros pasos con Three.js, creando una sencilla escena, en la cual varios tipos de figuras.
El cubo que es el elemento predilecto que aparece en software como Blender como primera figura geométrica; para esto, necesitamos cumplir 3 pasos.
Elementos fundamentales en Three.js
Para poder renderizar un objeto en Three.js necesitamos 3 elementos:
- El primer elemento viene siendo la cámara, la cámara es el ojo o el medio por el cual podemos visualizar nuestra figuras geométricas, que pueden tener varias configuraciones según necesidades, pero, usualmente se una una cámara de tipo perspectiva la cual permite visualizar como si fuera el ojo humano, es decir, podemos ver profundidad.
- Otro elemento que no puede faltar es la escena, la escena es el espacio en donde colocamos nuestras figuras, en este ejemplo, un cubo; por lo tanto, lo puedes ver como si fuera el universo, el cual no es más que un espacio vacío en el cual existen estrellas, planetas...
- Finalmente tenemos nuestras figuras geométricas que es lo que queremos visualizar; a las mismas podemos animarlas, aplicar operaciones geométricas como traslación, rotación, escalado, aplicar colores, materiales, entre otros.
Hay un cuarto elemento que viene siendo el render, que no es más que el motor de renderizado que soporta todo lo demás.
Preparing the work environment
Before writing a single line of Three.js, we need to have Node.js installed on our system. With Node, we have the npm package manager, which allows us to install libraries professionally instead of downloading files manually.
We open the terminal in the project folder and run:
$ npm init -y
$ npm install three
$ npm install --save-dev viteThe first command creates the package.json file with default values. The second installs Three.js as a project dependency. The third installs Vite, a modern development server that allows us to see changes in real-time without manually refreshing the browser.
Once the dependencies are installed, we create the project folder structure:
my-project/
├── js/
│ └── three.js ← library (or imported via npm)
└── index.htmlRegardless of the method you use to load the library, the result when opening the browser should be a completely blank page without any errors in the developer console (F12). This clean state indicates that everything is correctly configured and we are ready to begin.
Preparando el entorno de trabajo
Antes de escribir una sola línea de Three.js necesitamos tener instalado Node.js en nuestro sistema. Con Node disponemos del gestor de paquetes npm, que nos permite instalar librerías de manera profesional en lugar de descargar archivos manualmente.
Abrimos la terminal en la carpeta del proyecto y ejecutamos:
$ npm init -y
$ npm install three
$ npm install --save-dev viteEl primer comando crea el archivo package.json con los valores predeterminados. El segundo instala Three.js como dependencia del proyecto. El tercero instala Vite, un servidor de desarrollo moderno que nos permite ver los cambios en tiempo real sin recargar manualmente el navegador.
Una vez instaladas las dependencias, creamos la estructura de carpetas del proyecto:
mi-proyecto/
├── js/
│ └── three.js ← librería (o importada vía npm)
└── index.htmlIndependientemente del método que uses para cargar la librería, el resultado al abrir el navegador debe ser una página completamente en blanco sin ningún error en la consola de desarrolladores (F12). Ese estado limpio indica que todo está correctamente configurado y que estamos listos para comenzar.
Creando nuestra primera figura geométrica un cubo
Para renderizar cualquier gráfico 3D en Three.js necesitamos tres elementos fundamentales que siempre aparecerán juntos en cualquier proyecto:
- Escena (Scene): el universo o espacio vacío donde colocaremos todos los objetos. Piénsalo como un lienzo tridimensional infinito.
- Cámara (Camera): el ojo u observador que mira dentro de ese universo. Sin cámara no podemos ver nada.
- Renderizador (Renderer): el motor que procesa matemáticamente la escena y la dibuja en un elemento
canvasdentro de nuestra página HTML.
Código de la clase, con esto tenemos el ejemplo mínimo de crear una escena de Three.js que consta de una escena, cámara, proceso de render y una figura:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My first three.js app</title>
<style>
body { margin: 0; }
</style>
</head>
<body>
<script src="js/three.js"></script>
<script>
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 80, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial( { color: 0x00ffff } );
const cube = new THREE.Mesh( geometry, material );
cube.position.y = 1
scene.add( cube );
camera.position.z = 2;
renderer.render(scene, camera);
</script>
</body>
</html>Explicación del código anterior
Creamos el proceso de render con:
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );En la cual especificamos el tamaño de la pantalla.
Y lo agregamos al DOM:
document.body.appendChild( renderer.domElement );También creamos la cámara y la escena:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 80, window.innerWidth / window.innerHeight, 0.1, 1000 );Creamos una figura 3D; para eso tenemos que crear la geometría:
const geometry = new THREE.BoxGeometry();Y el material, que pueden ser varios tipos, pero, el más sencillo es este, que podemos indicar un color:
const material = new THREE.MeshBasicMaterial( { color: 0x00ffff } );Lo posicionamos en donde queramos con:
cube.position.y = 1 Agregamos la escena:
scene.add( cube );Reposicionamos la cámara:
camera.position.z = 2;Y finalmente, disparamos el render:
renderer.render(scene, camera);Con esto, tenemos una escena sencilla como la mostrada en la portada de esta publicación.
Crear mallado o wireframe
Podemos activar el wireframe o el mallado fácilmente en nuestras figuras geométricas usando la opción de wireframe en los distintos mesh que creemos en Three.js.
Partiendo de un mesh básico para un cubo, como el que vimos anteriormente para crear nuestra primera escena en Three.js:
const geometry = new THREE.BoxGeometry(4,4,4)
const material = new THREE.MeshBasicMaterial({ color: 0x00FF00})Agregamos la opción de wireframe; esto lo puedes hacer para la mayoría de los mesh soportados por Three.js:
const geometry = new THREE.BoxGeometry(4,4,4)
const material = new THREE.MeshBasicMaterial({ color: 0x00FF00, wireframe: true })Y eso es todo; por lo demás, lo agregamos a la escena y tendremos una figura como el de la portada:
const cube = new THREE.Mesh( geometry, material )
scene.add( cube )Crear una esfera
En Three.js tenemos distintas figuras que podemos crear, desde rectángulos o cajas como vimos en la primera escena con Three.js, como esferas; para dibujar una esfera, tenemos la función de SphereGeometry que recibe de manera obligatoria, el radio, y cantidad de segmentos para el ancho alto:
THREE.SphereGeometry(20, 20, 20);Por lo demás, sigue la misma estructura que cualquier otra figura geométrica que se quiera establecer; es decir, pasarle el mesh y agregarlo a la escena:
var sphereGeometry = new THREE.SphereGeometry(20, 20, 20);
var sphereMaterial = new THREE.MeshBasicMaterial({
color: 0x7777FF,
wireframe: true
});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(20, 4, 2);
scene.add(sphere);Aplicar rotaciones mediante figuras geométricas
En Three.js los ejes en su entorno 3D para girar en los 360 grados, tenemos que usar radianes y con esto tenemos que usar a PI; en donde un PI completo equivale a los 360 grados; en este caso, queremos 180 grados, es decir, medio PI:
cube.rotation.x = Math.PI * .5En el ejemplo anterior, lo estamos aplicando sobre un cubo, que fue la primera figura geométrica en Three.js que creamos anteriormente.
Crear un plano
Al igual que la esfera en Three.js que generamos anteriormente, lo importante es notar que solamente cambia el nombre de la figura geométrica; en este caso, la del plano que viene siendo la de PlaneGeometry que recibe como parámetro el ancho y largo del plano
const geometryPlane= new THREE.PlaneGeometry(14, 5)Por lo demás, hay que definir el material y a agregarlo a la escena:
// plano
const geometryPlane= new THREE.PlaneGeometry(14, 5)
const materialPlane= new THREE.MeshBasicMaterial({ color: 0xFF0000, wireframe: false })
const plane= new THREE.Mesh( geometryPlane, materialPlane)
scene.add(plane)Ya con el plano agregado, puedes variar sus transformaciones geométricas, por ejemplo:
//*** POSICIONES
cube.position.x = -5
cube.rotation.x = Math.PI * .5
plane.rotation.x = Math.PI * -0.5
plane.position.set(0,-2,1)Generar un eje cartesiano
Vamos a generar una figura de ayuda, como viene siendo el axis 3D; con esto, sabremos fácilmente dónde está cada uno de los ejes 3D; como tal, no es una figura con la cual podemos hacer escenas:
const axesHelper = new THREE.AxesHelper( 10 );
scene.add( axesHelper );Comparación de Three.js con herramientas 3D como Blender
Si alguna vez has usado software de modelado como Blender o motores como Unity, encontrarás que la arquitectura de Three.js es prácticamente idéntica. En todas estas herramientas existe:
- Un espacio o universo (la escena) con ejes X, Y y Z donde se colocan los objetos.
- Figuras geométricas (primitivas) como cubos, esferas o planos que se agregan a ese espacio.
- Una cámara que determina el ángulo y la perspectiva desde la que se observa la escena.
- Una fuente de luz (que veremos más adelante) para iluminar los materiales.
- Un proceso de renderizado que convierte la información 3D en una imagen 2D.
La única diferencia fundamental es que en Blender arrastramos los elementos con el ratón, mientras que en Three.js hacemos exactamente lo mismo pero mediante código JavaScript. La lógica subyacente es la misma.
Ejercicio: alinear esfera y cubo del mismo tamaño
Cuando agregamos múltiples objetos sin especificar posición, todos aparecen en el origen (0, 0, 0), solapándose unos sobre otros. Para componer la escena correctamente debemos manipular la propiedad position de cada objeto.
Hay que tener en cuenta una diferencia importante entre geometrías: BoxGeometry define el tamaño total del cubo (ancho × alto × profundidad), mientras que SphereGeometry define el radio, que equivale a la mitad del diámetro. Por eso, para que un cubo de lado 4 ocupe el mismo espacio que una esfera de radio 2 (diámetro 4), ambos valores deben calcularse en relación:
const geometry = new THREE.BoxGeometry(4, 4, 4) // cubo de lado 4
const geometrySphera = new THREE.SphereGeometry(2, 20, 20) // esfera de radio 2 = diametro 4
// desplazamos el cubo hacia la izquierda en el eje X
cube.position.x = -5Con esto ambas figuras quedan del mismo tamaño visual y separadas en el eje X. También se puede usar el método position.set(x, y, z) para definir las tres coordenadas en una sola instrucción:
plane.position.set(0, -2, 1)Rotaciones espaciales
Cualquier objeto en Three.js puede ser rotado a través de la propiedad rotation, que funciona en los tres ejes X, Y y Z. El aspecto más importante a recordar es que las rotaciones no se expresan en grados sino en radianes.
Para trabajar cómodamente con radianes usamos la constante Math.PI de JavaScript:
Math.PI * 2→ rotación completa de 360°Math.PI→ media vuelta de 180°Math.PI * 0.5→ un cuarto de vuelta de 90°Math.PI * 0.25→ un octavo de vuelta de 45°
El signo del valor controla la dirección: positivo rota en un sentido, negativo en el sentido contrario.
cube.rotation.x = Math.PI * .5 // rota el cubo 90° sobre el eje X
plane.rotation.x = Math.PI * -0.5 // acuesta el plano horizontalmenteLas rotaciones también pueden corregir problemas de visualización. Si un cubo y una esfera del mismo tamaño parecen tener dimensiones distintas al observarlos de frente, una pequeña rotación del cubo revela su profundidad real y confirma que ambos objetos son iguales.
Generar un plano base
Un plano es una superficie rectangular plana que se crea con PlaneGeometry. Acepta el ancho y el alto como parámetros, y opcionalmente la cantidad de segmentos en cada dimensión:
// plano
const geometryPlane = new THREE.PlaneGeometry(14, 5)
const materialPlane = new THREE.MeshBasicMaterial({ color: 0xFFFF00, wireframe: true })
const plane = new THREE.Mesh(geometryPlane, materialPlane)
scene.add(plane)Por defecto el plano aparece vertical, de cara a la cámara, como una pared. Para convertirlo en un suelo o mesa debemos acostarlo rotándolo 90° sobre el eje X y luego bajar su posición en Y para que quede justo por debajo de las demás figuras:
plane.rotation.x = Math.PI * -0.5 // acuesta el plano en posicion horizontal
plane.position.set(0, -2, 1) // lo desplaza hacia abajo y un poco al frenteEl resultado es un suelo sobre el que descansan visualmente el cubo y la esfera, lo que da contexto espacial a toda la escena.
El AxesHelper: visualizar los ejes de coordenadas
Cuando se trabaja en un espacio tridimensional es fácil confundirse con la orientación de los ejes. Three.js incluye el objeto AxesHelper precisamente para esto: dibuja tres líneas de colores que parten del origen y señalan cada eje:
- Rojo → eje X (ancho, izquierda-derecha)
- Verde → eje Y (altura, arriba-abajo)
- Azul → eje Z (profundidad, frente-atrás)
const axes = new THREE.AxesHelper(5)
scene.add(axes)
axes.position.x = 3El parámetro numérico indica la longitud de cada línea. El AxesHelper es un objeto de ayuda para el desarrollo que conviene eliminar antes de publicar el proyecto final.
lookAt: apuntar la cámara a un objetivo
Cuando elevamos o desplazamos la cámara para obtener un ángulo interesante, puede ocurrir que deje de apuntar hacia la escena y veamos solo negro. Calcular manualmente los ángulos de rotación exactos para que la cámara mire donde queremos es tedioso y propenso a errores.
El método lookAt() resuelve esto de manera automática: le pasamos las coordenadas del punto u objeto que queremos observar y Three.js calcula por sí solo todos los ángulos de rotación necesarios:
camera.position.z = 9
camera.position.y = 7
camera.position.x = 8
camera.lookAt(cube.position) // apunta automaticamente al cuboTambién podemos apuntar al centro de la escena entera:
camera.lookAt(scene.position)Independientemente de la posición de la cámara, lookAt() garantiza que siempre mirará al objetivo indicado, lo que facilita enormemente la composición de la escena.
El código fuente completo
A continuación se muestra el código fuente completo de todo lo que hemos construido a lo largo de este capítulo. Cada sección está comentada para facilitar su lectura:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Primeros pasos con Three.js</title>
<style>
body { margin: 0; }
</style>
</head>
<body>
<script src="../js/three.js"></script>
<script>
// escena - universo
const scene = new THREE.Scene()
// camara u ojo
const camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight)
// renderizado
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
// agregamos el proceso del render al documento
document.body.appendChild(renderer.domElement)
// geometria, elementos poligonales y otras figuras 3D
const geometry = new THREE.BoxGeometry(4, 4, 4)
const material = new THREE.MeshBasicMaterial({ color: 0x00FF00, wireframe: true })
// cubo
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
// esfera geometria y material
const geometrySphera = new THREE.SphereGeometry(2, 20, 20)
const materialSphera = new THREE.MeshBasicMaterial({ color: 0x00FFFF, wireframe: true })
const sphera = new THREE.Mesh(geometrySphera, materialSphera)
scene.add(sphera)
// plano
const geometryPlane = new THREE.PlaneGeometry(14, 5)
const materialPlane = new THREE.MeshBasicMaterial({ color: 0xFFFF00, wireframe: true })
const plane = new THREE.Mesh(geometryPlane, materialPlane)
scene.add(plane)
const axes = new THREE.AxesHelper(5)
scene.add(axes)
//*** POSICIONES
cube.position.x = -5
cube.rotation.x = Math.PI * .5
plane.rotation.x = Math.PI * -0.5
plane.position.set(0, -2, 1)
axes.position.x = 3
camera.position.z = 9
camera.position.y = 7
camera.position.x = 8
camera.lookAt(cube.position)
// renderizar
renderer.render(scene, camera)
</script>
</body>
</html>Resumen del capítulo
En este capítulo establecimos los fundamentos sobre los que se construye cualquier proyecto Three.js:
- Configuramos el entorno con Node.js y Vite para un flujo de trabajo moderno.
- Aprendimos la tríada esencial: Scene, Camera y Renderer.
- Creamos nuestras primeras primitivas:
BoxGeometry,SphereGeometryyPlaneGeometry. - Activamos el wireframe para comprender la estructura interna de las mallas.
- Manipulamos
positionyrotation(en radianes conMath.PI) para componer la escena. - Usamos
AxesHelpercomo herramienta visual de orientación durante el desarrollo. - Simplificamos la orientación de la cámara con el poderoso método
lookAt().
Con estas herramientas en mano estamos preparados para avanzar hacia escenas más complejas: animaciones, iluminación, texturas y controles interactivos que veremos en los próximos capítulos.
Fuente:
- https://github.com/libredesarrollo/curso-threejs-fundamentos-01/blob/main/base/esqueleto.html
- https://github.com/libredesarrollo/curso-threejs-fundamentos-01/blob/main/base/01_primeros_pasos.html
Proyecto Jardín
En este capítulo consolidaremos lo aprendido en el capítulo anterior construyendo un proyecto completo desde cero: un pequeño jardín tridimensional. La escena incluye un piso verde, una cerca de madera que delimita el espacio, un árbol compuesto por tronco y follaje, y una casa formada por una base cilíndrica y un techo cónico. A lo largo del proceso aprenderemos nuevas geometrías, técnicas de posicionamiento avanzadas y cómo organizar el código en funciones reutilizables.
Presentación del proyecto
El objetivo final es una escena que a primera vista parece sencilla —casi un dibujo infantil— pero que encierra varios desafíos técnicos:
- Componer objetos apilando varias geometrías (la casa, el árbol).
- Posicionar cada elemento correctamente dentro del espacio 3D.
- Descubrir y usar nuevas geometrías:
CylinderGeometryyConeGeometry. - Organizar el código en funciones para mantenerlo limpio y escalable.
El resultado final se ve así desde arriba: un plano verde rectangular rodeado por cuatro barras marrones que forman una cerca, un árbol a la izquierda y una casa a la derecha.
Preparar el esqueleto HTML
El punto de partida es el mismo esqueleto HTML del capítulo anterior: un documento con margin: 0 en el body, la librería Three.js incluida y un bloque <script> donde trabajaremos. Verificamos en el navegador que aparezca una página en blanco sin errores en la consola antes de añadir cualquier objeto.
El piso: PlaneGeometry como suelo del jardín
Lo primero que construimos es el suelo del jardín. Usamos un PlaneGeometry de 20 × 10 unidades con un color verde intenso. El plano aparece inicialmente de pie, como una pared, así que lo rotamos -90° sobre el eje X para acostarlo horizontalmente y lo bajamos ligeramente en Y para que sirva de suelo:
function floor(scene) {
const planeGeometry = new THREE.PlaneGeometry(20, 10)
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0x779922,
wireframe: false
})
var plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.rotation.x = -0.5 * Math.PI
plane.position.y = -0.5
scene.add(plane)
}Nota el uso de wireframe: false: a diferencia del capítulo anterior, aquí queremos un plano sólido que simule hierba, no un armazón de alambres. El valor -0.5 * Math.PI equivale a -90° en radianes.
La cerca: reutilizando geometrías y materiales
La cerca está formada por cuatro barras rectangulares de madera que rodean el jardín. Aquí aplicamos un patrón importante: el mismo material puede asignarse a varias mallas distintas. Creamos el material marrón una sola vez y lo reutilizamos en las cuatro barras:
function bar(scene) {
const barMaterial = new THREE.MeshBasicMaterial({ color: 0x994422 })
// barras izquierda y derecha (eje Z, largo 11)
const geometryBarRightLeft = new THREE.BoxGeometry(1, 1, 11)
const barRight = new THREE.Mesh(geometryBarRightLeft, barMaterial)
barRight.position.x = 10
scene.add(barRight)
const barLeft = new THREE.Mesh(geometryBarRightLeft, barMaterial)
barLeft.position.x = -10
scene.add(barLeft)
// barras superior e inferior (eje X, largo 19)
const geometryBarUpBottom = new THREE.BoxGeometry(19, 1, 1)
const barUp = new THREE.Mesh(geometryBarUpBottom, barMaterial)
barUp.position.z = 5
scene.add(barUp)
const barBottom = new THREE.Mesh(geometryBarUpBottom, barMaterial)
barBottom.position.z = -5
scene.add(barBottom)
}Observa la lógica de posicionamiento: el jardín mide 20 unidades de ancho, así que el origen está en el centro. Para colocar la barra derecha en el borde derecho la desplazamos +10 en X (la mitad del ancho). La izquierda va a -10. Las barras frontal y trasera se colocan en Z = ±5 (la mitad de los 10 de profundidad).
También es posible definir dos geometrías distintas (una para las barras laterales y otra para las frontales/traseras) porque sus dimensiones son diferentes: las laterales son largas en Z y las frontales son largas en X.
El árbol: composición de dos geometrías
El árbol demuestra cómo construir objetos complejos apilando primitivas simples. Está formado por:
- Tronco: un
BoxGeometrydelgado y alto (color marrón oscuro). - Follaje: una
SphereGeometryde pocos segmentos (color verde) colocada encima del tronco.
function tree(scene) {
// ARBOL
const trunkGeometry = new THREE.BoxGeometry(1, 8, 1)
const trunkMaterial = new THREE.MeshBasicMaterial({ color: 0x110000 })
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial)
trunk.position.y = 3
trunk.position.x = -3
scene.add(trunk)
const fodderGeometry = new THREE.SphereGeometry(4, 5, 6)
const fodderMaterial = new THREE.MeshBasicMaterial({ color: 0x00AA00 })
const fodder = new THREE.Mesh(fodderGeometry, fodderMaterial)
fodder.position.y = 9
fodder.position.x = -3
scene.add(fodder)
// FIN ARBOL
}El tronco tiene altura 8, por lo que crece 4 unidades hacia arriba y 4 hacia abajo desde su centro. Para que arranque desde el suelo lo desplazamos y = 3. El follaje (radio 4) se coloca en y = 9 para quedar justo sobre la punta del tronco. Ambos comparten la misma posición x = -3 para alinearse verticalmente.
Usar pocos segmentos en la esfera del follaje (5, 6) da un aspecto facetado y caricaturesco, coherente con el estilo de jardín de ilustración.
La casa: CylinderGeometry y ConeGeometry
La casa presenta dos geometrías nuevas. La base es un cilindro (CylinderGeometry) y el techo es un cono (ConeGeometry). La combinación de ambos forma una casa estilizada.
CylinderGeometry recibe: radio superior, radio inferior, altura y número de segmentos. Al usar el mismo valor de radio arriba y abajo obtenemos un cilindro perfecto:
function house(scene) {
// Base de la casa
const houseBaseGeometry = new THREE.CylinderGeometry(2, 2, 3)
const houseBaseMaterial = new THREE.MeshBasicMaterial({ color: 0xFFEECC })
const houseBase = new THREE.Mesh(houseBaseGeometry, houseBaseMaterial)
houseBase.position.y = 1
houseBase.position.x = 5
scene.add(houseBase)
// Techo en forma de cono
const houseRoofGeometry = new THREE.ConeGeometry(2, 3, 15)
const houseRoofMaterial = new THREE.MeshBasicMaterial({ color: 0x776600 })
const houseRoof = new THREE.Mesh(houseRoofGeometry, houseRoofMaterial)
houseRoof.position.y = 4
houseRoof.position.x = 5
scene.add(houseRoof)
// FIN Casa
}ConeGeometry recibe: radio de la base, altura y número de segmentos. Con 15 segmentos el cono luce suficientemente redondo para parecerse a un tejado. Si se usan pocos segmentos (3 o 4) el resultado sería una pirámide.
El cálculo de alturas es clave para que base y techo encajen: la base tiene altura 3 y su centro está en y = 1, por lo que su borde superior queda en y = 2.5. El techo se coloca en y = 4 para que su base arranque a esa misma altura sin hueco visible.
También se intentó usar un BoxGeometry como base de la casa, pero al intentar rematarlo con el cono surgió una incompatibilidad visual: un cuadrado con un techo circular no encaja bien. El cilindro, al tener la misma sección circular que la base del cono, produce una unión perfecta.
Refactorización: organizar el código en funciones
Cuando todo el código reside en un solo bloque plano, escalar la aplicación añadiendo más objetos se vuelve caótico. La solución es encapsular cada elemento en su propia función y crear una función init() que inicialice los elementos base (escena, cámara, renderizador) y llame a todas las demás pasándoles la escena como parámetro:
function init() {
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// llamadas a cada elemento del jardín
house(scene)
bar(scene)
tree(scene)
floor(scene)
camera.position.z = 20
camera.position.y = 20
camera.lookAt(scene.position)
renderer.render(scene, camera)
}
init()Este patrón tiene ventajas claras:
- Sin variables globales: la escena se crea dentro de
init()y se pasa como argumento a cada función, evitando el acceso desde el ámbito global. - Modularidad: para añadir un nuevo elemento basta con crear una función
function miElemento(scene)y llamarla desdeinit(). - Legibilidad: cada función tiene una responsabilidad única y su nombre describe qué construye.
Ajuste de la cámara con lookAt
Con varios objetos dispersos en la escena es importante apuntar la cámara hacia el centro del jardín. En lugar de calcular manualmente los ángulos de rotación, usamos lookAt() apuntando a la posición de la escena (el origen 0,0,0):
camera.position.z = 20
camera.position.y = 20
camera.lookAt(scene.position)Posicionar la cámara en y = 20 y z = 20 da un ángulo isométrico diagonal que muestra bien tanto el suelo como los objetos. lookAt(scene.position) se encarga de rotar la cámara automáticamente para que mire hacia el origen, sin importar dónde esté ubicada.
Ajuste fino de posiciones
Una vez ensamblados todos los objetos es habitual que algunos floten o se intersequen con el suelo. Algunos ajustes que se realizan en esta fase final:
- El suelo baja a
y = -0.5en lugar de-1para eliminar el espacio muerto que quedaba entre el piso y la base de los objetos. - Los objetos que flotan se desplazan en Y hasta que su base toca el plano.
- Las barras de la cerca se reajustan en Z para quedar exactamente en el borde del plano sin sobresalir ni quedarse cortas.
Para verificar el posicionamiento es útil cambiar temporalmente la posición de la cámara para observar la escena desde diferentes ángulos (desde arriba con y positivo, desde abajo con y negativo, de lado ajustando solo z). Esto ayuda a detectar objetos que sobresalen o dejan huecos indeseados.
El código fuente completo
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proyecto Jardín</title>
<style>
body { margin: 0; }
</style>
</head>
<body>
<script src="../js/three.js"></script>
<script>
function init() {
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
house(scene)
bar(scene)
tree(scene)
floor(scene)
camera.position.z = 20
camera.position.y = 20
camera.lookAt(scene.position)
renderer.render(scene, camera)
}
function house(scene) {
// Casa: base cilindrica
const houseBaseGeometry = new THREE.CylinderGeometry(2, 2, 3)
const houseBaseMaterial = new THREE.MeshBasicMaterial({ color: 0xFFEECC })
const houseBase = new THREE.Mesh(houseBaseGeometry, houseBaseMaterial)
houseBase.position.y = 1
houseBase.position.x = 5
scene.add(houseBase)
// Techo en forma de cono
const houseRoofGeometry = new THREE.ConeGeometry(2, 3, 15)
const houseRoofMaterial = new THREE.MeshBasicMaterial({ color: 0x776600 })
const houseRoof = new THREE.Mesh(houseRoofGeometry, houseRoofMaterial)
houseRoof.position.y = 4
houseRoof.position.x = 5
scene.add(houseRoof)
}
function bar(scene) {
const barMaterial = new THREE.MeshBasicMaterial({ color: 0x994422 })
const geometryBarRightLeft = new THREE.BoxGeometry(1, 1, 11)
const barRight = new THREE.Mesh(geometryBarRightLeft, barMaterial)
barRight.position.x = 10
scene.add(barRight)
const barLeft = new THREE.Mesh(geometryBarRightLeft, barMaterial)
barLeft.position.x = -10
scene.add(barLeft)
const geometryBarUpBottom = new THREE.BoxGeometry(19, 1, 1)
const barUp = new THREE.Mesh(geometryBarUpBottom, barMaterial)
barUp.position.z = 5
scene.add(barUp)
const barBottom = new THREE.Mesh(geometryBarUpBottom, barMaterial)
barBottom.position.z = -5
scene.add(barBottom)
}
function floor(scene) {
const planeGeometry = new THREE.PlaneGeometry(20, 10)
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0x779922,
wireframe: false
})
var plane = new THREE.Mesh(planeGeometry, planeMaterial)
plane.rotation.x = -0.5 * Math.PI
plane.position.y = -0.5
scene.add(plane)
}
function tree(scene) {
// Tronco
const trunkGeometry = new THREE.BoxGeometry(1, 8, 1)
const trunkMaterial = new THREE.MeshBasicMaterial({ color: 0x110000 })
const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial)
trunk.position.y = 3
trunk.position.x = -3
scene.add(trunk)
// Forraje / hojas
const fodderGeometry = new THREE.SphereGeometry(4, 5, 6)
const fodderMaterial = new THREE.MeshBasicMaterial({ color: 0x00AA00 })
const fodder = new THREE.Mesh(fodderGeometry, fodderMaterial)
fodder.position.y = 9
fodder.position.x = -3
scene.add(fodder)
}
init()
</script>
</body>
</html>Resumen del capítulo
En este capítulo aplicamos todos los fundamentos del capítulo anterior en un proyecto realista:
- Construimos el piso con
PlaneGeometrysólido (sin wireframe) y lo rotamos con radianes. - Aprendimos CylinderGeometry: radio superior, radio inferior, altura y segmentos.
- Aprendimos ConeGeometry: radio de la base, altura y segmentos.
- Vimos cómo apilar primitivas para crear objetos complejos (árbol = tronco + esfera; casa = cilindro + cono).
- Practicamos la reutilización de materiales en múltiples mallas.
- Refactorizamos el código usando funciones con
scenecomo parámetro, siguiendo el patróninit(). - Ajustamos la cámara con
lookAt(scene.position)para enmarcar la escena completa.
Este patrón de funciones separadas por elemento es la base del flujo de trabajo que utilizaremos en los proyectos más complejos de los próximos capítulos.
Fuente: