electricsquare / raymarching-workshop

přináší vám Electric Square

vytvořil a představil aj Weeks & Huw Bowles

přehled

vykreslení obrazu zahrnuje určení barvy každého pixelu v obraze, což vyžaduje zjistit, jaký povrch leží za pixelem na světě, a poté jej „zastínit“ pro výpočet konečné barvy.

GPU současné generace berou trojúhelníkové sítě jako vstup, rasterizují je na pixely (nazývané fragmenty, než jsou nakresleny na displej) a poté je stínují, aby vypočítaly svůj příspěvek k obrazu. I když je toto potrubí v současné době všudypřítomné, je to také komplikované a ne nutně nejlepší způsob, jak se naučit grafiku.

alternativním přístupem je obsazení paprsku přes každý pixel a jeho protnutí s povrchy ve scéně a poté výpočet stínování.

tento kurz představuje jednu techniku pro raycasting prostřednictvím „distančních polí“. Pole vzdálenosti je funkce, která vrací, jak blízko je daný bod k nejbližšímu povrchu ve scéně. Tato vzdálenost definuje poloměr koule prázdného prostoru kolem každého bodu. Signed distance fields (SDFs) jsou distanční pole, která jsou definována uvnitř i vně objektů; pokud je dotazovaná pozice „uvnitř“ povrchu, bude její vzdálenost hlášena jako záporná, jinak bude kladná.

co je možné s ray marching?

hra „Claybook“ používá pouze pole vzdálenosti k reprezentaci scény. To mu poskytuje spoustu zajímavých možností, jako jsou zcela dynamické povrchové topologie a morfování tvarů. Těchto účinků by bylo velmi obtížné dosáhnout pomocí trojúhelníkových sítí. Mezi další výhody patří snadno implementovatelné a vysoce kvalitní měkké stíny a okolní okluze.

https://www.claybookgame.com/

následující obrázek byl také vykreslen v reálném čase pomocí technik, které dnes pokryjeme (plus mnoho efektních technik, do kterých nebudeme mít čas se ponořit).

můžete jej spustit živě ve svém prohlížeči zde: https://www.shadertoy.com/view/ld3Gz2

pomocí SDF (signed distance field) nemusela být geometrie pro tuto scénu vytvořena v DCC jako Maya, ale místo toho je reprezentována zcela parametricky. Díky tomu je triviální animovat tvar jednoduše změnou vstupů do funkce mapování scény.

další grafické efekty jsou raymarchingem zjednodušeny ve srovnání s tradičními alternativami rasterizace. Podpovrchový rozptyl, například, vyžaduje jednoduše poslat několik dalších paprsků do povrchu, abyste zjistili, jak je silný. Okolní okluze, vyhlazování a hloubka ostrosti jsou tři další techniky, které vyžadují jen několik dalších řádků a přesto výrazně zlepšují kvalitu obrazu.

Raymarching distanční pole

budeme pochodovat podél každého paprsku a hledat křižovatku s povrchem ve scéně. Jedním ze způsobů, jak toho dosáhnout, by bylo začít na počátku paprsku (v rovině kamery) a provádět jednotné kroky podél paprsku a vyhodnocovat pole vzdálenosti v každém bodě. Když je vzdálenost od scény menší než prahová hodnota, víme, že jsme narazili na povrch, a proto můžeme raymarch ukončit a stínovat tento pixel.

efektivnějším přístupem je použití vzdálenosti vrácené SDF k určení velikosti dalšího kroku. Jak bylo uvedeno výše, vzdálenost vrácená SDF může být považována za poloměr koule prázdného prostoru kolem vstupního bodu. Je proto bezpečné postupovat tímto množstvím podél paprsku, protože víme, že neprojdeme žádnými povrchy.

v následující 2D reprezentaci raymarchingu je střed každého kruhu místem, odkud byla scéna vzorkována. Paprsek byl poté pochodován podél této vzdálenosti (sahající k poloměru kruhu)a poté převzorkován.

