(RealTime persze kicsit csúnyább…)

Induljunk el a kályhától.

A Voxel:

Ahogy a pixel egy terület megközelítése, a Voxel nem más, mint egy térfogat megközelítése. Egy pixelnek van adott szélessége és magassága, egy Voxelnek van adott szélessége, magassága, és mélysége. Néhol Volumized pixel-ként is említik. (ennek a rövidítése a Voxel)

A landscape Effekt:

Egy 3D-s táj (felület) realtime mozgatásáról szól. A 3D-s felületet mátrixba rendezett Voxelek segítségével közelítjük. A Voxeleknek ugyanaz a szélességük, hosszuk, csak a magasságuk más, ezért gyakorlatilag a mátrixban a magasságokat tároljuk. Ez a mátrix a Voxel Map. Matematikailag ez a Map a felületfüggvény diszkrét pontokban adott értékeit tárolja. Map[x,y]=magasság(x,y).

Itt merül fel a kérdés, hogy hány ponton nézzük meg és tároljuk el ezt a magasság értéket, magyarul mekkora legyen a Voxel Map. Egy igazán nagy felület finom modellezéséhez rengeteg memória kell, ez ennek a technikának az egyik hátránya. Többek között például ezért volt az első 4MB-t igénylő 386-os játék, a Commanche.

Mi igyekszünk megkönnyíteni az életünket, gyakorlati okok miatt az a legegyszerűbb, ha a mátrix 256X256-os, és a magasság értékeket byte-osan tároljuk. Ekkor real módban pont egy szegmens kell az eltárolásához, és a felület végtelenítése is megoldott.

A magasságon kívül a felület képét (textúráját is el kell tárolni). Ekkor lesz még egy 256X256-os Map-ünk. Persze az is megoldás, hogy magunk számoljuk ki a Voxelek színét és árnyékoljuk őket.

Ha két Map létezik, valahogy ki kell rajzolni a felületet. A trükk abban áll, hogy nem scanline-okat rajzolgatunk, hanem oszlopról oszlopra építjük fel a képet. A képernyő aljától haladva felfelé. Természetesen minden oszlop kirajzolásánál tudnunk kell, hol járunk éppen a Map-eken, és milyen lépésközzel jutunk el a következő Voxel-hez.

Tudjuk a kamera koordinátáit (X,Y), és a kamera elforgatási szögét (a). Az oszlop legalsó pixele a Voxel Map-en (U,V), és a következő pixel által takart Voxel-t lépésközökkel kapjuk meg (deltaU, deltaV). Hogy ne legyen bonyolult, a kamera koordinátáit is Voxel Map-re adjuk meg. Ekkor U=X, és V=Y.

Hogy a lépésközöket meg tudjuk határozni segítségül hívjuk a raycastingot. Amint a neve is mutatja, a nézőpontból (kamera pozícióból) kiinduló sugarakon alapszik. A látóterünkben valahány fokonként sugarakat bocsátunk ki, a sugarak által érintett Voxel-ek kerülnek ki a képernyőre.

Egy bonyolító ábra, felülnézetből a Voxel Map-en:

A kék a látótér, a piros vonalak a kibocsátott sugarak. Ahol minden összeér, az a kamera pozíciója. Az egész látótér (legyen b), ahogy a kamera is a szöggel el van forgatva. Ekkor az első kibocsátott sugár szöge a+270-b/2, az utolsóé a+270+b/2. Minden kibocsátott sugár egy-egy oszlop a képernyőn.

Minden oszlop kirajzolásánál más szöggel számolunk.

A programból 2048-as szögfüggvénytáblával:

for i := 0 to 319 do begin     {Calculate ray angle depending on view angle}     a := (Angle + 1536-160+i) and 2047;     {Cast the ray}     Ray (a,i); end;

Minden egyes oszlopra kiszámoljuk a kibocsátott sugár szögét, Majd meghívjuk az eljárást, ami kirajzolja az oszlopot. Átadjuk neki a sugár szögét, és hogy hányadik oszlopot rajzoljuk.

A szög a lépésköz kiválasztásakor kell majd. Mert ügye kézenfekvő, hogy:

DeltaU=Cos(Angle)
DeltaV=Sin(Angle)

Ha már tudjuk, mivel kell lépkedni, felmerül a kérdés, hogy vajon meddig. Mert ugye egy kibocsátott sugár hossza elvileg végtelen. Ezt tetszőlegesen megválaszthatjuk, csak azt jelenti, meddig akarunk ellátni a felületen. Minél messzebb, annál tovább adogatjuk a lépésközöket.

