Strona wykorzystuje ciasteczka by usprawnić komfort z jej korzystania. Korzystając ze strony akceptujesz naszą Politykę Ciasteczek. X

Podstawy silnika kafelkowego część 2: odnajdywanie ścieżki [zaawansowany]

W poprzedniej części omówiłem pokrótce przekształcenie izometryczne i jak uzyskać je w najłatwiejszy sposób. Tym razem, zgodnie z obietnicą zajmiemy się znajdywaniem ścieżki w silniku kafelkowym wykorzystując jak najprostszą metodę (aczkolwiek nie najszybszą). Nie wiem czy algorytm który tutaj przybliżę ma jakąś konkretną nazwę, ale na pewno jest jednym z najbardziej bezpośrednich spośród wszystkich dostępnych.
Dla wszystkich tych których nie interesują szczegóły przygotowałem klasę z całym algorytmem oraz dodatkowo plikiem FLA pokazującym jak go wykorzystać: PathFind.zip
W działaniu wygląda tak:
(Wybierz punkt startowy i końcowy naciskając na puste pola. Spacja by stworzyć nową mapę)

Działanie algorytmu jest całkiem proste i dzieli się na dwie fazy:
1. Poszukiwanie celu. Z punktu startowego krok po kroku sprawdzamy co raz większy obszar w poszukiwaniu docelowej pozycji, w międzyczasie zapisując do siatki aktualny krok iteracji. Numery iteracji będą nam później potrzebne by odtworzyć ścieżkę.
Urywek kodu z załączonego przykładu:

// ...
var vWrite:Vector.<Vector.<int>> = recreateTiles(tiles);
vWrite[startx][starty] = 1;
vWrite[endx][endy] = -1;
var vCheck:Vector.<Point>;
var vRead.<Point> = new Vector.<Point>();
			
var tile:Point;
var nStep:int = 2;
vRead.push( new Point(startx,starty) );
while(vRead.length != 0) {
				
	vCheck = vRead;
	vRead = new Vector.<Point>();
	for each(tile in vCheck) if(lookupTile(tile.x, tile.y, nStep, vRead,vWrite,vTile)) {
		return retracePath(tile.x,tile.y,nStep,vWrite);
	}
	nStep ++;
}
return null;
// ...
Funkcja lookupTile poszukuje kolejnych kafelek do iteracji. Jeśli któryś z 4 kierunków zawiera docelowy punkt, pętla zostaje zakończona i zwrócona zostaje znaleziona ścieżka. W przeciwnym wypadku w vWrite zapisywany jest aktualny krok, a do vRead wpisywana jest pozycja kolejnej kafelki do sprawdzenia.
2. Odtwarzanie ścieżki. Gdy już odnajdziemy punkt docelowy wystarczy krok po kroku zacząć wracać po zapisanych liczbach w vWrite by uzyskać naszą ścieżkę. Powiedzmy że doszliśmy do docelowej pozycji w 20 iteracji - teraz musimy zacząć się wracać zaczynając od znalezienia najbliższej kafelki z numerem 19, potem 18, 17, itd. Aż do 1 który będzie punktem startowym (0 powinny mieć kafelki które nigdy nie zostały sprawdzone).

Na koniec jeszcze tylko mała uwaga: algorytm można przyspieszyć wykonując go jednocześnie na punkcie startowym i końcowym, przez co ścieżka będzie odnaleziona gdy oba spotkają się w połowie drogi. Taki też sposób został użyty w dołączonej klasie.
Następnym razem przyjrzymy się idei "pola widzenia" w silnikach kafelkowych.

Podstawy silnika kafelkowego część 1: widok izometryczny [podstawowy]

