Playing with XNA Triangle Picking Sample I found out that it does not work well if you scale the world matrix of the objects you want to pick. When I dug into the implementation I found this comment in the RayIntersectsModel
method:
// The input ray is in world space, but our model data is stored in object
// space. We would normally have to transform all the model data by the
// modelTransform matrix, moving it into world space before we test it
// against the ray. That transform can be slow if there are a lot of
// triangles in the model, however, so instead we do the opposite.
// Transforming our ray by the inverse modelTransform moves it into object
// space, where we can test it directly against our model data. Since there
// is only one ray but typically many triangles, doing things this way
// around can be much faster.
After the comment they actually transformed the ray:
ray.Position = Vector3.Transform(ray.Position, inverseTransform);
ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform);
With this implementation, picking suffered from "short-sightedness" if you scaled the models: it could only pick those objects, that were close enough to it. Even the ray-boundingSphere intersection test, which implementation is hardcoded into XNA, failed in the same way.
I fixed this by "doing the wrong thing" - I actually started transforming every vertex by the model's world matrix and to fix the boundingSphere test I added this code:
Quaternion rot;
Vector3 scale, trans;
modelTransform.Decompose(out scale, out rot, out trans);
float maxScale = Math.Max(Math.Max(scale.X, scale.Y), scale.Z);
boundingSphere.Center = Vector3.Transform(boundingSphere.Center, modelTransform);
boundingSphere.Radius *= maxScale;
This obviously is not optimal and I wanted to know if there is a way to actually transform the ray back to the model's space with the model's inverted matrix, while making it work for scaled matrices?
SOLUTION: Thanks to Nathan's answer I found a way to fix the ray scaling - just renormalize the ray direction:
ray.Position = Vector3.Transform(ray.Position, inverseTransform);
ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform);
//ADD THE FOLLOWING LINE:
ray.Direction.Normalize();
SOLUTION UPDATE: As I tested the app, I found that Nathan was indeed completely right and another change was necessary. Here is the full code for the correct RayIntersectsModel() method:
static float? RayIntersectsModel(Ray ray, Model model, Matrix modelTransform,
out bool insideBoundingSphere,
out Vector3 vertex1, out Vector3 vertex2,
out Vector3 vertex3)
{
vertex1 = vertex2 = vertex3 = Vector3.Zero;
...
Matrix inverseTransform = Matrix.Invert(modelTransform);
// STORE WORLDSPACE RAY.
Ray oldRay = ray;
ray.Position = Vector3.Transform(ray.Position, inverseTransform);
ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform);
ray.Direction.Normalize();
// Look up our custom collision data from the Tag property of the model.
Dictionary<string, object> tagData = (Dictionary<string, object>)model.Tag;
if (tagData == null)
{
throw new InvalidOperationException(
"Model.Tag is not set correctly. Make sure your model " +
"was built using the custom TrianglePickingProcessor.");
}
// Start off with a fast bounding sphere test.
BoundingSphere boundingSphere = (BoundingSphere)tagData["BoundingSphere"];
if (boundingSphere.Intersects(ray) == null)
{
// If the ray does not intersect the bounding sphere, we cannot
// possibly have picked this model, so there is no need to even
// bother looking at the individual triangle data.
insideBoundingSphere = false;
return null;
}
else
{
// The bounding sphere test passed, so we need to do a full
// triangle picking test.
insideBoundingSphere = true;
// Keep track of the closest triangle we found so far,
// so we can always return the closest one.
float? closestIntersection = null;
// Loop over the vertex data, 3 at a time (3 vertices = 1 triangle).
Vector3[] vertices = (Vector3[])tagData["Vertices"];
for (int i = 0; i < vertices.Length; i += 3)
{
// Perform a ray to triangle intersection test.
float? intersection;
RayIntersectsTriangle(ref ray,
ref vertices[i],
ref vertices[i + 1],
ref vertices[i + 2],
out intersection);
// Does the ray intersect this triangle?
if (intersection != null)
{
// RECOMPUTE DISTANCE IN WORLD SPACE:
Vector3 vertexA = Vector3.Transform(vertices[i], modelTransform);
Vector3 vertexB = Vector3.Transform(vertices[i+1], modelTransform);
Vector3 vertexC = Vector3.Transform(vertices[i+2], modelTransform);
RayIntersectsTriangle(ref oldRay,
ref vertexA,
ref vertexB,
ref vertexC,
out intersection);
// If so, is it closer than any other previous triangle?
if ((closestIntersection == null) ||
(intersection < closestIntersection))
{
// Store the distance to this triangle.
closestIntersection = intersection;
// Store the three vertex positions in world space.
vertex1 = vertexA;
vertex2 = vertexB;
vertex3 = vertexC;
}
}
}
return closestIntersection;
}
}