Podstawową cechą, jaką powinna posiadać aplikacja 3d, jest szybkość. Powinieneś zawsze ograniczać ilość rysowanych wielokątów ? przy pomocy sortowania, usuwania tylnych powierzchni, czy też algorytmów do zmiany szczegółowości. Jeśli jednak wszystko inne zawodzi i potrzebujesz po prostu mieć możliwość wysłania do karty ogromnej ilości prymitywów, możesz zawsze użyć optymalizacji, którą udostępnia OpenGL [od tłumacza: tak naprawdę to niezależnie od stosowania innych metod przyśpieszania renderingu, technika przedstawiona w tym artykule powinna być używana zawsze gdy karta ją obsługuje]. Tablice wierzchołków(ang. vertex arrays) są jedną z metod takiej optymalizacji, powstało jednak rozszerzenie, dzięki któremu ilość FPS może powędrować pod sufit ? obiekty bufora wierzchołków(ang. Vertex Buffer Objects, w skrócie VBO). Rozszerzenie to, o nazwie ARB_vertex_buffer_object, działa tak jak tablice wierzchołków, z wyjątkiem tego, iż ładuje dane do pamięci karty graficznej, znacząco zmniejszając czas renderingu. Należy zauważyć, iż rozszerzenie jest w miarę nowe, więc nie wszystkie karty je obsługują, dlatego napiszemy kod, który działa również w przypadku, gdy nie jest ono dostępne[od tłumacza ? to dość stary artykuł, praktycznie wszystkie współczesne karty posiadają to rozszerzenie?tak dokładnie to wszystko od gf 2 oraz radeon`a 8500 a może jeszcze wcześniej, nie wiem, jak z kartami intel`a].
W tym tutorialu:
Więc zaczynajmy! Najpierw zdefiniujmy kilka stałych.
Pierwsze dwie stałe są często używane przy mapach wysokości ? pierwsza ustawia rozdzielczość siatki, czyli ile pikseli tekstury będzie przypadać na każdy kwadrat mapy wysokości, a druga ustawia pionowe skalowanie odczytanych danych. Trzecia stała, o ile będzie zdefiniowana, będzie wymuszać wyłączenie VBO ? dzięki temu będzie można łatwo zobaczyć różnice w wydajności.
Następnie mamy stałe specyficzne dla tego rozszerzenia, typy danych, oraz definicje wskaźników na funkcje.
Zdefiniowałem tylko rzeczy potrzebne w przykładowej aplikacji. Jeśli potrzebujesz większej funkcjonalności, polecam ściągnięcie najnowszej wersji pliku glext.h ze strony http://www.opengl.org i używanie rzeczy zdefiniowanych właśnie w nim(dzięki temu twój kod stanie się również znacznie czytelniejszy). W lekcji zostaną wytłumaczone tylko te funkcje, z których będziemy korzystali.
Teraz będą podstawowe definicje matematyczne oraz klasa naszej siatki. Wszystkie są bardzo proste, napisane specjalnie na potrzeby tej aplikacji. Jak zawsze, zalecam, abyś napisał swoją własną bibliotekę matematyczną.
Mam nadzieję, iż kod jest zrozumiały bez dodatkowych wyjaśnień. Należy zauważyć, iż umieściłem współrzędne tekstur w oddzielnym buforze, to nie jest konieczne, ale wyjaśnienie to w dalszej części tekstu.
Tutaj mamy nasze zmienne globalne. Najpierw mamy zmienną określającą, czy rozszerzenie VBO jest dostępne, która będzie odpowiednio ustawiana w kodzie inicjującym. Następnie mamy siatkę oraz rotację wokół osi Y. Na końcu znajdują się zmienne pomocnicze dla licznika FPS. Zdecydowałem się umieścić licznik FPS, aby łatwo było zauważyć zmianę wydajności przy używaniu rozszerzenia VBO.
Przeskoczmy do omawiania funkcji składowych CMesh, zaczynając od funkcji LoadHeightmap(wczytaj mapę wysokości). Mapa wysokości to dwuwymiarowy zbiór danych, często w formie obrazka, który mówi nam o wysokości, na jakiej znajdują się punkty w siatce terenu. Jest dużo sposobów na zaimplementowanie mapy wysokości i jak to często bywa, nie ma idealnego rozwiązania. Moja implementacja czyta składowe kolorów z bitmapy i używa algorytmu do policzenia jasności, aby określi wysokość w danym punkcie. Wynikowe dane powinny być identyczne niezależnie od tego, czy obrazek był kolorowy, czy też był zapisany w odcieniach szarości, dzięki czemu mapa wysokości może być kolorowa[ od tłumacza ? tylko po co? ]. Osobiście polecam zastosowanie obrazków czterokanałowych, np. tga(targa), i użycie kanału alfa do wyznaczania wysokości[od tłumacza ? to jest dobre dla małych terenów, dla większych dane potrzebne na tekstury będą zajmowały gigantyczną ilość pamięci, trzeba wtedy zastosować tzw. Terrain Splatting; dodatkowo dużo wartości kanału alfa nie będzie używane, więc kolejne marnotrawstwo miejsca na dysku]. Niezależnie od tego na potrzeby tego tutoriala zdecydowałem, iż prosta bitmapa będzie najlepsza.
Na początku upewnimy się, iż nasza bitmapa istnieje, jeśli tak będzie w istocie, wczytamy ją przy pomocy biblioteki GLaux. Tak, tak, najprawdopodobniej lepiej byłoby napisać własną funkcję ładującą, ale to wykracza poza ramy tego tutorial`a.
Teraz zaczynają się nieco bardziej interesujące rzeczy. Na początku chciałbym zwrócić uwagę, iż moja mapa wysokości ma trzy punkty na każdy trójkąt ? wierzchołki nie są współdzielone. Później wytłumaczę dlaczego wybrałem ten sposób, jednak uznałem, iż powinieneś to wiedzieć zanim spojrzysz na kod[od tłumacza ? nie słuchać go! On kłamie . Później wytłumaczę, czemu tak twierdzę, ale uznałem, że powinieneś to widzieć, zanim mu uwierzysz ? tutaj równie dobrze można by było użyć wierzchołków współdzielonych oraz bufora indeksów?byłoby nawet lepiej?]
Zaczynam od wyliczenia ilości wierzchołków w siatce. Algorytm jest intuicyjny ( (szerokość/rozdzielczość) * (długość/rozdzielczość)*3 wierzchołki na każdy trójkąt * 2 trójkąty w kwadracie). Następnie przydzielam odpowiednią ilość pamięci i przypisuję do wierzchołków dane.
Na końcu są funkcje ładujące teksturę do karty graficznej i zwalniające jej dane. Powinieneś je znać z poprzednich tutoriali.
PtHeight jest stosunkowo prostą funkcją. Liczy indeks na podstawie podanych parametrów, zawija(ang. wrap) wszystkie wartości wychodzące poza zakres, i liczy wysokość na podstawie danych w mapie wysokości.
Radujmy się, nareszcie nadszedł czas na zajęcie się buforami wierzchołków oraz VBO[od tłumacza ? już sobie przypomniałem, czemu nigdy nie czytałem tutoriali NEHE ? przez pół lekcji autor nie wspomina prawie nic o zagadnieniu z tematu, normalnie jak na lekcji polskiego w liceum?w sumie jak na każdej lekcji w liceum?]. Tak więc co to są te bufory wierzchołków? Otóż jest to sposób na przesłanie do karty graficznej wskaźników na twoje dane, a następnie wyrenderowanie wszystkiego przy pomocy zaledwie kilku wywołań funkcji OpenGL. W rezultacie mamy znacznie mniej wywołań (brak funkcji typu glVertex) oraz znaczący wzrost wydajności. Co to jest VBO? Otóż obiekty bufora wierzchołków wykorzystują szybką pamięć karty graficznej zamiast zwykłej pamięci RAM, którą wykorzystywałeś do tej pory. Owocuje to nie tylko mniejszą ilością odwołań do pamięci, ale również skraca czas przesyłania danych. Z moich doświadczeń wynika, iż dzięki VBO ilość FPS wzrasta trzykrotnie, a takiego skoku wydajności po prostu nie sposób zlekceważyć.
Tak więc zamierzamy utworzyć wspomniany VBO. Całość składa się z kilku kroków: najpierw trzeba wywołać glGenBuffersARB, aby otrzymać prawidłową nazwę dla naszego VBO. Nazwa to po prostu identyfikator, dzięki któremu OpenGL może powiązać twoje dane z odpowiednim obiektem. Chcemy wygenerować tą nazwę, gdyż każdy obiekt musi mieć unikatowy identyfikator, dzięki tej funkcji uzyskujemy pewność, iż ten warunek będzie spełniony. Następnie ustawiamy ten bufor jako aktywny przy pomocy funkcji glBindBufferARB[od tłumacza ? pierwsze wywołanie tej funkcji odpowiada również za utworzenie buforu, gdyż funkcja glGenBuffersARB ich nie tworzy, ona jedynie zwraca poprawne identyfikatory]. Na końcu wczytujemy dane do naszej karty przy pomocy wywołania glBufferDataARB, podając rozmiar danych oraz odpowiedni wskaźnik. glBufferDataARB skopiuje dane do pamięci twojej karty graficznej, a to oznacza, iż przechowywanie lokalnej kopii nie ma sensu, więc możemy ją usunąć.
[
Od tłumacza ? radzę się dokładnie zapoznać ze specyfikacją tego rozszerzenia, gdyż przedstawione tutaj informacje są bardzo niepełne, śmiem nawet twierdzić, iż w dużej ilości przypadków korzystanie z VBO będzie niemożliwe jeśli człowiek będzie chciał się opierać tylko na nich. W skrócie: do glBufferDataARB możemy zamiast wskaźnika przesłać NULL, przez co dane będą na początku przypadkowe. Trzeba natomiast przesłać poprawny rozmiar, i nie można go zmieniać podczas istnienia obiektu. Jeśli chce się osiągnąć taki efekt, trzeba usuną stary obiekt i utworzyć nowy, z zadanym rozmiarem. Następnie możemy aktualizować zawartość buforu przy pomocy funkcji glBufferSubDataARB, pobrać jego zawartość przy pomocy glGetBufferSubDataARB, lub też uzyskać wskaźnik na jego dane przy pomocy glMapBufferARB i zwolnić ten wskaźnik przy pomocy glUnmapBufferARB. Należy uważać, gdyż pomiędzy funkcjami glMapBufferARB / glUnmapBufferARB bufor może zostać utracony. Wspomniane funkcje możemy wywoływać dowolną ilość razy dla danego buforu, choć przed kolejnym wywołaniem glMapBufferARB trzeba wywołać glUnmapBufferARB. Po szczegółowe informacje odsyłam do dokumentacji.
]
No dobrze, czas, aby zająć się inicjalizacją. Na początek utworzymy i wczytamy naszą siatkę. Następnie sprawdzimy, czy GL_ARB_vertex_buffer_object jest dostępne. Jeśli będzie dostępne, pobierzemy wskaźniki na funkcje przy pomocy wglGetProcAddress i utworzymy nasz VBO. Jeśli rozszerzenie nie jest dostępne, będziemy przechowywać dane jak zwykle, w buforach wierzchołków. Jest też warunek sprawdzający, czy jest zdefiniowana stała wymuszająca korzystanie ze zwykłych buforów a nie VBO.
IsExtensionSupported jest funkcją, którą możesz pobrać ze strony http://www.opengl.org . Moja wersja, moim skromnym zdaniem (ang. in my humble opinion, w skrócie IMHO), jest nieco czytelniejsza.
To jest dość proste. Niektórzy ludzie wolą po prostu użyć funkcji strstr w celu wyszukania ciągu znaków w innym ciągu znaków, ale najwyraźniej http://opengl.org nie ufa wystarczająco zawartości łańcucha z rozszerzeniami, aby zaakceptować to rozwiązanie. I wiecie co? Chyba nie jestem upoważniony od krytykowania tych gości.
Prawie skończone! Jedyne, co musimy zrobić, to wyrenderować całość.
Proste ? po każdej sekundzie zapisz licznik klatek jako wartość FPS i zresetuj ten licznik. Zdecydowałem się wyświetlać dodatkowo informację o ilości wyrenderowanych wielokątów, aby wzrost wydajności był bardziej widoczny. Następnie przesuwamy kamerę nad teren(możliwe, że będziesz musiał odpowiednio dostosować tę wartość, jeśli zmienisz zawartość mapy wysokości), i stosujemy kilka obrotów. flYRot jest inkrementowany w funkcji Update.
Aby użyć buforów wierzchołków(oraz VBO), musisz powiedzieć OpenGL, które dane chcesz do niego przesłać. Z tego powodu musisz najpierw włączyć odpowiednie strumienie - w naszym przypadku GL_VERTEX_ARRAY oraz GL_TEXTURE_COORD_ARRAY, robisz to przy pomocy funkcji glEnableClientState. Następnie należy ustawić odpowiednie wskaźniki. Mam wątpliwości, czy musisz to robić dla każdej klatki jeśli masz tylko jedną siatkę[od tłumacza - tia?.wystarczy przesunąć kilka linijek kodu w inne miejsce, więc autor sprawdzenie tego pozostawił pewnie jako ćwiczenie dla czytelnika ], ale to nie ogranicza zbytnio ilości klatek, dlatego nie widzę problemu.
Aby ustawić wskaźnik do danego typu danych, musisz użyć odpowiedniej funkcji ? w naszym przypadku będą to glVertexPointer oraz glTexCoordPointer. Użycie jest całkiem proste ? należy podać ilość zmiennych dla każdego wierzchołka(trzy dla danych o pozycji oraz dwa dla danych o współrzędnych tekstur), typ danych w buforach (float), odległość pomiędzy danymi dla sąsiednich wierzchołków (w przypadku, gdy przesyłane dane nie są jedyne w strukturze), oraz wskaźnik na dane. Możesz również użyć funkcji glInterleavedArrays oraz trzymać wszystkie dane w jednym, wielkim buforze pamięci, ale chciałem zademonstrować, w jaki sposób użyć wielu VBO.
Przesyłanie danych zapisanych w VBO nie różni się zbyt bardzo. Tak naprawdę jedyną różnicą jest to, iż zamiast podawać wskaźnik na dane, musimy ustawić odpowiedni VBO jako aktywny, ponadto ustawić wskaźniki w odpowiednich funkcjach na zero[od tłumacza ? tak naprawdę wskaźnik staje się informacją o offsecie względem początku bufora. Jeśli chcielibyśmy z jakiś powodów pominąć pierwszy punkt w danych o wierzchołkach, musielibyśmy zamiast zera podać sizeof(float)*3].
Wiecie co? Rendering nigdy nie był prostszy :)
Jak widać, do renderingu używamy funkcji glDrawArrays. Sprawdza ona, które strumienie danych są włączone, a następnie używa ich przy renderingu. Podajemy jej również co chcemy wyrenderować(trójkąty, punkty, itd?, indeks, od którego ma zacząć odczytywać dane oraz ilość punktów do wyrenderowania [Od tłumacza ? Ważnie! Nie ilość prymitywów, ale punktów!]. Jest też wiele innych metod, dzięki którym możemy przesłać dane do renderingu np. glArrayElement, ale przedstawiona metoda jest najszybsza. Należy zauważyć, że glDrawArrays nie jest pomiędzy funkcjami glBegin oraz glEnd, gdyż nie jest to konieczne.[od tłumacza ? powiem więcej, gdy wywołamy tą funkcję pomiędzy glBegin oraz glEnd, to będzie zgłoszony błąd GL_INVALID_OPERATION].
Funkcja glDrawArrays jest powodem, dla którego zdecydowałem się nie współdzielić danych o wierzchołkach pomiędzy trójkąty ? to nie jest możliwe. Z tego, co wiem, najlepszym sposobem na ograniczenie użycia pamięci są paski trójkątów(ang. triangle strips), których użycie wykracza poza ramy tego tutorial`a. Powinieneś również uważać, gdyż każdy wierzchołek powinien mieć swoją własną normalną. Rozważ możliwość generowania normalnych dla każdego punktu - to powinno znacznie zwiększyć realizm grafiki.
[translator] ? aż nie wiem, czy dobrze przetłumaczyłem ? ostatnie zdanie brzmiało >Consider that an opportunity to calculate your normals per-vertex, which will greatly increase visual accuracy<. Szczerze mówiąc, mam nadzieję, iż się pomyliłem, bo to jest NEHE, najpopularniejsza stronka o OpenGL?jeśli jednak ze mną jest wszystko w porządku, to muszę zaznaczyć, iż autor powiedział głupotę, bo najczęściej jest zgoła na odwrót ? normalne generowane dla każdego punktu oddzielnie powodują, iż powierzchnia wydaje się bardziej kanciasta, natomiast gdy normalne są uśredniane ? znaczy każdy wierzchołek posiada normalną, która jest wynikiem uśrednienia normalnych ze wszystkich trójkątów, do których należy, to po włączeniu oświetlenia powierzchnia wydaje się gładsza, bo jest cieniowana tak, jakby faktycznie była gładka. Ostre, nie uśredniane normalne są potrzebne tylko wtedy, gdy siatka ma faktycznie ostre krawędzie, ale tak nie jest w przypadku mapy wysokości, o którą się tutaj rozchodzi.
Co się tyczy natomiast zdania >Funkcja glDrawArrays jest powodem, dla którego zdecydowałem się nie współdzielić danych o wierzchołkach pomiędzy trójkąty ? to nie jest możliwe< to winien jestem kolejne wyjaśnienie(obawiam się, że równie niekompletne, jak poprzednie, ale mimo wszystko mam nadzieję, że uświadamiające parę rzeczy).
1. Mam nauczkę, aby najpierw przeczytać artykuł, a dopiero później go tłumaczyć?w ten sposób już na początku mógłbym się zniechęcić do tego zajęcia .
2. Faktycznie glDrawArrays nie udostępnia takiej funkcjonalności, ale jest jeszcze funkcja glDrawElements, którą autor chyba przeoczył. Istnieje coś takiego jak indeksy, dzięki którym wierzchołki mogą być współdzielone pomiędzy trójkątami. Trzeba utworzyć tzw. bufor indeksów ? który ma postać (indeks punktu 1; indeks punktu 2; itd.). Bufor ten określamy w funkcji glDrawElements, którą się stosuje zamiast funkcji glDrawArrays, w tym przypadku wyglądałoby to w ten sposób:
Oczywiście zakładając, iż w klasie cMesh są podane zmienne, które zostały odpowiednio zainicjowane. Parametr GL_UNSIGNED_SHORT określa, jaki format mają nasze indeksy. Możliwe argumenty to GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, oraz GL_UNSIGNED_INT.
Powyższe wywołanie miałoby ten sam skutek, co
Przy czym glDrawElements wykonało by się znacznie szybciej.
Pozostaje jeszcze pytanie, w jaki sposób można wykorzystać VBO do przechowywania indeksów ? w końcu one też trochę zajmują, fajnie byłoby zaoszczędzić czas na ich przesyłaniu. Otóż przed funkcją glDrawElements trzeba wywołać
Jeśli operujemy na buforach wierzchołków, to jako pierwszy argument funkcji glBindBufferARB musimy podawać GL_ARRAY_BUFFER_ARB, jeśli natomiast chcemy operować na buforach indeksów, to musimy podawać GL_ELEMENT_ARRAY_BUFFER_ARB. W funkcji glDrawElements zamiast wskaźnika na dane podajemy wtedy offset od początku danych, wyrażony w bajtach.
Tworzenie bufora indeksów przebiega tak samo, jak buforów wierzchołków, róznica polega tylko na podaniu innego pierwszego argumentu w funkcji glBindBufferARB, o czym wspomniałem przed chwilą.
[/translator]
Teraz możemy wyłączyć odpowiednie strumienie danych, co zakończy naszą lekcję.