Wprowadzenie do filtrów niestandardowych (tzw. CSS Shader)

Filtry niestandardowe, czyli dawniej nazywane shaderami CSS, umożliwiają korzystanie z mocy shaderów WebGL w przypadku treści DOM. W ramach obecnej implementacji używane shadery są praktycznie takie same jak w WebGL, więc musisz się zastanowić nad terminologią 3D i trochę poznać architekturę grafową.

Dołączyłem nagranie prezentacji, którą niedawno przeprowadziłem na konferencji LondonJS. W filmie omawiam pokrótce terminologię 3D, którą warto poznać, różne typy zmiennych, z którymi się spotkasz, oraz sposób korzystania z filtrów niestandardowych. Musisz też pobrać slajdy, aby samodzielnie przetestować wersje demo.

Wprowadzenie do shaderów

Wcześniej napisałem wstęp do shaderów, który dobrze wyjaśnia, czym są shadery i jak można ich używać w ramach WebGL. Jeśli nigdy nie zajmowałeś/zajmowałaś się shaderami, koniecznie zapoznaj się z tym materiałem, ponieważ wiele pojęć i terminologii związanych z niestandardowymi filtrami opiera się na istniejącej terminologii shaderów WebGL.

Włączmy więc filtry niestandardowe i przejdźmy do rzeczy.

Włączanie filtrów niestandardowych

Filtry niestandardowe są dostępne w Chrome i Canary, a także w Chrome na Androida. Wystarczy, że otworzysz about:flags, wyszukasz „CSS Shaders”, włączysz je i ponownie uruchomisz przeglądarkę. Wszystko gotowe.

Składnia

Filtry niestandardowe rozszerzają zestaw filtrów, które możesz już stosować do elementów DOM, np. blur lub sepia. Eric Bidelman napisał dla nich świetne narzędzia, które warto wypróbować.

Aby zastosować filtr niestandardowy do elementu DOM, użyj tej składni:

.customShader {
    -webkit-filter:

    custom(
        url(vertexshader.vert)
        mix(url(fragment.frag) normal source-atop),

    /* Row, columns - the vertices are made automatically */
    4 5,

    /* We set uniforms; we can't set attributes */
    time 0)
}

Jak widać, deklarujemy tu shadery wierzchołków i fragmentów, liczbę wierszy i kolumn, na które chcemy podzielić element DOM, a potem wszystkie uniformy, które chcemy przekazać.

Na koniec warto zauważyć, że używamy funkcji mix() w shaderze fragmentu z trybem mieszania (normal) i trybem kompozytowym (source-atop). Przyjrzyjmy się shaderowi fragmentu, aby zrozumieć, dlaczego w ogóle potrzebujemy funkcji mix().

Pixel Pushing

Jeśli znasz shadery WebGL, zauważysz, że w przypadku filtrów niestandardowych jest to nieco inne. Po pierwsze, nie tworzymy tekstur, których używa nasz fragment shadera do wypełniania pikseli. Zamiast tego zawartość DOM, do której zastosowano filtr, jest automatycznie mapowana na teksturę. Oznacza to 2 rzeczy:

  1. Ze względów bezpieczeństwa nie możemy zapytać o wartości kolorów poszczególnych pikseli w teksturze DOM.
  2. Nie ustawiamy (przynajmniej w bieżących implementacjach) ostatecznego koloru piksela, czyli gl_FragColor jest poza zakresem. Zakładamy, że chcesz wyrenderować zawartość DOM, a Twoim zadaniem jest manipulowanie jej pikselami pośrednio za pomocą funkcji css_ColorMatrixcss_MixColor.

Oznacza to, że nasz „Hello World” w przypadku shaderów fragmentów wygląda tak:

void main() {
    css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
                            0.0, 1.0, 0.0, 0.0,
                            0.0, 0.0, 1.0, 0.0,
                            0.0, 0.0, 0.0, 1.0);

    css_MixColor = vec4(0.0, 0.0, 0.0, 0.0);

    // umm, where did gl_FragColor go?
}

Każdy piksel zawartości DOM jest mnożony przez css_ColorMatrix, co w tym przypadku nie powoduje żadnych zmian, ponieważ jest to macierz jednostkowa i nie zmienia żadnej z wartości RGBA. Jeśli chcemy zachować tylko wartości czerwone, użyjemy css_ColorMatrix w takiej postaci:

// keep only red and alpha
css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 1.0);

Jak widać, mnożenie wartości pikseli 4D (RGBA) przez macierz powoduje uzyskanie po drugiej stronie wartości piksela potraktowanej w specyficzny sposób. W tym przypadku powoduje to wyzerowanie komponentów zielonego i niebieskiego.

Wartość css_MixColor jest używana głównie jako kolor podstawowy, który chcesz zmieszać z treścią DOM. Mieszanie odbywa się za pomocą trybów mieszania znanych z pakietów graficznych: nakładania, ekranu, dodge’a, twardego światła itp.

Te 2 zmienne mogą manipulować pikselami na wiele sposobów. Aby lepiej zrozumieć, jak tryby mieszania i kompozycji się ze sobą łączą, zapoznaj się ze specyfikacją efektów filtra.

Tworzenie wierzchołka

W WebGL bierzemy na siebie pełną odpowiedzialność za tworzenie punktów 3D siatki, ale w przypadku filtrów niestandardowych wystarczy, że określisz liczbę wierszy i kolumn, a przeglądarka automatycznie podzieli zawartość DOM na wiele trójkątów:

Tworzenie wierzchołka
Obraz podzielony na wiersze i kolumny

Każdy z tych wierzchołków jest następnie przekazywany do naszego shadera wierzchołkowego w celu manipulacji, co oznacza, że możemy je przemieszczać w przestrzeni 3D. Już wkrótce będziesz mógł tworzyć niesamowite efekty.

efekt harmonijki
Obraz zniekształcony przez efekt harmonijki

Animowanie za pomocą shaderów

Dodanie animacji do shaderów sprawi, że będą one ciekawe i zachęcające. W tym celu w pliku CSS wystarczy użyć przejścia (lub animacji) do zaktualizowania wartości uniform:

.shader {
    /* transition on the filter property */
    -webkit-transition: -webkit-filter 2500ms ease-out;

    -webkit-filter: custom(
    url(vshader.vert)
    mix(url(fshader.frag) normal source-atop),
    1 1,
    time 0);
}

    .shader:hover {
    -webkit-filter: custom(
    url(vshader.vert)
    mix(url(fshader.frag) normal source-atop),
    1 1,
    time 1);
}

W powyższym kodzie warto zwrócić uwagę na to, że w okresie przejściowym czas będzie się zmieniał z 0 na 1. Wewnątrz shadera możemy zadeklarować uniform time i używać jego bieżącej wartości:

    uniform float time;

uniform mat4 u_projectionMatrix;
attribute vec4 a_position;

void main() {
    // copy a_position to position - attributes are read only!
    vec4 position = a_position;

    // use our time uniform from the CSS declaration
    position.x += time;

    gl_Position = u_projectionMatrix * position;
}

Zacznij grać.

Filtry niestandardowe są świetną zabawą, a efekty, które można dzięki nim uzyskać, są niesamowite. Bez nich trudno byłoby je uzyskać (a w niektórych przypadkach w ogóle). Jesteśmy wciąż na początku drogi i sytuacja zmienia się dość dynamicznie, ale dodanie ich do projektu nada mu nieco blasku.

Dodatkowe materiały