wandres.dev
NIVEL DIOS · Síntesis del 3D en la web

El mapa de decisiones técnicas del track completo

Los árboles de decisión que resumen cincuenta y dos niveles: qué geometría, qué material, qué shading, qué renderer y qué entrega, con el criterio explícito de cada bifurcación.

⏱ 24 min

Un track de cincuenta y tres niveles produce mucho conocimiento y poca capacidad de decisión, porque el conocimiento se organiza por temas y las decisiones se toman por situaciones. Esta lección le da la vuelta: en lugar de “qué es el instancing”, responde a “tengo cinco mil objetos, qué hago”. Son los cinco árboles que se usan de verdad al empezar un proyecto, con el criterio de cada bifurcación escrito, porque un árbol sin criterio es un dibujo bonito.

🎯 Al terminar esta lección sabrás
  • Identificar el recurso limitante de una escena antes de tomar cualquier decisión técnica.
  • Elegir la representación de los objetos según lo que varíe entre ellos.
  • Elegir material y aproximación al shading según coste y control necesarios.
  • Elegir renderer, formato de entrega y estrategia de carga con criterios verificables.

Primero: qué limita tu escena

Ninguna decisión posterior tiene sentido sin esta. Todas las técnicas de optimización atacan un recurso concreto, y aplicarlas cuando el cuello está en otro es trabajo perdido con degradación visual gratuita.

flowchart TB
a[Baja la resolucion del render a la mitad] --> b{Mejora mucho el frame}
b -->|Si| c[Limitado por relleno o ancho de banda]
b -->|No| d[Renderiza una escena vacia]
d --> e{Sigue lento}
e -->|Si| f[El cuello esta fuera del render]
e -->|No| g{Muchas llamadas de dibujo}
g -->|Si| h[Limitado por CPU]
g -->|No| i[Limitado por vertices o por sombras]
c --> j[Ataca overdraw fragment shader y resolucion]
h --> k[Ataca agrupacion de objetos y materiales]
i --> l[Ataca conteo de vertices skinning y shadow maps]
f --> m[Ataca logica de JavaScript y reconciliacion]
style c fill:#f38ba8,color:#11111b
style h fill:#f9e2af,color:#11111b
style i fill:#89b4fa,color:#11111b
style f fill:#cba6f7,color:#11111b
style j fill:#a6e3a1,color:#11111b
style k fill:#a6e3a1,color:#11111b
style l fill:#a6e3a1,color:#11111b
style m fill:#a6e3a1,color:#11111b

Las dos pruebas de la izquierda cuestan un minuto y descartan tres cuartas partes de las hipótesis. Bajar la resolución cambia el número de fragmentos sin tocar nada más; renderizar una escena vacía elimina todo el trabajo de render y deja solo tu lógica. Entre las dos acotan el problema antes de perfilar nada.

El detalle que más gente se salta: la respuesta cambia entre dispositivos. La misma escena puede estar limitada por CPU en un escritorio con gráfica potente y por relleno en un móvil con pantalla de alta densidad. Las decisiones de esta lección hay que tomarlas para el dispositivo objetivo, no para el de desarrollo.

Geometría y objetos

La pregunta que resuelve este árbol no es “cuál es más rápido” sino “qué necesito que varíe entre objetos”. Todo lo demás sale de ahí, y se desarrolla en fusionar o instanciar.

flowchart TB
a{Cuantos objetos} -->|Menos de 100| b[Mesh normal por objeto]
a -->|Mas de 100| c{Comparten material}
c -->|No| d[Reduce primero la variedad de materiales]
c -->|Si| e{Comparten geometria}
e -->|Si| f{Necesitas culling por objeto}
f -->|No| g[InstancedMesh]
f -->|Si| h[BatchedMesh con perObjectFrustumCulled]
e -->|No| i{Son estaticos y estan juntos}
i -->|Si| j[mergeGeometries]
i -->|No| h
d --> k[Atlas o texture array y material comun]
k --> e
style b fill:#89b4fa,color:#11111b
style g fill:#a6e3a1,color:#11111b
style h fill:#a6e3a1,color:#11111b
style j fill:#f9e2af,color:#11111b
style d fill:#f38ba8,color:#11111b

