electricsquare / raymarching-workshop

hozta neked Electric Square

készítette és bemutatta AJ Weeks & Huw Bowles

áttekintés

a kép renderelése magában foglalja a kép minden pixelének színének meghatározását, amelyhez meg kell határozni, hogy a pixel mögött milyen felület található a világon, majd árnyékolni kell a végső szín kiszámításához.

a jelenlegi generációs GPU-k háromszöghálókat vesznek bemenetként, raszterezik őket pixelekké (fragmentumoknak nevezzük, mielőtt a kijelzőre rajzolnák őket), majd árnyékolják őket, hogy kiszámítsák a képhez való hozzájárulásukat. Bár ez a csővezeték jelenleg mindenütt jelen van, ez is bonyolult, és nem feltétlenül a legjobb módja annak, hogy megtanulják a grafikát.

alternatív megközelítés az, ha minden pixelen átvetünk egy sugarat, keresztezzük azt a jelenet felületeivel, majd kiszámítjuk az árnyékolást.

ez a kurzus egy technikát vezet be a távmezőkön keresztüli sugárzáshoz. A távolság mező olyan függvény, amely visszaadja, hogy egy adott pont milyen közel van a jelenet legközelebbi felületéhez. Ez a távolság határozza meg az egyes pontok körüli üres tér gömbjének sugarát. Az SDF-ek (Signed distance fields) olyan távolságmezők, amelyek mind az objektumokon belül, mind azon kívül vannak meghatározva; ha a lekérdezett pozíció egy felületen belül van, akkor a távolság negatív, ellenkező esetben pozitív lesz.

mi lehetséges ray marching?

a Claybook játék kizárólag távolságmezőket használ a jelenet ábrázolására. Ez sok érdekes lehetőséget kínál, mint például a teljesen dinamikus felületi topológiák és az alakzat morfológiája. Ezeket a hatásokat háromszöghálókkal nagyon nehéz elérni. További előnyök közé tartozik a könnyen megvalósítható és kiváló minőségű puha árnyékok és a környezeti elzáródás.

https://www.claybookgame.com/

a következő kép is tette valós időben a technikákat fogunk fedezni ma (plusz sok divatos technikák, amelyek nem lesz ideje, hogy belevetik magukat).

itt futtathatja élőben a böngészőben: https://www.shadertoy.com/view/ld3Gz2

SDF (signed distance field) használatával ennek a jelenetnek a geometriáját nem kellett létrehozni egy olyan DCC-ben, mint a Maya, hanem teljesen parametrikusan ábrázolták. Ez triviálissá teszi az alak animálását a jelenet leképezési funkció bemeneteinek egyszerű változtatásával.

az egyéb grafikai effektusokat a raymarching egyszerűbbé teszi a hagyományos raszterizációs alternatívákkal összehasonlítva. A felszín alatti szóráshoz például egyszerűen néhány extra sugarat kell küldeni a felszínre, hogy lássa, milyen vastag. A környezeti elzáródás, az anti-aliasing és a mélységélesség három másik technika, amelyek csak néhány extra vonalat igényelnek, és mégis jelentősen javítják a képminőséget.

Raymarching distance fields

minden sugár mentén haladunk, és keresünk egy metszéspontot a helyszínen lévő felülettel. Ennek egyik módja az lenne, ha a sugár eredetétől indulnánk (a kamera síkján), és egységes lépéseket tennénk a sugár mentén, kiértékelve az egyes pontok távolságmezőjét. Amikor a helyszín távolsága kisebb, mint egy küszöbérték, tudjuk, hogy egy felületet értünk el, ezért megszüntethetjük a raymarch-ot és árnyékolhatjuk azt a pixelt.

hatékonyabb megközelítés az SDF által visszaadott távolság használata a következő lépés méretének meghatározásához. Mint fentebb említettük, az SDF által visszaadott távolság a bemeneti pont körüli üres térgömb sugarának tekinthető. Ezért biztonságos ezt az összeget a sugár mentén lépni, mert tudjuk, hogy nem haladunk át semmilyen felületen.

a raymarching következő 2D-s ábrázolásában minden kör középpontja az, ahonnan a jelenetet mintavételezték. A sugarat ezután ezen a távolságon haladták (a kör sugaráig terjedve), majd újramintázták.