W tym trzyczęściowym artykule postaram się przybliżyć kilka najbardziej podstawowych zagadnień z silników opartych o kratki - czy też kafelki (tile-based). Owe silniki istnieją już od bardzo dawna i wydawać by się mogło, że już wszystko w tej sprawie zostało powiedziane, jednak wciąż wiele osób ma z nimi problemy, nierzadko komplikując trywialne zagadnienia.
Jednym z takich zagadnień jest widok izometryczny (czasem nazywanym "widokiem po skosie") będący specyficznym rodzajem transformacji 3D bez dodatku perspektywy, często wykorzystywanym w starszych grach strategicznych. Chociaż definicja mówi o transformacji 3D takowa wcale nie jest wymagana i można ją zastąpić czymś o wiele prostszym, co niestety nie wszyscy próbują zrobić i dlatego w Internecie można całkiem sporo podobnych artykułów, które jednak najpierw tłumaczą podstawy 3D - nie potrzebnie.
Całość można sprowadzić do pojedynczej funkcji:
function getTile(nTargetX:Number,nTargetY:Number,nWidth:Number,nHeight:Number):Point {
	var nScale:Number = nWidth/nHeight;
	var nTransY:Number = (nScale*nTargetY-nTargetX)/2;
	var nTransX:Number = nTargetX+nTransY;
	var nTmpY:Number = Math.round( nTransY/(nHeight*nScale) );
	var nTmpX:Number = Math.round( nTransX/nWidth );	
	var nTileX:Number = (nTmpX-nTmpY)*nWidth;
	var nTileY:Number = (nTmpX+nTmpY)*nHeight;
	return new Point(nTileX,nTileY);
}
W efekcie dostając:
(Zielone kółko to Point zwrócony przez getTile)
Gotowy przykład: tile_example.zip

I to w zasadzie wszystko, dalej tylko jeszcze poświecę parę słów na wyjaśnienie działania funkcji getTile krok po kroku:
1.
var nScale:Number = nWidth/nHeight;
Skala będzie nam potrzebna przy transformacji by wyrównać wysokość do szerokości (ponieważ algorytm działa tylko w skali 1:1).
2.
var nTransY:Number = (nScale*nTargetY-nTargetX)/2;
var nTransX:Number = nTargetX+nTransY;

W tym miejscu następuje transformacja punktu na ekranie, na punkt w rzucie izometrycznym.
Najczęściej mówiąc o rzucie izometrycznym mamy na myśli przestrzeń obróconą o kąt 45 stopni, w taki sposób że ruchowi w prawo nie będzie odpowiadać samo +X, ale raczej pewna wartość X plus częściowo również i Y. Najlepiej zobrazować to na przykładzie:

Jak widać przesunięcie kursora w prawo, przesuwa punkt transformacji w prawo tylko po części, ponieważ dochodzi do tego jeszcze przesunięcie do góry. Uzbrojeni w ta wiedzę, możemy spróbować napisać generalny wzór - skoro przesunięcie w prawy (+X) daje nam również trochę przesunięcia do góry (-Y) można powiedzieć, że Y maleje wraz ze wzrostem X, czyli innymi słowy Y = -nTargetX. Jednak należy pamiętać, że nie jest to transformacja x=>y (inaczej mieli byśmy do czynienia z obrotem o 90 stopni), stąd też do równania należy dołączyć również dzielnie przez 2 (bo 90 stopni podzielone przez 2 to nasze 45 stopni... w wielkim uproszczeniu). Teraz gdy już znamy Y, obliczenie X jest trywialne i wystarczy, że odejmiemy wartość Y od docelowego X (by pokonany dystans był identyczny z tym przed transformacją), a ponieważ Y już jest ujemne wstawiamy znak dodawania Y = nTargetX+nTransY;. Przy okazji, plus i minus można w tych równaniach zamienić miejscami by uzyskać transformację dla obrotu -45 stopni.
3.
var nTmpY:Number = Math.round( nTransY/(nHeight*nScale) );
var nTmpX:Number = Math.round( nTransX/nWidth );
var nTileX:Number = (nTmpX-nTmpY)*nWidth;
var nTileY:Number = (nTmpX+nTmpY)*nHeight;

