Wzory z poprzedniej książki (Lambert, Fresnel, GGX) zakładają, że podwojenie liczby oznacza podwojenie jasności. Żaden plik obrazu i żaden ekran tego założenia nie spełnia.
TL;DR
Liniowość radiometryczna oznacza: podwojenie liczby fotonów daje podwojenie zapisanej wartości. Lambert, Fresnel i GGX (raytracing-book, Rozdziały 5, 10–11) są zbudowane na tym założeniu — działają poprawnie tylko na liczbach, które je spełniają.
Pliki tekstur i to, co widzisz na ekranie, są zakodowane percepcyjnie (Rozdział 2), nie fizycznie — trzeba je zdekodować na wejściu do obliczeń i zakodować dopiero na samym końcu, przy wyświetlaniu.
Pominięcie tego kroku nie daje oczywistego błędu w stylu „coś nie działa” — daje subtelnie zmatowiały, przyciemniony albo zbyt kontrastowy obraz, mimo że wzory są matematycznie bez zarzutu.
Etap: Definicja
Co dokładnie znaczy „liniowe”
Przypomnij sobie najprostszy wzór na oświetlenie rozproszone z raytracing-book (Rozdział 5):
L_o = k_d · (N·L) · L_iL_o — jasność odbita w stronę obserwatora; k_d — kolor/albedo powierzchni; N·L — cosinus kąta między normalną a kierunkiem do światła; L_i — jasność światła padającego
Ten wzór jest wprost proporcjonalny względem L_i: podwój jasność źródła światła, a L_o też się podwoi. To jest dokładnie to, co znaczy liniowość radiometryczna — nie „prosta linia na wykresie” w sensie ogólnym, tylko konkretna własność fizyczna: ilość energii świetlnej i liczba, którą tę energię reprezentujesz, rosną w tym samym tempie. Dwa razy więcej fotonów = dwa razy większa liczba. Bez tego założenia każdy wzór z poprzedniej książki — Lambert, Fresnel, rozkład GGX — przestaje dawać sensowne wyniki, bo cała ich algebra (dodawanie wkładów wielu świateł, mnożenie przez cosinus, całkowanie po hemisferze) zakłada dokładnie tę proporcjonalność.
Etap: Skąd bierze się problem
Piksel w pliku to nie to samo co jasność fizyczna
Rozdział 2 pokazał, że oko reaguje na jasność w przybliżeniu logarytmicznie, nie liniowo — i że z tego powodu formaty obrazu (a za nimi każdy monitor) kodują jasność percepcyjnie, gamma lub logiem, żeby nie marnować poziomów tam, gdzie oko i tak ich nie rozróżnia. To świetne rozwiązanie dla plików, które ktoś ogląda — i fatalne, jeśli wrzucić taką liczbę wprost do wzoru na L_o powyżej, bo ten wzór nie wie nic o percepcji. On zakłada, że liczba, którą dostaje, jest proporcjonalna do fotonów, a wartość 0,5 zapisana w pliku sRGB reprezentuje nie połowę fizycznej jasności bieli, tylko dużo mniej — mniej więcej jedną piątą (dokładny wywód krzywej sRGB w Rozdziale 5 i Dodatku C).
Etap: Policzone
Co się dzieje, kiedy zabraknie dekodowania
Najprostszy sposób, żeby zobaczyć problem na liczbach: uśrednij dwa piksele tekstury — jeden ciemny (0,2), jeden jasny (0,8) — tak, jak robi to każde rozmycie, mipmapping czy crossfade. Porównaj wynik, gdy uśredniasz wartości wprost z pliku (błędnie), z wynikiem, gdy najpierw je dekodujesz do linear, uśredniasz fizyczną jasność, i dopiero potem kodujesz z powrotem do wyświetlenia (poprawnie).
Policzone: uśrednianie dwóch pikseli (uproszczona gamma 2,2)
Wynik błędny: 0,5 • wynik poprawny: 0,596 — różnica ok. 16%, widoczna jako przyciemnienie
Sprawdzenie kierunku błędu: gamma „ściska” jasne wartości bliżej 1 i „rozciąga” ciemne bliżej 0, więc uśrednianie wprost w tej przestrzeni zawsze zaniża wynik względem uśredniania fizycznej jasności — stąd błąd jest systematycznym przyciemnieniem, nie losowym szumem.
Ten sam mechanizm — nie losowy, tylko systematyczny błąd w jedną stronę — stoi za klasycznym objawem „render wygląda spłowiały/zmatowiały mimo poprawnych wzorów”: ktoś gdzieś w łańcuchu uśrednił, przefiltrował albo przemnożył wartości zakodowane percepcyjnie tak, jakby były liniowe.
Etap: Diagnoza
Trzy miejsca, w których liniowość najczęściej się łamie
Tekstury kolorowe wchodzące bezpośrednio do BRDF — plik Base Color/Albedo zapisany jako PNG czy JPG jest w sRGB; jeśli renderer nie zdekoduje go przed podstawieniem jako k_d we wzorze powyżej, cały shading network liczy na złych liczbach od samego początku. Pełny mechanizm w Rozdziale 11.
Filtering i mipmapping na zakodowanych wartościach — dokładnie ten sam problem co w policzonym przykładzie wyżej, tylko wykonywany automatycznie przez GPU czy renderer przy budowie mipmap. Jeśli filtrowanie zdarza się przed dekodowaniem, każdy poziom mipmapy jest odrobinę pociemniony (raytracing-book, Dodatek G, opisuje mechanikę mipmappingu samą w sobie).
Blending i crossfade w kompozytingu — dissolve między dwoma ujęciami, rozmycie ruchu liczone na już zakodowanym obrazie, czy nawet zwykłe „opacity 50%” w złym trybie — wszystkie cierpią na ten sam błąd, jeśli operują na wartościach display-referred zamiast scene-referred.
Etap: Dyscyplina
Linear light workflow jako reguła, nie wyjątek
Linear light workflow to nazwa dla prostej zasady porządkującej wszystko powyżej: dekoduj na wejściu, licz w scene-linear, koduj dopiero na wyjściu — i nigdzie pomiędzy. „Scene-linear” oznacza tu przestrzeń, w której liczby są wprost proporcjonalne do fizycznej ilości światła (tak jak w formule z pierwszej sekcji) — cały shading, cały light transport, całe blending i filtering z poprzedniej sekcji powinny się odbywać właśnie w niej. Kodowanie percepcyjne (gamma, log) pojawia się tylko w dwóch miejscach: na wejściu, przy odczycie tekstur kolorowych zapisanych percepcyjnie (dekodowanie w drugą stronę), i na wyjściu, przy wysłaniu finalnego obrazu na ekran (display transform).
Linear.light.pipelinedecode → compute → encode
Jedyne miejsca z kodowaniem percepcyjnym to skrajne kolumny — wejście i wyjście. Cała środkowa strefa, gdzie dzieje się shading, liczy wyłącznie na wartościach liniowych.
Zwróć uwagę, że sam mechanizm dekodowania i kodowania to na razie czarna skrzynka — „jakaś krzywa gamma”. Rozdział 5 pokazuje, czym dokładnie ta krzywa jest (i że prawdziwe sRGB to coś więcej niż czysta potęga), a Rozdział 6 pokazuje, jak w praktyce ten cały pipeline konfiguruje się jednym configiem OCIO zamiast ręcznie w każdym node'zie. Ta sama zasada „dekoduj na wejściu, koduj na wyjściu” jest już wspomniana pobieżnie w raytracing-book (Dodatek E) — tam jako ciekawostka zamykająca temat renderowania; tutaj jest to fundament, na którym stoi reszta książki.
Słowniczek
Liniowość radiometryczna
Własność, w której podwojenie fizycznej ilości światła odpowiada dokładnie podwojeniu zapisanej liczby.
Scene-linear
Przestrzeń wartości proporcjonalnych do fizycznego światła w scenie — tu odbywa się cały shading i light transport.
Display-referred
Wartości zakodowane percepcyjnie pod konkretny ekran/wyjście — nie nadają się do dalszych obliczeń światła.
Decode / Encode
Przejście z wartości zakodowanej percepcyjnie do scene-linear (decode) i z powrotem (encode).
Co dalej
Sama liniowość jeszcze nie mówi, które RGB. Ta sama liczba „czerwony = 0,8” oznacza inny fizyczny kolor w sRGB niż w ACEScg — Rozdział 4 pokazuje, skąd się bierze ta różnica i jak jedna macierz 3×3 tłumaczy między dowolnymi dwiema przestrzeniami barw.