A Raycasting előző I, II, illetve III, nekifutásánál Wolf3D stílusú falakat (pályát) rajzolgattunk. Ezen falak egymásra mindig merőlegesek voltak. Abból az egyszerű tényből következően, hogy a falakat egy mátrixon tároltuk (GridMap), vagy X, vagy Y irányúak voltak. A módszer nagy előnye, hogy a kirajzolandó falakat a mátrixon indított sugarakkal könnyen megtaláltuk (ez a raycasting lényege), de egy idő után elég unalmas kocka alakú szobákban tévelyegni. Így megszülettek azok a játékok, ahol a falak még mindig merőlegesek a padlóra, de egymással tetszőleges szöget zárhatnak be. (ld. DOOM) Hát innen a cím. Így akár majdnem kör alakú szobák is összerakhatók. Egy ilyen pálya kirajzolásához nem használható az előző három számban bemutatott raycasting, de az elméleti résznél szükségünk van rá. Így a módszerben kevésbé járatosaknak ajánlanám az erről szóló előző 3 számot.

Tehát kezdhetünk mindent előröl.

Induljunk el a kályhától.

Gondolkodjunk 3D-ben. Ez lesz a továbbiakban a koordináta-rendszerünk:

Az X és Y tengelyek által meghatározott sík a képernyő, a Z tengely pedig a monitor belseje felé mutat (mélység). A kirajzoláshoz át kell térnünk 2D-be, le kell képezni a pontokat. A leképezésre több féle képlet is létezik, mi használjuk ezeket:

X2d,Y2d : a leképezés utáni, már kirajzolható képernyőkoordináták
Xofs,Yofs : a 3d-s koordinátarendszer középpontja a képernyőn
X,Y,Z : a 3d-s koordináták.
Zeye : a szemlélő távolsága a leképezési síktól (képernyőtől).

A falakat poligonként próbáljuk meg kezelni, így egy fal négy sarkát leképezés után fogjuk kapni. A fal kirajzolásánál ki fogjuk használni, vertikálisak, így oszloponként kirajzolhatóak.

Ha az egész pályát felülről nézzük (XZ sík), akkor minden falat egy vonalként fogunk látni, mivel minden fal merőleges a padlóra (Y tengellyel párhuzamos). Egy falat az XZ síkon lévő kezdő és végpontja határoz meg. Egy fal képe:

Zeye a szemlélő távolságát jelenti a leképezési síktól. A leképezési sík az XY sík, tehát a képernyő. A falat P1 és P2 pontok határozzák meg. Ezen pontokhoz a szemlélő felől indított sugarak a kék vonalak. Vastag kékkel a fal leképezése található meg. Ez hasonlít a raycasting-hoz, ott is sugarak segítségével állapítottuk meg a fal képét. Itt csak elméletben használjuk őket. A piros vonal egy, a fal kirajzolása során kibocsátott sugár. A leképezett fal:

A falat poligonként fogjuk fel.

A poligon bal és jobb széle mindig függőleges, így képernyő-oszloponként kirajzolható, akár a raycasting-nél. Egy-egy képernyőoszlopra számolunk egy sugarat, amit a sugár és a fal metszéspontján találunk, azt rakjuk ki a képernyőre.

A probléma a fal és a sugár metszéspontjának megtalálása. A metszéspont fogja meghatározni a textúrapont koordinátáját, amit kirakunk. Egy képernyőoszlopon belül érintett falpontok Z koordinátája egyenlő, egy képernyőoszlopon belül ugyanaz lesz az U textúrakoordináta. Az U kiszámolásához kell a metszéspont.

A metszéspont kiszámolásához szükségünk lesz egy kis vektormatematikára.

Ha fel tudnánk írni a sugár egyenletét, és a falak egyenletét, az ebből képzett egyenletrendszer megoldása lenne a metszéspont. Ehhez tudnunk kell a sugár irányvektorát, ami minden képernyőoszlopra más és más:

Ebből kiragadva egy sugarat:

