Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Preprocesor w shaderach HLSL

Wstęp

Artykuł pokazuje, jak się używa i jak można wykorzystać preprocesor, w który wyposażony jest język shaderów HLSL. Opisuje metodę subtraktywną tworzenia różnych wersji shaderów na podstawie jednego źródła. Do jego zrozumienia potrzebna jest umiejętność programowania w C++, znajomość biblioteki Direct3D oraz pisania shaderów w HLSL.

Wprowadzenie

Pojawienie się shaderów dało programistom grafiki dużo większą elastyczność w projektowaniu obliczeń, jakie ma wykonywać karta graficzna na wierzchołkach i pikselach. Wcześniej, w potoku predefiniowanym (ang. Fixed Function Pipeline), wszystko trzeba było ustawiać za pomocą stanów przez funkcje takie jak SetRenderState czy SetTextureStageState. Teraz rolę większości z tych ustawień przejął vertex shader i pixel shader, umożliwiając wpisanie kodu dokonującego niemal dowolnych obliczeń. Implementację wszystkich możliwości potoku predefiniowanego za pomocą shaderów HLSL można znaleźć w [1].

Niestety, shaderów nie można ze sobą łączyć. Na raz może być aktywny tylko jeden vertex shader i tylko jeden pixel shader i musi on liczyć wszystko, co jest do policzenia dla wierzchołków i dla pikseli. Tymczasem o obliczeniach tych nadal warto myśleć jak o zestawie ustawień. Przykładowo, zależnie od materiału, oświetlenie może mieć lub nie mieć liczony dodatkowo odblask Specular, a kolor może być brany albo z wierzchołków, albo z tekstury. Już samo to daje cztery różne kombinacje i dla każdej z nich należałoby napisać osobny shader.

Jeśli piszesz coś więcej niż pojedyncze efekty graficzne (na przykład silnik 3D), szybko natrafisz na problem rosnącej w tempie wykładniczym liczby różnych kombinacji możliwości, które musi dostarczać shader zależnie od ustawień materiału, światła i innych obiektów biorących udział w renderowaniu. Jest kilka sposobów na poradzenie sobie z tym problemem:

  • Można starać się używać tylko jednego lub kilku shaderów do renderowania wszystkiego. Na przykład skoro odblask jest dodawany do końcowego koloru, jako kolor odblasku można ustawić 0 (czarny) i wtedy wszystko działa tak, jak gdyby odblasku w ogóle nie było. Niestety, karta graficzna nadal musi wtedy liczyć dla każdego wierzchołka czy piksela to, czego i tak nie widać i czego mogłoby w ogóle w kodzie shadera nie być.
  • Można użyć Static Branching. Nowe wersje shaderów oferują instrukcje warunkowe. Możesz zdefiniować parametr typu bool i wstawić do kodu shadera warunek if. Jeśli warunek jest niespełniony, kod wewnątrz bloku (mówiąc w uproszczeniu) nie wykona się. Nadal jednak karta graficzna będzie musiała sprawdzać ten warunek dla każdego wierzchołka czy piksela, podczas gdy danego fragmentu kodu mogłoby w shaderze w ogóle nie być.

