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.
Ha például szeretnénk létrehozni egy slidert, ahhoz:
- először a "+" jelre kattintva hozzá kell adnunk egy float változót,
- majd a Node Settings fülön a Mode-nál kiválasztjuk a Slidert,
- é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.
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.
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.
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);
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:
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;
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:
A cikket írta: M. Tamara