Zeye : a szemlélő távolsága a leképezési síktól.
N: a képernyőoszlop száma a képernyő közepéhez, azaz a szemlélőhöz képest (N=X-Xofs).
Tehát 320X200-as képernyőn Xofs=160, N=X-160.
a: a sugár szöge a Z tengelyhez képest.
dZ : az irányvektor Z összetevője.
dX : az irányvektor X összetevője.
Csak a Zeye, Xofs, és N értékeket ismerjük, ebből kéne kifejezni a többit.

Az irányvektor (dX,dZ).
Minden oszlopra kiszámolhatjuk előre ezt az irányvektort:

procedure
Calc; var   i : integer;   angle : single;begin   for
i:=0 to XMAX do   begin     angle:=arctan((i-160)/abs(Zeye));     RayVect[i].z:= cos(angle);     RayVect[i].x:= sin(angle);  end; end;
Ha ez megvan, akkor a sugár egyenlete az i.-dik oszlopra:

A fal egyenlete (ld. a 2. Ábrát):

Az egyenletrendszer megoldása (x,z)-re lenne a metszéspont. Nekünk nincs szükségünk a pontos metszéspontra, hiszen csak azt akarjuk megtudni, hogy hol metsszük a falat. Csak ennyi kell a textúrázáshoz. Ezt a "t" paraméter mondja meg, ha 0 akkor az elejét érintettük, tehát a nullás textúraoszlopot kell kiraknunk, ha 1, akkor az utolsó textúraoszlopot. Csak a "t"-t kell kiszámolnunk:

Ezt a "t"-t csak össze kell szorozni a textúra szélességével, és megvan az oszlop.

A fal egy oszlopának kirajzolásának lépései:

  • Meg kell találni a távolságát. Csak vesszük az oszlop Z koordinátáját. Ezt a kezdő és végoszlop Z-jéből közelítjük.
  • Az oszlop magasságának kiszámolása a Z koordinátából (leképezéssel). Ezt csak az első és utolsó oszlopra tesszük meg, a többit interpoláljuk.
  • Az U textúrakoordináta kiszámolása a "t" segítségével. 256X256-os textúrán U=256*t
  • Az oszlop kirajzolása. Tudjuk a V-t, az U-ra ki tudjuk számolni a lépésközt:

  • TexHeight a textúra magassága, Height az oszlop magassága.
  • Vesszük a következő oszlopot.
A fal kirajzolása előtt kiszámoljuk a négy sarkát (leképezzük), aztán oszlopról oszlopra kirajzoljuk.

A pálya kirajzolása valamivel bonyolultabb lenne, ha igazán gyors megoldást akarnánk, de elsőre maradjunk meg a legegyszerűbbnél. Minden egyes falat megpróbálunk kirajzolni. Leképezzük, ha a leképezése ráesik a képernyőre, akkor kirajzoljuk, ha nem akkor vesszük a következőt. Az a baj, hogy egymást takaró falak is léteznek, ekkor az utoljára kirajzoltat fogjuk látni. A probléma többféleképpen is megoldható. Vagy Z szerint sorbarendezzük a falainkat, és a legtávolabbitól haladunk a kirajzolásban a legközelebbi felé, vagy Z buffert használunk. A Z buffer esetünkben egyszerűbb, egy fal egy oszlopán belül ugyanaz a Z, így egy képernyőoszlophoz csak egy Z-t kell eltárolnunk. Az igazi módszer a kettő kombinációja lenne, de ezt majd egy későbbi alkalommal.

Egy kis forráskód.
A fal egy oszlopának kirajzolása:

procedure
WallTexColomn(X, Yup, Ydown, Z : integer; U : integer; N: byte); var   V,DeltaV : longint;   Y : integer;   tex : ^Ttexture;begin   if
Zbuf[X]<=Z then exit;   tex:=@textures[n];   DeltaV:=32768 div (Ydown-Yup+1);   V:=0;  for
Y:=Yup to Ydown do   begin     Screen[Y,X]:=tex^[V shr 8,U];     V:=V+DeltaV;  end;   Zbuf[X]:=Z;end;
Ez persze nem a teljes verzió, kell még bele clipping, meg egy pár ellenőrzés (ne osszunk nullával, stb…), de az egyszerűsége magáért beszél.
Egy fal kirajzolása:
Procedure DrawWall(wall : integer); var   DeltaYup,DeltaYdown,DeltaZ,width : longint;   Yup, Ydown, Z, X : longint;   U : longint;   x1,y1,z1,x2,y2,z2,y3,y4 : integer;   n : byte; begin   ProjectWall(wall,x1,y1,x2,y2,y3,y4);   z1:=walls[wall].z1;   z2:=walls[wall].z2;   n:=map[wall].texN;   width:=x2-x1;inc(width);   DeltaYup:=((y2-y1) shl 7) div width;   DeltaYdown:=((y3-y4) shl 7) div width;   DeltaZ:=((z2-z1) shl 7) div width;   Yup:=y1 shl 7;   Ydown:=y4 shl 7;   Z:=z1 shl 7;   for x:=x1 to x2 do   begin     U:=RayIntersectWall(X,wall);     WallTexColomn(X,Yup shr 7,Ydown shr 7,Z div 128,U,N);     Z:=Z+DeltaZ;     Yup:=Yup+DeltaYup;     Ydown:=Ydown+DeltaYdown;   end; end;

Az eljárás az elején meghívja a leképező eljárást, ami a már említett képleteket használja. Innen már csak interpoláció az egész. Kell egy lépésköz a fal tetejének (DeltaYup), az aljának (DeltaYdown), és a Z-nek (DeltaZ). A fal első oszlopától az utolsóig halad. Minden oszlopra kiszámolja az U-t a már ismert képlettel, majd meghívja az előző eljárást az oszlop kirajzolásához.

Mozgás a pályán.

A raycastinggal ellentétben nem mi mozgunk a pályán, hanem a pálya mozog körülöttünk. Erre azért van szükség, mert a leképezésnél a szemlélőtől való távolságot a Z koordináta adja meg. Ez azt jelenti, hogy ha például elfordulunk, akkor nem a felőlünk kibocsátott sugarakat forgatjuk el, hanem a pályát körülöttünk. Ha pálya forog, akkor a Z koordináták a valódi távolságot fogják tartalmazni. Más szavakkal a szemlélő koordinátarendszere szerinti értékeket kell kiszámolnunk. A pálya fog forogni és mozogni körülöttünk, miközben mi a koordinátarendszer ugyanazon pontján (0,0,-Zeye) állunk. Ezt a forgatást igen gyorsan és könnyedén el tudjuk érni, hiszen csak 2d-s forgatásra van szükségünk. Egy falat csak végpontjainak X és Z koordinátái határozzák meg. A forgatás középpontja mi vagyunk. A pálya eltolását a forgatás előtt egyetlen összeadással meg tudjuk oldani.

procedure
RotateMap; var
i : integer; begin   rot2dmatr(angle);   rot2dcenter(trunc(pX),Zeye+trunc(Pz));   for i:=1 to WALLNUM do  begin     rot2d(map[i].x1-trunc(pX),map[i].z1-trunc(pZ),walls[i].x1,walls[i].z1);     rot2d(map[i].x2-trunc(pX),map[i].z2-trunc(pZ),walls[i].x2,walls[i].z2);  end; end;
A "map" tartalmazza a térképet, ezt el kell tolnunk a szemlélő koordinátáival, majd megforgatjuk, ennek az eredménye kerül a "walls" tömbbe, innen dolgozik a többi rutin.
A példaprogram megvan VESA-s 640X480-ra, és a régi jó 320X200-ra. Erre azért volt szükség, mert a VESA-s nem fut mindenhol. Pl. WinNT. A program specialitása ezen kívül az, hogy át tudunk menni a falakon. Nem annyira egyszerű a fallal való ütközés levizsgálása, úgyhogy majd egy következő alkalommal.

A forráskód itt található meg.