Extrusión, torno y tubo: geometría a partir de curvas
Cómo se dibuja una forma en 2D con Shape y Path, todas las opciones de ExtrudeGeometry con sus valores reales, y cómo generan LatheGeometry y TubeGeometry su superficie.
Shape y Path: dibujar en dos dimensiones
Path es un contorno abierto y Shape lo extiende añadiendo una lista de agujeros. Su API imita a la del lienzo 2D del navegador, lo cual no es casualidad: es la misma abstracción de contornos.
import * as THREE from 'three';function rectanguloRedondeado( ancho, alto, radio ) { const s = new THREE.Shape(); const x = - ancho / 2; const y = - alto / 2; s.moveTo( x + radio, y ); s.lineTo( x + ancho - radio, y ); s.quadraticCurveTo( x + ancho, y, x + ancho, y + radio ); s.lineTo( x + ancho, y + alto - radio ); s.quadraticCurveTo( x + ancho, y + alto, x + ancho - radio, y + alto ); s.lineTo( x + radio, y + alto ); s.quadraticCurveTo( x, y + alto, x, y + alto - radio ); s.lineTo( x, y + radio ); s.quadraticCurveTo( x, y, x + radio, y ); return s;}const forma = rectanguloRedondeado( 3, 2, 0.4 );// Un agujero circular en el centro, recorrido en sentido contrario.forma.holes.push( new THREE.Path().absarc( 0, 0, 0.4, 0, Math.PI * 2, true ) );Los agujeros van en el array holes y deben recorrerse en sentido contrario al contorno exterior. Es la convención de todos los sistemas de relleno de polígonos y también lo que el triangulador de Three.js espera. Un agujero con el sentido equivocado no se resta: se dibuja encima.
Una forma no es geometría todavía: es un conjunto de curvas continuas. La conversión a segmentos la hace curveSegments, y ese parámetro se pasa a la geometría, no a la forma. Con doce, un arco completo se divide en doce tramos, lo cual para un círculo grande es visiblemente poligonal.
ShapeGeometry y ExtrudeGeometry
ShapeGeometry triangula la forma y produce una superficie plana en el plano XY:
new THREE.ShapeGeometry( shapes, curveSegments = 12 )Es indexada y solo tiene position, normal —todas apuntando a más Z— y uv. Sirve para siluetas, iconos, máscaras y suelos con forma. Su coste depende de la complejidad del contorno, no de ningún parámetro de rejilla: el triangulador emite el mínimo de triángulos necesario para cubrir el polígono.
ExtrudeGeometry toma la misma forma y le da profundidad:
new THREE.ExtrudeGeometry( shapes, options = {} )Estas son todas las opciones con sus valores por defecto reales:
| Opción | Por defecto | Qué hace |
|---|---|---|
curveSegments |
12 |
Subdivisión de las curvas del contorno |
steps |
1 |
Divisiones a lo largo de la extrusión |
depth |
1 |
Profundidad de la extrusión |
bevelEnabled |
true |
Activa el chaflán |
bevelThickness |
0.2 |
Cuánto sobresale el chaflán en profundidad |
bevelSize |
bevelThickness - 0.1 |
Cuánto se retrae en el plano de la forma |
bevelOffset |
0 |
Desplazamiento del inicio del chaflán |
bevelSegments |
3 |
Divisiones del chaflán |
extrudePath |
sin valor | Curva a lo largo de la cual extruir |
UVGenerator |
interno | Objeto que genera las coordenadas de textura |
Tres interacciones hay que conocer. bevelEnabled está activado por defecto, con un grosor de 0,2 unidades: si tu forma mide dos unidades, eso es un chaflán del diez por ciento del tamaño, enorme. Casi todo el mundo que extruye por primera vez obtiene una forma hinchada y no sabe por qué. bevelSize se deriva de bevelThickness, así que cambiar uno cambia el otro salvo que fijes ambos. Y extrudePath fuerza bevelEnabled a falso, porque el chaflán no está soportado en extrusión a lo largo de una curva.
Cuando el chaflán está desactivado, el generador pone a cero los cuatro parámetros relacionados, así que no hay riesgo de residuos.
const geometria = new THREE.ExtrudeGeometry( forma, { depth: 0.4, curveSegments: 16, steps: 1, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05, bevelOffset: 0, bevelSegments: 3} );geometria.center(); // ExtrudeGeometry no centra nada por su cuentaExtrudeGeometry es no indexada y llama a computeVertexNormals() al terminar, lo que da normales suaves en todas partes, incluidas las aristas donde no las quieres. Emite dos grupos: el cero para las tapas y el uno para los laterales, lo cual permite darles materiales distintos, que es lo que se hace con letras metálicas o botones.
El coste del chaflán merece un cálculo. Con bevelSegments a tres, el generador añade tres anillos de vértices por cada extremo, es decir, seis anillos adicionales alrededor de todo el contorno. Para una forma de doscientos puntos de contorno, eso son mil doscientos vértices más y unos dos mil cuatrocientos triángulos, solo por el chaflán. Con bevelSegments a uno el chaflán sigue existiendo visualmente y cuesta un tercio.
steps solo importa si extruyes a lo largo de una curva o si vas a deformar el resultado. Para una extrusión recta, steps a uno es correcto y subirlo solo añade anillos intermedios inútiles.
LatheGeometry: el torno
Toma un perfil en dos dimensiones y lo hace girar alrededor del eje vertical:
new THREE.LatheGeometry( points, segments = 12, phiStart = 0, phiLength = Math.PI * 2 )Los puntos son Vector2 donde la coordenada X es la distancia al eje y la Y es la altura. Los valores de X deben ser no negativos; un cero cierra la forma sobre el eje.
import * as THREE from 'three';// Perfil de una copa: radio variable a lo largo de la altura.const perfil = [];for ( let i = 0; i <= 24; i ++ ) { const t = i / 24; const radio = 0.15 + 0.55 * Math.pow( Math.sin( t * Math.PI * 0.85 ), 1.6 ); perfil.push( new THREE.Vector2( radio, t * 2 ) );}const copa = new THREE.LatheGeometry( perfil, 48 );Las fórmulas, con N como número de puntos del perfil:
vertices = (segments + 1) · Ntriangulos = 2 · segments · (N − 1)Con veinticinco puntos y cuarenta y ocho segmentos: 1 225 vértices y 2 304 triángulos. Sube deprisa, porque las dos dimensiones se multiplican como en todos los sólidos de revolución.
phiLength se acota internamente al intervalo de cero a un giro completo, así que no puedes dar dos vueltas. Para una sección, phiLength menor que un giro deja la geometría abierta por los lados: no genera tapas y se ve el interior, con la advertencia habitual sobre FrontSide.
Una nota sobre las normales: LatheGeometry las calcula analíticamente a partir del perfil, no con computeVertexNormals, y hace un tratamiento especial en el primer y último punto para que las tapas planas no salgan redondeadas. Es una de las geometrías mejor resueltas del catálogo en ese aspecto.
TubeGeometry y los marcos de Frenet
Barre una sección circular a lo largo de una curva tridimensional arbitraria:
new THREE.TubeGeometry( path, tubularSegments = 64, radius = 1, radialSegments = 8, closed = false )import * as THREE from 'three';const curva = new THREE.CatmullRomCurve3( [ new THREE.Vector3( - 4, 0, 0 ), new THREE.Vector3( - 1, 2, 1 ), new THREE.Vector3( 2, - 1, - 1 ), new THREE.Vector3( 4, 1, 0 )] );const tubo = new THREE.TubeGeometry( curva, 128, 0.15, 12, false );Las fórmulas son las mismas que las del nudo:
vertices = (tubularSegments + 1) · (radialSegments + 1)triangulos = 2 · tubularSegments · radialSegmentsLo interesante es cómo decide la orientación de la sección en cada paso. Una curva en el espacio no tiene un “arriba” natural, así que hay que construir un sistema de referencia local que gire lo mínimo posible al avanzar. Eso son los marcos de Frenet, y TubeGeometry los deja accesibles:
console.log( tubo.tangents ); // Array de Vector3, uno por segmentoconsole.log( tubo.normals );console.log( tubo.binormals );Sirven para colocar objetos a lo largo del tubo con la orientación correcta —señales en una carretera, luces en un cable, vagones en una vía— sin recalcular nada:
const marcos = curva.computeFrenetFrames( 64, false );const punto = curva.getPointAt( 0.35, new THREE.Vector3() );const indice = Math.round( 0.35 * 64 );objeto.position.copy( punto );objeto.matrix.makeBasis( marcos.normals[ indice ], marcos.binormals[ indice ], marcos.tangents[ indice ] );objeto.quaternion.setFromRotationMatrix( objeto.matrix );Una distinción que importa aquí: getPoint( t ) recorre el parámetro de la curva, que no avanza a velocidad constante; getPointAt( t ) recorre la longitud de arco, y sí lo hace. Para colocar objetos equiespaciados, o para animar algo a velocidad uniforme, hay que usar el segundo. La diferencia se nota mucho en curvas con tramos de curvatura muy distinta.
El parámetro closed cierra el tubo sobre sí mismo, y para que el resultado sea limpio la curva tiene que ser realmente cerrada. Si no lo es, se ve la costura. Y hay un fenómeno inevitable con curvas cerradas: al dar la vuelta completa, el marco de Frenet puede no coincidir consigo mismo y aparece un giro residual. Three.js lo corrige repartiendo el error a lo largo del recorrido, que es la solución estándar y hace que la costura sea imperceptible salvo con texturas muy direccionales.