Sütik

Sütiket használunk a tartalom személyre szabására és a forgalom elemzésére. Kérjük, határozza meg, hogy hajlandó-e elfogadni weboldalunkon a sütiket.

Oldal tetejére
Bezárás
Zengo - Hogyan írjunk saját Shadert Unityben - Shader graph
Kategória:

Hogyan írjunk saját Shadert Unityben - Shader graph

Zengo - óra6 perc olvasási idő
2023. 10. 17.

A shaderekkel már foglalkoztunk az előző blogbejegyzésünkben - A kezdő lépések -, most pedig itt az idő, hogy megvizsgáljuk, hogyan tudjuk ezeket megalkotni modernebb köntösbe bújtatott vizuális eszközökkel! Az egyszerűség kedvéért a Unity saját, széles körben elérhető, node-alapú vizuális gráf-szerkesztőjét fogjuk használni, a Shader Graph-ot. Foglalkozni fogunk sub-gráfokkal, egyéni scriptek használatával és azzal, hogyan célszerű kód alapú shadert átalakítani gráffá.

Miért érdemes foglalkozni a Shader Graph-al? Ha nem írtunk még saját shadert, akkor elsőre meghökkentő lehet a szintaxis, a folyamatok, a módszerek. Gráffal egyszerűbb dolgozni, sokkal inkább felhasználóbarát. Kerültek bele olyan újdonságok is, amik a kód alapú shaderben nem elérhetőek, mint például a beépített zaj node (normál és Gauss). Azonban a gráf és a kód nem zárja ki egymást: a “Custom function” node segítségével továbbra is írhatunk saját kódot.

Shader Graph

A Shader Graph régóta elérhető SRP (Scriptable Render) pipeline-okban (URP, HDRP), és a 2021.1 verzió óta a beépített (Built-In) pipeline-ban is használható. Amennyiben valamelyik SRP-t használjuk, úgy a csomag automatikusan telepítésre kerül új projekt létrehozásakor. Built-In esetén azonban csupán a kompatibilitás érdekében támogatják a shader gráfot, új funkciók már nem kerülnek hozzáadásra. Épp ezért a blogbejegyzésünkben 2022.3f. URP verziót használunk.

Egy shader gráf létrehozása is hasonlóan történik, mint egy shader asset-é: jobb klikk a project mappában → Create → Shader Graph → URP. Itt ki tudjuk választani a számunkra megfelelő típust. Ha menet közben változnak az igények, például unlit shadert hoztunk létre, de lit shadert szeretnénk csinálni, akkor a Graph Settingsben át tudjuk állítani a tulajdonságait. Így kapunk a végén egy .shadergraph kiterjesztésű fájlt.

Miután megnyitottuk a létrehozott gráfunkat, bal oldalon található a "Blackboard", aminél a shader assetekhez hasonlóan a public változókat, a kategóriákat, továbbá a kulcsszavakat tudjuk megadni. A kulcsszavak itt is úgy működnek, mint a shader kódban: megszabhatjuk, hogy különböző grafikai beállításoknál hogyan viselkedjen a shader, a kategóriák segítségével pedig a változóinkat tudjuk átláthatóbbá tenni. Ebből a táblából drag&drop módszerrel tudjuk ezeket a gráfhoz hozzáadni.

A jobb oldali részen a Graph Inspector található. A Graph Settings részen a gráf egészére vonatkozó beállításokat, a Node Settings fülön pedig a különböző node-ok pontosságát és előnézeti információit érjük el, továbbá a változókat is itt tudjuk testre szabni.

100%

Ha például szeretnénk létrehozni egy slidert, ahhoz:

  1. először a "+" jelre kattintva hozzá kell adnunk egy float változót,
  2. majd a Node Settings fülön a Mode-nál kiválasztjuk a Slidert,
  3. és megadjuk a minimum, maximum, és alapértelmezett értéket.

Az “Exposed” rubrika azt jelenti, hogy az adott változó látható az Inspectorban. Ha nincs kipipálva ez a rubrika, akkor csak kódból tudjuk állítani az értékét.

Sub graph-ok és Custom function

A sub graph-ok segítenek nekünk az újrafelhasználhatóságban és átláthatóságban. Hasonlóképpen tudjuk létrehozni őket mint a .shadergraph fájlokat, csak a Shader Graph lehetőségnél nem az URP, hanem a Sub-graph opciót választjuk.

Ha megnyitjuk a létrehozott .shadersubgraph-ot, észrevehetjük, hogy egyetlen Output nevű node szerepel benne. Rákattintva az Inspector Node Settings részénél megadhatunk neki egy vagy több “Inputot”. Ha máshol ezt felhasználjuk, akkor ezekkel az értékekkel tudunk tovább dolgozni.

100%

A Custom function node arra szolgál, hogy saját HLSL kódunkat be tudjuk szúrni a gráfba. A kódot közvetlenül is beleírhatjuk a “string” módot használva, vagy referenciaként megadhatunk neki már kész megoldást is. A kimeneteket és bemeneteket azonban nekünk kell megadni.

Írjuk át az előző blogposzt során létrehozott toon shaderünket!