Tutaj zmieniamy wybrany punkt na podpadający pod niego kafelek, poprzez "skurczenie" całej przestrzeni o wysokość i szerokość kratki, następnie usunięcie informacji po przecinku i przywrócenia całości do "normalnych rozmiarów". Ponieważ uwielbiam przykłady o to następny: jeśli szerokość kafelki numer #0 wynosi 20 pikseli, to wszystko pomiędzy 0 a 20 pikseli będzie podpadać właśnie pod nią, tak więc: jak sprawdzić do której kafelki należy siódma piksela? Bierzemy owe 7, dzielimy przez 20 i dostajemy 0.35. Z rezultatu usuwamy wszystko po przecinku i dostajemy naszą kratkę #0. Jeszcze spróbujmy tego samego dla pikseli 27; 27/20 = 1.35; usuwamy liczby po przecinku i dostajemy #1, co się zgadza bo dwudziesta siódma piksela nie należy do kratki #0.

Uff, starczy na dzisiaj. Za dwa tygodnie napisze jak w najłatwiejszy sposób zaimplementować znajdywanie ścieżki w silnikach kafelkowych.

Silnik 3D dla początkujących III [zaawansowany]

W poprzednich częściach tego poradnika poświęconemu tworzeniu bardzo minimalistycznego silnika 3D wspomniałem jak stworzyć iluzję perspektywy oraz jak płynnie poruszać kamerą. Tym razem zajmiemy się obrotem widoku. Będzie to już ostatni rozdział tego poradnika gdyż dalsze zagłębianie się w temacie wymagać będzie znajomości bardziej zaawansowanych zagadnień, a wtedy już po prostu lepiej wziąć się za "prawdziwy" silnik (jak np. Alternativa3D lub Away3D). Do tego najnowsza wersja Flash'a z numerem 11 rozstała rozszerzona o akcelerację 3D co oznacza, że nic nie stoi na przeszkodzie by napisać w ActionScript gry na miarę naszych czasów jak Crysis albo Gears of War (szczególnie, że Adobe zaprezentowało Unreal Engine 3 działający we Flash'u).
No ale na to przyjdzie jeszcze czas, na razie zajmijmy się tą rotacją.

1. Za nim przejdę do konkretów do klasy utworzonej w poprzednich rozdziałach dopiszemy dwie stałe oraz jedną zmienną:

const SPEED:Number = 20;
const ROTATION:Number = Math.PI/16;
var a:Number = 0;
SPEED będzie prędkością poruszania się kamery, a ROTATION prędkością obrotu. Zmienna a posłuży do przechowywania aktualnego kąta obrotu.

2. Pamiętasz gdzie znajduje się kod odpowiedzialny za transformacje informacji 3D na 2D? Zmodyfikujemy go nieco by uwzględniał rotacje widoku:

var nx:Number;
var nz:Number;
var rx:Number;
var rz:Number;
var bx2:Array = new Array(bx.length);
var by2:Array = new Array(by.length);
var i:Number = 0;
while(i<bz.length) {
	nx = cx-bx[i];
	nz = cz-bz[i];
	rx = cx+( nx*Math.cos(-a) )-( nz*Math.sin(-a) );
	rz = cz+( nx*Math.sin(-a) )+( nz*Math.cos(-a) );
	bx2[i] = vw+( ((cx-rx) / (cz-rz) )*vw);
	by2[i] = vh+( ((cy-by[i]) / (cz-rz) )*vh);
	++i;
}
Właściwie różnica nie jest duża, dodałem jedynie parę zmiennych dla przejrzystości oraz wzór na obrót (wzdłuż osi Y, czyli tak samo jak w każdej grze FPS). Jeśli matematyka nie jest ci obca prawdopodobnie nie znajdziesz tutaj nic szokującego. W skrócie z każdego punktu wyliczany jest wektor przesunięcia (na przykładzie x: nx = cx-bx[i];), który następnie poddawany jest obrotowi (( nx*Math.cos(-a) )-( nz*Math.sin(-a) );) i ustawiany na nową pozycję ( rx = cx+...). Nowy punkt trafia do znanej nam już transformacji z 3D do 2D.

3. Teraz pozostaje nam jeszcze tylko zmodyfikować sposób poruszania się kamery tak aby korzystała ona z rotacji. W tym celu wrócimy do kodu odpowiedzialnego za przetwarzanie informacji o wciśniętych klawiszach na komendy w aplikacji, czyli switch(key). Przy okazji to również dobry moment by go trochę uprościć - stary kod w całości zastąp tym:

switch(key&0x0000ff) {
case 0x00000f:
		cz += Math.cos(-a)*SPEED;
		cx += Math.sin(-a)*SPEED;
	break;
	case 0x0000f0:
		cz -= Math.cos(-a)*SPEED;
		cx -= Math.sin(-a)*SPEED;
		break;
}
switch(key&0x00ff00) {
	case 0x000f00:
	a += ROTATION;
		break;
	case 0x00f000:
		a -= ROTATION;
		break;
}
W poprzedniej wersji switch sprawdzał wszystkie możliwe kombinacje zmiennej key, podczas gdy tutaj pierwszy switch wyciąga z key stan jedynie klawiszy góra i dół (0x000f0f&0x0000ff da w rezultacie 0x00000f a więc informacja o wciśniętym klawiszu lewo, 0x000f00, zostanie pominięta), a drugi klawiszy lewo i prawo.
Okej, więc teraz lepiej rozróżniamy wciśnięte klawisze, ale co właściwie się tutaj dzieje? Zaczynając od końca: klawisze lewo i prawo odpowiedzialne są za zmianę kąta obrotu, podczas gdy góra i dół za poruszanie się do przodu i do tyłu. Oczywiście teraz gdy mamy do czynienia z rotacją, definicja "do przodu" cały czas się zmienia stąd też potrzeba wykorzystania funkcji cos i sin które w tym przypadku wyliczają dla nas wektor przesunięcia zależny od aktualnego kąta.

4. Czas sprawdzić czy wszystko działa. Efekt końcowy powinien wyglądać mniej więcej tak :
(Poruszanie się na strzałkach)


A cała klasa do ściągnięcia jest tutaj: box3d_rot.zip

Silnik 3D dla początkujących II [zaawansowany]

W pierwszej części poradnika opisałem jak stworzyć podstawy silnika 3D który będzie w stanie wygenerować trójwymiarowy sześcian, jednak bez możliwości poruszania się taki statyczny obraz zapewne nie był zbyt imponujący. Dlatego w tej część zajmiemy się mechanizmami kontroli kamery.
Zacznijmy od dodania do naszego konstruktora box3d dwóch zdarzeń związanych z klawiaturę:
stage.addEventListener(KeyboardEvent.KEY_DOWN,eventKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP,eventKeyUp);
(Nie zapomnij zaimportować flash.events.KeyboardEvent!)
Nasłuchiwacze zostały powiązane z wartością stage ponieważ w przypadku innych obiektów trzeba byłoby najpierw ustawić na nich "focus" co na razie jest nam kompletnie nie przydatne i stage w zupełności wystarczy. W każdym razie, musimy jeszcze stworzyć funkcje obsługujące owe zdarzenia:
private function eventKeyDown(e:KeyboardEvent):void {

}
private function eventKeyUp(e:KeyboardEvent):void {

}
W tym momencie sugerował bym dopisać do którejś z nich linijkę trace(e.keyCode); by upewnić się, że Flash poprawnie przechwytuje naciśnięte klawisze.
W następny kroku zajmiemy się już właściwym wyłapywaniem klawiszy i tłumaczeniem ich na odpowiedni ruch kamerą, i choć całość można byłoby załatwić pisząc po prostu:
if(e.keyCode == Keyboard.LEFT) cx -= 20;
else if(e.keyCode == Keyboard.RIGHT) cx += 20;
Takie rozwiązanie jest niestety nie tylko ograniczające ale w dodatku niezbyt płynne. Dlatego w naszym silniku zastosujemy nieco bardziej zaawansowaną metodę opartą o operacje bitowe - tylko za nim ją opisze, dodaj do klasy nową zmienną: var key:Number = 0; której używać będziemy w następujący sposób:
private function eventKeyDown(e:KeyboardEvent):void {
	switch(e.keyCode) {
		case Keyboard.UP:
			key = key | 0x00000f;
			break;
		case Keyboard.DOWN:
			key = key | 0x0000f0;
			break;
		case Keyboard.LEFT:
			key = key | 0x000f00;
			break;
		case Keyboard.RIGHT:
			key = key | 0x00f000;
			break;
	}
}
(Nie zapomnij importować flash.ui.Keyboard!)
Okej, musze przyznać, że na pierwszy rzut oka wygląda to trochę bez sensu (szczególnie dla osób które nie miały styczności z operacjami bitowymi lub liczbami szesnastkowymi), ale za parę chwil wszystko stanie się jasne. Owy kod przypisuje pewne wartości do zmiennej key zależnie od wciśniętego klawisza - wciskając klawisze strzałek "do góry" i "w lewo" key będzie mieć wartość "0x000f0f". Rezultat ten otrzymujemy poprzez operacje bitowe na liczbach szesnastkowych, a więc dla przykładu jeśli weźmiemy liczby "0x000f00" oraz "0x00000f" i wykonamy na nich operację | (OR) dostaniemy "0x000f0f". Ta kreska | jest znana jako bitowe "lub" i przypomina w pewnych sytuacjach dodawanie - 100|1 da wynik 101 (więcej informacji znajdziesz w dokumentacji ActionScript'a). W każdym razie, dla nas ważne jest tylko to, że key będzie przypominał trochę jakby listwę ze światełkami gdzie 0 = zgaszona lampka (klawisz nie jest wciśnięty) a f = zapalona lampka (klawisz jest wciśnięty). 0x00ffff = wszystkie klawisze wciśnięte, 0x00f00f0 = tylko klawisz w dół i w prawo, 0x000000 = żaden klawisz nie jest wciśnięty itd.
Jednak sam eventKeyDown służy nam jedynie do zapalania światełek. Do pełnego obrazu brakuje nam jeszcze przeciwnej funkcji czyli:
private function eventKeyUp(e:KeyboardEvent):void {
	switch(e.keyCode) {
		case Keyboard.UP:
			key = key ^ 0x00000f;
			break;
		case Keyboard.DOWN:
			key = key ^ 0x0000f0;
			break;
		case Keyboard.LEFT:
			key = key ^ 0x000f00;
			break;
		case Keyboard.RIGHT:
			key = key ^ 0x00f000;
			break;
	}
}
W tym wypadku bitowe ^ (XOR) działa trochę jak odejmowanie, a więc operacja " 0x000f0f ^ 0x00000f" zwróci "0x000f00".
Najgorsze za nami, teraz już mamy w pełni funkcjonalną zmienną key która sygnalizuje nam jakie klawisze są w użyciu i pozostaje nam jedynie przetłumaczyć je na ruch kamery. W tym celu przejdź do funkcji render (utworzyliśmy ją w poprzedniej część poradnika) i przed linijką graphics.clear(); dopisz:
switch(key) {
	case 0x00000f: // ^
		cz += 20;
	break;
	case 0x0000f0: // v
		cz -= 20;
		break;
	case 0x000f00: // <
		cx -= 20;
		break;
	case 0x00f000: // >
		cx += 20;
		break;
}
To już prawie koniec. Skompiluj aplikacje i sprawdź czy wszystko działa poprawnie. Jeśli wcześniej próbowałeś poruszać kamerą bezpośrednio z poziomu eventKeyDown i eventKeyUp na pewno zauważyłeś teraz poprawę (a jeśli nie, proponuję powrót do owego rozwiązania i porównanie z aktualnym).
W każdym razie, została jeszcze jedna mała rzecz - poruszanie się po skosie. Używanie dwóch klawiszy naraz ustala wartość key na liczbę którą nasz switch(key) nie obsługuje, dlatego na koniec musimy go jeszcze rozbudować o cztery dodatkowe warunki:
	case 0x000f0f:
		cz += 20;
		cx -= 20;
		break;
	case 0x00f00f:
		cz += 20;
		cx += 20;
		break;
	case 0x000ff0:
		cz -= 20;
		cx -= 20;
		break;
	case 0x00f0f0:
		cz -= 20;
		cx += 20;
		break;
I tyle. W razie czego całą klasę można pobrać stąd: box3d_2.zip
A sama aplikacja zobaczyć tutaj: box3d.swf

Silnik 3D dla początkujących I [podstawowy]

Jeśli ktokolwiek miał ochotę zbudować własny silnik 3D we Flash'u (ale nie tylko) nie wiedząc dokładnie czego się spodziewać, przy szukaniu informacji na pewno został przywitany mnóstwem matematyki i zaawansowanych metod programowania. Może to być trochę zniechęcające pod czas gdy prawda jest tak, że nie potrzebujesz tego wszystkiego by stworzyć coś podstawowego jak np. aplikacje wyświetlającą trójwymiarową kostkę.
Dlatego w tym poście napisze jak stworzyć silnik 3D dla całkowitych nowicjuszy w temacie (który wyświetli owa kostkę).

Całość będzie zawierać się tylko w jednej klasie - najlepiej niech będzie to klasa dokumentu. W razie czego "Klasę Dokumentu" można stworzyć na zakładce właściwości.

(Po wpisaniu nazwy klasy należy kliknąć ikonkę ołówka).
Po stworzeniu nowej klasy pierwszym krokiem będzie przygotowanie zmiennych których będziemy używać w silniku. Zacznijmy od pozycji kamery:

var cx:Number = 0;
var cy:Number = 0;
var cz:Number = 0;
Na razie nic szczególnego - od pozycji kamery zależy co będziemy widzieć na ekranie. Może w tym momencie wyjaśnię jak będzie wyglądał ruch na osiach (gdyż nie musimy się trzymać podstaw matematyki i to od nas zależy gdzie będzie góra a gdzie dół):
cx to ruch w lewo(-) albo prawo(+).
cy to ruch w górę(-) albo w dół(+).
cz to ruch do tyłu/"od ekranu"(-) albo do przodu/"do ekranu"(+).
Okej, teraz potrzebujemy punktu do którego będzie się zbiegać obraz:
var vx:Number = stage.stageWidth/2;
var vy:Number = stage.stageHeight/2;
Punkt zbiegu zwykle znajduje się na środku ekranu, tak więc najprościej po prostu podzielić szerokość i wysokość aplikacji na pół. Aczkolwiek nic nie stoi na przeszkodzie by wybrać własną pozycję (tylko, że będzie się to wiązać ze zmianą metody transformacji o której wspomnę później). Przy okazji, na ekranie punkt 0,0 znajduje się w lewym górnym rogu, a wszystko na prawo lub dół od niego to liczby dodatnie.
Ostatni zestaw zmiennych to sześcian który będziemy wyświetlać w 3D:
var bx:Array = [-50, 50, 50, -50, -50, 50, 50, -50];
var by:Array = [-50, -50, 50, 50, -50, -50, 50, 50];
var bz:Array = [200, 200, 200, 200, 300, 300, 300, 300];
Tutaj definiujemy 3 tablice z punktami opisującymi pozycje kątów naszego trójwymiarowego pudła. Na przykład biorąc wartości bx[0],by[0],by[0] otrzymamy pozycje lewego górnego kąta na boku znajdującym się najbliżej kamery.
Teraz gdy już mamy wszystkie potrzebne parametry możemy przystąpić do stworzenia funkcji generującej obraz 3D. Najlepiej jest ją podłączyć do zdarzenia ENTER_FRAME by animacja była płynna, dlatego w konstruktorze swojej klasy dopisz:
addEventListener(Event.ENTER_FRAME,render);
Nie zapomnij zaimportować flash.events.Event! Jak widzisz ja swoją funkcję nazwałem "render" i na początek wygląda ona tak:
private function render(e:Event):void {
 graphics.clear();
}
Standardowa definicja funkcji. Linijka graphics.clear(); będzie nam potrzebna gdyż proces renderowania zawsze polega na tym samym - przy wejściu do nowej klatki animacji usuwamy starą grafikę i wstawiamy nową (wstawianiem zajmiemy się za chwilę). W każdym razie, czas przejść do najważniejszej części aplikacji, czyli przetwarzania informacji 3D na 2D (ponieważ jak na razie ekrany monitorów potrafią wyświetlić jedynie dwuwymiarowy obraz):
var bx2:Array = new Array(bx.length);
var by2:Array = new Array(by.length);
var i:Number = 0;
while (i<bx.length) {
	bx2[i] = vx+(((cx-bx[i])/(cz-bz[i]))*vx);
	by2[i] = vy+(((cy-by[i])/(cz-bz[i]))*vy);
	++i;
}
Tak więc po kolei:
var bx2:Array = new Array(bx.length);
var by2:Array = new Array(by.length);
Te dwie tablice będą przechowywać wszystkie punkty po transformacji do 2D.
Dalej, w pętli, mamy:
bx2[i] = vx+(((cx-bx[i])/(cz-bz[i]))*vx);
by2[i] = vy+(((cy-by[i])/(cz-bz[i]))*vy);
Czyli kod odpowiedzialny za ową transformację. Powiedzmy sobie szczerze, nikt nie musi rozumieć tych równań by z nich korzystać dlatego nic nie stoi na przeszkodzie by pominąć poniższe wyjaśnienie i ewentualnie powrócić do niego w przyszłości:
cx-bx[i] - odejmujemy pozycję punktu od kamery by otrzymać wektor przesunięcia. Jest to standardowe działanie przy większości transformacji, ponieważ nie obchodzi nas gdzie znajduje się punkt, tylko w jakiej jest odległości od kamery - taką relatywną informacje jest łatwiej przenieść między przestrzeniami.
cz-bz[i] - tutaj wyliczamy odległość punktu od kamery. Potrzebujemy tej wartości by stworzyć iluzję perspektywy.
(cx-bx[i])/(cz-bz[i]) - tworzymy iluzję perspektywy o której wspomniałem, czyli zmniejszamy obiekty im dalej znajdują się od kamery.
*vx - gdy już sobie poradziliśmy z perspektywą czas doliczyć kąt widzenia. Tylko nie koniecznie w zakresie w jakim można byłoby się spodziewać bo większa liczba rozciąga obraz (zmniejsza kąt). Wyraźnie nie ma to związku z punktem zbiegu dla którego zdefiniowaliśmy vx ale tak się składa, że również i tutaj najlepsza wartość to połowa ekranu.
vx+ - i w końcu nasz punkt zbiegu.

Teraz gdy już mamy wszystkie punkt w 2D, pozostaje je tylko wyświetlić:

//rysowanie:
graphics.lineStyle(1, 0xffffff, 100);
//przednia sciana:
graphics.moveTo(bx2[0], by2[0]);
graphics.lineTo(bx2[1], by2[1]);
graphics.lineTo(bx2[2], by2[2]);
graphics.lineTo(bx2[3], by2[3]);
graphics.lineTo(bx2[0], by2[0]);
//tylna sciana:
graphics.moveTo(bx2[4], by2[4]);
graphics.lineTo(bx2[5], by2[5]);
graphics.lineTo(bx2[6], by2[6]);
graphics.lineTo(bx2[7], by2[7]);
graphics.lineTo(bx2[4], by2[4]);
//linie laczace:
graphics.moveTo(bx2[0], by2[0]);
graphics.lineTo(bx2[4], by2[4]);
graphics.moveTo(bx2[1], by2[1]);
graphics.lineTo(bx2[5], by2[5]);
graphics.moveTo(bx2[2], by2[2]);
graphics.lineTo(bx2[6], by2[6]);
graphics.moveTo(bx2[3], by2[3]);
graphics.lineTo(bx2[7], by2[7]);
Linijka graphics.lineStyle(1, 0xffffff, 100); ustala wygląd linii którą będziemy rysować. Pierwsza wartość to grubość (w pikselach), druga to kolor, a ostatnia to przeźroczystość.
Dalej już samo rysowanie, zaczynając od przedniej ściany:

Dodajemy tylną ścianę:

Na koniec łączymy je by stworzyć sześcian:

(Oczywiście w aplikacji to wszystko zostanie narysowane natychmiast).
Jeśli wszystko przebiegło pomyślnie na ekranie aplikacji powinien pojawić się sześcian (taki sam jak na ostatnim obrazku).
W kolejnym kroku powinniśmy dodać możliwość przesuwania kamery ale to zostawię na jeden z późniejszych postów.

Cały kod jest do pobrania tutaj: box3d.zip