Van nekem egy magasságtérkép szerkesztő progim, nem egy nagy szám, van pár funkciója, többek között árnyékot is tud számolni, majd elmenti png-be. Eredetileg az árnyékot cpu-n számoltam, elég sok háromszög-sugár ütközés volt benne, és kb 10 percig tartott, mire egy 512x512 heightmap-en kiszámolt egy 2048x2048-as árnyéktérképet. Ráadásul nem is volt túl szép, még blur-ozni is kellett. Gondoltam, kipróbálom mi lenne ha gpu-n tolnánk a sugarakat, csináltam már afféle godray alapú árnyékokat 2d-s játékmotorban, ide pont jó lenne.
Elég egyszerű a dolog, kell hozzá 2 rendertarget textúra, az egyikbe készül a magasságtérkép, a másikba a shadowmap.
Az első mérete elég ha akkora, ahány cellából a térkép áll. Ebbe bele kell renderelni a hieghtmap-et úgy, hogy a legmagasabb pontja legyen 1, a legmélyebb pontja pedig 0. Ez nagyon könnyű, még projekciós mátrix sem kell hozzá. Csak -1 +1, és 1 -1 közé kell konvertálni a heightmap vecktorok xz komponensét, (vertex shaderben) majd a PS-nek átadva az y koordinátát azt elosztja a magasságtérkép legnagyobb magasságával, ezzel 0-1 közé konvertálja azt. (ha a lehet a vektor.y negatív, akkor még attól relatívvá kell tenni)
Kb ennyi a shader kód:
float2 origo; // hülye név csak a cellaszám felét adja meg
float2 zproj; // ’.x’ a minimum y, .y a himap magassága abszolút
void vs_himap(
// input data
float4 pos :POSITION,
float3 n :NORMAL,
float4 color0 :COLOR0,
float4 color1 :COLOR1,
// output
out float4 opos :POSITION,
out float opixelheight :TEXCOORD0)
{
opos.xy = (pos.xz/origo);// world coord to x: -1 -> +1 y: +1 -> -1
opos.zw = float2(0,1);
opixelheight = pos.y; // ps needz it
}
// simple diffuse with one color
void ps_himap( float pixelheight :TEXCOORD0,
out float4 oColor :COLOR)
{
oColor = (pixelheight-zproj.x) / zproj.y;
}
Most ebből számoltassam már ki a árnyékot, mégpedig úgy, hogy minden pixel koordinátájából a nap fele megnézünk egy csomó pixelt, és ha magasabb, mint a sugár y értéke ott, akkor árnyékol, csökkentsük a végső árnyékpixel-intenzitást. Ez gyakorlatilag egy csomó ’texture-fetch’ (pixel szín kiolvasás textúrából), ami talán a leglassabb, amit a gpu-n lehet. Egyébként ezt a módszert használják a pálmafa-levélen átszűrődő fénycsíkok rajzolására is a Crysis-ben például.
Kód:
float3 Invlightdir;
float2 HalfPixel;
texture Thimap; // heightmap
// Himap
sampler Shimap = sampler_state
{
Texture = <Thimap>;
MinFilter = POINT;
MagFilter = POINT;
MipFilter = POINT;
AddressU = CLAMP;
AddressV = CLAMP;
};
void vs_shadowmap(
float4 pos :POSITION,
// output
out float4 opos :POSITION,
out float2 texcoord :TEXCOORD0)
{
opos = pos;
// -1 -> +1 +1->-1 map to 0-1,0-1
texcoord.x = pos.x*0.5 + 0.5;
texcoord.y = (-pos.y)*0.5 + 0.5;
texcoord+=HalfPixel;
}
void ps_shadowmap( float2 texcoord :TEXCOORD0,
out float4 oColor :COLOR)
{
const int ITER = 256;
float3 texadd = Invlightdir;
float basehi = tex2D(Shimap,texcoord).r;
float3 tc = float3(texcoord.x,basehi,texcoord.y);
float finalcolor = 1;
for(int i=0;i<ITER;i++)
{
tc+=texadd;
float h = tex2D(Shimap,tc.xz).r;
if(h>tc.y) finalcolor -= 0.01; // soft shadow lesz
}
oColor = finalcolor;
oColor.a = 1; // csak hogy lássuk
}
A vertex shader nem nagy vaszidszdasz, egy target méretű négyzetet rajzolunk, aminek a koordinátái már -1 +1 közé esnek, csak a textúra koordinátákat kell ebből kiszámolni – át is lehetne adni a VS-nek, de mindegy.
A lényeg a pixelshaderben van (ps_shadowmap). A texadd az a növekmény, amit minden iterációban hozzáadunk a pixelpozícióhoz (tc). Kicsit bonyinak tűnhet, de nem az. A tc.xz a textúrakoordináta, a tc.y pedig a magassága a pixelnek a hieghtmap textúra alapján. Így a texadd.xz a texcoord növekmény, a texadd.y pedig a sugár emelkedésének növekménye pixelenként. Igazából itt van a cica elásva. Ez az Invlightdir –ból jön, amit be kell állítani a hieghtmapnek megfelelően. Az x és z komponens az egy pixelre jutó textúrakoordináta növekmény, mondjuk 512x512 heightmap textúra esetén 1.f / 512. Az y pedig az egy pixelre jutó sugáremelkedés. Ezt úgy kapjuk meg, hogy a heigthmap maximum magasságával osztjuk a normált raydir y komponensét. Inkább beszéljen a kód:
VECTOR smallm = Lightdir; // normalized!
smallm.y /= heightmapysize;
float pixelnum = float(HieghtMap.GetTexSizeX());// heightmap textúra mérete (x==y nálam)
smallm.x /= pixelnum; // osszuk le pixelméretre
smallm.z /= pixelnum;
S ezt a ’smallm’-et lehet beállítani az effect ’ float3 Invlightdir’ paraméterébe. S kész is. A kérdés csak az, megérte-e a hardvert használni? A szoftveres 10 percig fut – ez 2 EZREDMÁSODPERCIG! Szóval sokkal gyorsabb, és sokkal gyorsabban meg is lehet írni.
Pár apróság, hogy ne menjen el másnak erre egy este:
- Képernyőnél nagyobb rendertarget-et csak megfelelő méretű z bufferrel lehet használni dx9 alatt, ehhez a SetDepthStencilSurface() a kulcs.
- rendertarget formátuma: a hieghtmaphez én a kedvenc D3DFMT_R32F-t használom, ez pontos és talán elég elterjedt. A shadowmap-hez próbáltam egy 8 bites formátumot, a D3DFMT_L8-at, amit a kártyám támogat is a dxcapsviewer szerint, de érdekes módon x irányban széthúzta a kb 4 szeresére a textúrát, 32bit helyett 8 bitbe próbáltam renderelni, talán azért, de ez nem indok arra, hogy ne működjön. Mindegy, inkább D3DFMT_A8R8G8B8 lett, ami 4-szer annyit foglal, de úgyse kell sokáig, csak amíg kimentjük fileba.
Végül egy kép, hogy mégis, a kék a magasságtérkép, a fehér a shadowmap.
esetleg egy részletesebb, 64 iterációval és nagyobb fényerőcsökkentéssel: