A Stupidly Simple, Fast Octree Traversal Algorithm for Ray Intersection

I’ve been doing some game dev stuff lately and I needed to intersect a ray with an octree of triangles, for collision detection. I first implemented a naive algorithm that simply checked if the AABB of each octant intersected the ray, then found the closest point. This was devastatingly slow, as you might expect. I then implemented the algorithm described by Revelles et al which is a nice algorithm, but limited (all octants must be half the size of their parents, for instance; this means it can work only on true octrees and not “loose octrees” or k-d trees) and fairly complicated.

Today I had a random thought while doing day-job work: what if I treat the octree divisions as splitting planes and essentially do a binary search? By knowing which plane my ray is closest to at a given step, I know which nodes I need to search. To my surprise – and slight horror, because it’s never a good sign – this worked perfectly on the first try and is crazy fast. I can’t see any performance drop whatsoever when I enable physics in my game anymore, which is always a good sign.

Given how simple this is, I find it exceptionally hard to believe that this is actually a novel algorithm, but I haven’t found this documented anywhere. Anyone have an idea?

The algorithm #

Here’s a simple description of the algorithm as a recursive process. It’s possible to make this iterative, of course.

The code #

Here’s my implementation of this in C# along with the interfaces it expects.

// Planes are in normal+distance form
class Plane {
    Vector3 Normal;
    float Distance;

    float RayDistance(Vector3 origin, Vector3 direction); // Gives the distance along the ray (+direction) where intersection with the plane occurs
}

interface AABB {
    Vector3 Center; // Center point of the bounding box
    Plane[] MidPlanes = new[] { // These are the planes which split a bounding box in half in each direction
            new Plane(Vector3.UnitZ, Center.Z),
            new Plane(Vector3.UnitY, Center.Y),
            new Plane(Vector3.UnitX, Center.X)
        };
    bool Contains(Vector3 point); // True if the point lies within the bounding box
}

class Octree {
    bool Empty; // Whether this octant contains nothing
    Octree[] Nodes; // This is an array of 8 child octants, laid out where 0 bits correspond to the negative side of the plane, 1 bits are positive
                    // X == 4, Y == 2, Z == 1
    AABB BoundingBox; // Axis-aligned bounding box for this octant
    Mesh Leaf; // Triangles for a given leaf octant

    (Triangle, Vector3)? RayIntersection(Vector3 _origin, Vector3 direction) {
        if(Empty) return null;
        var origin = _origin;
        if(Leaf != null) return Leaf.RayIntersection(origin, direction);

        var planes = BoundingBox.MidPlanes;
        var side = (
            X: Vector3.Dot(origin, planes[2].Normal) - planes[2].Distance >= 0,
            Y: Vector3.Dot(origin, planes[1].Normal) - planes[1].Distance >= 0,
            Z: Vector3.Dot(origin, planes[0].Normal) - planes[0].Distance >= 0
        );
        var xDist = side.X == direction.X < 0
            ? planes[2].RayDistance(origin, direction)
            : float.PositiveInfinity;
        var yDist = side.Y == direction.Y < 0
            ? planes[1].RayDistance(origin, direction)
            : float.PositiveInfinity;
        var zDist = side.Z == direction.Z < 0
            ? planes[0].RayDistance(origin, direction)
            : float.PositiveInfinity;
        for(var i = 0; i < 3; ++i) {
            var idx = (side.Z ? 1 : 0) | (side.Y ? 2 : 0) | (side.X ? 4 : 0);
            var ret = Nodes[idx].RayIntersection(origin, direction);
            if(ret != null) return ret;

            var minDist = MathF.Min(MathF.Min(xDist, yDist), zDist);
            if(float.IsInfinity(minDist)) return null;

            origin = _origin + direction * minDist;
            if(!BoundingBox.Contains(origin)) return null;
            if(minDist == xDist) { side.X = !side.X; xDist = float.PositiveInfinity; }
            else if(minDist == yDist) { side.Y = !side.Y; yDist = float.PositiveInfinity; }
            else if(minDist == zDist) { side.Z = !side.Z; zDist = float.PositiveInfinity; }
        }

        return null;
    }
}
 
269
Kudos
 
269
Kudos

Now read this

Nintendo Switch nvservices Info Leak

In this post I’m going to discuss a Nintendo Switch bug I submitted to the Nintendo bug bounty program a few months ago, which they fixed recently (in 6.0, I believe, though I haven’t tested this myself). Background # The Switch runs on... Continue →