This is the Polish translation of Advanced-OpenGL/Cubemaps article of learnopengl.com tutorial series.
Od jakiegoś czasu używamy tekstur 2D, ale jest jeszcze więcej typów tekstur, których jeszcze nie odkryliśmy. W tym samouczku omówimy typ tekstury, który jest w rzeczywistości kombinacją wielu tekstur zmapowanych w jedną strukturę:
Cube mapa to zasadniczo tekstura zawierająca 6 pojedynczych tekstur 2D, z których każda tworzy jedną ściankę sześcianu: oteksturowaną kostkę od wewnątrz. Możesz się zastanawiać, jaki jest sens takiego sześcianu? Po co zawracać sobie głowę łączeniem 6 pojedynczych tekstur w jeden obiekt, zamiast używania 6 pojedynczych tekstur? Cube mapy mają przydatną właściwość, że mogą być one indeksowane/próbkowane za pomocą wektora kierunkowego. Wyobraźmy sobie, że mamy sześcian jednostkowy 1x1x1, gdzie początek wektora kierunkowego znajduje się w jego środku. Próbkowanie wartości tekstury z cube mapy za pomocą pomarańczowego wektora kierunku wygląda mniej więcej tak:
Wielkość wektora kierunku nie ma znaczenia. Dopóki dostarczany jest kierunek, OpenGL wyszukuje odpowiadające teksele, w które trafiamy na bazie dostarczonego kierunku i zwraca poprawnie spróbkowaną wartość tekstury.
Jeśli wyobrazimy sobie, że mamy kształt sześcianu, do którego przypinamy taką mapę, to wektor kierunkowy do próbkowania cube mapy byłby podobny do (interpolowanej) pozycji wierzchołka sześcianu. W ten sposób możemy próbkować cube mapę za pomocą rzeczywistych wektorów położenia kostki, o ile sześcian jest wyśrodkowany względem swojego punktu początkowego. Możemy wtedy pobrać współrzędne tekstury wszystkich wierzchołków jako pozycje wierzchołków sześcianu. Wynikiem jest współrzędna tekstury, która uzyskuje dostęp do odpowiedniej tekstury
Tworzenie cube mapy
Cube mapa jest teksturą podobną do każdej innej tekstury, więc aby ją utworzyć, generujemy teksturę i wiążemy ją z właściwym typem docelowym tekstury, zanim wykonamy dalsze operacje na teksturze. Tym razem powiążemy z typem GL_TEXTURE_CUBE_MAP:
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
Ponieważ cube mapa składa się z 6 tekstur, po jednej dla każdej ścianki, musimy wywołać sześć razy
Ponieważ mamy 6 ścianek, OpenGL zapewnia nam 6 specjalnych typów docelowych tekstur specjalnie do powiązania danej tekstury z odpowiednią ścianką cube mapy:
Typ docelowy tekstury | Orientacja |
---|---|
GL_TEXTURE_CUBE_MAP_POSITIVE_X | Prawo |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | Lewo |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y | Góra |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | Dół |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | Tył |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | Przód |
Podobnie jak wiele z typów wyliczeniowych OpenGL, ich wartości
int width, height, nrChannels;
unsigned char *data;
for(GLuint i = 0; i < textures_faces.size(); i++)
{
data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
}
Tutaj mamy
Ponieważ cube mapa jest teksturą jak każdą inna tekstura, określimy również jej metody zawijania i filtrowania:
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
Nie bój się wartości GL_TEXTURE_WRAP_R, to po prostu ustawia metodę zawijania dla współrzędnej R
tekstury, która odpowiada trzeciemu wymiarowi tekstury (jak z
dla pozycji). Ustawiamy metodę zawijania na GL_CLAMP_TO_EDGE, ponieważ współrzędne tekstur, które znajdą się pomiędzy dwiema ściankami mogą nie dotyczyć dobrej ścianki (ze względu na pewne ograniczenia sprzętowe), więc używając GL_CLAMP_TO_EDGE OpenGL zawsze zwraca ich wartości znajdujące się na krawędzi, gdy próbkujemy pomiędzy ściankami.
Następnie przed rysowaniem obiektów, które będą korzystać z cube mapy, aktywujemy odpowiednią jednostkę tekstur i wiążemy ją przed renderowaniem. Niewiele jest różnic w porównaniu do normalnych tekstur 2D.
W Fragment Shader musimy również użyć samplera typu samplerCube
, który próbujemy z wykorzystaniem funkcji vec3
zamiast vec2
. Przykład Fragment Shadera przy użyciu cube mapy wygląda następująco:
in vec3 textureDir; // wektor kierunkowy reprezentujący współrzędną tekstury 3D
uniform samplerCube cubemap; // sampler tekstury cube map
void main()
{
FragColor = texture(cubemap, textureDir);
}
To jest świetne, ale po co zawracać sobie tym głowę? Cóż, tak się składa, że istnieje sporo interesujących technik, które są o wiele łatwiejsze do wdrożenia z cube mapą. Jedną z tych technik jest tworzenie
Skybox
Skybox to (duży) sześcian, który obejmuje całą scenę i zawiera 6 tekstur otaczającego środowiska, dając graczowi złudzenie, że środowisko, w którym się znajduje jest w rzeczywistości o wiele większe, niż jest w rzeczywistości. Niektóre przykłady skyboxów używanych w grach wideo to obrazy gór, chmur lub gwiaździstego nocnego nieba. Przykład skyboxu, wykorzystującego obrazy rozgwieżdżonego nieba, można zobaczyć na poniższym zrzucie ekranu z trzeciej gry z serii The Elder Scrolls:
Najprawdopodobniej odgadłeś, że skyboxy takie jak ten pasują cube map idealnie: mamy sześcian, który ma 6 ścianek i musi mieć teksturę na każdej z nich. Na poprzednim obrazie użyli kilku zdjęć nocnego nieba, aby dać iluzję graczowi, że jest w jakimś dużym wszechświecie, podczas gdy on rzeczywiście znajduje się w maleńkim pudełeczku.
Zwykle jest wystarczająco dużo zasobów online, w których można znaleźć takie skyboxy. Ta strona internetowa ma na przykład wiele skyboxów. Te obrazy zazwyczaj mają następujący wzór:
Jeśli złożysz te 6 ścianek w kostkę, otrzymasz całkowicie oteksturowany sześcian symulujący duży krajobraz. Niektóre zasoby udostępniają skyboxy w takim formacie, w którym to przypadku trzeba ręcznie wyodrębnić 6 obrazów, ale w większości przypadków są one dostarczane jako 6 pojedynczych obrazów.
Ten szczególnie wysokiej jakości skybox jest tym, czego użyjemy dla naszej sceny i możemy go pobrać tutaj.
Ładowanie skybox’a
Ponieważ skybox jest tylko cube mapą, ładowanie skyboxa nie różni się zbytnio od tego, co widzieliśmy już wcześniej. Aby załadować skybox, użyjemy następującej funkcji, która akceptuje
unsigned int loadCubemap(vector<std::string> faces)
{
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
int width, height, nrChannels;
for (unsigned int i = 0; i < faces.size(); i++)
{
unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
stbi_image_free(data);
}
else
{
std::cout << "Cubemap texture failed to load at path: " << faces[i] << std::endl;
stbi_image_free(data);
}
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return textureID;
}
Funkcja sama w sobie nie powinna być zbyt zaskakująca. Jest to w zasadzie cały kod ładowania cube mapy, który widzieliśmy w poprzedniej sekcji, ale połączony w jedną, pomocniczą funkcję.
Następnie, zanim wywołasz tę funkcję, zdefiniujemy odpowiednie ścieżki tekstur w wektorze w kolejności określonej przez typ wyliczeniowy cube mapy:
vector<std::string> faces;
{
"right.jpg",
"left.jpg",
"top.jpg",
"bottom.jpg",
"front.jpg",
"back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);
Teraz, gdy już załadowaliśmy skybox jako cube mapę z cubemapTexture jako jego id. Możemy teraz powiązać go z kostką, aby w końcu zastąpić brzydki, jasny kolor, którego używamy jako tło.
Wyświetlanie skybox’a
Ponieważ skybox jest rysowany na sześcianie, potrzebujemy kolejnego VAO, VBO i nowego zestawu wierzchołków, jak dla każdego innego obiektu. Możesz uzyskać jego dane w wierzchołków tutaj.
Cube mapę, która jest używana do oteksturowania kostki można spróbkować, używając pozycji kostki jako współrzędnych tekstury. Gdy sześcian jest umieszczony w punkcie (0,0,0), każdy z jego wektorów pozycji jest również wektorem kierunkowym zaczepionym w tym punkcie. Ten wektor kierunkowy jest dokładnie tym, czego potrzebujemy, aby uzyskać odpowiednią wartość tekstury w tej konkretnej pozycji sześcianu. Z tego powodu musimy dostarczać jedynie wektory położenia i nie potrzebujemy współrzędnych tekstury.
Do narysowania skyboxa potrzebujemy nowego zestawu shaderów, które nie są zbyt skomplikowane. Ponieważ mamy tylko jeden atrybut wierzchołka. Vertex Shader jest dość prosty:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
Zauważ, że interesującą częścią Vertex Shadera jest ustawienie wektorów pozycji wejściowych jako wyjściowych współrzędnych tekstury dla Fragment Shadera. Fragment Shader pobiera je jako dane wejściowe, aby spróbkować samplerCube
:
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
Fragment Shader jest względnie prosty. Pobieramy wektory pozycji wierzchołka jako wektor kierunku i wykorzystujemy go do próbkowania wartości tekstury z cube mapy.
Renderowanie skyboxa jest teraz łatwe, gdy mamy już teksturę cube mapy, po prostu wiążemy teksturę cube mapy z kontekstem, a sampler skybox jest automatycznie wypełniany przez cube mapę skyboxa. Aby narysować skybox, narysujemy go jako pierwszy obiekt na scenie i wyłączymy pisanie do bufora głębokości. W ten sposób skybox będzie zawsze rysowany na tle wszystkich innych obiektów.
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... ustawić macierze widoku i projekcji
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... narysuj resztę sceny
Jeśli uruchomisz teraz aplikację, zobaczysz, że coś jest nie tak. Chcemy, aby skybox był wycentrowany wokół gracza, aby niezależnie od tego, jak daleko się poruszy, to gracz nigdy nie zbliży się do skyboxa, dając wrażenie, że otoczenie jest bardzo duże. Obecna macierz widoku przekształca wszystkie pozycje w skyboxie, obracając je, skalując i przesuwając, więc jeśli gracz się ruszy, cube mapa również się poruszy! Chcemy usunąć część translacyjną macierzy widoku, aby ruch nie wpłynął na wektory pozycji skyboxa.
Być może pamiętasz z tutoriala podstawowy oświetlenia, że możemy usunąć część translacyjną macierzy transformacji, biorąc lewą górną macierz 3x3 macierzy 4x4, co skutecznie usuwa komponent translacji. Możemy to osiągnąć, po prostu przekształcając macierz widoku na macierz 3x3 (usuwając translację) i przekształcając ją z powrotem w macierz 4x4:
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
To usuwa całą translację, ale zachowuje wszystkie transformacje rotacji, aby użytkownik mógł nadal rozglądać się po scenie.
Rezultatem jest scena, która natychmiast wygląda na olbrzymią dzięki naszemu skyboxowi. Jeśli będziesz latać wokół podstawowego pojemnika, natychmiast uzyskasz poczucie skali, które dramatycznie poprawia realizm sceny. Wynik wygląda mniej więcej tak:
Spróbuj poeksperymentować z różnymi skyboxami i zobacz, jaki mogą one mieć ogromny wpływ na wygląd i styl Twojej sceny.
Optymalizacja
Teraz renderowaliśmy skybox zanim wyrenderowaliśmy wszystkie inne obiekty na scenie. Działa to świetnie, ale nie jest zbyt wydajne. Jeśli najpierw wyrenderujemy skybox, uruchomimy Fragment Shader dla każdego piksela na ekranie, nawet jeśli tylko niewielka część nieba stanie się widoczna; fragmenty, które można było łatwo odrzucić za pomocą
Aby dać nam niewielki wzrost wydajności, sprawimy, że skybox będzie rysował się ostatni. W ten sposób bufor głębi jest całkowicie wypełniony wszystkimi wartościami głębokości obiektów, więc musimy renderować fragmenty skyboxa wszędzie tam, gdzie przechodzi wczesny test głębokości, znacznie redukując wywołania Fragment Shader’a. Problem polega na tym, że skybox najprawdopodobniej przestanie się renderować, ponieważ jest to kostką 1x1x1, która nie przechodzi większości testów głębi. Po prostu renderowania jej bez testowania głębi nie jest rozwiązaniem, ponieważ skybox nadpisze wtedy wszystkie inne obiekty na scenie. Musimy oszukać bufor głębokości, aby uwierzył, że skybox ma maksymalną wartość głębi równą 1.0
, tak, że nie przejdzie testu głębokości wszędzie tam, gdzie przed nim znajduje się inny obiekt.
W tutorialu układy współrzędnych powiedzieliśmy, że dzielenie perspektywiczne jest wykonywane po uruchomieniu Vertex Shader’a, dzieląc współrzędne xyz
wektora gl_Position przez jego komponent w
. Wiemy również z tutoriala test głębokości, że składnik z
wynikowego wektora jest równy wartości głębi tego wierzchołka. Używając tych informacji możemy ustawić składnik z
pozycji wyjściowej równy jego komponentowi w
, co spowoduje, że składnik z
będzie zawsze równy 1.0
, ponieważ po zastosowaniu podziału perspektywicznego do komponentu z
przekłada się to na takie równanie w
/ w
= 1.0
:
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
Wynikowe znormalizowane współrzędne urządzenia będą zawsze mieć wartość z
równą 1.0
: maksymalna wartość głębi. W efekcie skybox będzie renderowany wszędzie tam, gdzie nie ma widocznych obiektów (dopiero wtedy przejdzie test głębokości, wszystko inne znajdzie się przed skyboxem).
Musimy nieco zmienić funkcję głębi, ustawiając ją na GL_LEQUAL zamiast domyślnego GL_LESS. Bufor głębokości zostanie wypełniony wartościami 1.0
dla skybox’a, więc musimy upewnić się, że skybox przechodzi testy głębokości z wartościami niższymi lub równymi od wartości bufora głębi.
Możesz znaleźć bardziej zoptymalizowaną wersję kodu źródłowego tutaj.
Mapowanie środowiskowe
Teraz całe otoczenie jest odwzorowane na pojedynczym obiekcie tekstury. Możemy wykorzystać te informacje do czegoś więcej niż tylko skybox. Korzystając z cube mapy z otoczeniem, możemy nadać obiektom właściwości odbijające światło lub refrakcyjne. Techniki wykorzystujące cube mapę środowiska nazywa się
Odbicie światła
Odbicie światła jest właściwością, którą obiekt (lub część obiektu)
Podstawy odbicia światła nie są trudne. Poniższy obrazek pokazuje, jak obliczyć
Obliczamy wektor odbicia $\color{green}{\bar{R}}$ wokół wektora normalnego obiektu $\color{red}{\bar{N}}$ na podstawie wektora kierunku patrzenia widza $\color{gray}{\bar{I}}$. Możemy obliczyć ten wektor odbicia za pomocą wbudowanej funkcji GLSL
Ponieważ mamy już skonfigurowany skybox na naszej scenie, tworzenie odbić nie jest zbyt trudne. Zmienimy Fragment Shader używany przez kontener, aby nadać mu właściwości odbijania światła:
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 Position;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
Najpierw obliczamy wektor kierunku kamery I
i użyć go do obliczenia wektora odbicia R
, który następnie wykorzystamy do spróbkowania skyboxa. Zauważ, że mamy interpolowaną zmienną Normal i Position dla danego fragmentu, więc musimy również zmienić Vertex Shader.
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 Position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
Używamy wektorów normalnych, więc będziemy chcieli je ponownie przekształcić za pomocą macierzy normalnych. Wektor wyjściowy Position jest wektorem pozycji w przestrzeni świata. Wyjściowa wartość Vertex Shader’a Position służy do obliczania wektora kierunku widoku w Fragment Shaderze.
Ponieważ używamy wektorów normalnych, będziesz chciał zaktualizować dane wierzchołków i zaktualizować wskaźniki atrybutów. Pamiętaj także, aby ustawić zmienną uniform cameraPos.
Następnie chcemy również powiązać teksturę cube mapy przed wyrenderowaniem kontenera:
glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
Skompilowanie i uruchamianie kodu daje kontener, który działa jak idealne lustro. Otaczający skybox idealnie odbija się na powierzchni kontenera:
Możesz znaleźć pełny kod źródłowy tutaj.
Gdy odbicie zostanie zastosowane do całego obiektu (np. pojemnika), obiekt wygląda tak, jakby miał materiał o wysokim współczynniku odbicia, taki jak stal lub chrom. Gdybyśmy mieli załadować model nanosuit, którego użyliśmy w tutorialach o ładowaniu modeli, uzyskalibyśmy efekt, że kombinezon wygląda na wykonany w całości z chromu:
To wygląda całkiem nieźle, ale w rzeczywistości większość modeli nie odbija światła w całości. Możemy na przykład wprowadzić
Refrakcja
Inną formą mapowania środowiskowego jest
Refrakcja jest opisana przez prawo Snella, które w zastosowaniu z mapami środowiska wygląda mniej więcej tak:
Ponownie mamy wektor kierunku patrzenia $\color{gray}{\bar{I}}$, wektor normalny $\color{red}{\bar{N}}$ i tym razem wynikowy wektor refrakcji $\color{green}{\bar{R}}$. Jak widać, kierunek wektora widoku jest lekko załamany. Ten załamany wektor $\color{green}{\bar{R}}$ jest następnie używany do próbkowania cube mapy.
Refrakcję można łatwo zaimplementować za pomocą wbudowanej funkcji GLSL
Współczynnik załamania światła określa ilość zniekształcającego/załamanego światła. Każdy materiał ma swój własny współczynnik załamania światła. Listę najczęstszych współczynników załamania światła podano w poniższej tabeli:
Materiał | Współczynnik załamania światła |
---|---|
Powietrze | 1.00 |
Woda | 1.33 |
Lód | 1.309 |
Szkło | 1.52 |
Diament | 2.42 |
Korzystamy z tych współczynników załamania światła, aby obliczyć stosunek między obydwoma materiałami, przez które przechodzi światło. W naszym przypadku promień światła przechodzi z powietrza do szkła (jeśli założymy, że pojemnik jest wykonany ze szkła), więc stosunek równa się $\frac{1.00}{1.52} = 0.658$.
Mamy już powiązaną cube mapę, dostarczyliśmy dane wierzchołków z wektorami normalnymi i ustawiliśmy uniform pozycji kamery. Jedyną rzeczą, którą musimy zmienić jest Fragment Shader:
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
Zmieniając współczynniki załamania światła, można uzyskać zupełnie inne efekty wizualne. Skompilowanie aplikacji i jej uruchomienie nie daje interesujących wyników, ponieważ używamy prostego kontenera, który tak naprawdę nie pokazuje efektu refrakcji, i że teraz działa jak szkło powiększające. Użycie tych samych shaderów dla modelu nanokombinezonu pokazuje nam jednak efekt, którego szukamy: obiekt przypominający szkło.
Możesz sobie wyobrazić, że dzięki odpowiedniej kombinacji oświetlenia, odbicia światła, refrakcji i ruchu wierzchołków możesz stworzyć całkiem schludną grafikę wody. Zwróć uwagę, że dla uzyskania dokładnych wyników fizycznych powinniśmy ponownie załamać światło, gdy opuszcza ono obiekt; teraz po prostu używaliśmy refrakcji jednostronnej, która jest w wystarczająca dla większości celów.
Dynamiczne mapy środowiskowe
W tej chwili używaliśmy statycznej kombinacji obrazów jako skybox, który wygląda świetnie, ale nie zawiera rzeczywistej sceny z potencjalnie poruszającymi się obiektami. Tak naprawdę tego nie zauważyliśmy, ponieważ używaliśmy tylko jednego obiektu. Gdybyśmy mieli lustrzane obiekty z wieloma otaczającymi obiektami, tylko skybox byłby widoczny w lustrze, tak jakby był jedynym obiektem na scenie.
Używając framebufferów możliwe jest stworzenie tekstury sceny dla wszystkich 6 różnych kątów widzenia z obiektu, o który ma odbijać otoczenie, i zapisywanie ich w cube mapie po każdej iteracji. Następnie możemy użyć tej (dynamicznie wygenerowanej) cube mapy, aby stworzyć realistycznie wyglądające odbicie i refrakcję światła, które obejmują wszystkie inne obiekty na scenie. Jest to nazywane
Choć wygląda to świetnie, ma jedną ogromną wadę: musimy renderować scenę 6 razy na obiekt przy użyciu cube mapy, co jest ogromnym kosztem obliczeniowym dla aplikacji. Nowoczesne aplikacje starają się jak najlepiej wykorzystać skybox i tam, gdzie to możliwe, wstępnie prekompilować mapy, gdzie to tylko możliwe, by tworzyć dynamiczne mapy środowiskowe. Mapowanie dynamiczne jest świetną techniką, jednak wymaga wielu sprytnych sztuczek i hacków, aby działało to dobrze w rzeczywistej aplikacji bez zbyt dużego spadku wydajności.
Ćwiczenia
- Spróbuj wprowadzić mapy odbić w klasie Modelu, którą stworzyliśmy w tutorialach o ładowaniu modeli. Możesz znaleźć ulepszony model nanokombinezonu z dołączonymi mapami odbić tutaj. Jest jednak kilka rzeczy, na które należy uważać:
- Assimp naprawdę nie lubi map odbić w większości formatów modeli 3D, więc trochę oszukałem, przechowując mapy odbić jako mapy otoczenia. Można załadować mapy odbić, ustawiając aiTextureType_AMBIENT jako typ tekstury podczas ładowania materiałów.
- Trochę pospiesznie stworzyłem teksturę odbić wzorując się na mapach specular, więc mapy odbić nie będą dokładnie dopasowywać się do modelu w niektórych miejscach :).
- Ponieważ sam klasa ładowania modeli wykorzystuje już 3 jednostki tekstur w Fragment Shader, musisz powiązać skybox z czwartą jednostką teksturowania, ponieważ będziemy także próbkować skybox w tym samym Fragment Shaderze.
- Jeśli zrobiłeś wszystko dobrze, to efekt powinien wyglądać tak.