mint látható, az SDF mintavétele nem adja meg a sugár pontos metszéspontját, hanem egy minimális távolságot, amelyet egy felületen való áthaladás nélkül megtehet.

ha ez a távolság egy bizonyos küszöb alatt van, a raymarch megszünteti a pixelt a metszett Felület tulajdonságai alapján.

Játssz körül ezt shader a böngésző itt: (kattintson és húzza a képet, hogy állítsa be a sugár irányát) https://www.shadertoy.com/view/lslXD8

összehasonlítás a sugárkövetéssel

ezen a ponton felmerülhet a kérdés, hogy miért nem számítjuk ki közvetlenül a jelenet metszéspontját analitikus matematika segítségével, a Sugárkövetésnek nevezett technikával. Az offline megjelenítések általában így működnek – a jelenet összes háromszögét valamilyen téradatszerkezetbe indexelik, például a határoló térfogat hierarchia (BVH) vagy kD-fa, amelyek lehetővé teszik a sugár mentén elhelyezkedő háromszögek hatékony metszéspontját.

mi raymarch távolság mezők helyett, mert:

  • nagyon egyszerű a sugáröntési rutin megvalósítása
  • kerüljük a sugár-háromszög kereszteződések és a BVH adatstruktúrák megvalósításának minden bonyolultságát
  • nem kell az explicit jelenetábrázolást írnunk – háromszög hálók, tex koordok, színek stb.
  • a távolságmezők számos hasznos tulajdonságából profitálunk, amelyek közül néhányat fent említettünk

a fentiek alapján van néhány elegáns/egyszerű belépési pont a sugárkövetésbe. A sugárkövetés egy hétvégi ingyenes könyvben (és az azt követő fejezetekben) nagyon ajánlott, és elengedhetetlen olvasmány A grafika iránt érdeklődők számára.

kezdjük!

ShaderToy

a ShaderToy egy shader készítő webhely és platform az árnyékolók böngészésére, megosztására és megvitatására.

miközben egyenesen beugorhat és új shader-t írhat fiók létrehozása nélkül, ez veszélyes, mivel könnyen elveszítheti a munkáját, ha csatlakozási problémák merülnek fel, vagy ha felakasztja a GPU-t (könnyen elvégezhető pl. végtelen hurok létrehozása).Ezért erősen javasoljuk, hogy hozzon létre egy fiókot (ez gyors/egyszerű / ingyenes) itt: https://www.shadertoy.com/signin, és rendszeresen mentse.

a ShaderToy áttekintéséhez és az első lépések útmutatójához javasoljuk egy olyan oktatóanyagot, mint ez a @The_ArtOfCode: https://www.youtube.com/watch?v=u5HAYVHsasc. Az alapok itt szükségesek a Műhely többi részének követéséhez.

2D SDF demo

egyszerű keretet biztosítunk a 2D aláírt távolságmezők meghatározásához és megjelenítéséhez.

https://www.shadertoy.com/view/Wsf3Rj

a távolságmező meghatározása előtt az eredmény teljesen fehér lesz. Ennek a szakasznak az a célja, hogy olyan SDF-et tervezzen, amely megadja a kívánt jelenet alakját (fehér körvonal). Kódban ezt a távolságot a sdf() függvény számítja ki, amely bemenetként 2D helyet kap a térben. Az itt megtanult fogalmak közvetlenül a 3D-s térre általánosítanak, és lehetővé teszik a 3D-s jelenet modellezését.

Start simple-próbáld meg először csak a p pont x vagy y összetevőjét használni, és figyeld meg az eredményt:

float sdf(vec2 p){ return p.y;}

az eredménynek a következőképpen kell kinéznie:

a zöld a ‘külső’ felületeket, a piros a ‘belső’ felületeket jelöli, a fehér vonal magát a felületet jelöli, a belső/külső régiók árnyékolása pedig a távolságot mutatja iso-vonalak – rögzített távolságokon lévő vonalak. 2D-ben ez az SDF egy vízszintes vonalat modellez 2D-ben y=0. Milyen geometriai primitív lenne ez a 3D-ben?

