Stylized GGX Shading — Toon Speculaire Highlights

De rendering-vergelijking wordt dan:

float Threshold = 0.0; // elke waarde tussen 0 en 1
vec3 Radiance = BRDF * LightIntensity * step(Threshold, dot(Normal, LightDirection));

(Diffuse Thresholding / Specular Thresholding)

Men kan echter ook de 2D- en 3D-looks willen combineren: grote, scherpe highlights bij een lage ruwheid (roughness) en zachte gradiënten bij een hoge ruwheid.

Deze aanpak voelt moderner aan dan klassieke toon-shading. Bovendien kunnen we voor het grootste deel van de shading vertrouwen op PBR (Physically Based Rendering), wat het proces eenvoudiger maakt (dichter bij energiebehoud, makkelijkere integratie in bestaande pipelines, fysisch gebaseerde materiaalkalibratie, etc.).

Ik noem deze gemengde aanpak "Stylized GGX Shading".

(Video's: Toon / Stylized GGX / GGX)

Veel games hebben geëxperimenteerd met de combinatie van PBR en toon-shading. In Zelda: Breath of the Wild gebruiken de personages bijvoorbeeld pure toon-shading, terwijl de omgevingen meer leunen op een 3D/PBR-look (al is dit niet volledig, denk aan het gras). Hierdoor springen de personages uit de achtergrond.

Een ander voorbeeld is Spellcasters Chronicles, waaraan ik heb mogen werken. Tijdens de ontwikkeling hebben we veel zaken getest, waaronder deze "Stylized GGX Shading". Uiteindelijk zijn we voor iets anders gegaan, maar het maakt nog steeds gebruik van een mix tussen 2D- en 3D-looks.

(Screenshots van Zelda: Tears of the Kingdom en Spellcasters Chronicles)

Eenvoudige versie

Het meest gebruikelijke PBR-model is het GGX Cook-Torrance microfacet-model. De onregelmatigheid van het oppervlak wordt analytisch gemodelleerd als een verzameling georiënteerde microfacetten, die worden aangestuurd door een ruwheidsparameter (roughness).

De distributie van de normalen van deze microfacetten wordt beschreven door de Normal Distribution Function (NDF). Real-time engines maken meestal gebruik van de Trowbridge-Reitz NDF, die er als volgt uitziet:

// GGX Trowbridge-Reitz NDF
float D(vec3 Normal, vec3 HalfVector, float Roughness)
{
    float a = Roughness * Roughness;
    float a2 = a * a;
    float NdotH = max(dot(Normal, HalfVector), 0.0);
    float NdotH2 = NdotH * NdotH;
    float denom = NdotH2 * (a2 - 1.0) + 1.0;
    return a2 / (PI * denom * denom);
}

(GGX Trowbridge-Reitz NDF bij variërende ruwheid)

Door deze vergelijking licht aan te passen, kunnen we de speculaire highlights uitbreiden:

// Stylized GGX Trowbridge-Reitz NDF
float D(vec3 Normal, vec3 HalfVector, float Roughness, float SpecularSize)
{
    float a = Roughness * Roughness;
    float a2 = a * a;
    float NdotH = max(dot(Normal, HalfVector), 0.0);
    float NdotH2 = NdotH * NdotH;
    float denom = max(SpecularSize * NdotH2 * (a2 - 1.0) + 1.0, 0.0001);
    return a2 / (PI * denom * denom);
}

(Stylized GGX Trowbridge-Reitz NDF bij variërende ruwheid)

Om te visualiseren wat er gebeurt, heb ik een Desmos-grafiek gemaakt:

  • De x-as (abscisse) representeert het dot-product van de oppervlaktenormaal en de half-vector (NdotH).
  • De y-as (ordinaat) representeert de NDF-waarde. Een hogere waarde betekent meer gereflecteerde energie.
  • De zwarte curve is de originele Trowbridge-Reitz NDF; de rode curve is de gestileerde versie.

Onze nieuwe parameter "Specular Size" stelt ons in staat om bij hogere hoeken meer energie terug te geven, terwijl de oorspronkelijke vorm van de NDF behouden blijft.

(Desmos-grafiek die de Specular Size animeert) (Resulterende highlight-vorm)

Verbeterde versie

De eenvoudige versie heeft het voordeel dat deze zeer goedkoop is (1 vermenigvuldiging en 1 max), maar het heeft te veel impact op ruwe materialen, waardoor ze overmatig helder lijken.

Hier is een iets complexere vergelijking die de oorspronkelijke NDF-vorm bij een hoge ruwheid behoudt, terwijl de highlights bij een lage ruwheid nog steeds worden uitgebreid:

// Stylized GGX Trowbridge-Reitz NDF (Verbeterd)
float D(vec3 Normal, vec3 HalfVector, float Roughness, float SpecularSize, float GGXMatching)
{
    float a = Roughness * Roughness;
    float a2 = a * a;
    float NdotH = max(dot(Normal, HalfVector), 0.0);
    float NdotH2 = NdotH * NdotH;

    // De GGXMatching parameter bepaalt in hoeverre de gestileerde NDF overeenkomt met de GGX bij hoge ruwheid.
    // In de praktijk werken waarden van 4 of 8 goed.
    float denom = max((SpecularSize * pow(1.0 - a, GGXMatching) + 1.0) * NdotH2 * (a2 - 1.0) + 1.0, 0.0001);
    return a2 / (PI * denom * denom);
}

(Verbeterde Stylized GGX Trowbridge-Reitz NDF. De laatste drie kolommen komen beter overeen met de originele GGX-energiedistributie.)

In de bijbehorende grafiek die de ruwheid animeert (waarbij de curves afvlakken bij hoge ruwheid), is het volgende te zien:

  • Zwarte curve: Trowbridge-Reitz NDF.
  • Rode curve: De eenvoudige gestileerde versie.
  • Groene curve: De verbeterde gestileerde versie.

De verbeterde versie komt overeen met de rode curve bij hoge ruwheid, en met de zwarte curve bij lage ruwheid.

(Desmos-grafiek die de ruwheid animeert)

Demo

Hier is het uiteindelijke resultaat onder variërende ruwheid, metallic-waarden en specular size.

(Video: Stylized GGX Shading)

Aanvullende opmerkingen

Men zou kunnen vinden dat deze gestileerde GGX-look niet "toon" genoeg is. Voor Spellcasters hebben we diverse toevoegingen aan deze techniek getest:

  • Snoeien van GI probes: Alleen 3 SH-coëfficiënten berekenen (globaal, omhoog en omlaag voor de lucht en grond) in plaats van de gebruikelijke 9. Dit vermindert de directionaliteit, waardoor het meer aanvoelt als handgetekende 2D-kunst. Daarnaast bespaart dit prestaties omdat er minder coëfficiënten hoeven te worden opgeslagen. Een soortgelijk idee is gebruikt in Hi-Fi Rush.
  • Vereenvoudigen van verre props: Gebruik van een mix van een depth-based Kuwahara filter, mip biasing en eenvoudige fog.
  • Rim lighting.
  • Outlines en inlines.

En natuurlijk is er veel werk verzet in de textures en het modelleren.