wandres.dev
GEOMETRÍAS INTEGRADAS · El catálogo y sus parámetros

SphereGeometry frente a IcosahedronGeometry

Por qué la esfera UV concentra sus triángulos en los polos, cuántos genera exactamente, y por qué el icosaedro reparte mejor pero cuesta cinco veces más vértices.

⏱ 18 min

Three.js tiene dos formas de hacer una esfera y la elección no es cuestión de gusto. Una está construida por paralelos y meridianos, es indexada y tiene coordenadas de textura limpias; la otra subdivide un icosaedro, reparte los triángulos casi uniformemente y no tiene índice. Sus fórmulas de coste son distintas, sus artefactos son distintos y sirven para cosas distintas. Elegir mal se paga en triángulos desperdiciados en los polos o en cinco veces más vértices de los necesarios.

🎯 Al terminar esta lección sabrás
  • Calcular vértices y triángulos de una SphereGeometry para cualquier combinación de segmentos.
  • Explicar por qué los polos generan la mitad de triángulos que el resto de las filas.
  • Cuantificar la distorsión de la distribución de triángulos hacia los polos.
  • Elegir entre las dos esferas con un criterio basado en texturas, desplazamiento y presupuesto.

SphereGeometry: la esfera UV

new THREE.SphereGeometry( radius = 1, widthSegments = 32, heightSegments = 16,
                          phiStart = 0, phiLength = Math.PI * 2,
                          thetaStart = 0, thetaLength = Math.PI )

widthSegments son los meridianos —las divisiones alrededor del eje— y heightSegments los paralelos, de polo a polo. El generador los acota: el primero a un mínimo de tres y el segundo a un mínimo de dos, porque por debajo no hay superficie.

Los vértices son la rejilla completa, con las filas y columnas de cierre duplicadas:

vertices = (widthSegments + 1) · (heightSegments + 1)

Con los valores por defecto, 33 · 17 = 561. La columna extra existe porque el vértice donde se cierra el meridiano necesita la coordenada de textura uno además de la cero: es la misma posición en el espacio pero es un vértice distinto, por la razón que ya conocemos. Es una costura de UV.

Los triángulos son más interesantes, porque hay dos casos especiales:

triangulos = 2·W·H − (thetaStart es 0 ? W : 0) − (thetaEnd es PI ? W : 0)

Con la esfera completa, ambos descuentos se aplican y queda 2·W·(H−1). Para los valores por defecto: 2·32·15 = 960 triángulos.

El descuento sale de los polos. En una rejilla normal, cada celda son dos triángulos. En la fila que toca un polo, los dos vértices “superiores” de cada celda son en realidad el mismo punto, así que uno de los dos triángulos sería degenerado —área cero— y el generador simplemente no lo emite. Es la razón de que en el código fuente los dos push de índices estén cada uno bajo su condición.

Los cuatro parámetros angulares permiten porciones. phiStart y phiLength recortan alrededor del eje: una cuña, un pacman esférico. thetaStart y thetaLength recortan de polo a polo: una cúpula, una banda ecuatorial.

const cupula = new THREE.SphereGeometry( 5, 32, 8, 0, Math.PI * 2, 0, Math.PI / 2 );

Y aquí un despiste habitual: recortar no reduce los segmentos. Una cúpula con thetaLength de medio giro y heightSegments a dieciséis tiene la misma densidad de paralelos repartida sobre la mitad de superficie, es decir, el doble de resolución de la que tenía la esfera completa. Si quieres la misma densidad, baja también heightSegments a la mitad. Con la cúpula de arriba, ocho paralelos sobre media esfera equivalen a los dieciséis de la completa.

Dónde se concentran los triángulos

Aquí está el problema estructural de la esfera UV, y se ve mejor con números que con palabras.

