Saltar al contenido

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.

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.

JS
const albedo = new THREE.TextureLoader().load('/tex/albedo.jpg');albedo.colorSpace = THREE.SRGBColorSpace;      // color: SIconst 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.

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.

JS
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.

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.