jak vidíte, vzorkování SDF vám nedává přesný průsečík vašeho paprsku, ale spíše minimální vzdálenost, kterou můžete cestovat bez průchodu povrchem.

jakmile je tato vzdálenost pod určitým prahem, raymarch ukončí pixel může být stínován na základě vlastností povrchu protínajícího se.

Pohrajte si s tímto shader ve Vašem prohlížeči zde: (kliknutím a tažením v obraze nastavte směr paprsku) https://www.shadertoy.com/view/lslXD8

srovnání s Ray tracing

v tomto bodě by se člověk mohl ptát, proč nepočítáme průnik se scénou přímo pomocí analytické matematiky pomocí techniky označované jako Ray Tracing. Takto by obvykle fungovaly offline rendery-všechny trojúhelníky ve scéně jsou indexovány do nějaké struktury prostorových dat, jako je hierarchie ohraničujících objemů (BVH) nebo KD-strom, které umožňují efektivní průnik trojúhelníků umístěných podél paprsku.

místo toho jsme raymarch vzdálenost pole, protože:

  • je velmi jednoduché implementovat rutinu odlévání paprsků
  • vyhýbáme se veškeré složitosti implementace průsečíků paprskového trojúhelníku a datových struktur BVH
  • nepotřebujeme autorovat explicitní reprezentaci scény-trojúhelníkové sítě, tex coordy, barvy atd.
  • těžíme z řady užitečných funkcí distančních polí, z nichž některé jsou zmíněny výše

poté, co jsme uvedli výše, existuje několik užitečných funkcí elegantní/jednoduché vstupní body do ray tracing. Sledování paprsků v jedné víkendové knize zdarma (a následujících kapitolách) je velmi doporučeno a je nezbytným čtením pro každého, kdo se zajímá o grafiku.

začněme!

ShaderToy

ShaderToy je webová stránka pro vytváření shader a platforma pro prohlížení, sdílení a diskusi o shaderech.

i když můžete skočit přímo a začít psát nový shader bez vytvoření účtu, je to nebezpečné, protože můžete snadno ztratit práci, pokud se vyskytnou problémy s připojením nebo pokud zavěsíte GPU(snadno proveditelné např. vytvořením nekonečné smyčky).Proto důrazně doporučujeme vytvořit účet (je to rychlé/snadné/zdarma) nadpisem zde: https://www.shadertoy.com/signin a pravidelně ukládat.

pro přehled ShaderToy a průvodce začínáme doporučujeme následující tutoriál, jako je tento z @The_ArtOfCode: https://www.youtube.com/watch?v=u5HAYVHsasc. Základy zde jsou nezbytné pro sledování zbytku Workshopu.

2D SDF demo

poskytujeme jednoduchý rámec pro definování a vizualizaci polí 2D podepsané vzdálenosti.

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

před definováním pole vzdálenosti bude výsledek zcela bílý. Cílem této části je navrhnout SDF, který dává požadovaný tvar scény(bílý obrys). V kódu je tato vzdálenost vypočtena funkcí sdf(), která je zadána 2D pozice v prostoru jako vstup. Pojmy, které se zde naučíte, zobecní přímo do 3D prostoru a umožní vám modelovat 3D scénu.

začněte jednoduše-zkuste nejprve použít komponentu x nebo y bodu p a sledujte výsledek:

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

výsledek by měl vypadat následovně:

Zelená označuje „vnější“ povrchy, červená označuje „vnitřní“ povrchy, bílá čára vymezuje samotný povrch a stínování ve vnitřních / vnějších oblastech ilustruje vzdálenost iso-linií-linií na pevné vzdálenosti. Ve 2D tento SDF modeluje vodorovnou čáru ve 2D při y=0. Jaký geometrický primitiv by to představovalo ve 3D?

další dobrou věcí, kterou je třeba vyzkoušet, je použití vzdáleností, například: return length(p);. Tento operátor vrací velikost vektoru a v tomto případě nám dává vzdálenost aktuálního bodu Od počátku.

bod není velmi zajímavá věc, kterou je třeba vykreslit, protože bod je nekonečně malý a naše paprsky by ho vždy chyběly!Můžeme dát bodu nějakou oblast odečtením požadovaného poloměru od vzdálenosti: return length(p) - 0.25;.Můžeme také upravit vstupní bod před přijetím jeho velikosti: length(p - vec2(0.0, 0.2)) - 0.25;.Jaký vliv to má na tvar?Jaké hodnoty může funkce vracet pro body „uvnitř“ kruhu?

Gratulujeme-právě jste modelovali kruh pomocí matematiky :). To se triviálně rozšíří na 3D, v takovém případě modeluje kouli. Porovnejte tuto reprezentaci scény s jinými „explicitními“ reprezentacemi scén, jako jsou trojúhelníkové sítě nebo povrchy NURBS. Vytvořili jsme kouli během několika minut s jedním řádkem kódu a náš kód se přímo mapuje na jednu matematickou definici koule- „množinu všech bodů, které jsou stejně vzdálené od středového bodu“.

u jiných typů primitiv jsou funkce vzdálenosti podobně elegantní. iq udělal skvělou referenční stránku s obrázky: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

jakmile pochopíte, jak funguje vzdálenost k primitivu-vložte ji do pole-definujte pro ni funkci, takže si nemusíte pamatovat a psát kód pokaždé. Pro kruh sdCircle() je již definována funkce, kterou najdete v shaderu. Přidejte jakékoli primitivy, které si přejete.

kombinace tvarů

nyní víme, jak vytvořit jednotlivé primitivy, jak je můžeme kombinovat, abychom definovali scénu s více tvary?

jedním ze způsobů, jak toho dosáhnout, je operátor „union“ – který je definován jako minimálně dvě vzdálenosti. Nejlepší je experimentovat s kódem, abyste to dobře pochopili, ale intuice je, že SDF dává vzdálenost k nejbližšímu povrchu, a pokud má scéna více objektů, chcete vzdálenost k nejbližšímu objektu, což bude minimální vzdálenost ke každému objektu.

v kódu to může vypadat následovně:

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;}

tímto způsobem můžeme kompaktně kombinovat mnoho tvarů. Jakmile je to pochopeno, měla by být použita funkce opU(), což znamená „operation union“.

Jedná se pouze o poškrábání povrchu toho, co je možné. Hladké směsi můžeme získat pomocí efektní funkce soft min-zkuste použít dodaný opBlend(). Existuje mnoho dalších zajímavých technik, které lze použít, zainteresovaný čtenář je odkázán na tento rozšířený úvod do budování scén s SDFs: https://www.youtube.com/watch?v=s8nFqwOho-s

příklad:

přechod na 3D

doufejme, že jste získali základní znalosti o tom, jak mohou být pole vzdálenosti použita k reprezentaci dat scény a jak budeme používat raymarching k nalezení průsečíků se scénou. Nyní začneme pracovat ve třech dimenzích, kde se stane skutečná magie.

doporučujeme uložit aktuální shader a spustit nový, abyste se mohli později vrátit k 2D vizualizaci.Většina pomocníků může zkopírovat do nového shaderu a pracovat ve 3D výměnou vec2 s vec3 s.

Ray marching loop

spíše než vizualizovat SDF, jako jsme to udělali ve 2D, skočíme přímo do vykreslování scény. Zde je základní myšlenka, jak implementujeme ray marching (v pseudo kódu):

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

tyto kroky budou nyní podrobněji popsány.

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;}

Tato funkce nejprve vypočítá tři osy matice „view“ kamery; vektory vpřed, vpravo a nahoru.Dopředný vektor je normalizovaný vektor z polohy kamery do cílové polohy vzhledu.Správný vektor je nalezen křížením dopředného vektoru s osou nahoru.Dopředné a pravé vektory jsou pak zkříženy, aby se získal vektor kamery nahoru.