egy másik jó dolog, amit meg kell próbálni, a távolságok használata, például: return length(p);. Ez az operátor visszaadja a vektor nagyságát, és ebben az esetben megadja az aktuális pont távolságát az Origótól.

egy pontot nem túl érdekes megjeleníteni, mivel egy pont végtelenül kicsi, és a sugaraink mindig hiányoznának!A pontnak egy bizonyos területet adhatunk, ha kivonjuk a kívánt sugarat a távolságból: return length(p) - 0.25;.A bemeneti pontot módosíthatjuk annak nagysága előtt is: length(p - vec2(0.0, 0.2)) - 0.25;.Milyen hatással van ez az alakra?Milyen értékeket adhat vissza a függvény a kör belsejében lévő pontokra?

Gratulálunk – éppen most modellezett egy kört a matematika :). Ez triviálisan kiterjed a 3D-re, amely esetben egy gömböt modellez. Állítsd szembe ezt a jelenetábrázolást más ‘explicit’ jelenetábrázolásokkal, mint például a háromszöghálók vagy a NURBS felületek. Percek alatt létrehoztunk egy gömböt egyetlen kódsorral, és a kódunk közvetlenül leképezi egy gömb matematikai definícióját – ‘a középponttól egyenlő távolságra lévő összes pont halmazát’.

más típusú primitívek esetében a távolságfüggvények hasonlóan elegánsak. az iq nagyszerű referencia oldalt készített képekkel: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

ha egyszer megérted, hogyan működik a távolság egy primitívtől – tedd egy dobozba – határozz meg egy függvényt, hogy ne kelljen minden alkalommal megjegyezned és kiírnod a kódot. A sdCircle() körhöz már van egy függvény, amely megtalálható az árnyékolóban. Adjon hozzá minden kívánt primitívet.

alakzatok kombinálása

most már tudjuk, hogyan lehet egyedi primitíveket létrehozni, hogyan kombinálhatjuk őket egy több formájú jelenet meghatározásához?

ennek egyik módja az ‘Unió’ operátor – amelyet két távolság minimumaként határozunk meg. A legjobb kísérletezni a kóddal annak érdekében, hogy ezt erősen megértsük, de az intuíció az, hogy az SDF megadja a távolságot a legközelebbi felülethez, és ha a jelenetnek több objektuma van, akkor a legközelebbi objektumhoz való távolságot szeretné, amely az egyes objektumokhoz való távolság minimális lesz.

kódban ez a következőképpen nézhet ki:

float sdf(vec2 p){ float d = 1000.0; d = min(d, sdCircle(p, vec2(-0.1, 0.4), 0.15)); d = min(d, sdCircle(p, vec2( 0.5, 0.1), 0.35)); return d;}

ily módon kompaktan kombinálhatunk sok formát. Miután ezt megértettük, a opU() függvényt kell használni, amely az ‘Operation union’rövidítést jelenti.

ez csak a felszínt karcolja meg, ami lehetséges. Sima keverékeket kaphatunk egy divatos soft min funkcióval-próbálja meg használni a mellékelt opBlend() – et. Sok más érdekes technika is alkalmazható, az érdeklődő olvasót erre a kiterjesztett bevezetésre utalják az SDF-ekkel történő jelenetek építéséhez: https://www.youtube.com/watch?v=s8nFqwOho-s

példa:

átmenet 3D-re

remélhetőleg alapvető ismereteket szerzett arról, hogy a távolságmezők hogyan használhatók a jelenetadatok ábrázolására, és hogyan fogjuk használni a raymarching alkalmazást a jelenet metszéspontjainak megtalálásához. Most három dimenzióban fogunk dolgozni, ahol az igazi varázslat történik.

javasoljuk, hogy mentse el a jelenlegi árnyékolóját, és indítson egy újat, hogy később visszatérhessen a 2D megjelenítéshez.A legtöbb segítő átmásolható az új shaderbe, és 3D-ben dolgozhat úgy, hogy a vec2s-t vec3s-re cseréli.

Ray marching loop

ahelyett, hogy az SDF-et vizualizálnánk, mint a 2D-ben, egyenesen a jelenet renderelésére fogunk ugrani. Itt van az alapötlet, hogyan fogjuk végrehajtani ray marching (álkódban):

Main function Evaluate camera Call RenderRayRenderRay function Raymarch to find intersection of ray with scene Shade

