Una vez, el diseñador llamó al front-end y pidió hacer una "telaraña" detrás del vidrio empañado. Pero luego resultó que no se trataba de una "telaraña", sino de una cuadrícula hexagonal, y no detrás del cristal, sino que se fue en la distancia, y el reverso no estaba familiarizado con WebGL, y tuve que aprender toda la animación en el proceso de dibujo. Ese front-end fue
Yuri Artyukh (
akella ).

Yuri se ha dedicado a la composición tipográfica durante mucho tiempo y los domingos registra flujos con análisis de proyectos reales. No es un profesional en WebGL, no hace mapas en él, no escribe en el ensamblador web, pero le gusta aprender algo nuevo. En
FrontendConf RIT ++, Yuri dijo cómo realizar una animación desde el diseño hasta la entrega al cliente para que todos estén contentos y aprendan WebGL en el camino. La historia proviene de la perspectiva en primera persona e incluye: Three.js, GLSL, Canvas 2D, gráficos y un poco de matemática.
Telaraña detrás del vidrio empañado
De alguna manera me senté y trabajé en un proyecto importante. Luego, el diseñador del estudio, en quien los efectos especiales son muy aficionados, llama y pregunta: "¿Puedes hacer una telaraña, como si estuviera detrás de un vidrio empañado?"
Esto, por supuesto, describe inmediatamente todo el problema. Como resultó más tarde, la "telaraña" detrás del vidrio empañado era esta.
Esta es una cuadrícula hexagonal, pero para el diseñador, por alguna razón, una "telaraña". Vidrio empañado: esta cuadrícula va en la distancia. Dificultades de comunicación. ¿Imagina lo difícil que es ser introvertido y hacer animaciones? Pero yo soy así y eso es lo que estoy haciendo.
Esta "web" no parece una animación, después de lo cual puedes escribir un informe sobre un caso exitoso, abrir una startup, obtener mil millones de inversiones, tener un montón de fanáticos y lanzar un cohete al espacio. ¿De qué se trata todo esto? Una línea marrón sobre un fondo blanco grisáceo, como dibujada por un mouse. Más tarde resultó que debería ir por los bordes, pero más sobre eso más tarde. En general, el nombre en clave es "la red detrás del vidrio empañado".
En el sitio con esta animación había varias opciones más para la "telaraña": sobre un fondo gris en la parte superior, sobre un blanco debajo. Era necesario hacerlo interactivo para que respondiera al movimiento del mouse del usuario.
Lo primero que me preguntaron los diseñadores es qué tan difícil es y cuánto costará. Unos pocos pensamientos pasaron por mi cabeza: cómo dibujar una línea y una Cuadrícula de este tipo, cómo hacer que no disminuya la velocidad, cómo debería funcionar en absoluto. Nunca me he encontrado con esto antes. Pero, como una persona que se dedica al desarrollo, respondió: "
Eso es fácil, lo haremos ..."Me gusta involucrarme en extrañas aventuras, porque cuando hago esto, generalmente sufro.
A través del sufrimiento viene el crecimiento. Está inevitablemente asociado con el sufrimiento: no puedes ser feliz con todo, vivir felizmente y al mismo tiempo desarrollarte profesionalmente.
Three.js
Inmediatamente comencé a pensar cómo resolver el problema. Como todo estaba en 3D, recordé
Three.js . Esta es la biblioteca más popular de la que se habló en todas las conferencias. Esta biblioteca hace que WebGL sea más simple, más conveniente y más agradable que solo WebGL nativo.
Three.js tiene muchos objetos preparados.
PlaneGeometry es el primer objeto que me pareció perfecto. Este es un plano primitivo. La biblioteca tiene todo tipo de hexágonos, dodecaedros, icosaedros, cilindros, pero hay un plano simple de muchos triángulos.
Hay muchos triángulos, porque necesito ondas detalladas, la superficie debe preocuparse.Si miras dentro de Three.js, entonces, de hecho, este plano es un simple objeto JS con una lista de todas las coordenadas de los puntos.
En mi caso, tengo un plano de 50 × 50 cuadrados, así que necesitaba 2601 vértices. ¿Por qué 50 × 50 = 2601? Esto es matemática escolar. La coordenada z = 0, porque el plano, y = 1, porque esta es la primera fila de vértices de 50 piezas, y x cambia.
Pero, ¿por qué necesito un avión? ¿Necesito doblarlo de alguna manera? Lo primero que puede hacer con una matriz es realizar operaciones matemáticas en ella. Por ejemplo, revise
for each
ciclo y asigne la coordenada z al valor seno de la coordenada x.
Resultó algo similar a una onda sinusoidal, porque el valor de la altura de cada vértice será igual al seno de este valor a lo largo del eje x. Para complicar esto de alguna manera (ahora será un momento matemático difícil, prepárese): agregaré tiempo al seno, y este lienzo se moverá, simplemente porque las matemáticas funcionan de esta manera. Si agrega tiempo a la coordenada x, el gráfico se mueve horizontalmente. Si coordinar y, se moverá verticalmente. Estaba interesado en el movimiento horizontal: quería que mi océano se preocupara.
El diseñador no esperaba que tuviera una sinusoide que se arrastra de izquierda a derecha. Quería que fuera hermoso, como una telaraña, un océano o lo que sea que estuviera en su cabeza. Por lo tanto, la opción con una sinusoide no encajaba. Tomó algún tipo de aleatoriedad. El avión no debería haber sido predecible como una onda sinusoidal. Pero si llama al azar para cada uno de estos picos, obtenemos la imprevisibilidad.
La esencia de la aleatoriedad es que es aleatoria e independiente. Cada vértice aleatorio no depende de sus vecinos de ninguna manera. Al azar, no se pasan parámetros, no le importan los vecinos.
El resultado es una curva rota que se asemeja al océano o la web solo de forma remota. Más adecuado como ilustración para una película sobre "piratas informáticos" y robos cibernéticos.
Si observa al azar desde el punto de vista de cada punto en la pantalla, se ve como ruido blanco, muchos pequeños puntos blancos y negros. Cada punto es blanco y negro: 0 o 1.
Pero lo que necesito para crear un océano de olas debería verse así.
Se asemeja a la niebla, las nubes y las montañas.Hay funciones aleatorias que devuelven tales imágenes. Se llaman ruidos o
ruido : ruido simple, ruido de Perlin. Perlin en nombre del ruido es el nombre del creador del algoritmo de ruido de gradiente, que devuelve un hermoso aleatorio. Lo creó trabajando en los efectos especiales de la primera parte de la película "Tron". Este algoritmo matemático existía antes, pero ahora se usa activamente en películas y juegos.
Cuando se generan mapas aleatorios en "Heroes of Might and Magic III" (para los mayores de 30 años) o en estrategias, generalmente puedes ver algo similar. Siempre es la misma función que devuelve estos ruidos.
Hay todo un movimiento "Arte generativo". Los participantes generan obras de arte, paisajes, utilizando la función de ruido. Por ejemplo, en la imagen a continuación, un paisaje pseudo-natural de uno de los artistas. No está claro de inmediato si se trata de las matemáticas o la topografía de una montaña. La tarea del arte generativo es precisamente generar matemáticamente un paisaje que no se puede distinguir del presente.
Resultó que, a diferencia de la aleatoriedad, esta función toma parámetros, porque los puntos deben depender de los vecinos más cercanos a ellos. Si utiliza la función de ruido en lugar de la aleatoriedad habitual, obtendrá algo como esto.
Blanco y negro es simplemente altura: 0 son valles negros, 1 son picos blancos. Se convierte en una superficie ondulada.Esta función está en todos los PL porque es solo un algoritmo: senos, cosenos, multiplicación.
Puedo hacer la distorsión de la misma manera pasando por todos los vértices de mi objeto PlaneGeometry, asignando cada valor a la función de ruido:
geometry.vertices.forEach(v => { vz = noise(vx, vy, time); });
La función ocupa solo 30-40 líneas, pero es matemáticamente compleja.
Hay funciones de ruido de todas las dimensiones: unidimensional, bidimensional, tridimensional. En mi caso, esto es ruido tridimensional, porque se le pasan tres parámetros. Además de las coordenadas espaciales del plano xey, transmito el tiempo: la superficie se torcerá constantemente, cambiará su posición.
Three.js! == GPU
Cuando comencé el algoritmo, las ondas comenzaron a moverse. Cuando hago algo para la Web, siempre miro en el generador de perfiles, y ahora también lo busqué. Así es como se veían las olas allí.
En la pantalla hay un cuadro dibujado por el navegador. Los marcos se indican mediante líneas discontinuas grises verticales. Dentro del marco, 2/3 del tiempo está ocupado por la ejecución de la función de ruido. Cuando anima algo en la Web, utiliza el marco de animación de solicitud, que se ejecuta cada 16 ms, en el mejor de los casos. El cuadro cada 16 ms cuenta la función de ruido para 2600 vértices. Para cada vértice, se considera un movimiento hacia arriba y hacia abajo y la altura. En cada cuadro posterior, los valores se vuelven a calcular, porque la superficie debe vivir en el tiempo.
Resultó que la función de ruido, que se volvió 2600 veces, ya toma 2/3 del marco en mi computadora. Y este no es todo el marco. Al desarrollar animaciones, esto ya es una bandera roja.
Ninguna animación debe ocupar más de la mitad del cuadro.
Si es más, existe un alto riesgo de perder el marco durante cualquier interacción, cualquier botón, cualquier mouseover.
Por lo tanto, fue una bandera roja dura. Me di cuenta de que Three.js no es necesariamente WebGL. A pesar del hecho de que parecía usar Three.js, dibujé todo en 3D, se renderizó en WebGL, no obtuve un rendimiento fantástico de WebGL. Solo tengo 2.600 vértices, esto no es suficiente para WebGL. Por ejemplo, en cada mapa hay miles de objetos, cada uno formado por docenas de triángulos. Estime la escala: cientos de miles son normales, pero solo hay 2600 picos.
Sombreador de vértices
Después de un problema con los marcos, descubrí que hay sombreadores. Solo hay dos tipos de ellos:
- Sombreador de vértices;
- Fragmento sombreador.
Estaba interesado en el sombreador de vértices: el sombreador de vértices. Si reescribimos la animación, entonces se ve así:
position.z = noise( vec3(position.x, position.y, time) );
Position.z
: coordenadas z del componente de cada punto con sus propios tipos de datos.
vec3
indica que habrá tres parámetros.
No hay bucle en el sombreador.
Antes de eso, en el guión, puse un
for each
bucle, y para cada vértice, los cálculos se realizaron en un bucle. La diferencia entre sombreadores y no sombreadores es la ausencia de un ciclo.
Shader: este es el ciclo.
Se ejecuta en paralelo para todos los vértices a la vez. Este es su significado principal, y misión, y chip, y misión.
La GPU en la tarjeta de video tiene un núcleo más grande, a diferencia de la CPU del procesador principal. En el procesador hay muchos menos, pero es capaz de realizar cálculos universales más rápido. Hay cálculos muy simples disponibles en la tarjeta de video, pero hay muchos núcleos, por lo que le permite paralelizar muchos cálculos. Esto es lo que suele pasar en los sombreadores. El significado del sombreador de vértices es que el ruido se calculará en paralelo para 2600 vértices en el sombreador de la tarjeta de video.
Si observa el generador de perfiles, la apariencia de la animación no cambiará, pero se verá así.
Nada se ejecuta en la CPU en absoluto . Por supuesto, a continuación se agregó otro hilo en la GPU. También hay hilos en la GPU, CPU, trabajadores web, pero estos cálculos ya se realizarán en un hilo separado en la tarjeta de video.
Por supuesto, esto no es gratis. La tarjeta de video en el trabajo se calienta más que el procesador principal. Por lo tanto, a menudo cuando visitas sitios con animaciones similares, los fanáticos comienzan a trabajar para ti. Esto se debe a que cuando la tarjeta de video está encendida, requiere enfriamiento, a diferencia del resto del tiempo. En los dispositivos móviles, esto es más importante que en las computadoras de escritorio: los teléfonos móviles simplemente se agotan más rápido. Pero al mismo tiempo obtienes una ganancia de rendimiento, bastante radical.
El resultado es una superficie de este tipo: este es el ruido perlin habitual. Si lo inicia y cambia solo el tiempo, se obtienen olas frías.Pero eso no es todo. Todavía estaba obligado a "telaraña" - una cuadrícula hexagonal en la superficie. Con experiencia en diseño, la forma más simple y obvia es seleccionar un fragmento que pueda repetirse. Curiosamente, para una cuadrícula hexagonal, no es cuadrada, sino rectangular. Si repite el patrón como un rectángulo, obtendrá una cuadrícula. La biblioteca Three.js le permite superponer png y no aprender todo de WebGL antes de eso. Corté png y lo puse en la superficie, resultó algo así.
A primera vista, lo que necesitas! Pero solo al principio. Esto no me convenía, ya que la animación era necesaria para el sitio de criptomonedas: todo debería ser "costoso, rico".
Cuando usa texturas png y están cerca de la cámara, puede ver que los bordes del elemento más cercano están borrosos. No hay sensación de que la imagen sea clara. El png parece haberse extendido en el navegador. El problema es que en WebGL no hay forma de usar texturas vectoriales en el sentido completo de la palabra. Entonces lloré y luego leí en Internet que
GLSL resuelve este problema.
GLSL es un lenguaje tipo C en el que se escriben los sombreadores. Todos tienen miedo de usarlo, porque estos son sombreadores, WebGL: ¡nada está claro! Pero descubrí que es posible hacer imágenes claras en él y recurrí al segundo tipo de sombreador.
Sombreador de fragmentos
Este sombreador hace lo mismo que el vértice. Pero, si el vértice construye una superficie de polilínea, realizando cálculos para cada vértice, entonces el sombreador Fragmento calcula el color para cada píxel de la superficie.
La función de
fragment shader – step(a,b)
más básica
fragment shader – step(a,b)
. Solo devuelve 0 y 1:
- si a> b, entonces 0;
- si a <b, entonces 1.
Hice una pseudo implementación en JS para dejar en claro cuán simple es esta función.
function step(a, b) { if (a < b) return 0 else return 1 }
Cuando trabajas en WebGL, generalmente en cualquier objeto hay un sistema de coordenadas. Si es un objeto cuadrado, el sistema de coordenadas es primitivo: puntos (0,0), (0,1), (1,0), (1,1).
Se ejecuta un Fragment Shader para cada píxel. Si Vertex Shader es 2600 veces por cuadro, entonces Fragment Shader se ejecuta tantas veces como haya píxeles. Tal vez un millón de veces para cada cuadro, si la superficie es 1000 × 1000 px. Suena aterrador, pero simplemente porque pocas personas están familiarizadas con los recursos de las tarjetas de video en nuestro tiempo.
Si utiliza la función de paso (a, b) con las coordenadas de estos píxeles, puede ejecutar la función de paso con el parámetro 0.4 y pasar la coordenada x de cada píxel a cada punto.
Resulta que todo lo que sea menor que 0.4 será 0, todo lo que sea mayor que 1. En WebGL, los números y los colores son uno y lo mismo. Cada color es un número. Blanco - 1, negro - 0. Hay tres de ellos en RGB, pero sigue siendo 0.0.0 y 1.1.1.

