El catálogo, la caja y el plano: aprender a contar triángulos
Las veintiuna geometrías que exporta r184, por qué un cubo por defecto tiene veinticuatro vértices y seis grupos, y cómo calcular exactamente lo que cuesta cada elección de segmentos.
El catálogo de r184
El módulo de geometrías del núcleo exporta exactamente veintiuna clases:
| Familia | Clases |
|---|---|
| Rectilíneas | BoxGeometry, PlaneGeometry |
| Revolución | SphereGeometry, CylinderGeometry, ConeGeometry, TorusGeometry, TorusKnotGeometry, CapsuleGeometry, LatheGeometry, TubeGeometry |
| Poliedros | PolyhedronGeometry, TetrahedronGeometry, OctahedronGeometry, IcosahedronGeometry, DodecahedronGeometry |
| Planas | CircleGeometry, RingGeometry, ShapeGeometry |
| Derivadas de curvas | ExtrudeGeometry |
| Auxiliares | EdgesGeometry, WireframeGeometry |
TextGeometry no está en esa lista: vive en los addons, en three/addons/geometries/TextGeometry.js, y extiende ExtrudeGeometry.
Todas guardan los argumentos con los que se construyeron en una propiedad parameters, que es lo que permite serializarlas y reconstruirlas sin volcar los vértices. Es también la forma más fiable de inspeccionar una geometría que te llega de otro sitio:
const g = new THREE.SphereGeometry( 2, 48, 24 );console.log( g.parameters );// { radius: 2, widthSegments: 48, heightSegments: 24, phiStart: 0, ... }Las dos auxiliares merecen mención aparte porque no generan sólidos: EdgesGeometry( geometria, umbralEnGrados ) extrae solo las aristas cuyo ángulo diedro supera el umbral —las que perfilan la forma— y WireframeGeometry extrae todas. Las dos producen geometrías de líneas y se usan con LineSegments, no con Mesh.
BoxGeometry: por qué veinticuatro vértices
new THREE.BoxGeometry( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 )Con los valores por defecto genera veinticuatro vértices y doce triángulos. Los doce triángulos son evidentes: seis caras de dos triángulos. Los veinticuatro vértices no lo son, porque un cubo tiene ocho esquinas.
La razón está en cómo se construye. El generador no crea ocho puntos y los conecta: llama seis veces a una función que construye un plano, una por cara, y cada plano genera sus propios vértices. Nunca se comparte nada entre caras. Y eso no es una implementación perezosa, es lo único posible: cada esquina del cubo pertenece a tres caras con tres normales distintas y tres coordenadas de textura distintas, y como un vértice es una fila de la tabla de atributos, tres combinaciones distintas son tres vértices. Ocho esquinas por tres caras cada una dan veinticuatro. Este es el hecho que hay que interiorizar y al que volveremos al construir geometría a mano.
Las fórmulas generales, con wS, hS y dS por segmentos de anchura, altura y profundidad:
vertices = 2·(dS+1)·(hS+1) + 2·(wS+1)·(dS+1) + 2·(wS+1)·(hS+1)triangulos = 4·( dS·hS + wS·dS + wS·hS )Los tres sumandos son los tres pares de caras opuestas. Con todo a uno: 2·4 + 2·4 + 2·4 = 24 vértices y 4·(1+1+1) = 12 triángulos. Con todo a diez: 2·121·3 = 726 vértices y 4·300 = 1200 triángulos.
La geometría es indexada, con lo cual el índice tiene treinta y seis entradas para los doce triángulos y no ahorra ni un vértice: cada cara de un segmento tiene cuatro vértices y seis índices, así que el índice cuesta más de lo que ahorra. Es el ejemplo de manual de geometría donde indexar no aporta nada, y aun así Three.js lo hace por uniformidad: hay código —computeTangents, mergeVertices, los algoritmos de simplificación— que exige índice.
Seis grupos y su precio
BoxGeometry declara seis grupos, uno por cara, con índices de material del cero al cinco en el orden más X, menos X, más Y, menos Y, más Z, menos Z. Si le pasas un array de seis materiales, cada cara usa el suyo.
const cubo = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), [ new THREE.MeshStandardMaterial( { color: 0xf38ba8 } ), new THREE.MeshStandardMaterial( { color: 0xa6e3a1 } ), new THREE.MeshStandardMaterial( { color: 0x89b4fa } ), new THREE.MeshStandardMaterial( { color: 0xf9e2af } ), new THREE.MeshStandardMaterial( { color: 0xcba6f7 } ), new THREE.MeshStandardMaterial( { color: 0xfab387 } )] );Con un solo material, los grupos se ignoran y sale una única llamada de dibujo. Con seis, salen seis. Es un coste real y multiplicable: cien cubos con seis materiales son seiscientas llamadas de dibujo, y ahí ya estás en territorio de problema. Cuando necesites caras distintas en muchos objetos, la respuesta correcta casi siempre es un atlas de texturas y un solo material, no seis materiales.
PlaneGeometry y el suelo
new THREE.PlaneGeometry( width = 1, height = 1, widthSegments = 1, heightSegments = 1 )Es la más simple del catálogo. Fórmulas:
vertices = (widthSegments + 1) · (heightSegments + 1)triangulos = 2 · widthSegments · heightSegmentsUn plano de cien por cien segmentos son 10 201 vértices y 20 000 triángulos.
Dos cosas hay que saber siempre. La primera es que el plano mira hacia el eje Z positivo, no hacia arriba. Para un suelo hay que girarlo, y hay dos formas que no son equivalentes:
// Opción A: girar la malla. La geometría sigue en el plano XY.const suelo = new THREE.Mesh( new THREE.PlaneGeometry( 20, 20 ), material );suelo.rotation.x = - Math.PI / 2;// Opción B: girar la geometría. Los vértices se reescriben una vez.const g = new THREE.PlaneGeometry( 20, 20 );g.rotateX( - Math.PI / 2 );const suelo2 = new THREE.Mesh( g, material );La opción B deja el objeto con rotación identidad, lo que simplifica cualquier razonamiento posterior sobre transformaciones y hace que las normales ya salgan hacia arriba en espacio local. La opción A es reversible y no toca los datos. Para un suelo estático, la B; para algo que vayas a orientar dinámicamente, la A.
La segunda es que un plano solo se ve por delante, porque los materiales usan FrontSide por defecto. Un suelo que desaparece al mirarlo desde abajo no es un bug: es la respuesta correcta al culling de caras traseras.
Contar triángulos
La función vale para cualquier geometría y hay que tener en cuenta el índice:
function contarTriangulos( geometria ) { const entradas = geometria.index ? geometria.index.count : geometria.attributes.position.count; return entradas / 3;}function contarVertices( geometria ) { return geometria.attributes.position.count;}Los dos números son distintos y los dos importan. Los triángulos determinan el trabajo del rasterizador; los vértices, el del vertex shader y el peso en memoria. Una geometría no indexada con mil triángulos tiene tres mil vértices; una indexada bien construida puede tener quinientos.
Y para el total real de la escena, la fuente de verdad es el renderer, que cuenta lo que se ha dibujado de verdad después del culling:
renderer.render( scene, camera );console.log( renderer.info.render.calls, renderer.info.render.triangles );Ese contador se resetea automáticamente al principio de cada render() salvo que pongas renderer.info.autoReset = false, que es lo que hay que hacer cuando renderizas varias veces por frame —post-procesado, sombras manuales, varias vistas— y quieres el total. El reinicio ocurre justo después de la pasada de sombras y antes de la pasada principal, así que la cifra que lees tras render() es la de la escena y no incluye lo que dibujaron los mapas de sombra: para contar también eso hay que desactivar autoReset y llamar a renderer.info.reset() tú al cerrar el frame.
Ahora la pregunta que importa: ¿cuándo merece la pena subir los segmentos de una caja o un plano? La respuesta corta es que casi nunca por motivos de iluminación. Los materiales PBR de Three.js calculan la iluminación por fragmento, interpolando la normal entre los vértices, así que subdividir una superficie plana no cambia absolutamente nada de cómo se ilumina: la normal interpolada entre cuatro normales idénticas sigue siendo la misma.
Los segmentos hacen falta por tres motivos concretos y solo por esos tres: porque vas a desplazar los vértices, en la CPU o en el vertex shader, y necesitas resolución donde deformar; porque usas un mapa de desplazamiento, que actúa sobre vértices y no sobre fragmentos; o porque la superficie es tan grande que la interpolación de algo por vértice —un color, una coordenada— se nota. Fuera de esos casos, un plano de cien por cien segmentos son veinte mil triángulos gastados en algo que dos habrían resuelto igual de bien.