The Book of Shaders by Patricio Gonzalez Vivo & Jen Lowe

Bahasa Indonesia - Tiếng Việt - 日本語 - 中文版 - 한국어 - Español - Portugues - Français - Italiano - Deutsch - Русский - Polski - English


Uniformy

Do tej pory widzieliśmy jak GPU zarządza wieloma równoległymi wątkami, z których każdy odpowiada za kolor części renderowanego obrazu. Choć wątki nie komunikują się między sobą, to jednak muszą jakoś otrzymywać input z CPU. Ze względu na architekturę karty graficznej taki input musi być jednakowy (ang. "uniform") dla wszystkich wątków i, z konieczności, tylko do odczytu (ang. "read-only"). Innymi słowy, każdy wątek otrzymuje takie same dane, które może odczytać, ale nie nadpisać, zmienić.

Inputy te nazywamy uniformami i mogę być większości wspieranych typów: float, vec2, vec3, vec4, mat2, mat3, mat4, sampler2D i samplerCube. Uniformy definiowane są zwykle na górze shaderu zaraz po przypisaniu domyślnej precyzji float'ów.

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;  // wielkość/rozdzielczość kanwy (szerokość, wysokość)
uniform vec2 u_mouse;       // pozycja myszy na kanwie (wyrażona w pikselach)
uniform float u_time;       // czas w sekundach od załadowania shadera

Wyobraź sobie te uniformy jak małe mosty między CPU i GPU. Ich nazwy bywają różne, ale w tej książce używam: u_time, u_resolution i u_mouse (przeczytaj komentarze w kodzie, aby wiedzieć, co robią). Podążam za konwencją dodawnaia u_ przed nazwą uniformów, aby było wiadomo, że nie są to zwykłe zmienne, ale ostatecznie jest to kwestia gustu. Przykładowo, ShaderToy.com używa takich samych uniformów, ale z następującym nazewnictwem:

uniform vec3 iResolution;   
uniform vec4 iMouse;        
uniform float iTime;        

(zwróć uwagę, że iResolution jest typu vec3, a iMouse typu vec4; uniformy te zawierają po prostu dodatkowe informacje, np.: stosunek szerokości do wysokości pikseli na ekranie, czy któryś z przycisków myszy został kliknięty albo czy jest przytrzymywany)

Koniec gadania, czas zobaczyć uniformy w akcji. W pożniszym kodzie używamy u_time (liczby sekund od uruchomienia shadera) razem z funkcją sinus, aby stworzyć animację przejścia od koloru czerwonego do czarnego.

Jak widać GLSL skrywa wiele niespodzianek, na przykład w postaci specjalnych, zaimplementowanych w hardware'rze, funkcji trygonometryczne czy wykładniczych. Tutaj podaję część z nich: sin(), cos(), tan(), asin(), acos(), atan(), pow(), exp(), log(), sqrt(), abs(), sign(), floor(), ceil(), fract(), mod(), min(), max() i clamp().

Pobawmy się powyższym kodem:

gl_FragCoord

GLSL daje nam nie tylko domyślny output vec4 gl_FragColor, ale również domyślny input w postaci vec4 gl_FragCoord, który przechowuje współrzędne piksela (inaczej: fragmentu), nad którym aktualnie pracuje wątek - dzięki vec4 gl_FragCoord wiemy, gdzie wątek pracuje wewnątrz kanwy. Nie nazywamy go uniformem, ponieważ jego wartość różni się między wątkami. Zamiast tego gl_FragCoord nazywamy varying (z ang. "zmieniający się", "różniący się").

W powyższy kodzie, normalizujemy współrzędne fragmentu poprzez podzielenie go przez rozdzielczość kanwy. W ten sposó otrzymujemy wartości z przedziału od 0.0 do 1.0, co ułatwia zmapowanie współrzędnych x i y do, odpowiednio, czerwonego i zielonego kanału.

W świecie shaderów nie mamy zbyt dużo sposóbów debuggowania poza przypisywaniem jaskrawych kolorów do zmiennych i wyciągania wniosków o działaniu shadera, na podstawie tego, co widzimy. Odkryjesz, że programowania GLSL jest często jak wkładanie miniaturowych statków do butelki - jest to trudne, ale tez piękne i satysfakcjonujące.

Czas przetestować nasze rozumienie kodu:

Po wykonaniu tych ćwiczeń, zapewne zastanawiasz się, gdzie jeszcze można użyć twoich nowych shaderowych mocy. W następnym rozdziale zobaczymy jak stworzyć swój własny shader w three.js, Processing i openFrameworks.