Pietrzak Roman
yosh (at) 3redants (dot) com
Copyright 2004

3 Red Ants Studio
www.3redants.com

Programming of color gradients


Programowanie gradientów (płynnych przejść kolorów)


O czym to jest


Ten artykuł jest przeznaczony dla początkujących programistów.
Jest to krótkie wprowadzenie do tematu programowania gradientów, czyli płynnych przejść kolorów.
Artykuł należy traktować jako wprowadzenie do tematu, a nie wyczerpującą informację.

Co się z tego dowiesz


- Najpierw opisujemy samo zagadnienie. Krótkie przypomnienie teorii koloru (RGB) i tematu interpolacji - w jak najbardziej strawnej postaci.

- Potem piszemy troszkę prostego kodu. Chodzi o podanie konkretnych rozwiązań i przykładów - skupiamy się tu nad prostymi gradientami.
Raz będzie to "pseudokod" (język domyślny), a czasami piszemy przykłady w PHP. Wybrałem PHP, bo wydaje mi się dość przejrzysty, kod jest minimalny, a przeniesienie tego na dowolny inny język nie jest problemem.
Zwróćcie uwagę, że kod podawany tutaj, nie jest optymalny, ani napisany dobrym stylem - jego celem jest zrozumienie tematu.

- Następnie spróbujemy rozważyć troszkę bardziej złożone gradienty i... i kończymy - krótko i na temat.

GRADIENT - teoria


KOLOR


Niezależnie od modelu software'owej reprezentacji koloru, prędzej czy później, w procesie obróbki obrazu, musi ona zostać zamieniona 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.

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 jest czułe inaczej na każdą z nich.

RGB


Z 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 wydaje się być 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:
- w C zapisujemy zgodnie ze specyfikacją hexa C, czyli np 0x000000, albo 0xFFFFFF - dodajemy przedrostek 0x,
- w HTMLu, CSSie itp. (zagadnienia związane z WWW), wymagają dodania znaku #. Odpowiednio mamy #000000, lub #FFFFFF,

GRADIENT - pierwsze podejście


Gradient powstaje przez przejście wartości RGB pomiędzy jednym kolorem, a drugim. Z tymże, 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ę gradientowaną z czarnego 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).

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;

R, G, B - poszczególne składowe (w zakresie 0 - 255)
value - pełny kolor
<< - to w C i PHP operator przesunięcia bitowego w lewo. Pascalowy odpowiednik to shl
| - to w C 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ście


Ró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 teorii


Właściwie, koder z fantazją, mógłby w tym miejscu odpuścić sobie pozostałą część artykułu. Po prostu w dalszej części, będziemy tylko starali się przechodzić w coraz bardziej zaawansowane matematycznie efekty i ich modele...

GRADIENT - konkrety


W 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 gradientu


Mamy 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.
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

Należy więc rozłożyć pozostałe 4 kroki równo (liniowo) na zakresie <100 ; 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 zaczynamy
B - 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 interpolacji


Podczas 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)
{
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;
}
(nawiasy są pokolorowane tylko po to żeby było łatwiej się połapać...)

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 ograniczaj


Kolejny 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:
- ścisłe typy i co za tym idzie ograniczenie wartości - należy zadbać, żeby wartości na wejściu nigdy nie były inne niż "3 razy kanał <0 ; 255>". Jakiekolwiek ujemne wyliczenia lub przekroczenie zakresu, spowoduje problemy.
- w językach z typami, należy przemyśleć, gdzie w kodzie użyć signed int, a gdzie unsigned int. Generalnie, w pewnych okolicznościach można się zdecydować na float'y (choćby do tego, żeby tylko jednokrotnie liczyć współczynik (pos/max) ).
- w C czy C++ warto zastosować unie - dobry kompilator sam zoptymalizuje rozdzielenia i scalenia kanałów.
- w kodzie niższego poziomu, nieocenione są roszerzenia typu MMX - wszelkie operacje na kanałach, możnaby wykonać bez ich dzielenia/scalania.

GRADIENT - rozwinięcie


Suma, czyli ciąg


Wygenerowanie 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)
{
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;
}

W tej funkcji, "na sztywno" zdefiniowano, że w połowie długości <0; max_value>, ma być kolor c2.

Podobna funkcja została użyta do generowania dynamicznych progress-barów w projekcie dla "Warsztatu":
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 (typowy gradient "Summer Fields"):
definicja gradientu:
{
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
}
oczywiście, nie rozważamy sposobu w jaki powyższa definicja jest zakodowana - rodzaj użytej struktury danych zależy tu już od programisty.

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ładanie


Praktycznie 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 - po ludzku "nakładanie".

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++)
{
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);
}
}
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.

Powyższy kod wygeneruje:


Ciekawe efekty daje też przekraczanie zakresu <0 ; 1>, szczególnie "w górę". Uzyskujemy wtedy efekt przesycenia. Przy ciekawie 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żona


Ciekawe 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 nieliniowa


Bardzo 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:


Koniec


Temat 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)3redants(dot)com

O kopiowaniu


Ten 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 www.3redants.com (z dopiskiem, że stąd pochodzi).