Bardziej zaawansowane podejście zakłada tworzenie osobnych wersji shaderów dla danej kombinacji ustawień. Można spróbować przygotować wszystkie możliwe shadery przed uruchomieniem gry, ale to trwałoby zbyt długo i zajęło za dużo miejsca na dysku. Każda zmiana w kodzie źródłowym HLSL pociągałaby za sobą konieczność przekompilowania setek czy tysięcy shaderów z różnymi kombinacjami ustawień. Dlatego dobrym wyjściem jest kompilować tylko wybrane shadery, dla takich ustawień dla jakich są aktualnie potrzebne. Zasadniczo są dwie metody składania shaderów [2]:

  • Metoda subtraktywna polega na tym, że mamy jeden długi kod HLSL, który zawiera wszystko co może być potrzebne i który poprzeplatany jest dyrektywami preprocesora do kompilacji warunkowej - #ifdef albo #if. Chcąc skompilować shader z odpowiednim zestawem ustawień definiujemy po prostu odpowiednie makra preprocesora, a dzięki nim do kompilacji wybrane zostaną odpowiednie fragmenty kodu. Właśnie tą metodę opisuje ten artykuł.
  • Metoda addytywna polega na tym, że kod shadera z danym zestawem ustawień jest składany z części. Te części są przygotowane jako fragmenty kodu HLSL (lub jakoś inaczej), które można ze sobą łączyć (wyjścia jednych z wejściami innych). Można sobie wyobrazić nawet graficzny edytor takich shaderów, w którym myszką stawiałoby się i łączyło elementy. Przykład implementacji tej metody znajdziesz w [3].

Przykładowy shader

Język HLSL wspiera preprocesor, podobny jak C i C++. W szczególności dostępne są dyrektywy: #define, #undef, #if, #ifdef, #ifndef, #elif, #else, #endif, #include, #error. Z ich użyciem można pisać kod, który będzie działał inaczej zależnie od zdefiniowanych makr.

Jako przykład rozpatrzymy w tym artykule jeden dość prosty shader. Będzie wyliczał oświetlenie per vertex - składową Diffuse i Specular oraz wykonywał teksturowanie. Zobacz rys. 1. Jego działanie będą mogły zmieniać dwa makra:

  • COLOR_MODE: 0 oznacza pomnożenie intensywności światła Diffuseprzez teksturę, 1 oznacza pokazanie samej tekstury, bez cieniowania, 2 oznacza pokazanie samej intensywności światła, bez tekstury.
  • SPECULAR: 0 oznacza brak odblasku, każda inna wartość (w szczególności 1) włącza odblask Specular.


Rys. 1. Wynik działania opisywanego shadera. Kolumna 1 - cieniowanie i teksturowanie. Kolumna 2 - samo teksturowanie. Kolumna 3 - samo cieniowanie. Wiersz 1 - brak odblasku. Wiersz 2 - dodany odblask.

A oto pełny kod tego shadera:

float4x4 g_WorldViewProj;
texture g_Texture;
float3 g_DirToLight; // Kierunek do źródła światła
float3 g_CameraPos; // Pozycja kamery we wsp. lokalnych modelu

sampler2D TextureSampler = sampler_state
{
  Texture = g_Texture;
  MinFilter = LINEAR; MagFilter = LINEAR; MipFilter = LINEAR;
};

struct VS_INPUT 
{
  float4 Pos : POSITION0;
  float3 Normal : NORMAL;
  float2 Tex : TEXCOORD0;
};

struct VS_OUTPUT 
{
  float4 Pos : POSITION0;
  #if (COLOR_MODE == 0 || COLOR_MODE == 2)
    float DiffuseIntensity : COLOR0;
  #endif
  #if (SPECULAR != 0)
    float SpecularIntensity : COLOR1;
  #endif
  #if (COLOR_MODE == 0 || COLOR_MODE == 1)
    float2 Tex : TEXCOORD0;
  #endif
};

void vs_main(VS_INPUT In, out VS_OUTPUT Out)
{
  Out.Pos = mul(In.Pos, g_WorldViewProj);

  #if (COLOR_MODE == 0 || COLOR_MODE == 2 || SPECULAR != 0)
    float N_dot_L = dot(In.Normal, g_DirToLight);
  #endif

  #if (COLOR_MODE == 0 || COLOR_MODE == 2)
    Out.DiffuseIntensity = N_dot_L;
  #endif

  #if (SPECULAR != 0)
    float3 DirToCam = normalize(g_CameraPos - In.Pos.xyz);
    float3 Half = normalize(g_DirToLight + g_DirToCam);
    float N_dot_H = dot(In.Normal, Half);
    Out.SpecularIntensity = pow(N_dot_H, 32) * N_dot_L;
  #endif

  #if (COLOR_MODE == 0 || COLOR_MODE == 1)
    Out.Tex = In.Tex;
  #endif
}

