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

Podstawy Stage3D część IV: korzystanie z shaderów [ekspert]

Shadery są już nie odłączanym elementem w grafice 3D i to do tego stopnia, że nie możliwe jest wyświetlanie obrazu bez ustawienia chociaż najprostszego programu cieniującego. Dlatego tym razem opisze pokrótce jak pracować z shaderami na przykładzie oświetlenia i choć te można symulować na kilka różnych sposobów to właśnie shadery będą najłatwiejszym rozwiązaniem na dodanie świtała w naszym wirtualnym świecie. Programu cieniujące, czyli zestawy instrukcji przekazywanych karcie graficznej wymagają znajomości AGAL który jest zupełnie innym językiem od ActionScript i na jego opanowanie potrzebny byłby całkowicie osobny poradnik - tych na szczęście jest całkiem sporo w Internecie, dlatego na razie pominę szczegóły i skupię się tylko na absolutnym minimum potrzebnym do zastosowania prostego cieniowania.
Mając kod z poprzedniego artykuły, od razu zacznijmy od przygotowania Fragment Shader'a:
private const FRAGMENT_SHADER_LIGHT:String = "dp3 ft1, fc2, v1 n"+
"neg ft1, ft1 n"+
"max ft1, ft1, fc0 n"+
"mul ft2, fc4, ft1 n"+
"mul ft2, ft2, fc3 n"+
"add oc, ft2, fc1";
oraz Vertex Shader'a:
private const VERTEX_SHADER_LIGHT:String = "mov vt0, va0n"+
"m44 op, vt0, vc0n"+
"nrm vt1.xyz, va0.xyzn"+
"mov vt1.w, va0.wn"+	
"mov v1, vt1n" +
"mov v2, va1";
(Kod zapożyczony z http://blog.norbz.net/2012/04/stage3d-agal-from-scratch-part-vii-let-there-be-light)
Powyższe shadery będą rozjaśniać renderowane trójkąty zależnie od ich kąta i ustawienia względem kamery stwarzając wrażenie jakoby ta rzucała światło. Za nim cokolwiek zrobimy z tym kodem musimy go jeszcze skompilować do specjalnego "programu", który przygotujemy wykorzystując AGALMiniAssembler:
vertexAssembly.assemble(Context3DProgramType.VERTEX, VERTEX_SHADER_LIGHT, false);
fragmentAssembly.assemble(Context3DProgramType.FRAGMENT, FRAGMENT_SHADER_LIGHT, false);

programPairLight = renderContext.createProgram();
programPairLight.upload(vertexAssembly.agalcode, fragmentAssembly.agalcode);
(Nie zapomnij utworzyć zmiennej programPairLight)
W tym momencie mamy w naszej aplikacji dwa shadery: standardowy programPair oraz generujący oświetlenie programPairLight. Pozostanie nam jeszcze tylko zdecydować z którego będziemy korzystać podczas renderowania grafiki 3D wykorzystując funkcje setProgram w obiekcie Context3D. Aby łatwiej było widać różnicę w ich działaniu stworzymy osobny obiekt Box3D (nazwijmy go "box3DLight"), a następnie dopiszemy kod odpowiedzialny za jego wyświetlanie przed wywołaniem present() ale po wyrysowania "box3D":
boxMat = box3DLight.getMatrix3D();
boxMat.identity();
boxMat.append(rotMat);
boxMat.appendTranslation(posVec.x + 1, posVec.y + 1, posVec.z+1);

finalTransform.identity();
finalTransform.append(boxMat);
finalTransform.append(world);
finalTransform.append(camera);
finalTransform.append(projection);
	
renderContext.setProgram(programPairLight);
renderContext.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, finalTransform, true);

var p:Vector3D = camera.position.clone();
p.normalize();
var m3d:Matrix3D = finalTransform.clone();
m3d.invert();
p = m3d.transformVector(p);
p.normalize();
p.negate();

renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([0,0,0,0]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, Vector.<Number>([0,0,0,0]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 2, Vector.<Number>([p.x,p.y,p.z,1]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 3, Vector.<Number>([1, 1, 1, 1]));
renderContext.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 4, Vector.<Number>([0.6, 0.6, 0.6, 1]) );

box3DLight.render(renderContext);
Jest tego trochę, ale najważniejsze dla nas to to co się dzieje po linijce setProgram (operacje na "boxMat" i "finalTransform" to znane nam już transformacje z 3D do 2D). Termin "program" jest tutaj nieprzypadkowy bo shadery podobnie jak każdy inny program na komputerze wykonują pewne operacje matematyczne na zmiennych i stałych, aczkolwiek wykorzystując do tego bardzo nisko-poziomowy język. Dodatkowo w shaderach istnieją duże ograniczenia i tak np. zmienne mogą być tworzone tylko w programie i może ich być góra 8. Na szczęście inaczej sprawa ma się ze stałymi, ponieważ te możemy przekazywać z zewnątrz wykorzystując do tego funkcje setProgramConstantsFromVector oraz setProgramConstantsFromMatrix (również maksymalnie 8) i to właśnie dzięki nim do programu cieniującego przekażemy informacje o położeniu światła (zmienna "p") i jego kolorze.
Jeśli wszystko pójdzie zgodnie z planem powinniśmy zobaczyć coś takiego:
W tym przykładzie operujemy jedynie na dwóch shaderach, jednak nic nie stoi na przeszkodzie by stworzyć ich o wiele, wiele więcej a następnie wymieniać je funkcją setProgram gdy tylko chcemy, podobnie jak w Graphics wywołujemy funkcję lineStyle.
Całość do pobrania stąd: Tutorial3D_light.zip.

Podstawy Stage3D część III: korzystanie z rotacji [ekspert]

Kontynuując z poprzedniej części tutaj tym razem chce poruszyć temat łączenia rotacji. Każdy kto bez wcześniejszego kontaktu z 3D spróbował obrócić bryłę we Flash'owym Stage3D korzystając z funkcji appendRotation() na pewno prędzej czy później otrzymał wynik, którego się nie spodziewał. Do tego by jeszcze bardziej zniechęcić początkujących, w Internecie główną odpowiedział na owe zagadnienie jest "będziesz musiał skorzystać z liczb zespolonych (kwaternionów dokładnie)". Hmm? Dobra, nie wiem jak bardzo liczby zespolone przydają się do wyliczania obrotu, ale jeśli do rotacji 2D nie potrzebne są obliczenia 3D, to i zapewne w obrocie 3D da się przeżyć bez operacji 4D, a już szczególnie jeśli dopiero zaczynamy poruszać ten temat.
W każdy bądź razie, przejdźmy do sedna sprawy zaczynając od kodu z poprzedniego artykułu: Tutorial3D-2.zip, najpierw usuńmy wszystkie sześciany zostawiając tylko jedno pudełko, reszta nie będzie nam potrzebna. Przy okazji dodać można jeszcze zmienne do przechowywania jednego Vector3D i jednej Matrix3D - przydadzą się później.
Generalnie rotacje można dodać na dwa sposoby: lokalnie i globalnie - innymi słowy biorąc pod uwagę już istniejący obrót lub nie. Jakikolwiek sposób wybierzemy należy pamiętać, że na transformacje ma jeszcze wpływ istniejące przesunięcie, dlatego by ułatwić sobie pracę dobrze jest trzymać samą rotację w osobnej macierzy, a potem ją dodawać do transformacji naszego pudełka 3D. Tak więc zacznijmy od rotacji globalnej oraz utworzenia dodatkowej matrycy (jeśli jeszcze tego nie zrobiłeś):
private var rotMat:Matrix3D = new Matrix3D();
Następnie zmodyfikujemy kod odpowiedzialny za obsługę strzałek klawiatury tak by teraz dodawał obrót do matrycy rotMat:
switch(key&0x0000ff) {
	case 0x00000f: // forward
		rotMat.appendRotation(SPEED, Vector3D.X_AXIS);
		break;
	case 0x0000f0: // backward
		rotMat.appendRotation(-SPEED, Vector3D.X_AXIS);
		break;
}
switch(key&0x00ff00) {
	case 0x000f00: // left
		rotMat.appendRotation(SPEED, Vector3D.Y_AXIS);
		break;
	case 0x00f000: // right
		rotMat.appendRotation(-SPEED, Vector3D.Y_AXIS);
		break;
}
W tym momencie pozostaje nam już tylko dodać rotacje do obiektu sześcianu box3D zanim zostanie utworzona ostateczna transformacja w matrycy finalTransform:
var boxMat:Matrix3D = box3D.getMatrix3D();
boxMat.identity();
boxMat.append(rotMat);
W efekcie dostając:
Żeby uczynić ten przykład nieco ciekawszym i przy okazji sprawdzić, że faktycznie rotacja będzie niezależna od pozycji, możemy wykorzystać wcześniej wspomniany Vector3D. Nazwijmy go:
private var posVec:Vector3D = new Vector3D();
Jego manipulacja nie powinna być problemem na tym poziomie, dlatego wspomnę tylko aby dodać jego wartości w odpowiednim miejscu - zaraz po linijce boxMat.append(rotMat); należy dopisać:
boxMat.appendTranslation(posVec.x, posVec.y, posVec.z);
Otrzymując:
Tak więc globalna rotacja nie była szczególnie skomplikowanym zagadnieniem, ale pozostaje jeszcze kwestia lokalnej transformacji. Na szczęście i tutaj rozwiązanie jest trywialne i sprowadza się tylko do znalezienia prawidłowych osi w obiekcie:
switch(key&0x0000ff) {
	case 0x00000f: // forward
		rotMat.appendRotation(SPEED, rotMat.transformVector(Vector3D.X_AXIS));
		posVec.y += SPEED/50;
		break;
	case 0x0000f0: // backward
		rotMat.appendRotation(-SPEED, rotMat.transformVector(Vector3D.X_AXIS));
		posVec.y -= SPEED/50;
		break;
}
switch(key&0x00ff00) {
	case 0x000f00: // left
		rotMat.appendRotation(SPEED, rotMat.transformVector(Vector3D.Y_AXIS));
		posVec.x -= SPEED/50;
		break;
	case 0x00f000: // right
		rotMat.appendRotation(-SPEED, rotMat.transformVector(Vector3D.Y_AXIS));
		posVec.x += SPEED/50;
		break;
}
Wektor X_AXIS zostanie przekształcony do lokalnego systemu obiektu, dzięki czemu będziemy wiedzieli gdzie ma oś X w około której następnie dokonamy obrotu. W rezultacie jedna z krawędzi obracanego sześcianu będzie pozostawać nieruchomo, zależnie od wybranej osi:
Kod źródłowy: Tutorial3D_rot.zip

Podstawy Stage3D część II: poruszanie kamerą i funkcja poinatAt [ekspert]

W poprzedniej części artykułu pisałem jak rozbudować przykład z dokumentacji ActionScript3 na temat grafiki 3D by obsługiwał wyświetlanie wielu obiektów przy jednoczesnym zachowaniu możliwości transformacji każdego obiektu z osobna. Pojawiła się tam również wzmianka o matrycy odpowiedzialnej za kamerę ("view"), która tak naprawdę nie do końca działała jak należy ponieważ brakowało w niej kilku podstawowych ustawień. Tu właśnie zaczyna się schody dla osób które nigdy nie miały styczności z grafiką 3D, ponieważ informacje dostępne w Internecie na temat owych ustawień i jak powinny wyglądać są dość mieszane, szczególnie że prawidłowe poruszanie kamerą będzie zależało od ustawień i implementacji samego środowiska 3D. Dlatego tą cześć artykułu o podstawach Stage3D poświecę prawidłowemu stosowaniu wirtualnej kamery, a w szczególności konstrukcji, pozycjonowaniu i korzystaniu z funkcji pointAt.

Zaczynając gdzie skończyliśmy ostatnim razem (kod źródłowy z poprzedniej części artykułu: Tutorial3D.zip) mamy już całkiem niezły start do zaimplementowanie wirtualnej kamery. Co prawda można dla niej przygotować jakąś osobną klasę, ale matryca na razie w zupełności wystarcza. W każdym razie, matryca kamery jest gotowa i można z niej skorzystać jednak rezultaty transformacji będą co najmniej... nieprzewidywalne, albo przynajmniej innej niż się można było by spodziewać (np. x+=-5 okaże się przesunięciem w prawo zamiast lewo). Wiąże się to z pominięciem pewnego szczegółu w funkcji renderującej w przykładzie dostarczonym przez Adobe, a który może nam przysporzyć kilku nieprzespanych nocy. O co dokładnie chodzi? Zerknijmy jak tworzona jest matryca finalTransform w funkcji render:

finalTransform.identity();   
finalTransform.append(box.getMatrix3D());   
finalTransform.append(world);   
finalTransform.append(view);   
finalTransform.append(projection);
Niby wszystko wydaje się w porządku, ale wystarczy się zatrzymać na chwilę i przyjrzeć dokładnie - powiedzmy, że chcemy aby cały świat był przesunięty w lewo, a kamera w prawo, dlatego ustawiamy world na x = -10, a view na x = 5. W idealnym świecie oznaczało by to, że teraz jest między nimi 15 jednostek różnicy, ale oczywiście w naszej aplikacji tak nie będzie, a wszystko przez to jak powstaje finalTransform. Spójrzmy jeszcze raz, world to przesuniecie o -10 a view o +5:
finalTransform.append(-10);   
finalTransform.append(5);
Wiec jaki będzie wynik? Oczywiście każdy obiekt w programie zostanie najpierw przesunięty w lewo (-10) a potem w prawo (+5), ostatecznie lądując na pozycji x = -5. Niby oczywista oczywistość, ale bardzo ważne jest zrozumienie, że tak naprawdę nie przesuwamy kamery, ale cały świat wokół niej. I to jest ten ważny szczegół: nie przesuwamy kamery w lewo, tylko cały świat w prawo! I prawda jest taka, że można było by to załatwić prostym "po prostu transformuj kamerę w przeciwnym kierunku i będzie ok", ale … to tragiczny pomysł - jeszcze rotacje i pozycje da się tak zmieniać, ale o funkcji poinatAt możesz zapomnieć. Na szczęście właściwie rozwiązanie jest bardzo proste, wystarczy obrócić matrycę widoku:
var camera:Matrix3D = view.clone();  
camera.invert();
Zgadza się, tyle wystarczy. Cała funkcja render będzie teraz wyglądać tak:
private function render(event:Event):void {  
renderContext.clear(.3, .3, .3);  
var camera:Matrix3D = view.clone();  
camera.invert();  
for each(var box:Box3D in vBox) {  
finalTransform.identity();  
finalTransform.append(box.getMatrix3D());  
finalTransform.append(world);  
finalTransform.append(camera);  
finalTransform.append(projection);  
renderContext.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, finalTransform, true);  
box.render(renderContext);  
}  
renderContext.present();  
}
Te dwie linijki kodu wystarczą by nasza kamera zaczęła działać dokładnie tak jak się tego spodziewamy, przesunięcie view w lewo autentycznie będzie wyglądać jakbyśmy przesunęli kamerę w lewo, a co najważniejsze będziemy mogli skorzystać z funkcji pointAt, o czym napiszę za chwilę. W tym momencie najlepiej byłoby przetestować poruszanie widokiem w rezultacie dostając coś takiego:
(Poruszanie na klawiszach strzałek)
W razie czego możesz skorzystać z kodu na końcu poradnika.
Teraz gdy już wszystko jest poprawnie ustawione, czas wziąć się za funkcję pointAt. Bez wiedzy o tym, że matryce view należy obrócić (funkcją invert) przed użyciem w finalTransform, pointAt nigdy nie działało by jak należy! Jednak to nie koniec problemów, zostaje jeszcze kwestia parametrów które są potrzebne dla pointAt, bo oczywiście domyślne nie będą działać. Te parametry to:
1. Pozycja celu - matryca zostanie zwrócona w kierunku tego punktu.
2. Kierunek zwrotu - innymi słowy kierunek "do przód" bez żadnych transformacji; dla kamery zawsze będzie to Vector3D(0, 0, -1).
3. Kierunek wznoszenia - w którą stronę jest "do góry"; domyślnie Vector3D(0, -1, 0).
To powinno wystarczyć. Poprawnie działający pointAt będzie wyglądać tak:
Niestety, zmiana pozycji nie uwzględnia kierunku w jakim spogląda kamera - zostawmy to na kiedy indziej.
Kod źródłowy: Tutorial3D-2.zip

Podstawy Stage3D część I: wyświetlanie wielu obiektów [zaawansowany]

Dwa tygodnie temu pisałem jak przystosować Flash Professional do obsługi Flash Player 11 przy okazji korzystając z przykładu w dokumentacji na wyświetlanie grafiki 3D (http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display3D/Context3D.html#includeExamplesSummary). Jednak skorzystanie z gotowego rozwiązania to jedno, a zrozumienie co właściwie robi i jak działa to już zupełnie inna sprawa - dlatego tym razem napiszę co nieco o podstawowych zagadnieniach Stage3D (jak to Adobe nazywa swoją akcelerację 3D we Flash'u).
Tak na wszelki wypadek cały kod z owego przykładu (wraz z dodatkowymi bibliotekami potrzebnymi do jego uruchomienia) znajduje się tutaj: Context3DExample.zip

Pierwszą cześć tego artykułu poświęcę na opis tworzenia i wyświetlania kilku obiektów naraz przy jednoczesnym zachowaniu możliwości ich transformacji. Dla ułatwienia sugeruje stworzyć osobną klasę dla sześcianów, kopiując już istniejący kod (z przykładu w dokumentacji) odpowiedzialny za tworzenie geometrii: IndexBuffer i VertexBuffer. Taka klasa może wyglądać tak:

private var vbBuffer:VertexBuffer3D;
private var ibIndex:IndexBuffer3D;
private var m3D:Matrix3D;
private var nCount:int;

public function Box3D(context:Context3D) {
			
            var triangles:Vector.<uint> = Vector.<uint>([...]);
	ibIndex = context.createIndexBuffer(triangles.length);
	ibIndex.uploadFromVector(triangles, 0, triangles.length);
			
	const dataPerVertex:int = 6;
	var vertexData:Vector.<Number> = Vector.<Number>([...]);
	vbBuffer = context.createVertexBuffer(vertexData.length / dataPerVertex, dataPerVertex);
	vbBuffer.uploadFromVector(vertexData, 0, vertexData.length / dataPerVertex);
					
	m3D = new Matrix3D();
	nCount = 12;	
}
				
public function render(context:Context3D):void {
	context.setVertexBufferAt( 0, vbBuffer, 0, Context3DVertexBufferFormat.FLOAT_3 );
	context.setVertexBufferAt( 1, vbBuffer, 3, Context3DVertexBufferFormat.FLOAT_3 );
	context.drawTriangles(ibIndex, 0, nCount);
}
				
public function getMatrix3D():Matrix3D {return m3D;}
Nie ma tutaj nic szczególnie skomplikowanego - warto jedynie pamiętać, że matryca m3D będzie przechowywać transformacje (pozycję, skalę) więc sześcian najlepiej zbudować na pozycji 0,0,0 a później go przestawić w wybrane miejsce za pomocą funkcji appendTranslation.
Może jeszcze tylko wyjaśnię, że funkcją setVertexBufferAt ustawiamy który VertexBuffer będzie wykorzystywany do wyrysowania trójkątów (funkcja drawTriangles), a wywoływana jest dwa razy bo za pierwszym razem przekazujemy wierzchołki, a za drugim kolory.
Tak więc przygotowanie klasy sześcianu jest relatywnie łatwe, problemy zaczynają się gdy chcemy je wyświetlić. Zakładając, że wszystkie te kostki będą przechowywane w wektorze vBox funkcję render w przykładzie Context3DExample należy przebudować na coś takiego:
private function render(event:Event):void {
	renderContext.clear(.3, .3, .3);
			
	for each(var box:Box3D in vBox) {
		finalTransform.identity();
		finalTransform.append(box.getMatrix3D());
		finalTransform.append(world);
		finalTransform.append(view);
		finalTransform.append(projection);
	renderContext.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, finalTransform, true);
				
		box.render(renderContext);
	}
			
	renderContext.present();
}
Tutaj już się robi trochę ciekawiej, a to za sprawą dwóch najważniejszych rzeczy: funkcji setProgramConstantsFromMatrix oraz konstrukcji matrycy finalTransform. Za pomocą setProgramConstantsFromMatrix ustawiamy matrycę (transformację) dla renderowanego obiektu i nie da się jej pominąć (a przynajmniej nie ma to sensu, bo nic nie zobaczymy na ekranie), podczas gdy finalTransform posłuży nam do przygotowanie owej matrycy.
Dla osób który nigdy nie miały do czynienia z grafiką 3D będzie to zapewne z początku trudne do zrozumienia, ale wszystko sprawdza się do faktu, że każdy obiekt (sześciany w naszym wypadku) tak naprawdę istnieje niezmodyfikowany przez cały czas działania aplikacji. Innymi słowy gdzieś tam tworzymy naszą kostkę, definiujemy jej trójkąty i nadajemy kolor, ale sama z siebie nie ma żadnej pozycji w przestrzeni - do tego wykorzystywane są właśnie matryce. Sęk w tym, że matryce również same z siebie nic konkretnego nie robią i nie mają żadnego bezpośredniego wpływy na geometrię aż do momentu gdy nie wykorzystamy setProgramConstantsFromMatrix by połączyć geometrię z transformacją. Jednak należy pamiętać, że jest to tylko pewnego rodzaju filtr - "Program" (osobny kod odpowiedzialny za renderowanie) bierze geometrię i przepuszcza ją przez matryce, a rezultat wyświetla na ekranie, nie modyfikując przy tym żadnych zmiennych.
Teraz jeśli chodzi o finalTransform - każdą klatkę animacji zaczynamy od wyzerowania tej matrycy (funkcja identity) by mieć pewność, że nie pozostaną tam jakieś inne transformacje. Ma to szczególne znacznie podczas operacji dodawania (append), ponieważ już istniejąca transformacja ma wpływ na późniejsze rezultaty, na przykład dodanie przesunięcia o 10 jednostek w lewo a potem obrotu o 90 stopni ustawi obiekt w innym miejscu niż gdybyśmy najpierw dodali obrót o 90 stopni a potem przesuniecie o 10 jednostek w lewo. Dokładnie jak w prawdziwym świecie, kierunek jaki obejmiemy na komendę "idź w lewo" będzie zależeć od tego w jaką stronę już spoglądamy. W każdym bądź razie, najlepiej samemu z tym poeksperymentować, pamiętając jedynie, że kolejność dodawani do siebie matryc ma ogromne znaczenie i dlatego właśnie w takiej a nie innej kolejności dodawane są do finalTransform matryce odpowiedzialne za: świat (world), pozycję kamery (view) oraz sposób wyświetlania (projection).

Jak zwykle cały kod użyty w tym artykule jest spakowany i gotowy do pobrania stąd: Tutorial3D.zip

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