El nodo rojo es el que más veces se salta y el que más veces bloquea todo lo demás: si los objetos tienen materiales distintos, ninguna técnica de agrupación se aplica. El trabajo previo es unificar materiales, y de ahí que el atlas y los texture arrays aparezcan en el camino y no como una optimización aparte.

mergeGeometries está en amarillo porque su ventaja viene con una pérdida seria: multiplica la memoria por el número de copias y elimina el culling individual. Es correcto para grupos estáticos y compactos, y una mala idea para cualquier cosa repartida por un mundo grande.

Sobre el detalle de la malla, la regla que resume el nivel de mundos grandes: si un objeto puede ocupar menos de cincuenta píxeles de altura en pantalla, necesita un nivel de detalle. Por debajo de eso, los triángulos son subpíxel y el hardware los procesa en cuadrados de dos por dos, desperdiciando tres cuartas partes del trabajo.

Material, luz y shading

Aquí hay dos decisiones encadenadas: qué modelo de iluminación necesitas y cuánto control quieres sobre el código.

flowchart TB
a{Necesitas iluminacion} -->|No| b[MeshBasicMaterial]
a -->|Estilizada| c[MeshToonMaterial o shader propio]
a -->|Realista| d{Necesitas capas fisicas}
d -->|No| e[MeshStandardMaterial mas entorno HDRI]
d -->|Si| f[MeshPhysicalMaterial con coste alto]
e --> g{Hay que modificar el shading}
f --> g
g -->|Cambios pequenos| h[onBeforeCompile con clave de cache propia]
g -->|Control total en WebGL| i[ShaderMaterial]
g -->|Portable a WebGPU| j[NodeMaterial con TSL]
g -->|No| k[Listo]
style b fill:#a6e3a1,color:#11111b
style e fill:#a6e3a1,color:#11111b
style f fill:#f38ba8,color:#11111b
style j fill:#cba6f7,color:#11111b
style h fill:#f9e2af,color:#11111b

Tres criterios explícitos que cierran las bifurcaciones dudosas.

Entorno frente a luces. Un buen mapa de entorno con PMREMGenerator mejora más el aspecto de una escena PBR que añadir cuatro luces, y cuesta menos: una luz dinámica añade código al shader de todos los materiales afectados y una luz con sombra añade además una pasada de geometría. La secuencia correcta es entorno primero, y luego las mínimas luces que hagan falta para dar dirección.

Cuándo MeshPhysicalMaterial. Está en rojo porque cada capa que activas tiene un coste real y transmission en particular obliga a dibujar la escena una segunda vez sobre un render target aparte. Esa pasada es una sola por fotograma y la comparten todos los objetos transmisivos, así que lo caro es el primer cristal y no el décimo. Es el material correcto para el cristal, el barniz de un coche o una tela con brillo direccional, y es un desperdicio para todo lo demás.

TSL frente a GLSL. Si la escena va a vivir solo en WebGLRenderer y el shader es corto, GLSL directo es más rápido de escribir y de depurar. Si hay alguna posibilidad de migrar a WebGPURenderer, TSL compila el mismo grafo a GLSL y a WGSL, y esa portabilidad se paga sola en cuanto la migración deja de ser hipotética.

Sobre sombras, la tabla que resume las decisiones del nivel correspondiente:

Situación Solución Coste
Todo estático Lightmap horneado Cero en tiempo real
Un objeto móvil sobre suelo plano Sombra de contacto o textura proyectada Muy bajo
Pocos objetos móviles, escena pequeña Un shadow map de 1024 con PCF Medio
Escena grande al aire libre Cascadas de shadow maps Alto
Móvil o visor XR Horneado más una sombra dinámica Presupuesto obliga

Renderer, entrega y carga

