czwartek, 22 maja 2008

Programowanie Shaderów w Pixel Bender

Jak wspomniałem za programowanie filtrów i shaderów (blendingu) w Flash Player 10 odpowiada technologia Bender Pixel. Najpierw instalujemy Pixel Bender Toolkit i instalacja przebiega po polsku. Po zainstalowaniu Pixel Bender Toolikt uruchamiamy to cudo - może sie pojawić komunikat po angielsku, że nie mamy karty GPU. Z menu File -> Load Images wybieramy jakiś obrazek. Następnie wybieramy skrypt z menu File -> Open Pixel Bender Kernel Filter i wskazujemy sciezkę C:\Program Files\Adobe\Adobe Utilities\Pixel Bender Toolkit\pixel bender files i wybieramy plik sepia.pbk. Uruchamiamy to z menu Build -> Run
Ustawiamy w pasku scrollera intensity na 0.2.Jeśli podoba się to nam to tworzymy plik bajtkod dla Flash Player 10. Z memu wybieramy File -> Export Pixel Bender Byte Code for Flash i zapisujemy jako sepia.pbj. Następnie wchodzimy na tą stronę. Klikamy w przycisk Add
Pojawi sie okno do wybrania pliku wybieramy (tam gdzie zapisaliśmy) plik sepia.pbj
Ale nie sobaczyliśmy czego chcieliśmy czyli efektu sepii. Wracamy do Pixel Bender Toolkit. W kodzie pliku sepia.pbk szukamy linijki 86 która zawiera kod:
yiqaColor.y = intensity;
zmieniamy to na
yiqaColor.y = 0.2;
Zapisujemy to wybierając z menu File -> Save Pixel Blender Kernel Filter As.. jako sepia2.pbk. A następnie wybieramy z memu File -> Export Pixel Bender Byte Code for Flash i zapisujemy jako sepia2.pbj. Następnie wchodzimy i odświeżamy tą stronę. Klikamy w przycisk Add. Pojawi sie okno do wybrania pliku i wybieramy (tam gdzie zapisaliśmy) plik sepia2.pbj. I zobaczyliśmy to co chcieliśmy.

Na początek programowania weźmiemy sobie kod który nic nie robi a jest podstawą do pracy nad kolejnymi skryptami

<languageVersion : 1.0;>
kernel NowyFiltr
< namespace : "info.malaj";
vendor : "Michal Malaj";
version : 1;
description : "Pierwszy kod";
>
{
input image4 src;
output pixel4 dst;
void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
}
}


Nazwa kernela nie może zawierać polskich znaków, i tak lepiej poza komentarzami i metatagami nie używać w plikach języka polskiego. Kopiujemy powyższy kod i zapisujemy go. Otóż powyższy kod działa w ten sposób, że pobiera dane z obrazka. Słowo kluczowe input oznacza że dane są wyjściowe typu image4 i są zdefiniowanej w zmiennej o nazwie src. Następne słowo kluczowe output oznacza że dane będą wynikiem końcowym jako typu pixel4 (oznacza to że informacje o kolorze jednego piksela są przechowywane w 4 wartościach RGBA) i są zdefiniowanej w zmiennej dst. Następnie mamy zwykła procedurę evaluatePixel(), która dokonuje obliczeń na tych danych. Tym razem mamy jedną linijkę kodu przypisujemy do zmiennej dst wynik działania funkcji sampleNearest(). Ta funkcja pobiera dane z obrazka i o jego współrzędnych i przekształca to na piksele, na których dalej będzie można dokonywać operacji. W tym przypadku można powiedzeć że zwraca informacje o pikselach w tym obrazku i nic więcej.
Teraz dokonamy operacji przyciemniania obrazu oznacza to, że każda wartość z RGBA zostanie pomniejszona o połowę. Matematyczne to polega na pomnożeniu przez wartość 0.5.

Oto kod przyciemnianie.pbk
<languageVersion : 1.0;>
kernel Przyciemnianie
< namespace : "malaj.info";
vendor : "Michał Małaj";
version : 1;
description : "efekt przyciemniania";
>
{
input image4 src;
output pixel4 dst;
void
evaluatePixel()
{
dst = 0.5 * sampleNearest(src,outCoord());
}
}

