Saltar al contenido

Cilindro, cono, toro y compañía: los sólidos de revolución

Cómo se reparten los triángulos entre torso y tapas, por qué los nombres de los segmentos del toro están al revés de lo que esperas, y el orden invertido del nudo.

CylinderGeometry: torso, tapas y tres grupos

JS
new THREE.CylinderGeometry( radiusTop = 1, radiusBottom = 1, height = 1,                            radialSegments = 32, heightSegments = 1,                            openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 )

radialSegments son las divisiones alrededor del eje —lo que determina la suavidad de la silueta— y heightSegments las divisiones a lo largo. La geometría se genera en dos partes independientes, y hay que contarlas por separado.

El torso es una rejilla normal:

Texto
torso_vertices  = (heightSegments + 1) · (radialSegments + 1)torso_triangulos = 2 · radialSegments · heightSegments                   − (radiusTop === 0 ? radialSegments : 0)                   − (radiusBottom === 0 ? radialSegments : 0)

Los descuentos son la misma degeneración de los polos que ya vimos en la esfera: si un radio es cero, esa fila se colapsa en un punto y la mitad de sus triángulos tendría área nula.

Las tapas son abanicos y esconden un detalle contraintuitivo:

Texto
tapa_vertices  = 2 · radialSegments + 1tapa_triangulos = radialSegments

Un abanico de treinta y dos triángulos parece que debería necesitar treinta y tres vértices: uno central y treinta y dos del borde, más uno de cierre. Pero genera sesenta y cinco. La razón es que el vértice central se duplica por cada triángulo, y no por capricho: en un abanico, el centro necesita una coordenada de textura distinta para cada sector si se quiere que el mapeo sea correcto. Es otra vez la misma regla —un vértice es una fila de atributos— aplicada al centro de un disco.

Con los valores por defecto: torso de 66 vértices y 64 triángulos, dos tapas de 65 vértices y 32 triángulos cada una. Total 196 vértices y 128 triángulos. Más de la mitad de los triángulos están en las tapas, que en muchas escenas no se ven nunca.

De ahí una optimización trivial y muy rentable: si el cilindro es un poste, un tronco o cualquier cosa cuyas tapas queden ocultas, openEnded: true recorta la mitad de la geometría.

JS
const poste = new THREE.CylinderGeometry( 0.1, 0.1, 3, 12, 1, true );// 26 vértices, 24 triángulos. Frente a 76 y 48 con las tapas.

El cilindro declara tres grupos: el cero para el torso, el uno para la tapa superior y el dos para la inferior. Como siempre, con un solo material se ignoran y con un array de tres salen tres llamadas de dibujo.

Conos, cápsulas y tapas planas

ConeGeometry no es una clase distinta con su propio generador: es un cilindro con el radio superior a cero.

JS
new THREE.ConeGeometry( radius = 1, height = 1, radialSegments = 32,                        heightSegments = 1, openEnded = false,                        thetaStart = 0, thetaLength = Math.PI * 2 )

Aplicando las fórmulas: el torso pierde radialSegments triángulos por la punta degenerada, y solo se genera una tapa. Con los valores por defecto, 2·32·1 − 32 = 32 triángulos de torso más 32 de tapa: 64 triángulos.

CapsuleGeometry sí tiene generador propio y en r184 extiende directamente BufferGeometry:

JS
new THREE.CapsuleGeometry( radius = 1, height = 1, capSegments = 4,                           radialSegments = 8, heightSegments = 1 )

height es la longitud de la parte cilíndrica, sin contar los casquetes, así que la altura total es height + 2·radius. Los tres parámetros de segmentos se combinan en una sola rejilla vertical:

Texto
filas       = 2 · capSegments + heightSegmentsvertices    = (filas + 1) · (radialSegments + 1)triangulos  = filas · radialSegments · 2

Con los valores por defecto: 9 filas, 90 vértices y 144 triángulos. Es de las geometrías más baratas del catálogo, y por eso las cápsulas son la primitiva de colisión favorita de los motores de física: aproximan bien un cuerpo humano con muy poco.

Los clamps son generosos: capSegments mínimo uno, radialSegments mínimo tres, heightSegments mínimo uno. Con capSegments a uno los casquetes son conos y la silueta se rompe; el valor por defecto de cuatro es el mínimo razonable.

Y las dos tapas planas del catálogo, para completar:

JS
new THREE.CircleGeometry( radius = 1, segments = 32, thetaStart = 0, thetaLength = Math.PI * 2 )// triangulos = segments;  vertices = segments + 2new THREE.RingGeometry( innerRadius = 0.5, outerRadius = 1, thetaSegments = 32,                        phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 )// triangulos = thetaSegments · phiSegments · 2;  vertices = (thetaSegments+1)·(phiSegments+1)

El círculo es un abanico, con la misma duplicación del centro. El anillo es una rejilla y por eso es más barato por triángulo, además de admitir subdivisión radial con phiSegments para desplazar u ondular.