void ps_main(VS_OUTPUT In, out float4 Out : COLOR0)
{
  #if (COLOR_MODE == 0)
    Out = In.DiffuseIntensity * tex2D(TextureSampler, In.Tex);
  #elif (COLOR_MODE == 1)
    Out = tex2D(TextureSampler, In.Tex);
  #else // COLOR_MODE == 2
    Out = In.DiffuseIntensity;
  #endif

  #if (SPECULAR != 0)
    Out += In.SpecularIntensity;
  #endif
}

technique
{
  pass
  {
    VertexShader = compile vs_3_0 vs_main();
    PixelShader = compile ps_3_0 ps_main();
  }
}

Omawianie tego shadera zaczniemy od końca. Popatrz na kod pixel shadera (funkcja ps_main). Jej główną część stanowi łańcuszek warunków. Nietrudno zauważyć, że odpowiada on funkcji, jaką pełni makro COLOR_MODE. Jeśli jest równe 0, kolorem wynikowym będzie otrzymana z vertex shadera intensywność oświetlenia pomnożona przez kolor pobrany z tekstury. Jeśli jest równe 1, na wyjście trafi sam kolor tekstury. Jeśli jest równe 2, na wyjście trafi sama intensywność oświetlenia, samplowanie tekstury nie zostanie wykonane i sampler TextureSampler, a co za tym idzie tekstura g_Texture, w ogóle nie będzie użyta. Na koniec dodawany jest otrzymana z vertex shadera intensywność odblasku SpecularIntensity, o ile odblask jest włączony.

vertex shader (funkcja vs_main) zaczyna się przekształceniem pozycji wierzchołka przez połączoną macierz świata, widoku i rzutowania. To trzeba wykonać zawsze. Kończy się natomiast przepisaniem do interpolatora otrzymanych ze struktury wierzchołka współrzędnych tekstury. To jest robione tylko jeśli COLOR_MODE jest różne od 2, bo kiedy jest równe 2, wtedy pixel shader nie wykonuje teksturowania i nie ma sensu przekazywać do niego współrzędnych tekstury.

W środku kodu vertex shadera znajdują się obliczenia oświetlenia. Intensywność odblasku SpecularIntensity jest wyliczana tylko, jeśli odblask jest włączony (SPECULAR != 0). Wykorzystuje w tym celu wzór Blinna. Intensywność oświetlenia podstawowego - DiffuseIntensity - jest wyliczana o ile COLOR_MODE jest różne od 1. Jeśli jest równe 1, wtedy pixel shader nie wykorzystuje tej intensywności do ustalenia ostatecznego koloru piksela, dlatego nie ma sensu go wyliczać.

Ten najbardziej skomplikowany warunek, otaczający wyliczanie współczynnika N_dot_L (wg prawa Lamberta), został wydzielony jako osobny blok nie bez powodu. Otóż ten współczynnik jest potrzebny zarówno do obliczenia DiffuseIntensity, jak i SpecularIntensity. Tak więc jeśli choć jeden z ich warunków jest spełniony (pixel shader będzie potrzebował intensywności odblasku lub intensywności zwykłego cieniowania), wtedy ten współczynnik trzeba wyliczyć.

Jeszcze wyżej znajduje się definicja struktury przekazywanej z vertex shadera do pixel shadera. Jak widać, w niej również poszczególne pola są otoczone dyrektywami kompilacji warunkowej. Dlaczego? Nie musiałyby, gdyby vertex shader mógł je pozostawiać niezainicjalizowane. Jednak kompilator shaderów niestety pilnuje, żeby wszystkie zadeklarowane składowe tej struktury zostały przez vertex shader wypełnione (choćby zerami), a wypełnianie pola niepotrzebnego w danej sytuacji kosztuje dodatkową instrukcję shadera. Dlatego lepiej takich pól w ogóle w tej strukturze nie mieć.

