Ahogy azt mondani szokták, a téma az utcán hever. Megcsodálható a Quake-ben és társaiban, de egy magára valamit is adó 3D engine sem mellőzheti.

Miért és hol is van rá szükség?

Olyan poligonoknál van rá szükség, ahol az alkotó pontok mélysége nagyon eltér. Magyarul, ha valami be van döntve. Apró poligonoknál még nem zavaró, de minél nagyobb a poligon, annál látványosabb lesz az affine (linear) texture mapping hibája. Az affine texture mapping-nél a textúrát egyszerűen ráhúzzuk a poligonra. Ha négyszögről van szó, a dolog nem annyira ronda, hiszen a textúrát valahogy beletorzítjuk a helyére. Így csak kicsit torznak érezzük a dolgot. Ha háromszögre húzunk textúrát, a dolog sokkal vészesebb, főleg, ha kihasználjuk a konstans lépésközöket. Az eredmény nem csak egyszerűen torz lesz, hanem még a textúra is össze lesz törve. 

Tehát a hangzatos név annyit jelent, hogy most nem csak ráhúzzuk a textúrát a poligonra (háromszögre), hanem perspektivikusan torzítjuk is. A torzítás szó itt szándékos, hiszen nem arról van szó, hogy miden egyes pixelre pontos értékeket számítunk, hanem csak N pixelenként korrigálunk. 

Mivel háromszögekkel operálunk, a téma erősen támaszkodik a normál háromszöges texture mapping-re, ezért az abban kevésbé járatosak előbb annak ugorjanak neki.

Az elv:

Eddig csak az U és V értékeket interpoláltuk lineárisan a poligon szélein, és a scanline-okon belül. Ez ugye elvileg csak akkor helyes, ha nincsen Z eltérés, mert a leképezés miatt az U és a V változása csak ekkor lineáris a képernyőtérben (screen space). Ez túl tudományosan hangzik, de elég abba belegondolni, hogy ha egy része egy poligonnak közelebb van, ott szét kell húzni a textúrát, a legtávolabbi sarokban, meg összébb kell nyomni. Ld. a felső nagy kép. A kettő közötti átmenet pedig nyílván nem lineáris, hiszen a Z változása sem az.

Első hallásra ez sem kézenfekvő, hiszen mi az hogy egy sík lapnak a Z-je nem lineárisan változik. Itt természetesen a leképezés utáni koordinátákat vizsgáljuk, ahol az előbb említett nagyítás, illetve kicsinyítés miatt tényleg nem lesz lineáris.

Szóval, ami szerint interpolálunk:

1 / Z
U / Z
V / Z

Mert ezek változása lineáris a screen space (leképezés utáni tér)-ben.
Valahol láttam is egy 3 oldalas bizonyítást, hogy tényleg azok, de jobban járunk, ha csak egyszerűen elhisszük.

Ez annyit jelent, hogy a sarkokra kiszámoljuk az U/Z, V/Z és 1/Z értékeket, és ezeket interpoláljuk ugyanúgy, mint az affine texture mapping-nél az U-t meg a V-t. A dolgot nem könnyíti meg, hogy az 1/Z egynél kisebb érték, Az U/Z meg a V/Z pedig 255-nél kisebb (256X256-os textúránál). Vagy lebegőpontosan kell számolni, vagy nagyon eltalálni a fix pontos számolást.

Ebből a három interpolált értékből megkaphatjuk minden pixelre az U és V értékeket:

U = (U / Z) / (1 / Z)
V = (V / Z) / (1 / Z)

És már meg is kaptuk a textúrakoordinátákat.

Ezeket az osztásokat fix pontosan végezve nehéz célt érni, mivel túl durva értékeket kapunk. Rászorulunk a KoProcira. 

Egy Scanline:

For X:=startX to stopX do Begin   U:=UperZ/perZ;   V:=VperZ/perZ;   Screen[x,y]:=texture[U,V];   UperZ:=UperZ+UperZStep;   VperZ:=VperZ+VperZStep;   perZ:=perZ+perZstep End;

Ez ugye két lebegőpontos osztás pixelenként. Finoman fogalmazva ez baromi lassú. 
A két osztásból lehet egyet csinálni, ha vesszük az 1/Z reciprokát, ami Z, és ezzel szorzunk kétszer.

For X:=startX to stopX do Begin   Z=1/perZ;   U:=UperZ*Z;   V:=VperZ*Z;   Screen[x,y]:=texture[U,V];   UperZ:=UperZ+UperZStep;   VperZ:=VperZ+VperZStep;   perZ:=perZ+perZstep End;

Mivel a szorzás gyorsabb művelet, mint az osztás, egy kis időt nyerünk vele, de túl nagy változás nem történt.

