Around The World, Deel 34: Een prachtige lucht
In het vorige bericht heb ik de wolken in het spel een acceptabel uiterlijk gegeven. Tijdens het playtesten was ik echter nog steeds niet tevreden over het totaalbeeld van de zee, de lucht en de wolken. Met enkele kleine aanpassingen is het resultaat nu aanzienlijk verbeterd.
Lichtverstrooiing in wolken
In het vorige bericht eindigde ik met een lookup table (LUT) die was gegenereerd met povray. Deze tabel bevat de hoeveelheid licht die door wolken wordt verstrooid, afhankelijk van de lichthoek (horizontale as) en de wolkenintensiteit (verticale as).
Ik uitte toen mijn ontevredenheid over deze tabel:
"Ik begin te vermoeden dat povray helemaal geen multiple scattering simuleert; de documentatie geeft hier geen duidelijk antwoord op. Ongeacht hoeveel samples en intervallen ik configureer, ik krijg de onderkant van die sferen niet lichter."
Ik was in Toen opnieuw geneigd om mijn eigen recursieve ray tracer te schrijven om mijn vermoedens te bevestigen, maar ik hield me aanvankelijk in.
Uiteindelijk won de nieuwsgierigheid en schreef ik in een paar honderd regels Rust mijn eigen multithreaded recursieve Monte Carlo ray tracer. Het is een zeer eenvoudig programma dat specifiek is hardcoded voor deze situatie; het rendert slechts één perfecte bolvormige wolk met een uniforme dichtheid. Dat was echter voldoende om te bevestigen dat povray inderdaad geen multiple scattering simuleert. Met multiple scattering ziet de lookup table er heel anders uit.
De vergelijking is niet volledig gelijkwaardig, omdat ik ook enkele parameters en de schaal van de assen heb aangepast. Maar over het algemeen is het effect duidelijk: het licht dat uit de wolk komt, is veel gelijkmatiger verdeeld, waardoor de wolk over het algemeen lichter oogt.
Bij een lage dichtheid (dunne wolken) is het verschil niet erg opvallend; de bovenste rijen van de lookup tables lijken sterk op elkaar. Bij een lage dichtheid is er immers weinig kans dat een lichtstraal meer dan één keer wordt verstrooid. Als we kijken naar een hogere dichtheid, wat staat voor een dikkere wolkenlaag, is het verschil duidelijker. Met de oude LUT zagen de wolken er "metaalachtig" uit. Met de nieuwe LUT zijn de onderkant van de wolken helderder en is de belichting over het algemeen vloeiender.
Een vreemde truc
In de realiteit worden wolken niet alleen direct door de zon verlicht. Ze ontvangen en verstrooien ook indirect zonlicht uit de lucht. Omdat dit licht meestal een blauwe tint heeft, zouden de wolken ook een meer blauwachtige kleur moeten aannemen.
Ik wilde geen constante "blauwe" waarde toevoegen, omdat dat alleen correct is overdag; ik wil dat de wolken de huidige kleur van de lucht volgen, ook tijdens zonsopgang, zonsondergang of 's nachts. De vraag was hoe ik dit in Godot zou kunnen realiseren.
Het antwoord blijkt simpel, maar vereist enige kennis van hoe de lichtberekening van Godot werkt. Naast direct licht houdt Godot ook rekening met indirect licht uit de omgeving. Dit gebeurt grotendeels onder de motorkap, buiten de directe controle van de gebruiker.
Voor zover ik begrijp, wordt indirect licht gesampled uit één of twee cube maps die de radiance en irradiance vanuit elke richting bevatten. Voor diffuse materialen zoals wolken zou ik verwachten dat deze cube map wordt gesampled in de richting van de surface normal (de normaalvector van het oppervlak), aangezien dit aangeeft waar het meeste indirecte licht vandaan komt.
Aan de onderkant van wolken wijst deze normaal naar beneden, richting de grond, waardoor de wolk daar met weinig indirect licht wordt gerenderd. In werkelijkheid ontvangt hij daar wel indirect licht, maar vanuit de lucht: dit licht wordt vervolgens door de wolk verstrooid. Wat zou er gebeuren als we de normaal zo aanpassen dat deze naar boven, richting de lucht, wijst?
Het effect is subtiel maar belangrijk. De wolken hebben nu inderdaad een blauwachtige tint, waardoor ze veel beter versmelten met de lucht en over het algemeen een zachtere, meer distante uitstraling krijgen.
Ook bij een zonsondergang, wanneer de lucht niet uniform blauw is, werkt dit. Normaal gesproken zijn de onderkant van de wolken oranje, omdat het binnenkomende directe zonlicht oranje is door zware Rayleigh outscattering. Met de aangepaste normalen kleuren de wolken echter roze. Dit komt veel dichter bij hoe echte zonsondergangen eruitzien; de roze tint is blijkbaar het resultaat van het mengen van rood (direct zonlicht) en blauw (indirect licht uit de lucht), waarbij het groene kanaal ertussenin verloren gaat.
Atmosferische verstrooiing
Een ander punt dat me stoorde, was dat de lucht bij de horizon onnatuurlijk geel leek. Dit is verwacht wanneer de zon laag staat, maar het gebeurde zelfs wanneer de zon recht boven het hoofd stond.
Ik vermoedde dat dit kwam door een te grote hoeveelheid outscattering tijdens de raymarch in vergelijking met inscattering. Na verschillende pogingen bleek de oplossing te liggen in het veranderen van de volgorde van de operaties tijdens de raymarch.
In plaats van:
- Voeg (blauwachtig) licht toe dat in het straalsegment wordt verstrooid.
- Demp door outscattering over het straalsegment (verroding).
Heb ik de volgorde omgedraaid:
- Demp door outscattering over het straalsegment (verroding).
- Voeg (blauwachtig) licht toe dat in het straalsegment wordt verstrooid.
Het resultaat is dat er minder demping plaatsvindt in vergelijking met de inscattering, wat er veel beter uitziet. Theoretisch zou dit een artefact moeten zijn van de numerieke integratiemethode, waardoor het verschil zou moeten verdwijnen bij een hoger aantal integratiestappen. Dat gebeurt echter niet, dus ik vermoed dat er ergens nog iets mis is. Voor nu is het echter voldoende.
Buiging van de horizon
Om een realistische lucht te renderen, moet ik parameters van de echte aarde gebruiken, zoals atmosferische dichtheid, hoogte en planeetgrootte. De planeet in mijn spel is echter ongeveer 75 keer kleiner dan de aarde, waardoor deze sterker gekromd is. Wanneer we aardse parameters gebruiken voor de lucht, komt de horizon van de lucht boven de eigenlijke horizon van de planeet te liggen.
Tussen deze twee horizons in doet de sky shader alsof de atmosferische dichtheid exponentieel blijft toenemen richting het centrum van de planeet. Dit veroorzaakt een donkere band die vooral opvalt tijdens zonsopgang en zonsondergang.
Om de twee horizons op elkaar te laten aansluiten, heb ik een simpele truc toegepast in de sky shader: ik voeg een kleine waarde toe aan de y-component (verticaal) van de globale view vector en normaliseer deze vervolgens. Het effect is dat de gehele lucht naar beneden wordt getrokken, het sterkst rond de horizon, waardoor de donkere band achter de echte horizon verdwijnt. Het is een hack, maar goedkoop en effectief.
Tone mapping
Games worden tegenwoordig gerenderd in HDR (high dynamic range), wat betekent dat kleurwaarden helderder kunnen zijn dan het maximum van 1.0. Als we deze op een typische monitor weergeven, moeten ze worden afgeclipped, wat resulteert in vlakken met oververzadigde kleuren.
Om dit op te lossen, wordt er meestal een tone mapping-formule gebruikt om de HDR-waarden terug te brengen naar de LDR-ruimte voor weergave. Godot implementeert verschillende tone mappers. Ik vond Reinhard te donker, en ACES en AgX te onverzadigd. Uiteindelijk heb ik gekozen voor de Filmic tone mapper met een white point van 2.0.
Dit geeft vooral de wolken een mooie zachte look en zorgt ervoor dat ze beter in de verte vervagen. De keuze van het white point is subtiel; Godot adviseert minstens 6.0 voor "fotorealistische rendering", maar dat maakt alles te donker.
Het afstemmen van de tone mapping is een lastig proces, omdat wijzigingen hierin invloed hebben op alles wat eraan voorafging. In de praktijk was dit proces niet zo lineair als hier beschreven; er was veel heen-en-weer schakelen tussen het aanpassen van de tone mapper en de belichting van de scène. Zo heb ik de helderheid van de lucht met een factor 2 verminderd, wat na de tone mapping zorgde voor een veel beter contrast.
Het planetarium (The Orrery)
Voor een correcte dag-nachtcyclus moet de zon opkomen en ondergaan. Om de planeet aards te laten aanvoelen, moeten de tijdstippen van zonsopgang en zonsondergang afhangen van de locatie en het seizoen. Deze afhankelijkheid wordt veroorzaakt door de beweging van de aarde om de zon en om haar eigen as. Hetzelfde geldt voor de beweging van de maan door de lucht en de maanfasen.
Het leek ingewikkeld om alle wiskunde voor deze berekeningen correct uit te werken. Daarom koos ik voor een makkelijkere route: ik modelleerde de volledige situatie in de engine, waarbij de planeet en zijn maan om elkaar heen draaien, zoals in een planetarium (orrery).
Vervolgens liet ik de engine de inverse transformatie berekenen om te bepalen waar de hemellichamen staan vanuit het perspectief van een waarnemer op het oppervlak van de planeet. Dit werkte goed, en het gerenderde planetarium bleek bovendien een nuttig hulpmiddel voor het debuggen.
Around The World, Deel 34: Een prachtige lucht
In het vorige bericht heb ik de wolken in het spel een acceptabel uiterlijk gegeven. Tijdens het playtesten was ik echter nog steeds niet tevreden over het totaalbeeld van de zee, de lucht en de wolken. Met enkele kleine aanpassingen is het resultaat nu aanzienlijk verbeterd.
Lichtverstrooiing in wolken
In het vorige bericht eindigde ik met een lookup table (LUT) die was gegenereerd met povray. Deze tabel bevat de hoeveelheid licht die door wolken wordt verstrooid, afhankelijk van de lichthoek (horizontale as) en de wolkenintensiteit (verticale as).
Ik uitte toen mijn ontevredenheid over deze tabel:
"Ik begin te vermoeden dat povray helemaal geen multiple scattering simuleert; de documentatie geeft hier geen duidelijk antwoord op. Ongeacht hoeveel samples en intervallen ik configureer, ik krijg de onderkant van die sferen niet lichter."
Ik was in Toen opnieuw geneigd om mijn eigen recursieve ray tracer te schrijven om mijn vermoedens te bevestigen, maar ik hield me aanvankelijk in.
Uiteindelijk won de nieuwsgierigheid en schreef ik in een paar honderd regels Rust mijn eigen multithreaded recursieve Monte Carlo ray tracer. Het is een zeer eenvoudig programma dat specifiek is hardcoded voor deze situatie; het rendert slechts één perfecte bolvormige wolk met een uniforme dichtheid. Dat was echter voldoende om te bevestigen dat povray inderdaad geen multiple scattering simuleert. Met multiple scattering ziet de lookup table er heel anders uit.
De vergelijking is niet volledig gelijkwaardig, omdat ik ook enkele parameters en de schaal van de assen heb aangepast. Maar over het algemeen is het effect duidelijk: het licht dat uit de wolk komt, is veel gelijkmatiger verdeeld, waardoor de wolk over het algemeen lichter oogt.
Bij een lage dichtheid (dunne wolken) is het verschil niet erg opvallend; de bovenste rijen van de lookup tables lijken sterk op elkaar. Bij een lage dichtheid is er immers weinig kans dat een lichtstraal meer dan één keer wordt verstrooid. Als we kijken naar een hogere dichtheid, wat staat voor een dikkere wolkenlaag, is het verschil duidelijker. Met de oude LUT zagen de wolken er "metaalachtig" uit. Met de nieuwe LUT zijn de onderkant van de wolken helderder en is de belichting over het algemeen vloeiender.
Een vreemde truc
In de realiteit worden wolken niet alleen direct door de zon verlicht. Ze ontvangen en verstrooien ook indirect zonlicht uit de lucht. Omdat dit licht meestal een blauwe tint heeft, zouden de wolken ook een meer blauwachtige kleur moeten aannemen.
Ik wilde geen constante "blauwe" waarde toevoegen, omdat dat alleen correct is overdag; ik wil dat de wolken de huidige kleur van de lucht volgen, ook tijdens zonsopgang, zonsondergang of 's nachts. De vraag was hoe ik dit in Godot zou kunnen realiseren.
Het antwoord blijkt simpel, maar vereist enige kennis van hoe de lichtberekening van Godot werkt. Naast direct licht houdt Godot ook rekening met indirect licht uit de omgeving. Dit gebeurt grotendeels onder de motorkap, buiten de directe controle van de gebruiker.
Voor zover ik begrijp, wordt indirect licht gesampled uit één of twee cube maps die de radiance en irradiance vanuit elke richting bevatten. Voor diffuse materialen zoals wolken zou ik verwachten dat deze cube map wordt gesampled in de richting van de surface normal (de normaalvector van het oppervlak), aangezien dit aangeeft waar het meeste indirecte licht vandaan komt.
Aan de onderkant van wolken wijst deze normaal naar beneden, richting de grond, waardoor de wolk daar met weinig indirect licht wordt gerenderd. In werkelijkheid ontvangt hij daar wel indirect licht, maar vanuit de lucht: dit licht wordt vervolgens door de wolk verstrooid. Wat zou er gebeuren als we de normaal zo aanpassen dat deze naar boven, richting de lucht, wijst?
Het effect is subtiel maar belangrijk. De wolken hebben nu inderdaad een blauwachtige tint, waardoor ze veel beter versmelten met de lucht en over het algemeen een zachtere, meer distante uitstraling krijgen.
Ook bij een zonsondergang, wanneer de lucht niet uniform blauw is, werkt dit. Normaal gesproken zijn de onderkant van de wolken oranje, omdat het binnenkomende directe zonlicht oranje is door zware Rayleigh outscattering. Met de aangepaste normalen kleuren de wolken echter roze. Dit komt veel dichter bij hoe echte zonsondergangen eruitzien; de roze tint is blijkbaar het resultaat van het mengen van rood (direct zonlicht) en blauw (indirect licht uit de lucht), waarbij het groene kanaal ertussenin verloren gaat.
Atmosferische verstrooiing
Een ander punt dat me stoorde, was dat de lucht bij de horizon onnatuurlijk geel leek. Dit is verwacht wanneer de zon laag staat, maar het gebeurde zelfs wanneer de zon recht boven het hoofd stond.
Ik vermoedde dat dit kwam door een te grote hoeveelheid outscattering tijdens de raymarch in vergelijking met inscattering. Na verschillende pogingen bleek de oplossing te liggen in het veranderen van de volgorde van de operaties tijdens de raymarch.
In plaats van:
- Voeg (blauwachtig) licht toe dat in het straalsegment wordt verstrooid.
- Demp door outscattering over het straalsegment (verroding).
Heb ik de volgorde omgedraaid:
- Demp door outscattering over het straalsegment (verroding).
- Voeg (blauwachtig) licht toe dat in het straalsegment wordt verstrooid.
Het resultaat is dat er minder demping plaatsvindt in vergelijking met de inscattering, wat er veel beter uitziet. Theoretisch zou dit een artefact moeten zijn van de numerieke integratiemethode, waardoor het verschil zou moeten verdwijnen bij een hoger aantal integratiestappen. Dat gebeurt echter niet, dus ik vermoed dat er ergens nog iets mis is. Voor nu is het echter voldoende.
Buiging van de horizon
Om een realistische lucht te renderen, moet ik parameters van de echte aarde gebruiken, zoals atmosferische dichtheid, hoogte en planeetgrootte. De planeet in mijn spel is echter ongeveer 75 keer kleiner dan de aarde, waardoor deze sterker gekromd is. Wanneer we aardse parameters gebruiken voor de lucht, komt de horizon van de lucht boven de eigenlijke horizon van de planeet te liggen.
Tussen deze twee horizons in doet de sky shader alsof de atmosferische dichtheid exponentieel blijft toenemen richting het centrum van de planeet. Dit veroorzaakt een donkere band die vooral opvalt tijdens zonsopgang en zonsondergang.
Om de twee horizons op elkaar te laten aansluiten, heb ik een simpele truc toegepast in de sky shader: ik voeg een kleine waarde toe aan de y-component (verticaal) van de globale view vector en normaliseer deze vervolgens. Het effect is dat de gehele lucht naar beneden wordt getrokken, het sterkst rond de horizon, waardoor de donkere band achter de echte horizon verdwijnt. Het is een hack, maar goedkoop en effectief.
Tone mapping
Games worden tegenwoordig gerenderd in HDR (high dynamic range), wat betekent dat kleurwaarden helderder kunnen zijn dan het maximum van 1.0. Als we deze op een typische monitor weergeven, moeten ze worden afgeclipped, wat resulteert in vlakken met oververzadigde kleuren.
Om dit op te lossen, wordt er meestal een tone mapping-formule gebruikt om de HDR-waarden terug te brengen naar de LDR-ruimte voor weergave. Godot implementeert verschillende tone mappers. Ik vond Reinhard te donker, en ACES en AgX te onverzadigd. Uiteindelijk heb ik gekozen voor de Filmic tone mapper met een white point van 2.0.
Dit geeft vooral de wolken een mooie zachte look en zorgt ervoor dat ze beter in de verte vervagen. De keuze van het white point is subtiel; Godot adviseert minstens 6.0 voor "fotorealistische rendering", maar dat maakt alles te donker.
Het afstemmen van de tone mapping is een lastig proces, omdat wijzigingen hierin invloed hebben op alles wat eraan voorafging. In de praktijk was dit proces niet zo lineair als hier beschreven; er was veel heen-en-weer schakelen tussen het aanpassen van de tone mapper en de belichting van de scène. Zo heb ik de helderheid van de lucht met een factor 2 verminderd, wat na de tone mapping zorgde voor een veel beter contrast.
Het planetarium (The Orrery)
Voor een correcte dag-nachtcyclus moet de zon opkomen en ondergaan. Om de planeet aards te laten aanvoelen, moeten de tijdstippen van zonsopgang en zonsondergang afhangen van de locatie en het seizoen. Deze afhankelijkheid wordt veroorzaakt door de beweging van de aarde om de zon en om haar eigen as. Hetzelfde geldt voor de beweging van de maan door de lucht en de maanfasen.
Het leek ingewikkeld om alle wiskunde voor deze berekeningen correct uit te werken. Daarom koos ik voor een makkelijkere route: ik modelleerde de volledige situatie in de engine, waarbij de planeet en zijn maan om elkaar heen draaien, zoals in een planetarium (orrery).
Vervolgens liet ik de engine de inverse transformatie berekenen om te bepalen waar de hemellichamen staan vanuit het perspectief van een waarnemer op het oppervlak van de planeet. Dit werkte goed, en het gerenderde planetarium bleek bovendien een nuttig hulpmiddel voor het debuggen.