Wadą powyższego rozwiązania jest to, że kanał alfa stawał sie bardziej przezroczysty i widać było przenikanie tła. Kolejny przykład pokaże jak sobie z tym poradzić

Kod przyciemnianiebezalfy.pbk
<languageVersion : 1.0;>
kernel Przyciemnianie
< namespace : "info.malaj";
vendor : "Michał Małaj";
version : 1;
description : "Przyciemnianie bez alfy";
>
{
input image4 src;
output pixel4 dst;
void
evaluatePixel()
{
float4 inputColor = sampleNearest(src,outCoord());
dst.rgb = 0.5 * inputColor.rgb;
dst.a = inputColor.a;
}
}

Podobnie można zrobić filtr ekspozycji. Potocznie mówi się że to jest rozjaśnianie.

<languageVersion : 1.0;>
kernel Rozjasnianie
< namespace : "info.malaj";
vendor : "Michal Malaj";
version : 1;
description : "Filtr ekspozycji";
>
{
input image4 src;
output pixel4 dst;
void
evaluatePixel()
{
float4 inputColor = sampleNearest(src, outCoord());
dst.rgb = pow(inputColor.rgb, float3(0.5));
dst.a = inputColor.a;
}
}

Najlepszą rzeczą jaką jest w tej technologii to modyfikacja parametrów podczas działania. Czyli trzeba dodać parametry, które mogą być modyfikowane. Oto kod parametry.pbk, który pozwoli na modyfikację parametru ekspozycji naświetlania.

<languageVersion : 1.0;>
kernel Rozjasnianie
< namespace : "info.malaj";
vendor : "Michal Malaj";
version : 1;
description : "Filtr ekspozycji";
>
{
input image4 src;
output pixel4 dst;
parameter float exposure
<
minValue:float(-0.5);
maxValue:float(0.5);
defaultValue:float(0.0);
>;
void
evaluatePixel()
{
float4 inputColor = sampleNearest(src, outCoord());
dst.rgb = pow(inputColor.rgb, float3(1.0 - exposure));
dst.a = inputColor.a;
}
}

Teraz zrobimy efekt czarno-białej fotografii

<languageVersion : 1.0;>
kernel BlackWhite
< namespace : "info.malaj";
vendor : "Michał Małaj";
version : 1;
description : "efekt czarno-białej fotografii";
>
{
input image4 src;
output pixel4 dst;
void
evaluatePixel()
{
dst = sampleNearest(src,outCoord()).rrra;
}
}

Jak widać oznacza to, że kompilator bierze tylko z dane z pikseli kanału koloru czerwonego i przypisuje jej zwartość do kanału koloru niebieskiego i zielonego. To dobrze działa jak na zdjęciu mamy dużo czerwonego koloru. Więc trzeba dać użytkownikowi możliwość wpływania na takie działanie. W tej sytuacji pokażę kod w którym w wyniku zmiany parametru obraz przekształca sie w czarno-białą fotografię.

<languageVersion : 1.0;>
kernel ParamBlackWhite
< namespace : "info.malaj";
vendor : "Michał Małaj";
version : 1;
description : "efekty przejścia w czarno-białą fotografię";
>
{
input image4 src;
output pixel4 dst;
parameter float crossfade;
void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
float3 bw = dst.rrr;
dst.rgb = ((1.0 - crossfade) * dst.rgb) + (crossfade * bw);
}
}
To widać jak mało doskonałe jest przetwarzanie kolorów. W tym momencie przychodzi znajomość luminacji. W skrócie to polega na obliczeniu natężenia światła. Dla naszych potrzeb wystarczy już gotowy wzór dla wartości RGB Y = 0.2126 R + 0.7152 G + 0.0722 B