La última decisión encadenada: con qué se dibuja, en qué formato llegan los recursos y cuándo.

flowchart TB
a{Necesitas compute o TSL} -->|Si| b[WebGPURenderer con fallback automatico]
a -->|No| c{Publico con dispositivos antiguos}
c -->|Si| d[WebGLRenderer]
c -->|No| b
b --> e[Formato glTF binario]
d --> e
e --> f{Geometria pesada}
f -->|Si| g[Draco o Meshopt]
f -->|No| h[Sin comprimir]
g --> i{Texturas pesadas}
h --> i
i -->|Si| j[KTX2 con detectSupport]
i -->|No| k[AVIF o WebP]
j --> l[Carga por prioridad en tres niveles]
k --> l
l --> m[compileAsync antes de mostrar]
style b fill:#fab387,color:#11111b
style d fill:#89b4fa,color:#11111b
style j fill:#94e2d5,color:#11111b
style m fill:#a6e3a1,color:#11111b

WebGPURenderer cae automáticamente a WebGL cuando no hay WebGPU, así que elegirlo no excluye a nadie: lo que cambia es que las funcionalidades exclusivas de WebGPU, como el compute, no estarán disponibles en el camino de reserva y tu código tiene que contemplarlo.

El criterio para Draco frente a Meshopt merece explicitarse porque casi nunca se explica. Draco comprime más, sobre todo en mallas densas, pero descomprime más despacio y necesita un decodificador de un tamaño considerable. Meshopt comprime algo menos y descomprime muy rápido, con un decodificador pequeño. Para un modelo grande que se carga una vez, Draco. Para muchos modelos pequeños que se cargan durante la sesión, Meshopt. Y para geometría que además se cuantiza, Meshopt encaja mejor porque están pensados juntos.

compileAsync al final del árbol no es opcional: sin él, el parón de compilación ocurre en el momento en que el objeto entra en cámara, que es el peor momento posible.

Nivel dios

Hay una decisión que no aparece en ningún árbol porque no es técnica y sin embargo condiciona todas las demás: cuál es el dispositivo objetivo y quién lo ha decidido. Un proyecto que no responde a esa pregunta acaba, por omisión, con el dispositivo del desarrollador como objetivo, y ese es siempre mejor que la media de los usuarios. La consecuencia se ve en producción y se atribuye a “el 3D en la web es lento”. Escríbelo antes de la primera línea de código: un modelo concreto de móvil de hace tres años, una resolución, un objetivo de fotogramas y un presupuesto de memoria. A partir de ahí, cada bifurcación de estos árboles tiene una respuesta objetiva, y las discusiones de diseño dejan de ser de gusto para pasar a ser de presupuesto. Sin esa cifra, todos los árboles de esta lección se responden con “depende”, que es la forma educada de no decidir.

Y la lista final, la que se consulta antes de dar por terminada una experiencia. Cada línea corresponde a un nivel entero del track, y ninguna se puede responder con una opinión.

El cuello está identificado con una medición, no con una intuición. El número de programas de shader es el que esperabas. renderer.info vuelve a sus valores iniciales tras montar y desmontar veinte veces. La escena arranca en menos de tres segundos en el dispositivo objetivo. Existe una alternativa sin WebGL y es contenido real, no una disculpa. La escena se opera entera con teclado. prefers-reduced-motion cambia el comportamiento y no lo congela. El presupuesto de memoria de vídeo se verifica en la construcción. Y hay un límite de tiempo en cada descarga, con su camino de degradación.

Ocho comprobaciones. Una experiencia que las pasa todas está por delante de la inmensa mayoría de lo que se publica.

⚔️ Reto práctico

Coge un proyecto tuyo y recorre los cuatro árboles anotando la rama que tomaste en su día y la rama que tomarías hoy. Las divergencias son tu lista de refactorizaciones ordenada por impacto, porque los árboles están ordenados por lo que más cuesta cambiar después: la representación de los objetos es cara de cambiar, la estrategia de carga es barata.