TorusGeometry: cuidado con los nombres

JS
new THREE.TorusGeometry( radius = 1, tube = 0.4, radialSegments = 12, tubularSegments = 48,                         arc = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI * 2 )

Los siete parámetros son los de r184; las versiones antiguas solo tenían cinco. Y aquí está la trampa de nombres que hace perder tiempo a todo el mundo:

  • radialSegments son las divisiones de la sección del tubo, es decir, cuántos lados tiene el círculo pequeño.
  • tubularSegments son las divisiones alrededor del anillo, cuántos pasos da la sección al recorrer el círculo grande.

Es exactamente al revés de lo que sugiere la intuición, donde “radial” evoca el radio grande y “tubular” el tubo. Por eso el valor por defecto de tubularSegments es cuarenta y ocho y el de radialSegments doce: el anillo es más largo y necesita más pasos.

Texto
vertices   = (radialSegments + 1) · (tubularSegments + 1)triangulos = 2 · radialSegments · tubularSegments

Con los valores por defecto, 637 vértices y 1152 triángulos. Es de las geometrías más caras del catálogo por defecto, casi el doble que la esfera, y casi nadie se da cuenta.

Los tres parámetros angulares también son fáciles de confundir. arc recorta el anillo, produciendo un trozo de rosquilla. thetaStart y thetaLength recortan la sección del tubo, produciendo un canal o una media caña:

JS
const canal = new THREE.TorusGeometry( 3, 0.5, 8, 64, Math.PI * 2, 0, Math.PI );// Media caña circular: media sección de tubo barrida por todo el anillo.

Un aviso sobre serialización: el método estático fromJSON de TorusGeometry en r184 solo pasa cinco argumentos al constructor, así que thetaStart y thetaLength se pierden al deserializar. Si guardas y recuperas escenas con toJSON, un toro con sección recortada vuelve completo.

El nudo y la tabla de costes

TorusKnotGeometry traza una curva anudada y barre una sección circular a lo largo de ella. Su firma tiene el mismo par de parámetros de segmentos en el orden contrario al del toro:

JS
new THREE.TorusKnotGeometry( radius = 1, tube = 0.4, tubularSegments = 64,                             radialSegments = 8, p = 2, q = 3 )

Tercero tubularSegments, cuarto radialSegments. En TorusGeometry es al revés. No hay ninguna razón de diseño, es historia acumulada, y es una de esas inconsistencias que solo se aprenden equivocándose.

p y q son los números de vueltas alrededor del eje de simetría y del centro del toro. Con p y q coprimos sale un nudo de verdad; si comparten factor, sale un enlace de varios componentes. El valor por defecto, dos y tres, es el nudo trébol.

Texto
vertices   = (tubularSegments + 1) · (radialSegments + 1)triangulos = 2 · tubularSegments · radialSegments

Por defecto, 585 vértices y 1024 triángulos.

La tabla completa del catálogo con los valores por defecto, que es la referencia que conviene tener a mano:

Geometría Triángulos Vértices Índice
PlaneGeometry() 2 4 sí
BoxGeometry() 12 24 sí
IcosahedronGeometry() 20 60 no
CircleGeometry() 32 34 sí
ConeGeometry() 64 130 sí
RingGeometry() 64 66 sí
CylinderGeometry() 128 196 sí
CapsuleGeometry() 144 90 sí
SphereGeometry() 960 561 sí
TubeGeometry() 1 024 585 sí
TorusKnotGeometry() 1 024 585 sí
TorusGeometry() 1 152 637 sí

Y el criterio para elegir los segmentos de una silueta curva, que es la única decisión que de verdad importa aquí. El error de una silueta poligonal frente al círculo real es la sagita: para un círculo de radio r dividido en n lados, la desviación máxima es r · (1 − cos(π/n)). Lo que hay que garantizar es que esa desviación quede por debajo de un píxel en pantalla.

JS
function segmentosNecesarios( radioMundo, radioPantallaEnPixeles, toleranciaPx = 0.5 ) {  // Desviación relativa admisible: tolerancia en píxeles sobre el radio en píxeles.  const relativa = toleranciaPx / radioPantallaEnPixeles;  return Math.max( 3, Math.ceil( Math.PI / Math.acos( 1 - relativa ) ) );}// Un cilindro que ocupa 100 px de radio en pantalla:segmentosNecesarios( 1, 100 );   // 32segmentosNecesarios( 1, 25 );    // 16segmentosNecesarios( 1, 8 );     // 9

Los treinta y dos segmentos por defecto de CylinderGeometry corresponden, con esta cuenta, a un objeto que ocupa unos cien píxeles de radio. Para algo que se ve a lo lejos, doce sobran; para algo que llena la pantalla, sesenta y cuatro se quedan cortos. Ese es el número que hay que ajustar, y ajustarlo bien puede dividir por tres el coste de una escena llena de tuberías sin que nadie note la diferencia.