Ray Marching: gesigneerde afstandsvelden, sphere tracing en SDF-belichting


Ray marching is een rendermethode die oppervlakken vindt door herhaaldelijk langs een straal vooruit te bewegen, in plaats van vooraf een exacte geometrische snijformule op te lossen. De methode wordt het vaakst uitgelegd samen met signed distance fields (SDF’s), omdat een SDF aangeeft hoe ver je kunt bewegen voordat je mogelijk iets raakt. Daardoor is het basisidee eenvoudig: begin bij de camera, vraag de scène om de afstand tot het dichtstbijzijnde oppervlak, beweeg dat stuk naar voren en herhaal totdat je dicht genoeg bent om het als een treffer te tellen.

Dit artikel richt zich op het kernmodel achter ray marching, niet op één specifieke shader-implementatie. Als je begrijpt waarom de afstandswaarde een veilige stapgrootte geeft, waarom herhaalde evaluatie naar het oppervlak convergeert en hoe normalen uit het veld komen, wordt de meeste shadercode veel makkelijker om te lezen. De interactieve secties hieronder blijven bewust in 2D, zodat elk geometrisch idee zichtbaar blijft. Precies dezelfde logica werkt ook voor 3D-scènes die in fragment shaders worden gerenderd.

Ray Marching en Sphere Tracing

De term ray marching is breed. Hij verwijst naar het vooruitgaan langs een straal in stappen, terwijl je de scène herhaaldelijk controleert. Die stappen kunnen vast, adaptief, stochastisch of gebaseerd op geaccumuleerde volumetrische dichtheid zijn. In graphics-tutorials betekent ray marching meestal echter sphere tracing over een signed distance field.

Een signed distance field geeft de kortste afstand terug van een punt tot het dichtstbijzijnde oppervlak, met een tekenconventie:

  • positief buiten het object
  • nul op het oppervlak
  • negatief binnen het object

Als het veld correct is, betekent een waarde van d(p)d(p) dat er geen oppervlak dichterbij ligt dan die afstand vanaf het huidige samplepunt pp. Daardoor kun je exact d(p)d(p) naar voren springen zonder over het dichtstbijzijnde oppervlak heen te schieten. In plaats van veel kleine vaste stappen te nemen, neem je de grootste veilige stap die het veld toestaat. Dat is de reden dat ray marching met SDF’s verrassend efficiënt kan zijn.

Veilige stappen uit het afstandsveld

De eerste visualisatie toont één straal en één cirkel-SDF. Elke oranje ring is de afstand die op het huidige samplepunt wordt teruggegeven. Omdat de cirkel het dichtstbijzijnde oppervlak raakt maar niet kruist, kan de straal veilig naar de rand van die ring bewegen.

Distance-Guided Stepping

Move the ray angle and sphere position to see how the marcher advances by safe distances.

pn+1=pn+d(pn)r^p_{n+1} = p_n + d(p_n)\,\hat{r}

Wanneer de straal direct naar de cirkel wijst, wordt de teruggegeven afstand snel kleiner en convergeren de stappen naar de rand. Wanneer de straal ervan af wijst, kunnen de afstanden juist groeien en stopt het algoritme zonder treffer. Dat is in feite al bijna het hele idee van sphere tracing. De update-regel is simpel:

pn+1=pn+d(pn)r^p_{n+1} = p_n + d(p_n)\,\hat{r}

waarbij r^\hat{r} de richting van de straal is. Het belangrijke detail is dat de stapgrootte niet willekeurig is. Die komt direct uit de geometrie van het veld zelf. Dat is het verschil tussen SDF-ray-marching en naïeve marching met vaste stapgrootte.

In een shader stop je meestal wanneer een van deze drie voorwaarden optreedt:

  1. de afstand zakt onder een kleine trefferdrempel zoals 0.001
  2. de opgetelde afgelegde afstand overschrijdt een maximaal bereik
  3. de lus bereikt een maximaal aantal stappen

Alle drie de limieten zijn belangrijk. De trefferdrempel bepaalt de precisie, het maximale bereik begrenst het werk voor stralen die niets raken en de staplimiet beschermt tegen lastige gevallen of slecht gedragen afstandsschatters.

Een SDF-scène opbouwen

Eén bol is nuttig voor intuïtie, maar ray marching wordt pas echt krachtig wanneer een hele scène als één afstandsfunctie beschreven kan worden. Dat doe je door primitieve SDF’s te combineren. In 2D is een cirkel de afstand tot een middelpunt minus de straal. Een lijn of vlak kan een vloer voorstellen. Om ze te combineren neem je meestal de minimumafstand tot alle objecten, omdat het dichtstbijzijnde oppervlak de volgende veilige stap bepaalt.

2D Ray Marching Scene

Cast a small fan of rays through circles and a ground line to see how the minimum SDF controls every step.

In de scene explorer vraagt elke straal herhaaldelijk de minimumwaarde van de cirkelafstanden en de afstand tot de grond op. Dat minimum is om een eenvoudige reden belangrijk: als één object dichtbij is en een ander ver weg, dan bepaalt het nabije object de grootste veilige stap. Als je de verkeerde, grotere afstand gebruikt, kun je over het dichterbij liggende oppervlak heen springen.

Deze regel van “minimum van primitieve vormen” is de basis van constructieve SDF-modellering. Veelgebruikte combinaties zijn:

  • min(a, b) voor unie
  • max(a, b) voor doorsnede
  • max(a, -b) voor aftrekking

Zodra die combinaties duidelijk zijn, kun je verrassend complexe procedurele scènes bouwen met een kleine verzameling primitieve formules. Een shader heeft geen driehoeksmeshes nodig om een scène te beschrijven. Hij heeft alleen een functie nodig die antwoord geeft op: “hoe ver ben ik nu van het dichtstbijzijnde oppervlak?”

Waarom de methode convergeert

Een veelgestelde beginnersvraag is waarom dit proces niet over dunne geometrie heen springt. Het antwoord is dat sphere tracing vertrouwt op een conservatieve afstandsschatting. Als de SDF exact is, is de teruggegeven waarde een gegarandeerde ondergrens voor de werkelijke afstand tot het dichtstbijzijnde oppervlak. Daardoor landt de volgende stap op of vóór het dichtst mogelijke oppervlak, nooit er voorbij.

Die garantie wordt zwakker wanneer de functie alleen een afstandsschatter is in plaats van een perfect signed distance field. Fractal-rendering is daar een klassiek voorbeeld van. De schatter kan nog steeds goed werken, maar dan hangt de veiligheid van je stap af van hoe conservatief die schatting is. Daarom letten hoogwaardige ray-marching-renderers op veldkwaliteit, Lipschitz-gedrag en zorgvuldig gekozen drempels. De renderer is maar zo betrouwbaar als de afstandsinformatie die hij volgt.

Oppervlaktenormalen uit het veld

Gerasteriseerde meshes slaan meestal vertexnormalen op of leiden ze af uit driehoeken. Een met ray marching gerenderd SDF-oppervlak heeft geen expliciete mesh, dus de normaal moet uit het veld zelf komen. De gebruikelijke aanpak is om het veld op kleine verschuivingen te samplen en de gradiënt te benaderen:

d(p)[d(p+εx)d(pεx)d(p+εy)d(pεy)d(p+εz)d(pεz)]\nabla d(p) \approx \begin{bmatrix} d(p + \varepsilon_x) - d(p - \varepsilon_x) \\ d(p + \varepsilon_y) - d(p - \varepsilon_y) \\ d(p + \varepsilon_z) - d(p - \varepsilon_z) \end{bmatrix}

In 2D heb je alleen de x- en y-verschuivingen nodig. Na normalisatie van die gradiënt krijg je een oppervlaktenormaal die je voor belichting kunt gebruiken.

Gradient Samples and Surface Normal

Probe the SDF just left, right, above, and below a surface point. Their differences reconstruct the normal direction.

In de visualisatie is het zwarte punt een positie op het oppervlak. De rode meetpunten samplen het veld iets links en rechts, en de paarse meetpunten samplen het iets onder en boven. Als de rechter sample verder buiten het object ligt dan de linker, wijst de gradiënt naar rechts. Als de bovenste sample verder buiten ligt dan de onderste, wijst de gradiënt omhoog. Door die twee verschillen te combineren krijg je de groene normale pijl. Dit is een van de nuttigste ideeën in SDF-rendering: hetzelfde veld dat de snijtest stuurt, levert ook een manier om het gevonden oppervlak te belichten. Je vindt dus niet alleen waar het oppervlak is. Je haalt ook direct de lokale oriëntatie uit de functie.

Typische shaderlus