A fentiek alapján hozzunk létre egy unlit shadergraph assetet és egy subshader assetet, amit nevezzünk el MainLight-nak. A shadergraph-ban előkészíthetjük a változókat, viszont a továbbiakban egyelőre a MainLight-ban dolgozunk.

Vizsgáljuk meg egy kicsit az előző blogbejegyzésben lévő kódunkat! Az NDotL és NDotH változókat többször is használjuk. A _WorldSpaceLightPos0, _LightColor0, és a SHADOW_ATTENUATION(i) értékekre mindenképpen szükségünk lesz.

100%

Felmerülhet bennünk a kérdés: mégis hogyan kapjuk meg például au AutoLight.cginc, Lightning.cgnic-ben tárolt segédváltozókat? Bár valóban nincs rá készen node (bár 2022.1 verziótól elérhető a Main Light direction), tudunk egyéni kódot használni a “Custom function” node segítségével.

Az interneten elérhető több, különböző fények számítására szolgáló HLSL kód, vagy akár írni is lehet sajátot, ha a helyzet megkívánja. Jelen esetünkben az egyszerűség kedvéért a Unity erre vonatkozó példaprojektjéből fogjuk használni a kódot. Töltsük le és adjuk hozzá a projekthez.

Nyissuk meg a subraph-ot és adjuk hozzá a Custom Function node-ot. Ha megvizsgáljuk a kódot, észrevehetjük, hogy több függvény is szerepel benne. Mi most a MainLight_float()-ot fogjuk használni - később az AdditionalLights-ot felhasználhatjuk arra, hogy a shaderünk a directional light helyett más fényforrásokat is használjon.

Figyeljük meg a MainLight paramétereit: 1 bemenő, és 3 kimenő. Paraméterezzük fel az előbb létrehozott Custom Function-t eszerint. Vigyázzunk, hogy a változók típusát helyes sorrendben adjuk meg. "Name"-nél elég a függvény nevét megadni, a pontosság nem kell.

100%

Ezzel minden fontosabb változó a birtokunkban van a shader elkészítéshez. Szerencsére a Normal Vector (worldNormal) és View Direction (viewDir) elérhető beépített node-ként. A kód alapján készítsük el az NDotL és NDotH változókat is.

A felhasznált sorok a következők:

float3 normal = normalize(i.worldNormal);
float NdotL = dot(_WorldSpaceLightPos0, normal);
float shadow = SHADOW_ATTENUATION(i);
float3 viewDir = normalize(i.viewDir); 
float3 halfVector = normalize(_WorldSpaceLightPos0 + viewDir); 
float NdotH = dot(normal, halfVector);

100%

Adjuk hozzá a subgraph kimeneti node-jába az egyedi függvényünk kimeneteit, ügyelve a típusokra és a pontosságra (a DistanceAttent-t kihagyhatjuk, mert a továbbiakban nem fogjuk használni), és az NDotL, NDotH-t is, illetve kössük be őket. Mivel a rimlight is sok műveletből áll, érdemes azt is egy subgraph-ba kiszervezni. Láthatjuk, hogy csak a végén használjuk a rim változót, úgyhogy elég egy kimenetet megadni neki.

Ez a gráf annyiban fog különbözni az előzőtől, hogy bemenetekre is szükség lesz (NDotL, RimColor, RimThreshold, Rimamount). Hiába hoztuk esetleg már létre őket a fő gráfunkban, itt a subshaderben ezt ismét meg kell tennünk. Az eredeti shaderhez képest módosíthatunk rajta azzal, hogy a 1 - dot(viewDir, normal) helyett egy Fresnel Effect node-ot használunk.

A felhasznált sorok a következők:

float4 rimDot = 1 - dot(viewDir, normal);
float rimIntensity = rimDot * pow(NdotL, _RimThreshold);
rimIntensity = smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rimIntensity); 
float4 rim = rimIntensity * _RimColor;

A subshaderekkel való műveleteket ezzel befejeztük, visszatérhetünk a fő gráfunkra. Ha mindent jól csináltunk, az alábbi műveleteket kell még implementálnunk:

100%

A felhasznált sorok a következők:

float lightIntensity = smoothstep(0, 0.01, NdotL * shadow);
float4 light = lightIntensity * _LightColor0; 
float specularIntensity = pow(NdotH * lightIntensity, _Glossiness * _Glossiness);
float specularIntensitySmooth = smoothstep(0.005, 0.01, specularIntensity);
float4 specular = specularIntensitySmooth * _SpecularColor;
float4 sample = tex2D(_MainTex, i.uv);

A végén pedig a Base Color kimenetbe az alábbit kötjük:

(light + _AmbientColor + specular + rim) * _Color * sample;

100%

A könnyebb követhetőség érdekében változó nevenként csoportokba szedtük a node-okat. Láthatjuk, hogy bár ez nem egy bonyolult gráf, a subgraph-okba rendezés növelte az olvashatóságot, ami később hibakeresésnél és visszaellenőrzéskor hasznos lesz.

Érdekességképpen az alábbi képen látható, hogy milyen lett volna subgraphok és módosítás nélkül a shaderünk:

100%


A cikket írta: M. Tamara