Mindenki értetlenül néz, hogy miért foglalkozok shadow volume-al, hiszen múlt századi technológia. De nem ám! CAD-ban a mai napig ez az egyetlen ami szóba jön ("itt kezdődik az árnyék"),
annak ellenére, hogy memóriaigényes és lassú (DX10 óta létezik gyorsabb megoldás).
Why so serious?
Történelemből sosem voltam túl jó, de a Google szerint ezt a koncepciót már 1977-ben felvetette egy Frank Crow nevű ember, de az első implementáció 1991-ben történt csak meg, hol máshol mint
a Silicon Graphics-nál (SGI). Aki nem tudná tőlük ered az OpenGL (amit akkor még IRIS GL-nek hívtak). Ezt csak azért mondom, hogy tudjátok kinek küldeni az (OpenGL-t) anyázó leveleket.
Ezt itt és most kőbe vési mindenki. Van egyébként - kvázi - megkerülési lehetőség, de a hablatyolásait olvasva felejtősnek mondanám. A feltételt egyesek (az emlegetett idióta tutoriálok, ami nem ez) úgy fogalmazzák meg, hogy az objektum zárt kell legyen (vagyis nem lehet benne törés). Ez gyengébb feltétel, nem elégséges. A helyes megfogalmazás az, hogy az objektum 2-manifold kell legyen (bármit is jelentsen az). A feltételt az objektumok 99%-a nem teljesíti (még egy kocka sem), tehát rögtön következik, hogy külön célszerű tárolni a rendereléshez szükséges változatot (texcoordokkal, normálokkal) és amit árnyékgeneráláshoz használsz (csak pozíció). Amennyiben az árnyéktestet a CPU-n generálod, akkor a generátor objektum lehet a rendszermemóriában (és így kirakhatod külön szálakra). A Doom 3 tudtommal nem, de a Quake 4 előre eltárol collision object-et a modelljeihez, amit fizikához és árnyékhoz használ.
Menüett
Amennyiben sikerült kitalálnod az éleket (nem különösebben egetrengető feladat), akkor a rendereléskor az első teendő, hogy a fény irányából nézve meghatározd az objektum "körvonalait" (sziluett), azaz a megfelelő éleket. Ezt úgy lehet könnyen és gyorsan megcsinálni, hogy végigrombolsz az éleken, melyekben eltároltad, hogy melyik pontosan kettő háromszöghöz tartozik, és megvizsgálod a pontosan kettő háromszög normáljait. Ha az egyik a fény felé néz, a másik meg nem, akkor az a sziluetthez tartozó él. Vidám kódrészlet következik: CODE
D3DXMATRIX inv;
D3DXVECTOR3 lp;
D3DXMatrixInverse(&inv, NULL, &caster.world);
D3DXVec3TransformCoord(&lp, &lightpos, &inv);
// for ...
const Edge& e = edges[i];
a = D3DXVec3Dot(&lp, &e.n1) - D3DXVec3Dot(&e.n1, &e.v1);
b = D3DXVec3Dot(&lp, &e.n2) - D3DXVec3Dot(&e.n2, &e.v1);
if( (a < 0) != (b < 0) )
{
if( a < 0 )
{
std::swap(e.v1, e.v2);
std::swap(e.n1, e.n2);
}
silhouette.push_back(e);
}
Ha pont fényed van (nekem most az), akkor nyilván a háromszög síkjától való távolságot kell nézni. Ha pozitív, akkor a fény felé néz. Illetve van még egy megemlítendő rész, de az implementációtól függ: ha a második háromszög néz a fény felé, akkor megfordítom az élet (különben a kinyújtott volume bejárása fordított lesz). Remélem senki nem siklott át azon tény felett, hogy a fény object space-ben kell legyen (és a modell is btw., ha még nem tűnt volna fel). Nem mindegyik tutoriál említi meg ezt a lényegtelen kulcskérdést. A következő feladat az, hogy a sziluettet kinyújtsad a végtelenbe. Ha nem akarod nem muszáj, de valamivel ki kell töltenem a helyet. Valamikor mondtam ilyet, hogy homogén koordináta, sőt azt is mondtam, hogy ha w = 0, akkor az egy vektort jelent, speciel az (x, y, z) irányú egyenesek végtelenbeli közös pontját. A volume palástja tehát élenként két háromszög, azaz négy vertex, melyből kettő az él vertexei, a másik kettő pedig... nos egy irányvektor a fényből a megfelelő élvertexbe. CODE
D3DXMATRIX inv;
D3DXVECTOR3 lp;
D3DXMatrixInverse(&inv, NULL, &world);
D3DXVec3TransformCoord(&lp, &lightpos, &inv);
// for ...
const Edge& e = silhouette[i / 4];
vdata[i] = D3DXVECTOR4(e.v1, 1);
vdata[i + 1] = D3DXVECTOR4(e.v1 - lp, 0);
vdata[i + 2] = D3DXVECTOR4(e.v2, 1);
vdata[i + 3] = D3DXVECTOR4(e.v2 - lp, 0);
A fényt és a modellt továbbra is object space-ben kell megadni. World space-ben nem jó!!! ((p - l)M = pM - lM igaz, de gondold végig, hogy lM az micsoda). Ezek után a vertex shader a következőt műveli: CODE
void vs_shadowvolume(in out float4 pos : POSITION)
{
pos = mul(mul(pos, matWorld), matViewProj);
}
Ó mily egyszerű, nem kell ide semmiféle ?: Ne erőlködj, a fixed function pipeline-on nem tudod megcsinálni ezt, hacsak nem csinálod az egész transzformációt te és a végén D3DFVF_XYZRHW-ben küldöd le. Ugyanis a D3DFVF_XYZW azzal nem működik (OpenGL-ben lehet, hogy igen).
Vicc a projekciós mátrixról
A távoli vágósík a végtelenben kell legyen.
CODE
proj._33 = 1;
proj._43 = -near;
Ez DirectX-es projmátrix, ne károgj hogy 2-vel kéne szorozni. Kiszámolod a lim (z - near) / z határértéket (z → ∞) és megnyugtatod magadat, hogy ez tényleg jó.
Epic fail
Másfél milliós kérdés következik: hogy mondod meg, hogy egy pont az árnyéktesten belül van-e? A közönséget nem tudod megkérdezni, de Tim Heidmann kitalált egy tök jó megoldást, úgyhogy őt hívd fel inkább.
A képen sárga a shadow volume és zöld a kamera néhány sugara. A számok a stencil buffer értékét jelentik abban a pixelben. Vegyük észre, hogy ahol árnyék van, ott a stencil buffer értéke >= 1 marad. Tök jó nem? Most jöjjön egy tévedés:
Az elülső befedésre a legegyszerűbb módszer, hogy végigdarálsz a háromszögeken, és ami a fény felé néz azt bepakolod a shadow volume-ba. Csak zöldfülűek ájulnak be a lockolástól; az okosak eleve sysmem-be rakják az árnyékvetőt. A hátsó befedésre mondok két módszert:
A legjobb cikk ami ezt az egészet leírja 12-szer ennyi oldalban itt található.
Gyorsítás geometry shader-el
Eddig DX10-el nem foglalkoztam és most emlékeztettem magamat arra, hogy miért nem. Rögtön mondom, hogy a PIX-et el lehet felejteni, mert nem működik (a Windows 7 SDK-val még igen),
helyette Visual Studio 2012-ben van beépített debugger, ami jóval kevesebbet tud. A DirectX jó szokásához híven szarul van dokumentálva (a reference azért jó), az SDK-s példaprogikhoz pedig sok szerencsét... apropó, van shadow volume-os sample. 30 ezer sor.
CODE
caster->GenerateGSAdjacency();
caster->Discard(D3DX10_MESH_DISCARD_DEVICE_BUFFERS);
caster->CommitToDevice();
Érdemes figyelni az output ablakot, mert kiírja, ha valami baja van. A Discard() különösen fontos (magától nem csinálná ám meg, neeeeeem...). Na de, ha ezen túltette magát mindenki, akkor jöhet a geometry shader.
CODE
[maxvertexcount(18)]
void gs_extrude(
in triangleadj GS_Input verts[6],
in out TriangleStream<GS_Output> stream)
{
float4 planeeq;
float3 a = verts[2].origpos - verts[0].origpos;
float3 b = verts[4].origpos - verts[0].origpos;
float dist;
// távolság a fénytől
planeeq.xyz = cross(a, b);
planeeq.w = -dot(planeeq.xyz, verts[0].origpos);
dist = dot(planeeq, lightPos);
if( dist > 0 )
{
// palást
ExtrudeIfSilhouette(verts[0].origpos, verts[1].origpos, verts[2].origpos, stream);
ExtrudeIfSilhouette(verts[2].origpos, verts[3].origpos, verts[4].origpos, stream);
ExtrudeIfSilhouette(verts[4].origpos, verts[5].origpos, verts[0].origpos, stream);
// ...
}
}
A kinyújtás elég trivi, ha figyeltél fentebb, akkor könnyen ki lehet találni (a másik háromszög a fénytől elfelé kell nézzen). Egy kinyújtott él két darab háromszög (azaz 4 vertex, triangle strip). Hátravan még a két sapka. Mivel lusta vagyok, ezt is ugyanebben a shaderben fogom elintézni (ne szivassuk a pipeline-t, szivat ő minket eleget). Meglepően egyszerű, a // ... helyére kell tenni. Kitolod a domináns háromszöget (front cap), majd kinyújtod a végtelenbe és a bejárását megfordítod (back cap). A homogén w koordinátának 1e-5f-et adtam meg, nézzük mi fog történni: depth = (z - near * 1e-5f) / z Ugye normál esetben (w = 0) az érték pont 1 lenne, namost ez nekem zfightolt (bármennyire is nem kéne neki). A módosítással legalábbis kisebb lesz 1-nél, de vigyázz, mert az xy is kisebb lesz!!! Mivel a pici érték miatt nem vehető észre, ezért nem foglalkoztam vele. Ami viszont megér egy misét az a DX10 eszméletlenül idióta szemlélete. Oké, nem kötelező használni (arra lett tervezve, hogy shaderből állítsál be minden renderstatet), de ennél idiótábban nem is csinálhatták volna meg. CODE
D3D10_RASTERIZER_DESC rasterdesc;
ID3D10RasterizerState* nocull = 0;
rasterdesc.AntialiasedLineEnable = FALSE; // mit érdekel engem???
rasterdesc.DepthBias = 0; // mit érdekel engem???
rasterdesc.DepthBiasClamp = 0; // mit érdekel engem???
rasterdesc.DepthClipEnable = TRUE; // mit érdekel engem???
rasterdesc.FillMode = D3D10_FILL_SOLID; // mit érdekel engem???
rasterdesc.FrontCounterClockwise = FALSE; // mit érdekel engem???
rasterdesc.MultisampleEnable = FALSE; // mit érdekel engem???
rasterdesc.ScissorEnable = FALSE; // mit érdekel engem???
rasterdesc.CullMode = D3D10_CULL_NONE; // ezt akartam beállítani
device->CreateRasterizerState(&rasterdesc, &nocull);
device->RSSetState(nocull);
// és akkor most visszaállítani...
Emlékeztetőül DX9 és OpenGL: CODE
device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); // dx9
glCullFace(GL_NONE); // opengl
Véleményemet egyetlen tömör és frappáns mondatban fogalmaznám meg a Microsoft felé: De visszatérve a shadow volume-hoz: itt bukott ki, hogy a stencil operáció wrap kell legyen (thx syam kollégának). Hát persze, hiszen nem tudod, hogy two-sided stencil esetében először csökkent-e vagy növel. A Visual Studio hiányossága, hogy a stencil buffert nem tudja megmutatni (hacsak ki nem hackeled valahogy az alfából). Az nVidia NSight pedig ilyen üzenetet dob fel, hogy hát sajnos DX10-el nem működik. Megérte regisztrálni...nekik is címezném a fenti frappáns mondatot.
Summarum
Ez a cikk nem fölöslegesssss!!!! Ezt az oldalt meg nem akarom efelejteni (ikon konverter, nagyon jó minőség).
Höfö:
|