Si ejecutamos este paso de función más complicado, obtenemos blanco a la izquierda. Esta función se ejecutará para cada punto en la pantalla y considerará que es 0 o 1. Esto es normal, no se preocupe por esto.
Si multiplica estas dos expresiones, obtendrá una franja blanca vertical. Si haces lo mismo en un eje diferente, puedes dibujar un cuadrado blanco:
Debería ser el clímax: ¡dibujamos un cuadrado blanco!Usando combinaciones de una sola función, puede dibujar lo que quiera.
Si recuerdas, los elementos del patrón estaban en ángulo. Si crea una superficie inclinada, que consiste solo en colores blanco y negro, entonces será acanalada, no alisada. Las costillas te llaman la atención, es feo. Para que la superficie se vea lisa, no solo se necesitan píxeles en blanco y negro, sino también medios tonos grises.
Smoothstep
Los sombreadores tienen una función de paso suave. Hace lo mismo que el paso, pero interpola entre 0 y 1 para que haya un gradiente.
De izquierda a derecha, después de la máxima compresión.Si comprime esta función tanto como sea posible, obtendrá una línea de gradiente mínima. Esto es justo lo que necesita para generar una línea perfectamente suave en cualquier ángulo en el sombreador Fragment.
Entonces pude hacer un cuadrado blanco con bordes lisos. Si hay un cuadrado blanco, puedes hacer 3 cuadrados blancos.
Los cuadrados se pueden rotar, use las funciones de seno y coseno.Luego tuve que usar papel y una hoja para romper mi patrón.
Captura de pantalla de la producción.Allí todo está conectado con 23 grados diferentes de multiplicidad, por lo que no fue muy difícil calcular las coordenadas de todos estos puntos. Y luego puedes obtener ese patrón.
Dibujé un fragmento y lo repetí muchas veces. Puede ver claramente el modo de depuración, donde se repite el patrón. Todos los fragmentos se realizan utilizando las funciones paramétricas
step
y
smoothstep
. Esto significa que al hacer un patrón para inclinar el plano, puede generar un número infinito de estos patrones. Si dentro del fragmento cambiamos el grosor de la línea o el tamaño de los hexágonos, obtenemos muchos otros patrones.
Torcí los parámetros y encontré un número infinito de patrones. Es como "Arte generativo": no está claro lo que se ha hecho, pero es hermoso.
SDF
Luego descubrí que también
hay campos de distancia firmados, que generan imágenes con un mapa de distancia . SDF se usa en tarjetas o en juegos de computadora para dibujar textos y objetos, porque es óptimo. En WebGL, es difícil dibujar texto de una manera diferente, especialmente suave y contorneado.
Este es un formato matemático que es difícil de usar fuera de WebGL. La idea es simple, pero elegante y da un hermoso efecto.
Si queremos dibujar una estrella clara, para ello debemos guardar la imagen a la derecha: esta es la imagen generada de la imagen. Ya existe un algoritmo existente que convierte cualquier imagen clara en una borrosa. Después de eso, puede usarse para generar una versión clara, pero al mismo tiempo no obtenemos una imagen, sino muchas. A partir de una imagen clara del mismo tamaño, puede generar lo mismo, pero más grande. Será con errores, pero el enfoque es matemáticamente interesante.
Por ejemplo, si toma una imagen con un tamaño de 128 × 128 px, a partir de una imagen en un tamaño pequeño puede obtener una imagen clara varias veces más grande que la fuente. Esta es una de las razones por las que usan SDF: una fuente borrosa a menudo pesa menos que en un formato vectorial optimizado.
Por supuesto, hay una limitación. Es imposible aumentar las letras a 1000 px, incluso 100 px se verá feo. Pero, ¿con qué frecuencia se necesitan fuentes de este tamaño?
Fragmento de sombreado, dibujo de rectángulos, propagación: con la ayuda de estas perturbaciones, finalmente logré encontrar la superficie deseada.
Nuevas condiciones
Ella estaba como debería: retorciéndose, todos los elementos estaban claros. Todo estaba como quería, pero resultó que había más:
- Y deja que se mueva con el mouse y se está estableciendo un nuevo camino. ¡Y los panales están resaltados!Se supuso que cuando el usuario mueve el mouse, metafóricamente pavimenta su camino espinoso a lo largo de la "telaraña" rota usando el servicio.
No es difícil describir la tarea en palabras, pero ¿cómo implementarla? Lo primero que pensé, dado que tengo una cuadrícula hexagonal, probablemente ya ha sido estudiada. Luego me encontré con un interesante artículo "
Referencia de cuadrícula hexagonal y guía de implementación ". En él, el autor recolectó materiales durante 20 años. Él es genial sin dudas, y el artículo es divino para aquellos a quienes les gustan los algoritmos y las matemáticas. Contiene muchos datos interesantes sobre mallas hexagonales.
El artículo es largo, pero contiene enfoques matemáticos interesantes: cómo construir un sistema de coordenadas en una cuadrícula hexagonal, cómo numerar estos hexágonos y luego referirse a ellos donde se usa. Resulta que esto siempre estuvo ante mis ojos, porque en los viejos juegos de computadora las redes hexagonales se usan en todas partes.
Si ya estás sintonizado con un traste hexagonal, mira el castillo. En otras texturas, también se adivina una cuadrícula hexagonal.
En "Civilización" todo es generalmente obvio.También fue interesante saber que si haces una sección transversal a lo largo de la diagonal de un cubo tridimensional, que consta de muchos cubos pequeños, entonces, por un lado, estos son cubos y, por otro, hexágonos regulares.
La sección transversal de un cubo tridimensional da una cuadrícula hexagonal bidimensional. Fue divertido aprender que los cubos tridimensionales están de alguna manera conectados con hexágonos bidimensionales.
En el artículo, incluido, había un algoritmo para encontrar el camino a lo largo de la cuadrícula hexagonal. Tuve que buscar una forma de altura a través del mouse.
Los algoritmos de búsqueda de ruta son complejos y simples. Lo más primitivo es dibujar una línea entre los puntos y ver en qué hexágonos cae esta línea. Entonces resulta que en los viejos tiempos, las unidades iban del punto A al punto B.
Necesitaba algo así.Pero eso no es lo que necesito. Aquí el camino se establece a lo largo de las áreas de hexágonos, y necesito a lo largo de los bordes. Tuve que resolver el problema de manera diferente.
Canvas2d
Quizás haya mejores formas, pero la mía es más interesante. Al principio, dibujé
Canvas2D para mi depuración, paso 1.

