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