ezeket a lépéseket most részletesebben ismertetjük.

kamera

vec3 getCameraRayDir(vec2 uv, vec3 camPos, vec3 camTarget){ // Calculate camera's "orthonormal basis", i.e. its transform matrix components vec3 camForward = normalize(camTarget - camPos); vec3 camRight = normalize(cross(vec3(0.0, 1.0, 0.0), camForward)); vec3 camUp = normalize(cross(camForward, camRight)); float fPersp = 2.0; vec3 vDir = normalize(uv.x * camRight + uv.y * camUp + camForward * fPersp); return vDir;}

ez a funkció először kiszámítja a kamera ‘view’ mátrixának három tengelyét; az előre, jobbra és felfelé vektorokat.Az előre mutató vektor a normalizált vektor a kamera helyzetétől a kinézet célhelyzetéig.A jobb vektort úgy találjuk meg, hogy az előre mutató vektort keresztezzük a világ felfelé tengelyével.Az előre és jobbra vektorokat ezután keresztezzük, hogy megkapjuk a kamera fel vektorát.

végül a kamerasugár kiszámítása ezzel a képkockával történik úgy, hogy a kamera előtt egy pontot veszünk, és a uv pixelkoordináták segítségével kiegyenlítjük a kamera jobb és felfelé irányuló irányait.fPersp lehetővé teszi számunkra, hogy közvetetten irányítsuk kameránk látómezőjét. Gondolhat erre a szorzásra úgy, hogy a közeli síkot egyre távolabb mozgatja a kamerától. Kísérletezzen különböző értékekkel az eredmény megtekintéséhez.

jelenet meghatározása

float sdSphere(vec3 p, float r){ return length(p) - r;} float sdf(vec3 pos){ float t = sdSphere(pos-vec3(0.0, 0.0, 10.0), 3.0); return t;}

mint látható, hozzáadtunk egy sdSphere() – et, amely megegyezik a sdCircle – vel, kivéve a bemeneti pontunk összetevőinek számát.

Raymarching

Álkód:

castRay for i in step count: sample scene if within threshold return dist return -1

próbáld meg ezt magad írni – ha csak elakadsz, akkor nézd meg az alábbi megoldást.

valódi kód:

float castRay(vec3 rayOrigin, vec3 rayDir){ float t = 0.0; // Stores current distance along ray for (int i = 0; i < 64; i++) { float res = SDF(rayOrigin + rayDir * t); if (res < (0.0001*t)) { return t; } t += res; } return -1.0;}

most hozzáadunk egy render függvényt, amely végül felelős lesz a megtalált metszéspont árnyékolásáért. Most azonban lehetővé teszi, hogy megjelenítse a távolságot a jelenet, hogy ellenőrizze vagyunk a pályán. Skálázzuk és megfordítjuk, hogy jobban lássuk a különbségeket.

vec3 render(vec3 rayOrigin, vec3 rayDir){ float t = castRay(rayOrigin, rayDir); // Visualize depth vec3 col = vec3(1.0-t*0.075); return col;}

az egyes sugarak irányának kiszámításához a fragCoord pixelkoordináta bemenetet a , tartományból kell átalakítani, ahol a w és a h a képernyő szélessége és magassága pixelben, a a pedig a képernyő képaránya. Ezután átadhatjuk az ebből a segítőből visszaadott értéket a fent definiált getCameraRayDir függvénybe, hogy megkapjuk a sugár irányát.

vec2 normalizeScreenCoords(vec2 screenCoord){ vec2 result = 2.0 * (screenCoord/iResolution.xy - 0.5); result.x *= iResolution.x/iResolution.y; // Correct for aspect ratio return result;}

fő képfunkciónk ezután a következőképpen néz ki:

void mainImage(out vec4 fragColor, vec2 fragCoord){ vec3 camPos = vec3(0, 0, -1); vec3 camTarget = vec3(0, 0, 0); vec2 uv = normalizeScreenCoords(fragCoord); vec3 rayDir = getCameraRayDir(uv, camPos, camTarget); vec3 col = render(camPos, rayDir); fragColor = vec4(col, 1); // Output to screen}

gyakorlatok:

  • kísérletezzen a lépésszámmal, és figyelje meg, hogyan változik az eredmény.
  • kísérletezzen a végződési küszöbértékkel, és figyelje meg, hogyan változik az eredmény.

a teljes munkaprogramot lásd: Shadertoy: Part 1a

környezeti kifejezés

ahhoz, hogy színt kapjunk a jelenetbe, először meg kell különböztetnünk az objektumokat és a hátteret.

ehhez visszatérhetünk -1-re a castRay-ben, hogy jelezzük, hogy semmit sem találtak el. Ezután kezelhetjük az ügyet a renderben.

vec3 render(vec3 rayOrigin, vec3 rayDir){ vec3 col; float t = castRay(rayOrigin, rayDir); if (t == -1.0) { // Skybox colour col = vec3(0.30, 0.36, 0.60) - (rayDir.y * 0.7); } else { vec3 objectSurfaceColour = vec3(0.4, 0.8, 0.1); vec3 ambient = vec3(0.02, 0.021, 0.02); col = ambient * objectSurfaceColour; } return col;}

https://www.shadertoy.com/view/4tdBzj

diffúz kifejezés

a valósághűbb megvilágítás érdekében számítsuk ki a normál felületet, így kiszámíthatjuk az alapvető Lamberti világítást.

a normál kiszámításához kiszámítjuk a felület gradiensét mindhárom tengelyen.

ez a gyakorlatban azt jelenti, hogy az SDF-et négy extra alkalommal mintavételezzük, mindegyik kissé eltolva az elsődleges sugárunktól.

vec3 calcNormal(vec3 pos){ // Center sample float c = sdf(pos); // Use offset samples to compute gradient / normal vec2 eps_zero = vec2(0.001, 0.0); return normalize(vec3( sdf(pos + eps_zero.xyy), sdf(pos + eps_zero.yxy), sdf(pos + eps_zero.yyx) ) - c);}

egy nagyszerű módja annak, hogy ellenőrizze normals megjelenítésével őket, mintha képviselt szín. Így kell kinéznie egy gömbnek a méretezett és elfogult normál megjelenítésekor ( – ből – be hozva, mivel a monitor nem képes negatív színértékeket megjeleníteni)

col = N * vec3(0.5) + vec3(0.5);

most, hogy van egy normális, tudjuk venni a dot termék között, és a fény irányát.

ez megmutatja nekünk, hogy a felület milyen közvetlenül néz a fény felé, és ezért milyen fényesnek kell lennie.

ennek az értéknek a maximumát 0-val vesszük, hogy megakadályozzuk a negatív értékek nem kívánt hatásokat az objektumok sötét oldalán.

// L is vector from surface point to light, N is surface normal. N and L must be normalized!float NoL = max(dot(N, L), 0.0);vec3 LDirectional = vec3(0.9, 0.9, 0.8) * NoL;vec3 LAmbient = vec3(0.03, 0.04, 0.1);vec3 diffuse = col * (LDirectional + LAmbient);

a renderelés egyik nagyon fontos része, amely könnyen figyelmen kívül hagyható, a gamma-korrekció. A monitorra küldött pixelértékek gamma térben vannak, amely egy nemlineáris tér, amelyet a pontosság maximalizálására használnak, kevesebb bit használatával olyan intenzitási tartományokban, amelyekre az emberek kevésbé érzékenyek.

mivel a monitorok nem “lineáris” térben működnek, kompenzálnunk kell a gamma görbéjüket, mielőtt színt adnánk ki. A különbség nagyon észrevehető, ezért mindig korrigálni kell. A valóságban nem tudjuk, hogy egy adott megjelenítő eszköz gamma-görbéje az, tehát a megjelenítési technológiával az egész helyzet szörnyű rendetlenség (ezért sok játékban a gamma-hangolási lépés), de általános feltételezés a következő gamma-görbe:

a 0,4545 állandó egyszerűen 1.0 / 2.2

col = pow(col, vec3(0.4545)); // Gamma correction

https://www.shadertoy.com/view/4t3fzn

Shadows

az árnyékok kiszámításához kilőhetünk egy sugarat attól a ponttól kezdve, ahol a jelenetet kereszteztük, és a fényforrás irányába haladunk.

ha ez a sugárhajtás azt eredményezi, hogy eltalálunk valamit, akkor tudjuk, hogy a fény is akadályozva lesz, így ez a pixel árnyékban van.

float shadow = 0.0;vec3 shadowRayOrigin = pos + N * 0.01;vec3 shadowRayDir = L;IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir);if (shadowRayIntersection.mat != -1.0){ shadow = 1.0;}col = mix(col, col*0.2, shadow);

Földsík

adjunk hozzá egy földsíkot, hogy jobban láthassuk a gömbök által vetett árnyékokat.

az n W komponense azt a távolságot jelenti, amelyet a sík az Origótól tartalmaz.

float sdPlane(vec3 p, vec4 n){ return dot(p, n.xyz) + n.w;}

puha árnyékok

az árnyékok a való életben nem állnak meg azonnal, van némi esésük, amelyet félárnyéknak neveznek.

ezt úgy modellezhetjük, hogy több sugarat veszünk a felszíni pontról, amelyek mindegyike kissé eltérő irányokkal rendelkezik.

ezután összegezhetjük az eredményt és az átlagot az elvégzett iterációk számával. Ez azt eredményezi, hogy az árnyék szélei

egyes sugarak eltalálnak, mások pedig hiányoznak, 50% – os sötétséget adva.

megállapítás kissé ál véletlen szám lehet tenni számos módon, akkor használja a következő mégis:

float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);}

ez a függvény egy számot ad vissza a [0, 1) tartományban. Tudjuk, hogy a kimenet ehhez a tartományhoz van kötve, mert a legkülső művelet fract, amely egy lebegőpontos szám törtkomponensét adja vissza.

ezután ezt használhatjuk az árnyéksugár kiszámításához az alábbiak szerint:

float shadow = 0.0;float shadowRayCount = 1.0;for (float s = 0.0; s < shadowRayCount; s++){ vec3 shadowRayOrigin = pos + N * 0.01; float r = rand(vec2(rayDir.xy)) * 2.0 - 1.0; vec3 shadowRayDir = L + vec3(1.0 * SHADOW_FALLOFF) * r; IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir); if (shadowRayIntersection.mat != -1.0) { shadow += 1.0; }}col = mix(col, col*0.2, shadow/shadowRayCount);

Texture mapping

ahelyett, hogy egyetlen felületi színt (vagy más jellemzőt) határozna meg egyenletesen a teljes felületen, meghatározhat mintákat a felületre textúrák segítségével.Három módot fogunk lefedni ennek elérésére.

3D textúra leképezés

vannak kötet textúrák könnyen elérhető shadertoy lehet rendelni egy csatornát. Próbáljon meg mintát venni ezen textúrák egyikéből a felületi pont 3D-s helyzetével:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = texture(iChannel0, textureFreq * surfacePos).xyz;

a zaj mintavételének egyik módja több skála összeadása, az alábbiak felhasználásával:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = 0.5 * texture(iChannel0, 1.0 * textureFreq * surfacePos).xyz + 0.25 * texture(iChannel0, 2.0 * textureFreq * surfacePos).xyz + 0.125 * texture(iChannel0, 4.0 * textureFreq * surfacePos).xyz + 0.0625 * texture(iChannel0, 8.0 * textureFreq * surfacePos).xyz ;

a fenti állandókat/súlyokat általában fraktál zajra használják, de bármilyen kívánt értéket felvehetnek. Próbáljon kísérletezni a súlyokkal/mérlegekkel/színekkel, és nézze meg, milyen érdekes hatásokat érhet el.

próbálja meg animálni az objektumot az iTime használatával, és figyelje meg, hogyan viselkedik a kötet textúrája. Meg lehet változtatni ezt a viselkedést?

2D textúra leképezés

a 2D textúra alkalmazása érdekes probléma – hogyan vetítsük ki a textúrát a felületre? Normál 3D-s grafika esetén az objektum minden háromszögéhez egy vagy több UV van hozzárendelve, amelyek megadják a textúra régiójának koordinátáit, amelyet a háromszögre kell leképezni (textúra leképezés). A mi esetünkben nincs UVs biztosított, így meg kell, hogy kitaláljuk, hogyan kell mintát a textúra.

az egyik megközelítés az, hogy a textúrát felülről lefelé vetített világprojekt segítségével mintavételezzük a textúra alapján X & Z koordináták:

// top down projectionfloat textureFreq = 0.5;vec2 uv = textureFreq * surfacePos.xz; // sample texturevec3 surfaceCol = texture2D(iChannel0, uv).xyz;

milyen korlátokat lát ezzel a megközelítéssel?

Triplanáris leképezés

a textúrák leképezésének fejlettebb módja az, ha az elsődleges tengelyekből 3 vetületet hajtunk végre, majd az eredményt triplanáris leképezéssel keverjük össze. A keverés célja a legjobb textúra kiválasztása a felület minden pontjához. Az egyik lehetőség a keverési súlyok meghatározása az egyes világtengelyekkel normál felület összehangolása alapján. Az a felület, amely az egyik tengellyel elöl néz, nagy keverési súlyt kap:

vec3 triplanarMap(vec3 surfacePos, vec3 normal){ // Take projections along 3 axes, sample texture values from each projection, and stack into a matrix mat3 triMapSamples = mat3( texture(iChannel0, surfacePos.yz).rgb, texture(iChannel0, surfacePos.xz).rgb, texture(iChannel0, surfacePos.xy).rgb ); // Weight three samples by absolute value of normal components return triMapSamples * abs(normal);}

milyen korlátokat lát ezzel a megközelítéssel?

anyagok

a castRay függvénytől visszatérő távolsággal együtt visszaadhatunk egy indexet is, amely az objektum anyagát képviseli találat. Ezt az indexet használhatjuk az objektumok megfelelő színezésére.

operátorainknak az úszók helyett vec2-t kell venniük, és össze kell hasonlítaniuk mindegyik első komponensét.

most, a jelenetünk meghatározásakor minden primitívhez egy anyagot is megadunk, mint egy vec2 y összetevőjét:

vec2 res = vec2(sdSphere(pos-vec3(3,-2.5,10), 2.5), 0.1);res = opU(res, vec2(sdSphere(pos-vec3(-3, -2.5, 10), 2.5), 2.0));res = opU(res, vec2(sdSphere(pos-vec3(0, 2.5, 10), 2.5), 5.0));return res;

ezután megszorozhatjuk ezt az anyagindexet a render függvény néhány értékével, hogy minden objektumhoz különböző színeket kapjunk. Próbálja ki a különböző értékeket.

col = vec3(0.18*m, 0.6-0.05*m, 0.2)if (m == 2.0){ col *= triplanarMap(pos, N, 0.6);}

színezzük az alaplapot egy kockás mintával. Vettem ezt a divatos analitikusan anti-aliased checkerbox funkciót Inigo Quilez weboldaláról.

float checkers(vec2 p){ vec2 w = fwidth(p) + 0.001; vec2 i = 2.0*(abs(fract((p-0.5*w)*0.5)-0.5)-abs(fract((p+0.5*w)*0.5)-0.5))/w; return 0.5 - 0.5*i.x*i.y;}

átmegyünk a sík helyzetünk xz komponensein, hogy a minta megismétlődjön ezekben a dimenziókban.

https://www.shadertoy.com/view/Xl3fzn

köd

most hozzáadhatunk ködöt a jelenethez annak alapján, hogy az egyes kereszteződések milyen messze voltak a kamerától.

nézze meg, hogy kaphat-e valami hasonlót az alábbiakhoz:

https://www.shadertoy.com/view/Xtcfzn

alak & anyagkeveréshogy elkerüljük a min operátor által adott durva gyűrődést, használhatunk egy kifinomultabb operátort, amely simán ötvözi az alakzatokat.

// polynomial smooth min (k = 0.1);float sminCubic(float a, float b, float k){ float h = max(k-abs(a-b), 0.0); return min(a, b) - h*h*h/(6.0*k*k);} vec2 opBlend(vec2 d1, vec2 d2){ float k = 2.0; float d = sminCubic(d1.x, d2.x, k); float m = mix(d1.y, d2.y, clamp(d1.x-d,0.0,1.0)); return vec2(d, m);}

Anti-aliasing

ha sokszor mintavételezzük a jelenetet kissé eltolt kamera irányvektorokkal, akkor simított értéket kapunk, amely elkerüli az aliasingot.

kihoztam a jelenet színszámítását a saját funkciójára, hogy tisztábbá tegyem a hurok hívását.

float AA_size = 2.0;float count = 0.0;for (float aaY = 0.0; aaY < AA_size; aaY++){ for (float aaX = 0.0; aaX < AA_size; aaX++) { fragColor += getSceneColor(fragCoord + vec2(aaX, aaY) / AA_size); count += 1.0; }}fragColor /= count;

Step count optimization

ha vizualizáljuk, hogy hány lépést teszünk az egyes pixelekhez piros színnel, akkor világosan láthatjuk, hogy a semmit nem érő sugarak felelősek a legtöbb iterációnkért.

ez jelentős teljesítménynövekedést adhat bizonyos jeleneteknél.

if (t > drawDist) return backgroundColor;

alak & anyag interpoláció

két alakzat között interpolálhatunk a mix függvény használatával és az iTime használatával az idő múlásával.

vec2 shapeA = vec2(sdBox(pos-vec3(6.5, -3.0, 8), vec3(1.5)), 1.5);vec2 shapeB = vec2(sdSphere(pos-vec3(6.5, -3.0, 8), 1.5), 3.0);res = opU(res, mix(shapeA, shapeB, sin(iTime)*0.5+0.5));

tartomány ismétlés

elég könnyű megismételni egy alakzatot egy aláírt távolságmező segítségével, lényegében csak egy vagy több dimenzióban kell modulálni a bemeneti pozíciót.

ez a technika használható például egy oszlop többszöri megismétlésére anélkül, hogy növelné a jelenet ábrázolási méretét.

itt megismételtem a bemeneti pozíció mindhárom összetevőjét, majd a kivonási operátorral ( max() ) korlátoztam az ismétlést egy határoló dobozra.

az egyik dolog az, hogy le kell vonnia a modulált érték felét annak érdekében, hogy az ismétlést az alakjára központosítsa, hogy ne vágja félbe.

float repeat(float d, float domain){ return mod(d, domain)-domain/2.0;}

utófeldolgozási effektusok

Matrica

a képernyő közepétől távolabb lévő pixelek elsötétítésével egyszerű matrica hatást érhetünk el.

kontraszt

a sötétebb és világosabb értékek kiemelhetők, ami az észlelt dinamikatartomány növekedését eredményezi a kép intenzitásával együtt.

col = smoothstep(0.0,1.0,col);

“környezeti elzáródás”

ha a fenti kép inverzét vesszük (optimalizálás esetén), furcsa ao-szerű hatást kaphatunk.

col *= (1.0-vec3(steps/maxSteps));

Ad infinitum

mint látható, sok utófeldolgozó hatások végre triviálisan; játék körül a különböző funkciókat, és milyen egyéb hatások hozhat létre.

www.shadertoy.com/view/MtdBzs

mi a következő lépés?

itt csak az alapokat fedeztük fel; sokkal többet kell feltárni ezen a területen, mint például:

  • felszín alatti szórás
  • környezeti elzáródás
  • animált primitívek
  • primitív vetemedési funkciók (twist, bend,…)
  • átláthatóság (fénytörés, maró,…)
  • optimalizálás (kötethierarchiák korlátozása)

böngésszen ShaderToy, hogy egy kis ihletet, hogy mit lehet tenni, és piszkálni a különböző shaders, hogy milyen különböző hatások vannak végrehajtva. Sok shaderek változók, amelyek lehet csípés, és azonnal látni a hatását (alt-enter a parancsikont lefordítani!).

olvassa el a referenciákat is, ha többet szeretne megtudni!

Köszönjük, hogy elolvasta! Ügyeljen arra, hogy küldje el nekünk a hűvös árnyékolók! Ha bármilyen visszajelzése van a tanfolyamról, mi is szívesen hallanánk!

lépjen kapcsolatba velünk a Twitteren @ liqwidice & @ hdb1

elektromos tér bérbeadása!

ajánlott olvasmány:

SDF funkciók: http://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/

Claybook demo: https://www.youtube.com/watch?v=Xpf7Ua3UqOA

sugárkövetés egy hétvégén: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html

fizikai alapú renderelés Biblia, PBRT: https://www.pbrt.org/

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.