Een fragment-shaderversie ziet er conceptueel meestal zo uit:

  1. bouw een camerastraal voor de huidige pixel
  2. zet t = 0
  3. evalueer sceneSdf(rayOrigin + rayDir * t)
  4. als de afstand onder epsilon ligt, registreer een treffer
  5. verhoog anders t met die afstand en ga door
  6. teken de achtergrond als de lus eindigt zonder treffer
  7. als er een treffer is, schat een normaal en bereken de belichting

De lus is eenvoudig, maar de details eromheen bepalen de beeldkwaliteit. De camera-opstelling beïnvloedt vervorming en compositie. De epsilon-drempel beïnvloedt banding en self-shadow-artifacten. Het maximale aantal stappen bepaalt of schuine stralen netjes eindigen. De kwaliteit van de belichting hangt af van normaalinschatting, schaduwen, ambient-termen, reflecties en welk materiaalmodel je daarna ook bouwt.

Voordelen en afwegingen

Ray marching is aantrekkelijk omdat procedurele geometrie er ongewoon direct mee wordt. Een primitieve vorm is gewoon een formule. Scènecombinatie is vaak slechts een paar min- en max-bewerkingen. Zachte overgangen, herhaling, twists en door ruis vervormde vormen kunnen vaak analytisch worden uitgedrukt zonder mesh-topologie opnieuw op te bouwen. Daarom werd deze techniek populair in shader art, de demoscene en procedurele rendering-experimenten.

De afwegingen zijn net zo echt:

  • elke pixel kan veel scène-evaluaties vereisen
  • dunne of zeer gedetailleerde kenmerken kunnen veel stappen of zorgvuldige drempels nodig hebben
  • zachte schaduwen, ambient occlusion en reflecties voegen nog meer marches toe
  • afstandsschatters die niet conservatief zijn kunnen missers of artifacten veroorzaken
  • grote scènes hebben zorgvuldige acceleratie of domeinontwerp nodig om snel te blijven

Ray marching is dus geen universele vervanging voor rasterisatie of hardwarematige triangle tracing. Je begrijpt het beter als een krachtig procedureel renderinggereedschap met een specifieke sterkte: situaties waarin geometrie eenvoudiger als veld te definiëren is dan als mesh, in contrast met het standaard rasterpad uit vertex vs fragment shaders in de grafische pipeline.

Veelvoorkomende uitbreidingen

Zodra de primaire straaltreffer werkt, worden veel klassieke effecten met precies dezelfde machinerie gebouwd. Schaduwstralen marchen van het oppervlak naar het licht. Ambient occlusion samplet hoe snel nabije geometrie verschijnt langs de richting van de normaal. Reflecties sturen een nieuwe straal vanaf het trefferpunt. Mist en volumetrische effecten accumuleren bijdragen terwijl de straal beweegt. Elk van die effecten hergebruikt hetzelfde centrale patroon van herhaalde scène-evaluatie, en veel van zulke scènes worden nog bruikbaarder wanneer je vormen of materialen moduleert met value noise, Perlin noise en fractale ruis.

Dat hergebruik is een van de redenen dat ray marching zo goed uitlegbaar is. De renderer blijft conceptueel consistent terwijl je meer functies toevoegt. Je vraagt de scène nog steeds om afstandsinformatie en gebruikt die informatie nog steeds om te bepalen hoe ver je daarna beweegt. De complexiteit neemt toe, maar het onderliggende denkmodel verandert niet.

Samenvatting

Ray marching met signed distance fields werkt omdat de scène elke straal vertelt hoe ver hij veilig kan bewegen. Daardoor wordt snijdetectie een iteratief proces dat door geometrie wordt gestuurd in plaats van door vaste stapgroottes. Als je vier ideeën onthoudt, wordt de techniek veel makkelijker om over na te denken:

  1. een SDF geeft de afstand tot het dichtstbijzijnde oppervlak terug, inclusief teken
  2. de minimumafstand in de scène bepaalt de volgende veilige stap
  3. herhaalde veilige stappen convergeren naar het oppervlak wanneer het veld conservatief is
  4. de gradiënt van het veld geeft een bruikbare normaal voor belichting

Die vier onderdelen vormen de kern van de meeste inleidende SDF-shader-renderers. Daarna is het werk vooral engineering: drempels kiezen, scène-evaluatie optimaliseren en belichting of secundaire effecten erbovenop stapelen.