nakonec se paprsek kamery vypočítá pomocí tohoto snímku tak, že se vezme bod před kamerou a započte jej ve směru kamery vpravo a nahoru pomocí souřadnic Pixelů uv.fPersp nám umožňuje nepřímo ovládat zorné pole naší kamery. Toto násobení si můžete představit jako pohyb blízké roviny blíže a dále od kamery. Experimentujte s různými hodnotami, abyste viděli výsledek.

definice scény

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;}

jak vidíte, přidali jsme sdSphere(), který je totožný s sdCircle, kromě počtu komponent v našem vstupním bodě.

Raymarching

Pseudo kód:

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

zkuste to napsat sami-pokud uvíznete, podívejte se na níže uvedené řešení.

skutečný 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;}

nyní přidáme funkci render, která bude nakonec zodpovědná za zastínění nalezeného průsečíku. Prozatím však umožňuje zobrazit vzdálenost k scéně a zkontrolovat, zda jsme na správné cestě. Budeme měřítko a Invertovat, abychom lépe viděli rozdíly.

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

pro výpočet směru každého paprsku budeme chtít transformovat vstup souřadnic Pixelů fragCoord z rozsahu , , kde w A h jsou šířka a výška obrazovky v pixelech a a je poměr stran obrazovky. Pak můžeme předat hodnotu vrácenou z tohoto pomocníka do funkce getCameraRayDir, kterou jsme definovali výše, abychom získali směr paprsku.

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;}

naše hlavní funkce obrazu pak vypadá následovně:

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}

cvičení:

  • experimentujte s počtem kroků a sledujte, jak se výsledek mění.
  • experimentujte s prahem ukončení a sledujte, jak se výsledek mění.

pro plný pracovní program, viz Shadertoy: část 1a

okolní termín

Chcete-li získat nějakou barvu do scény, nejprve rozlišujeme mezi objekty a pozadím.

k tomu se můžeme vrátit -1 v castRay, abychom signalizovali, že nic nebylo zasaženo. Ten případ pak můžeme řešit v renderu.

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

difuzní výraz

abychom získali realističtější osvětlení, vypočítáme povrch normální, abychom mohli vypočítat základní Lambertovské osvětlení.

pro výpočet normálu vypočítáme gradient povrchu ve všech třech osách.

to v praxi znamená vzorkování SDF čtyřikrát navíc, každý mírně odsazený od našeho primárního paprsku.

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);}

jeden skvělý způsob, jak kontrolovat normály, je jejich zobrazení, jako by představovaly barvu. Takto by koule měla vypadat při zobrazení zmenšeného a zkresleného normálu (přeneseného z do , protože váš monitor nemůže zobrazit záporné hodnoty barev)

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

Nyní, když máme normální, můžeme vzít bodový produkt mezi ním a směrem světla.

to nám řekne, jak přímo povrch směřuje ke světlu, a proto by měl být jasný.

vezmeme max této hodnoty s 0, abychom zabránili negativním hodnotám v nežádoucích účincích na temnou stranu objektů.

// 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);

jednou z velmi důležitých částí Vykreslování, kterou lze snadno přehlédnout, je korekce gama. Hodnoty pixelů odeslané na monitor jsou v gama prostoru, což je nelineární prostor používaný k maximalizaci přesnosti, použitím méně bitů v rozsazích intenzity, na které jsou lidé méně citliví.

protože monitory nepracují v „lineárním“ prostoru, musíme kompenzovat jejich gama křivku před výstupem barvy. Rozdíl je velmi patrný a měl by být vždy opraven. Ve skutečnosti nevíme, že křivka gama pro konkrétní zobrazovací zařízení je, takže celá situace s zobrazovací technologií je hrozný nepořádek (tedy krok ladění gama v mnoha hrách), ale běžným předpokladem je následující křivka gama:

konstanta 0.4545 je jednoduše 1.0 / 2.2

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

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

stíny

pro výpočet stínů můžeme vystřelit paprsek začínající v místě, kde jsme protínali scénu a šli ve směru světelného zdroje.