Ezért mondjuk minden N pixelre számoljuk ki csak a pontos értékeket, a többit közelítjük. Az N a felbontástól a textúra finomságától, és főleg a proci sebességétől függ. Úgy kell megválasztani, hogy megfelelő framerate-et kapjunk, és még ne látsszon a csalás. Szóval a káposzta is elfogyjon, meg a kecske se lakjon jól.

Általában kettő hatványa. 8,16, esetleg 32. Esetünkben 320X200-as felbontásnál legyen 8.

Ekkor a sebesség megnyolcszorozódik, hiszen minden nyolc pixelre jut egy lebegőpontos osztás. Ez már szinte elviselhető sebességet ad. A két osztás között pedig lineárisan interpolálunk

Sl:=Xend-Xstart; Z:=1/perZ U0:=UperZ*Z V0:=VperZ*Z While sl>0 doBegin   If sl-8>0 then l:=8 else l:=sl   UperZ:=UperZ+UperZStep;   VperZ:=VperZ+VperZStep;   perZ:=perZ+perZstep   Z=1/perZ;   U1:=UperZ*Z;   V1:=VperZ*Z;   DU:=(V1-V0) shr 3;  {lepeskozok}   DV:=(V1-V0) shr 3;   While l>0 do  Begin     Screen[x,y]:=texture[U0,V0];     U0:=U0+DU;     V0:=V0+DV;     Dec(l);   End;   U0:=U1;   V0:=V1   Dec(sl,8);  {8 pixelt raktunk ki} End;

Nem tűnik valami egyszerűnek, pedig tényleg nem az.

Tehát az elején kiszámoljuk a kezdő U-t és V-t, a cikluson belül pedig mindig a végpontot. A ciklus végén az addigi végpont lesz a kezdő. A kettő között pedig lineárisan interpolálunk. Az U és V lépésközök kiszámítása nagyon gyors és egyszerű, hiszen a kezdő és végpont különbségét 8-al kell osztani, tehát 3-al jobbra léptetni. A ciklusban arra is figyelni kell, hogy a scanline hossza valószínűleg nem pont 8-al osztható. Tehát a scanline-on belül már működik a dolog.

A poligon szélein ugyanaz a helyzet, mint az affine texture mapping-nél. Csak a baloldalon kell végigléptetnünk három interpolált értéket, mivel a háromszög belsejében konstans lépésközöket használhatunk.

Mivel háromszögről van szó, az alapelv ugyanaz. A háromszög 3 pontját Y szerint sorba kell rendezni, ki kell számolni a leghosszabb scanline-t, és eldönteni, hogy balos, vagy jobbos. A scanline hosszának segítségével pedig meg kell határozni a konstans lépésközöket.

T=( y2 - y1) / (y3 - y1)
Width=x1 + T * (x3 - x1) - x2

A lépésközök az affine texture mapping-nél:

UStep=(U1 + T*(U3 - U1) - U2) / width
VStep=(V1 + T*(V3 - V1) - V2) / width

Lépésközök a Perspecitve corected-nél ugyanezek, csak az U-k helyére U/Z-t, a V-k :helyére V/Z-t kell írni. 

Upz1 = U1/Z1
Upz2 = U2/Z2
Upz3 = U3/Z3
Vpz1 = V1/Z1
Vpz2 = V2/Z2
Vpz3 = V3/Z3
UpZStep = (UpZ1 + T*(UpZ3 - UpZ1) - UpZ2) / width
VpZStep = (VpZ1 + T*(VpZ3 - VpZ1) - VpZ2) / width

Mivel a kirajzolás majdnem ugyanaz, mint az affine texture mapping-nél volt innen már nincs nehéz dolgunk. Nézzük a kirajzolás elvét.

Texturetriangle proc p1,p2,p3 : pontra mutato pointerek:

Var
  T : arányossági tényező
  Width : leghosszab scanline hossza
  UpZstep : U/Z lépésköz a scanline-on belül
  VpZstep : V/Z lépésköz a scanline-on belül
  PZstep : 1/Z lépésköz
  UpZL,VpzL,PzL,XL : bal oldali értékek
  XR :  jobb oldali érték
  UpzLstep, VpzLstep,PzLstep, Xlstep,Xrstep : a lépésközök a scanline-ok között.
  Y,yend : a kirajzolo loop-nak kell, hogy mettől meddig menjen
{sorbarendezés}
If  p1.y>p2.y then csere(p1,p2) 
If  p1.y>p3.y then csere(p1,p3) 
If  p2.y>p3.y then csere(p2,p3) 

T=(p2.y-p1.y)/(p3.y-p1.y)
Width=p1.x+ T*(p3.x-p1.x)-p2.x
Upz1=p1.U/p1.Z
Upz2=p2.U/p2.Z
Upz3=p3.U/p3.Z
Vpz1=p1.V/p1.Z
Vpz2=p2.V/p2.Z
Vpz3=p3.V/p3.Z
pz1=1/p1.Z
pz2=1/p2.Z
pz3=1/p3.Z