<languageVersion : 1.0;>
kernel LunaBlackWhite
< namespace : "info.malaj";
vendor : "Michał Małaj";
version : 1;
description : "efekty lumy przejścia w czarno-białą fotografię";
>
{
input image4 src;
output pixel4 dst;
parameter float crossfade;
void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
float luminance = dst.r * 0.212 + dst.g * 0.715 + dst.b * 0.07;
dst.rgb = ((1.0 - crossfade) * dst.rgb) + (crossfade * float3(luminance));
}
}
Luminację można zrobić przy pomocy iloczynu skalarnego i wykorzystać funkcję mix(), odpowiadającą za liniową interpolacją kanałów RGB z tą luminacją

<languageVersion : 1.0;>
kernel LunaDotBlackWhite
< namespace : "info.malaj";
vendor : "Michał Małaj";
version : 1;
description : "efekty lumy z iloczynem skalarnym przejścia w czarno-białą fotografię";
>
{
input image4 src;
output pixel4 dst;
parameter float crossfade;
const float3 lumMult = float3(0.212, 0.715, 0.07);
void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
float luminance = dot(dst.rgb, lumMult);
dst.rgb = mix(dst.rgb, float3(luminance), crossfade);
}
}

Warto przyjrzeć się kolejnemu kodu odpowiedzialnego za wygenerowanie sepii

<languageVersion : 1.0;>
kernel Sepia
< namespace : "info.malaj";
vendor : "Michał Małaj";
version : 1;
description : "efekty przejścia w fotografię sepii";
>
{
input image4 src;
output pixel4 dst;
parameter float crossfade
<
minValue:float(0.0);
maxValue:float(1.5);
defaultValue:float(1.0);
>;
const float3 lumMult = float3(0.212, 0.791, 0.07);
const float3x3 sepiaMatrix = float3x3(0.400, 0.769, 0.189,0.349, 0.686, 0.168, 0.272, 0.534, 0.131);

void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
float3 sepia = dst.rgb * sepiaMatrix;
dst.rgb = mix(dst.rgb, sepia, crossfade);
}
}

Zawsze można wygenerować bajtkod z tych efektów dla Flash Playera 10. Jak widać kod efektów jest bardzo zwięzły. Warto pokazać jak za pomocą ActionScript 3 można wykorzystać ten bajtkod.

4 komentarze:

Marek Brun pisze...

Jak zwykle świetny post!
Nie wiesz może jak pobrać szerokość/wysokośc obrabianego obrazka?

Michał Małaj pisze...

@marek
Przypuszczam, że Ci chodziło o te parametry?
float2 pixelSize( image1 ).x
float2 pixelSize( image1 ).y

Marek Brun pisze...

Zdaje się że te wartości to zazwyczaj 1, bo pixel ma taką szerokość/wysokość :)
Ale muszą być sytuacje że jest inaczej skoro jest taka funkcja. Tak czy inaczej to o co mi chodziło jest niedostępne. Jak pisze wPixelBenderLanguage10.pdf:
"Note that we have not anywhere referenced the size of the output image. Images do not
have a “size” per se in the Pixel Bender model—neither outputs nor inputs. Each image is
instead thought of as being defined over an infinite plane of discrete pixel coordinates.
The run-time machinery that invokes the kernel takes care of determining the actual
buffer sizes in which the pixels to be operated on are stored. This lack of “size” has some
implications for filter design. For example, it is not possible to write a Pixel Bender
kernel that reflects an image around its “center”, because the center is undefined. Instead,
3ഊthe kernel must reflect around the origin, or the line of reflection must be explicitly
denoted by passing it as a kernel parameter."

Co może wydawać się dziwne jeśli pracujemy z samym PB, tam mamy obrazek no i niby taki podstawowy parametr ;) Ale w końcu docelowo to bedzie działać na DisplayObject który właśnie nie ma stałego rozmiaru, więc jest ok.

BurzaOne pisze...

Denerwujące są ograniczenia dla FlashPlayera, które uniemożliwiają sensowne pisanie filtrów. Jak na razie to tylko zabawka do robienia efektów ;/ Użycie jakiegokolwiek kontekstu wiąże się z pisaniem niesamowicie długiego kodu. Sama idea fajna.