Los errores que ya no deberías cometer
El catálogo de fallos que separan a quien ha leído la documentación de quien ha llevado una experiencia 3D a producción, con el síntoma, la causa y la corrección de cada uno.
Los errores caros del 3D en la web tienen una característica común: no producen un mensaje de error. Producen una escena que se ve mal, o que va lenta, o que funciona en tu máquina, y el síntoma aparece lejos de la causa. Este catálogo recoge los que se repiten proyecto tras proyecto, organizados por el momento en que se cometen, con el síntoma exacto por el que los reconocerás y la corrección concreta. Si algo de esto te resulta familiar, ya has pagado por aprenderlo.
- Reconocer el síntoma característico de cada error frecuente sin necesidad de perfilar.
- Corregir los errores de color y de escala que arruinan el aspecto sin dar ningún aviso.
- Identificar los antipatrones del bucle de render y del ciclo de vida.
- Evitar los errores de proceso que hacen que un proyecto se degrade solo con el tiempo.
Errores de aspecto: la escena se ve mal y no sabes por qué
Los colores están lavados o apagados. El error es el espacio de color de las texturas. Una textura que representa color (albedo, emisión) tiene que declararse en sRGB; una que representa datos (normales, rugosidad, oclusión, desplazamiento, metalicidad) tiene que quedarse en lineal. Confundirlos no da error: da colores mal.
const albedo = new THREE.TextureLoader().load('/tex/albedo.jpg');
albedo.colorSpace = THREE.SRGBColorSpace; // color: SI
const rugosidad = new THREE.TextureLoader().load('/tex/rough.jpg');
// rugosidad.colorSpace se queda en NoColorSpace: son datos, NO tocar
El síntoma inverso, texturas de datos marcadas como sRGB, es más difícil de ver: la rugosidad queda mal distribuida y el material parece de plástico sin que sepas por qué.
Todo tiene el mismo brillo plano y muerto. Falta el mapa de entorno. Un MeshStandardMaterial sin scene.environment no tiene nada que reflejar, y el término especular de la ecuación de renderizado se queda casi a cero. Añadir un HDRI pasado por PMREMGenerator transforma la escena más que cualquier ajuste de luces.
La escena está quemada o demasiado oscura. Falta el tone mapping, o sobra. Con ACESFilmicToneMapping los valores altos se comprimen suavemente en lugar de saturar a blanco. Y toneMappingExposure es el mando fino: cambiar la intensidad de las luces para corregir la exposición es atacar el síntoma.
El modelo aparece gigante o microscópico. Unidades. glTF define el metro como unidad, y muchos exportadores escriben centímetros. Se corrige en el pipeline de assets, no con un scale en el código, porque un scale en la raíz descoloca la física, las sombras y el near/far de la cámara.
Hay franjas y parpadeo en superficies lejanas. Z-fighting por un rango de profundidad excesivo. El culpable casi siempre es near demasiado pequeño: la precisión del buffer de profundidad se distribuye de forma no lineal y un near de 0,001 desperdicia casi toda la precisión en el primer metro. Subir near a un valor realista mejora más que cualquier otra cosa.
El modelo se ve del revés o con agujeros. Winding order o normales invertidas al exportar con escalas negativas. Se detecta poniendo side: THREE.DoubleSide temporalmente: si el problema desaparece, son las caras; si persiste, son las normales.
side: THREE.DoubleSide es una herramienta de diagnóstico, no una solución. Dejarlo puesto duplica el trabajo de rasterización, rompe el rechazo de caras traseras y empeora las sombras. Úsalo para localizar el problema y luego arregla el modelo.
Errores de rendimiento: va lento y el perfilador no ayuda
Optimizar antes de medir. El error madre. Fusionar geometrías en una escena limitada por relleno, o reducir polígonos en una limitada por CPU, es trabajo perdido con degradación visual gratuita. Las dos pruebas de un minuto de el mapa de decisiones descartan casi todas las hipótesis antes de tocar nada.
Crear objetos dentro del bucle. No por el recolector de basura, que hoy maneja bien los objetos pequeños, sino por lo que suele acompañarlo: un new THREE.MeshStandardMaterial() en el bucle compila un programa, un new THREE.BufferGeometry() sube buffers, y un texture.clone() duplica memoria de vídeo. El síntoma es una escena que va bien dos minutos y luego se degrada progresivamente.
material.needsUpdate = true cada fotograma. Fuerza a recalcular la clave de caché y, si cambió, a recompilar. Solo hace falta para cambios estructurales, que por definición no ocurren sesenta veces por segundo. Cambiar color, opacity o roughness no lo necesita.
Un InstancedMesh que desaparece al girar la cámara. Falta computeBoundingSphere() después de mover las instancias. La esfera envolvente de un InstancedMesh nace a null, y la primera vez que el culling la necesita se calcula recorriendo todas las instancias, así que de entrada es correcta. El problema es que se cachea y nadie la recalcula: en cuanto colocas o mueves instancias con setMatrixAt, el culling sigue comparando contra la esfera vieja y descarta la nube entera de golpe.
Post-procesado con muchas pasadas en móvil. Cada pasada de pantalla completa es un render completo del framebuffer. Tres pasadas en un móvil con pantalla de alta densidad son más trabajo que toda la escena. Se comprueba desactivando el composer y midiendo.
setPixelRatio(window.devicePixelRatio) sin techo. En un móvil con densidad tres, eso son nueve veces más píxeles que con densidad uno. Acotar a dos es prácticamente imperceptible y recorta el coste a menos de la mitad.
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
Sombras de 4096 en todo lo que proyecta. Un mapa de 4096 son sesenta y cuatro megabytes de memoria de vídeo y una pasada de geometría completa. Casi siempre, un mapa de 1024 bien ajustado con el frustum de la luz ceñido a la escena se ve mejor que uno de 4096 con el frustum desperdiciado.
Errores de ciclo de vida: funciona hasta que navegas
scene.remove() creyendo que libera. El error que da nombre a una lección entera. Quita del grafo y no toca la GPU. El síntoma es una pestaña que se vuelve lenta y muere tras varias navegaciones, y el diagnóstico es renderer.info.memory creciendo.
Olvidar las texturas del material. material.dispose() no las libera, deliberadamente, porque suelen compartirse. Hay que recorrer las propiedades del material.
Olvidar lo que no cuelga del grafo. scene.background, scene.environment, los render targets del composer y los workers de los loaders comprimidos no aparecen en ningún traverse.
Un bucle que sobrevive al desmontaje. Con requestAnimationFrame a mano siempre queda un fotograma en vuelo que se ejecuta después de liberar recursos. setAnimationLoop(null) para de forma atómica.
Un renderer por ruta. Los navegadores limitan los contextos WebGL simultáneos y, al superarse, destruyen silenciosamente el más antiguo. El síntoma es que la primera escena se queda en negro a la novena navegación.
Desactivar el modo estricto de React porque “da problemas”. El doble montaje está detectando que tu limpieza es incompleta. Silenciarlo traslada la fuga a producción.
setState dentro del bucle. Reconcilia el árbol sesenta veces por segundo. El síntoma es un perfilado donde el tiempo se lo lleva JavaScript mientras la GPU está ociosa.
Errores de proceso: el proyecto se degrada solo
Los anteriores se arreglan una vez. Estos vuelven, porque son fallos de método y no de código.
No fijar el dispositivo objetivo. Sin una cifra escrita, el objetivo es por omisión la máquina de quien programa, que siempre es mejor que la media. Todo se decide “a ojo” y todo sale caro.
No verificar el presupuesto automáticamente. Nadie sube una textura de 4096 a propósito. Un presupuesto que solo vive en un documento se incumple en semanas; uno que hace fallar la construcción se respeta siempre.
Probar solo el camino feliz. La pérdida de contexto, el fallo de descarga, la conexión lenta y el dispositivo sin WebGL son estados que existen en producción desde el primer día. Si no los has provocado a propósito en desarrollo, no sabes qué hace tu aplicación en ellos.
Tratar la accesibilidad al final. Retrofitear una alternativa textual sobre una escena terminada es rehacer la arquitectura, porque exige que la información esté en los datos y no solo en el grafo. Empezar por el HTML alternativo y añadir el 3D encima cuesta lo mismo y sale bien.
Medir el rendimiento con el contador de fotogramas. Sesenta de media puede esconder un fotograma de cien milisegundos cada dos segundos, y eso se percibe como una escena rota. Lo que hay que mirar es el percentil noventa del tiempo de fotograma.
Confiar en que “se optimizará al final”. Las decisiones caras de deshacer son las primeras: cómo se representan los objetos, quién es el dueño del renderer, si hay alternativa accesible. Al final solo quedan los ajustes baratos, que son los que menos aportan.
El error que más caro sale no está en esta lista porque no es técnico: es enamorarse del efecto antes de conocer el presupuesto. Un equipo decide que la experiencia lleva cristal con refracción, partículas volumétricas y reflejos en tiempo real, y solo después descubre que el objetivo era un móvil de gama media. A partir de ese momento todo el proyecto se convierte en un intento de meter con calzador algo que no cabe, con soluciones cada vez más frágiles y una degradación que nunca acaba de verse bien. La secuencia correcta es la inversa y cuesta lo mismo: presupuesto primero, y dentro de él se decide qué efectos caben. Las experiencias 3D que mejor se recuerdan casi nunca son las que más técnica llevan; son las que eligieron dos efectos y los ejecutaron impecablemente a sesenta fotogramas en cualquier dispositivo. La restricción, cuando se acepta pronto, mejora el resultado en lugar de limitarlo.
Y un cierre honesto sobre esta lista: no se aprende leyéndola. Se aprende cometiendo la mitad y reconociendo la otra mitad cuando aparecen. Lo que sí cambia después de leerla es el tiempo entre el síntoma y el diagnóstico, que es donde de verdad se va el tiempo de un proyecto. Saber que una escena que se degrada progresivamente huele a recursos creados en el bucle, o que unos colores lavados huelen a espacio de color, convierte tardes en minutos.
Recorre esta lista sobre tu proyecto actual y marca cada punto con “no aplica”, “correcto” o “hay que mirarlo”. No arregles nada todavía. La lista de “hay que mirarlo” ordenada por la sección en que aparece (aspecto, rendimiento, ciclo de vida, proceso) es tu plan de trabajo, y está ordenada por relación entre esfuerzo y resultado: los de aspecto se arreglan en minutos y se notan enseguida, los de proceso cuestan una conversación con el equipo y evitan que todo lo demás vuelva.