{konstans lépésközök}
UpZStep=(UpZ1+T*(UpZ3-UpZ1)-UpZ2)/width
VpZStep=(VpZ1+T*(VpZ3-VpZ1)-VpZ2)/width
{kezdőértékek beállítása}
XL=p1.x
XR=p1.x
VpZL=VpZ1
UpZL=UpZ1
pZL=pZ1
Y=p1.y
Yend=p2.y
If width<0 then goto jobbos
Balos:
{a háromszög balos, tehát a jobb oldalon végig ugyanaz az x lépésköz a képernyőn 1. és 3. Pont között}
XRStep=(p3.x-p1.x)/(p3.y-p1.y)
{A bal oldalon először az 1. ponttól megyünk a 2. Felé}
XLStep=(p2.x-p1.x)/(p2.y-p1.y)
UpZLStep=(UpZ2-UpZ1)/(p2.y-p1.y)
VpZLStep=(VpZ2-VpZ1)/(p2.y-p1.y)
pZLStep=(pZ2-pZ1)/(p2.y-p1.y)

{Az ertekek megvannak, mar csak ki kell rajzolni}
Call loop

{a háromszög egyik fele megvan, most vagyunk a töréspontnál. A bal oldalon új vonalon kell tovább menni. 2. Ponttól a 3. ig}

XL=p2.x
UpZL=UpZ2
VpZL=VpZ2
 

{lepeskozok}
UpZLStep=(UpZ3-UpZ2)/(p3.y-p2.y)
VpZLStep=(VpZ3-VpZ2)/(p3.y-p2.y)
pZLStep=(pZ3-pZ2)/(p3.y-p2.y)
XLStep=(p3.x-p2.x)/(p3.x-p2.x)
Yend=p3.y

Call loop

Goto ki

{a  jobbos rész}
Jobbos:

{a bal oldalon vegig jók lesznek ezek a lépésközök 1. És 3. Pont között}

UpZLStep=(UpZ3-UpZ1)/(p3.y-p1.y)
VpZLStep=(VpZ3-VpZ1)/(p3.y-p1.y)
pZLStep=(pZ3-pZ1)/(p3.y-p1.y)
XLStep=(p3.x-p1.x)/(p3.x-p1.x)

{a jobb oldalon ez p2.y-ig jó :}
XRStep=(p2.x-p1.x)/(p2.y-p1.y)

Call loop

{csak a jobb oldali értékeket kell újraszámolni, azokból meg csak egy van}
XRStep=(p3.x-p2.x)/(p3.y-p2.y)
Yend=p3.y

Call loop

Goto ki

{A tényleges kirajzolás és lépkedés}
 
Loop:
  UpZ=UpZL
  VpZ=VpZL
  PZ=pZL
{egy scanline kirajzolása}
Sl:=XR-XLt;
Z:=1/pZ
U0:=UpZ*Z
V0:=VpZ*Z
X:=Xl;
While sl>0 do
Begin
  If sl-8>0 then l:=8 else l:=sl
  UpZ:=UpZ+UpZStep;
  VpZ:=VpZ+VpZStep;
  pZ:=pZ+pZstep
  Z=1/pZ;
  U1:=UpZ*Z;
  V1:=VpZ*Z;
  DU:=(V1-V0) shr 3;  {lepeskozok}
  DV:=(V1-V0) shr 3;
  While l>0 do
  Begin
    Screen[x,y]:=texture[U0,V0];
    U0:=U0+DU;
    V0:=V0+DV;
    Dec(l);
    Inc(x);
  End;
  U0:=U1;
  V0:=V1
  Sl:=sl-8;  {8 pixelt raktunk ki}
  x:=X+8;
End;
{a háromszög szélén külön lépésközök}
UpZL+=UpZLStep
VpZL+=VpZLStep
PZL+=pZLstep
XL+=XLStep
XR+=XRStep
Y++

If  Y<=Yend then goto loop

Ret

Ki:
Return
Texturetriangle endproc

Na a valóságban azért nem ilyen egyszerű a helyzet. Azt is figyelni kell, hogy a két felső pont nincs-e egy magasságban, vagy a width nem nulla-e, esetleg y1=y3 stb.. Azt is figyelni kell, nem írunk-e a képernyőn kívülre.

A progi Free Pascal-ban sikerült.

Senki ne próbálja meg Borland Pascal-ból lefordítani, nem fog menni. A képernyőkezelés eleve protected módra van írva. Akinek esetleg még nincs meg, a Pc-X 30. (1999. márciusi) CD-jén rajta van.

Na ennyi.