Kompilowanie shadera

Makra COLOR_MODE i SPECULAR nie są po to, żeby gdzieś na początku kodu shadera wstawić dyrektywy #define COLOR_MODE 0 i #define SPECULAR 1. One są po to, żeby z kodu powstawały różne shadery bez najmniejszych zmian w źródle. Dlatego wartości tych makr trzeba wprowadzić "z zewnątrz".

Jeśli kompilujesz shader off-line używac kompilatora fxc, możesz to zrobić podając parametry /D, np.:

fxc
  /T fx_2_0
  /Fo TestShader.fxo
  /D COLOR_MODE=0
  /D SPECULAR=1
  TestShader.fx

Jeśli natomiast chcesz kompilować shader z poziomu kodu C++ za pomocą funkcji Direct3D (a tego będziemy używali w dalszej części artykułu), musisz nauczyć się prawidłowo wypełniać parametr pDefines, zawierający makra do kompilacji (nie używając makr, podawało się tam po prostu NULL). Najpierw jednak pokażę mały przegląd funkcji D3D związanych z kompilowaniem i wczytywaniem shaderów:

  • D3DXCompileShader - przyjmuje wskaźnik z kodem źródłowym HLSL, zwraca ID3DXBufferze skompilowanym shaderem.
  • D3DXCompileShaderFromFile - wczytuje z pliku kod źródłowy HLSL, zwraca ID3DXBufferze skompilowanym shaderem.
  • D3DXCompileShaderFromResource wczytuje z zasobu kod źródłowy HLSL, zwraca ID3DXBuffer ze skompilowanym shaderem.
  • D3DXCreateEffect, D3DXCreateEffectEx - przyjmuje wskaźnik z kodem źródłowym HLSL lub skompilowanym shaderem, zwraca ID3DXEffectz utworzonym efektem.
  • D3DXCreateEffectFromFile, D3DXCreateEffectFromFileEx - wczytuje z pliku kod źródłowy HLSL lub skompilowany shader, zwraca ID3DXEffectz utworzony efektem.
  • D3DXCreateEffectFromResource, D3DXCreateEffectFromResourceEx - wczytuje z zasobu kod źródłowy HLSL lub skompilowany shader, zwraca ID3DXEffect z utworzony efektem.
  • D3DXCreateEffectCompiler - przyjmuje wskaźnik z kodem źródłowym HLSL, zwraca utworzony ID3DXEffectCompiler.
  • D3DXCreateEffectCompilerFromFile - wczytuje z pliku kod źródłowy HLSL, zwraca utworzony ID3DXEffectCompiler.
  • D3DXCreateEffectCompilerFromResource - wczytuje z zasobu kod źródłowy HLSL, zwraca utworzony ID3DXEffectCompiler.

Wracając do makr, sprawa jest o tyle trudna, że tego zagadnienia dokumentacja DirectX SDK nie wyjaśnia dostatecznie dokładnie. Jako parametr pDefines podać trzeba tablicę struktur D3DXMACRO, zdefiniowanych tak:

// Źródło: DirectX SDK
typedef struct D3DXMACRO {
    LPCSTR Name;
    LPCSTR Definition;
} D3DXMACRO, *LPD3DXMACRO;

Każdy element zawiera więc wskaźnik na łańcuch z nazwą i wartością makra. Co ciekawe, nie ma nigdzie w wyżej wspomnianych funkcjach miejsca na podanie liczby makr. Zamiast tego, koniec tablicy musi wskazywać dodatkowy element, w którym obydwa wskaźniki są ustawione na NULL (tego właśnie dokumentacja DX SDK nie mówi). Oto przykładowy kod układający tablicę makr i kompilujący z ich użyciem shader, z pliku źródłowego HLSL do efektu ID3DXEffect:

// Liczba makr
const unsigned MacroCount = 2;

// Nazwy makr
const char * MacroNames[MacroCount] = {
  "COLOR_MODE",
  "SPECULAR",
};

// Wartości makr
std::vector<std::string> MacroValues(MacroCount);
MacroValues[0] = "0"; // COLOR_MODE = 0
MacroValues[1] = "1"; // SPECULAR = 1

// Tu powstaje tablica makr dla D3DX
std::vector<D3DXMACRO> D3dMacros;
D3dMacros.resize(MacroCount + 1);
unsigned mi;
for (mi = 0; mi < MacroCount; mi++)
{
  D3dMacros[mi].Name = MacroNames[mi];
  D3dMacros[mi].Definition = MacroValues[mi].c_str();
}
// Ostatni element tablicy to wskaźniki puste - wskazuje koniec
D3dMacros[mi].Name = NULL;
D3dMacros[mi].Definition = NULL;

// Kompilacja shadera
ID3DXEffect *Effect = NULL;
ID3DXBuffer *CompilationErrors = NULL;
HRESULT hr = D3DXCreateEffectFromFile(
  g_Dev, // pDevice
  "TestShader.fx", // pSrcFile
  &D3dMacros[0], // pDefines
  NULL, // pInclude
  0, // Flags
  NULL, // pPool
  &Effect, // ppEffect
  &CompilationErrors); // ppCompilationErrors

// Obsługa błędów
if (FAILED(hr))
{
  std::string ErrMsg;
  if (CompilationErrors != NULL &&
    CompilationErrors->GetBufferSize() > 0)
  {
    ErrMsg.assign(
      (const char*)CompilationErrors->GetBufferPointer(),
      CompilationErrors->GetBufferSize());
  }
  // Jakaś Twoja obsługa błędów
  HandleError(ErrMsg);
}
if (CompilationErrors != NULL)
  CompilationErrors->Release();

// Używaj Effect...

Effect->Release();

Multishader

Jako punkt kulminacyjny tego artykułu proponuję większy kawałek kodu. Możesz w tym miejscu przerwać czytanie, ale jeśli Ci się chce, możesz przeanalizować moją implementację klasy do kompilowania shaderów z różnymi ustawieniami. Będzie to klasa, którą nazwałem Multishader. Reprezentuje ona shader, a właściwie całą kolekcję shaderów kompilowanych ze wspólnego pliku z kodem źródłowym HLSL, ale z różnymi wartościami makr preprocesora. Przechowuje w pamięci wczytane shadery i daje do nich dostęp. Nowe shadery kompiluje na żądanie, przy pierwszym użyciu. Skompilowane shadery zapisuje w plikach binarnych, żeby w następnym uruchomieniu programu tylko je wczytać, a nie znów kompilować ze źródła.

Kodu o którym mowa nie zacytuję tu w tekście, bo jest dość długi. Znajdziesz go w załączonym pliku: Multishader.cpp. To nie jest moduł C++ gotowy do kompilacji - jest tam kilka miejsc do uzupełnienia dla Ciebie, m.in. wczytywanie i zapisywanie danych binarnych z/do pliku (ja używam do tego celu funkcji z mojej biblioteki). Jednak jest w miarę kompletny i (mam nadzieję) poprawny. Jeśli piszesz poważniejszy projekt, warto zintegrować tą klasę z Twoim mechanizmem zasobów - taki multishader może być zasobem, tak jak tekstura czy siatka trójkątów.

Żeby utworzyć multishader, trzeba podać:

  • SourceFileName- nazwa pliku z kodem źródłowym HLSL,
  • CacheFileNameMask - maska dla nazw plików ze skompilowanymi, shaderami, która musi zawierać miejsce na numer (Hash) oznaczone jako %X,
  • MacroCount- liczba makr preprocesora,
  • MacroNames- tablica z nazwami makr,
  • MacroBits- numery bitów makr (patrz niżej),
  • ParamCount- liczba parametrów efektu,
  • ParamNames - nazwy parametrów efektu.

Pomysł polega tutaj na tym, że każdy shader powstający z konkretnej kombinacji wartości makr ma przypisany swój własny 32-bitowy numer, który nazywam w kodzie "Hash". Jest on składany z wartości makr za pomocą operacji bitowych. Jednak żeby wiedzieć, od którego bitu zacząć wstawianie wartości danego makra, trzeba mieć informacje, ilu bitów potrzeba na zapisanie danego makra, a więc jakie są jego dopuszczalne wartości. W naszym przykładzie pierwsze makro - COLOR_MODE - przyjmuje trzy różne wartości (0, 1, 2), więc na jego zapisanie potrzeba dwóch bitów. Drugie makro - SPECULAR, może wynosić tylko 0 lub 1, więc na jego zapisanie wystarczy jeden bit. Tak więc numer bitu dla pierwszego makra to 0, dla drugiego 0 + 2 = 2, a dla trzeciego, gdybyśmy takie mieli, wynosiłby 2 + 1 = 3. Zobacz zmienną MacroBits w poniższym kodzie.

Multishader interesują również nazwy parametrów posiadanych przez efekt, ponieważ dla każdego skompilowanego shadera pobiera i udostępnia ich uchwyty. Dzięki temu nie trzeba za każdym razem odwoływać się do nich przez nazwy, co przyspiesza renderowanie.

Oto przykład tworzenia i użycia mojego multishadera do naszego omawianego wyżej kodu HLSL:

////// STA£E
const char *MacroNames[] = {
  "COLOR_MODE",
  "SPECULAR",
};
const unsigned MacroBits[] = {
  0, 2
};
const char *ParamNames[] = {
  "g_WorldViewProj",
  "g_Texture",
  "g_DirToLight",
  "g_CameraPos",
};
// Nadajemy parametrom nazwy dla czytelności, żeby nie używać numerów
enum PARAMS
{
  Param_WorldViewProj = 0,
  Param_Texture,
  Param_DirToLight,
  Param_CameraPos,
};

////// UTWORZENIE
Multishader *MS = new Multishader(
  "TestShader.fx", // SourceFileName
  "TestShader_%X.fxo", // CacheFileNameMask
  2, // MacroCount
  MacroNames,
  MacroBits,
  4, // ParamCount
  ParamNames);

////// UŻYCIE
// Ustal potrzebne wartości makr
unsigned MacroValues[] = {
  0, // COLOR_MODE = 0
  1, // SPECULAR = 1
};
// Pobierz/wczytaj/skompiluj shader
const Multishader::SHADER_INFO & ShaderInfo = MS->GetShader(MacroValues);
// Użyj efektu
ShaderInfo.Effect->SetMatrix(ShaderInfo.Params[Param_WorldViewProj], ...);
ShaderInfo.Effect->SetTexture(ShaderInfo.Params[Param_Texture], ...);
ShaderInfo.Effect->SetVector(ShaderInfo.Params[Param_DirToLight], ...);
ShaderInfo.Effect->SetVector(ShaderInfo.Params[Param_CameraPos], ...);
UINT PassCount;
ShaderInfo.Effect->Begin(&PassCount, 0);
ShaderInfo.Effect->BeginPass(0);
// Renderuj...
ShaderInfo.Effect->EndPass();
ShaderInfo.Effect->End();

Serce algorytmu stanowi metoda Multishader::GetShader. Możesz otworzyć dołączony do artykułu plik i przejrzeć jej kod. W skrócie możnaby w pseudokodzie zapisać jej działanie tak:

