This is the Polish translation of Advanced-Lighting/HDR article of learnopengl.com tutorial series.
Domyślnie wartości jasności i koloru są obcinane do wartości pomiędzy 0.0
i 1.0
, gdy są zapisywane do bufora ramki. To na pozór niewinne stwierdzenie sprawia, że zawsze określamy wartości światła i koloru gdzieś w tym zakresie, próbując dopasować je do sceny. Działa to dobrze i daje przyzwoite wyniki, ale co się stanie, jeśli będziemy chodzić w szczególnie jasnym obszarze z wieloma jasnymi źródłami światła, które w sumie przekraczają wartość 1.0
? Odpowiedź brzmi, że wszystkie fragmenty, których jasność lub suma kolorów przekracza 1.0
, zostają obcięte do 1.0
, co nie jest zbyt miłe do oglądania:
Ze względu na dużą liczbę wartości kolorów fragmentów obciętych do wartości 1.0
każdy z jasnych fragmentów ma dokładnie taki sam biały kolor na dużym obszarze, tracąc znaczną ilość szczegółów i nadając im fałszywy wygląd.
Rozwiązaniem tego problemu byłoby zmniejszenie mocy źródeł światła i upewnienie się, że żaden obszar fragmentów w twojej scenie nie będzie jaśniejszy niż 1.0
; nie jest to dobre rozwiązanie, ponieważ zmusza do użycia nierealistycznych parametrów oświetlenia. Lepszym rozwiązaniem jest, aby wartości kolorów tymczasowo przekroczyły wartość 1.0
i przekształcić je z powrotem do oryginalnego zakresu 0.0
i 1.0
w ostatnim kroku, ale bez utraty szczegółów.
Monitory są ograniczone do wyświetlania kolorów w zakresie 0.0
i 1.0
, ale nie ma takiego ograniczenia w równaniach oświetlenia. Dzięki temu, że kolory fragmentów mogą przekroczyć 1.0
, mamy znacznie większy zakres wartości kolorów znany jako
HDR był pierwotnie używany tylko w fotografii, gdzie fotograf robił wiele zdjęć tej samej sceny z różnymi poziomami ekspozycji, rejestrując duży zakres wartości kolorów. Te połączone obrazy tworzą obraz HDR, w którym duży zakres szczegółów jest widoczny na podstawie połączonych poziomów ekspozycji lub określonej ekspozycji, z jaką jest oglądany. Na przykład obraz poniżej pokazuje wiele szczegółów w jasno oświetlonych regionach z niską ekspozycją (patrz okno), ale te szczegóły zniknęły przy dużej ekspozycji. Jednak wysoka ekspozycja ujawnia teraz dużą ilość szczegółów w ciemniejszych regionach, które wcześniej nie były widoczne.
Jest to również bardzo podobne do działania ludzkiego oka i podstawą renderowania HDR. Kiedy jest mało światła, ludzkie oko dostosowuje się, więc ciemniejsze części są znacznie lepiej widoczne i podobnie jest dla jasnych obszarów, tak jakby ludzkie oko miało automatyczny suwak ekspozycji w oparciu o jasność sceny.
Renderowanie z wykorzystaniem HDR działa trochę tak: pozwalamy na uzyskanie znacznie większego zakresu wartości kolorów w celu zebrania szerokiego zakresu ciemnych i jasnych szczegółów sceny, a na końcu przekształcamy wszystkie wartości HDR z powrotem na 0.0
, 1.0
]. Ten proces przekształcania wartości HDR w wartości LDR nazywa się
Jeśli chodzi o rendering w czasie rzeczywistym, HDR pozwala nam nie tylko przekroczyć zakres LDR [0.0
, 1.0
] i zachować więcej szczegółów, ale także daje nam możliwość określenia intensywności źródła światła przez realnej intensywności. Na przykład słońce ma znacznie większą intensywność niż latarka, więc dlaczego nie skonfigurować słońca takiego jakie jest (np. z jasnością diffuse 10.0
). To pozwala nam na lepsze skonfigurowanie oświetlenia sceny z bardziej realistycznymi parametrami oświetlenia, co nie byłoby możliwe przy renderowaniu LDR, ponieważ wtedy zostaną one bezpośrednio obcięte do 1.0
.
Ponieważ monitory wyświetlają tylko kolory w zakresie od 0.0
do 1.0
, musimy przekształcić wartości kolorów HDR z powrotem do zakresu monitora LDR. Po prostu ponowne przekształcenie kolorów za pomocą prostego uśredniania wartości nie przyniesie nam zbyt wiele dobrego, ponieważ jaśniejsze obszary stają się bardziej dominujące. Możemy jednak użyć różnych równań i/lub krzywych, aby przekształcić wartości HDR z powrotem w LDR, co daje nam pełną kontrolę nad jasnością sceny. Jest to proces wcześniej określony jako mapowanie tonalne i jest to ostatni krok renderowania HDR.
Zmiennoprzecinkowe framebuffery
Aby zaimplementować renderowanie HDR, potrzebujemy jakiegoś sposobu, aby zapobiec obcinaniu wartości kolorów po uruchomieniu każdego Fragment Shadera. Gdy framebuffery używają znormalizowanego formatu koloru (np. GL_RGB) jako wewnętrznego formatu bufora kolorów OpenGL automatycznie obcina wartości do zakresu pomiędzy 0.0
i 1.0
przed zapisaniem ich w buforze ramki. Ta operacja dotyczy większości typów formatów bufora ramki, z wyjątkiem formatów zmiennoprzecinkowych, które są używane dla ich rozszerzonego zakresu wartości.
Gdy wewnętrzny format bufora kolorów bufora ramki jest określony jako GL_RGB16F, GL_RGBA16F, GL_RGB32F lub GL_RGBA32F znany jest jako 0.0
i 1.0
. Jest to idealne do renderowania w HDR!
Aby utworzyć zmiennoprzecinkowy bufor ramki, jedyną rzeczą, którą musimy zmienić, jest parametr formatu wewnętrznego bufora kolorów:
glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
Domyślny framebuffer OpenGL (domyślnie) zajmuje tylko 8 bitów na składnik koloru. Dzięki zmiennoprzecinkowemu framebufferowi z 32 bitami na składnik koloru (przy użyciu GL_RGB32F lub GL_RGBA32F) używamy 4 razy więcej pamięci do przechowywania wartości kolorów. Ponieważ 32 bity nie są tak naprawdę potrzebne, chyba że potrzebujesz wysokiego poziomu precyzji, wystarczy użyć GL_RGBA16F.
Dzięki zmiennoprzecinkowemu buforowi kolorów dołączonemu do bufora ramki możemy teraz renderować scenę do tego bufora ramki, wiedząc, że wartości kolorów nie zostaną obcięte do zakresu między 0.0
i 1.0
. W przykładzie demonstracyjnym tego samouczka najpierw renderujemy oświetloną scenę w zmiennoprzecinkowym buforze ramki, a następnie wyświetlamy bufor kolorów bufora ramki na wypełnionym kwadracie pełnoekranowym; będzie wyglądać to tak:
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// [...] render (lighted) scene
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// now render hdr colorbuffer to 2D screen-filling quad with different shader
hdrShader.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
RenderQuad();
Tutaj wartości kolorów sceny są wprowadzane do bufora kolorów zmiennoprzecinkowych, który może zawierać dowolną wartość koloru, prawdopodobnie przekraczającą 1.0
. W tym samouczku powstała prosta scena demo z dużym rozciągniętym sześcianem działającym jak tunel z czterema punktowymi światłami, z których jedno jest bardzo jasne i znajduje się na końcu tunelu:
std::vector<glm::vec3> lightColors;
lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f));
lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f));
lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f));
lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f));
Rendering do zmiennoprzecinkowego framebuffera jest dokładnie taki sam jak taki sam jak rendering do normalnego framebuffera. Nowością jest Fragment Shader hdrShader, który renderuje ostateczny kwadrat 2D z dołączoną teksturą bufora zmiennoprzecinkowego. Najpierw określmy prosty Fragment Shader:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D hdrBuffer;
void main()
{
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
FragColor = vec4(hdrColor, 1.0);
}
Tutaj bezpośrednio próbkujemy zmiennoprzecinkowy bufor kolorów i używamy jego wartości koloru jako wyjścia Fragment Shadera. Jednakże, ponieważ dane wyjściowe kwadratu 2D są bezpośrednio renderowane do domyślnego bufora ramki, wszystkie wartości wyjściowe Fragment Shadera będą obcięte do zakresu między 0.0
i 1.0
, mimo że mamy kilka wartości w teksturach zmiennoprzecinkowych przekraczających 1.0
.
Staje się jasne, że intensywne wartości światła na końcu tunelu są obcięte do 1.0
, ponieważ duża ich część jest całkowicie biała, skutecznie tracąc wszystkie szczegóły oświetlenia które przekraczają 1.0
. Ponieważ bezpośrednio przekształcamy wartości HDR w wartości LDR, to tak, jakbyśmy nie mieli włączonego HDR. To, co musimy zrobić, aby to naprawić, to przekształcenie wszystkich wartości zmiennoprzecinkowych z powrotem w zakres 0.0
- 1.0
bez utraty jakichkolwiek szczegółów. Musimy zastosować proces o nazwie
Mapowanie tonalne
Mapowanie tonalne to proces przekształcania wartości kolorów zmiennoprzecinkowych do oczekiwanego zakresu [0,0
, 1,0
] znanego jako LDR bez utraty zbyt dużej ilości szczegółów, któremu często towarzyszy określony stylistyczny balans kolorów.
Najprostszy algorytm mapowania tonalnego znany jest jako
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
// reinhard tone mapping
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
// gamma correction
mapped = pow(mapped, vec3(1.0 / gamma));
FragColor = vec4(mapped, 1.0);
}
Dzięki zastosowaniu mapowania tonalnego Reinharda nie tracimy już szczegółów w jasnych obszarach naszej sceny. Algorytm ma tendencję do faworyzowania jaśniejszych obszarów, sprawiając, że ciemniejsze regiony wydają się mniej szczegółowe i wyraźne:
Ponownie można zobaczyć szczegóły na końcu tunelu, ponieważ wzór tekstury drewna staje się ponownie widoczny. Dzięki temu względnie prostemu algorytmowi mapowania tonalnego możemy właściwie zobaczyć cały zakres wartości HDR przechowywanych w zmiennoprzecinkowym buforze ramki, co daje nam precyzyjną kontrolę nad oświetleniem sceny bez utraty szczegółów.
Zauważ, że mogliśmy również bezpośrednio zastosować mapowanie tonalne na końcu naszego Fragment Shadera, nie potrzebując żadnego zmiennoprzecinkowego bufora ramki! Ponieważ sceny stają się coraz bardziej złożone, często będziesz musiał przechowywać wyniki pośrednie HDR w buforach zmiennoprzecinkowych, więc jest to dobre ćwiczenie.
Innym interesującym zastosowaniem mapowania tonalnego jest umożliwienie użycia parametru ekspozycji. Prawdopodobnie pamiętasz ze wstępu, że obrazy HDR zawierają wiele szczegółów widocznych na różnych poziomach ekspozycji. Jeśli mamy scenę z cyklem dziennym i nocnym, sensowne jest używanie mniejszej ekspozycji w świetle dziennym i wyższej ekspozycji w porze nocnej, podobnie jak dostosowuje się ludzkie oko. Przy takim parametrze ekspozycji pozwala nam skonfigurować parametry oświetlenia, które działają zarówno w dzień, jak i w nocy w różnych warunkach oświetleniowych, ponieważ musimy jedynie zmienić parametr ekspozycji.
Stosunkowo prosty algorytm mapowania tonalnego wykorzystujący ekspozycję wygląda następująco:
uniform float exposure;
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
// Exposure tone mapping
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
// Gamma correction
mapped = pow(mapped, vec3(1.0 / gamma));
FragColor = vec4(mapped, 1.0);
}
Tutaj zdefiniowaliśmy uniform exposure, który domyślnie przyjmuje wartość 1.0
i pozwala nam precyzyjniej określić, czy chcielibyśmy bardziej skupić się na ciemnych czy jasnych regionach wartości kolorów HDR. Na przykład, przy wysokich wartościach ekspozycji ciemniejsze obszary tunelu wykazują znacznie więcej szczegółów. W przeciwieństwie do tego, niska ekspozycja w dużym stopniu usuwa szczegóły ciemnego regionu, ale pozwala nam zobaczyć więcej szczegółów w jasnych obszarach sceny. Rzuć okiem na poniższy obraz, aby zobaczyć tunel o różnych poziomach ekspozycji:
Ten obraz wyraźnie pokazuje korzyści płynące z renderingu HDR. Zmieniając poziom ekspozycji, widzimy wiele szczegółów naszej sceny, które w innym przypadku zostałyby utracone przy LDR. Na przykład na końcu tunelu, przy normalnej ekspozycji struktura drewna jest ledwo widoczna, ale przy niewielkiej ekspozycji drewniane wzory są wyraźnie widoczne. To samo dotyczy drewnianych wzorów w pobliżu, które są znacznie lepiej widoczne przy wysokiej ekspozycji.
Możesz znaleźć kod źródłowy wersji demonstracyjnej tutaj.
Więcej HDR
Pokazane algorytmy mapowania tonalnego to tylko garstka z dużej kolekcji (bardziej zaawansowanych) algorytmów mapowania tonalnego, z których każdy ma swoje mocne i słabe strony. Niektóre algorytmy mapowania tonalnego faworyzują pewne kolory/intensywności niż inne, a niektóre algorytmy wyświetlają równocześnie kolory o niskiej i wysokiej ekspozycji, aby uzyskać bardziej kolorowe i szczegółowe obrazy. Istnieje również zbiór technik znanych jako
Prawdziwa korzyść z renderowania HDR naprawdę pokazuje się w dużych i złożonych scenach z ciężkimi algorytmami oświetlenia. Ponieważ trudno jest stworzyć tak złożoną scenę demo do celów dydaktycznych, przy zachowaniu jej dostępności scena demo tutoriala jest niewielka i brakuje jej szczegółów. Chociaż jest stosunkowo prosta, pokazuje niektóre zalety renderowania HDR: żadne szczegóły nie są tracone w obszarach jasnych i ciemnych, ponieważ można je odzyskać za pomocą mapowania tonalnego, a dodanie wielu świateł nie powoduje utraty szczegółów i wartości światła można określić za pomocą ich oryginalnych wartości jasności nie ograniczonych przez wartości LDR. Co więcej, renderowanie HDR sprawia, że kilka interesujących efektów jest bardziej wykonalnych i realistycznych. Jednym z tych efektów jest
Dodatkowe materiały
- Does HDR rendering have any benefits if bloom won’t be applied?: pytanie stackexchange, które zawiera długą odpowiedź opisującą niektóre zalety renderowania HDR.
- What is tone mapping? How does it relate to HDR?: kolejna interesująca odpowiedź z doskonałymi obrazami referencyjnymi do objaśnienia mapowania tonalnego.