Antes de eso, había WebGL, Three.js, shaders, ¡y esto es solo Canvas2D! Dibujé todos los puntos de la cuadrícula hexadecimal en él. Si te fijas bien, estos son los mismos hexágonos. Luego recordé los gráficos que almacenan información sobre cómo se conectan los puntos entre sí, y conecté cada punto con tres vecinos y obtuve un gráfico - paso número 2. Para esto utilicé Open Source
Beautiful Graphs .
Un gráfico es simplemente una colección de puntos e información sobre cómo están conectados. Están bien estudiados, hay muchos buenos algoritmos para todos los gustos para encontrar la ruta desde el punto A al punto B dentro del gráfico. Todas las tarjetas usan este tipo de algoritmos.
Se ve algo como esto.
graph = createGraph( ); graph.addNode(..);
Construimos un gráfico, agregamos 1000 puntos y todas las conexiones entre ellos. Luego, pasamos la
id
cada punto: el primero está conectado al tercero, el tercero al quinto. No es necesario inventar nada, hay un algoritmo optimizado con una función preparada que le permitirá encontrar este camino.
Este algoritmo se ejecuta menos de un marco. Por supuesto, se necesita algún tipo de recurso, pero no es necesario ejecutarlo cada 16 ms, sino solo cuando la ruta cambia.
Así que pude construir esta ruta en el paso número 3. En Canvas2D, comenzó a verse así: la ruta más corta desde el punto A al punto B, todo, como en la vida. A primera vista parece que este no es el camino más corto, pero resulta que hay muchos caminos más cortos desde el punto A al punto B a lo largo de la cuadrícula hexagonal.
En WebGL, todas las imágenes son números. Allí puede transferir texturas en el sombreador, por ejemplo, intenté transferir png. No hay diferencia para el navegador: se pasa png o Canvas2D. Para el navegador Canvas2D, es lo mismo que la imagen terminada, mapa de bits. Por lo tanto, primero dibujé esta imagen en forma de serpiente. № 4.
Canvas2D . Canvas2D , — . , . , Canvas2D 3D, , .
, , WebGL — , «, », .
, , Canvas2D, , , . Canvas2D , WebGL, .
: , , ., --. , , «» .
?
: « ? ?» , : «, !». , . .
, WebGL. , . , WebGL. 2 — , .
, , . , .

, . , , , 3D — .
, . , , .
FrontendConf . , Canvas , RxJS JSX React .
30 . , , .