Hash = Zbuduj numer shadera z otrzymanych wartości makr Macros
Pobierz shader o numerze Hash z tablicy shaderów w pamięci
Jeśli shadera o numerze Hash nie ma w pamięci:
{
  CacheFileName = nazwa pliku skompilowanego shadera o numerze Hash
  Jeśli plik CacheFileName istnieje:
  {
    Pobierz datę modyfikacji kodu źródłowego shadera HLSL
    Pobierz datę modyfikacji skompilowanego shadera CacheFileName
    Jeśli data modyfikacji kodu < data modyfikacji CacheFileName:
    {
      Wczytaj skompilowany shader z pliku CacheFileName
    }
  }
  Jeśli plik CacheFileName nie istnieje lub jego data modyfikacji niewiększa
    od daty modyfikacji kodu źródłowego:
  {
    Wczytaj kod źródłowy, jeśli nie był wcześniej wczytany
    Sformułuj tablicę z nazwami i wartościami makr dla D3DX
    Skompiluj shader z kodu źródłowego do bufora w pamięci z użyciem makr
    Zapisz bufor ze skompilowanym shaderem do pliku CacheFileName
    Utwórz z bufora w pamięci shader
  }
}
Zwróć shader.

Tak więc metoda GetShader próbuje najpierw znaleźć shader o danych wartościach makr (danym numerze Hash) w pamięci. Jeśli już był kiedyś wczytany, znajduje go w tablicy hashującej i zwraca. Jeśli nie ma go w pamięci, próbuje wczytać go z pliku tymczasowego cache. Taki plik jest tworzony przy pierwszej kompilacji danego shadera i zawiera jego wersję skompilowaną. Nazwa pliku zawiera numer Hash danego shadera. Jeśli plik cache istnieje i jego data modyfikacji jest nie starsza, niż data modyfikacji pliku z kodem źródłowym HLSL (innymi słowy, jeśli kod źródłowy nie zmienił się od czasu jego kompilacji), shader zostaje wczytany z tego pliku, od razu w postaci skompilowanej. Dopiero jeśli nawet to się nie uda, wczytany zostaje kod źródłowy (tylko za pierwszym razem, potem już jest w pamięci) i shader jest kompilowany z niego z użyciem podanych wartości makr.

Podsumowanie

Subtraktywna metoda budowania shaderów jest stosunkowo prosta do zrozumienia i zaimplementowania (w porównaniu z metodą addytywną). Stanowi dobre rozwiązanie problemu opanowania rosnącej w tempie wykładniczym liczby możliwych kombinacji ustawień, z którymi powinny działać shadery w zaawansowanej aplikacji 3D, np. w silniku graficznym. Posiada też jednak wady. Kiedy pisałem swój silnik, jego główny shader urósł do długości 907 linii i stał się bardzo trudny w debugowaniu i rozbudowie.

Załącznik

Pobierz kod: Multishader.cpp.

Bibliografia

  1. Pedro V. Sander, A Fixed Function Shader in HLSL, 2003, http://www2.ati.com/misc/samples/dx9/FixedFuncShader.pdf.
  2. Dominic Filion, Recombinant Shaders, w: Kim Pallister, Game Programming Gems 5, Charles River Media, 2005.
  3. Shawn Hargreaves, Generating Shaders From HLSL Fragments, http://www.talula.demon.co.uk/hlsl_fragments/hlsl_fragments.html.
Adam Sawicki
http://asawicki.info
22.05.2008

Tekst dodał:
Adam Sawicki
22.05.2008 19:25

Ostatnia edycja:
Adam Sawicki
05.05.2012 15:11

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#2 edytuj (poprz.) 05.05.2012 15:11 Adam Sawicki 25.07 KB (+336)
#1 edytuj (bież.) 22.05.2008 19:25 Adam Sawicki 24.75 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • niuteq (@niuteq) 22 maja 2008 19:52
    slow down, man! :p fajnie, ze sie tak "produkujesz" dla nas, Adamie. "Na wieki dozgonnim Ci wdziecznosc". ++ rzecz jasna.

    pozdrawiam niuteq
  • kapitan_hak (@kapitanhak) 26 grudnia 2008 21:51
    "Dla wszystkich zających C++(...)"

    Co prawda zającem nie jestem, ale artykuł się przydał ;).
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)