Quite a few snippets. I'll try to walk you through them.
First, to get a responsive application, you'll still use AABB SkeletonRenderer::getBoundingBox () method to quickly discard skeletons. Personally, I might derive a custom class from SkeletonAnimation to cache the result of getBoundingBox, or depending on the hit level I might cache it during the computation of the render geometry.
Well, let me backup to 'hit levels'. You should have different levels of hit testing to speed up testing. Try to do as little computations as possible. For instance, if you have a spine animation on the screen that the user can't manipulate, there's no reason to include it in the search. Something like:
enum Spine_HitTestLevel{
SPINEHIT_NONE = 0, // DO NOT TEST [default]
SPINEHIT_GLOBAL_AABB = 1, // Skeletal AABB
SPINEHIT_SLOT_AABB = 2, // Per-slot AABB
SPINEHIT_SLOT_MESH = 3, // Per-Slot Geometry (mesh, quad, hull)
};
Interestingly, unless you are doing some cool caching scheme, SLOT_AABB might be slower than SLOT_MESH. My engine caches the Spine geometry to do a lot of other cool stuff, so for me the SLOT_AABB is quick. In any event, you always check GLOBAL_AABB first as a quick out:
///////////////////////////////////////
// \brief Tests against the Spine Movie Quads in the Draw cache
///////////////////////////////////////
bool SpineMovie::ContainsClick( int globalX, int globalY )
{
if(mHitLevel == SPINEHIT_NONE) return false; // [1.] quick out
PointF testPoint((float)globalX, (float)globalY);
testPoint = TransformToObjectSpace(testPoint); // [2.] all of our geometry is in local space
if(mAABB.Contains(testPoint)){ // [3.] Level always gets tested
if(mHitLevel >= SPINEHIT_SLOT_AABB){ // [4.]
size_t aCount = mRenderGeometry.size();
for(size_t i = 0; i < aCount; ++i){
SpineSlotRenderGeometry& rSlotGeo = mRenderGeometry[i];
if(rSlotGeo.GetAABB().Contains(testPoint)){ // [5.]
if(mHitLevel >= SPINEHIT_SLOT_MESH){
if(wn_SpineHull(testPoint, rSlotGeo.GetWorldVertArray(), rSlotGeo.GetHullCount())){ // [6.]
return true;
}
}
else
return true;
}
}
}
else
return true;
}
return false;
}
Let's walk through the code:
-
If no hit testing is turned on for animation, fail quickly
-
In my engine, all spine animations are done in local space to a DisplayObject container that I move around the screen. Basically, I never mess with Skeleton->{x,y}.
-
First, always check the AABB for the skeleton. It's fairly quick at culling spine animations that you are no where near.
-
Check to see if we want a finer grain hit test for the animation.
-
Again, I already have this cached, so slot_aabb is something you would skip.
-
Check the hull of the attachment against the point using the Winding Number
Here's the code to check the winding number:
// Copyright 2000 softSurfer, 2012 Dan Sunday
// This code may be freely used, distributed and modified for any purpose
// providing that this copyright notice is included with it.
// SoftSurfer makes no warranty for this code, and cannot be held
// liable for any real or imagined damage resulting from its use.
// Users of this code must verify correctness for their application.
//////////////////////////////////////
// Method: isLeft: tests if a point is Left|On|Right of an infinite line.
// See: the January 2001 Algorithm "Area of 2D and 3D Triangles and Polygons"
//////////////////////////////////////
inline int isPointLeft( const VertXY& P0, const VertXY& P1, const PointF& P2 )
{
return (int)( (P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y) );
}
static bool wn_SpineHull( const PointF& theTestPoint, VertXY* thePolygonClipPoints, int vertCount )
{
int wn = 0; // the winding number counter
// loop through all edges of the polygon
for (int i = 0; i < vertCount; ++i)
{
// edge from theQuadClipPoints[i] to theQuadClipPoints[i+1]
if (thePolygonClipPoints[i].y <= theTestPoint.y)
{
// start y <= theTestPoint.mY
if (thePolygonClipPoints[(i + 1) % vertCount].y > theTestPoint.y) // an upward crossing
if (isPointLeft( thePolygonClipPoints[i], thePolygonClipPoints[(i + 1) % vertCount], theTestPoint) > 0) // theTestPoint left of edge
++wn; // have a valid up intersect
}
else
{
// start y > theTestPoint.mY (no test needed)
if (thePolygonClipPoints[(i + 1) % vertCount].y <= theTestPoint.y) // a downward crossing
if (isPointLeft( thePolygonClipPoints[i], thePolygonClipPoints[(i + 1) % vertCount], theTestPoint) < 0) // theTestPoint right of edge
---
wn; // have a valid down intersect
}
}
return (wn != 0);
}
Ok, discussion of the Winding Number test... Basically, it checks the attachment geometry as a polygon. There are 3 types of attachments that I worry about. The Region attachment is a quad with 4 verts, so the hull length of that is going to be 4. With Mesh and Skinned Mesh attachments, the hull length is stored in the mesh attachment struct (mesh->hullLength).
What happens is that the editor saves the mesh in such a manner that the first N verts in the mesh are actually the hull (the outside of the polygon). So if we compute the mesh world geometry, and then check the first N verts when passing to the winding number algorithm, we'll get the right hit test result.
Above, the algorithm expects struct VertXY{ float x, float y}
. Really, you can use any float container where the geometry is represented x,y,x,y,x,y... in floats.
Caching...
Instead of caching your geometry, you could iterate over the skeleton attachments (just like if you were drawing, e.g. see skeletonrenderer:😃raw) and call the associated sp****Attachment_computeWorldVertices. For region attachments, just use a 8 float array in the hit test function. For Meshes, use an stl::vector<float> resized to mesh->verticesCount and pass vector.begin() to the winding number test.
//psuedo
foreach(slot in Skeleton)
if(attachment)
switch(attachment->type)
case region:
float verts[8] = {0};
spRegionAttachment_ComputeWorldVertices(region, slot, verts);
if(wn_SpineHull(testPoint, (VertXY*)verts, 4)) return true; // else keep testing
case Mesh, SkinnedMesh:
std::vector<float> verts; verts.resize(mesh->verticesCount);
spMeshAttachment_ComputeWorldVertices(mesh, slot, verts.begin());
if(wn_SpineHull(testPoint, (VertXY*)verts.begin(), mesh->hullLength)) return true; // else keep testing