En una esfera de radio uno con treinta y dos meridianos y dieciséis paralelos, el paso a lo largo de un meridiano es constante: π/16 ≈ 0,196 unidades de arco, siempre. El paso a lo largo de un paralelo, en cambio, depende de la latitud: la circunferencia de un paralelo a ángulo polar θ es 2π·sin(θ), así que el paso es 2π·sin(θ)/32 ≈ 0,196·sin(θ).

Ángulo polar sin(θ) Paso horizontal Relación con el vertical
90 grados, ecuador 1,000 0,196 1 a 1, cuadrados
60 grados 0,866 0,170 1,2 a 1
30 grados 0,500 0,098 2 a 1
10 grados 0,174 0,034 5,8 a 1
5 grados 0,087 0,017 11,5 a 1

En el ecuador los cuadriláteros son cuadrados perfectos, que es lo ideal. Cerca de los polos son astillas once veces más altas que anchas. Y como la longitud del paralelo tiende a cero pero el número de meridianos no cambia, el área que cubre cada triángulo tiende a cero mientras el número de triángulos por fila se mantiene: los polos acumulan una densidad de triángulos absurda para la superficie que representan.

Las consecuencias son tres y se ven todas. Los reflejos especulares se deforman en los polos, porque la interpolación de la normal ocurre sobre triángulos con proporciones extremas. Los desplazamientos de vértice producen artefactos radiales visibles. Y desde el punto de vista del presupuesto, estás gastando triángulos en los sitios donde menos se ven.

A cambio, la esfera UV tiene una virtud que ninguna otra parametrización iguala: su mapeo de coordenadas de textura es exactamente el equirrectangular. Una panorámica, un mapa de la Tierra, cualquier textura pensada para latitud y longitud se aplica directamente y sin distorsión conceptual. Por eso todas las esferas celestes y todos los planetas usan esta y no la otra.

IcosahedronGeometry: reparto uniforme

new THREE.IcosahedronGeometry( radius = 1, detail = 0 )

Extiende PolyhedronGeometry, que toma una lista de vértices y caras y subdivide cada cara en una rejilla triangular. Con detail a n, cada cara se convierte en (n+1)² triángulos, y el icosaedro tiene veinte caras:

triangulos = 20 · (detail + 1)²
vertices   = 60 · (detail + 1)²

Fíjate en la relación: exactamente tres vértices por triángulo. PolyhedronGeometry no es indexada, y por tanto ninguna de sus derivadas lo es: ni el icosaedro, ni el octaedro, ni el tetraedro, ni el dodecaedro.

detail Triángulos Vértices
0 20 60
1 80 240
2 180 540
3 320 960
4 500 1 500
5 720 2 160
6 980 2 940

Hay un detalle de comportamiento que sorprende: el nivel cero se sombrea plano y el resto suave, sin que tú hagas nada. El generador llama a computeVertexNormals() cuando detail es cero, lo que da normales de cara, y a normalizeNormals() cuando es mayor, que al estar los vértices sobre la esfera de radio uno equivale a poner la normal igual a la posición normalizada, es decir, normales de esfera perfecta. Es la decisión correcta —un icosaedro con veinte caras se quiere ver facetado, una esfera subdividida no— pero si esperabas facetas con detail uno, ahí está el motivo.

El reparto de triángulos es casi uniforme. No perfectamente: al proyectar la rejilla plana de cada cara sobre la esfera, los triángulos cercanos a los doce vértices originales del icosaedro quedan algo más pequeños que los del centro de cada cara. La variación de área es de aproximadamente un veinte por ciento entre el mayor y el menor, frente al factor de más de diez de la esfera UV. Para efectos prácticos, es uniforme.

Las coordenadas de textura son su punto débil. PolyhedronGeometry las genera por proyección esférica y luego tiene que corregir dos problemas: la costura donde el ángulo da la vuelta y los triángulos que tocan los polos, donde la coordenada horizontal es indeterminada. El resultado es utilizable pero tiene distorsión, y una textura equirrectangular aplicada a un icosaedro no queda igual que sobre una esfera UV.

Cuál elegir

Comparémoslas a presupuesto parecido:

Geometría Triángulos Vértices Índice Bytes aprox.
SphereGeometry(1, 32, 16) 960 561 sí, 16 bits 23 700
IcosahedronGeometry(1, 6) 980 2 940 no 94 000

Con prácticamente los mismos triángulos, el icosaedro pesa cuatro veces más, y ejecuta el vertex shader tres veces por triángulo frente a menos de una de la esfera indexada. Eso es lo que cuesta el reparto uniforme, y es un precio alto que hay que justificar.

El criterio, sin rodeos:

Usa SphereGeometry cuando la esfera lleve textura, siempre. Panorámicas, planetas, cualquier cosa con un mapa equirrectangular. También cuando el presupuesto sea ajustado y la esfera se vea entera y de lejos, donde la distorsión polar no se aprecia. Y cuando necesites solo una porción, porque los parámetros angulares no tienen equivalente en el icosaedro.

Usa IcosahedronGeometry cuando vayas a desplazar los vértices, que es su caso estrella: un ruido aplicado a una esfera UV produce artefactos visibles en los polos y a una icosfera no. También para geometría facetada de estilo, con detail cero o uno. Y como base para cualquier cosa que necesite una distribución uniforme de puntos sobre una esfera.

Y si quieres lo mejor de ambos, hay una tercera vía que la gente descubre tarde: partir de un icosaedro y volver a indexarlo.

import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';

const icosfera = mergeVertices( new THREE.IcosahedronGeometry( 1, 6 ) );
console.log( icosfera.attributes.position.count );   // muy por debajo de 2940

Como las normales son la posición normalizada y las posiciones coincidentes tienen normales idénticas, la fusión funciona bien y recupera casi todo el ahorro del índice. Lo que se rompe son las UVs en la costura, donde los vértices tienen coordenadas distintas y no se fusionan, que es exactamente lo correcto.

No existe una parametrización de la esfera sin distorsión, y por eso hay que elegir qué distorsión te molesta menos

El problema de la esfera UV no es un defecto de implementación: es un teorema. La superficie de una esfera tiene curvatura gaussiana positiva constante, y el Theorema Egregium de Gauss dice que la curvatura es invariante bajo isometrías, así que no existe ninguna forma de aplanar una esfera sin deformarla. Es el mismo resultado que lleva quinientos años obligando a los cartógrafos a elegir entre conservar áreas, ángulos o distancias, porque no se pueden conservar las tres. Cuando eliges entre esfera UV e icosfera estás eligiendo exactamente lo mismo que un cartógrafo eligiendo proyección: la equirrectangular conserva la cuadrícula de latitud y longitud —y por tanto es perfecta para texturas— a costa de deformar áreas hacia los polos; la subdivisión geodésica conserva el área y la forma de los triángulos a costa de no admitir un sistema de coordenadas global limpio. Y por eso ninguna de las dos gana: son dos proyecciones distintas del mismo teorema. La consecuencia práctica más útil es que cuando un problema de la esfera te parezca resoluble con más segmentos, probablemente no lo sea: la distorsión polar de la esfera UV no desaparece al subir widthSegments, solo se hace más pequeña y más numerosa, porque la proporción entre el paso horizontal y el vertical no depende del número de segmentos sino solo de la latitud. La única solución de verdad es cambiar de parametrización, o hacer lo que hacen los motores de terreno planetario: un mapeo de cubo proyectado, que reparte el error entre seis caras y no lo concentra en dos puntos.

⚔️ Compara las dos esferas
  1. Verifica la fórmula de triángulos de la esfera para la completa, una cúpula y una banda ecuatorial.
  2. Aplica una textura de cuadrícula a las dos esferas y compara la deformación en los polos.
  3. Desplaza los vértices de ambas con una función de ruido y localiza los artefactos radiales de la esfera UV.
  4. Aplica mergeVertices a una icosfera de detalle seis y mide cuántos vértices ahorra.
  5. Construye una cúpula con la misma densidad de triángulos que la esfera completa equivalente.