Spatial Subdivision for General Quadrics

One of the key concepts behind realtime raytracing is very effective spatial subdivision to limit the number of surfaces that have to be tested against each ray. I am not sure if there is a standard, but kD-Trees and variants thereof are being used widely.

To make any of these methods work for quadrics, the computer has to determine if a given axis aligned bounding box overlaps a given quadric surface. A practical algorithm must not miss any quadric surface that does in fact overlap the box. In traditional raytracing, it was tolerable if the algorithm occasionally claimed a hit when a surface did in fact miss the box. In realtime raytracing, we cannot afford such false positives, because they cause unnecessary computation, costing valuable time.

Thanks to the relative simplicity of quadrics, the overlap problem can be solved analytically. This idea has been looming in my mind for quite a while before I could finally prove it (thinking in implicit surfaces is an unusual thing to do for computer graphics). Applying the idea to axis aligned boxes took me more than one attempt, but eventually I arrived at a reasonably concise procedure. The code has survived extensive testing against a brute force implementation, and is currently part of Boar. It has been slightly cleaned up to serve as an illustration here:

/*
  Intersecting an Arbitrary Quadric with an Axis Aligned Box
  (an illustrative code fragment)

  An explanation of the geometry and math behind this algorithm can be found at
  http://www.vectorizer.org/IntersectQuadricTetrahedron.html

  Feel free to send comments, improvements and questions to
  "hobold@vectorizer.org". To my knowledge this algorithm is new, and
  unencumbered by intellectual property rights.

  This code fragment is Copyright 2012 by Holger Bettag, licensed under GPLv2.

  If the GPLv2 is too restrictive for you, contact me. I just want to learn
  about improvements you might find. I am sure we can agree on something that
  won't cost you a dime, and doesn't force all the obligations of the GPLv2 on
  you. The GPLv2 is merely the default.
*/

class Quadric {
public:
  double A, B, C;  // A x^2 + B y^2 + C z^2
  double D, E, F;  // + D yz + E zx + F xy
  double G, H, I;  // + G x + H y + I z
  double J;        // + J
}


// handle apex of a parabola
// internal use
static
inline void refineMinMax1D(double& minval, double& maxval,
            double a, double b, double c,
            double tmin, double tmax) {
  if(a != 0.0) {
    double pt = -b/(2.0*a);
    // clamp to limits
    pt = qmin(pt, tmax);
    pt = qmax(pt, tmin);
    
    double value = (a*pt + b)*pt + c;
    minval = qmin(minval, value);
    maxval = qmax(maxval, value);
  }
  return;
}

// handle apex of a 2D quadric and the two edges u = umin, u = umax and
// the one corner at u = umax, v = vmin
// internal use
static
void refineMinMax2D(double& minval, double& maxval,
                    double a, double b, double c, double d, double e, double f,
                    double umin, double umax, double vmin, double vmax) {

  // check 2D apex point
  double denominator = 4.0*a*b - c*c;
  double value;
  if (denominator != 0.0) {
    double pu = (c*e - 2.0*b*d)/denominator;
    double pv = (c*d - 2.0*a*e)/denominator;
    // clamp to limits
    pu = qmin(pu, umax);
    pu = qmax(pu, umin);
    pv = qmin(pv, vmax);
    pv = qmax(pv, vmin);

    value = (a*pu + c*pv + d)*pu + (b*pv + e)*pv + f;
    minval = qmin(minval, value);
    maxval = qmax(maxval, value);
  }

  // handle border at u = umin
  refineMinMax1D(minval, maxval,
                 b, c*umin + e, (a*umin + d)*umin + f,
                 vmin, vmax);
  // ... and border at u = umax
  // extract coefficients of parabola (for reuse)
  c = c*umax + e;
  a = (a*umax + d)*umax + f;
  refineMinMax1D(minval, maxval, b, c, a, vmin, vmax);
  
  // look at corner at u = umax, v = vmin
  value = (b*vmin + c)*vmin + a;
  minval = qmin(minval, value);
  maxval = qmax(maxval, value);
  
  return;
}

