Stage3D Picking: Mouse and Touch Interaction
Virtually every 3D app or game will need to support mouse or touch interaction with the 3D scene. Consider a real-time strategy game like StarCraft where clicking on units is essential to the gameplay. Check out the Stage3D
API and you’ll quickly realize that there’s zero functionality to handling mouse or touch events. So how do all of these games handle it? The answer is a technique called “picking” and this article will show you how to implement it.
The “picking” process is actually pretty simple and consists only of a couple steps:
- Construct a ray from the camera pointing “into” the world through the touch/click point. Think of the user’s click as occurring on the near plane of the camera.
- Find the 3D object in the scene that the ray hits first
The first step is done like so:
// Normalize the click var xUnit:Number = (x * 2 / stage.stageWidth) - 1.0; var yUnit:Number = -((y * 2 / stage.stageHeight) - 1.0); // Make the picking ray var rayOrigin:Vector3D = new Vector3D(); var rayDir:Vector3D = new Vector3D(); camera.getPickingRay(xUnit, yUnit, rayOrigin, rayDir);
For this article, I’m upgrading my Simple Stage3D Camera by adding the getPickingRay
function to construct the ray from the camera’s position into the world through the touch/click point:
public function getPickingRay(xUnit:Number, yUnit:Number, intoOrigin:Vector3D, intoDir:Vector3D): void { intoOrigin.x = __position.x; intoOrigin.y = __position.y; intoOrigin.z = __position.z; intoOrigin.w = 1; var nearPlaneHeight:Number = __near * Math.tan(__vFOV); var nearPlaneWidth:Number = nearPlaneHeight * __aspect; var rightOffset:Number = xUnit * nearPlaneWidth; var upOffset:Number = yUnit * nearPlaneHeight; // dir = viewDir*near + rightDir*rightOffset + realUpDir*upOffset intoDir.x = __viewDir.x*__near + __rightDir.x*rightOffset + __realUpDir.x*upOffset; intoDir.y = __viewDir.y*__near + __rightDir.y*rightOffset + __realUpDir.y*upOffset; intoDir.z = __viewDir.z*__near + __rightDir.z*rightOffset + __realUpDir.z*upOffset; intoDir.w = 0; intoDir.normalize(); }
Now we just need to fire that ray at all of the objects in the 3D scene. In this article, I’ve upgraded the Sphere3D
class from Procedurally-Generated Sphere to get the intersection distance between the sphere and the ray:
public function intersectRay(origin:Vector3D, dir:Vector3D): Number { var temp:Vector3D = new Vector3D(); temp.x = origin.x - posX; temp.y = origin.y - posY; temp.z = origin.z - posZ; var a:Number = dir.dotProduct(dir); var b:Number = 2 * dir.dotProduct(temp); var c:Number = temp.dotProduct(temp) - radius*radius; var disc:Number = b*b - 4*a*c; return disc >= 0 ? (-b - Math.sqrt(disc))/(2*a) : NaN; }
Lastly, we just need to use the above function on all of the spheres in the scene:
// Test the picking ray against all renderables var intersectionDistance:Number = Infinity; var picked:Sphere3D; for each (var sphere:Sphere3D in spheres) { // Compute intersection distance and reject non-intersections // and bounding volumes we're inside var distance:Number = sphere.intersectRay(rayOrigin, rayDir); if (!(distance > 0) || distance > intersectionDistance) { continue; } intersectionDistance = distance; picked = sphere; }
And, of course, hook up a mouse or touch listener to call the above code.
Launch the Demo (click a sphere to stop its rotation, click off the spheres to resume)
Full source code and test texture
The above is a simple picking implementation and there’s much more you can do from here:
- Add support many more types of bounding volumes
- Use culling to reduce the number of objects you test against
- Support picking against the triangles of the 3D object’s mesh
Perhaps there will be articles on those some day. For now, if you’ve spotted any bugs or have any questions or suggestions, feel free to leave a comment!
#1 by Mark on July 17th, 2012 ·
Why is it not exact? When I click a few pixels next to the sphere, it also stops rotating.
#2 by ben w on July 17th, 2012 ·
probably because it is using a bounding volume (a sphere in this case) but there will still be some gaps in between the bounding volume an the mesh, a much higher resolution mesh would lead to less gaps.
also the mouse coordinates are whole integers meaning that some accuracy will be lost… for pixel perfect you will need to render a picking buffer but its sloooow, so the minor accuracy issues are worth the trade-off usually (if its a problem you could always shrink the bounding volume a teeny bit)
#3 by Volgogradetzzz on July 23rd, 2012 ·
I wonder what will happen if object too far away from near plane. I.e. the farther it from screen the closer it to the center if you use perspective projection. So when you click, say in top right corner, you can catch object thas is at the center. And it will be correct but not pleasure visually. How to solve this?
#4 by jackson on July 23rd, 2012 ·
That’s a valid concern and a bad picking algorithm could definitely produce such a problem. In the picking code I posted in the article, however, the picking ray is fired into the scene in accordance with the perspective projection. That means that the ray is not fired straight out in the direction of the camera’s view, but instead at an angle defined by the camera’s field of view. Imagine if the perspective view frustum had a pyramid-shaped top on it. The tip of that pyramid would be the camera position and the base of the pyramid would be the near plane. The picking algorithm in the article fires the ray from the tip of the pyramid through the click point on the base of the pyramid. This means that the picking ray can reach any point in the view frustum, not just those points in the middle “box”.
I hope that clears it up.
#5 by Damian on August 13th, 2012 ·
Testing in chrome and firefox, fp 11.3.31.222, the intersection is well off – almost like the collision sphere is twice the size of the earth spheres
#6 by Martin on November 12th, 2012 ·
Yes I can also confirm, this code is buggy. The Spheres are tested as if they were twice their size.
#7 by jackson on November 12th, 2012 ·
Thanks to you and the others who’ve pointed this out. I’ve finally gotten around to fixing the code in the article. The radius was being computed, as you say, at double its actual size. I’ve halved it so that is is accurate and tested that the picking is much more accurate. Obviously a polygonal sphere only approximates a real sphere so the picking is not pixel-perfect, but it is very close. A new ZIP file with the updated source code (just Picking.as) has been uploaded. Thanks again!
#8 by Martin on November 13th, 2012 ·
I just redownloaded the “Full source code and test texture” Picking.zip and it is the old source file ?
#9 by jackson on November 13th, 2012 ·
I just re-downloaded it and it looks right to me. The change was on line 60 of Sphere3D.as from:
To:
This changes it from computing the diameter to computing the radius, which is obviously more correct and explains the “off by 2x” bug.
#10 by Martin on November 13th, 2012 ·
The getPickingRay() function is buggy too. If the camera as at a steep angle, then the ray is shot into a totally wrong direction.
It is as if the function fails to account for perspective distortion??
To reproduce:
1) Apply a camera.pitch(50);
(I simply added the Camera controll buttons from your Simple cam tut, so you can try different settings )
2) Try clicking on the circles now, it wont work.
at lower pitches if you click somewhere ‘close’ where the spheres visually appear then they stop spinning, ie, it reall is if the pitch is not taken into consideration.
If you just MOVE the camera (up/down/left/right) it remains accurate.
#11 by Shai on June 21st, 2016 ·
If I rotate the camera, lets say just for the testing I make the __worldToClip property public and add this line to the code:
“camera.__worldToClip.appendRotation(25, Vector3D.X_AXIS);”
the picking is not working it is off to the right to the object the more you go to the left of the screen. Is there a way to solve that?
#12 by jackson on June 22nd, 2016 ·
Why not use the
Camera3D.rotate
function rather than changing its private variables directly?#13 by Shai on June 22nd, 2016 ·
you mean camera.pitch(50); it is not working properly, the same problem.
#14 by Shai on June 26th, 2016 ·
I did notice that when I change the field of view value in getPickingRay like this, it fixes the pitch problem. did this:
var nearPlaneHeight:Number = __near * Math.tan(.21);//originaly was __vFOV
my FOV was about 0.15 and I’ve changed it to 0.21, so if you are intrested is fixing it, that’s where you have to look.