pokud tento pochod paprsků způsobí, že něco zasáhneme, pak víme, že světlo bude také zablokováno, takže tento pixel je ve stínu.

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);

rovina země

přidejme rovinu země, abychom mohli lépe vidět stíny vržené našimi kuličkami.

w složka n představuje vzdálenost roviny Od počátku.

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

měkké stíny

stíny v reálném životě se okamžitě nezastaví, mají nějaký spad, označovaný jako penumbra.

můžeme to modelovat tím, že vezmeme několik paprsků z našeho povrchového bodu, každý s mírně odlišnými směry.

pak můžeme shrnout výsledek a průměr nad počtem iterací, které jsme provedli. To způsobí, že okraje stínu budou mít

některé paprsky zasáhly a jiné chyběly, což dává 50% temnotu.

nalezení poněkud pseudo náhodného čísla lze provést několika způsoby, použijeme však následující:

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

Tato funkce vrátí číslo v rozsahu [0, 1). Víme, že výstup je vázán na tento rozsah, protože nejvzdálenější operací je fract, který vrací zlomkovou složku čísla s plovoucí desetinnou čárkou.

pak to můžeme použít k výpočtu našeho stínového paprsku následujícím způsobem:

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);

mapování textur

spíše než definovat jednotnou barvu povrchu (nebo jinou charakteristiku) rovnoměrně po celém povrchu, lze definovat vzory, které se použijí na povrch pomocí textur.Budeme se zabývat třemi způsoby, jak toho dosáhnout.

3D mapování textur

v shadertoy jsou snadno dostupné objemové textury, které lze přiřadit kanálu. Zkuste vzorkovat jednu z těchto textur pomocí 3D polohy povrchového bodu:

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

jedním ze způsobů, jak vzorkovat šum, je sečíst více měřítek pomocí něčeho podobného:

// 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 ;

výše uvedené konstanty / závaží se obvykle používají pro fraktální šum, ale mohou mít libovolné požadované hodnoty. Zkuste experimentovat s váhami / váhy / barvy a vidět, jaké zajímavé efekty můžete dosáhnout.

zkuste animovat svůj objekt pomocí iTime a sledovat, jak se chová struktura svazku. Lze toto chování změnit?

2D mapování textur

použití 2D textury je zajímavý problém-jak promítat texturu na povrch? V normální 3D grafice má každý trojúhelník v objektu přiřazen jeden nebo více UV, které poskytují souřadnice oblasti textury, která by měla být mapována na trojúhelník (mapování textury). V našem případě nemáme UV poskytnuté, takže musíme přijít na to, jak vzorkovat texturu.

jedním z přístupů je vzorkování textury pomocí světové projekce shora dolů vzorkováním textury založené na souřadnicích X & Z:

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

jaká omezení vidíte s tímto přístupem?

Triplanární mapování

pokročilejší způsob mapování textur je provést 3 projekce z primárních OS a poté výsledek smíchat pomocí triplanárního mapování. Cílem míchání je vybrat nejlepší texturu pro každý bod na povrchu. Jednou z možností je definovat hmotnosti směsi na základě vyrovnání povrchu normální s každou světovou osou. Povrch, který směřuje dopředu s jednou z OS, obdrží velkou hmotnost směsi:

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);}

jaká omezení vidíte s tímto přístupem?

materiály

spolu se vzdáleností, kterou vracíme z funkce castRay, můžeme také vrátit index, který představuje materiál zasaženého objektu. Tento index můžeme použít k odpovídajícím barvením objektů.

naši operátoři budou muset vzít vec2s spíše než plováky a porovnat první složku každého z nich.

nyní při definování naší scény také určíme materiál pro každý primitiv jako y složku vec2:

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;

tento index materiálu pak můžeme vynásobit některými hodnotami ve funkci render, abychom získali různé barvy pro každý objekt. Vyzkoušejte různé hodnoty.

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

