Alternativa 3D Series – Tutorial 6 – Dragging 3D Objects in Flex 3 using Alternativa3D and Actionscript 3

by Matthew Casperson 3

In the  last Flex/Alternativa3D tutorial you saw how to respond to mouse events that involve Alternativa 3D objects. Once the view was set to be interactive handling mouse events was actually pretty straight forward: you simply attach an event listener to the Alternativa object, just as you would any normal GUI element like a button or checkbox. In this tutorial we will take this interactivity one step further and allow 3D objects to be dragged around on the screen.

Requirements

View DemoDownload Source Files

Prerequisites

You should have read the first tutorial in this series, as it explains a lot of the underlying code that we build on for this tutorial. It’s also worth reading the creating interactivity tutorial to understand the basics of handling events invoving Alternativa objects.

Overview

The end result of this tutorial will be 3 objects that you can drag around the screen. Check out the demo here to see this in action. We achieve this effect by projecting a ray from the mouse cursor, through the camera and into the scene. If an object has been selected by clicking on it then it is moved to the point where this ray intersects the ground. It sounds simple, but there is a bit of work involved in implementing it. Let’s take a look at the ApplicationManager class to see how this is achieved.

ApplicationManager.as

package
{
	import alternativa.engine3d.events.MouseEvent3D;
	import alternativa.engine3d.primitives.Box;
	import alternativa.engine3d.primitives.Cone;
	import alternativa.engine3d.primitives.GeoSphere;
	import alternativa.engine3d.primitives.Plane;
	import alternativa.types.Point3D;

	import flash.events.MouseEvent;
	import flash.geom.Point;

	import mx.core.Application;

	/**
	 * 	The ApplicationManager holds all program related logic.
	 */
	public class ApplicationManager extends BaseObject
	{
		protected static var instance:ApplicationManager = null;
		protected var box:MeshObject = null;
		protected var boxSelected:Boolean = false;
		protected var cone:MeshObject = null;
		protected var coneSelected:Boolean = false;
		protected var sphere:MeshObject = null;
		protected var sphereSelected:Boolean = false;
		protected var ground:MeshObject = null;

		/**
		 *  returns the singelton instance of the ApplicationManager
		 */
		public static function get Instance():ApplicationManager
		{
			if (instance == null)
				instance = new ApplicationManager();
			return instance;
		}

		public function ApplicationManager()
		{
			super();
		}

		/**
		 *  Initialise the ApplicationManager
		 */
		public function startupApplicationManager():ApplicationManager
		{
			super.startupBaseObject();

			// here we watch for any mouse up event. because we want to stop dragging any object in
			// response to a mouse up event it's best to attach this at the application level
			// rather than with individual Alternativa object
			Application.application.addEventListener(MouseEvent.MOUSE_UP, mouseUp);

			// set the camera to look down at the scene, giving a nice perspective
			Application.application.engineManager.camera.coords = new Point3D(0, 75, -75);
			Application.application.engineManager.cameraController.lookAt(new Point3D());

			// create a 3D object
			box = new MeshObject().startupModelObject(new Box(20, 20, 20));
			// texture it
			box.model.cloneMaterialToAllSurfaces(ResourceManager.redTexture);
			// and add an anonymous function to the mouse down event, which sets a flag that allows us to drag the object around
			box.model.addEventListener(MouseEvent3D.MOUSE_DOWN, function(event:MouseEvent3D):void{boxSelected=true;});

			// repeat for two more 3D objects
			cone = new MeshObject().startupModelObject(new Cone(20, 20));
			cone.model.cloneMaterialToAllSurfaces(ResourceManager.greenTexture);
			cone.model.addEventListener(MouseEvent3D.MOUSE_DOWN, function(event:MouseEvent3D):void{coneSelected=true;});
			cone.model.rotationX = -Math.PI/2.0;
			cone.model.x = 50;

			sphere = new MeshObject().startupModelObject(new GeoSphere(20));
			sphere.model.cloneMaterialToAllSurfaces(ResourceManager.blueTexture);
			sphere.model.addEventListener(MouseEvent3D.MOUSE_DOWN, function(event:MouseEvent3D):void{sphereSelected=true;});
			sphere.model.x = -50;

			// create a Plane to serve as our ground, and rotate it so it is horizontal
			ground = new MeshObject().startupModelObject(new Plane(250, 250, 4, 4));
			ground.model.cloneMaterialToAllSurfaces(ResourceManager.CheckerboardTex);
			ground.model.y = -20;
			ground.model.rotationX = Math.PI/2.0;

			return this;
		}

		public override function enterFrame(dt:Number):void
		{
			// get the point on the ground that lies under the mouse pointer
			var newPoint:Point3D = getCurrentGroundPosition();

			// if anoy object has been clicked on, move that object to the point on the
			// ground
			if (boxSelected)
				box.model.coords = newPoint;
			else if (coneSelected)
				cone.model.coords = newPoint;
			else if (sphereSelected)
				sphere.model.coords = newPoint;
		}

		public function mouseUp(event:MouseEvent):void
		{
			// the mouse button was released, so stop dragging all objects
			boxSelected = false;
			coneSelected = false;
			sphereSelected = false;
		}

		public function getCurrentGroundPosition():Point3D
		{
			// use the get3DCoords to find a point 1 uint into the scene from the camera to the point that the current mouse coordinates
			// translate to. We make this point 1 unit long so we don't incure the expense of normalising the vector later
			var point:Point3D = Application.application.engineManager.view.get3DCoords(new Point(Application.application.mouseX, Application.application.mouseY), 1);
			// the point we found is in the cameras local coordinates, so translate it back into global coordinates
			point = Application.application.engineManager.camera.localToGlobal(point);
			// now use the current cameras position and the mouse pointers global coordinates to create a general dierction vector
			var direction:Point3D = Point3D.difference(point, Application.application.engineManager.camera.coords);

			// find where the plane intersects the ray we calculated abive
			var collision:CollisionResult = MathUtils.testIntersionPlane(new Point3D(0, 1, 0), new Point3D(), Application.application.engineManager.camera.coords, direction);
			// it's possible that the ray is pointing away from or perpendicular to the ground plane, so make sure was actually have
			// found a collision between the ray and the gound plane
			if (collision.result)
			{
				// we have a collision. testIntersionPlane returns the distance along the ray that the intersection with
				// the plane occurs, so multiply the direction vector by this value
				direction.multiply(collision.distance);
				// now add that distance vector to the cameras position, which gets us the collision
				// point on the plane
				return Point3D.sum(Application.application.engineManager.camera.coords, direction);
			}

			// no collision was found, so just return a default Point3D
			return new Point3D();
		}
	}
}

We have three MeshObjects: sphere, box and cone. Each MeshObject has an accompanying Boolean flag, which is set to true if the MeshObject has been selected. The majority of the code in the startupApplicationManager function is used to create, initialise and add event listeners to these MeshObject’s. If this code isn’t familiar to you, you should read this tutorial on loading models, and this tutorial on mouse interactions. The only code that stands out is where we attach an event listener to the MOUSE_UP event to the Application object. The reason for this is that releasing the mouse button anywhere on the screen has the same effect, which is to stop dragging the selected Alternativa object. Sure, we could attach event listeners to the MOUSE_UP event on each individual Alternativa object, but that involves wiring up 3 functions (one for the cone, box and sphere) when just 1 will suffice.

In order to give the user a sense of the ground and it’s perspective we also create a Plane with a simple checkerboard texture, and rotate it so it is horizontal.

You may also notice that we don’t actually create any named functions for the MOUSE_DOWN events. See the line that says box.model.addEventListener(MouseEvent3D.MOUSE_DOWN, function(event:MouseEvent3D):void{boxSelected=true;}); . ActionScript allows you to create anonymous functions, and since all we are doing when we click on the 3D objects is to set their accompanying Boolean flags to true, we can save some time by writing the functions directly inside the addEventListener function. It saves a few keystrokes, and anyone looking at the code can see exactly what happens when the event is triggered, without having to scroll through the rest of the code looking for another function.

So now that we have set the Boolean flags to true when an object is selected (via the anonymous functions attached to the Alternativa objects), and set them to false when the mouse button is release (via the mouseUp function, attached to the Flex Application object), it’s time to actually move the objects around when they are being dragged. This happens in the enterFrame function. Here we make a call to getCurrentGroundPosition, which returns the position on the ground that lies underneath the mouse pointer. With that position in hand it then looks to see which Boolean flag has been set to true, and moves the appropriate 3D object to this new position.

The hard work of finding this new position on the ground is done by the getCurrentGroundPosition function. The real trick here is taking the 2D coordinates of the mouse pointer and translating them into a 3D value that has some meaning in our 3D scene. As I mentioned in the introduction this is done by projecting the 2D mouse coordinates through the camera, and out into the scene. If calculating that sounds like a lot of hard work, you’ll be pleased to know that the get3DCoords function in the View class does it all for you. You simply give the get3DCoords function the current mouse coordinates and the depth (i.e. how far into the scene from the camera the point should be) and it returns the position of that point, relative to the camera.

Because the point returned by get3DCoords is relative to the cameras local coordinates, and we need a point in world space, we need to call the cameras localToGlobal function. This converts the local coordinates into world coordinates.

Then in order to test this new point against the ground we need to convert it from a point in space to a direction from the cameras current position. This is done simply by subtracting the cameras world position from the world coordinate that was returned from the localToGlobal function.

The direction, combined with the position of the camera, gives us a ray. This ray represents the line from the 2D mouse coordinates out into the 3D scene, and it’s this ray that we want to test against the ground.

In this tutorial the ground is conceptually represented by a plane, although it’s not the Plane class that is part of the Alternativa library. The Alternativa Plane class is a flat, rectangular object that is drawn to the screen. The plane that makes up our ground is a spatial concept rather than something that is actually seen. The ground plane is defined by a normal (a vector perpendicular to the surface of the plane), and a point that exists on the plane. Our ground is quite simple in that it is horizontal along the x,z axis (which means it’s normal is straight up along the y axis), and that it sits at the origin (so a point on the plane is the default Point3D position of 0,0,0).

MathUtils.as

package
{
	import alternativa.engine3d.core.Camera3D;
	import alternativa.types.Point3D;

	import mx.core.Application;

	/**
	 * 	Takes a screen coordinate and returns a the distance along the ray where
	 *  the collision with the supplied plane occurs, if at all.
	 */
	public final class MathUtils
	{
		public static function testIntersionPlane(planeNormal:Point3D, planePosition:Point3D, rayStart:Point3D, rayDirection:Point3D): CollisionResult
		{
			var dot:Number = Point3D.dot(rayDirection, planeNormal);
			if (dot == 0)
				return new CollisionResult(0, false);

			var collisionDistance:Number = Point3D.dot(planeNormal, Point3D.difference(planePosition, rayStart)) / dot;

			if (collisionDistance <= 0)
				return new CollisionResult(0, false);

			return new CollisionResult(collisionDistance, true);
		}
	}
}

With our ray and plane defined, we can call the testIntersionPlane function. This is a static function in the MathUtils class. It takes the plane, as defined by a normal and a point on the plane, and a ray , as defined by the cameras position and the direction we calculated above.

CollisionResult.as

package
{
	/**
	 * 	A simple class that holds the result of a collision calculation
	 */
	public class CollisionResult
	{
		public var distance:Number = 0;
		public var result:Boolean = false;

		public function CollisionResult(d:Number, r:Boolean)
		{
			this.distance = d;
			this.result = r;
		}
	}
}

The result of the testIntersionPlane function is a CollisionResult. The CollisionResult class has two properties. The result property is true if a collision was found, and false if one was not. The distance property tells us how far along the ray the collision between the plane and the ray occurred. The distance property is only meaningful if the result property is true.

If the call to testIntersionPlane results in a collision (i.e. the result property of the CollisionResult is true) then it is simply a matter of multiplying the CollisionResult distance by the direction (which we calculated for the ray). Since we found a point just 1 unit from the camera (with the depth value we supplied to the get3DCoords function being 1), which essentially gives up a normalised vector, we can do a straight multiplication of this vector with the distance. Adding the result of this to the cameras current position gives us the point on the ground that lies beneath the mouse pointer. If there is no collision, we just return a default Point3D object.

The end result of this is that we can find out where the mouse pointer hits the ground plane in our 3D scene. Once we know that it’s quite trivial to move a 3D object to that point, which essentially gives the effect of dragging an object around in the 3D world.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>