Upload
dangbao
View
237
Download
0
Embed Size (px)
Citation preview
Rok akademicki 2010/2011
Politechnika Warszawska
Wydział Elektroniki i Technik Informacyjnych
Instytut Informatyki
PRACA DYPLOMOWA MAGISTERSKA
Stanisław Ignacy Gąsiorowski
Silnik graficzny 3D
Opiekun pracy
dr inż. Tomasz Martyn
Ocena: .....................................................
................................................................
Podpis Przewodniczącego
Komisji Egzaminu Dyplomowego
Streszczenie
Przedmiotem niniejszej pracy jest rozbudowa silnika graficznego. W pracy przedstawiono
nową architekturę silnika, opisano jego działanie, a także przetestowano przydatność do
tworzenia gier komputerowych. Silnik napisano w językach C++, CG oraz GLSL
i przystosowano do pracy w systemach Windows oraz Linux. Do generowania grafiki
wykorzystana została biblioteka OpenGL. Z silnikiem zintegrowano silnik fizyki NVidia
PhysX. Dodano również nowe elementy: mechanizm zarządzania sceną, efekty
cząsteczkowe, mechanizm renderowania map kubicznych. Jednym z ciekawszych
elementów jest autorski system portali, którego implementacja sprawiła najwięcej
trudności. Powstała także baza materiałów, które można nakładać na obiekty.
Zastosowanie kubicznych map otoczenia pozwoliło stworzyć materiały przezroczyste
i odbijające. Opracowano szereg efektów, z których najbardziej złożone to symulacja głębi
ostrości, rozmycie ruchu i mgła wolumetryczna. Pozostałe efekty to m.in.: stereoskopia,
rozmycie, wyostrzenie, poświata, mieszanie kolorów. Wygodny w obsłudze interfejs
użytkownika pozwala na proste zarządzanie efektami.
Słowa kluczowe: silnik graficzny, grafika 3d, efekty, głębia ostrości, mgła, rozmycie
ruchu, portale, efekty cząsteczkowe, interfejs użytkownika, OpenGL, shadery.
Abstract
Title: 3D graphics engine
The subject of this thesis is development of graphics engine. Thesis describes new engine
architecture and the usability to create games. Engine was written in C++, CG and GLSL
and uses OpenGL for rendering graphics. It can work in both Windows and Linux. NVidia
PhysX physics engine was integrated with the engine. New elements were added, such as:
scene management, particle effects, environmental cube map rendering. One of the most
interesting elements is a portal system, which was created from the beginning. The engine
has set of base materials, which can be applied to objects. Transparent and reflective
materials can be created using environmental cube maps. Many effects were created, of
which the most interesting are depth of field, motion blur and volumetric fog. There are
also other effects such as: blur, sharpen, glow, color blend. The engine offers a useful
interface for managing effects.
Keywords: graphics engine, 3d graphics, effects, depth of field, fog, motion blur, portals,
particles, user interface, OpenGL, shaders.
Spis treści
1 Wprowadzenie ...................................................................................................... 4
1.1 Przegląd istniejących rozwiązań ............................................................................... 5
1.1.1 Podział przestrzeni .............................................................................................. 6
1.1.2 Fizyka .................................................................................................................. 7
1.1.3 Portale ................................................................................................................. 7
1.2 Cel i zakres pracy ....................................................................................................... 8
2 Architektura ......................................................................................................... 9
2.1 Założenia sprzętowe ................................................................................................... 9
2.2 Silnik ............................................................................................................................ 9
2.3 Narzędzia ................................................................................................................... 11
3 Implementacja .................................................................................................... 14
3.1 Drzewo ósemkowe .................................................................................................... 14
3.2 Silnik fizyki NVidia PhysX ...................................................................................... 18
3.3 Portale ....................................................................................................................... 23
3.3.1 Wprowadzenie ................................................................................................... 23
3.3.2 Renderowanie sceny za portalem ...................................................................... 23
3.3.3 Integracja fizyki z mechanizmem portali .......................................................... 25
3.3.4 Fałszywe kolizje ................................................................................................ 28
3.3.5 Skalowanie portali ............................................................................................. 29
3.3.6 Kamera a portale ............................................................................................... 29
3.3.7 Oświetlenie a portale ......................................................................................... 30
3.4 Kubiczne mapy otoczenia ........................................................................................ 31
3.5 Efekty cząsteczkowe ................................................................................................. 35
4 Efekty i materiały ............................................................................................... 39
4.1 Stereoskopia .............................................................................................................. 39
4.2 Głębia ostrości .......................................................................................................... 42
4.3 Rozmycie ruchu ........................................................................................................ 57
4.4 Mgła ........................................................................................................................... 63
4.5 Inne efekty ................................................................................................................. 71
4.6 Materiały ................................................................................................................... 72
5 Przykładowa gra ................................................................................................ 81
6 Podsumowanie .................................................................................................... 83
Bibliografia ................................................................................................................ 85
Spis załączników ..................................................... Błąd! Nie zdefiniowano zakładki.
~ 4 ~
1 Wprowadzenie
Grafika komputerowa to dziedzina zajmująca się tworzeniem grafiki za pomocą
komputera. Istnieje ona od czasów pierwszych komputerów zdolnych przedstawiać obrazy.
Od lat używana jest przez artystów jako ich kolejne narzędzie, obok pędzla i ołówka.
Grafikę komputerową wykorzystuje się m.in. do wzbogacania filmów o efekty specjalne,
tworzenia gier komputerowych, a także obrazowania medycznego.
Grafika komputerowa może być dwuwymiarowa lub trójwymiarowa. Pojęcie
trójwymiarowości w grafice komputerowej można interpretować na kilka sposobów. W tej
pracy będzie oznaczało przedstawianie trójwymiarowych obiektów na dwuwymiarowej
płaszczyźnie ekranu za pomocą odpowiednich rzutowań perspektywicznych.
Gałęzią grafiki komputerowej, na której skupia się niniejsza praca, jest grafika czasu
rzeczywistego. Polega ona na tym, że obrazy muszą być wygenerowane w czasie
rzeczywistym, co oznacza, że muszą zostać stworzone w skończonym, krótkim czasie.
W praktyce jest to czas rzędu kilkudziesięciu milisekund. Właśnie dlatego jednym
z kluczowych parametrów opisujących system tworzący grafikę jest liczba klatek
generowanych na sekundę (FPS – ang. frames per second). W grafice czasu rzeczywistego
jakość grafiki nie jest najważniejsza i może zostać zdegradowana w celu zwiększenia
liczby klatek generowanych na sekundę. W tradycyjnej grafice komputerowej ilość czasu,
który można poświęcić na wygenerowanie obrazu jest teoretycznie nieograniczona, dlatego
też można stworzyć obrazy w dowolnie wysokiej jakości. W grafice czasu rzeczywistego
ograniczenie narzucone na czas generowania jest bardzo ostre i dlatego grafika ta stanowi
duże wyzwanie dla programisty [1].
Grafika czasu rzeczywistego znajduje zastosowanie tam, gdzie wymagana jest
interakcja z użytkownikiem. Zdarzają się jednak wyjątki, takie jak animacje generowane
w czasie rzeczywistym, pokazujące możliwości sprzętu oraz programistów. Systemy
szkoleniowe pilotów cywilnych, jak i wojskowych, używają systemów grafiki czasu
rzeczywistego dla symulatorów lotniczych. Programy dla architektów pozwalają na
wizualizację m.in. budynków i jachtów, stwarzając możliwość odbycia spaceru przez
klienta, np. po swoim przyszłym mieszkaniu. Jednak największą dziedziną wykorzystującą
grafikę czasu rzeczywistego są gry komputerowe. To właśnie dzięki nim przemysł sprzętu
komputerowego rozwija się tak szybko. Coraz większe wymagania gier powodują bardzo
dynamiczny rozwój sprzętu.
Gry komputerowe wyznaczają kierunek rozwoju sprzętu komputerowego i najlepiej
pokazują jego możliwości. Gry dążą do fotorealizmu i bardzo szybko zbliżają się do
poziomu obrazu znanego z filmów.
Gry komputerowe tworzone są w oparciu o tzw. „silnik graficzny”. W przypadku
gier trójwymiarowych rola silnika graficznego w dzisiejszych produkcjach nie ogranicza
się tylko do generowania grafiki. Silnik graficzny pełni kluczową rolę w całym programie.
To od możliwości silnika zależy, jakie gry można na nim zrobić. Niestety, pojęcia silnika
graficznego i silnika gry są coraz częściej mylone, a najpopularniejsze staje się używanie
samego określenia „silnik”, bez podawania jakiego jest on rodzaju. Jest to silnik
„do wszystkiego”. Poprawną nazwą będzie w tym wypadku silnik gry, natomiast silnik
graficzny jest raczej modułem graficznym silnika gry. Silnik gry integruje wszystkie
1.1 Przegląd istniejących rozwiązań
~ 5 ~
elementy, takie jak moduł graficzny, dźwiękowy, silnik fizyki, interfejs, mechanizmy
wejścia-wyjścia, komunikację sieciową oraz wiele innych, w jedną całość.
Silnik stanowi podstawę, na której buduje się grę komputerową. Wprowadza on
wyższy poziom abstrakcji dla projektanta gry, ukrywając szczegóły implementacyjne
poszczególnych elementów. Udostępnia tak abstrakcyjne byty jak: scena, obiekt, kamera,
światło, tekstura. Programista gry nie musi zastanawiać się nad implementacją tych
elementów. Dzięki takiemu rozdzieleniu możliwe jest stworzenie gry na bazie silnika,
którą potem można przenieść na inne platformy, zmieniając tylko implementację silnika.
Silnik stanowi również warstwę między grą a systemem operacyjnym.
1.1 Przegląd istniejących rozwiązań
Na temat grafiki komputerowej czasu rzeczywistego powstało bardzo wiele książek,
artykułów i stron internetowych. Można na nich znaleźć kursy dla początkujących,
chcących programować grafikę komputerową, jak również informacje dla osób na każdym
poziomie zaawansowania.
Pierwszym zagadnieniem jest budowa samego silnika. Na temat jego tworzenia
można znaleźć wzmianki w wielu książkach, jednak skupiających się głównie na tym
zagadnieniu jest niewiele. Warto wymienić [2], [3] i [4]. Znaleźć można również wiele
artykułów na stronach internetowych, niektóre z nich zawierają sporo użytecznych
informacji, np. [5] i [6].
Kolejnym zagadnieniem jest przeanalizowanie dostępnych bibliotek graficznych.
Dzisiaj w praktyce liczą się tylko dwie technologie: OpenGL oraz DirectX. Wybór między
nimi jest trudny. Obydwie udostępniają podobne możliwości, jednak na niekorzyść
technologii DirectX przemawia fakt, że wszystkie nowe funkcje dostępne od 10. wersji
biblioteki dostępne są tylko na systemach Windows Vista i Seven. OpenGL natomiast
udostępnia wszystkie swoje możliwości niemalże na każdym systemie operacyjnym.
OpenGL dostępny jest na wszystkie wersje systemu Windows, a także na systemach Linux
oraz Mac OS [7].
Przy tworzeniu silnika gry warto przyjrzeć się profesjonalnym silnikom i ich
możliwościom. Bardzo wartościowe wskazówki co do tworzenia silnika dostarczają kody
źródłowe trzech części gry Quake, udostępnione publicznie [8].
Silnik Unreal Engine 3.0 jest jednym z najlepszych silników dostępnych na rynku.
Warto wzorować się na możliwościach i elastyczności jakie oferuje [9].
Następnie warto wymienić kolejne wersje silnika ID Tech o numerach 4 i 5,
o których można się dowiedzieć od Johna Carmacka. Zamieszcza on swoje wypowiedzi
odnośnie problemów podczas pisania silników na wielu stronach internetowych [10, 11].
Kolejnym silnikiem wartym uwagi jest CryENGINE, aktualnie w wersji 3 [12].
Warto zwrócić również uwagę na silnik Unigine [13]. Jego autor tworzył małe
programy z dziedziny grafiki trójwymiarowej i zamieszczał je na swojej stronie.
Nabierając doświadczenia zaczął pisać swój silnik, aż w końcu postanowił rozwinąć go do
postaci komercyjnej. Warto zatem przejrzeć programy wcześniej udostępniane przez tego
autora.
1.1 Przegląd istniejących rozwiązań
~ 6 ~
1.1.1 Podział przestrzeni
Podział przestrzeni i zarządzanie geometrią jest bardzo ważną składową silnika. Od
mechanizmu zarządzania geometrią wymaga się, by można było dodawać do niego
obiekty, usuwać je, a także zmieniać ich położenie i wielkość. Mechanizm ten odpowiada
również za to, które obiekty są widoczne przez kamerę lub światło.
Najpopularniejszymi metodami dzielenia przestrzeni są m.in. drzewa BSP
(ang. binary space partition), drzewa ósemkowe (ang. octree) i portale. Drzewa BSP oraz
ósemkowe dzielą przestrzeń na komórki (sektory), do których przypisują obiekty.
Zastosowanie portali polega natomiast na podzieleniu sceny na sektory, a między nimi
umieszczenie portali – przejść miedzy sektorami. Drzewa BSP opisane są w wielu
źródłach, m.in. [14], [15] i [16]. Opis portali można znaleźć w [17] oraz [18]. Drzewa
ósemkowe opisane są w [19].
Metody używające drzew BSP i ósemkowych dobrze sprawdzają się dla otwartych
przestrzeni (ang. out-door). Natomiast portale najlepiej spisują się w przestrzeniach
zamkniętych (ang. in-door), ponieważ mogą być z powodzeniem umieszczone
np. w drzwiach pomiędzy pomieszczeniami (sektorami). Jednak drzewa BSP i ósemkowe
również sprawdzają się w przestrzeniach zamkniętych.
Możliwe jest także łączenie metod. Np. dla zamkniętych przestrzeni, w których
niektóre pomieszczenia (sektory) mają duże rozmiary, można zastosować portale dzielące
pomieszczenia, a także drzewa ósemkowe dla podziału dużych pomieszczeń.
Przeważnie sceny obsługiwane przez silnik są typu zamkniętego lub otwartego.
Wtedy wybór mechanizmu podziału przestrzeni jest łatwy. Jednak czasami sceny są
otwarte, lecz zawierają w sobie przestrzenie zamknięte, np. planszą gry jest wyspa, na
której znajduje się wejście do podziemnej bazy. Wtedy przestrzenią otwartą jest teren
wyspy, a przestrzenią zamkniętą podziemna baza. Elementem łączącym te dwie
przestrzenie mogą być drzwi. Najlepszym rozwiązaniem jest wtedy łączenie metod,
np. do przestrzeni otwartej drzewa ósemkowego, a dla przestrzeni zamkniętej systemu
sektorów i łączących je portali. Drzwi do bazy mogą być zatem portalem, który łączy
przestrzeń otwartą z przestrzenią zamkniętą. Jednak podział sceny na przestrzeń otwartą
i zamkniętą musi być wykonany przez projektanta poziomu, ponieważ automatyczne
wykrycie gdzie przestrzeń jest zamknięta, a gdzie otwarta jest wyjątkowo trudnym
zadaniem.
Największym problemem przy łączeniu mechanizmu portali z np. drzewem
ósemkowym jest określenie czy kamera znajduje się w przestrzeni zamkniętej czy otwartej.
Jeśli jednak założymy, że przestrzeń zamknięta będzie opisana przez projektanta poziomu
za pomocą w miarę prostej bryły, sprawdzenie, iż znajdujemy się w przestrzeni zamkniętej
będzie wymagało tylko określenia czy jesteśmy w środku bryły. Jeśli tak, to widoczne
obiekty określamy za pomocą mechanizmu portali, a jeśli nie – to za pomocą drzewa
ósemkowego. Jeśli natomiast widoczny jest portal łączący obie przestrzenie, to
w zależności, w której przestrzeni jesteśmy, dodajemy obiekty, które są widoczne przez
portal z drugiej przestrzeni.
1.1 Przegląd istniejących rozwiązań
~ 7 ~
1.1.2 Fizyka
Silnik fizyki wzbogaca silnik gry o dodatkowe cechy nadające realności rozgrywce.
Dzięki niemu możliwe jest symulowane zachowania obiektów, które jest wystarczająco
zgodne z zachowaniem obiektów w prawdziwym świecie. Umożliwia także tworzenie gier
opartych na fizyce, w których istotnym elementem jest właśnie zachowanie obiektów lub
cieczy. Można na przykład stworzyć prostą grę polegającą na rzucaniu przedmiotów do
celu, graniu w bilard lub kręgle albo balansowaniu na linie.
Silnik fizyki odpowiada za symulowanie zachowania obiektów. Należy dostarczyć
mu opis sceny, w tym obiektów i materiałów z jakich są wykonane, a także parametrów
symulacji, takich jak siła i kierunek grawitacji czy odcinek czasu jaki ma być symulowany.
Najpopularniejszymi silnikami używanymi w grach są Havok Physics [20], który jest
jednak silnikiem płatnym, oraz NVidia PhysX [21]. Używane są także mniej profesjonalne
silniki, takie jak Newton Dynamics [22], które również oferują duże możliwości.
Wiele silników gier zawiera własne implementacje silnika fizyki. Pozwala to na
dowolną kontrolę nad kodem silnika, jednak wymaga opracowania takiego silnika,
co związane jest z dużym nakładem pracy.
1.1.3 Portale
Portale opisane w niniejszej pracy nie są związane z portalami wykorzystywanymi
do podziału przestrzeni. Zbieżność nazw może być myląca, jednak w niniejszej pracy nie
będziemy się zajmować portalami w podziale przestrzeni.
Portale, opisane w niniejszej pracy, są to teleporty, przez które mogą się
przedostawać obiekty oraz światło. Portale łączą się w pary, tzn. każdy portal ma
odpowiadający mu drugi portal. Często portal, o którym mówimy, będziemy nazywać
portalem wejściowym, a sparowany z nim portal – wyjściowym. Patrząc przez portal
widzimy w nim to, co znajduje się za odpowiadającym mu portalem wyjściowym. Portal
jest płaski i ma ograniczoną powierzchnię, np. prostokąta.
Nie istnieje praktycznie żadna literatura opisująca portale, przez które może
przedostawać się światło oraz obiekty symulowane przez silnik fizyki, i które to portale
same mogą się poruszać. Portale takie pojawiły się w grze „Prey” filmy 3D Realms, jednak
nie umożliwiały przemieszczania obiektów przez portale. Dopiero gra „Portal” firmy
Valve pokazała potencjał portali. Gracze mogli przemieszczać obiekty pomiędzy
portalami. Gra zawierała zagadki logiczne, do rozwiązania których trzeba było
wykorzystać portale, np. mając przed sobą mur nie do przeskoczenia, można było stworzyć
jeden portal w podłodze, a drugi na suficie za przeszkodą i wskoczyć w portal na podłodze,
wyskakując w ten sposób za przeszkodą.
Jednym ze źródeł informacji o portalach są komentarze twórców gry „Portal”,
zawarte w samej grze. Pewne wypowiedzi twórców na temat technologii stojących za
portalami można znaleźć w [23].
1.2 Cel i zakres pracy
~ 8 ~
1.2 Cel i zakres pracy
Celem pracy jest rozszerzenie silnika grafiki trójwymiarowej, opracowanego
w ramach pracy inżynierskiej, o nowe elementy, takie jak:
integracja z silnikiem fizyki NVidia PhysX,
mechanizm zarządzania geometrią sceny,
system portali (w rozumieniu teleportów wspomnianych w pkt. 1.1.3),
efekty cząsteczkowe,
mechanizm renderowania map kubicznych.
Kolejnym celem pracy jest stworzenie bazy wielu prostych efektów, a także kilku
bardziej złożonych efektów, takich jak: głębia ostrości, rozmycie ruchu, specjalna mgła
oraz mgła wolumetryczna.
Dodatkowym celem jest stworzenie bazy materiałów, takich jak: różne rodzaje
metalu, drewno, szkło, kamień.
Ponadto celem jest przystosowanie silnika do pracy w systemie Linux.
Ostatnim celem jest stworzenie przykładowej gry opartej na rozbudowanym silniku.
Praca ma charakter integracyjny z elementami innowacyjności. Opracowany silnik
składa się z wielu małych elementów, które należy zintegrować w jedną spójną całość.
Innowacyjnymi elementami są: system zarządzania efektami, bardziej złożone efekty, takie
jak głębia ostrości, której w ramach niniejszej pracy poświęcono najwięcej czasu, a także
system portali, który jest w całości autorski.
~ 9 ~
2 Architektura
2.1 Założenia sprzętowe
Architektura silnika została zaprojektowana na komputer klasy PC wyposażony
w kartę graficzną zgodną z Shader Model 3. Mogą to być m.in. karty NVidia GeForce
począwszy od serii 6xxx. Niektóre funkcje silnika, takie jak bardziej złożone efekty, będą
wymagały jednak bardziej nowoczesnych kart, obsługujących Shader Model 4, np. karty
GeForce serii 8xxx. Silnik będzie starał się wykorzystać możliwości komputera, na którym
zostanie uruchomiony.
Pozostałe komponenty komputera nie odgrywają tak istotnej roli jak karta graficzna.
Dlatego też, do poprawnego działania wystarczy procesor odpowiadający mocą
procesorowi AMD Athlon 64 3000+, a także przynajmniej 1 GB pamięci RAM.
2.2 Silnik
Opracowana architektura silnika składa się wielu elementów, są to m.in.:
interpreter,
konsola,
zbiór funkcji matematycznych,
warstwa obsługująca system operacyjny,
system obsługi shaderów,
mechanizm przechowywania scen,
mechanizm obsługi materiałów,
mechanizm wczytywania tekstur,
system animacji,
system zarządzania sceną i pamięcią,
mechanizm sterowania światłami i cieniami,
mechanizm wczytywania scen z plików,
mechanizm renderujący,
mechanizm zarządzania buforami,
silnik przetwarzania efektów,
silnik fizyki,
mechanizm efektów cząsteczkowych,
system portali.
Schemat budowy silnika przedstawiono na rysunku 1. Przedstawia on relacje między
poszczególnymi elementami. Każde połączenie między elementami oznacza, że dany
element komunikuje się z drugim elementem lub korzysta z jego mechanizmów.
Ponad wszystkimi elementami znajduje się zarządca. Może się on komunikować ze
wszystkimi elementami składającymi się na silnik.
2.2 Silnik
~ 10 ~
Rys. 1 Schemat budowy silnika.
Dla zwiększenia czytelności nie narysowano wszystkich połączeń do „parametrów”, jednak element ten
łączy się ze wszystkimi innymi.
Parametry silnika są to zmienne, które można umieścić w dowolnym elemencie
silnika, dzięki czemu można je kontrolować za pomocą interfejsu użytkownika oraz
interpretera. Parametry te są dostępne z każdego elementu silnika.
Mechanizm zarządzania sceną odpowiada za przechowywanie sceny, w której
znajdują się obiekty, światła i kamery. Jego zadaniem jest zarządzanie obiektami,
używając mechanizmów podziału przestrzeni. Odpowiada również za zarządzanie
pamięcią, potrzebną do przechowywania obiektów.
Renderer korzysta z mechanizmu zarządzania sceną w celu uzyskania danych
o obiektach, które należy wyrenderować.
Mechanizm zarządzania sceną komunikuje się z silnikiem fizyki w celu przekazania
kontroli nad obiektami do silnika fizyki, a następnie na odczytaniu obliczonego położenia
obiektów z silnika fizyki. Pracuje on równolegle z innymi elementami silnika.
Mechanizm zarządzania buforami używany jest przez efekty, które uzyskują od
niego bufory, do których zlecają renderowanie sceny rendererowi.
Renderer korzysta z mechanizmu zarządzania materiałami w celu uzyskania
informacji o materiałach, których należy użyć do wyrenderowania poszczególnych
obiektów.
2.3 Narzędzia
~ 11 ~
System obsługi shaderów używany jest przez renderer, efekty oraz materiały w celu
wykorzystania shaderów do renderowania. System ten udostępnia mechanizmy do
kompilowania shaderów, przekazywania do nich parametrów i używania shaderów.
Silnik efektów zarządza efektami i wykonuje je w odpowiedniej kolejności.
System portali współpracuje z mechanizmem zarządzania sceną oraz silnikiem fizyki
w celu symulacji zachowania obiektów przechodzących przez portale. Renderer
współpracuje z systemem portali w celu renderowania widoków prze portale.
System efektów cząsteczkowych opiera się na silniku fizyki do symulowania
zachowania cząsteczek. Może dynamicznie zarządzać światłami oraz potrzebuje dostępu
do danych obiektów. Renderer wykorzystywany jest do renderowania cząsteczek,
natomiast efekty mogą zarządzać sposobem ich renderowania.
Konsola oraz graficzny interfejs użytkownika mają kontrolę nad parametrami silnika.
Dodatkowo interfejs ma dostęp do mechanizmu zarządzania sceną, ponieważ umożliwia
manipulację obiektami znajdującymi się na ekranie.
Nadrzędną kontrolę nad działaniem silnika sprawuje interpreter, który interpretuje
skrypt sterujący zachowaniem silnika.
Poszczególne elementy nie są sztywno oddzielone od siebie, wiele z nich jest ze sobą
w bardzo ścisłej zależności. Dla przykładu, system portali jest praktycznie w całości
zintegrowany z mechanizmem zarządzania sceną oraz rendererem.
Diagram przebiegu wykonania dla renderowania poszczególnej klatki przedstawiono
na rysunku 2. Pokazuje on jak długa jest droga od komendy interpretera „go()” do
wyrenderowanej klatki. Wszystkie elementy muszą ze sobą współdziałać.
Diagram nie zawiera wszystkich wywołań jakie zachodzą podczas renderowania
pojedynczej klatki, jednak dobrze oddaje ogólne zależności między elementami oraz
sekwencję przebiegu.
Większość z elementów silnika została opisana w pracy inżynierskiej. Całkowicie
nowymi elementami są silnik fizyki oraz system portali. Elementy wcześniej opracowane
zostały usprawnione, a także przebudowane. Gruntownie rozbudowany został mechanizm
zarządzania sceną, do którego zaimplementowano luźne drzewa ósemkowe. Integracja
z silnikiem fizyki oraz implementacja systemu portali wymagały wielu zmian
w elementach takich jak renderer, mechanizm zarządzania sceną czy efekt renderujący.
W celu przystosowania silnika do działania w systemach Linux oraz Windows
stworzona została warstwa pośrednicząca między właściwym silnikiem a systemem
operacyjnym. Warstwa ta została napisana w dwóch wersjach, oddzielnie dla każdego
systemu. Odpowiada ona za czynności takie jak: inicjowanie aplikacji, tworzenie okna,
obsługa zdarzeń systemowych oraz odczytywanie wejścia, takiego jak wciśnięte klawisze
oraz ruch myszki.
2.3 Narzędzia
Silnik graficzny wymaga dodatkowych narzędzi, z których najważniejszy jest edytor
modeli, scen i animacji. W produkcji gier komputerowych wykorzystywany jest on do
tworzenia modeli występujących w świecie gry, plansz, animacji postaci itp. Wiele
silników ma opracowane własne edytory, jednak stworzenie ich pochłania bardzo
2.3 Narzędzia
~ 12 ~
Rys. 2 Diagram sekwencji dla wyrenderowania pojedynczej klatki.
dużo czasu produkcji. Dlatego też postanowiono użyć dostępnego programu do edycji scen
trójwymiarowych – 3D Studio Max. Aby możliwe było przeniesienie stworzonych scen do
opracowanego silnika wymagane było stworzenie wtyczki do programu 3D Studio Max,
która pozwalałaby na eksport scen.
Opracowana wtyczka, powstała na bazie [24], pobiera dane o budowie sceny oraz
materiałach i zapisuje je w prostym binarnym formacie, który jest odczytywany przez
silnik. Eksporter pobiera informacje o obiektach, światłach, kamerach oraz kościach
w scenie, a następnie je przetwarza. Dla każdego obiektu zapisuje do pliku dane
o współrzędnych wierzchołków, koordynatach tekstur, a także wylicza współrzędne
przestrzeni stycznych wszystkich wierzchołków oraz dla każdego trójkąta wyznacza
indeksy sąsiadujących z nim trójkątów i zapisuje je do pliku. Dodatkowo tworzy wskaźniki
na wierzchołki składające się na ciągi trójkątów za pomocą biblioteki firmy NVidia
NvTriStrip i zapisuje je do pliku. Obiekty będące pod kontrolą systemu kości zawierają
2.3 Narzędzia
~ 13 ~
dodatkowe dane zapisywane do pliku, określające, które kości kontrolują które
wierzchołki. Dla animowanych obiektów, świateł, kamer oraz kości tworzona jest lista
kluczy animacji i zapisywana do pliku. Obiekty mają dodatkowo różne właściwości, które
są zapisywane, takie jak rzucanie cieni, a także ciąg znaków, w którym mogą być zapisane
dowolne informacje. Pole to jest używane do zapisywania nazwy materiału, który ma być
nałożony na dany obiekt, a także parametry fizyczne dla silnika PhysX, takie jak np.
kształt i masa. Dla świateł zapisuje się dane o położeniu, typie oraz promieniu
określającemu zakres światła, a także zmienną określającą czy światło rzuca cienie. Dla
kamer zapisywana jest informacja o położeniu kamery oraz jej celu, czyli punktu,
w kierunku którego kamera jest skierowana, a także o kącie patrzenia. Następnie
zapisywane są dane materiałów, takie jak ścieżki do plików z teksturami.
Kolejnym użytym narzędziem jest edytor graficzny. Obrazy odczytywane przez
silnik w formatach JPG, BMP oraz TGA można tworzyć w dowolnym programie, jednak
jednym z najlepszych formatów dla grafiki trójwymiarowej jest format DDS [25].
Opracowany został przez firmę Microsoft jako format zapisu grafiki rastrowej dla DirectX,
może być jednak używany w silniku graficznym opartym na OpenGL. Format ten
umożliwia zapisanie skompresowanych tekstur w formatach obsługiwanych sprzętowo
przez karty graficzne. Dodatkowo można w nim zapisać mip-mapy dla tekstury. Do zapisu
plików w tym formacie został użyty program NVidia Texture Tools 2 [26]. Do odczytu
plików DDS użyto biblioteki nv_dds firmy NVidia.
Do tworzenia map wektorów normalnych użyto programu NVidia Normal Map
Filter.
~ 14 ~
3 Implementacja
3.1 Drzewo ósemkowe
W opracowanym silniku zastosowano luźne drzewo ósemkowe (ang. loose octree)
[27], ponieważ zapewniają dobrą wydajność zarówno dla przestrzeni otwartych
i zamkniętych. Drzewa takie charakteryzują się lepszą wydajnością niż zwykłe drzewa
ósemkowe, ponieważ lepiej przypisują do swoich komórek małe obiekty.
Działanie drzewa ósemkowego zostanie przedstawione za pomocą drzewa
czwórkowego, gdyż schemat ich działania jest taki sam, z tą różnicą, że drzewo
czwórkowe operuje na płaszczyźnie, a drzewo ósemkowe na przestrzeni trójwymiarowej.
Przykładowe drzewo czwórkowe ilustruje rysunek 3.
Rys. 3 Drzewo czwórkowe. Dla małych obiektów drzewo dzieli się na mały komórki, a duże obiekty
umieszcza w większych komórkach.
Korzeniem drzewa jest kwadrat o wielkości takiej, aby objął wszystkie obiekty.
Przestrzeń jest rekurencyjnie dzielona na kwadraty w taki sposób, że począwszy od
korzenia, każdy kwadrat może dzielić się na cztery mniejsze kwadraty, wpisane w rodzica.
Mniejsze kwadraty mają długość boku równą połowie długości boku rodzica. Każdy
kwadrat (w przypadku drzewa ósemkowego sześcian) jest węzłem. W opracowanej
implementacji węzeł opisany jest klasą COctreeNode.
3.1 Drzewo ósemkowe
~ 15 ~
class COctreeNode
{
public:
CVektor center;
float length;
COctreeNode *children[2][2][2];
COctreeNode *parent;
vector<CObiekt*> objects;
vector<CObiekt*> splitableObjects;
vector<CSwiatlo*> lights;
void MakeChildren();
COctreeNode() {...};
~COctreeNode(){...};
};
Aby stworzyć drzewo należy najpierw określić maksymalną liczba obiektów, które
mogą znajdować się w jednym kwadracie. Następnie należy kolejno dodawać wszystkie
obiekty do drzewa, dodając je do korzenia. Procedurę dodawania do węzła przedstawiono
na następującym listingu:
Procedura DodajObiektDoWęzła(Węzeł, Obiekt)
Jeśli Węzeł.LiczbaObiektów+1 > MAKSYMALNA_LICZBA_OBIEKTÓW
Jeśli istnieją Węzeł.Podwęzły
Jeśli Obiekt zawiera się w całości w którymś Podwęźle
DodajObiektDoWęzła(Podwęzel)
W przeciwnym przypadku
Węzeł.Obiekty.Dodaj(Obiekt)
W przeciwnym przypadku
PodzielWęzeł(Węzeł)
DodajObiektDoWęzła(Węzeł)
W przeciwnym przypadku
Węzeł.Obiekty.Dodaj(Obiekt)
Obiekty dodawane są do najmniejszych kwadratów, w których można je pomieścić.
Jeśli jednak obiekt, choćby był bardzo mały, będzie znajdował się np. na środku kwadratu
korzenia, nie będzie się on w całości mieścił w żadnym dziecku korzenia i zostanie
przypisany do korzenia. Jest to największa wada zwykłych drzew czwórkowych
(i ósemkowych), którą rozwiązują właśnie luźne drzewa.
Luźne drzewa mają rozluźnione granice kwadratów, tzn. nie tylko obiekty, które
zawierają się w całości w danym kwadracie mogą być do niego przypisane, ale mogą być
przypisane także obiekty, które znajdują się w „rozluźnionych” – powiększonych,
granicach. Opracowana metoda używa dwa razy większych granic niż rozmiar komórki.
Przynależność obiektu do rozluźnionej komórki ilustruje rysunek 4.
W przypadku rozluźnionych drzew obiekt może mieścić się w luźnych granicach
więcej niż jednego kwadratu, zostanie wtedy przypisany do najbliższego z nich.
3.1 Drzewo ósemkowe
~ 16 ~
Rys. 4 Biały kwadrat oznacza komórkę, szare pole wokół niego oznacza jego rozluźnione granice.
Obiekty we wszystkich trzech przypadkach należą do komórki.
Na obrazie po prawej stronie mały czerwony obiekt należy do komórki zaznaczonej na biało, ponieważ
na lewo od tej komórki znajduje się o wiele większa komórka, więc lepiej jest przydzielić obiekt do
małej komórki.
Algorytm dodawania obiektów będzie wyglądał następująco:
Procedura DodajObiektDoWęzła(Węzeł, Obiekt)
Jeśli Węzeł.LiczbaObiektów+1 > MAKSYMALNA_LICZBA_OBIEKTÓW
Jeśli istnieją Węzeł.Podwęzły
Wybierz najbliższy Podwęzeł, w którego luźnych granicach mieści się Obiekt
Jeśli brak takiego Podwęzła
Węzeł.Obiekty.Dodaj(Obiekt)
W przeciwnym przypadku
DodajObiektDoWęzła(Podwęzeł)
W przeciwnym przypadku
PodzielWęzeł(Węzeł)
DodajObiektDoWęzła(Węzeł)
W przeciwnym przypadku
Węzeł.Obiekty.Dodaj(Obiekt)
Usuwanie obiektów z drzewa przebiega według następującego algorytmu:
Procedura UsuńObiekt(Węzeł, Obiekt)
Węzeł.Obiekty.Usuń(Obiekt)
Jeśli Węzeł != Korzeń
Połącz(Węzeł)
Pomocnicza procedura łącząca węzły:
Procedura Połącz(Węzeł)
LiczbaObiektów <- 0
Dla Każdego Podwęzła w Wężle
Jeśli istnieją Podwęzeł.Podwęzły
Przerwij procedurę
LiczbaObiektów += Podwęzeł.LiczbaObiektów
Jeśli LiczbaObiektów = 0
Usuń wszystkie Węzeł.Podwęzły
LiczbaObiektów += Węzeł.LiczbaObiektów
Jeśli LiczbaObiektów < MAKSYMALNA_LICZBA_OBIEKTÓW
Jeśli istnieją Węzeł.Podwęzły
3.1 Drzewo ósemkowe
~ 17 ~
Dla Każdego Podwęzła w Wężle
Węzeł.Obiekty.Dodaj(Podwęzeł.Obiekty)
Usuń Podwęzeł
Jeśli Węzeł != Korzeń
Połącz(Węzeł.Rodzic)
Pobieranie obiektów widzianych przez kamerę polega na sprawdzeniu, które
komórki znajdują się w całości lub części w bryle widzenia, a następnie zwróceniu
obiektów przypisanych do tych komórek. Jeśli bryła widzenia kamery nie obejmuje danej
komórki to nie będzie wykonane sprawdzanie, czy któreś z dzieci tej komórki jest
widziane. Dzięki temu możliwe jest szybkie eliminowanie dużej liczby obiektów, które nie
są widziane przez kamerę. Ta sama procedura jest użyta do wyznaczania oświetlonych
obiektów. Jeśli światło jest punktowe to jego bryłą jest kula o promieniu takim, jaki jest
zasięg światła. Jeśli jest to światło reflektorowe (ang. spotlight), to bryłą jest stożek.
Procedura wyznaczania widocznych obiektów opisana jest następująco:
Procedura PobierzWidoczneObiekty(Węzeł, Bryła, Całość)
Obiekty <- Puste
Jeśli Całość = Prawda
Obiekty.Dodaj(Węzeł.Obiekty)
Dla każdego Podwęzła
Obiekty.Dodaj(PobierzWidoczneObiekty(Podwęzeł, Bryła, Prawda))
W przeciwnym przypadku
Jeśli Węzeł w całości w Bryle
Obiekty.Dodaj(Węzeł.Obiekty)
Dla każdego Podwęzła
Obiekty.Dodaj(PobierzWidoczneObiekty(Podwęzeł, Bryła, Prawda))
Jeśli Węzeł częściowo w Bryle
Obiekty.Dodaj(Węzeł.Obiekty)
Dla każdego Podwęzła
Obiekty.Dodaj(PobierzWidoczneObiekty(Podwęzeł, Bryła, Fałsz))
Zwróć Obiekty
Użycie parametru Całość pozwala na pominięcie sprawdzenia każdego z podwęzłów
i pobranie z nich wszystkich obiektów. Właśnie ten zabieg odpowiada za wysoką
wydajność tych drzew.
Implementacja luźnego drzewa ósemkowego znajduje się w plikach octree.h oraz
octree.cpp. Drzewo opisane jest klasą COctree, której listing wygląda następująco:
class COctree
{
public:
CVektor minBound, maxBound;
float len;
int maxObjectsPerNode;
COctreeNode *root;
void MakeOctree(CScene3D *scene, int maxObjectsPerNode=10);
void AddObject(CObiekt *ob);
void AddLight(CSwiatlo *sw);
3.2 Silnik fizyki NVidia PhysX
~ 18 ~
void DeleteObject(CObiekt *ob);
void JoinNodes(COctreeNode *node);
void UpdateObject(CObiekt *ob);
void GetObjectsFrustum(CFrustum *frustum, vector<CObiekt*>
&visibleObjects, vector<COctreeNode*> &visibleNotEmptyNodes);
void GetObjectsLight(CSwiatlo *sw, vector<CObiekt*> &visibleObjects,
vector<COctreeNode*> &visibleNotEmptyNodes);
};
W opracowanej implementacji pobieranie obiektów będących w bryle widzenia
wykonuje metoda GetObjectsFrustum, natomiast pobieranie obiektów będących
w bryle światła metoda GetObjectsLight.
Wygląd przykładowego drzewa ósemkowego przedstawia poniższy rysunek.
Rys. 5 Drzewo ósemkowe w trójwymiarze. Żółte linie przedstawiają linie drzewa.
3.2 Silnik fizyki NVidia PhysX
Do integracji z opracowywanym silnikiem gry został wybrany silnik NVidia PhysX,
ponieważ staje się on bardzo popularnym silnikiem fizyki, używanym w coraz większej
liczbie gier. Jego wykonywalna wersja, a także zestaw narzędzi SDK (ang. source
development kit) są darmowe. Umożliwia on symulowanie zachowania ciał stałych, cieczy,
materiałów takich jak ubrania, lin, silników obrotowych oraz wielu innych. W tej pracy
zostały wykorzystane tylko podstawowe możliwości tego silnika fizyki.
Silnik PhysX opisuje świat za pomocą sceny oraz obiektów – aktorów, w niej
występujących. Obiekty muszą mieć opisany kształt oraz właściwości. Kształt obiektów
może być pudełkiem (ang. box), kulą (ang. sphere), obiektem wypukłym (ang. convex) lub
siatką trójkątów, zwaną również „zupą trójkątów”. Kształt obiektów może być opisany za
pomocą kilku podstawowych kształtów. Ze względów wydajnościowych najlepiej jest, gdy
obiekty będą opisane za pomocą najprostszych kształtów, czyli pudełek oraz kul. W silniku
3.2 Silnik fizyki NVidia PhysX
~ 19 ~
NVidia PhysX nie są obsługiwane kolizje między kształtami typu siatka trójkątów,
ponieważ wymagają zbyt dużego nakładu obliczeń.
Obiekty mogą być statyczne, dynamiczne oraz kinematyczne. Statyczne obiekty raz
stworzone nie poruszają się, ani w żaden sposób nie zmieniają, aż do końca symulacji.
Nadają się doskonale do opisu stałych części planszy, takich jak podłoga, ściany
i wszystkie elementy, które nie mogą być przesuwane. Obiekty dynamiczne są
kontrolowane przez silnik fizyki, co oznacza, że oblicza on ich położenie. Obiekty
kinematyczne nie są poruszane przez silnik fizyki, można je przesuwać tylko ręcznie, tzn.
wywołując odpowiednie polecenia. Obiekty te nadają się jako obiekty, które kontroluje
gracz, takie jak np. postać gracza, która ma oddziaływać z innymi obiektami
dynamicznymi, jednak której ruchy są odgórnie kontrolowane. Symulacja fizyki odbywa
się w krokach. Do silnika PhysX podaje się odstęp czasu, który ma zostać zasymulowany.
Istotnym elementem jest również liczba wewnętrznych iteracji silnika fizyki (ang. solver)
dla każdego obiektu. Im większa liczba iteracji, tym silnik będzie symulował mniejsze
przedziały czasowe dla każdego obiektu i dzięki temu wzrośnie stabilność symulacji.
Stabilność symulacji jest kluczową kwestią przy symulacji zachowań fizycznych.
Przy braku stabilności obiekty zaczynają poruszać się nienaturalnie, skaczą,
przemieszczają się w losowe miejsca lub osiągają w jednej chwili ogromne prędkości, co
jest zwykle nie do przyjęcia. Kiedy obiekt z jakiegoś powodu stanie się niestabilny,
najczęściej drga coraz mocniej, aż w końcu wylatuje ze sceny z wielką prędkością.
W opracowanym silniku obiekty opisane są za pomocą klasy CObiekt, znajdującej
się w plikach obiekt.h oraz obiekt.cpp. Aby obiekty brały udział w symulacji do klasy tej
zostały dodane następujące parametry i metody:
int PhysXControlled;
NxActor *PhysX_Actor;
int PhysX_Movable;
float PhysX_Mass;
// 0 - box, 1 - sphere, 2 - convex, 3 - triangles, 4 - cylinder
int PhysX_Shape;
int PhysX_Static;
string PhysX_Proxy_Name;
bool PhysX_IsProxy;
NxMat34 PhysX_Mat34;
CMatrix PhysX_Matrix;
void SetMovable(bool movable);
void ReleaseShapes();
Zmienna PhysXControlled mówi czy obiekt bierze udział w symulacji.
Obiekt PhysX_Actor jest potrzebny dla silnika PhysX, który w nim przetrzymuje
wszystkie dane opisujące obiekt fizyczny.
Zmienna PhysX_Movable mówi czy obiekt może się poruszać, tzn. czy jest
dynamiczny czy kinematyczny.
3.2 Silnik fizyki NVidia PhysX
~ 20 ~
Zmienna PhysX_Shape określa jaki obiekt ma kształt.
Zmienna PhysX_Static mówi, czy obiekt jest statyczny.
Aby stworzyć scenę w silniku PhysX należy najpierw stworzyć obiekt PhysicsSDK.
Tworzy go procedura NxCreatePhysicsSDK. Kiedy obiekt taki jest utworzony, można
przystąpić do tworzenia sceny. Scena opisana jest obiektem typu NxScene. Scenę tworzy
się metodą createScene obiektu PhysicsSDK.
Skrócony kod tworzenia sceny przedstawiony jest następującym listingu:
NxPhysicsSDKDesc desc;
NxSDKCreateError errorCode = NXCE_NO_ERROR;
PhysicsSDK = NxCreatePhysicsSDK(NX_PHYSICS_SDK_VERSION, NULL,
new ErrorStream(), desc, &errorCode);
NxSceneDesc sceneDesc;
sceneDesc.simType = NX_SIMULATION_SW;
sceneDesc.gravity = NxVec3(0, -9.81, 0);
PhysX_Scene = PhysicsSDK->createScene(sceneDesc);
Gdy scena jest stworzona można przystąpić do tworzenia i dodawania obiektów.
Stworzenie przykładowego pudełka prezentuje następujący listing:
NxBodyDesc bodyDesc;
bodyDesc.angularDamping = 0.5f;
NxActorDesc actorDesc;
NxBoxShapeDesc boxDesc;
boxDesc.dimensions = NxVec3(10, 40, 10);
boxDesc.localPose.t = localPose;
actorDesc.shapes.pushBack(&boxDesc);
actorDesc.body = &bodyDesc;
actorDesc.density = 100.0f;
actorDesc.globalPose.t = NxVec3(0, 0, 0);
PhysX_Actor = PhysX_Scene->createActor(actorDesc);
Obiekty opisane klasą CObiekt posiadają metodę InitPhysX, która tworzy z nich
obiekty fizyczne – aktorów, i dodaje je do sceny.
Aby wykonać krok symulacji należy wywołać metodę simulate, klasy NxScene.
Metoda ta przyjmuje wartość zmiennoprzecinkową, która opisuje długość kroku symulacji,
który ma zostać zasymulowany. Następnie należy wywołać metodę flushStream,
a potem fetchResults, która będzie czekać aż wszystkie obliczenia związane z fizyką
zostaną zakończone.
Ponieważ symulacji fizyki może odbywać się równolegle z innymi czynnościami,
np. z renderowaniem grafiki, dobrze ustawić wywołania metody symulacji oraz innych
czynności silnika w odpowiedniej kolejności. Opisuje to następujący pseudo-kod:
3.2 Silnik fizyki NVidia PhysX
~ 21 ~
Pętla wykonania programu
Pobierz wyniki symulacji (fetchResults)
Symuluj (simulate)
Renderuj scenę
Reszta czynności silnika
Silnik fizyki będzie pracował równolegle z silnikiem graficznym renderującym
scenę, ponieważ metoda simulate tylko inicjuje symulację, która wykonywana jest
w oddzielnym wątku. Jest to istotne, gdyż symulacji fizyki może być wykonywana na
procesorze GPU lub CPU. Jeśli będzie wykonywana na GPU to w tym czasie procesor
CPU może wykonywać inne obliczenia związane z pracą silnika. Jeśli natomiast obliczenia
fizyki wykonywane są na CPU to mogą one być wykonywane na innym rdzeniu niż
pozostałe obliczenia, a także w tym czasie procesor GPU może renderować grafikę.
Renderując obiekty należy teraz uwzględnić ich położenie obliczone przez silnik
fizyki. Macierz przekształcenia pobiera metoda getGlobalPose() obiektu
PhysX_Actor. Aby zastosować tę macierz należy wykonać następujące operacje:
float macierz[16];
ob->PhysX_Actor->getGlobalPose().getColumnMajor44(macierz);
glMultMatrixf(macierz);
Aby ułatwić kontrolę nad obiektami na scenie wprowadzono ułatwienia do interfejsu
użytkownika, który był opracowany w ramach pracy inżynierskiej. Używając myszki
można teraz zaznaczyć dowolny obiekt i przesuwać go po scenie. Wymagało to
zaimplementowania wybierania obiektów.
Wybieranie obiektów zostało zaimplementowane w efekcie renderującym. Po
kliknięciu prawym przyciskiem myszy renderowany jest drugi obraz, który zawiera
16-bitowe, zmiennoprzecinkowe komponenty RGBA, które jednak nie opisują barwy,
tylko numer renderowanego obiektu oraz pozycję każdego wyrenderowanego piksela
w przestrzeni sceny. W miejscu kliknięcia myszką następuje odczytanie piksela pod
kursorem i pobranie z niego numeru obiektu, który znajduje się w tym miejscu na obrazie.
Następnie tworzony jest niewidoczny obiekt, który będzie kontrolowany przez
kursor myszy. Jest on obiektem kinematycznym, a jego położenie jest bezpośrednio
określane przez ruchy myszy. Wybrany obiekt jest łączony z obiektem kontrolowanym
ruchem myszy za pomocą złącza, które będzie trzymało połączone obiekty w stałej
odległości od siebie, jednak będzie pozwalało na dowolne ich obroty.
3.2 Silnik fizyki NVidia PhysX
~ 22 ~
Rys. 6 Wiele obiektów symulowanych przez silnik fizyki.
Kod odpowiedzialny za stworzenie tego łącza prezentuje się następująco:
NxActorDesc actorDesc;
// pozycja piksela w scene
actorDesc.globalPose.t = pickPos.ToNxVec3();
// obiekt kontrolowany ruchem myszki
pickedActor = PhysX_Scene->createActor(actorDesc);
pickedActor->raiseBodyFlag(NX_BF_KINEMATIC);
pickedActor->raiseActorFlag(NX_AF_DISABLE_COLLISION);
NxD6JointDesc d6Desc;
d6Desc.actor[0] = ob->PhysX_Actor; // zaznaczony obiekt
d6Desc.actor[1] = pickedActor;
...
// pozwolenie na dowolne obroty
d6Desc.twistMotion = NX_D6JOINT_MOTION_FREE;
d6Desc.swing1Motion = NX_D6JOINT_MOTION_FREE;
d6Desc.swing2Motion = NX_D6JOINT_MOTION_FREE;
// zablokowanie obiektów w stałej odległości
d6Desc.xMotion = NX_D6JOINT_MOTION_LOCKED;
d6Desc.yMotion = NX_D6JOINT_MOTION_LOCKED;
d6Desc.zMotion = NX_D6JOINT_MOTION_LOCKED;
// stworzenie złącza
pickedJoint = (NxD6Joint*)PhysX_Scene->createJoint(d6Desc);
3.3 Portale
~ 23 ~
Rys. 7 Usprawniony interfejs użytkownika umożliwia przesuwanie i rzucanie obiektami.
3.3 Portale
3.3.1 Wprowadzenie
Z implementacją portali wiąże się wiele zagadnień i problemów, które trzeba
rozwiązać. Aby zaimplementować portale do silnika graficznego, należy rozważyć
następujące zagadnienia: widok przez portal, integracja silnika fizyki z portalami,
oświetlenie a portale. Każde z tych zagadnień wymaga oddzielnego omówienia.
3.3.2 Renderowanie sceny za portalem
Aby wyrenderować scenę z portalami należy najpierw wyrenderować scenę, w której
umieszczone są portale. Następnie w miejscu portalu usunąć obraz oraz informację o głębi
i wyrenderować tam widok przez portal. Będziemy używać do tego bufora szablonu (ang.
stencil buffer) oraz shaderów. Najpierw czyścimy bufor szablonu dowolną wartością,
np. 0. Następnie ustawiamy parametry bufora szablonu tak, by renderowanie obiektów
powodowało ustawianie wartości szablonu na jakąś inną wartość, np. 128. Ustawiamy test
głębokości (ang. depth test) tak, by renderowane były tylko piksele znajdujące się w tej
samej bądź bliższej odległości od obserwatora, co piksele uprzednio będące na danej
pozycji. Następnie renderujemy obiekt portalu. Dzięki temu w buforze szablonu mamy
maskę określającą, gdzie obiekt portalu się znajduje. Teraz renderujemy ten obiekt jeszcze
raz, tym razem wyłączając test głębokości, ale używając testu szablonu tak, by
renderowane były tylko te piksele, które mają wartość szablonu równą 128. Dodatkowo
3.3 Portale
~ 24 ~
Rys. 8 Widok przez portal.
używamy shadera pikseli, który modyfikuje głębię fragmentów na swoim wyjściu na
wielkość największą. W ten sposób w buforze głębokości sceny w miejscu portalu mamy
„pustkę”, tzn. dowolny obiekt wyrenderowany w tym miejscu przejdzie test głębokość
i będzie widoczny. Następnie renderujemy widok za portalem. Przykładowy widok przez
portal pokazany jest na rysunku8.
Aby tego dokonać musimy odpowiednio ustawić kamerę oraz użyć shadera pikseli,
po to, by uciąć renderowane obiekty przed płaszczyzną portalu. Używamy do tego shadera
sprawdzającego czy renderowany piksel znajduje się za płaszczyzną portalu i jeśli tak,
wykonywana jest instrukcja discard, oznaczająca przerwanie renderowania piksela i jego
odrzucenie [28].
Portale opisujemy przez podanie ich płaszczyzny, punktu środkowego, macierzy
obrotu oraz współczynnika skali. Macierz obrotu ma wymiar 3x3 i jej wiersze zawierają
wektory bazowe układu współrzędnych portalu. Są to wektory określające osie X, Y i Z.
Oś Z jest normalną do płaszczyzny portalu. Nazwijmy portal, przez który widok właśnie
renderujemy, portalem wejściowym, a sparowany z nim portal – wyjściowym. Aby
wyrenderować widok przez portal wejściowy należy ustawić pozorną kamerę odpowiednio
za portalem wyjściowym, tak aby patrzyła przez niego. Aby tego dokonać użyjemy
przekształcenia matematycznego opisanego wzorem:
W yjW ejW ejW yj PozPozPozRotRotPoz
))(**(''1
(1)
gdzie Poz’ oznacza położenie pozornej kamery, Rot oznacza macierz obrotu
odpowiedniego portalu, natomiast Rot’ oznacza macierz obrotu z zanegowanym trzecim
wierszem – wektorem normalnym. Poz oznacza bazowe położenie kamery, Poz
z indeksami Wej oraz Wyj oznacza pozycję środka odpowiedniego portalu.
3.3 Portale
~ 25 ~
Mając w ten sposób ustawioną kamerę, możemy przystąpić do renderowania sceny
znajdującej się za portalem.
Przez portal można zobaczyć inny portal. Aby było to możliwe, należy po
wyrenderowanie widoku za portalem powtórzyć całą procedurę raz jeszcze, tym razem
jednak dla widoku przez pierwszy portal. Kolejne zagnieżdżenia portali renderuje się
rekurencyjnie. Portale mogą się też poruszać, ilustruje to poniższy rysunek.
Rys. 9 Poruszający się portal.
3.3.3 Integracja fizyki z mechanizmem portali
Docelowym efektem jest wchodzenie obiektu do jednego portalu i wyłanianie się
z drugiego portalu. Ponieważ portale tworzą swojego rodzaju tunel przestrzenny, który jest
nienaturalny z punktu widzenia fizyki, nie zostały one przewidziane przez autorów
większości silników fizycznych, w tym NVidia PhysX. Dlatego też trzeba opracować
rozwiązania pozwalające na interakcje obiektów z portalami.
Integracja fizyki z mechanizmem portali nie jest zdaniem prostym. W silniku NVidia
PhysX brakuje odległego łączenia obiektów, które by powiązało obiekty przechodzące
przez portal jako jeden obiekt, dlatego też trzeba to robić inaczej. Obiekt przechodzący
przez portal pojawia się w drugim portalu, tak więc trzeba ten „wychodzący” obiekt
stworzyć jako oddzielny obiekt w mechanizmie silnika fizyki.
Można stworzyć kopię obiektu wchodzącego do portalu i umieścić ją odpowiednio
do położenia portali tak, by wychodziła z drugiego portalu. Obiekt wchodzący byłby
w pełni symulowany przez silnik fizyki, natomiast obiekt wychodzący – kopia, byłby
kinematyczny i po każdym kroku symulacji ustawiany w odpowiedniej pozycji. Można
3.3 Portale
~ 26 ~
powiedzieć, że obiekt wchodzący byłby symulowany i kontrolował położenie obiektu
wychodzącego. Ten ostatni byłby tylko obrazem obiektu wchodzącego.
Pozycję i obrót obiektu tworzymy według podobnego wzoru jak przy umieszczaniu
pozornej kamery:
W yjW ejW ejW yj PozPozPozRotRotPoz
))(**(''1
(2)
Natomiast macierz obrotu obiektu obliczamy następująco:
)**(''1
RotRotRotRot W ejW yj
(3)
gdzie Rot’ to wynikowa macierz obrotu, a Rot to macierz obrotu obiektu wejściowego.
Takie rozwiązanie sprawdza się dla mniejszych obiektów, które tylko przelatują
przez portal. Problem pojawia się przy obiektach dłuższych, które zatrzymują się w trakcie
przechodzenia przez portal, tzn. jedna część obiektu jest wystaje z jednego, a druga
z drugiego portalu. Wtedy obiekt, który jest kontrolujący (wchodzący) będzie oddziaływał
z innymi obiektami, natomiast obiekt wychodzący (kopia) będzie nieruchomy. Żadne
uderzenie ani siła przyłożona do obiektu wychodzącego nie spowoduje jego poruszenia,
ponieważ jest on obiektem kinetycznym. Przechodzenie długiego obiektu przez portal
pokazano na rysunku 10.
Rys. 10 Długi obiekt przechodzi przez portal.
3.3 Portale
~ 27 ~
Innym sposobem będzie zamiana kontrolujących obiektów co drugi krok symulacji.
W krokach nieparzystych obiekt wchodzący będzie symulowany, a obiekt wychodzący
będzie kinematyczny i po kroku symulacji zostanie ustawiony w pozycji, którą ma obiekt
wchodzący. W parzystych krokach symulacji będzie na odwrót.
Po każdym kroku symulacji ustawiamy pozycję oraz rotację obiektu, który był
kinetyczny, na podstawie pozycji i rotacji obiektu, który był symulowany, za pomocą tych
samych wzorów, co w sposobie pierwszym. Ponadto musimy ustawić prędkość liniową
i kątową obiektu, który będzie symulowany w następnym kroku.
Prędkość liniową otrzymujemy ze wzoru:
W yjW ejW ejW yj VelVelVelRotRotVel
))(**(''1
(4)
gdzie Vel’ oznacza poszukiwaną prędkość, Vel oznacza prędkość obiektu, który był
symulowany, VelWej i VelWyj oznaczają prędkości portalów wejściowego oraz wyjściowego.
Prędkość kątową otrzymujemy ze wzoru:
)**(''1
AngRotRotAng W ejW yj
(5)
gdzie Ang’ oznacza poszukiwaną prędkość kątową, a Ang oznacza prędkość kątową
obiektu, który był symulowany.
Takie rozwiązanie spowoduje, że obydwa obiekty będą symulowane i będą
oddziaływały z innymi obiektami. Jednak tutaj pojawia się problem stabilności. Odgórne
ustawianie pozycji obiektu w co drugim kroku symulacji powoduje efekt drgania obiektu,
gdyż silnik fizyki nie ma ciągłości w kontrolowaniu obiektów i przez to nie działa tak jak
powinien. Ponadto takie rozwiązanie może powodować ciągłe drżenie obiektu, ponieważ
jeśli na jeden obiekt działa ciągle jakaś siła (np. grawitacji) i w każdym symulowanym
kroku przesuwa obiekt, a drugi nie (bo stoi na ziemi), to co drugą klatkę obiekt będzie
przesuwany raz w jedną, raz w drugą stronę.
Najlepszym sposobem byłoby stworzenie łączenia (ang. joint), które wiązałoby dwa
obiekty tak, by zachowywały się jako jeden. W silniki PhysX występują łączenia, jednak
nie nadają się one do tego zadania. W tym silniku występuje łączenie stałe (ang. fixed),
które łączy dwa obiekty tak, by zachowywały się jako jeden, jednak tylko w tej samej
przestrzeni. Oznacza to, że jeśli jeden obiekt poruszy się np. w lewo, to drugi obiekt
również poruszy się lewo. Nie nadaje się to, niestety, do zastosowania w przypadku
portali, ponieważ portale mogą się obracać. Dogodnym rozwiązaniem byłoby łączenie
portalowe (ang. portal joint) lub łączenie przestrzenne (ang. spatial joint), które
powodowałoby łączenie obiektów, gdzie każdy obiekt miałby swój własny układ
współrzędnych.1
1 Autor wysłał zapytanie do firmy NVidia o wprowadzenie takiego łączenia, jednak do tej pory nie uzyskał
żadnej odpowiedzi.
3.3 Portale
~ 28 ~
Łączenie przestrzenne jest wprowadzone w innych silnikach fizyki np. Newton
Dynamics [22].
3.3.4 Fałszywe kolizje
Gdy obiekt przechodzi przez portal, to z punktu widzenia silnika fizyki nic się z nim
nie dzieje. Dlatego też, obiekty znajdujące się za portalem wejściowym będą kolidowały
z obiektem, który przychodzi przez portal. Silnik fizyki nie posiada informacji o tym, że
część obiektu przeszła przez portal i nie ma jej za portalem. Aby to rozwiązać, trzeba po
każdym kroku symulacji ciąć obiekt płaszczyzną portalu. Ilustracja fałszywych kolizji
znajduje się na poniższym rysunku.
Rys. 11 Fałszywe kolizje.
Na rysunki widać długi, zielony obiekt, przechodzący przez portal oraz mały, sześcienny obiekt,
z którym obiekt przechodzący przez portal nie powinien kolidować.
Cięcie obiektu w kształcie pudełka jest proste, wystarczy sprawdzić które z dwunastu
krawędzi pudełka przechodzą przez płaszczyznę portalu i uciąć je tak, by się kończyły na
płaszczyźnie portalu. Powstaje w ten sposób obiekt wypukły z niewielką liczbą
wierzchołków, który jest bez problemu obsługiwany przez silnik fizyki.
Przecięcie obiektu o kształcie kuli powoduje stworzenie również obiektu wypukłego,
jednak o dużo większej liczbie wierzchołków, zależnie od tego, jak dokładną powierzchnię
kuli chcemy uzyskać.
Natomiast przecięcie obiektu wypukłego wymaga sprawdzenia wszystkich
trójkątów, z których ten obiekt się składa. Jeśli któryś trójkąt znajduje się za płaszczyzną
portalu, jest usuwany. Jeśli w całości przed płaszczyzną, pozostaje bez zmian. Natomiast
jeśli przecina płaszczyznę portalu, musi zostać przecięty. Przecięcie takiego obiektu
wymaga większego nakładu obliczeń.
3.3 Portale
~ 29 ~
3.3.5 Skalowanie portali
Portale mogą mieć różne wielkości. Gdy przeskalujemy macierze obrotu (lub po
przekształceniach zastosujemy odpowiednie skalowanie) otrzymamy przeskalowany układ
współrzędnych danego portalu. Pozwala to na tworzenie portali powiększających lub
pomniejszających. Przykład takiego portalu pokazano na poniższym rysunku.
Rys. 12 Portal przeskalowany. Do małego portalu wchodzi obiekt i z dużego wychodzi powiększony.
Po przeskalowaniu macierzy obrotu, widok przez portal będzie odpowiednio
przeskalowany, ponieważ pozorna kamera będzie umieszczana w odpowiednim miejscu,
symulującym skalowanie. Natomiast aby obiekt przechodzący przez portal został
przeskalowany, trzeba zastosować odpowiednie techniki.
W celu utworzenia drugiego obiektu – wychodzącego, należy go przeskalować
odpowiednio do relatywnej skali portalu, z którego wychodzi. Jeśli obiekt miał kształt
pudełka lub kuli należy tylko zmienić ich rozmiar. Jeśli natomiast miał kształt wypukły,
należy wszystkie wierzchołki przemnożyć przez odpowiedni współczynnik. Również przy
wyświetlaniu obiektu należy uwzględnić, czy został on przeskalowany.
3.3.6 Kamera a portale
Aby kamera przechodziła przez portale, a nie przez nie przenikała bez zachowania
efektu portali, trzeba po każdym ruchu kamery lub portalu sprawdzać, czy punkt poprzedni
kamery był po drugiej stronie portalu, niż jest punkt aktualny. Jeśli tak się stało należy
pozycję kamery przekształcić zgodnie ze wzorem (1).
3.3 Portale
~ 30 ~
3.3.7 Oświetlenie a portale
Światło również może przechodzić przez portale. Same patrzenie przez portal jest
symulacją przechodzenia światła odbitego od obiektów przez portal, jednak oświetlenie
obiektów światłem przechodzącym przez portal wymaga oddzielnego rozpatrzenia.
Aby światło przechodziło przez portal i oświetlało obiekty po drugiej stronie, należy
przy renderowaniu sceny stworzyć pozorne światła – te, których światło wydostaje się
z portalu. Ustawienie pozycji pozornych świateł uzyskamy za pomocą tego samego wzoru,
który był używany do określenia pozycji pozornej kamery (1). Następnie należy dodać
sztuczny cień, w taki sposób, by zasymulować przechodzenie światła przez portal. Ilustruje
to rysunek 13.
Rys. 13 Sztuczny cień. Pozorne światło umieszczane jest za portalem. Obiekty znajdujące się za
płaszczyzną portalu (lewa strona) nie zostaną oświetlone, ani nie będą rzucały cienia (obiekt A).
Natomiast obiekty przed płaszczyzną portalu będą zachowywały się w normalny sposób (B, C).
Sztuczny cień będzie rzucany przez całą płaszczyznę podziału z wyciętym otworem o wielkości samego
portalu.
Innym zagadnieniem jest przechodzenie cieni przez portal. Obiekty znajdujące się
przed portalem, przez który świeci światło, będą rzucały cień, który przejdzie przez portal
i padnie na obiekty po drugiej stronie. Aby tego dokonać należy umieścić pozorne obiekty
za płaszczyzną portalu. Same portale również rzucają cienie, ponieważ światło przechodzi
przez portal i nie trafia w obiekty znajdujące się za nim. Ilustruje to rysunek 14.
3.4 Kubiczne mapy otoczenia
~ 31 ~
Rys. 14 Cienie przechodzące przez portal.
3.4 Kubiczne mapy otoczenia
Jednym z ciekawszych mechanizmów opracowanych w ramach niniejszej pracy jest
ten umożliwiający tworzenie materiałów symulujących odbicia i załamania światła na
powierzchniach zakrzywionych. W grafice komputerowej takie materiały uzyskuje się
często techniką śledzenia promieni (ang. raytracing), jednak technika ta wymaga bardzo
wielu obliczeń. Można jednak symulować takie materiały w inny sposób, mniej dokładny,
jednak wystarczający dla pewnych zastosowań, takich jak gry komputerowe.
Opracowany mechanizm polega na stworzeniu kubicznej mapy otoczenia dla
każdego obiektu posiadającego materia odbijający lub przezroczysty. Mapa taka jest
sześcianem o środku w pozycji środka obiektu, na którego ścianach, stojąc w jego środku,
widać to, co widać patrząc ze środka obiektu.
Po stworzeniu wszystkich map następuje renderowanie poszczególnych obiektów.
Poszczególne mapy otoczenia są przekazywane do shaderów odpowiednich materiałów,
które mogą ich użyć w celu wyznaczenia odbić lub załamań światła na tych obiektach.
Współrzędne tekstury, potrzebne do uzyskania barwy piksela z tekstury kubicznej, są
wektorem trójelementowym, reprezentującym wektor kierunkowy, oznaczający kierunek
ze środka sześcianu. Z miejsca przecięcia wektora z sześcianem odczytywana jest wartość
barwy piksela.
Metoda ta nie jest zbyt dokładna, bowiem nie pozwala na uzyskanie odbić zgodnych
z prawami fizyki, jednak daje wystarczające przybliżenie by uzyskać wrażenie
prawdziwych odbić i załamań. Najlepiej prezentują się obiekty o kształcie kuli,
a największe niedoskonałości ujawniają obiekty, których jeden z wymiarów jest znacznie
większy niż pozostałe, np. długi metalowy długopis.
Wadą tej metody jest to, że na mapie otocznia obiektu będą widoczne tylko inne
obiekty, przez co obiekt sam w sobie nie będzie się odbijał. Ograniczenie to dotyczy
3.4 Kubiczne mapy otoczenia
~ 32 ~
jedynie odpowiednio zakrzywionych obiektów, jednak przy większości zastosowań będzie
to niezauważalne.
Jednym ze sposobów na ukrycie niedoskonałości opracowanej metody, a także
w celu uzyskania bardziej matowych, lecz ciągle odbijających, obiektów, jest rozmycie
mapy otoczenia. Każda z 6 ścian mapy kubicznej jest rozmywana dwuprzebiegowym
filtrem Gaussa, jednak przesunięcie próbek nie jest określane w tekselach na
dwuwymiarowej teksturze, tylko w stopniach obrotu promienia kierunkowego.
Podczas renderowania map otoczenia obiektów, do wyrenderowania innych
obiektów z materiałem odbijającym, potrzebne są mapy otoczenia tych obiektów, a te
dopiero są tworzone. Dla obiektów, które jeszcze w danej klatce nie miały stworzonej
mapy otoczenia, używana jest ich mapa otoczenia z poprzedniej klatki. Taki zabieg
powoduje pewne opóźnienie w renderowaniu odbić drugiego rzędu, jednak w praktyce
opóźnienie to jest zwykle niezauważalne. Zastosowanie map z poprzedniej klatki
zaoszczędza bardzo kosztownego rekurencyjnego renderowania map dla obiektów, które
odbijają się w sobie nawzajem.
Przykładowe materiały stworzone przy pomocy opracowanego mechanizmu
zaprezentowano na poniższych obrazach. Więcej przykładów pokazano w rozdziale 4.6.
3.4 Kubiczne mapy otoczenia
~ 33 ~
3.4 Kubiczne mapy otoczenia
~ 34 ~
Rys. 15 Przykładowe materiały symulujące odbijanie i załamywanie promieni światła.
3.5 Efekty cząsteczkowe
~ 35 ~
3.5 Efekty cząsteczkowe
Efekty cząsteczkowe są bardzo ważnym elementem silnika. Za ich pomocą można
symulować takie zjawiska jak dym, sypiące się iskry, ogień, eksplozje, deszcz lub śnieg,
a także wodę. Zjawiska te dodają realizmu do przedstawianych scen. Iskry mogą być
generowane przy silnym zderzaniu się obiektów ze sobą, co podkreśli siłę uderzenia.
Ogień, eksplozje i dym są nieodłącznymi elementami gier akcji. Efekty cząsteczkowe
mogą być również wykorzystywane do symulacji zachowania wody. Nie tylko
powierzchni wody, ale całej objętości cieczy.
Do symulacji zachowania cząsteczek wykorzystano silnik fizyki. Cząsteczki są
niewidocznymi kulami o małej masie, dzięki czemu nie wpływają na zachowania innych
obiektów. Jest to pożądany efekt, ponieważ cząsteczki przeważnie reprezentują obiekty
bardzo lekkie, nie wpływające na zachowania innych obiektów w scenie, takie jak
cząsteczki dymu czy iskry. Nic jednak nie stoi na przeszkodzie, by nadać im większą
masę.
Cząsteczki nie zderzają się ze sobą, ponieważ reprezentowane są przez kule
w silniku fizyki, a przedstawiają tylko bardzo małe obiekty (jak iskry) lub fragmenty
obiektów gazowych (jak dym). Brak kolizji między cząsteczkami oznacza również
znaczące zwiększenie wydajności, ponieważ cząsteczek jest bardzo wiele i kolizję między
nimi byłyby bardzo obciążające dla silnika fizyki.
Cząsteczki generowane są przez emiter cząstek. W niniejszej pracy emiter jest kołem
o określonym promieniu, umieszczonym w pewnym punkcie sceny i skierowanym
w jakimś kierunku. Wszystkie parametry emitera mogą być animowane w czasie. Emiter
wyrzuca z siebie cząstki w określonych odstępach czasu. Początkowe prędkości i kierunki
cząstek mogą być losowane z określonego zakresu.
Każda cząsteczka opisana jest przez kilka parametrów, takich jak położenie,
wielkość, pozostały czas życia i kolor. Dodatkowo cząstka może być dynamicznym
źródłem światła, wtedy opisują ją dodatkowo parametry takie jak promień światła
i natężenie. Wielkość, promień światła oraz natężenie mogą się zmieniać w trakcie życia
cząstki. Cząstka może rosnąć (np. dla dymu) lub maleć (np. dla iskier).
Emiter opisany jest przez bardzo wiele parametrów, które pozwalają skonfigurować
go w taki sposób, by generował cząstki np. dymu, iskier, ognia.
Renderowanie cząsteczki polega na wyświetleniu obiektu typu „billboard”
w miejscu położenia cząstki. Obiekt tego typu jest płaską teksturą zwróconą zawsze prosto
w stronę obserwatora. Cząsteczki mogą również być renderowane jako trzy kwadraty
o wspólnym środku, ustawione prostopadle do siebie. Taki sposób wyświetlania może
generować lepsze efekty dla wyświetlania dymu.
Implementacja efektów cząsteczkowych znajduje się w plikach particles.cpp oraz
particles.h. Klasę opisująca emiter przedstawia poniższy listing:
3.5 Efekty cząsteczkowe
~ 36 ~
class CParticleEmiter
{
public:
vec3 position; // pozycja emitera
vec3 direction; // kierunek wyrzucania cząsteczek
vec3 upDir; // kierunek wskazujący „górę” emitera
float timeBetweenParticles; // czas pomiędzy wyrzuceniem cząstek
float lifetime; // czas życia cząstek
float radius; // średnica emitera
float timeToEmit;
float angle; // rozpiętość kierunku wyrzucania cząstek
float initialSpeed; // początkowa szybkość cząstek
float speedVariation; // wariacja początkowej szybkości
float initialSize; // początkowa wielkość cząstek
float sizeVariation; // wariacja początkowej wielkości
bool shrink; // określa, czy cząstki mają maleć czy rosnąć
float growRate; // szybkość wzrostu cząstek
bool generateLights; // czy cząstki mają być źródłami światła
vec3 lightsColorMin; // cząstki dostają losowy kolor z zakresu
vec3 lightsColorMax; // pomiędzy Min i Max
bool lightIntensityShrink; // czy natężenie światła ma maleć
float initialAttenuation; // początkowy promień światła
float attenuationVariance; // wariacja początkowego promienia
bool attenuationShrink; // czy promień światła ma maleć
vec3 applyForce; // siła działająca na cząstki
bool sortBlend; // czy sortować cząstki przed rendrowaniem
bool motionBlur; // czy zastosować rozmycie ruchu cząstek
bool perLight; // czy renderować ze światłami
void Setup(vec3 _position, vec3 _direction, vec3 _upDir,
float _radius,
float _timeBetweenParticles, float _lifetime,
float _angle, float _initialSpeed, float _speedVariation,
float _initialSize, float _sizeVariation,
bool _shrink, float _growRate, vec3 _applyForce,
bool _sortBlend, bool _motionBlur, bool _perLight);
void SetupLights(vec3 lightsColorMin, vec3 lightsColorMax,
bool _lightIntensityShrink,
float _initialAttenuation, float _attenuationVariance,
bool _attenuationShrink);
void SetTexture(const char *filename);
void SetMaterial(int _material);
deque<CParticle*> particles; // kontener przechowujący cząstki
CTexture particleTexture; // tekstura cząstek
int material;
CParticleEmiter();
~CParticleEmiter();
void Update(float timeElapsed);
void Render(CSwiatlo *sw);
};
Opis poszczególnych parametrów znajduję na powyższym listingu.
3.5 Efekty cząsteczkowe
~ 37 ~
Poprzez metodę Setup należy określić wszystkie parametry emitera.
Metoda SetupLights ustawia parametry świateł przypisanych do cząsteczek.
Metoda SetTexture ustawia teksturę cząsteczek.
SetMaterial służy do ustawiania materiału, którym będzie używany do
renderowania cząstek.
Update wykonywany jest w każdej klatce i służy do tworzenia nowych cząsteczek,
niszczenia starych, aplikowania sił i zmieniania parametrów cząsteczek.
Metoda Render renderuje cząsteczki.
Klasę opisującą cząsteczkę przedstawia poniższy listing:
class CParticle
{
public:
vec3 position; // pozycja cząstki
float lifetime; // pozostały czas życia
float initialSize; // początkowa wielkość
float size; // aktualna wielkość
vec3 color; // kolor
float initialAttenuation;// początkowy promień światła
NxActor *PhysX_Actor; // aktor silnika fizyki PhysX
CParticleEmiter *emiter;
CSwiatlo *light; // wskaźnik do światła, jeśli cząsteczka
// jest dynamiczym światłem
float toCamDist;
vec3 lastPosition;
CParticle();
~CParticle();
void Update(float timeElapsed);
};
Przykładowe obrazy prezentujące efekty cząsteczkowe przedstawiono na rysunku 16.
3.5 Efekty cząsteczkowe
~ 38 ~
Rys. 16 Efekty cząsteczkowe. Cząsteczki są małymi świecącymi punkami. Każda cząsteczka jest
dynamicznym źródłem światła.
~ 39 ~
4 Efekty i materiały
4.1 Stereoskopia
Obraz generowany przez silnik grafiki trójwymiarowej jest dwuwymiarowy, płaski,
ponieważ jest tylko rzutem perspektywicznym sceny trójwymiarowej na płaską
powierzchnię monitora. Nie daje wrażenia głębi, choć oczywiście człowiek postrzega tę
głębię, wie które obiekty są bliżej, a które dalej, jednak nie odczuwa wrażenia głębi. Dzieje
się tak dlatego, gdyż obraz jest wyświetlany na płaskim ekranie. Istnieją jednak sposoby,
by obraz wyświetlany na płaskim ekranie dawał wrażenie głębi.
Techniką umożliwiającą odbieranie wrażenia głębi jest stereoskopia. Polega ona na
dostarczaniu do każdego oka innych obrazów, tak jak to się dzieje w prawdziwym świecie.
Jedno oko jest przesunięte względem drugiego o kilka centymetrów, dzięki czemu obrazy
trafiające do oczu różnią się od siebie. Należy stworzyć dwa obrazy sceny trójwymiarowej,
w jednym ustawić kamerę w pozycji jednego oka, a w drugim drugiego. Następnie należy
zadbać, by każde oko otrzymało obraz dla niego przeznaczony. Obrazy te będą bardzo
podobne do siebie, jednak będą one zawierały małe różnice i to właśnie one powodują
wrażenie głębi. Dwa obrazy trafiają do mózgu, gdzie składane są w jeden, widziany przez
człowieka, obraz, w taki sposób, że czujemy, które obiekty są blisko, a które daleko.
Rys. 17 Przykładowy obraz stereoskopowy.
Ostatnio wiele filmów jest produkowanych w technologii trójwymiarowej.
Wymagało to stworzenia specjalnych kamer, które byłyby tak naprawdę połączonymi
dwiema kamerami ustawionymi obok siebie. Jednym z najbardziej znaczących filmów dla
technologii 3D w kinie był „Avatar”, dla którego wiele kin zakupiło sprzęt potrzebny do
wyświetlania filmów trójwymiarowych. Równocześnie z premierą tego filmu weszła na
rynek gra o tym samym tytule, również przystosowana do technologii 3D. Jest to jedna
z pierwszych gier, które od początku były tworzone z myślą o stereoskopii.
Zaimplementowanie stereoskopii w opracowywanym silniku nie było trudnym
zadaniem, gdyż wymagało tylko renderowania dwóch obrazów zamiast jednego.
Problemem okazało się dobranie odpowiednich ustawień położenia kamer względem
siebie oraz punktu, na który kamery są skierowane. Ustawienia te mają znaczący wpływ na
4.1 Stereoskopia
~ 40 ~
komfort odbioru obrazu trójwymiarowego. Jednym z największych problemów
związanych z technologią stereoskopii jest właśnie komfort oglądania trójwymiarowych
filmów czy gier, ponieważ wiele osób odczuwa bóle oczu i głowy, zawroty oraz szybkie
zmęczenie podczas oglądania. Dzieje się tak dlatego, że oglądając obraz trójwymiarowy
oczy muszą ciągle pracować, zmieniać ogniskową patrząc na przemian na bliskie i dalekie
obiekty. Zły dobór ustawień kamer powoduje szybsze męczenie, ponieważ wtedy oczy
pracują w sposób nienaturalny. Te różnice w pracy oczu, nawet jeśli bardzo znikome,
powodują wymienione wcześniej skutki uboczne. Nawet przesunięcie kamer względem
siebie o kilka centymetrów lub zmiana kąta między nimi o kilka stopni, może znacząco
polepszyć lub pogorszyć komfort oglądania. Dlatego właśnie dobre ustawienie tych dwóch
parametrów okazało się wyzwaniem. Specjalnie dla filmu „Avatar” została opracowana
kamera rejestrująca obraz stereoskopowy, która miała odległość między obiektywami
równą średniej odległości między oczami ludzkimi, a także zmieniała punkt, na który
skierowane są kamery, tak, by oba obiektywy celowały w obiekt będący w centrum
zainteresowania.
Rys. 18 Różne sposoby ustawienia kamer. Na rysunku po lewej kamery skierowane są równolegle do
siebie, jednak nie jest to najlepsze rozwiązanie, ponieważ oczy ludzkie kierują się na obiekt, który
obserwują. Dlatego lepszym rozwiązaniem jest automatyczne kierowanie kamer na obiekt
zainteresowania, jak pokazano na rysunku po prawej.
Kolejną kwestią w stereoskopii jest sposób dostarczania dwóch obrazów oddzielnie
dla każdego z oczu. Jednym ze sposobów jest użycie specjalnych okularów, które
przefiltrują obraz wyświetlany na ekranie, przepuszczając dla każdego oka inny obraz.
Najprostsze są okulary czerwono-zielone lub czerwono-niebieskie lub cyjanowe,
w których jedno szkło jest czerwone i tym samym przepuszcza tylko czerwony kolor
(kanał R), a drugie zielone, niebieskie lub cyjanowe i przepuszcza tylko kanał G, B lub
4.1 Stereoskopia
~ 41 ~
cyjan. Obraz na ekranie jest wyświetlany w taki sposób, że składowa obrazu dla każdego
oka jest wyświetlana tylko w danym kanale. Rozwiązanie takie było popularne
w telewizyjnych programach, przeważnie dla dzieci, a także w niektórych starszych
filmach kinowych. Rozdzielenie obrazów na kanały powoduje jednak, że tracimy
informację o kolorze, przez co widziany obraz jest szaro-bury.
Lepszym rozwiązaniem jest zastosowanie polaryzacji do oddzielenia obrazów lub
najnowszej technologii filtrowania długości fal światła, która została opracowana przez
firmę Infitec. W pierwszym przypadku obrazy dla lewego i prawego oka są spolaryzowane
prostopadle do siebie, a w okularach zamontowane są szła polaryzacyjne, ustawione tak,
by filtrowały odpowiednie obrazy. Taka technologia jest używana w kinach IMAX.
W drugiej technologii stosuje się różne długości fal światła dla składowych RGB koloru.
Trójkąt odwzorowania koloru dla każdego oka jest trochę inny, co nie przeszkadza
w dostarczeniu do oczu pełnej informacji o kolorze. W okularach zamontowane są szkła,
które filtrują światło o pewnych długościach fali, dzięki czemu do każdego oka trafia tylko
przeznaczony mu obraz. Technologia ta jest używana dziś w większości kin [29].
Technologie te nie są używane w przypadku monitorów komputerowych
i telewizorów, ponieważ wymagają specjalnego sprzętu. Dla monitorów oraz telewizorów
stosuje się technikę używającą okularów migawkowych. Monitor wyświetla dwa razy
więcej klatek niż normalnie i są one naprzemiennie kierowane dla jednego i drugiego oka.
Okulary zasłaniają jedno lub drugie szkło, dzięki czemu obrazy trafiają do odpowiedniego
oka. Taką technologię wspiera m.in. produkt NVidia 3D Vision.
Implementacja opracowana w niniejszej pracy używa wyświetlania czerwono-
niebieskiego obrazu, ponieważ do oglądania wystarczą zwykłe okulary z czerwonym
i niebieskim szkłem. Nic nie stoi jednak na przeszkodzie aby przystosować ją np. do
technologii NVidia 3D Vision.
Widok stereoskopowy został zaimplementowany jako kombinacja efektów: dwóch
efektów „render” oraz specjalnego efektu o nazwie „3D”, który łączy dwa obrazy w ten
sposób, że w obrazie wynikowym umieszcza kanał R pierwszego obrazu oraz kanał B
drugiego obrazu. Dwa efekty renderujące mają parametry określające przesunięcie oraz
obrót kamery w lewo lub prawo. Schemat połączenia efektów przedstawiono na poniższym
rysunku.
Rys. 19 Schemat połączenia efektów dla uzyskania obrazu stereoskopowego.
Istnieje możliwość regulacji odstępu między kamerami. Każdy efekt renderujący
wykrywa następujący po nim efekt „3D”, oraz slot, do którego podpięta jest gałąź
4.2 Głębia ostrości
~ 42 ~
z efektem renderującym. Z efektu „3D” pobierana jest zmienna „rozstaw”, która mówi
o ile efekt renderujący powinien przesunąć kamerę w lewo lub w prawo, w zależności od
tego, do którego slotu jest podpięty (slot o indeksie 0 odpowiada lewemu oku, a o indeksie
1 prawemu oku). Kamery automatycznie ustawiane są tak, by celować w obiekt znajdujący
się na środku ekranu. W każdej klatce wykrywana jest głębokość środkowego punktu
lewego obrazu, i na jej podstawie ustalany jest punkt skupienia kamer w kolejnej klatce.
Przykładowe obrazy stereoskopowe przedstawiono poniżej.
Rys. 20 Obraz stereoskopowy.
Rys. 21 Obraz stereoskopowy przeznaczony do oglądania przez czerwono-niebieskie okulary.
4.2 Głębia ostrości
Symulacja efektów graficznych, które występują w rzeczywistej fotografii nie jest
zadaniem łatwym. Niemniej dzisiejsze karty graficzne posiadają wystarczającą moc, by
4.2 Głębia ostrości
~ 43 ~
generować niesamowite efekty oraz częściowo symulować prawdziwe zjawiska fizyczne
zachodzące w obiektywach aparatów fotograficznych.
Generowanie głębi ostrości zaczyna odgrywać coraz ważniejszą rolę w grafice, m.in.
za sprawą gier komputerowych, które starają się być coraz bardziej realistyczne
i „filmowe”. W filmach głębia ostrości odgrywa wielką rolę, pozwalając sterować obrazem
w taki sposób, by widz skupiał uwagę na konkretnym elemencie sceny bądź bohaterze,
kryjąc elementy nieistotne bądź rozpraszające uwagę, za mgłą rozmycia.
Głębia ostrości jest to parametr używany w optyce, opisujący zakres odległości,
w którym obiekty obserwowane będą ostre, wyraźne. Głębia ostrości w optyce zależy od
ustawienia wielkości przysłony (otworu, przez który wpada światło), a także od rozmiaru
obiektywu (soczewek).
Duża głębia ostrości oznacza, że pole, w którym obiekty są wyraźne jest duże,
natomiast mała oznacza, że tylko obiekty w punkcie skupienia soczewki będą wyraźne,
a reszta będzie rozmyta. Dzieje się tak dlatego, ponieważ soczewka skupia promienie
światła pochodzące od obiektów, nie będących w jej punkcie skupienia, za kliszą bądź
matrycą lub przed. Proces ten ilustruje rysunek 22.
Rys. 22 Schemat powstawania rozmycia poza punktem skupienia. Źródło: Wikipedia.
Nieskończona głębia ostrości jest bardzo prosta do osiągnięcia w grafice, gdyż na
obrazach generowanych komputerowo obiekty od razu są wyraźne, natomiast rozmycie
może być efektem dodatkowym. Interesuje nas zatem mała głębia ostrości, tzn. by obiekty
nie były ostre. Można to osiągnąć nakładając odpowiednie rozmycie na wygenerowany
obraz. Takie nałożenie filtrów na gotowy obraz, a nie wpływanie na sam proces
generowania obrazu, nazywane jest obróbką post-processingu. Najpierw jednak zostanie
przedstawiona metoda generowania dokładnego rozmycia za pomocą wielokrotnego
renderowania sceny.
4.2 Głębia ostrości
~ 44 ~
Rys. 23 Zdjęcie pokazujące rozmycie bliskich obiektów.
Renderujemy wiele obrazów, za każdym razem ustawiając kamerę w różnych
punktach soczewki, tzn. przesuwając kamerę na boki oraz w górę i dół. Pozwala to
symulować wpadanie światła przez różne punkty soczewki. W ten sposób wygenerujemy
bardzo dokładne rozmycie, jednak słaba wydajność tego rozwiązania uniemożliwia jego
stosowanie w grafice czasu rzeczywistego. Wielokrotne renderowanie sceny zmniejsza
wydajność tyle razy, ile próbek będziemy renderować. Dla otrzymania zadowalających
rezultatów potrzeba ok. 100 próbek, co zmniejsza wydajność 100-krotnie, przez co
rozwiązanie to nie może być stosowane na dzisiejszych domowych komputerach.
Uzyskane jednak w ten sposób obrazy posłużą jako obrazy referencyjne dla
opracowywanych rozwiązań.
Przykładowe obrazy uzyskane tą metodą przedstawione są na rysunkach 24 i 25.
Rys. 24 Wzorcowe rozmycie. Na lewym obrazie punkt ostrości jest na najbliższym obiekcie, a na
prawym na najdalszym obiekcie.
4.2 Głębia ostrości
~ 45 ~
Rys. 25 Wzorcowe rozmycie. Punkt ostrości ustawiony na najdalszym obiekcie. Widać poprawne
rozmycie obiektu najbliższego, z zachowaniem szczegółów obiektów leżących za nim.
W metodach post-processingu generujemy obraz, który poddamy rozmywaniu,
a także mapę głębokości, określającą odległości do obiektów znajdujących się na obrazie.
Następnie określamy, na której głębokości (czyli odległości od obserwatora) będzie punkt
ostrości, a co za tym idzie, obiekty znajdujące się za nim jak i przed nim będą rozmyte.
Dalej stosujemy filtr rozmywający na obrazie, biorąc za stopień rozmycia każdego piksela
wartość określoną przy pomocy mapy głębokości.
Proces ten wydaje się prosty, jednak stwarza wiele problemów. Najpierw należy
określić w jaki sposób piksele będą rozmywane. Każdy piksel jest rozmywany za pomocą
pikseli w jego otoczeniu. Wielkość otoczenia ma kluczowy wpływ na wydajność
algorytmu, a także na jakość wynikowego obrazu. Im większe otoczenie, tym większe
rozmycia można uzyskać, jednak tracąc na wydajności.
Promień rozmycia każdego piksela wylicza się na podstawie jego głębokości wg
wzoru:
)( fdf
lm
gdzie m oznacza promień rozmycia, f oznacza rozmiar soczewki, d oznacza głębokość
piksela, f oznacza głębokość ostrości.
Następnie możemy przystąpić do rozmywania obrazu. W tym celu należy
zastosować shader pikseli, który dla każdego piksela obrazu przegląda jego otoczenia
4.2 Głębia ostrości
~ 46 ~
o wielkości proporcjonalnej do promienia rozmycia. Piksele z obszaru otoczenia są
próbkowane i na każdej takiej próbce wykonywane są odpowiednie operacje. Stosując filtr
Gaussa operacje te sprowadzają się do przemnożenia koloru próbki przez odpowiednią
wagę. Jednak przy bardziej złożonych algorytmach operacji na każdej próbce może być
znacznie więcej. Shader realizujący rozmycie Gaussa przedstawia poniższy listing:
struct vertout
{
vec4 Color0 : COLOR0;
vec4 TexCoord0 : TEXCOORD0;
};
vec4 main(
vertout IN,
// obraz wejsciowy RGBA, zawierający kolory RGB oraz głębokość
// w kanale A
uniform sampler2D obrazWejsciowy : TEXUNIT0,
uniform vec blur_moc,
uniform vec blur_size,
uniform vec noise_size,
uniform vec noise_rand
) : COLOR0
{
vec4 color = 0;
vec baseDepth;
vec2 pixelSizes =
vec2(1.0/MAIN_SCREEN_RES_X, 1.0/MAIN_SCREEN_RES_Y);
// głębokość piksela, 0 oznacza najbliżej obserwatora (największe
// rozmycie), 0.5 oznacza punkt skupienia (brak rozmycia),
// 1 oznacza najdalej od obserwatora (największe rozmycie)
baseDepth = s4tex2D(obrazWejsciowy, IN.TexCoord0.xy).w;
// moc – promień rozmycia
vec baseMoc = abs(baseDepth*2-1);
vec radius = baseDepth * 2 - 1;
radius *= blur_size;
vec2 texSize = pixelSizes.xy*radius;
vec4 cHigh;
vec4 cLow;
vec l;
vec4 c;
vec moc;
const int SAMPLES = 100;
// próbki to wektory 3-elem: x, y i waga próbki z rozkładu gaussa
const vec3 samples[100] = {...};
for (int i=0; i<SAMPLES; i++)
{
c = s4tex2D(obrazWejsciowy, IN.TexCoord0.xy +
samples[i].xy)*texSize);
4.2 Głębia ostrości
~ 47 ~
color.xyz += c.xyz * samples.z;
color.w += samples.z;
}
return float4(color.rgb/color.w, 1);
}
Efekt zastosowania rozmycia z filtrem Gaussa przedstawiono na rysunku 26.
Rys. 26 Zastosowanie rozmycia Gaussa. Widać efekt „wyciekania” koloru wyraźnych obiektów
(czerwona kula).
Jak widać na powyższym rysunku powstały efekty „wyciekania” koloru obiektu
znajdującego się w polu ostrości. Dzieje się tak ponieważ algorytm rozmywania nie bierze
pod uwagę głębokości pikseli, a co za tym idzie piksele, które znajdują się za polem
ostrości (są tłem), będą rozmywane również przez piksele będące w polu ostrości.
Efekt „wyciekania” można zminimalizować sprawdzając głębokość pikseli podczas
rozmywania w taki sposób, że piksele, które znajdują się bliżej i nie są rozmyte w takim
stopniu jak rozmywany piksel, nie będą na niego wpływały. Próbkując otoczenie
rozmywanego piksela możemy każdej próbce przypisać wagę równą stopniowi rozmycia
danej próbki. Podejście takie zredukuje efekt „wyciekania”, ponieważ obiekty będące
w polu ostrości będą miały stopień rozmycia równy zeru. Wynik zastosowania tej redukcji
przedstawia rysunek 27.
4.2 Głębia ostrości
~ 48 ~
Rys. 27 Zastosowanie redukcji „wyciekania”.
Na przedstawionym rysunku widzimy teraz inny problem – obiekty znajdujące się
przed punktem skupienia są rozmyte, jednak mają ostre krawędzie. Dzieje się tak dlatego,
ponieważ operujemy na obrazie wejściowym, który jest cały wyraźny i nie możemy
zobaczyć tego, co jest za tymi obiektami, a właśnie to powinniśmy zobaczyć rozmywając
ich krawędzie. Wynik jaki powinnyśmy zobaczyć ilustruje rysunek 28, uzyskany metodą
wielokrotnego renderowania.
Rys. 28 Poprawne rozmycie bliskiego obiektu.
4.2 Głębia ostrości
~ 49 ~
Jest to największe ograniczenie algorytmów operujących na jednym wyraźnym
obrazie wejściowym. Wydaje się, że nie jest możliwe rozwiązanie tego problemu mając
tylko jeden wyraźny obraz wejściowy.
Można zatem dostarczyć kilka obrazów, tzw. warstw, np. obraz z obiektami
znajdującymi się przed punktem skupienia oraz drugi z obiektami za i w punkcie
skupienia. Oba te obrazy rozmywane będą oddzielnie. Drugi obraz rozmyty zostanie w taki
sam sposób jak w pierwotnym sposobie, ponieważ będzie on zawierał tylko obiekty będące
w polu skupienia oraz za nim. Pierwszy obraz, zawierający obiekty przed punktem
skupienia, będzie rozmyty w podobny sposób, jednak dodatkowo będzie uwzględnione
rozmywanie kanału alfa – przezroczystości. Dzięki temu krawędzie obiektów zostaną
rozmyte. Następnie obrazy zostaną połączone w taki sposób, że na obraz drugi zostanie
nałożony obraz pierwszy, uwzględniając kanał alfa pierwszego obrazu.
Rozwiązanie to sprawdzi się tylko wtedy, gdy żadne obiekty nie będą na tyle długie,
by ich części znalazły się na dwóch obrazach. W takim przypadku miejsce przecięcia
obiektu będzie widoczne na obrazie wynikowym i nie będzie dobrze się prezentowało.
Problem ten był rozpatrywany przez wielu badaczy, co zaowocowało wieloma
rozwiązaniami, jednak jednym z najlepszych z nich jest praca [30]. Przedstawiono w niej
sposób generowania obrazu używający dwóch obrazów. Pierwszym jest ostry obraz
wejściowy, drugi (tzw. ukryty) generowany jest poprzez renderowanie sceny jeszcze raz,
tym razem ustawiając odpowiednio test głębokości. Ustawiany jest on w taki sposób by
odrzucane były piksele będące tłem (będące za punktem ostrości), mające tę samą
głębokość co w warstwie pierwszej oraz te, które na pewno będą niewidoczne. To, czy
piksel będzie na pewno niewidoczny sprawdza się porównując głębię pikseli w jego
otoczeniu. Jeśli różnica w głębi jest odpowiednio mała oznacza to, że piksel nie będzie
odsłonięty. Duża różnica w głębokości oznacza, że piksel znajduje się blisko krawędzi,
która może być rozmyta, a zatem piksel ten może zostać odsłonięty.
Następnie te dwa obrazy są rozmywane za pomocą powiększania pikseli (ang. point
splatting) w taki sposób, że każdy piksel rozmytego obrazu zostaje przydzielony do jednej
z trzech warstw w zależności od głębokości piksela i jego otoczenia. Dalej warstwy te są
łączone, co przy odpowiednim doborze parametrów daje zadowalające rezultaty. Algorytm
ten dobrze radzi sobie z rozmywaniem obiektów znajdujących się przed punktem
skupienia, odsłaniając to co jest za nimi, a także z długimi obiektami, które przechodzą
przez wszystkie trzy obszary: rozmyty przed punktem skupienia, ostry w punkcie
w skupienia, rozmyty za punktem skupienia.
Dla potrzeb gry można jednak zrezygnować z poprawnego rozmywania obiektów
znajdujących się blisko. Dlatego autor zdecydował się na opracowanie najbardziej
atrakcyjnego wizualnie efektu głębi ostrości, cechującego się dobrą wydajnością.
Aby osiągnąć „filmowy” efekt głębi ostrości należy rozpatrzyć tzw. efekt
soczewkowy lub efekt „bokeh”, który powoduje, że jasne punkty rozmywane są w duże
i jasne figury geometryczne, takie jak pięciokąt lub sześciokąt foremny bądź koło.
4.2 Głębia ostrości
~ 50 ~
Rys. 29 Jasne punkty tła zostały rozmyte w jasne plamy w kształcie kół.
Aby osiągnąć taki efekt należy zmodyfikować filtr rozmywający tak, aby za
otoczenie piksela rozmywanego brał obszar o kształcie figury jaką chcemy uzyskać na
rozmyciu. Aby osiągnąć dobry efekt musimy użyć bardzo wielu próbek z sąsiedztwa,
w praktyce powyżej stu. Wynik działania przykładowego filtru prezentuje następujący
rysunek:
Rys. 30 Zastosowanie rozmycia z próbkami rozmieszczonymi wg maski.
4.2 Głębia ostrości
~ 51 ~
Maska rozmieszczenia próbek wyglądała następująco:
Rys. 31 Rozmieszczenie punktów wg maski.
W celu osiągnięcia efektu wyraźniejszych krawędzi można zwiększyć liczby próbek
na krawędzi maski. Po takim zabiegu maska może wyglądać następująco:
Rys. 32 Rozmieszczenie punktów wg maski z większą ilością próbek na krawędzi.
Rozmieszczenie próbek na kształcie maski odbywa się automatycznie na podstawie
bitmapy maski. Najpierw znajdowany jest brzeg figury przedstawionej na bitmapie,
następnie wybierane są punkty leżące wzdłuż brzegu w równej odległości od siebie. Dalej
w miejscu wszystkich punktów brzegowych stawiane są czarne koła (usuwanie maski)
o promieniu równym odstępowi między wybieranymi punktami, i znów następuje
wyszukanie brzegu i rozmieszczenie punktów. Operacja ta jest wykonywana do momentu,
gdy cała maska zostanie usunięta.
Istotnym pomysłem, który znacząco wpłynął na jakość efektu soczewkowego jest
ważenie próbek za pomocą ich jasności. Jasność próbki określamy wg następującego
wzoru:
bbrl 11.059.03.0
gdzie l oznacza jasność, a r, g i b składowe RGB piksela.
Stosując wagę próbki równą l uzyskujemy zwiększenie udziału jasnych pikseli
w obrazie, co daje bardzo dobry wynik. Możemy również jako wagę próbki używać potęgi
l, co da jeszcze większy udział jasnych pikseli w rozmyciu. Zastosowanie jako wagi l oraz
l2 pokazano na rysunku 33.
4.2 Głębia ostrości
~ 52 ~
Rys. 33 Na lewym rysunku zastosowano wagę l, a na prawym l
2.
Punkty maski również mogą mieć swoje wagi dla każdej składowej RGB, co
pozwala na stosowanie dowolnych obrazów jako maski, dzięki czemu możliwe jest
uzyskiwanie ciekawych rezultatów. Wynik zastosowania kolorowych masek
przedstawiono na rysunku 34.
Rys. 34 Zastosowanie kolorowych masek.
Jak widzimy na powyższych rysunkach opracowany sposób pozwala na generowanie
atrakcyjnych wizualnie obrazów, mających „filmowy” wygląd.
4.2 Głębia ostrości
~ 53 ~
Shader pikseli implementujący wszystkie wyżej wymienione metody różni się od
poprzednio przedstawionego o zawartość pętli oraz kilka dodatkowych instrukcji.
// próbki to wektory 2-elem (x, y) oraz wagi RGB
const vec2 samplesXY[SAMPLES] = {...};
const vec3 samplesRGB[SAMPLES] = {...};
// sumator wag RGB
vec3 ca = 0;
for (int i=0; i<SAMPLES; i++)
{
c = s4tex2D(obrazWejsciowy, IN.TexCoord0.xy +
samplesXY[i].xy)*texSize);
moc = abs(c.a*2-1);
l = 0.3*c.r + 0.59*c.g + 0.11 * c.b;
c.a *= moc * (l*l + 0.01)
color.xyz += c.xyz * c.a * samplesRGB[i];
ca.rgb += c.a * samplesRGB[i];
}
return float4(color.rgb/ca.rgb, 1);
Zastosowanie stu próbek maski pozwala na uzyskanie promienia rozmycia
o długości ok. 10 pikseli, ponieważ w przypadku użycia większego promienia ujawni się
struktura maski, co prezentuje rysunek poniżej.
Rys. 35 Zbyt duże rozmycie powoduje ujawnienie się punktów maski.
4.2 Głębia ostrości
~ 54 ~
Aby temu zapobiec zastosowano dwa obrazy wejściowe, jeden oryginalny, a drugi
rozmyty filtrem Gaussa o odpowiednim promieniu. Rozmyty obraz jest używany gdy
promień nakładanej maski przekracza wielkość ok. 10 pikseli. Dobranie odpowiedniego
momentu przejścia między ostrym i rozmytym obrazem jest kluczowe dla jakości
końcowego obrazu.
Finalny shader przedstawia następujący listing:
// próbki to wektory 2-elem (x, y) oraz wagi RGB
const vec2 samplesXY[SAMPLES] = {...};
const vec3 samplesRGB[SAMPLES] = {...};
// sumator wag RGB
vec3 ca = 0;
for (int i=0; i<SAMPLES; i++)
{
cHigh = s4tex2D(obrazWejsciowy, IN.TexCoord0.xy +
samplesXY[i].xy)*texSize);
cLow = s4tex2D(obrazRozmyty, IN.TexCoord0.xy +
samplesXY[i].xy)*texSize);
moc = abs(cHigh.a*2-1);
// wartości dobrane do rozmiaru ekranu i maski oraz promienia
// rozmycia
l = saturate((moc - 0.10) * 9);
c = lerp(cHigh, cLow, l);
l = 0.3*c.r + 0.59*c.g + 0.11 * c.b;
c.a *= moc * (l*l + 0.01)
color.xyz += c.xyz * c.a * samplesRGB[i];
ca.rgb += c.a * samplesRGB[i];
}
return float4(color.rgb/ca.rgb, 1);
Opracowane rozmycie przedstawiono na kolejnych rysunkach.
4.2 Głębia ostrości
~ 55 ~
4.2 Głębia ostrości
~ 56 ~
Rys. 36 Rozmycie soczewkowe. Jasne punkty rozmywane są w pięciokąty foremne.
4.3 Rozmycie ruchu
~ 57 ~
Kolejną istotną kwestią jest wydajność. Mogłoby się wydawać, że powyższy kod
zostanie skompilowany przez kompilator dostarczony przez producenta karty graficznej
w sposób najbardziej optymalny. Jednak (z niewiadomych przyczyn) kompilator ten
potrafi skompilować identyczny kod do postaci, które różnią się szybkością wykonania o
rząd wielkości. Najbardziej optymalna wersja shadera, poza oczywistym
zoptymalizowaniem kodu według podstawowej wiedzy programistycznej, została
opracowana metodą prób i błędów, która pozwoliła wskazać błędy kompilatora języka CG
w wersji 2.2.0017 lub sterowników NVidii w wersji 257.21.
Jednym ze znalezionych błędów było bardzo powolne wykonywanie
skompilowanego kodu, który zawierał następującą operację wykonywaną dla każdej
próbki:
c.a = (c.a < baseDepth) ? 1.0 : saturate(c.a*2-1);
Gdy kod został przepisany do następującej postaci:
if (c.a < baseDepth)c.a = 1.0;
else c.a = saturate(c.a*2-1);
był wykonywany 5 razy szybciej. Te dwie wersje kodu mają identyczne działanie, jednak
z niewiadomego powodu druga z nich wykonuje się znacznie szybciej.
Najważniejszą optymalizacją było ręczne rozwinięcie pętli. Wykonano to za pomocą
procedury w języku C++ generującej kod języka CG dla każdej próbki z maski.
Przyspieszyło to działanie programu z 10 klatek na sekundę do 55!
W przyszłych pracach można zastosować metody opisane w pracy [30] w celu
poprawy wyglądu rozmycia obiektów znajdujących się blisko obserwatora.
4.3 Rozmycie ruchu
Rozmycie ruchu jest jednym z „filmowych” efektów, który daje bardzo atrakcyjne
wizualnie wyniki. Może on służyć również do stworzenia wrażenia płynności ruchu, gdy
liczba klatek na sekundę jest niewystarczająca do płynnej animacji.
Rozmycie ruchu powstaje na klatce filmowej gdy w czasie ekspozycji obiekt będzie
się poruszał. Ponieważ obiekt będzie zmieniał swoją pozycję, w każdej chwili czasu
zostawi on swój ślad w innym miejscu klatki filmowej.
Efekt ten można uzyskać na kilka sposobów. Najprostszym jest wyrenderowanie
kilku lub kilkunastu klatek animacji, w zwolnionym tempie, i złączeniu ich w jedną.
Uzyskane w ten sposób klatki będą zawierały rozmycie ruchu, w którego jakość zależna
jest od liczby klatek składających się na jedną wynikową klatkę.
Innym sposobem jest zastosowanie post-processingu. Wyrenderowany obraz
zostanie rozmyty w taki sposób, by oddać kierunek ruchu obiektów. Wymaga to
przygotowania dodatkowego obrazu, który zawierał będzie dwuwymiarowy wektor ruchu
każdego piksela na ekranie. Obraz taki uzyskuje się renderując scenę ze specjalnym
shaderem, który dostaje na wejściu, oprócz współrzędnej położenia renderowanego
piksela, współrzędną położenia piksela w poprzednio renderowanej klatce. Shader ten
4.3 Rozmycie ruchu
~ 58 ~
odejmuje te dwie współrzędne i uzyskuje w ten sposób wektor ruchu. Kod shadera
wierzchołków przedstawia poniższy listing:
struct vertout
{
float4 HPosition : POSITION;
float4 OldPosition : TEXCOORD0;
};
vertout main(appin IN,
// macierz przekształcenia danej klatki
uniform float4x4 ModelViewProj,
// macierz przekształcenia poprzedniej klatki
uniform float4x4 prevModelaViewProj)
{
vertout OUT;
float4 position = float4(IN.Position.xyz, 1);
// pozycja renderowanego piksela na ekranie
float4 HP = mul(ModelViewProj, position);
// pozycja tego piksela w poprzedniej klatke
float4 HPprev = mul(prevModelaViewProj, position);
OUT.HPosition = HP;
OUT.OldPosition = HPprev;
return OUT;
}
Kod shadera pikseli przedstawia następujący listing:
struct vertout
{
vec4 Position : POSITION;
vec4 OldPosition : TEXCOORD0;
};
vec4 main(vertout IN) : COLOR0
{
// w wektorach a i b zapisane będą współrzędne pikseli na ekranie
// w zakresie (0, 1)
float2 a = IN.OldPosition.xy;
a /= IN.OldPosition.w;
a = a * 0.5 + 0.5;
float2 b = IN.Position.xy;
b /= IN.Position.w;
b = b * 0.5 + 0.5;
float2 velocity = (b - a);
velocity.x *= MAIN_SCREEN_RES_X;
velocity.y *= MAIN_SCREEN_RES_Y;
// przycięcie wektora do zakresu (-128, 128) i zapisanie w formie
// koloru w składowych RG
velocity = clamp(velocity / 128 + 0.5, 0, 1);
return float4(velocity, 1, 1);
}
4.3 Rozmycie ruchu
~ 59 ~
Następnie należy rozmyć wejściowy obraz używając mapy ruchu. W ramach
niniejszej pracy opracowano w tym celu shader, który dla każdego piksela, próbkuje
piksele leżące w kierunku ruchu rozmywanego piksela. Kod shadera pikseli przedstawia
następujący listing:
struct vertout
{
vec4 TexCoord0 : TEXCOORD0;
};
vec4 main(vertout IN,
uniform sampler2D obrazWejsciowy : TEXUNIT0,
uniform sampler2D mapaRuchu : TEXUNIT1,
// rozmiar rozmycia
uniform float size) : COLOR0
{
vec2 b = IN.TexCoord0.xy;
// wektor rozmycia
vec2 vel = (s2tex2D(mapaRuchu, b).xy - 0.5) * 128;
vel.x /= MAIN_SCREEN_RES_X;
vel.y /= MAIN_SCREEN_RES_Y;
vel *= size;
vec3 c;
vec2 v;
vec4 color = 0;
vec l;
const int SAMPLES = 20;
for (vec i=0; i<=SAMPLES; i++)
{
vec t = i / SAMPLES;
// próbki leżą na wektorze ruchu
c = s3tex2D(obrazWejsciowy, b + vel * t).rgb;
// próbki leżące dalej od rozmywanego piksela mają
// mniejszy wpływ w rozmywaniu
color.rgb += c * (1.0 - t);
color.a += (1.0 - t);
}
color.rgb /= color.a;
return vec4(color.rgb, 1);
}
Przykładowe obrazy uzyskane za pomocą tego rozwiązania przedstawiono na
rysunkach 37 i 38.
4.3 Rozmycie ruchu
~ 60 ~
Rys. 37 Rozmycie podczas ruchu kamery.
Rys. 38 Rozmycie poruszającego się obiektu.
Aby osiągnąć bardziej „filmowy” efekt, można zastosować, podobnie jak w efekcie
głębi ostrości, ważenie próbek za pomocą ich jasności. Podejście takie uzasadnione jest
faktem, że jaśniejsze fragmenty obrazu z reguły mocniej się utrwalają na ekranie. Aby
zastosować to ważenie należy zmodyfikować pętlę w kodzie shadera w sposób
następujący:
4.3 Rozmycie ruchu
~ 61 ~
for (vec i=0; i<=SAMPLES; i++)
{
vec t = i / SAMPLES;
// próbki leżą na wektorze ruchu
c = s3tex2D(obrazWejsciowy, b + vel * t).rgb;
l = 0.3*c.r + 0.59*c.g + 0.11*c.b + 0.05;
l = l*l * (1.0 - t);
color.rgb += c * l;
color.a += l;
}
Poniżej przedstawiono porównanie rozmycia ruchu z zastosowaniem ważenia po
jasności oraz bez ważenia próbek.
Rys. 39 Porównanie rozmycia bez ważenia próbek po jasności (po lewej) i z ważeniem (po prawej).
Przykładowe obrazy pokazujące opracowane rozmycie ruchu w działaniu
przedstawiono na kolejnych rysunkach.
4.3 Rozmycie ruchu
~ 62 ~
Rys. 40 Przykładowe obrazy prezentujące rozmycie ruchu.
4.4 Mgła
~ 63 ~
4.4 Mgła
Mgła jest bardzo ważnym zjawiskiem, ponieważ w prawdziwym świecie występuje
niemal wszędzie. Nawet na zdjęciach wykonanych w bardzo pogodny dzień widać lekką
niebieską mgłę na oddalonych obiektach. Mgła dodaje realizmu do generowanej grafiki,
a umiejętne posługiwanie się mgłą daje wielkie możliwości w kontrolowaniu nastroju, jaki
udziela się oglądającemu obraz, czy klimatu w filmie lub grze komputerowej.
Symulacja mgły jest możliwa prawie od początków grafiki trójwymiarowej w czasie
rzeczywistym. Początkowo mgła była używana do ograniczania widoczności w celu
zwiększenia wydajności renderowania. Dziś również w niektórych grach, jak np. „World
of Warcraft”, spotkać można mgłę służącą do ograniczania pola widzenia w celu
zmniejszenia ilości renderowanej geometrii.
Najprostszym sposobem generowania mgły jest mieszanie kolorów na
wygenerowanym obrazie z ustalonym kolorem mgły, którego natężenie rośnie wraz ze
wzrostem odległości od obserwatora. Natężenie koloru mgły może następować liniowo
wraz ze wzrostem odległości – mówimy wtedy o mgle liniowej – lub wykładniczo –
mówimy wtedy o mgle wykładniczej. Mgła liniowa wymaga określenia początku mgły
oraz końca fazy przejścia. Obiekty przed początkiem nie będą zmieszane z kolorem mgły,
natomiast obiekty za końcem fazy przejścia będą zastąpione całkowicie kolorem mgły.
Mgła wykładnicza natomiast wymaga określenia gęstości i wygląda o wiele naturalniej.
Natężenie mgły wykładniczej oblicza się ze wzoru:
*1 def
gdzie d to odległość od obserwatora, a δ to gęstość mgły.
Taka mgła wygląda dobrze i dlatego jest bardzo szeroko stosowana. Jednak
obserwując prawdziwą mgłę można dostrzec, że tworzy ona wiele innych efektów
wizualnych, których ten podstawowy model nie obejmuje.
Przyjrzyjmy się fotografiom przedstawiającym prawdziwą mgłę (Rys. 41). Widać na
nich, że lampy rozświetlają obszar wokół siebie. Dzieje się tak dlatego, że cząstki mgły
odbijają do obserwatora pewną część światła generowanego przez lampę. Drugą istotną
cechą, którą widać na fotografiach jest to, że jasne obiekty, takie jak lampa, przebijają się
przez mgłę o wiele bardziej skutecznie. Przyglądając się prawdziwej mgle możemy
również dostrzec, że jasne obiekty przebijające się przez mgłę będą niewyraźne, rozmyte.
Właśnie te cechy mgły zostały rozpatrzone i zasymulowane w rozwiązaniu
opracowanym w ramach niniejszej pracy.
Efekt rozświetlenia wokół punktowych źródeł światła został osiągnięty przez
dodanie flar. Są one teksturami nałożonymi na wygenerowany obraz w miejscu źródła
światła. Tekstura flary pokazana jest na rysunku 42.
Flary są obiektami typu „billboard”, co oznacza, że zawsze są zwrócone w stronę
obserwatora. Tekstura flary jest barwiona kolorem światła oraz jego intensywnością,
a rozmiar obiektu flary jest dopasowany do promienia światła. Dodatkowo, flary
renderowane są za pomocą specjalnego shadera, który symuluje „kulistość” flary, tzn. jeśli
flara będzie renderowana za jakimś obiektem, to zostanie odpowiednio przyciemniona.
4.4 Mgła
~ 64 ~
Rys. 41 Zdjęcia prawdziwej mgły.
Rys. 42 Tekstura flary.
4.4 Mgła
~ 65 ~
Efekt zastosowania flar przedstawia poniższy rysunek.
Rys. 43 Działanie flar. Po lewej flary włączone, po prawej wyłączone.
Rozmycie obiektów widocznych przez mgłę zostało osiągnięte przy użyciu rozmytej
wersji wejściowego obrazu. Rozmywany obraz jest mieszany z kolorem mgły by powstało
tzw. tło, w taki sposób, by jasne obiekty bardziej przebijały się przez mgłę niż obiekty
ciemne. Realizuje to następujący shader pikseli:
struct vertout
{
vec4 TexCoord0 : TEXCOORD0;
};
vec4 main(vertout IN,
uniform sampler2D obraz : TEXUNIT0,
uniform sampler2D mapaGlebokosci : TEXUNIT1,
uniform vec3 color,
uniform vec ambient,
uniform vec density
) : COLOR0
{
vec3 tlo = tex2D(obraz, IN.TexCoord0.xy);
vec dep = -tex2D(mapaGlebokosci, IN.TexCoord0.xy).a;
// wyliczanie jasności
float l = clamp(0.3*tlo.r + 0.59*tlo.g + 0.11*tlo.b - 0.05, 0, 1);
// kwadrat jasności
l = l*l;
// ciemnienie z odlegloscią
// składnik (1.0-l) odpowiada za mocniejsze przebijanie się
// jasnych obiektów
float moc = exp(-(dep/100)*density*0.25*(1.0-l));
// rozjasnianie z oglegloscią
moc *= 1.0-exp(-((dep)/100)*density);
vec3 res = (tlo*moc + float3(ambient, ambient, ambient)) * color;
return float4(res, 0);
}
4.4 Mgła
~ 66 ~
Następnie na obraz wejściowy nakładana jest mgła wykładnicza, za kolor mgły
przyjmowane jest tło. Również tutaj zastosowano dodatkową modyfikację pozwalającą by
jasne obiekty bardziej przebijały się przez mgłę. Kod shadera pikseli wykonującego
tę operację przedstawia następujący listing:
struct vertout
{
vec4 TexCoord0 : TEXCOORD0;
vec4 TexCoord1 : TEXCOORD1;
};
vec4 main(vertout IN,
uniform sampler2D obraz : TEXUNIT0,
uniform sampler2D mapaGlebokosci : TEXUNIT1,
uniform sampler2D tlo : TEXUNIT2,
uniform vec density
) : COLOR0
{
vec2 a = IN.TexCoord1.xy;
a /= IN.TexCoord1.w;
a = a * 0.5 + 0.5;
vec dep = -tex2D(mapaGlebokosci, a).a;
vec3 fog = tex2D(obraz, IN.TexCoord0.xy).rgb;
vec3 background_color = tex2D(tlo, IN.TexCoord0.xy).rgb;
// obliczanie jasności
float l = 0.3*background_color.r + 0.59*background_color.g +
0.11*background_color.b - 0.15;
l = clamp(l, 0, 1);
// kwadrat jasności
l = l*l;
// tłumienie jasności w zależności od gęstości mgły
l = pow(l, density+0.001);
// obliczanie mocy mgły wykładniczej z uwzględnieniem jasności
vec moc = 1.0 - exp(-(dep/100) * density * (1.0 - l));
return float4(lerp(background_color, fog, moc), 1);
}
Przykładowe wyniki działania opracowanej mgły przedstawiono na kolejnych
rysunkach.
4.4 Mgła
~ 67 ~
Rys. 44 Przykładowe obrazy przedstawiające mgłę.
Rys. 45 Obrazy przedstawiające rosnącą gęstość mgły.
4.4 Mgła
~ 68 ~
Kolejnym typem mgły jest mgła wolumetryczna. Służy ona do jeszcze dokładniejszej
symulacji świecenia oświetlonych cząstek mgły. Dzięki mgle wolumetrycznej możliwe jest
stworzenie struktur cieni widocznych we mgle. Za jej pomocą można tworzyć bardzo
atrakcyjne wizualnie obrazy.
W rozwiązaniu opracowanym w ramach niniejszej pracy skupiono się na stworzeniu
mgły oświetlanej przez punktowe źródło światła.
Aby wygenerować taką mgłę należy stworzyć kubiczną mapę głębokości o środku
w źródle światła. Mając taką mapę można określić, czy dowolny punkt na scenie jest
oświetlony przez światło, czy też znajduje się w cieniu. Następnie należy zastosować
próbkowanie promieni wyemitowanych od obserwatora do każdego punktu
wyrenderowanej sceny.
Każdy promień próbkowany jest określoną liczbą próbek. Mając trójwymiarową
pozycję próbki możemy określić, czy jest ona w cieniu czy nie, używając kubicznej mapy
głębokości światła. Należy zsumować próbki nie będące w cieniu, a następnie zastosować
dodatkowo mieszanie stosowane w metodzie mgły wykładniczej.
Światło punktowe może świecić różnymi kolorami w różne strony. Wykorzystuje się
do tego mapę kubiczną światła. Można za jej pomocą stworzyć światło reflektorowe, które
tylko na jednej ścianie mapy kubicznej będzie miało jasne koło. Zastosowanie kolorowych
świateł wraz z mgłą wolumetryczną daje bardzo efektowne rezultaty.
Ostateczny shader pikseli renderujący mgłę wolumetryczną przedstawia następujący
listing:
struct vertout
{
vec4 TexCoord0 : TEXCOORD0;
vec4 TexCoord1 : TEXCOORD1;
vec4 Color0 : COLOR0;
};
vec4 main(vertout IN,
uniform sampler2D tlo : TEXUNIT0,
uniform sampler2D mapaGlebokosciScreen : TEXUNIT1,
uniform sampler2D obraz : TEXUNIT2,
uniform samplerCUBE mapaGlebokosciSwiatla : TEXUNIT3,
uniform samplerCUBE kolorSwiatla : TEXUNIT4,
uniform vec density,
uniform vec3 cam_pos,
uniform vec3 light_pos
) : COLOR0
{
vec dep = -tex2D(mapaGlebokosciScreen, IN.TexCoord0.xy).a;
// kierunek promienia od kamery do piksela
vec3 dir = normalize(IN.TexCoord1.xyz);
const int SAMPLES = 40;
dir = dir * dep / SAMPLES;
// pozycja próbki w stosunku do swiatla
vec3 pos = cam_pos - light_pos;
4.4 Mgła
~ 69 ~
vec moc = 0;
vec3 fog_color = 0;
for (int i=0; i<SAMPLES; i++)
{
vec light_dep = texCUBE(mapaGlebokosciSwiatla, pos).r;
vec3 light_color = texCUBE(kolorSwiatla, pos).rgb;
// jesli próbka nie jest w cieniu
if (dot(pos, pos) < light_dep*light_dep)
{
moc += 1.0/SAMPLES;
fog_color += light_color * 1.0/SAMPLES;
}
pos += dir;
}
fog_color += 0.01;
fog_color /= moc+0.01;
// obliczanie mgły jak w podstawowej mgle
vec3 fog = tex2D(obraz, IN.TexCoord0.xy).rgb;
vec3 background_color = tex2D(tlo, IN.TexCoord0.xy).rgb;
float l = 0.3*background_color.r + 0.59*background_color.g +
0.11*background_color.b - 0.15;
l = clamp(l, 0, 1);
l = l*l;
l = pow(l, density);
moc = 1.0 - exp(-(moc) * density * (1.0 - l));
vec4 res;
// dodawanie koloru światła fog_color
res = float4(lerp(background_color, fog+fog_color, moc), 1);
return res * IN.Color0;
}
Przykładowe obrazy przedstawiające opracowaną mgłę wolumetryczną
przedstawiono na rysunku 46.
4.4 Mgła
~ 70 ~
Rys. 46 Obrazy przedstawiające mgłę wolumetryczną. Na dolnych rysunkach widać mgłę, która
została wygenerowana przez kolorowe światło (z teksturą witrażu).
4.5 Inne efekty
~ 71 ~
4.5 Inne efekty
Poza efektami wymienionymi w poprzednich podrozdziałach, opracowano również
wiele innych, prostszych efektów. Są to m.in.:
rozmycie – z możliwością regulacji promienia rozmycia,
wyostrzenie – z możliwością regulacji promienia maski wyostrzającej oraz
mocy wyostrzania,
poświata – z możliwością regulacji stopnia rozmycia i koloru poświaty oraz
długości powidoku,
odbicie lustrzane – w pionie lub poziomie,
ustawienie dwóch obrazów obok siebie – w pionie albo poziomie,
mieszanie obrazów – z regulacją wag poszczególnych obrazów,
rozmycie radialne – z regulacją stopnia rozmycia,
zmiana jasności, kontrastu oraz parametru gamma obrazu,
konwersja do przestrzeni HSV i w drugą stronę,
dodanie szumu – z regulacją mocy szumu,
efekt „telewizyjny” – dodanie filtra z czarnych linii,
współrzędne biegunowe – przekształcenie obrazu do współrzędnych
biegunowych oraz w drugą stronę.
Przykład zastosowania wielu efektów i złączenia ich wszystkich na jednym obrazie
ilustruje rysunek 47. Schemat połączenia efektów ilustruje rysunek 48.
Rys. 47 Zastosowanie wielu efektów.
4.6 Materiały
~ 72 ~
Rys. 48 Schemat połączenia wielu efektów.
4.6 Materiały
Materiały są bardzo ważnym elementem w grafice trójwymiarowej, ponieważ to od
nich zależy realność generowanego obrazu. Materiał w grafice trójwymiarowej musi
uwzględniać takie parametry jak kąt patrzenia i kąt padania światła. Patrząc na obiekt
z różnych stron i pod różnymi kątami możemy zobaczyć zupełnie inne kolory, np. patrząc
wprost na powierzchnię mokrego kamienia zobaczymy jego kolor, jednak patrząc pod
kątem zobaczymy to, co odbija się w wodzie. Skrajnym przypadkiem jest lustro, które
odbija wszystko i samo nie ma żadnego koloru.
W większości materiałów wyodrębnić można parametry takie jak rozpraszanie
(ang. diffuse) i odblask (ang. specular). Za pomocą tylko tych dwóch cech można stworzyć
bardzo wiele ciekawych materiałów.
Kolejnym elementem, który przydaje się podczas tworzenia materiałów, jest mapa
otoczenia obiektu. Za jej pomocą można tworzyć materiały lustrzane i szklane. Również
wypolerowany kamień odbija otaczający świat, więc można wykorzystać mapę otoczenia.
Opracowano bazę materiałów, które można nakładać na obiekty. Najprostszymi
są materiały matowe, takie jak drewno, cegła. Zaprezentowano je na rysunkach 49 i 50.
4.6 Materiały
~ 73 ~
Rys. 49 Drewniana deska.
Rys. 50 Mur ceglany.
Bardziej efektowne materiały to wyszlifowany kamień oraz drewno pokryte
lakierem. Przedstawiono je na rysunkach 51 i 52.
4.6 Materiały
~ 74 ~
Rys. 51 Polakierowane drewno.
Rys. 52 Wyszlifowany kamienny królik.
4.6 Materiały
~ 75 ~
Poniżej przedstawiono kilka dodatkowych materiałów, takich jak różnego rodzaju
metale i ceramiczne kafelki.
Rys. 53 Zardzewiały metal.
Rys. 54 Podłoga z kafelków.
4.6 Materiały
~ 76 ~
Rys. 55 Różne rodzaje metali.
Dzięki zastosowaniu kubicznych map otoczenia można było uzyskać materiały
wyglądające jak różnego rodzaju metale, szkło i obiekty wyszlifowane lub polakierowane.
Zostały one częściowo pokazane w rozdziale 3.4. Kolejne przykłady zaprezentowano
następnych rysunkach.
4.6 Materiały
~ 77 ~
Rys. 56 Model królika
2 ze szkła prezentujący aberrację chromatyczną.
Rys. 57 Model szklanego smoka
3. Zaprezentowano różne materiały szkła.
2, 3
Źródło: The Stanford 3D Scanning Repository. http://graphics.stanford.edu/data/3Dscanrep.
4.6 Materiały
~ 78 ~
Rys. 58 Zastosowano różny stopień rozmycia kubicznej mapy otoczenia.
Rys. 59 Prezentacja różnych materiałów metalowych. Metal może być błyszczący bądź matowy.
4.6 Materiały
~ 79 ~
Rys. 60 Materiał odwzorowujący polakierowany plastik. Może on odbijać wyraźnie swoje otocznie
(obrazek po prawej) lub jego rozmytą wersję (obrazek po lewej).
Interesującym materiałem jest animowany materiał znikający. Przedstawiono go na
poniższym rysunku.
Rys. 61 Obiekt przedstawiający znikający materiał.
4.6 Materiały
~ 80 ~
W obiekcie powstają dziury, aż w końcu znika on cały. Sposób znikania określany
jest za pomocą monochromatycznej tekstury. Animację osiąga się za pomocą zmiennej,
określającą próg. Piksel jest odrzucany, gdy próg jest większy od jasności tekstury.
Przykładowa tekstura, która dobrze symuluje tworzenie się dziur w obiekcie przedstawiona
jest na rysunku 62.
Rys. 62 Im jaśniejsze piksele, tym dłużej pozostaną widoczne.
Fragment shadera pikseli realizującego odrzucanie pikseli przedstawia poniższy
listing.
float4 main(
vertout IN,
uniform sampler2D texMaska : TEXUNIT0,
...
const uniform float prog)
{
float w = tex2D(texMaska, IN.TexCoord.xy).r;
if (w < prog)discard; // odrzucenie piksela
... // obliczanie koloru materiału
return color;
}
~ 81 ~
5 Przykładowa gra
Przydatność silnika do tworzenia gier powinna zostać sprawdzona poprzez napisanie
gry. Dlatego też opracowano prostą grę wykorzystującą zintegrowany silnik fizyki.
Stworzenie gry nie wymagało wiele pracy, ponieważ silnik udostępniał solidną bazę.
Stworzona gra polega na jak najszybszym przeprowadzeniu kuli przez labirynt. Za
pomocą myszki gracz obraca planszę, tym samym kierując kulę w odpowiednim kierunku.
Stworzenie gry wymagało tylko stworzenia modelu planszy i oprogramowania
sterowania nią. Gra nakazuje silnikowi wczytanie planszy. Silnik przekazuje ruchy myszki
oraz wciśnięte klawisze do gry, która z kolei przekazuje silnikowi położenie kamery oraz
zmienia kierunek siły grawitacji.
Kilka obrazów z gry przedstawiono na kolejnych rysunkach.
Rys. 63 Gra.
5 Przykładowa gra
~ 82 ~
Rys. 64 Początek i koniec gry.
Rys. 65 Różne wersje kolorystyczne gry.
Rys. 66 Druga wersja gry.
~ 83 ~
6 Podsumowanie
Opracowany został wieloplatformowy silnik umożliwiający zbudowanie gry
komputerowej. Najważniejsza część, czyli silnik graficzny, umożliwia tworzenie efektów
i wygodne zarządzanie nimi. Dzięki temu możliwe było stworzenie wielu interesujących
efektów, takich jak głębia ostrości, rozmycie ruchu czy mgła. Zaimplementowano również
bazę mniej zaawansowanych efektów, takich jak rozmycie, wyostrzenie, korekcja gamma
itp.
Z silnikiem został zintegrowany silnik fizyki PhysX firmy NVidia. Wymagało to
wprowadzenia wielu modyfikacji w pierwotnej wersji silnika, powstałego w ramach pracy
inżynierskiej. Silnik graficzny ściśle współpracuje z silnikiem fizyki, ponieważ obiekty
symulowane przez silnik fizyki są tymi, które są wyświetlane na ekranie.
System efektów cząsteczkowych został oparty o silnik fizyki, dzięki czemu
cząsteczki mogą wchodzić w interakcje ze wszystkimi obiektami na scenie. System
umożliwia tworzenie zjawisk, takich jak iskry, dym, ogień oraz eksplozje.
Przystosowanie silnika do działania pod kontrolą systemów Linux oraz Windows,
wymagało stworzenia dla każdego systemu oddzielnej implementacji warstwy
pośredniczącej między silnikiem a systemem operacyjnym.
Opracowany został system portali, pozwalających na przechodzenie światła oraz
obiektów przez portale. Najtrudniejszym zadaniem była integracja silnika fizyki
z systemem portali, ponieważ zastosowany silnik fizyki nie przewidywał takich rozwiązań.
Portale mogą służyć do tworzenia zadań logicznych dla gracza i sprawdzać jego orientację
w nienaturalnie zniekształconej przestrzeni. Najprawdopodobniej portale pojawią się
w wielu nadchodzących produkcjach. Opracowany system portali jest w całości autorski.
Mechanizm zarządzania sceną został oparty o luźne drzewa ósemkowe. Rozwiązanie
to sprawdza się dla zamkniętych oraz otwartych przestrzeni.
Symulacji głębi ostrości w czasie rzeczywistym nie jest zadaniem łatwym, jednak
opracowane rozwiązania są wystarczające m.in. dla gier komputerowych, gdzie jakość nie
musi być idealnie zgodna z rzeczywistością. Opracowane rozwiązanie pozwala na
symulowanie efektu soczewkowego, dzięki czemu rozmyte jasne punkty przybierają
kształt jakiejś figury geometrycznej. Odpowiednie operowanie głębią ostrości nadaje
obrazom filmowego charakteru.
Rozmycie ruchu również jest bardzo ważnym efektem, nadającym grom czy
animacjom filmowego charakteru. Opracowane rozwiązanie wykorzystuje spostrzeżenie,
że podczas ruchu jasne punkty zostawiają wyraźniejszy, jaśniejszy, ślad. Wynik działania
opracowanego efektu jest podobny do tego na obrazach widzianych na poszczególnych
klatkach w filmach.
Opracowany efekt mgły wykorzystuje podobne spostrzeżenie co dwa poprzednie
efekty. Zauważono, że jasne punkty są bardziej widoczne przez mgłę. Można to
zaobserwować w prawdziwym świecie, gdzie w gęstej mgle będziemy widzieć w oddali
tylko jasne punkty, np. źródła światła, natomiast cała reszta będzie całkowicie zakryta
kolorem mgły. Wyniki działania opracowanego efektu są spektakularne.
6 Podsumowanie
~ 84 ~
Stworzona została baza materiałów dla obiektów. Mechanizm renderowania
kubicznych map otoczenia pozwala na tworzenie przezroczystych i odbijających
materiałów. Materiały takie jak drewno, kamień, szkło, metal prezentują się atrakcyjnie
wizualnie.
W celu zademonstrowania możliwości silnika została stworzona prosta gra.
W dalszej pracy silnik może zostać rozbudowany o kolejne elementy. Mechanizm
zarządzania sceną może być rozszerzony w taki sposób, by mógł również zarządzać
geometrią sceny poprzez portale. Mechanizm ten powinien również pozwalać na płynne
przejścia między scenami otwartymi i zamkniętymi, z których pierwsze będą zarządzane
luźnym drzewem ósemkowym, a drugie portalami.
Ponadto, silnik może zostać rozbudowany o kolejne efekty i materiały. Architektura
pozwala na dodawanie efektów w formie wtyczek, co ułatwia ich tworzenie.
~ 85 ~
Bibliografia
[1] J. D. Foley, A. van Dam, S. K. Feiner, J. F. Hughes, R. L. Phillips, Wprowadzenie do
grafiki komputerowej, Wydawnictwa Naukowo–Techniczne, 2001.
[2] S. Zerbst, O. Düvel, 3D Game Engine Programming, Course Technology PTR, 2004.
[3] J. Gregory, Game Engine Architecture, A K Peters, 2009.
[4] W. Jawor, Principia Silnika
[5] J. Simpson, Game Engine Anatomy 101, http://www.extremetech.com/article2/
0,2845,594,00.asp
[6] V. Mönkkönen, Multithreaded Game Engine Architectures,
http://www.gamasutra.com/features/20060906/monkkonen_01.shtml
[7] OpenGL Overview, http://www.opengl.org/about/overview/
[8] Quake source code, http://www.idsoftware.com/firstweb/business/techdownloads/
[9] Unreal Engine 3 Technology, http://www.unrealtechnology.com/technology.php
[10] John Carmack’s Blog, http://www.armadilloaerospace.com/n.x/johnc/recent updates
[11] Carmack on Shadow Volumes, http://developer.nvidia.com/attach/6832
[12] CryENGINE 3 Specifications, http://www.crytek.com/technology/cryengine-3/
specifications/
[13] Unigine, http://unigine.com/
[14] BSP Tree FAQ, http://www.gamedev.net/reference/articles/article657.asp
[15] BSP Trees: Theory and Implementation, http://www.devmaster.net/articles/bsp-trees
[16] BSP Trees in 3D Worlds, http://web.cs.wpi.edu/~matt/courses/cs563/talks/bsp/
bsp.html
[17] M. Kozioł, Portale, http://kb.komires.net/article.php?id=21
[18] J. Bikker, Building a 3D Portal Engine, http://www.flipcode.com/archives/
Building_a_3D_Portal_Engine-Issue_01_Introduction.shtml
[19] D. Ginsburg, Octree Construction, Game Programming Gems 1, Charles River
Media, 2000.
[20] Havok Physics, http://www.havok.com/index.php?page=havok-physics
[21] NVidia PhysX, http://developer.nvidia.com/object/physx_features.html
[22] Newton Game Dynamics, http://newtondynamics.com
[23] J. Barnett, K. Swift, E. Wolpaw, Thinking With Portals: Creating Valve's New IP,
http://www.gamasutra.com/view/feature/3839/thinking_with_portals_creating_.php
[24] Skinned Mesh, http://frustum.unigine.com/3d/feedback.php?demo=45
[25] DDS, http://msdn.microsoft.com/en-us/library/bb943990.aspx
[26] NVidia Texture Tools 2, http://developer.nvidia.com/object/texture_tools.html
[27] T. Ulrich, Loose Octrees, Game Programming Gems 1, Charles River Media, 2000.
[28] NVidia CG Reference Manual
[29] H. Jorke, M. Fritz, Infitec – A new stereoscopic visualization tool by wavelength
multiplex imaging, Journal of Three Dimensional Images, 2005
[30] T. Igarashi, N. Max, and F. Sillion, Real-Time Depth-of-Field Rendering Using Point
Splatting on Per-Pixel Layers