Kolory
Nie mieliśmy zbytnio okazji porozmawiać o typach wektorowych w GLSL. Zanim przejdziemy dalej, ważne jest żebyśmy dowiedzieli się o nich więcej, a temat kolorów może być w tym bardzo pomocny.
Jeżeli paradgymat programowania obiektowego jest ci bliski, to prawdopodobnie zauważyłeś, że proces ekstrakcji danych z wektorów wygląda podobnie jak ekstracja danych z struct
'ów w C.
vec3 red = vec3(1.0,0.0,0.0);
red.x = 1.0;
red.y = 0.0;
red.z = 0.0;
Definiowanie kolor za pomocą notacji x, y i z jest trochę mylące, prawda? Właśnie dlatego istnieją inne sposoby dostępu do tej samej informacji, ale za pomocą innych nazw. Wartości .x
, .y
i .z
mogą być również uzyskane za pomocą .r
, .g
i .b
, jak i .s
, .t
i .p
(.s
, .t
i .p
są zwykle używane przy współrzędnych tekstur, które zobaczymy w następnych rozdziałach). Możesz również uzyskać dane wektora za pomocą indeksów [0]
, [1]
i [2]
.
Następujący kod przedstawia wszystkkie sposoby uzysakania tych samych danych:
vec4 vector;
vector[0] = vector.r = vector.x = vector.s;
vector[1] = vector.g = vector.y = vector.t;
vector[2] = vector.b = vector.z = vector.p;
vector[3] = vector.a = vector.w = vector.q;
Ta mnogość metod uzyskiwania tych samych danych jest tylko po to, aby ułatwić pisanie zrozumiałego kodu. Ta elastyczność wbudowana w język shadingowy stanowi też okazję, żebyś zaczął myśleć o współrzędnych koloru i przestrzeni jako wzjamenie zastępowalnych.
Inną świetną własnością typów wektorowych w GLSL jest to, że współrzędne mogą być mieszane w dowolnej kolejności, co ułatwia castowanie i mieszanie wartości. Właśność ta nazywana jest swizzle'owaniem.
vec3 yellow, magenta, green;
// Tworzenie żółtego
yellow.rg = vec2(1.0); // Przypisanie 1. do kanału czerwonego i zielonego
yellow[2] = 0.0; // Przypisanie 0. do kanału niebieskiego
// Tworzenie magenty
magenta = yellow.rbg; // Przypisanie wektora z przestawionym kanałem zielonym i niebieskim
// Tworzenie zielonego
green.rgb = yellow.bgb; // Przypisanie kanału niebieskiego do kanału czerwonego i niebieskiego
Mieszanie koloru
Teraz, gdy już wiesz jak definiuje się kolory, pora na połączenie tego z naszą wcześniejszą wiedzą. W GLSL istnieje bardzo przydatna funkcja mix()
, która pozwala mieszać dwie wartości wobec określonego stosunku wyrażonego w procentach. Potrafisz zgadnąć jaki jest zakres procentów? Oczywiście, że 0.0 i 1.0! Czas na wykorzystanie naszego shadingowego karate!
Sprawdź linijkę 18 poniższego kodu i zoabcz jak używamy wartości bezwględnej z sinusa od czasu, aby mieszać colorA
i colorB
.
Pokaż na co cie stać:
- Stwórz ekspresywną tranzycje między kolorami. Pomyśl o konkretnej emocji. Jaki kolor najlepiej ją reprezentuje? Jak wygląda? Jak zanika? Pomyśl o innej emocji i pasującym do niej kolorze. Zmień
colorA
icolorB
w kodzie powyżej, aby pasowały do tych emocji. Następnie zanimuj tę tranzycję za pomocą shaping function. Robert Penner stworzył serię popularnych shaping functions, z zasotoswaniami w animacji komputerowej, zwanych easing functions, skorzystaj z tego przykładu jako inspiracji, jednak najlepsze rezultaty osiągniesz tworząc własne tranzycje.
Zabawa z gradientem
Funkcja mix()
ma więcej do zaoferowania. Zamiast pojedynczego float
a, możemy podać zmienną tego samego typu, co dwa pierwsze argumenty; w naszym wypadku jest to vec3
. W ten sposób zdobywamy kontrolę nad stosunkiem mieszania każdego indywidualnego kanału koloru, r
, g
i b
.
Spójrz na poniższy przykład. Tak jak w przykładach z poprzedniego rozdziału, dzięki znormalizowanej współrzędnej x
tworzymy gradient i wizualizujemy go za pomocą linii. Aktualnie, wszystkie kanały leżą na tej samej linii.
Odkomentuj linjkę 25 i zobacz, co się stanie. Następnie odkomentuj linijki 26 i 27. Pamiętaj, że linie wizualizują, w jakim stosunku kanały (R, G, B) kolorów colorA
i colorB
są obecne w ostatecznym gradiencie.
Prawdopodobnie rozpoznajesz trzy shaping functions, które używamy w linijkach 25 i 27. Baw się nimi! Czas pokazać swoje umiejętności z poprzednich rozdziałów, tworząc interesujące gradienty. Spróbuj następujących ćwiczeń:
-
Skomponuj gradient przypominający zachód słońca Williama Turnera.
-
Zanimuj tranzycje między wschodem i zachodem za pomocą
u_time
. -
Czy potrafisz stworzyć tęczę korzystając z tego, czego nauczyliśmy się dotychaczas?
- Użyj
step()
, by stworzyć kolorową flagę.
HSB
Nie da się mówić o kolorze bez poruszenia tematu przestrzeni barw. Jak prawdopodobnie wiesz, istnieją też inne sposoby reprezentacji koloru poza RGB (z kanałem czerwonym, zielonym i niebieskim).
HSB oznacza Hue (pol. "barwa"), Saturation (pol. "nasycenie") i Brightness (pol. "jasność) i jest o wiele bardziej intuicyjną reprezentacją koloru niż RGB. Czasem Brightness nazywany jest "Value", stąd zamiast HSB można spotkać się też ze skrótem HSV. Przyjrzyj się funkcjom rgb2hsv()
i hsv2rgb()
w następującym kodzie:
Mapując pozycję na osi x do barwy i pozycję na osi y do jasności, otrzymujemy spektrum widzialnych kolorów. O wiele bardziej intuicyjnie wybiera się kolor HSB niż RGB.
HSB we współrzędnych biegunowych
Oryginalnie, HSB miało być reprezentowane z pomocą współrzędnych biegunowych (opartych na kącie i promieniu), a nie kartezjańskich (opartych na x i y). By zmapować naszą funkcję HSB do współrzędnych biegunowych, musimy otrzymać kąt i dystans od centrum kanwy do współrzędnej piksela. W tym celu użyjemy funkcje length()
i atan(y,x)
(który jest odpowiednikiem atan2(y,x)
w GLSL).
Pamiętaj: vec2
, vec3
i vec4
traktowane są jak wektory nawet jeśli reprezentują kolor. Zaczniemy traktować kolory i wektory bardzo podobnie.
Uwaga: Jest weięcej funkcji geometrycznych poza length
jak: distance()
, dot()
, cross
, normalize()
, faceforward()
, reflect()
i refract()
. Ponadto, GLSL ma specjalne funkcje do porównywania wektorów: lessThan()
, lessThanEqual()
, greaterThan()
, greaterThanEqual()
, equal()
i notEqual()
.
Gdy już zdobędziemy kąt i promień, musimy znormalizować ich wartości do zakresu od 0.0 do 1.0. W linijce 27, atan(y,x)
zwróci kąt w radianach między -PI a PI (-3.14 a 3.14), więc musimy podzielić tę liczbę przez TWO_PI
(zdefiniowane na górze kodu), uzyskując wartości między -0.5 i 0.5, które, przez proste dodawanie, mapujemy dalej do zakresu od 0.0 do 1.0. Promień ma długość 0.5 (ponieważ liczymy odległość od środka kanwy), więc musimy podwoić ten zakres (mnożąc przez 2), by uzyskać 1.0.
Jak widzisz, sedno leży w transformowaniu i mapowaniu zakresów do 0.0 i 1.0.
Spróuj poniższych ćwiczeń:
-
Zmodyfikuj przykład ze współrzędnymi biegunowymi, aby uzyskać kręcące się (jak w ikonce czekania myszki) koło barw.
- Użyj shaping function razem z funkcją konwersji z HSB do RGB, aby rozszerzyć jedną barwę i zwężyć inną.
- Jeśli przyjrzysz się uważnie kołom barw używanym w narzędziach do wybierania koloru (ang "color picker") (spójrz na obrazek poniżej), zobaczysz, że używają innego spektrum koloru, zgodnego z przestrzenią barw RYB. Przykładowo, w RGB kolorem przeciwnym do czerwonego jest cyjan, a w RYB - zielony. Czy potrafiłbyś zrekonstruować poniższy obrazek? [Wskazówka: to dobry moment, by użyć shaping functions]
- Przeczytaj książkę Josefa Albersa Interaction of Color i przestudiuj poniższe przykłady shaderów.
Uwaga o funkcjach i ich argumentach
Zanim przeskoczysz do następnego rozdziału, zatrzymajmy się na chwilę. Wróć do funkcji hsb2rgb
z poprzedniego interaktywnego przykładu. Zauważysz in
przed typem argumentu. Jest to kwalifikator (ang. "qualifier") i akurat ten oznacza, że zmienna jest tylko do odczytu. W przyszłości zobaczymy, że jest również możliwe, by poprzedzić argumenty kwalifikatorami out
i inout
. Kwalifikator out
określa, że argument jest tylko do zapisu (ang. "write-only"), natomiast inout
działa podobnie jak przekazywanie argumentu przez referencje, co umożliwia również modyfikowanie go.
int newFunction(in vec4 aVec4, // read-only
out vec3 aVec3, // write-only
inout int aInt); // read-write
Może w to nie uwierzysz, ale mamy wszystkie składniki potrzbne do tworzenia fajnych rysunków. W następnym rozdziale nauczymy się jak połączyć wszystkie poznane tricki, by stworzyć geometryczne formy/kształty przez blendowanie przestrzeni. Dobrze słyszysz... blendowanie przestrzeni.