Informacje i porady dotyczące pisania shaderów. 1. Informacje ogólne Będziemy się zajmować shaderami w wersji 2.0, które są obsługiwane przez karty graficzne z procesorami ATI Radeon serii 9000 (premiera w 2002 roku) i późniejszymi oraz NVIDIA GeForce serii FX 5000 (premiera w 2003 roku) i późniejszymi. Komputery w laboratoriach komputerowych w sali 317 mają dobre karty graficzne, na których nie będzie problemów z uruchomieniem shaderów. Shadery można pisać w dowolnym edytorze i kompilować z użyciem funkcji DirectX/OpenGL, które również zwracają informacje o błędach w kodzie. Poza tym istnieją narzędzia do testowania samych shaderów takie jak ATI Rendermonkey (http://ati.amd.com/developer/rendermonkey/downloads.html), czy NVIDIA FX Composer, z którego jednak nigdy nie korzystałem (http://developer.nvidia.com/object/fx_composer_home.html). Produkt ATI obsługuje języki HLSL i GLSL, produkt NVIDII HLSL i CgFX, oba zawierają sporo przykładów. W dalszej części tego tekstu omawiane będą tylko języki HLSL dla DirectX i GLSL dla OpenGL. Shadery to programy wykonywane na karcie graficznej. Początkowo istniały dwa rodzaje shaderów i tylko one będą przedmiotem zajęć: shadery wierzchołków i shadery pikseli (w dokumentacji OpenGL nazywane programami wierzchołków i programami fragmentów). Shader wierzchołków, po jego uaktywnieniu, wywoływany jest raz dla każdego wierzchołka renderowanej siatki trójkątów. Otrzymuje na wejściu wszystkie dane związane z wierzchołkiem (pozycja w lokalnym układzie współrzędnych, normalna, współrzędne tekstury) i musi policzyć pozycję wierzchołka w układzie kamery. Poza tym może policzyć i przekazać do shadera pikseli dowolne dodatkowe informacje, przydatne potem przy wyliczeniach koloru piksela. Shader pikseli wywoływany jest raz dla każdego rysowanego piksela. Jego zadaniem jest policzenie koloru, wraz z wartością kanału alfa. Shader pikseli otrzymuje na wejściu to wszystko, co jest wyjściem shadera wierzchołków. Dokładniej po wywołaniu shadera wierzchołków dla każdego z wierzchołków rysowanego trójkąta, każda z wartości zwracanych przez shader wierzchołków jest interpolowana liniowo z korekcją perspektywy na powierzchni trójkąta i po interpolacji przekazywana do shadera pikseli.
2. Obsługa shaderów w DirectX. Skompilowane zbiory shaderów przechowywane są w obiektach typu Effect. Na początku po uruchomieniu aplikacji i zainicjowaniu urządzenia DirectX trzeba wczytać ich kod źródłowy w języku HLSL z pliku tekstowego (zwykle ma on rozszerzenie.fx ) i skompilować. Służy do tego na przykład polecenie Effect.FromFile(...). W jednym pliku HLSL może mieścić się dowolnie dużo shaderów wierzchołków i pikseli. Fragment pliku w języku HLSL postaci: technique render pass p0 VertexShader = compile vs_2_0 RenderVS(); PixelShader = compile ps_2_0 RenderPS(); definiuje technikę o nazwie render, na którą składa się para złożona z shadera wierzchołków (procedura RenderVS() w programie HLSL) i pikseli (procedura RenderPS() w programie HLSL). Wybranie aktywnej techniki dla obiektu typu Effect odbywa się poprzez przypisanie jej nazwy do pola.technique. Renderowanie z użyciem shaderów odbywa się tak: numpasses = effect.begin(fx.none); for (int passnumber = 0; passnumber < numpasses; ++passnumber) effect.beginpass(passnumber);... // tu wstawić wywołania funkcji rysujących effect.endpass(); effect.end(); Zmienne globalne w programie HLSL (stałe shadera) można i trzeba zmieniać z poziomu DirectX. Służy do tego metoda SetValue, której pierwszy parametr to napis z nazwą zmiennej globalnej, a drugim może być w zależności od typu tej zmiennej, obiekt tekstury, macierz, wektor, liczba. W kodzie HLSL macierz deklarujemy tak: float4x4 ViewProjectionMatrix;, wektor tak: float4 LightDir;, a teksturę tak: texture ColorTexture <string TextureType = "2D";>; sampler ColorSampler = sampler_state Texture = <ColorTexture>; MipFilter = LINEAR; MagFilter = LINEAR; MinFilter = LINEAR; ADDRESSU = clamp; ADDRESSV = clamp; ; (do tekstury będziemy się odwoływać poprzez ColorSampler ; widać, że w samym programie HLSL ustawiane są parametry filtrowania i zawijania tekstur). Wejście i wyjście shadera wierzchołków i pikseli najczęściej delkaruje się w HLSL w postaci struktur np.: struct VSInput float4 p : POSITION; float3 n : NORMAL;
float3 t : TEXCOORD0; ; Dla wejścia shadera wierzchołków napisy :POSITION, czy :NORMAL informują DirectX jak powiązać dane siatki trójkątów z odpowiednimi polami struktury. Takie same opisy muszą mieć wszystkie pola dla wyjścia oraz zarówno wejścia jak i wyjścia shadera pikseli. Shader wierzchołków musi zwrócić strukturę z wypełnionym polem z opisem :POSITION, a shader pikseli z :COLOR. Wejście shadera pikseli różni się zwykle od wyjścia shadera wierzchołków tylko tym, że nie posiada pola postaci float4 pos : POSITION;. Pola z opisami :TEXCOORD0 do :TEXCOORD7 należy traktować jako ogólnego przeznaczenia mogą przechowywać dowolne wartości, nie mające nic wspólnego z teksturowaniem. Shader wierzchołków i shader pikseli to pojedyncze funkcje, które mogą się odwoływać do innych funkcji w programie HLSL. Po resztę informacji na temat języka HLSL odsyłam do dokumentacji DirectX oraz analizy załączonych tam tutoriali i przykładów. Trzeba jeszcze tylko pamiętać, że uaktywnienie shaderów, wyłącza część funkcjonalności DirectX: światła, teksturowanie, macierze przekształceń odpowiedzialność za te zadania zostaje przeniesiona na shadery.
3. Obsługa shaderów w OpenGL Opisane tu zostanie korzystanie z języka GLSL (OpenGL Shading Language), który wszedł do standardu OpenGL w wersji 2.0. Aby mieć dostęp do funkcji standardu w wersji 2.0, w C/C++ wygodnie jest skorzystać z GLEW (OpenGL Extension Wrangler: http://glew.sourceforge.net/), a w C# z bibliotek TAO. Dokumentację OpenGL i GLSL można znaleźć tutaj: http://www.opengl.org/documentation/specs/. W GLSL najprościej przechowywać każdy shader w osobnym pliku tekstowym (shader wierzchołków i pikseli związany z jednym efektem w dwóch osobnych plikach). Na początku po zainicjowaniu OpenGL, trzeba wczytać i skompilować wszystkie shadery. Poniżej zamieszczam przykładowy fragment kodu w C, który to wykonuje: char * read_text_file(const char *filename) FILE *file = fopen(filename, "rt"); fseek(file, 0L, SEEK_END); long size = ftell(file); fseek(file, 0L, SEEK_SET); char *buf = new char[size+1]; int bytes = (int)fread(buf, 1, size, file); buf[bytes] = 0; fclose(file); return buf; void print_info_log(glhandlearb object) int maxlength = 0; glgetobjectparameterivarb(object, GL_OBJECT_INFO_LOG_LENGTH_ARB, &maxlength); char *infolog = new char[maxlength]; glgetinfologarb(object, maxlength, &maxlength, infolog); MessageBoxA(NULL, infolog, "GLSL_ERROR", MB_OK); delete[] infolog; void add_shader(glhandlearb programobject, const char *filename, GLenum shadertype) GLcharARB *shaderdata = read_text_file(filename); const GLcharARB *const_shaderdata = shaderdata; GLhandleARB object = glcreateshaderobjectarb(shadertype); glshadersourcearb(object, 1, &const_shaderdata, NULL); glcompileshaderarb(object); GLint compiled = 0; glgetobjectparameterivarb(object, GL_OBJECT_COMPILE_STATUS_ARB, &compiled); if (!compiled) print_info_log(object); exit(1); glattachobjectarb(programobject, object); gldeleteobjectarb(object); delete[] shaderdata; GLhandleARB init(const char *filenamevp, const char *filenamefp) GLhandleARB program = glcreateprogramobjectarb();
if (filenamevp) add_shader(program, filenamevp, GL_VERTEX_SHADER_ARB); add_shader(program, filenamefp, GL_FRAGMENT_SHADER_ARB); gllinkprogramarb(program); GLint linked = false; glgetobjectparameterivarb(program, GL_OBJECT_LINK_STATUS_ARB, &linked); if (!linked) print_info_log(program); exit(1); return program; Wywołanie funkcji init(...) z podanymi nazwami plików z programem wierzchołków (shaderem wierzchołków) i programem fragmentów (shaderem pikseli) w GLSL, utworzy obiekt typu GLhandleARB, przez który będzie można się od tej pory odwoływać do shaderów z poziomu OpenGL. W przypadku błędów w kodzie GLSL, funkcja print_info_log wypisze odpowiednie komunikaty w okienku informacyjnym. Renderowanie siatki trójkątów z użyciem shaderów odbywa się następująco: gluseprogramobjectarb(program);...// tu wywołujemy funkcje rysujące gluseprogramobjectarb(0); W OpenGL shadery mogą korzystać z macierzy przekształceń OpenGL, oraz ustawień tekstur OpenGL. Mimo to można i trzeba korzystać ze zmiennych globalnych (stałych shadera) w programach GLSL, które zmieniane są z poziomu OpenGL. W kodzie GLSL deklarujemy liczby, wektory, macierze, tekstury w postaci: uniform float fvalue; varying vec3 vvalue; uniform mat4 mvalue; uniform sampler2d texture;. Z poziomu OpenGL modyfikujemy ich wartość za pomocą wywołań: gluniform1farb(glgetuniformlocationarb(program, "fvalue"), fvalue); gluniform3fvarb(glgetuniformlocationarb(program, "vvalue"), 1, &(vvalue.x)); gluniformmatrix4fvarb(glgetuniformlocationarb(program, "mvalue"), 1, false, &(mvalue[0][0])); gluniform1iarb(glgetuniformlocationarb(program, "texture"), 0); W przypadku tekstury widać, że podajemy nie obiekt tekstury, tylko numer tekstury (numer aktywnej tekstury, której dotyczą wywołania OpenGL można zmienić z domyślnego 0 poprzez wywołanie glactivetexture(...)). Shader wierzchołków musi zawierać funkcję void main(), która musi zapisać do zmiennej gl_position pozycję wierzchołka w układzie kamery (normalnie jest to: gl_position = gl_projectionmatrix * gl_modelviewmatrix * gl_vertex; co jest równoważne w GLSL gl_position = ftransform();). W GLSL istnieje wiele nazw specjalnych zmiennych i stałych rozpoczynających się od gl_, które związane są z odpowiednimi obiektami w OpenGL i które są dostępne w dowolnym miejscu podczas pisania shaderów. Shader pikseli musi zawierać funkcję void main(), która zapisze kolor piksela wraz z wartością kanału alfa, do gl_fragcolor. Przekazywanie danych pomiędzy shaderem wierzchołków i shaderem pikseli odbywa się w bardzo wygodny sposób. Wystarczy zadeklarować w każdym z nich zmienną globalną o tej samej nazwie poprzedzoną słowem varying, a następnie zapisać do niej odpowiednio wyliczoną wartość w shaderze wierzchołków.