vybarvme rovinu země pomocí šachovnicového vzoru. Vzal jsem si tuto fantastickou analyticky vyhlazenou funkci checkerbox z webových stránek Inigo Quilez.

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;}

projdeme XZ komponenty naší rovinné polohy, aby se vzorec opakoval v těchto rozměrech.

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

mlha

nyní můžeme na scénu přidat mlhu na základě toho, jak daleko došlo k každé křižovatce od kamery.

zjistěte, zda můžete získat něco podobného následujícímu:

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

tvar & materiál blendingabyste se vyhnuli drsnému záhybu danému operátorem min, můžeme použít sofistikovanější operátor, který hladce mísí tvary.

// 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

vzorkováním scény mnohokrát s mírně odsazenými vektory směru kamery můžeme získat vyhlazenou hodnotu, která zabraňuje aliasingu.

vyvedl jsem výpočet barvy scény do své vlastní funkce, aby bylo volání ve smyčce jasnější.

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;

optimalizace počtu kroků

pokud si představíme, kolik kroků podnikneme pro každý pixel červeně, můžeme jasně vidět, že paprsky, které nezasáhly nic, jsou zodpovědné za většinu našich iterací.

to může u některých scén výrazně zvýšit výkon.

if (t > drawDist) return backgroundColor;

tvar & materiálová interpolace

můžeme interpolovat mezi dvěma tvary pomocí funkce mix a pomocí iTime modulovat v průběhu času.

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));

opakování domény

je poměrně snadné opakovat tvar pomocí pole se znaménkem vzdálenosti, v podstatě stačí modulo vstupní pozici v jedné nebo více rozměrech.

tato technika může být použita například k opakování sloupce několikrát bez zvětšení velikosti reprezentace scény.

zde jsem zopakoval všechny tři složky vstupní polohy a poté použil operátor odčítání ( max ()) k omezení opakování na ohraničující rámeček.

jeden gotcha je, že musíte odečíst polovinu hodnoty, kterou modulujete, abyste soustředili opakování na svůj tvar, abyste jej neřezali na polovinu.

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

efekty po zpracování

viněta

ztmavnutím pixelů, které jsou dále od středu obrazovky, můžeme získat jednoduchý efekt viněty.

kontrast

tmavší a světlejší hodnoty mohou být zvýrazněny, což způsobuje zvýšení vnímaného dynamického rozsahu spolu s intenzitou obrazu.

col = smoothstep(0.0,1.0,col);

„Ambient occlusion“

pokud vezmeme inverzní obrázek zobrazený výše (v optimalizacích), můžeme získat podivný efekt podobný AO.

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

Ad infinitum

jak můžete vidět, mnoho efektů po zpracování může být implementováno triviálně; pohrajte si s různými funkcemi a podívejte se, jaké další efekty můžete vytvořit.

www.shadertoy.com/view/MtdBzs

co bude dál?

právě jsme zde pokryli základy; v této oblasti je třeba prozkoumat mnohem více, jako například:

  • podpovrchový rozptyl
  • okolní okluze
  • animované primitivy
  • primitivní deformační funkce (kroucení, ohyb,…)
  • průhlednost (lom, louh, …)
  • optimalizace (hierarchie svazků)

Procházet ShaderToy získat nějakou inspiraci o tom, co lze udělat, a hrabat přes různé shadery vidět, jak jsou implementovány různé efekty. Mnoho shaderů má proměnné, které můžete vyladit a okamžitě vidět účinky (alt-enter je zkratka pro kompilaci!).

také dát odkazy přečíst, pokud máte zájem dozvědět se více!

Díky za přečtení! Nezapomeňte nám poslat své skvělé shadery! Pokud máte nějakou zpětnou vazbu na kurz, rádi bychom to také slyšeli!

kontaktujte nás na Twitteru @liqwidice & @hdb1

Electric Square je pronájem!

Doporučená četba:

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

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

Ray Tracing za jeden víkend: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html

Physically-based rendering bible, PBRT: https://www.pbrt.org/

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.