SDF Realtime Schaduwen en Ambient Occlusion

EN NL ES PT-BR


SDF-gebaseerde renderingeffecten gaan verder dan de sphere-traced primaire oppervlakken die in het hub-artikel worden behandeld. Afstandsvelden drijven ook twee belichtingseffecten aan die moeilijk goed te krijgen zijn met traditionele shadow maps: zachte schaduwen met vloeiende halfschaduw, en ambient occlusion zonder een screen-space pass. Dit artikel legt de wiskunde achter beide uit, loopt door de GLSL en vergelijkt de resultaten met de standaard shadow mapping-pijplijn.

Voor de fundamenten van sphere tracing en de SDF-gradiënt, begin met het Signed Distance Fields overzicht . Voor de engine-brede context, zie SDF’s in game-ontwikkeling .

Zachte Schaduwen met Afstandsvelden

Zachte schaduwen van oppervlaktelichten zijn duur met shadow mapping omdat je de lichtbron op meerdere posities moet samplen en de resultaten moet middelen. Shadow maps aliassen ook slecht onder kleine hoeken en hebben cascades nodig voor grote scènes.

Zachte schaduwen met afstandsvelden benaderen de halfschaduw van oppervlaktelichten door een secundaire straal van elk oppervlaktepunt naar de lichtbron te marcheren en te registreren hoe dicht de straal langs occulterende geometrie passeert. De minimale afstand-tot-occulder langs de straal bepaalt de zachtheid van de schaduw:

float sdfSoftShadow(vec3 origin, vec3 lightDir, float maxDist, float softness,
                    SDF scene) {
    float res = 1.0;
    float t = 0.02;  // Start slightly off the surface to avoid self-occlusion

    for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
        float d = evaluateSDF(origin + lightDir * t, scene);
        // The closer the ray passes to an occluder, the darker the shadow
        res = min(res, softness * d / t);
        t += clamp(d, 0.01, 0.5);
        if (res < 0.001 || t > maxDist) break;
    }
    return clamp(res, 0.0, 1.0);
}

Het belangrijkste inzicht zit in de regel softness * d / t. Wanneer de straal ver van elk oppervlak passeert, blijft d groot en blijft res dicht bij 1,0 (volledig verlicht). Wanneer de straal dicht langs een occulder scheert, wordt d klein, krimpt d / t en daalt res naar bijna 0,0 nabij het contact. De softness-parameter bepaalt hoe breed de halfschaduw is: grotere waarden spreiden de schaduwrand over een breder gebied.

Een uitgewerkt voorbeeld maakt de verhouding concreet. Op stapafstand t=1.0t = 1.0 langs de lichtstraal leest het veld d=0.1d = 0.1, wat betekent dat de straal 0,1 eenheden van een oppervlak passeert. Met softness = 8.0 is de bijdrage 8.00.1/1.0=0.88.0 \cdot 0.1 / 1.0 = 0.8, dus die stap dimt het resultaat naar 80% helderheid. Als de straal twee keer zo dichtbij passeert, d=0.05d = 0.05, is de bijdrage 8.00.05/1.0=0.48.0 \cdot 0.05 / 1.0 = 0.4, en valt het punt terug naar 40% helderheid. De halfschaduw is de regio waar deze verhouding interpoleert tussen de volledig verlichte en volledig beschaduwde waarden, en de softness-parameter rekt of comprimeert die regio.

Dit produceert vloeiende, fysiek plausibele schaduwen met één straal per oppervlaktepunt, zonder shadow map-resolutielimieten of cascade-naden. De kost zijn de extra ray-march-stappen in de schaduwlus, maar die stappen evalueren dezelfde SDF die de primaire straal al gebruikte, dus er zijn geen nieuwe datastructuren nodig. Wanneer de scène al een SDF is, is de schaduwpass gewoon een tweede lus over dezelfde functie.

SDF Ambient Occlusion

Ambient occlusion meet hoezeer een oppervlaktepunt wordt afgeschermd door nabije geometrie. In een mesh-pijplijn vereist dit screen-space sampling (SSAO) of voorgebakken occlusiekaarten. Met een SDF kan ambient occlusion direct worden berekend tijdens de shading-pass door het afstandsveld te samplen in een halfrond rond het oppervlaktepunt:

float sdfAmbientOcclusion(vec3 position, vec3 normal, float radius,
                           SDF scene) {
    float occlusion = 0.0;
    float weightSum = 1e-6;

    for (int i = 0; i < AO_SAMPLES; i++) {
        vec3 dir = sampleHemisphere(normal, i, AO_SAMPLES);
        float t = 0.01;
        float sampleOcclusion = 0.0;

        // March a short ray into the hemisphere
        for (int j = 0; j < AO_STEPS; j++) {
            float d = evaluateSDF(position + dir * t, scene);
            // If we are inside nearby geometry, accumulate occlusion
            sampleOcclusion += max(0.0, -d) / (1.0 + t);
            t += max(abs(d), 0.01);
            if (t > radius) break;
        }

        float weight = 1.0 / (1.0 + t);
        occlusion += sampleOcclusion * weight;
        weightSum += weight;
    }
    return 1.0 - occlusion / weightSum;
}

Elke samplerichting marchet een korte straal naar buiten vanaf het oppervlak en accumuleert occlusie telkens wanneer de afstand negatief wordt (binnen nabije geometrie). De 1.0 + t-noemer zorgt dat nabije occulders meer bijdragen dan verre. Een samplerstraal die geometrie raakt vlak naast het oppervlaktepunt accumuleert een grote occlusieterm; een straal die uitkomt in open ruimte accumuleert niets en draagt een groot gewicht bij aan het uiteindelijke gemiddelde, waardoor het resultaat helder blijft.

Een uitgewerkt voorbeeld: een samplerstraal op stapafstand t=0.5t = 0.5 leest d=0.2d = -0.2, wat betekent dat de straal binnen nabije geometrie is. De bijgedragen term is max(0,0.2)/(1+0.5)=0.133\max(0, 0.2) / (1 + 0.5) = 0.133 en het gewicht voor deze richting is 1/(1+0.5)=0.6671 / (1 + 0.5) = 0.667, dus deze richting trekt het gemiddelde naar occlusie. Op open grond leest elke samplerichting positieve afstanden, is elke occlusieterm nul en blijft het resultaat 1.0.

Dit produceert contactschaduwen in spleten en onder overhangen zonder een aparte screen-space pass of voorberekening. Het deelt de bestaande SDF van de renderer, dus dynamische geometrie wordt automatisch afgehandeld: verplaats een blok en de AO-respons beweegt mee, zonder opnieuw bakken en zonder screen-space-artefacten.

Vergelijking met Shadow Maps

EigenschapShadow MapsSDF-schaduwen
OpzetkostenEén render-pass per lichtGeen (gebruikt bestaande SDF)
Kosten per pixelEén texture-sample (hard) / meerdere (zacht)Meerdere SDF-evaluaties
AliasingBeperkt door resolutie, cascades nodigGlad van nature
Dynamische geometrieGratis (shadow map opnieuw renderen)Gratis (SDF-updates met scène)
Grote wereldenGecascadeerde shadow maps nodigEnkele SDF dekt alles
Halfdoorzichtige schaduwenMoeilijkNatuurlijk (afstandsgebaseerd)

SDF-schaduwen zijn geen universele vervanging voor shadow maps. Shadow maps zijn sneller voor harde schaduwen van verre directionele lichten in statische scènes. SDF-schaduwen blinken uit wanneer je zachte halfschaduw nodig hebt, wanneer de scène al als SDF is weergegeven, of wanneer dynamische geometrie de occlusie elke frame verandert.

De beperkingen zijn het waard om ronduit te vermelden. De schaduw- en AO-lussen voegen ray-march-kosten toe bovenop de primaire straal, dus een scène die volledig mesh-gerasteriseerd is met screen-space-schaduwen kan in totaal goedkoper zijn. En de kwaliteit van beide effecten hangt af van de afstandsgarantie van het veld: een gesampeld of vereffend veld dat afstand overschat kan schaduwstralen door dunne occulders laten stappen, dus dit soort effecten zijn het meest betrouwbaar op exacte analytische velden of zorgvuldig gebakken grids met conservatieve stapbegrenzingen.

Samenvatting

Twee belichtingseffecten komen voort uit hetzelfde SDF-contract:

  • Zachte schaduwen marcheren een straal naar het licht en registreren de dichtste nadering tot geometrie, softness * d / t, die de halfschaduwbreedte schat uit één straal.
  • Ambient occlusion marchet korte stralen in een halfrond en accumuleert -d / (1 + t) waar het veld negatief wordt, wat contactschaduwen produceert zonder screen-space pass.

Beide effecten hergebruiken de scène-SDF, dus dynamische geometrie is gratis en er gelden geen shadow map-resolutielimieten. Wanneer de rest van de scène al veldgebaseerd is, zijn zij het natuurlijke belichtingspad; de vernietigbaar-terrein-deep-dive toont hetzelfde veld dat de geometrie aanstuurt die deze passes verlichten.