Ennek segítségével már tudjuk, hol járunk a Voxel Map-en. Ha megvan a magasság, perspektivikus leképezéssel megtudjuk, a képernyőbeli Y koordinátáját. Ha ez nagyobb, mint a legutóbbi, azt jelenti, hogy a Voxel takart, nézzük meg a következőt. Ha kisebb, akkor az előző Y-tól az újig ki kell rakni a Voxelhez tartozó színt.

Ez így leírva sem egyszerű hát, még ha meg kell programozni.

Nézzük:
 
 

procedure Ray(a,Row : integer); var UStep,VStep,U,V : word;     MinY,Y,H : integer;     Ub,Vb : byte;     Yr : word     i : integer; begin     MinY:=200;  {lent indulunk}     U:=Cx;V:=Cy;          {Get Camera Coords}     UStep:=CosT[a];       {lepeskozok}     VStep:=SinT[a];     for i:=1 to Depth do     begin        Ub:=Hi(U);Vb:=Hi(V);        H:=HeightMap[Vb,Ub];        Y:=100-(((H-CHeight) shl 5) div i);  {Perspektivikus lekepezes}        if Y<0 then Y:=0;        if Y<MinY then        begin for Yr:=Y to MinY-1 do Screen[Yr,Row]:=ColorMap[Vb,Ub]; {Draw Row} MinY:=Y;        end;        inc(U,UStep);        inc(V,VStep);     end; end;

A HeightMap tartalmazza a magasságokat, a ColorMap pedig a felület textúrája. A Depth egy konstans, ami azt határozza meg, milyen messze akarunk ellátni. A Voxel Map-en a kezdőpontot egyszerűen a kamera pontjának (Cx,Cy) választjuk. Minél nagyobb lesz az I ciklusváltozó értéke, annál messzebb vagyunk a nézőponttól, hiszen annál többször adtuk hozzá a lépésközöket. Az I tulajdonképpen a távolság (mélység), így a perspektivikus leképezésnél, melynek képlete:

Y`=M*Y/(Z+P)

Beírhatjuk a Z+P helyére az I-t.

Y`=M*Y/I

Mivel a leképezendő Y a Voxel magassága, amelyből kivonjuk a kameramagasságot, a képlet:

H= HeightMap[V,U]
Y`=M*(H-CHeight)/I

Figyelembe véve, hogy a képernyő 200 pixel magas (a horizont a felénél, 100-nál van), és az Y lefelé nő:

Y`=-M*(H-CHeight)/I+100

Ha ez az Y kisebb, mint az eddigi legkisebb (MinY), tehát magasabban van, akkor kirajzolhatjuk, ha nagyobb, akkor takart, így semmi dolgunk vele. A kirajzolásnál a legutolsó nem takart Y-tól (ami szintén MinY) húzunk egy függőleges vonalat a mostani Y-ig, a Voxel színével (ColorMap[Vb,Ub]). Ezután természetesen a MinY-t egyenlővé tesszük a mostani Y-al, és hozzáadjuk a Voxel koordinátáihoz (U-hoz és V-hez) a lépésközöket. A sugarat egészen addig húzzuk, amíg ellátunk, tehát amíg i<=Depth.

Ez a megoldás ugyan nincs túltrükközve, de elég ronda.

Egy pár kis dologgal fel lehet dobni. Vezessük be a kép távolságát a nézőponttól. A rutin elején annyiszor növeljük a kezdőpozíciót, amennyi a távolság:

for i:=1 to ViewDist do begin     inc(U,UStep);     inc(V,VStep); end;

Egy másik plussz a torzítási tábla bevezetése. Előfordulhat, hogy a túl közel lévő Voxel-ek egyszerűen "lefolynak" a monitorról. Egy távolsági torzítási tábla bevezetésével megoldható. Tulajdonképpen a perspektívát növeljük vele.

for i:=1 to Depth do begin     DisTab[i]:=100+(10000 div (i+100)); end;

A leképezés képlete ennek megfelelően:

Y`=DisTab[I]-M*(H-CHeight)/I

Azért még maradtak problémák:

A közeli kép nagyon kockás, mert a Voxel Map-et, ami amúgy is csak 256X256-os még fel is nagyítottuk. Megnövelhetjük a kép és a nézőpont távolságát, máshogy számolhatjuk a torzítási táblát, vagy be kell vezetni valami trükköt. Jó megoldás lehet, ha egy pixel színét nem csak a hozzá tartozó Voxel színe határozza meg, hanem a környező Voxel-ek is. Valamiféle átlagát kell venni ezeknek a színeknek, és ezt kirakni. Ehhez egy sötétedő /világosodó paletta kell. Persze használhatunk gouroud típusú árnyékolást, azaz lineáris interpolációval jutunk el a kezdőszíntől, a végsőig. Ha ezt a paletta nem engedi meg, még midig ott van a jó öreg texture Mapping.

Az első program ezzel a texture mapping-es dologgal számol. A textúrát és a Voxel Map-et "kölcsönvettem".

Felületek létrehozása:

Többféleképpen hozhatunk létre véletlenszerű felületeket. Erősebb idegzetűek akár a Bézier felületekkel is próbálkozhatnak, most egy másik módszert mutatok be.

Recursive Subdivison, avagy fractal.

Az eljárás gyakran használatos plasma vagy bármely más Map létrehozásánál is.

A létrehozni kívánt felület négy sarkát véletlenszerű magasságúnak választjuk. Az eljárásnak terület kezdőpontját (x,y), és a méretét (először az egész méretet) kell megadni. Minden élen kiszámoljuk a rajta lévő két sarok magasságának átlagát. Továbbá ki kell számolni egy a terület méretétől függő "zaj"-összetevőt, amit az átlaghoz hozzáadunk. Minden él közepére beírjuk az átlag+zaj-t. A terület közepén lévő elem értéke a négy sarok átlaga lesz, plusz a "zaj". Ezután a területet elnegyedeljük, és mindegyik negyedre meghívjuk az eljárást. (Rekurzió)

Egész addig folytatjuk az eljárást, amíg a terület oldalhossza nem kisebb, mint 2.

Ha egy 256X256-os Map-et akarunk feltölteni, amit később végteleníteni fogunk, akkor mind a négy sarok ugyanaz az elem lesz. 0,0 256,0 256,256 0,256

Mivel a mátrix elemei 0-tól 255-ig vannak számozva, a koordináták túlcsordulása után mind 0, 0-t fog jelenteni.

Pl:

procedure MakeMaps; var i : integer; begin    HeightMap[0,0]:=Random(200)+20;    SubDivison(HeightMap,0,0,256);      {Start SubDivison} end; procedure SubDivison(var Map : TMap;x,y,Size : integer); var c: byte;     HalfSize : integer;     Noise : byte; begin    if Size>=2 then    begin       HalfSize:=Size div 2;       Noise:=Size div 2;       c:=integer(Map[byte(x),byte(y)]+Map[byte(x+Size),byte(y)]) div 2;       c:=c+Noise;       Map[byte(x+HalfSize),byte(y)]:=c;       c:=integer(Map[byte(x),byte(y)]+Map[byte(x),byte(y+Size)]) div 2;       c:=c+Noise;       Map[byte(x),byte(y+HalfSize)]:=c;       c:=integer(Map[byte(x+Size),byte(y)]+Map[byte(x+Size),byte(y+Size)]) div 2;       c:=c+Noise;       Map[byte(x+Size),byte(y+HalfSize)]:=c;       c:=integer(Map[byte(x),byte(y+Size)]+Map[byte(x+Size),byte(y+Size)]) div 2;       c:=c+Noise;       Map[byte(x+HalfSize),byte(y+Size)]:=c;       c:=integer(Map[byte(x),byte(y)]+Map[byte(x+Size),byte(y)]+             Map[byte(x),byte(y+Size)]+Map[byte(x+Size),byte(y+Size)]) div 4;       c:=c+Noise;       Map[byte(x+HalfSize),byte(y+HalfSize)]:=c;       if HalfSize>1 then       begin          SubDivison(Map,x,y,HalfSize);          SubDivison(Map,x+HalfSize,y,HalfSize);          SubDivison(Map,x,y+HalfSize,HalfSize);          SubDivison(Map,x+HalfSize,y+HalfSize,HalfSize);       end;    end; end;

A második program ezzel a módszerrel hoz létre egy felületet.

A példaprogramok Free Pascal-ra születtek. Ettől még nem lenne reménytelen a lefordításuk Borland-ból, de mivel a képernyőkezelés DPMI-re lett megírva, és 65536 byte hosszú tömbökként (A Borland 65535-ig bírja) kezelem a Map-eket, nem lehet Borland alól fordítani.

Hát ennyi.

Forráskód és EXE:

forrkod1.zipforrkod2.zip