Zawód: murarz - Rafał 'revo' Kozik

Wstęp

Artykuł przedstawia prostą alternatywę dla użycia powtarzalnych tekstur do rysowania kafelków, cegieł, paneli itp. Materiał w nim przedstawiony jest średnio trudny, jednak wymaga podstawowej znajomości pixel shadera.

Typowe rozwiązanie

Jeżeli chcesz narysować mur z teksturą z cegieł, to pewnie użyjesz tekstury podobnej do tej poniżej i użyjesz koordynatów spoza zakresu [0,1], aby ją powielić:


Rys 1. Tekstura dla muru

Niestety ma ono jedną bardzo dużą wadę. Bardzo łatwo zauważyć powtarzający się wzorzec, a przez to wygląda to mało realistycznie:


Rys 2. Tekstura powtórzona czterokrotnie na wysokości i szerokości
Pomysł

Zamiast przedstawionej na początku tekstury użyjemy takiej tekstury i będziemy starali się wybrać cegłówkę losowo:


Rys 3. Nasza nowa tekstura

Jeżeli wzór nie będzie się za bardzo powtarzał, to w teorii powinno wszystko wyglądać znacznie lepiej niż w pierwotnym rozwiązaniu. Prezentowany tu kod był pisany w programie RenderMonkey, z niego też pochodzą zrzuty ekranu.

Generowanie liczb losowych

Chwilowo będziemy bawić się sześcianem, którego ścianki mają koordynaty tekstury od [0,0] do [32,32]. Na początku wypadałoby obliczyć wiersz i kolumnę:

float row = floor(Input.Texcoord.y) + 1;
float col = floor(Input.Texcoord.x) + 1;

Teraz będziemy chcieli dla każdego wiersza i kolumny dostać jakąś w miarę losową, całkowitą, wartość od 0 do 3. Wyniki wyświetlimy używając tej wartości podzielonej przez 4. Na początku próbowałem mnożyć wiersz i kolumnę przez liczby pierwsze:

float n = fmod(7 * col + 11 * row, 4);

Rezultat nie jest jednak zbyt ciekawy:


Rys 4. Pseudolosowość, podejście pierwsze

Metodą prób i błędów udało mi się jednak dojść do całkiem ciekawych rezultatów, które zdecydowanie wystarczają do prezentowanego tu zagadnienia:

float a = 1.37 * row * sin(col);
float b = 0.42 * col * sin(row);
float r = frac(a + b);
float n = floor(r * 4);

Rys 5. Wynik funkcji otrzymanej metodą prób i błędów

Może komuś uda się dojść do jakiejś prostszej funkcji, która tu wystarczy. Można też użyć dodatkowej tekstury z szumem i z niej próbkować odpowiednie wartości.

Budujemy mur

Teraz będziemy bawić się sześcianem, który ma koordynaty tekstury z zakresu od [0,0] do [4,8]. Mamy już obliczone, które cegły będą miały zostać użyte w którym miejscu, teraz wystarczy odpowiednio wybrać koordynaty:

Input.Texcoord.x = frac(Input.Texcoord.x);
Input.Texcoord.y = 0.25f * n + frac(Input.Texcoord.y) / 4;

Rys 6. Cegły po raz pierwszy

Nie jest źle, udało nam się już wyświetlić cegły, jednak nie jest to do końca to co byśmy chcieli:

Rozsunięcie cegieł

Ta część jest prosta - po prostu chcemy, aby nieparzyste wiersze były przesunięte o pół cegły. Wystarczy zatem przed obliczeniem kolumny dodać:

Input.Texcoord.x += fmod(row, 2) / 2;

Rys 7. Cegły po raz drugi
Naprawa mip-map

Jak wspomniałem wcześniej, błędy użycia mip-map powstały przez to, że koordynaty próbkowanej tekstury zmieniały się gwałtowanie na łączeniach. Przez to ich zmiana względem współrzędnych ekranowych była na tyle duża, że próbkowana była inna (mniejsza) mip-mapa niż byśmy tego chcieli. Musimy zatem sprawić, żeby próbkowanie odbywało się w inny sposób. W poziomie tekstura jest próbkowana z taką częstością jak podano w parametrze shadera. W pionie natomiast cztery razy wolniej, ponieważ używamy tylko jednej z czterech cegieł. Zatem możemy udać taką zmianę koordynatów tekstury i podać obliczone ddx i ddy do samplera:

float2 wallSpaceTexcoord = Input.Texcoord;
wallSpaceTexcoord.y /= 4;
return tex2D(baseMap, Input.Texcoord, ddx(wallSpaceTexcoord), ddy(wallSpaceTexcoord));

Rys 8. Cegły po raz trzeci, sprzedane!

Aby upewnić się, że pobierane są takie mip-mapy jak chcemy i że jedynymi miejscami gdzie nastąpiła znaczna zmiana są łączenia cegieł, możemy zerknąć na różnicę:


Rys 9. Różnica obrazów bez i z korekcją mip-map
Po jasnej stronie kodu

Poniżej znajduje się cały kod pixel shadera z komentarzami:

sampler2D baseMap;

struct PS_INPUT 
{
   float2 Texcoord : TEXCOORD0;
};

float4 ps_main( PS_INPUT Input ) : COLOR0
{
   // tak zmieniają się koordynaty w przestrzenii naszej ściany
   float2 wallSpaceTexcoord = Input.Texcoord;
   // tylko 1/4 wysokości tekstury cegieł jest próbkowana na jedną cegłę
   wallSpaceTexcoord.y /= 4;
   
   float row = floor(Input.Texcoord.y) + 1;
   
   // w wierszach parzystych przesuwamy się o pół cegły
   Input.Texcoord.x += fmod(row, 2) / 2;
   
   float col = floor(Input.Texcoord.x) + 1;
   
   // próbujemy wygenerować pseudolosową liczbę
   float a = 1.37 * row * sin(col);
   float b = 0.42 * col * sin(row);
   float r = frac(a + b);
   
   // wybieramy numer cegły
   float n = floor(r * 4);
   
   Input.Texcoord.x = frac(Input.Texcoord.x);
   Input.Texcoord.y = 0.25f * n + frac(Input.Texcoord.y) / 4;
   
   return tex2D(baseMap, Input.Texcoord, ddx(wallSpaceTexcoord), ddy(wallSpaceTexcoord));
}
Podsumowanie

Okazało się, że w ten sposób możemy osiągnąć bardzo dobre rezultaty, a wizualne artefakty są pomijalne. Należy pamiętać jednak, żeby wszystkie składane elementy do siebie idealnie pasowały - w przeciwnym wypadku na łączeniach (zwłaszcza przy przybliżeniu) widać będzie color bleeding. Możemy zastosować tę technikę dla cegieł, kafli, paneli, parkietu, kostki brukowej itp. Dzięki temu nie powinno to być aż tak powtarzalne i monotonne. W ten sposób można też próbkować mapy normalnych itp., aby dodać trochę detali. Niestety przez użycie ddx i ddy wymagany jest pixel shader w wersji conajmniej 2.0a.


Rys 10. The Wall