Pietrzak Roman Kemu Studio - yosh.ke.mu Pierwsza wersja: 2004 Ostatnie zmiany: 2010.11.21 Wszelkie prawa zastrzeżone Copyrights reserved 2004 - 2010 Programming of color gradientsProgramowanie gradientów (płynnych przejść kolorów)O czym to jestArtykuł jest przeznaczony dla początkujących programistów.Jest to krótkie wprowadzenie do tematu programowania gradientów, czyli płynnych przejść kolorów. Co się z tego dowiesz
GRADIENT - teoriaKOLORNiezależnie od modelu software'owej reprezentacji koloru, przed wyświetleniem każdego pixela na monitorze, jego reprezentaca zamieniana jest na model RGB. Z tego powodu, w tym artykule mówimy tylko o RGB. Inne reprezentacje, a także teoria koloru (wbrew pozorom RGB nie oddaje pełnej palety widzianych przez oko barw) nie są tematem tego artykułu.Model RGB pozwala na dowolne manipulowanie 3-ma składowymi koloru. R - czerwoną, G - zieloną, B - niebieską. Manipulujemy tutaj luminancją (świeceniem) tych składowych - odwrotnie, niż robią to malarze czy drukarze, którzy operują na przyciemnianiu składowych - dla nich, bardziej naturalny jest CMYK. W momencie prezentacji koloru jako RGB, przyjmujemy zwykle, że są to 3 wartości z tego samego zakresu (np od 0 do 100), chociaż oko ludzkie ma inną czułość na każdą ze składowych. RGBZ założenia, w komputerkach wszystko kręci się wokół reprezentacji dwójkowej. Dlatego najbardziej naturalną będzie reprezentacja składowych RGB w zakresach od 0 do 255.Z tego powodu, wygodnym jest zapis wartości RGB koloru w postaci szesnastkowej, jako 3 pary szesnastkowych cyfr. Wg szablonu: ![]() Czyli FFFFFF daje nam piękną czystą biel (wszystkie trzy kolory na maxa). Natomiast 000000 - czerń (trzy kolory na zero). Słówko o typowych zapisach hexa : - w C, C++ i Javie zapisujemy z prefixem 0x, czyli np 0x000000, albo 0xFFFFFF, - w HTMLu, CSSie itp. (zagadnienia związane z WWW), wymagają dodania znaku #. Odpowiednio mamy #000000, lub #FFFFFF, GRADIENT - pierwsze podejścieGradient powstaje przez "płynne" przejście wartości RGB pomiędzy jednym kolorem, a drugim. Ważne: każdą składową musimy rozpatrywać osobno.Najbanalniejsze gradienty widać na rysunku powyżej. Mamy tu liniowe przejścia pojedyńczej wartości z zera do FF (czerwony: #000000 do #FF0000, zielony: #000000 do #00FF00, niebieski: #000000 do #0000FF). Takie przejścia są banalne i łatwe do opisania prostym wzorem: value = ( x / max ) * 255 max - to długość gradientu (np długość linii na której rysujemy gradient, czy szerokość gradientowanego prostokąta)x - to położenie w gradiencie value - uzyskana wartość składowej Pseudokod który stworzy poziomą linię 100 pixelową, gradientowaną z czerni do niebieskiego wyglądałby więc np tak: for (x = 0; x < 100; x++) PutPixel(x, 0, (x*255)/100); Uzyskujemy tu liniowy rozkład liczb od 0..255. Czyli gradient od #000000 do #0000FF.Jak uzyskać pozostałe dwie składowe ? Najprościej - przesuwając bitowo (o 8 bitow dla zieleni lub 16 bitow dla czerwieni), albo mnożąc arytmetycznie (razy 256 dla zieleni lub 65536 dla czerwieni): czerwony = R << 16 = R * (256 * 256) = R * 65536 zielony = G << 8 = G * 256 niebieski = B W związku z tym, powstaje nam prosty wzór, który jest dla nas PODSTAWĄ do pracy z kolorem: value = ( R << 16 ) | ( G << 8 ) | B; ten sam wzór - wersja z operatorami arytmetycznymi:value = ( R * 65536 ) | ( G * 256 ) | B; R, G, B - poszczególne składowe (w zakresie 0 - 255)value - pełny kolor << - to w C, Javie i PHP operator przesunięcia bitowego w lewo. Pascalowy odpowiednik to shl | - to w C, Javie i PHP operator sumy logicznej (OR). Pascalowy odpowiednik to or Przykład: W celu otrzymania gradientu czarny-żółty, mixujemy wzrost liniowy R z takim samym wzrostem G: for (x = 0; x <= 100; x++) { value = (x*255)/100; PutPixel(x, 0, (value << 16) | (value << 8)); } ![]() GRADIENT - drugie podejścieRównie prosty efekt, polega na odwróceniu przyrostu składowych gradientu względem siebie. Np czerwony rosnie, podczas gdy niebieski maleje. Efekt uzyskamy przez proste odejmowanie od maxa:for (x = 0; x <= 100; x++) { value = (x*255)/100; PutPixel(x, 0, (value << 16) | ((255-value) << 8)); } ![]() Podsumowanie teoriiWłaściwie, koder z fantazją, mógłby w tym miejscu odpuścić sobie pozostałą część artykułu. W dalszej części, przejdziemy tylko w trochę bardziej zaawansowane matematycznie efekty i ich modele...GRADIENT - konkretyW powyższych przypadkach rozważamy tylko bardzo proste gradienty. A co jeśli chcielibyśmy zrobić płynne przejście z koloru A do koloru B ? Co jeśli kolory A i B są dla nas nieznane na etapie tworzenia kodu (nie możemy wówczas zdefiniować stałych wartości we wzorze) ?W tym momencie z pomocą przychodzi interpolacja. Bez strachu. Pojęcie interpolacji śni się studentom matematyki i informatyki po nocach. Ale w tym zastosowaniu jest potrzebna tylko w bardzo prościutkiej postaci... W 90% wystarczą nam gradienty liniowe. Tzn liniowe przejścia z koloru A do koloru B. Dlatego: Interpolacja liniowa - z punktu widzenia użycia w tworzeniu gradientuMamy liczbę A i liczbę B oraz liczbę kroków max, która oznacza ilość żądanych kroków pomiędzy liczbą A i liczbą B. Nasze zadanie polega na tym, żeby wyliczyć wartość value, która odda liniowo (proporcjonalnie) wartość spomiędzy A do B w punkcie x.Przykład A = 100, B = 200, max = 5. Należy więc rozłożyć pozostałe 4 kroki równo (liniowo) na zakresie <100 ; 200>.Czyli mamy 6 kroków od A do B - bo zerowy krok też liczymy (więc max + 1). W tym: krok(0) = A = 100 krok(5) = B = 200 I tu się przyda mały wzorek, który jest rdzeniem interpolacji liniowej w gradiencie: value = (B - A)*pos/max + A; A - wartość z której zaczynamyB - wartość na której kończymy max - ilość kroków (np pixeli w linii albo klatek w filmie) pos - aktualny krok, czyli pozycja, z przedziału <0 ; max> 3 kanały w interpolacjiPodczas interpolowania należy oczywiście każdy kanał liczyć oddzielnie. Typowo tworzymy funkcję interpolującą (która jako argumenty przyjmuje wartości A, B, pos, max, a zwraca nam zinterpolowany kolor) w takim schemacie:1. Separujemy składowe RGB z wejść A i B - np do tablicy. 2. Na każdej parze składowych pobranych z kolorów A i B wykonujemy interpolację i wynik umieszczamy w tablicy C. 3. Łączymy składowe z tablicy C do koloru i zwracamy wynik. Rozwiązanie ideowe napisane w PHP: function Interpolate2Colors($A, $B, $pos, $max_pos) { // Separujemy kanały $A_R = $A >> 16; $A_G = ($A >> 8) & 0xff; $A_B = $A & 0xff; $B_R = $B >> 16; $B_G = ($B >> 8) & 0xff; $B_B = $B & 0xff; //Interpolujemy wartości na kanałach $C_R = (($B_R - $A_R)*$pos)/$max_pos + $A_R; $C_G = (($B_G - $A_G)*$pos)/$max_pos + $A_G; $C_B = (($B_B - $A_B)*$pos)/$max_pos + $A_B; //Scalamy kanały wyniku $C = ($C_R << 16) + ($C_G << 8) + $C_B; return $C; } Skrócony pseudokod : function Interpolate2Colors(A, B, pos, max) (nawiasy są pokolorowane tylko po to żeby było łatwiej się połapać...){ C = (((B & 0xff) - (A & 0xff))*pos)/max + (A & 0xff); // liczymy B C = C + ((( ((B >> 8) & 0xff) - ((A >> 8) & 0xff) )*pos)/max + ((A >> 8) & 0xff)) << 8; // dodajemy G C = C + ((( ((B >> 16) & 0xff) - ((A >> 16) & 0xff) )*pos)/max + ((A >> 16) & 0xff)) << 16; // dodajemy R return C; } I kod który wykorzysta tą funkcję, do wygenerowania poziomej linii (od zółtego - 0xFFFF00, do różowego - 0xFF00FF): for (x = 0; x <= 100; x++) PutPixel(x, 0, Interpolate2Colors(0xFFFF00, 0xFF00FF, x, 100) ); Optymalizuj i ograniczajKolejny raz zaznaczam, że powyższe przykładowe źródełka, są ideowe. W przypadku, gdy gradientujemy pixel po pixelu, to powyższe kody będą za wolne. Aż proszą się o optymalizacje. Podobnie, w zależności od sposobu użycia, należy zastosować jakieś sprawdzanie zakresów (np. aby zapobiec wynikom ujemnym, lub co gorsza podawać wartości wyższe niż 0xFF) - oczywiście tylko, gdy to jest konieczne.Kilka uwag z doświadczenia:
GRADIENT - rozwinięcieMultigradient - ciąg liniowych gradientówWygenerowanie liniowego gradientu z ciągu n kolorów, to tak naprawdę suma niezależnych n - 1 gradientów.Np funkcja generująca gradient z 3 kolorów: function Interpolate3Colors($c1, $c2, $c3, $value, $max_value) W tej funkcji, na sztywno zdefiniowano, że w połowie długości <0; max_value>, ma być kolor c2.{ if ( $value < $max_value/2) $color = Interpolate2Colors($c1, $c2, $value, $max_value/2); else $color = Interpolate2Colors($c2, $c3, $value - $max_value/2, $max_value/2); return $color; } Podobna funkcja została użyta do generowania dynamicznych progress-barów w jednym z projektów ke.mu: ![]() Pełny rezultat można obejrzeć TUTAJ. Dla pełnego wachlarza możliwości (dla gradientowania n kolorów), polecam użyć wektora, tablicy, czy listy. W każdym elemencie definiującym jeden kolor potrzebujemy wtedy: - wartość RGB do interpolacji, - położenie tej wartości w całej długości gradientu (np w skali <0 ; 1000> - tak by się łatwo/szybko przeliczało na dowolny gradient końcowy), Wtedy do generacji wartości w danym punkcie, przeszukujemy wektor (tablicę/listę), w poszukiwaniu elementu zdefiniowanego przed i elementu za danym punktem. Rozważmy przykład Summer Fields: definicja gradientu: oczywiście, nie rozważamy sposobu w jaki powyższa definicja jest zakodowana - rodzaj użytej struktury danych zależy tu już od programisty.{ gradient(0) - kolor: 0x8080FF, pozycja: 0 gradient(1) - kolor: 0xC0FFFF, pozycja: 420 gradient(2) - kolor: 0xFFFFFF, pozycja: 499 gradient(3) - kolor: 0x008000, pozycja: 500 gradient(4) - kolor: 0xC0FFC0, pozycja: 570 gradient(5) - kolor: 0x008000, pozycja: 1000 } Załóżmy, że szukamy wartości koloru w punkcie 530 gradientu: ![]() Używamy interpolacji liniowej. Potrzebne są nam więc tylko wartości sąsiednie (najbliższe) 530. Postępowanie: 1. Przeszukujemy naszą strukturę danych zawierającą definicję gradientu, w poszukiwaniu wartości najbliższych. Widać, że są to kroki numer 3 i 4 (bo gradient(3) jest w pkt 500, a gradient(4) w pkt 570). 2. Przeliczamy interpolację standardową funkcją (Interpolate2Colors) podając jej: - pierwszy kolor: gradient(3) 0x008000 - drugi kolor: gradient(4) 0xC0FFC0 - pos: nasza poszukiwana pozycja minus pozycja gradient(3) 530 - 500 = 30 - max_pos: różnica między pozycjami gradient(3) i gradient(4) 570 - 500 = 70 Przykłady prostych gradientów uzyskanych tą metodą: ![]() Iloczyn, czyli nakładaniePraktycznie każdy gradient na linii (jednowymiarowy), można zrealizować za pomocą sumy przedstawionej wyżej. Natomiast w dwóch wymiarach (i więcej ?) przydaje się mnożenie gradientów.W wielu przypadkach, wystarczy mnożyć kanały koloru przez liczbę (gradient monochromatyczny). Np mnożenie przez gradientu kolorowego gradient monochromatyczny z zakresu <0 ; 1>, da efekt cieniowania. Tak jest zrealizowane cieniowanie w progress-barach przedstawionych wyżej. Przykład: Rysujemy gradientowany i cieniowany prostokąt 100x100. Gradient podstawowy (od 0xFFFF00 do 0x00FFFF), rozkładamy w osi X. Mnożymy go przez gradient monochromatyczny rozłożony w osi Y. for (y = 0; y <= 100; y++) UWAGA: raz jeszcze przypominam, że kod tu przedstawiany jest NIEOPTYMALNY (a powyższy, wręcz woła o pomstę). NIE stosować bez przemyślenia i optymalizacji.{ alpha = 100 - y; for (x = 0; x <= 100; x++) { color = Interpolate2Colors( 0xFFFF00, 0x00FFFF, x, 100); color_cieniowany = Interpolate2Colors( 0x000000, color, alpha, 100); PutPixel(x, y, color_cieniowany); } } Powyższy kod wygeneruje: ![]() Ciekawe efekty daje też przekraczanie zakresu <0 ; 1>, szczególnie w górę. Uzyskujemy wtedy efekt przesycenia. Przy dobranych parametrach, uzyskujemy łatwo wrażenie specular (odbijania światła), co daje efekt szklanej/metalicznej powierzchni... W takim przypadku należy zwrócić uwagę, żeby ostateczne składowe przed scaleniem obcinać do <0 ; 255> (żeby kanały nie przebijały się pomiędzy sobą - chociaż i w takim przypadku da się uzyskać ciekawe efekty). Kolejną wariacją, jest mnożenie przez siebie wielu gradientów kolorowych. Polecam experymentować z tym tematem... Suma ważonaCiekawe gradienty uzyskuje się przez zastosowanie sumy ważonej. Niezależnie od podejścia i rzeczywistej implementacji (można to zrobić na wiele sposobów), całość sprowadza się do tego, że definiuje się kilka punktów przestrzeni (płaszczyzny), każdemu nadając kolor i moc.Następnie dla każdego pixela generowanego gradientu badamy odległość od wszystkich (optymalizacja!) zdefiniowanych punktów w opisie gradientu. Tą odległość pomnożoną przez moc dla każdego gradientu uznajemy za wagę (wpływ). Następnie obliczamy sumę ważoną wszystkich (optymalizacja!) punktów koloru dla danego pixela. Powyżej jest użyte określenie odległość. Także tu można ciekawie podefiniować znaczenie tego pojęcia (osobno dla każego zdefiniowanego punktu !). Można np brać pod uwagę tylko odległość (przesunięcie, różnicę) w jednym wymiarze, albo dysproporcjonować wymiary... Opcji jest baaardzo wiele. W połączeniu z pozostałymi technikami (plus nieliniowości) dają niesamowite rezultaty. W tym miejscu jedynie pokażę prosty przykład (typowa skala kolorów z programów graficznych): ![]() Interpolacja nieliniowaBardzo ciekawe efekty daje zastosowanie interpolacji nieliniowej. W pomysłowo dobranych funkcjach, taki gradient może przestać przypominać gradient :)Sądzę, że jeśli ktoś dobrnął do tego punktu, to już sam ma setki pomysłów, jakich funkcji użyć i jak je ze sobą łączyć. Np, żeby uzyskać taki efekt: ![]() KoniecTemat ten jest bardzo obszerny, a to jest tylko wprowadzenie - dziękuję za przeczytanie...Mam nadzieję, że komuś się to przyda. Byłbym wdzięczny za wszelkie sugestie na adres yosh(at)ke(dot)mu O kopiowaniuTen artykuł może być kopiowany w inne miejsca, po uprzednim uzyskaniu zgody autora.Artykuł powinien być kopiowany W CAŁOŚCI (z rysunkami) oraz powinien zawierać link do yosh.ke.mu (z dopiskiem, że stąd pochodzi). |