// intersect a quadric with an axis aligned box
//
// returns 2 (VoxInside) if the box is contained in the quadric,
//         1 (VoxOutside) if the box is completely outside the quadric
//         0 (VoxSurface) if the box overlaps the quadric surface
VoxClass Quadric::intersectVoxel(const Vec3& Min, const Vec3& Max) const {

  // handle the coordinate minimum corner and the coordinate maximum corner
  double value = evaluatePoint(Min);
  double minval = evaluatePoint(Max);
  double maxval = qmax(minval, value);
  minval = qmin(minval, value);

  // process the faces and edges of the box
  // (each of the six face calls handles two of the 12 edges and
  // one of the 8 corners; the remaining two corners are already done)
  
  // extract face at z = zmin as 2 dimensional quadric and process it
  // q(u,v) := Q(u,v,zmin)
  refineMinMax2D(minval, maxval,
    A, B, F, E*Min.z + G, D*Min.z + H, (C*Min.z + I)*Min.z + J,
    Min.x, Max.x, Min.y, Max.y);
  
  // q(u,v) := Q(u,v,zmax)
  refineMinMax2D(minval, maxval,
    A, B, F, E*Max.z + G, D*Max.z + H, (C*Max.z + I)*Max.z + J,
    Min.x, Max.x, Min.y, Max.y);
  
  // y = ymin (switched u and v to q(v,ymin,u) for symmetry)
  // q(u,v) := Q(v,ymin,u)
  refineMinMax2D(minval, maxval,
    C, A, E, D*Min.y + I, F*Min.y + G, (B*Min.y + H)*Min.y + J,
    Min.z, Max.z, Min.x, Max.x);

  // q(u,v) := Q(v,ymax,u)
  refineMinMax2D(minval, maxval,
    C, A, E, D*Max.y + I, F*Max.y + G, (B*Max.y + H)*Max.y + J,
    Min.z, Max.z, Min.x, Max.x);

  // x = xmin
  // q(u,v) := Q(xmin,u,v)
  refineMinMax2D(minval, maxval,
    B, C, D, F*Min.x + H, E*Min.x + I, (A*Min.x + G)*Min.x + J,
    Min.y, Max.y, Min.z, Max.z);
  
  // q(u,v) := Q(xmax,u,v)
  refineMinMax2D(minval, maxval,
    B, C, D, F*Max.x + H, E*Max.x + I, (A*Max.x + G)*Max.x + J,
    Min.y, Max.y, Min.z, Max.z);
  
  // find the 3D apex point
  double denominator = 8.0*A*B*C + 2.0*(D*E*F - A*D*D - B*E*E - C*F*F);
  if (denominator != 0.0) {
    double px = (2.0*B*E*I + D*D*G + 2.0*C*F*H - 4.0*B*C*G - D*F*I - D*E*H);
    double py = (E*E*H + 2.0*A*D*I + 2.0*C*F*G - 4.0*A*C*H - D*E*G - E*F*I);
    double pz = (2.0*B*E*G + 2.0*A*D*H + F*F*I - 4.0*A*B*I - E*F*H - D*F*G);
    px /= denominator;
    py /= denominator;
    pz /= denominator;
    
    // clamp position to limits
    px = qmin(px, Max.x);
    px = qmax(px, Min.x);
    py = qmin(py, Max.y);
    py = qmax(py, Min.y);
    pz = qmin(pz, Max.z);
    pz = qmax(pz, Min.z);

    value = evaluatePoint(Vec3(px, py, pz));
    minval = qmin(minval, value);
    maxval = qmax(maxval, value);
  }
  
  return VoxClass(((maxval < 0.0) << 1) + (minval > 0.0)); 
}

// brute force method, for debugging & test reference
VoxClass Quadric::intersectVoxelDBG(const Vec3& Min, const Vec3& Max) const {
  const int size = 100;
  int x,y,z;
  double min = evaluatePoint(Min);
  double max = min;
  double val;
  double fx, fy, fz;

  for (z = size; z >= 0; z--) {
    fz = Min.z + double(z)*(Max.z - Min.z)/double(size);
    for (y = size; y >= 0; y--) {
      fy = Min.y + double(y)*(Max.y - Min.y)/double(size);
      for (x = size; x >= 0; x--) {
        fx = Min.x + double(x)*(Max.x - Min.x)/double(size);

        val = evaluatePoint(Vec3(fx,fy,fz));
        min = qmin(min, val);
        max = qmax(max, val);
      }
    }
  }

  if (min > 0.0)
    return voxOutside;  // positive minimum value => voxel is outside
  
  if (max < 0.0)
    return voxInside;   // negative maximum value => voxel is inside
  
  return voxSurface;    // otherwise voxel must straddle quadric surface
}

I admit it is rather lengthy, and quite a bit of computation is required. But for now I am pretending that the construction of the spatial subdivision structure is not in the critical path.