GLSL Shadery od zera — pisanie pierwszego fragment shadera

Każdy żywy efekt wizualny na tej stronie — fale oceanu, wzory reakcji-dyfuzji, poświaty zorzy — działa wewnątrz fragment shadera GLSL wykonywanego na GPU. Oto jak to działa, krok po kroku.

Czym jest shader GLSL?

Shader to mały program działający równolegle na GPU — raz na piksel (fragment shader) albo raz na wierzchołek (vertex shader). GLSL (OpenGL Shading Language) to język przypominający C, w którym się je pisze. W WebGL i Three.js wymagane są oba typy, ale to fragment shader odpowiada za większość wizualnej magii.

GPU uruchamia ten sam fragment shader dla każdego piksela jednocześnie. Canvas 1920×1080 uruchamia shader ~2 miliony razy w każdej klatce, wszystkie równolegle — dlatego shadery są tak szybkie.

Minimalny fragment shader

Jedynym wymaganym wyjściem jest gl_FragColorvec4 zawierający (czerwony, zielony, niebieski, alfa), każdy w zakresie 0.0–1.0.

// Jednolita czerwień — "hello world" shaderów precision mediump float; void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // RGB = czerwień, A = nieprzezroczysty }

Korzystanie ze współrzędnych: wbudowane gl_FragCoord

Uniform gl_FragCoord.xy podaje pozycję piksela w pikselach okna. Znormalizuj ją do [0,1] i użyj jako gradientu koloru:

precision mediump float; uniform vec2 u_resolution; // szerokość, wysokość — przekazane z JS void main() { vec2 uv = gl_FragCoord.xy / u_resolution; // normalizacja do [0,1]² gl_FragColor = vec4(uv.x, uv.y, 0.5, 1.0); // czerwony rośnie lewo→prawo, zielony dół→góra }

To renderuje gradient czerwono-żółty od lewej do prawej i zielono-cyjanowy od dołu do góry — klasyczny wynik wprowadzający do GLSL.

Podstawowe funkcje wbudowane

Funkcja Co robi Typowe zastosowanie
sin(x) / cos(x) Fala trygonometryczna, okres 2π Fale, oscylacje, rotacje
fract(x) Część ułamkowa x — czyli x mod 1.0 Powtarzające się kafelki, paski, siatki
length(v) Odległość euklidesowa wektora od początku Gradienty radialne, okręgi, pola odległości
smoothstep(a, b, x) Krzywa S od 0→1, gdy x zmienia się od a→b Wygładzone krawędzie, winiety
mix(a, b, t) Interpolacja liniowa: a*(1-t) + b*t Mieszanie kolorów, przejścia
abs(x) Wartość bezwzględna Symetria, składanie przestrzeni
floor(x) Zaokrąglenie w dół do liczby całkowitej Ekstrakcja identyfikatora komórki/siatki
clamp(x, lo, hi) Ograniczenie x do [lo, hi] Bezpieczne wartości koloru, głębi
dot(a, b) Iloczyn skalarny dwóch wektorów Oświetlenie, rzutowania, cieniowanie rozproszone
normalize(v) Wektor jednostkowy w tym samym kierunku Normalne, kierunki

Rysowanie okręgu za pomocą SDF

Pole odległości znakowanej (SDF) to funkcja zwracająca wartości ujemne wewnątrz kształtu i dodatnie na zewnątrz. Użyj length + smoothstep, aby uzyskać ostry, wygładzony okrąg:

precision mediump float; uniform vec2 u_resolution; void main() { vec2 uv = (gl_FragCoord.xy / u_resolution) * 2.0 - 1.0; // [-1, 1]² uv.x *= u_resolution.x / u_resolution.y; // korekta proporcji float d = length(uv); // odległość od środka float circle = smoothstep(0.5, 0.49, d); // 1 wewnątrz, 0 na zewnątrz gl_FragColor = vec4(vec3(circle), 1.0); }

Szum: tajny składnik

GLSL nie ma funkcji losowej, ale wiele shaderów udaje ją funkcją hash, a następnie wygładza w szum w stylu Perlina. Oto zwarta pseudolosowa funkcja hash używana w całej witrynie:

// Pseudolosowa z ziarna vec2 (0–1) float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 19.19); return fract(p.x * p.y); } // Szum wartościowy: bi-liniowa interpolacja 4 wartości hash float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); // krzywa smoothstep return mix(mix(hash(i), hash(i + vec2(1,0)), u.x), mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); }

Nakładając kolejne oktawy szumu o rosnącej częstotliwości i malejącej amplitudzie, otrzymujemy fraktalny ruch Browna (fBm) — turbulentną, przypominającą chmury teksturę używaną w symulacjach oceanu i atmosfery.

Używanie uniformów: przekazywanie danych z JavaScript

Uniformy to wartości tylko do odczytu przekazywane z CPU (JavaScript) do shadera w każdej klatce. W Three.js deklaruje się je w obiekcie uniforms, a w GLSL słowem kluczowym uniform:

// Strona JavaScript (Three.js) const material = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } }, vertexShader: `...`, fragmentShader: ` precision mediump float; uniform float u_time; uniform vec2 u_resolution; void main() { vec2 uv = gl_FragCoord.xy / u_resolution; float wave = sin(uv.x * 10.0 + u_time) * 0.5 + 0.5; gl_FragColor = vec4(wave, uv.y, 1.0 - wave, 1.0); } ` }); // W pętli animate: material.uniforms.u_time.value += 0.016; // przesuń czas w każdej klatce

Gdzie na tej stronie pojawiają się shadery

Fragment shader miesza odcienie głębokiego i płytkiego błękitu na podstawie uniformu mapy wysokości. Blask brzegowy Fresnela dodany obliczeniem dot(normal, viewDir).
4-oktawowy szum fBm napędza gęstość chmur. Progowy smoothstep tworzy ostrą wolumetryczną granicę między czystym niebem a chmurą.
Symulacja Gray-Scotta zapisuje do zmiennoprzecinkowej tekstury renderowania w każdym kroku. Fragment shader mapuje stężenia chemiczne u/v na kolor przez tablicę palety.
Natężenie obliczane analitycznie dla każdego piksela — bez tekstury, tylko matematyka. Wzór prążków jest wyłącznie wynikiem funkcji superpozycji fal w GLSL.
Warstwowy szum ze zmienną w czasie fazą i zniekształceniem o wysokich proporcjach daje efekt kurtynowego migotania. Całkowicie proceduralne — bez plików tekstur.
Kształty modów fali stojącej są obliczane dla każdego piksela. Próg blisko przejścia przez zero jest podświetlony, pokazując linie węzłowe, w których osiada piasek.

Debugowanie GLSL

GLSL nie ma console.log. Standardową techniką jest wizualizacja danych jako koloru:

Wskazówka wydajnościowa: Na urządzeniach mobilnych rozgałęzienia (instrukcje if) w shaderach mogą być znacznie wolniejsze niż arytmetyka, ponieważ na niektórych GPU obie gałęzie mogą wykonywać się równolegle. Preferuj mix(a, b, step(...)) i clamp zamiast warunków, kiedy tylko to możliwe.

Kolejne kroki

Gdy poczujesz się swobodnie z fragment shaderami, poznaj: