senäh

17senäh und so…

Flash/AS3
01. Jun 2011
Kommentare: 3

Ein Dreieck in Molehill rotieren

Kategorien: Flash/AS3 | 01. Jun 2011 | Kommentare: 3

Serie: Einführung in Molehill

Im letzten Teil dieser Serie hatten wir ein Dreieck mit Hilfe der neuen Molehill-3D-API erstellt. Um die 3D-Funktionalität von Molehill besser verstehen zu können, lassen wir dieses Dreieck als nächstes rotieren. Öffnet dazu das Projekt vom letzten mal, ladet es euch hier herunter oder übernehmt einfach diese Klasse:

package de.senaeh.pipo.molehill.helloworld
{
	import com.adobe.utils.AGALMiniAssembler;
	import flash.display.Sprite;
	import flash.display3D.Context3D;
	import flash.display3D.Context3DProgramType;
	import flash.display3D.Context3DTriangleFace;
	import flash.display3D.Context3DVertexBufferFormat;
	import flash.display3D.IndexBuffer3D;
	import flash.display3D.Program3D;
	import flash.display3D.VertexBuffer3D;
	import flash.events.Event;
	import flash.geom.Matrix3D;
	import flash.geom.Rectangle;

	/**
	 * You can use this code as you wish,
	 * but recommend our site http://www.senaeh.de at least once 😉
	 * @author Philipp Zins/Donald Pipowitch
	 */
	public class MolehillHelloWorld extends Sprite
	{
		public function MolehillHelloWorld():void
		{
			if (stage == null)
				addEventListener(Event.ADDED_TO_STAGE, init);
			else
				init();
		}

		private function init(e:Event = null):void
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// set view port size (every graphic will be displayed here)
			stage.stage3Ds[0].viewPort = new Rectangle(0, 0, 800, 600);
			// request context3d
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
			stage.stage3Ds[0].requestContext3D();
		}

		private function onContext3DCreate(e:Event):void
		{
			stage.stage3Ds[0].removeEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
			var context3D:Context3D = e.currentTarget.context3D;

			// configure back buffer
			var width:int = 800;
			var height:int = 600;
			var antiAlias:int = 2;	// possible 0, 2, 4, 16 (0 = low quality, 16 = very high quality)
			var enableDepthAndStencil:Boolean = true;	// depth-buffer or z-buffer for depth information, stencil-buffer for masking operation, default = true
			context3D.configureBackBuffer(width, height, antiAlias, enableDepthAndStencil);

			// set culling methode (culling can be used to ignore specific triangles to increase performance,
			// with Context3DTriangleFace.BACK we can ignore triangles which don't face in our direction and
			// can't be seen anyway)
			context3D.setCulling(Context3DTriangleFace.BACK);

			// create the vertex buffer
			// (vertex buffer store vertices, which are points with XYZ-coordinates and RGB-information)
			var numVertices:int = 3;	// 3, because we want to create a triangle
			var data32PerVertex:int = 6;	// 6, because each vertex has x, y, z, r, g and b values!
		    var vertexBuffer:VertexBuffer3D = context3D.createVertexBuffer(numVertices, data32PerVertex);

			// create the index buffer
			// (index buffer store indices, which determine the order how vertices of a vertex buffer should be rendered)
			var numIndices:int = 3;	// our triangle has 3 vertices so we need 3 indices
			var indexBuffer:IndexBuffer3D = context3D.createIndexBuffer(numIndices);

			// create vertex data, which stores our vertices
			// each vertex has x, y and z values for a position and r, g and b values for a color
			// each vertex and his values is declared in a sequence of 6 values in this order => x, y, z, r, g, b
			// attention: colors are represent as decimals (from 0.0 to 1.0 instead from 0 to 255)
			var vertexData:Vector.<Number> = Vector.<Number>
				(
					[
					-0.5, -0.5, 0,  1, 0, 0, // vertex 1 = bottom left, red
					0, 0.5, 0,  0, 1, 0, // vertex 2 = top middle, green
					0.5, -0.5, 0,  0, 0, 1 // vertex 3 = bottom right, blue
					]
				);

			// index data (order how to draw the vertices)
			// 0, 1, 2 == vertex 1, vertex 2, vertex 3
			var indexData:Vector.<uint> = Vector.<uint>([0, 1, 2]);

			// send our vertex data to the the vertex buffer
			var startVertex:int = 0;
			vertexBuffer.uploadFromVector(vertexData, startVertex, numVertices);

		    // send our index data to the index buffer
		    var startOffset:int = 0;
		    indexBuffer.uploadFromVector(indexData, startOffset, numIndices);

			// agal (Adobe Graphics Assembly Language)
			// creates shader which we need to render triangles
			// you will need AGALMiniAssembler (get it here: http://code.google.com/p/away3d/source/browse/trunk/broomstick/Away3D/src/com/adobe/utils)
			// every agal command is build this way (a so called opcode):
			// <opcode> <destination> <source 1> <source 2 - optional> or in other words
			// <operation> <output> <input1> <input 2 - optional>
			// list of possible operations can be found here: http://www.matthewfabb.com/fp11incubator27_02_11/flash/display3D/Program3D.html#upload()

			// vertex shader modify vertices (rotate, scale, etc.)
			// fragment shader (or pixel shader) modifiy pixel (tint, etc.)

			// use "\n" to flag the end of a command
			var agalVertexSource:String =
				"m44 op, va0, vc0\n"	// command 1: m44 (do matrix multiplication), op (save vertex output, i.e. position of vertices), va0 (x, y, z from vertex buffer), vc0 (a matrix to change vertices) == place our vertices on the screen
				+ "mov v0, va1\n";	// command 2: mov (move data from source 1 to destination), v0 (a "variable" which passes data from vertex shader to fragment shader), va1 (r, g, b from vertex buffer) == send color of vertices to fragment shader
			var agalFragmentSource:String = "mov oc, v0\n";	// command 1: mov, oc (save fragment output, i.e. color of pixels), v0 == show colored pixels on screen

			// compile our agal-opcode to bytearray (for vertex shader and fragment shader)
			var agalVertex:AGALMiniAssembler = new AGALMiniAssembler();
			agalVertex.assemble(Context3DProgramType.VERTEX, agalVertexSource);
			var agalFragment:AGALMiniAssembler = new AGALMiniAssembler();
			agalFragment.assemble(Context3DProgramType.FRAGMENT, agalFragmentSource);

			// Program3D modifies the data from VertexBuffer3D described as in our vertex shader and fragment shader)
			var program3D:Program3D = context3D.createProgram();
			// pass bytearray of our vertex shader and fragment shader to program3D
			program3D.upload(agalVertex.agalcode, agalFragment.agalcode);

			// clear screen (i.e. paint background black)
			context3D.clear(0, 0, 0, 1);

		    // set program in context3D
		    context3D.setProgram(program3D);

			// set vertex buffer in context3D
			// -> index of data, vertex buffer, offset, format
			// we used "va0" and "va1" in our agal code - "va0" == x, y, z of vertices, "va1" == r, g, b of vertices
			// vertex buffer stores data of vertices in x, y, z, r, g, b
			// we say:
			// use three following float values (== Context3DVertexBufferFormat.FLOAT_3) from vertexBuffer
			// with an offset of "0" (== x, y, z) and save them to "va" with index "0"
		    context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
			// we say:
			// use three following float values (== Context3DVertexBufferFormat.FLOAT_3) from vertexBuffer
			// with an offset of "3" (== r, g, b) and save them to "va" with index "1"
			context3D.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);

		    // use our matrix3D to place our vertices (== "vc0" in our agalVertexSource)
		    context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, new Matrix3D());

		    // draw our triangles
		    context3D.drawTriangles(indexBuffer);

		    // show the drawn triangles on the screen
		    context3D.present();
		}
	}
}

Vorbereitung zum Rotieren des Dreiecks

Als erstes solltet ihr folgende Variablen klassenübergreifend zugänglich machen und dabei könnt ihr gleich noch eine neue Variable „frame“ vom Typ uint erstellen, die wir später benötigen:

		private var context3D:Context3D;
		private var vertexBuffer:VertexBuffer3D;
		private var indexBuffer:IndexBuffer3D;
		private var program3D:Program3D;

		private var frame:uint = 0;

Damit wir unser Dreieck durchgängig rotieren lassen können, benötigen wir eine Funktion, die pro Frame aufgerufen wird. Erstellt deswegen die neue Funktion „onEnterFrame()“ und lasst in dieser schon einmal „frame“ hochzählen:

		private function onEnterFrame(e:Event):void
		{
			frame++;
		}

Übernehmt nun aus der Funktion „onContext3DCreate()“ alle Zeilen ab (und einschließlich) „context3D.clear(0, 0, 0, 1);“ und fügt diese nach „frame++“ in „onEnterFrame()“ ein. Fügt außerdem in „onContext3DCreate()“ als letzte Zeile „addEventListener(Event.ENTER_FRAME, onEnterFrame);“ ein. Eure beiden Funktionen sollten jetzt so aussehen:

		private function onContext3DCreate(e:Event):void
		{
			stage.stage3Ds[0].removeEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate);
			context3D = e.currentTarget.context3D;

			// configure back buffer
			var width:int = 800;
			var height:int = 600;
			var antiAlias:int = 2;	// possible 0, 2, 4, 16 (0 = low quality, 16 = very high quality)
			var enableDepthAndStencil:Boolean = true;	// depth-buffer or z-buffer for depth information, stencil-buffer for masking operation, default = true
			context3D.configureBackBuffer(width, height, antiAlias, enableDepthAndStencil);

			// set culling methode (culling can be used to ignore specific triangles to increase performance,
			// with Context3DTriangleFace.BACK we can ignore triangles which don't face in our direction and
			// can't be seen anyway)
			//context3D.setCulling(Context3DTriangleFace.BACK);

			// create the vertex buffer
			// (vertex buffer store vertices, which are points with XYZ-coordinates and RGB-information)
			var numVertices:int = 3;	// 3, because we want to create a triangle
			var data32PerVertex:int = 6;	// 6, because each vertex has x, y, z, r, g and b values!
		    vertexBuffer = context3D.createVertexBuffer(numVertices, data32PerVertex);

			// create the index buffer
			// (index buffer store indices, which determine the order how vertices of a vertex buffer should be rendered)
			var numIndices:int = 3;	// our triangle has 3 vertices so we need 3 indices
			indexBuffer = context3D.createIndexBuffer(numIndices);

			// create vertex data, which stores our vertices
			// each vertex has x, y and z values for a position and r, g and b values for a color
			// each vertex and his values is declared in a sequence of 6 values in this order => x, y, z, r, g, b
			// attention: colors are represent as decimals (from 0.0 to 1.0 instead from 0 to 255)
			var vertexData:Vector.<Number> = Vector.<Number>
				(
					[
					-0.5, -0.5, 0,  1, 0, 0, // vertex 1 = bottom left, red
					0, 0.5, 0,  0, 1, 0, // vertex 2 = top middle, green
					0.5, -0.5, 0,  0, 0, 1 // vertex 3 = bottom right, blue
					]
				);

			// index data (order how to draw the vertices)
			// 0, 1, 2 == vertex 1, vertex 2, vertex 3
			var indexData:Vector.<uint> = Vector.<uint>([0, 1, 2]);

			// send our vertex data to the the vertex buffer
			var startVertex:int = 0;
			vertexBuffer.uploadFromVector(vertexData, startVertex, numVertices);

		    // send our index data to the index buffer
		    var startOffset:int = 0;
		    indexBuffer.uploadFromVector(indexData, startOffset, numIndices);

			// agal (Adobe Graphics Assembly Language)
			// creates shader which we need to render triangles
			// you will need AGALMiniAssembler (get it here: http://code.google.com/p/away3d/source/browse/trunk/broomstick/Away3D/src/com/adobe/utils)
			// every agal command is build this way (a so called opcode):
			// <opcode> <destination> <source 1> <source 2 - optional> or in other words
			// <operation> <output> <input1> <input 2 - optional>
			// list of possible operations can be found here: http://www.matthewfabb.com/fp11incubator27_02_11/flash/display3D/Program3D.html#upload()

			// vertex shader modify vertices (rotate, scale, etc.)
			// fragment shader (or pixel shader) modifiy pixel (tint, etc.)

			// use "\n" to flag the end of a command
			var agalVertexSource:String =
				"m44 op, va0, vc0\n"	// command 1: m44 (do matrix multiplication), op (save vertex output, i.e. position of vertices), va0 (x, y, z from vertex buffer), vc0 (a matrix to change vertices) == place our vertices on the screen
				+ "mov v0, va1\n";	// command 2: mov (move data from source 1 to destination), v0 (a "variable" which passes data from vertex shader to fragment shader), va1 (r, g, b from vertex buffer) == send color of vertices to fragment shader
			var agalFragmentSource:String = "mov oc, v0\n";	// command 1: mov, oc (save fragment output, i.e. color of pixels), v0 == show colored pixels on screen

			// compile our agal-opcode to bytearray (for vertex shader and fragment shader)
			var agalVertex:AGALMiniAssembler = new AGALMiniAssembler();
			agalVertex.assemble(Context3DProgramType.VERTEX, agalVertexSource);
			var agalFragment:AGALMiniAssembler = new AGALMiniAssembler();
			agalFragment.assemble(Context3DProgramType.FRAGMENT, agalFragmentSource);

			// Program3D modifies the data from VertexBuffer3D described as in our vertex shader and fragment shader)
			program3D = context3D.createProgram();
			// pass bytearray of our vertex shader and fragment shader to program3D
			program3D.upload(agalVertex.agalcode, agalFragment.agalcode);

			// update
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}

		private function onEnterFrame(e:Event):void
		{
			frame++;

			// clear screen (i.e. paint background black)
			context3D.clear(0, 0, 0, 1);

		    // set program in context3D
		    context3D.setProgram(program3D);

			// set vertex buffer in context3D
			// -> index of data, vertex buffer, offset, format
			// we used "va0" and "va1" in our agal code - "va0" == x, y, z of vertices, "va1" == r, g, b of vertices
			// vertex buffer stores data of vertices in x, y, z, r, g, b
			// we say:
			// use three following float values (== Context3DVertexBufferFormat.FLOAT_3) from vertexBuffer
			// with an offset of "0" (== x, y, z) and save them to "va" with index "0"
		    context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
			// we say:
			// use three following float values (== Context3DVertexBufferFormat.FLOAT_3) from vertexBuffer
			// with an offset of "3" (== r, g, b) and save them to "va" with index "1"
			context3D.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);

		    // use our matrix3D to place our vertices (== "vc0" in our agalVertexSource)
		    context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, new Matrix3D());

		    // draw our triangles
		    context3D.drawTriangles(indexBuffer);

		    // show the drawn triangles on the screen
		    context3D.present();
		}

Ab jetzt wird euer Dreieck nicht nur ein einziges Mal gezeichnet, sondern pro Frame einmal. Jetzt haben wir die Voraussetzung geschaffen um unser Dreieck animiert rotieren zu lassen.

Rotation des Dreiecks

Um Objekte zu verschieben, zu rotieren oder zu skalieren werden Matrizen genutzt. Wenn ihr euch an den letzten Teil dieser Serie erinnert, wisst ihr vielleicht noch, dass wir im VertexShader unseren VertexBuffer mit eine Matrize multipliziert haben. Diese haben wir in folgender Zeile erstellt:

		    context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, new Matrix3D());

Um unser Dreieck rotieren zu können, müssen wir diese Matrix entsprechend modifizieren. Ersetzt obige Zeile mit folgendem Code und importiert die Klasse Vector3D:

			var matrix3D:Matrix3D = new Matrix3D();
			matrix3D.appendRotation(2 * frame, Vector3D.Y_AXIS);
			matrix3D.appendRotation(1 * frame, Vector3D.X_AXIS);
		    context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, matrix3D, true);

Ihr erstellt eine Variable „matrix3D“ vom Typ Matrix3D. Diese lasst ihr in zwei unterschiedlichen Geschwindigkeiten einmal um die Y-Achse und einmal um die X-Achse drehen. Bei „context3D.setProgramConstantsFromMatrix();“  legt der letzte Parameter, welcher „true“ übergibt, fest, dass die Matrix transponiert übernommen werden soll. Leider kann ich euch nicht 100%ig genau erklären, welche Berechnungen bei einer nicht-transponierten Matrix ablaufen, aber es kommt zu Darstellungsfehlern, sollten wir nicht „true“ übergeben. (Hinweis: Die Darstellungsfehler wären erst am Ende dieses Tutorials mit dem Code, der noch folgt, sichtbar).

Wenn ihr euer Projekt jetzt kompiliert, solltet ihr euer Dreieck rotieren sehen. Allerdings müsste euer Dreieck fast immer angeschnitten sein:

Clipping-Fehler bei einem Dreieck

Clipping-Fehler bei einem Dreieck

Diesen Darstellungsfehler nennt man Clipping. Er entsteht, weil Molehill versucht den darstellbaren Bereich aus Performancegründen möglichst klein zu halten. Dadurch wenn Objekte, die sich außerhalb eines bestimmten Bereiches befinden, nicht mehr dargestellt. Wenn ihr euch das Beispielbild anseht, seht ihr, dass die blaue Ecke nicht angezeigt wird. Sie befindet sich außerhalb des darstellbaren Bereiches. Natürlich müssen wir etwas dagegen unternehmen!

PerspectiveMatrix3D, flieg und sieg!

Unser Helfer in der Not gegen Clipping-Fehler ist die PerspectiveMatrix3D, eine spezielle Matrix3D, mit der wir den darstellbaren Bereich beeinflussen können. Ihr könnt euch die PerspectiveMatrix3D wie eine Art Kamera vorstellen mit einem Seitenverhältnis und einer Brennweite und zwei weiteren Einstellungen, die das Bild in der Nähe und in der Ferne begrenzen. Um PerspectiveMatrix3D nutzen zu können, müsst ihr sie euch allerdings erst noch hier herunterladen. Habt ihr das getan geht es auch schon los!

Als erstes erstellt ihr eine klassenübergreifende Variable „perspectiveMatrix3D“ vom Typ – Achtung, jetzt kommt’s – PerspectiveMatrix3D! Fügt dann in der Funktion „onContext3DCreate()“ vor dem ENTER_FRAME-EventListener folgenden Code ein:

			perspectiveMatrix3D = new PerspectiveMatrix3D();
			var aspect:Number = 800/600;
			var zNear:Number = 0.1;
			var zFar:Number = 1000;
			var fov:Number = 45 * Math.PI / 180;
			perspectiveMatrix3D.perspectiveFieldOfViewLH(fov, aspect, zNear, zFar);

„aspect“ bezeichnet das Seitenverhältnis und „fov“ das Sichtfeld in Bogenmaß. Mit den richtigen „fov“-Werten könntet ihr bspw. Weitwinkelaufnahmen simulieren. „zNear“ und „zFar“ legen nun den Bereich auf der z-Achse fest, in dem Grafiken dargestellt werden. Jetzt müsst ihr die „perpectiveMatrix3D“ mit unserer ursprünglichen „matrix3D“ verrechnen. Springt dazu zu folgendem Codeabschnitt:

			var matrix3D:Matrix3D = new Matrix3D();
			matrix3D.appendRotation(2 * frame, Vector3D.Y_AXIS);
			matrix3D.appendRotation(1 * frame, Vector3D.X_AXIS);

Fügt nach der dritten Zeile folgenden Code hinzu:

			matrix3D.appendTranslation(0, 0, 2);
			matrix3D.append(perspectiveMatrix3D);

Mit „matrix3D.appendTranslation(0, 0, 2);“ verschiebt ihr unser Dreieck zwei Einheiten auf der z-Achse nach hinten. Diese Verschiebung hat nichts mit der Rotation zu tun. Sie hilft uns nur das Dreieck besser zu sehen. Mit „matrix3D.append(perspectiveMatrix3D);“ wendet ihr nun die „perpectiveMatrix3D“ auf „matrix3D“ an.

Wenn ihr euer Projekt nun kompiliert, werdet ihr keine Clipping-Fehler mehr sehen. Das Dreieck wird nie angeschnitten. Trotzdem ist es manchmal nicht zu sehen. Warum? Erinnert ihr euch an die Culling-Einstellungen aus dem letzten Teil der Serie? Hier haben wir festgelegt, dass die Rückseite des Dreiecks nicht berechnet werden soll, um Performance zu gewinnen. In diesem Fall wollen wir aber die Rückseite sehen. Löscht deswegen folgende Codezeile oder kommentiert sie aus:

context3D.setCulling(Context3DTriangleFace.BACK);

Fertig! Kompiliert euer Projekt abermals und ihr solltet folgendes sehen (nur sichtbar mit Flash Palyer Incubator, verkleinerte Darstellung):

Get Adobe Flash player

Die Projektdateien könnt ihr euch hier herunterladen.

Happy coding und so 😉

Autor: Pipo

...kommt ursprünglich aus der Designerecke, ist aber im Laufe seines Studiums in die Webentwicklung gestolpert. Kann sich seit dem nicht so richtig entscheiden wo er hingehört und wagt deswegen vielleicht die Flucht nach vorne in ein komplett neues Gebiet. Vielleicht Management? Niemand weiß es. Auch er nicht.