Ha! Jestem z siebie dumny. Po 10 dniach klepania kodu mój prototypowy kompilator shaderów działa i ma się dobrze.
Jak pisałem w poprzednich odcinkach, jest kilka koncepcji co do shaderów. Okazało się, że #ifdefowe shadery substraktywne nie są na tyle elastyczne, by podołać rosnącemu skomplikowaniu silnika. Napisałem więc kompilator który buduje PS i VS na podstawie schematu i parametrów. Wygląda to mniej więcej tak: kod shadera.
Język zawiera kilka elementów.
- zmienne - jest ich kilka typów. Generalnie można podzielić je na następujące typy:
- zmienne stream czyli pochodzące bezpośrednio z danych o modelu (jak pozycja wierzchołka czy normalna w model space)
- zmienne varying obliczane w VS i przekazywane do PS (jak np. pozycja w world space; dzielą się na zmienne globalne dla obiektu i zmienne per-light jak np. pozycja w shadowmap space
- zmienne uniform przekazywane jako parametry do PS/VS, również dzielą się na parametry per-object (lub też per-material) oraz per-light
ze zmiennymi jest trochę zabawy:
- silnik musi być kompatybliny z kompilatorem i wiedzieć jakie dokładnie parametry będą potrzebne dla danego shadera, jaki będzie ich typ, itd.
- pojawia się problem ograniczonej ilości interpolatorów między VS a PS (konkretnie mamy do dyspozycji 8 texcoordów). Pozycja i texcoord to dwa. Normalna i tangent? Cztery. Jeżeli dla światła chcielibyśmy przekazać jego pozycję w tangent space to konieczne będą następne 2. Współrzędne dla shadowmapy? Razem mamy już 7 argumentów. Jeżeli chcemy mieć 1 pass, to obliczenia trzeba przenieść niestety do world space. Wtedy możemy mieć do 4 cieni na obiekt.
- parametry - przekazujemy je do kompilatora również na kilku poziomach (scene, material, light)
- funckje - serce języka
Zobaczmy przykładowy kod:
[Require(Material.ColorSampler, TextureCoord, Material.AlphaTestValue)]
Null AlphaTest[Material.AlphaTestMode = TRUE]
{
!let %texture Texture
if (%texture.a < Material.AlphaTestValue)
{
discard;
}
}
Funkcja ta zgłasza zapotrzebowanie na sampler tekstury, koordynaty dla niej i wartość do testu alfa. Sampler i wartość dla testu są uniform, dlatego zostaną wystawione jako zewnętrzne parametry (i przekazane przez silnik), natomiast TextureCoord jest varying per-object, dlatego zostanie uruchomiony odpowiedni podprogram VS:
[Requires(Stream.Texcoord)]
[Provides(TextureCoord)]
{
float2 %coord = Stream.Texcoord;
$ %coord
}
OK, dalej mamy instrukcję !let. Jest to statyczne przypisane wartości funkcji. Potęga tego mechanizmu polega na wybraniu odpowiedniej wartości funkcji na podstawie parametrów. Texture na przykład, może być samplowane w sposób prosty lub używając splattingu. Instruckja !let wybierze odpowiednią, i co więcej - jeżeli gdzieś będziemy mieli np. taką instrukcję:
!let %ambient Ambient
!color %color
!add %color %ambient
to w przypadku gdy Ambient jest statycznym nullem, żadne dodawanie nie zostanie wykonane. Szkoda w końcu tracić czas na dodawanie niczego ;).
Zobaczmy jak wygląda przykładowy wynik działania kompilatora (przykładowy, większość "prawdziwych" funkcji graficznych nie jest zaimplementowana albo jest uproszczona) dla następujących parametrów:
Compile{Fragment=fp40,Vertex=vp40}Technique{DRAW}Lights{2}Object{}Material{AlphaTestMode=TRUE}Light0{}Light1{LightType=POINT,ShadowType=SHADOWMAPPING}
Co jeszcze można dodać? Język jest supersetem Cg, więc w razie potrzeby można używać dowolnych features z Cg. Jedynie deklarowanie zmiennych jest nieco opakowane (float3 %zmienna) w celach bezpieczeństwa (unikanie 2 takich samych nazw zmiennych), ale jeżeli mamy pewność że nasza zmienna nie pogryzie się z żadną inną do funkcje można pisać w "czystym" Cg. I warto dodać że całość mieści się w wielu plikach, więc można to dosyć logicznie zorganizować (osobno funkcje/zmienne związanie z shadowmapami, oddzielnie liczenie attenuation, itd.)
Aha, jeszcze słowo o założeniach. Mam zamiar użyć techniki którą roboczo nazywam At-once-rendering, czyli renderowanie geometrii w 1 przebiegu (+ pomocniczy przebieg Z-fill). Wydajnośc powinna być wyższa niż w przypadku deferred (fillrate!), choć koszta przełączania shaderów mogą dać o sobie znać. Okaże się w praktyce.
Nie wiem jaki to jest rodzaj shaderów (zapewne nadal substraktywny), ale mam nadzieję, że da radę. Zabieram się za integrację z silnikiem. :)