¿Cuánto sabes sobre `box-shadow`?
(dgerrells.com)- El box-shadow de CSS suele ser una propiedad para dar profundidad a la UI, pero si superpones cientos o miles sobre un solo
div, puede funcionar como una especie de API de dibujo - Aunque tiene la limitación de la forma de caja, con esquinas redondeadas, cadenas de múltiples sombras y combinaciones de color, desenfoque y transparencia se pueden crear puntos circulares y efectos por capas
- En los experimentos, el desenfoque y la transparencia redujeron mucho la cantidad que se podía animar, y al quitarlos se podían dibujar miles de
box-shadowen una laptop M1 - La simulación de pelotas, los píxeles de imágenes, los puntos sobre superficies de cubos y esferas, y las visualizaciones reactivas a la música se implementaron actualizando en cada frame una cadena de
box-shadow - Al final, se muestra que incluso es posible renderizar una escena usando solo un
divybox-shadow, al paralelizar con Web Workers un ray tracer basado en CPU
Usar box-shadow como herramienta de dibujo
- El
box-shadowde CSS es una forma de drop shadow, y originalmente se usa para dibujar una sombra detrás de una imagen o un elemento de UI y dar sensación de profundidad - Un drop shadow típico desplaza el raster de la imagen en los ejes x/y y lo dibuja detrás del original con un solo color para crear profundidad
filter: drop-shadow()de CSS recibe desplazamientos x/y y un color, y opcionalmente también admite un valor de blur- Si se apilan varios filtros de drop shadow, se puede añadir más profundidad a la composición visual
Características y limitaciones de box-shadow
- El “Box” de
box-shadowsignifica que la forma de la sombra está limitada al bounding box del contenedor - Esa restricción puede parecer limitante, pero como la mayoría de las UI están compuestas por cajas, en la práctica sigue siendo útil
- La implementación de
box-shadowen CSS incluye optimizaciones matemáticas para dibujar cajas redondeadas de forma barata, y una caja redondeada puede verse como un círculo - Los diseñadores pueden aprovechar
box-shadowsin usar imágenes prerenderizadas y así evitar aumentar el tamaño de descarga - En un solo
divse pueden encadenar múltiplesbox-shadow, y el ejemplo construye colores y sombras de esa manera - Puedes experimentar con ajustes de
border-radiusen CSS border-radius generator
Formas “incorrectas” de usar box-shadow
- En diseño de UI normal, las sombras se usan como un elemento de apoyo junto con espaciado, padding, tipografía y accesibilidad para distinguir estados e interacciones
- Pero
box-shadowtambién puede tratarse como una API de pintado que colorea una cantidad arbitraria de rectángulos en pantalla y aplica blur de forma opcional - Los primeros experimentos comenzaron con el arte minimalista del artículo anterior, y lograban resultados visuales solo con una configuración simple de bloques de color
- Después se creó una herramienta de visualización para ver cómo más
box-shadow, blur y transparencia afectaban el rendimiento- Generaba una cadena grande de
box-shadowy la asignaba a un solodiv - La animación comenzó cambiando la cadena de
box-shadowcada 300 ms y dejando el resto atransition: all - Ese enfoque tenía jank y era más lento que asignar el
box-shadowen cada frame
- Generaba una cadena grande de
- El ejemplo de 100
box-shadowremezcla paletas de color con toques y muestra a la izquierda el historial reciente de 10 paletas - Al aplicar blur, disminuía la cantidad animable, y los colores transparentes también reducían mucho la cantidad que se podía dibujar
- El tamaño del
divtambién afectaba el rendimiento, y durante la animación parecía intervenir el rasterizador por software - Sin transparencia ni blur, una laptop M1 podía dibujar miles de
box-shadow
Crear una simulación de pelotas con box-shadow
box-shadowno puede rotarse, pero conborder-radiuspuede hacer que se vea como un círculo- Se trataron varias sombras circulares como pelotas, y su tamaño se escaló según el valor z para crear una falsa sensación de profundidad 3D
- La implementación actualiza el estado del juego en
requestAnimationFramey asigna una gran cadena debox-shadowaldiven cada frame - El proceso de renderizado es el siguiente
- Ordenar las pelotas según su valor z
- Calcular el tamaño según z
- Convertir la posición x/y, el spread y el color de cada pelota en una entrada de
box-shadow - Unir las entradas con comas para formar una sola cadena
- El ejemplo con 50 pelotas permite moverlas arrastrando y hacer que reboten dentro de la caja
- El escalado 3D falso da cierta profundidad, pero cuando una pelota se acerca a la “cámara”, el círculo puede verse roto
- Eso se debe a que el
divbase que genera elbox-shadowes demasiado pequeño en relación con el escalado - Se puede resolver agrandando el contenedor, pero mientras más grande es el contenedor, peor es el rendimiento
- Eso se debe a que el
- La versión donde las pelotas chocan entre sí usa una comprobación de colisiones
n^2y, al colisionar, solo refleja la velocidad- No simula una interacción física real
- Para que se vea más claro, la posición z se fija y queda en 2D
- También se creó un ejemplo donde las pelotas intentan volver a posiciones iniciales aleatorias, y al atraerlas con la fuerza del toque se produce un efecto como arrancar pedazos de una esponja
Imágenes y nubes de puntos 3D
- El siguiente experimento mapea los píxeles de una imagen como puntos sobre un plano 2D y dibuja cada punto con
box-shadow - Se leen la posición y el color de los píxeles de la imagen y se guardan en
state.particles, usando cada píxel como una partícula - El código fuente está en CodeSandbox
- Este ejemplo renderiza miles de
box-shadowen espacio 3D y permite una interacción donde al arrastrar parece que la imagen se dispersa - La versión en vivo con rotación automática se ofrece en
/box-shadow/v3?width=80&size=5&autoRotate=1y muestra una advertencia sobre consumo de batería al ejecutarse - El ejemplo con mayor cantidad llegaba a unos 12,000
box-shadow, y a ese nivel ya se notaba entrecortado - En M1 se podían procesar muchísimos
box-shadow, pero en desktop, iPhone y Android antiguos no se alcanzaba el mismo nivel
Colocar puntos sobre superficies de cubos y esferas
- También siguieron los experimentos para proyectar puntos de manera uniforme sobre la superficie de una malla
- El cubo se implementa colocando puntos a lo largo de cada cara
- Se asume que todas las aristas tienen la misma longitud
- Se recorren las caras y se llenan con puntos según el tamaño dado
- Al aumentar la cantidad de puntos, mejora la precisión del cubo
- El ejemplo del cubo permite interactuar con toques y añade una pequeña luz que sigue la posición del mouse para dar más profundidad
- El cálculo de iluminación no es exacto y usa varias “magic constants”
- La función de mapeo de partículas del cubo y varios helpers matemáticos se obtuvieron con gypity
- El primer resultado fue una distribución aleatoria
- En el segundo intento se logró una distribución uniforme sobre la superficie del cubo
- La distribución uniforme sobre la superficie de una esfera es más compleja, por lo que se usó un método de spiral discretization
- La idea es colocar puntos uniformemente sobre una línea que envuelve la esfera desde abajo hacia arriba
- Se compara con una cuerda rodeando una pelota
- Con este mapeo lineal también se creó una animación conectada al arreglo de valores de frecuencia de la música
- A los puntos se les aplicó una animación de paleta con gradientes interpolados en el tiempo, y esta parte fue más difícil que el trabajo anterior
- El ejemplo de esfera con pocas vueltas muestra que la espiral aproxima una distribución uniforme, pero si no hay suficientes vueltas, se rompe visualmente
Ray tracer con box-shadow
- La idea de que si se pueden dibujar triángulos con
box-shadowcasi se podría crear cualquier CGI llevó al experimento de ray tracing - El objetivo era dibujar una escena usando solo un
divybox-shadow - Los ejemplos posteriores combinan renderizado en vivo de baja precisión y renderizado de imagen de alta precisión, y algunos no tienen límite de precisión, así que conviene ejecutarlos con cuidado
- Los ray tracers y ray marchers son formas exactas pero lentas de generar imágenes, y se usan en toda la industria del CGI
- Aquí se usó un tracer basado en CPU en lugar de GPU
- Porque usar GPU le quitaría sentido al experimento y además haría más difícil la implementación
- El tracer inicial se hizo para renderizar solo esferas, y el modelo de datos de la escena incluía cámara, varias esferas e información de materiales
- El código inicial obtenido con gypity funcionaba en tiempo real, pero tenía problemas
- Faltaba la línea que suma el bias coefficient necesario en la optimización de muestreo
- Había un comentario relacionado, pero faltaba la línea real, y se corrigió después de revisar la técnica de muestreo
progressive rendering y Three.js
- Después, el código cambió a una estructura de progressive rendering
- El progressive rendering reparte muchos cálculos de rayos entre varios frames y muestra cómo el render se acerca gradualmente al “ground truth”
- Esta estructura encaja bien con la posibilidad de mover la cámara de manera interactiva
- La cámara y los orbit controls usan la librería Three.js
- Para evitar implementar a mano la matemática de matrices y el soporte móvil
- Esa versión solo puede renderizar esferas, y todos los elementos de la escena son, en cierta medida, esferas escaladas
- El ejemplo corre por defecto a poco más de 6% de la resolución completa
- Mientras más lejos está la escena en pantalla, más nítida parece la baja resolución, y al acercarse se hace más evidente la falta de precisión
- Con parámetros de consulta se pueden ajustar la escala de resolución,
pixelSize, la cantidad de rebotes de rayos y el máximo de muestras - Si se aumentan la resolución y las muestras, el resultado mejora, pero se vuelve muy lento
Implementación del tracer y cuellos de botella de rendimiento
- El renderizado sigue el enfoque estándar: disparar un rayo por cada píxel según la cámara, calcular el color y luego promediar entre frames
- Como el tracer usaba Three.js, se creaban muchos objetos nuevos y se acumulaban rápidamente como basura
- Se intentó reutilizar algunos objetos, pero para exprimir el rendimiento al máximo habría sido mejor no usar Three.js
- Según el profiler, la recolección de basura no marcó una diferencia grande, así que se mantuvo el uso de Three.js
- La idea básica del tracer es que el rayo rebota contra los objetos de la escena hasta llegar a la luz y devuelve un color según las propiedades del objeto y de la iluminación
- Se necesitan muchos rayos porque algunos no alcanzan la fuente de luz y otros sí
- Este tracer usa un modelo de iluminación simple
- Sin BRDF físicamente correcta
- Sin texturas
- Sin subsurface scattering
- Con iluminación difusa simple y specular reflection
- Como material para aprender ray tracing, se puede consultar Ray Tracing in One Weekend
- Después se logró renderizar planos y, tras corregir un error al manipular bits de valores de coma flotante, también se pudieron renderizar rectángulos de luz
Paralelizar con Web Workers
- La mejora de rendimiento se abordó con multithreading usando Web Workers, en lugar de corregir el problema de GC en Three.js
- El ray tracing encaja bien con la paralelización porque cada cálculo devuelve un solo resultado y no tiene efectos secundarios
- El worker manager crea un pool de workers y ofrece dos métodos,
renderyupdateScene, para poder cambiar la escena en tiempo de ejecución - El código del worker es básicamente una copia del tracer existente, y para reemplazar la escena fue necesario hacer marshaling de datos
- Para reducir el overhead de
postMessage, el worker renderiza frames completos en lugar de un solo píxel - El ejemplo a pantalla completa funciona mucho más rápido que antes
- La desventaja es que si cambia la cámara o la escena, todos los resultados previos de rayos quedan inválidos y la pantalla puede verse negra
- La solución se implementó levantando un flag
isDirtyen los eventos de entrada y dejando que el loop de actualización decida si debe limpiar el frame - Puede pasar que un worker envíe un frame calculado con datos de la escena anterior justo después de una actualización de escena
- Se podría descartar incluyendo un timestamp o scene id en
postMessage - En el experimento se dejó así, porque un frame con rayos incorrectos pronto queda promediado
- Se podría descartar incluyendo un timestamp o scene id en
- En conjunto, el ray tracing basado en
box-shadowfuncionó lo bastante bien, y el código fuente está en CodeSandbox - Las estadísticas de rendimiento muestran una estimación del total de rayos, la cantidad de muestras y la resolución de renderizado
- El muestreo predeterminado se detiene en 1200, pero se puede configurar
- También se puede activar un modelo de iluminación alternativo, más rápido pero menos preciso
- Los datos de la escena están en JSON, así que es fácil modificarlos directamente
Conclusión: un experimento posible, pero no recomendable
- Solo con
box-shadowde un únicodivse pueden dibujar simulaciones de pelotas, nubes de puntos, partículas basadas en imágenes, superficies de cubos y esferas, e incluso escenas con ray tracing - Aun así, todo el experimento se acerca más a una forma de “no deberías hacer esto” sin un caso práctico real
- Hay margen de mejora si se añaden carga de triangle mesh, acceleration structure y modelos de iluminación más precisos
- Al principio, gypity respondió que un ray tracer con
box-shadowera imposible, pero el ejemplo real terminó existiendo - CSS no es intuitivo, pero a veces tiene posibilidades extrañas que claramente funcionan
1 comentarios
Opiniones de Hacker News
Usar colores transparentes lo vuelve más lento porque limita el procesamiento por lotes de dibujo en GPU.
El dibujo opaco puede usar el búfer de profundidad para reordenar libremente las operaciones y maximizar el procesamiento por lotes. En el caso de la transparencia, hay que dibujar en orden de pintor para que la mezcla funcione correctamente.
En particular, Chromium intenta minimizar la cantidad total de capas, renderiza cada capa como un mapa de píxeles y luego compone en cada frame las capas visibles en la imagen final. En la práctica, muchas capas no cambian sus píxeles y solo se mueven de posición, así que basta con componerlas sin rasterizarlas en cada frame.
Si hay muchos box-shadow sin transparencia, Chromium puede rasterizar todo de una sola vez en una capa, pero si hay muchos box-shadow transparentes, podría convertir cada uno en una capa separada. En ese caso puede ser ineficiente, pero también es un enfoque necesario si esos box-shadow semitransparentes deben moverse de forma independiente en la página.
Se puede empezar con un búfer totalmente transparente (α=0.0), recorrer cada superficie de adelante hacia atrás y, para cada píxel, mezclar en el búfer existente la proporción
1.0-buffer.αdel nuevo píxel. Sibuffer.α == 1.0, se puede omitir por completo, como con un búfer de profundidad.Eso sí, habría que revisar la matemática cuando hay otro objeto transparente detrás de un objeto transparente, y los casos en los que las superficies se superponen de forma cíclica o se atraviesan entre sí son complicados.
Esa es también la razón por la que, en general, suele ser mejor dibujar más triángulos que usar texturas transparentes.
El renderizado opaco puede dibujarse de adelante hacia atrás, de modo que solo se renderiza lo que realmente se verá en el framebuffer final, y la cantidad de píxeles después del pase de profundidad es proporcional al tamaño del framebuffer.
El renderizado transparente debe dibujarse de atrás hacia adelante, y hay que renderizar muchas partes de la escena que luego quedarán parcialmente cubiertas por otros polígonos. Por eso la cantidad de píxeles que pasan por el pipeline de shaders puede crecer hasta ser proporcional al tamaño de la malla.
Si los elementos no se superponen entre sí, la transparencia casi no debería ralentizar nada. Al fin y al cabo, hay que tocar cada píxel una vez, y lo único que cambió es la fórmula del shader.
Si un dibujo opaco queda encima de otro, en el mejor caso se pueden eliminar todas las operaciones de dibujo superpuestas, y en el peor caso solo se usa el ancho de banda correspondiente a cada dibujo individual.
Con transparencia, si no se pueden combinar de alguna manera las operaciones, hay que volver a leer toda el área superpuesta, por lo que con cada dibujo transparente termina viajando por el bus de memoria un bitmap de al menos el doble del tamaño del framebuffer final.
Si recordamos que muchos dispositivos móviles ni siquiera tenían suficiente ancho de banda de memoria para hacer dos blits de pantalla completa dentro del tiempo necesario para mantener 60 fps, es un problema bastante grande.
Fue una exploración realmente divertida.
También es cierto que capas es una palabra clave. En un proyecto de sombras de texto que hice hace 14 años, los efectos absurdos pero a veces vistosos también dependían de las capas: https://paulirish.github.io/mothereffingtextshadow/
Me dio un poco de vergüenza darme cuenta recién en el último párrafo de que
gypityse refería a ChatGPT.gyp.Bienvenido por completo el buen viejo hackeo impráctico. Solo hay que recordar que ya existe canvas, y que para estas cosas canvas es más fácil, más rápido y lo hace mejor.
La visualización musical fue definitivamente genial. De verdad extraño aquellos tiempos de Winamp, cuando ponías música y abrías las visualizaciones en pantalla completa.
Ojalá los reproductores de audio por streaming actuales ofrecieran algo así.
Aunque es cierto que los reproductores de audio por streaming son software con funciones realmente básicas.
Al final, Firefox y Chrome todavía renderizan de forma distinta un box-shadow de 1 px con el zoom del navegador al 150%.
Tengo esperanzas en Baseline 2025.
Me gustaría saber si hay referencias para aprender más sobre esos “hacks matemáticos para dibujar muy barato las cajas redondeadas que tanto les gustan a la gente de UI”.
https://www.folklore.org/Round_Rects_Are_Everywhere.html
http://wg20.criticalcodestudies.com/index.php?p=/discussion/...
Para un enfoque moderno con aceleración 3D, como especuló otra persona, hay un artículo que usa SDF:
https://mortoray.com/quickly-drawing-a-rounded-rectangle-wit...
Es el tipo de hack que me gusta.
Parece casi lo opuesto a los artículos de Josh Comeau que había leído sobre este tema: https://www.google.com/search?q=josh+comeau+shadows
Un artículo excelente, quizá el mejor que leí este año, terminó al final con your welcome en vez de
you're.Hay que corregirlo rápido. O tal vez no entendí el chiste.
Durante los últimos 30 años me volví bueno programando, pero como no me gustan los videojuegos, casi no hice gráficos.
Ahora veo eso como un error enorme y llevo más de un año intentando ponerme al día, pero es realmente difícil.