Tutorial 44: Bounding Volumes and Spatial Queries

CNA — C++ XNA 4.0 reimplementation

CNA implements the full XNA 4.0 set of bounding volumes. They are CPU-side structs used for collision detection, frustum culling, and spatial partitioning — not rendered directly.

BoundingBox — axis-aligned box

#include "Microsoft/Xna/Framework/BoundingBox.hpp"
#include "Microsoft/Xna/Framework/BoundingSphere.hpp"
#include "Microsoft/Xna/Framework/BoundingFrustum.hpp"
using namespace Microsoft::Xna::Framework;

// Construct from min/max corners
BoundingBox box(Vector3(-1, 0, -1), Vector3(1, 2, 1));

// The box has a fixed centre and half-extents
Vector3 centre = (box.Min + box.Max) * 0.5f;
Vector3 half   = (box.Max - box.Min) * 0.5f;

BoundingSphere — sphere defined by centre and radius

BoundingSphere sphere(Vector3(0, 1, 0), 1.5f); // centre, radius

BoundingFrustum — camera view frustum

Matrix view = Matrix::CreateLookAt(
    Vector3(0, 5, 10), Vector3::Zero, Vector3::Up);
Matrix proj = Matrix::CreatePerspectiveFieldOfView(
    MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f);

// Construct from the combined view-projection matrix
BoundingFrustum frustum(view * proj);

Intersects() — between types

BoundingBox   boxA(Vector3(-1,-1,-1), Vector3(1,1,1));
BoundingBox   boxB(Vector3( 0, 0, 0), Vector3(2,2,2));
BoundingSphere sph(Vector3(3, 0, 0), 1.0f);

bool bbOverlap = boxA.Intersects(boxB);    // true (shared region)
bool bsOverlap = boxA.Intersects(sph);     // false (sphere is outside)

// Ray intersection
Ray ray(Vector3(-5, 0, 0), Vector3::Right);
std::optional<float> hit = ray.Intersects(boxA);
if (hit.has_value()) {
    Vector3 hitPoint = ray.Position + ray.Direction * hit.value();
}

Contains() — point and volume tests

Returns ContainmentType::Contains, Intersects, or Disjoint.

Vector3 point(0.5f, 0.5f, 0.5f);

ContainmentType ct = boxA.Contains(point);
// ct == ContainmentType::Contains

ContainmentType ct2 = boxA.Contains(sph);
// ct2 == ContainmentType::Intersects or Disjoint

// Frustum contains test (used for culling)
ContainmentType vis = frustum.Contains(sph);
if (vis == ContainmentType::Disjoint) {
    // object is outside the frustum — skip drawing
}

CreateFromPoints() — fit a volume to geometry

std::vector<Vector3> meshVerts = { /* ... loaded from model ... */ };

BoundingBox   aabb   = BoundingBox::CreateFromPoints(meshVerts);
BoundingSphere bsph  = BoundingSphere::CreateFromPoints(meshVerts);

// Or fit a sphere to an existing box
BoundingSphere fromBox = BoundingSphere::CreateFromBoundingBox(aabb);

Merging bounding volumes

BoundingBox child1(Vector3(-2,-1,-1), Vector3(0,1,1));
BoundingBox child2(Vector3( 1,-1,-1), Vector3(3,1,1));

// Parent AABB encompassing both children
BoundingBox parent = BoundingBox::CreateMerged(child1, child2);
// parent.Min ≈ (-2,-1,-1), parent.Max ≈ (3,1,1)

BoundingSphere s1(Vector3(-2, 0, 0), 1.0f);
BoundingSphere s2(Vector3( 2, 0, 0), 1.0f);
BoundingSphere merged = BoundingSphere::CreateMerged(s1, s2);

Code example: frustum culling with BoundingFrustum and BoundingSphere array

struct SceneObject {
    Vector3       position;
    BoundingSphere bounds;   // in world space
    bool          visible = false;
};

class FrustumCullDemo final : public Game {
public:
    FrustumCullDemo() : graphics_(this) {}

protected:
    void Initialize() override {
        Game::Initialize();
        // Scatter 200 objects at random positions
        std::mt19937 rng(42);
        std::uniform_real_distribution<float> dist(-30.0f, 30.0f);
        objects_.resize(200);
        for (auto& obj : objects_) {
            obj.position = Vector3(dist(rng), 0.0f, dist(rng));
            obj.bounds   = BoundingSphere(obj.position, 1.0f);
        }
    }

    void LoadContent() override {
        effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
        effect_->setVertexColorEnabled(true);
        // ... build sphere mesh into vb_ ...
    }

    void Update(GameTime& gameTime) override {
        float t = static_cast<float>(gameTime.TotalGameTime.TotalSeconds());
        camPos_ = Vector3(20 * std::cos(t * 0.2f), 5,
                          20 * std::sin(t * 0.2f));

        Matrix view = Matrix::CreateLookAt(camPos_, Vector3::Zero, Vector3::Up);
        Matrix proj = Matrix::CreatePerspectiveFieldOfView(
            MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 60.0f);
        BoundingFrustum frustum(view * proj);

        visibleCount_ = 0;
        for (auto& obj : objects_) {
            ContainmentType ct = frustum.Contains(obj.bounds);
            obj.visible = (ct != ContainmentType::Disjoint);
            if (obj.visible) ++visibleCount_;
        }
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color::CornflowerBlue);

        Matrix view = Matrix::CreateLookAt(camPos_, Vector3::Zero, Vector3::Up);
        Matrix proj = Matrix::CreatePerspectiveFieldOfView(
            MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 60.0f);
        effect_->setView(view);
        effect_->setProjection(proj);

        gd.setVertexBuffer(*vb_);
        for (const auto& obj : objects_) {
            if (!obj.visible) continue; // culled
            effect_->setWorld(Matrix::CreateTranslation(obj.position));
            for (auto& pass : effect_->getCurrentTechnique().Passes) {
                pass.Apply();
                gd.DrawPrimitives(PrimitiveType::TriangleList, 0, primitiveCount_);
            }
        }
        // visibleCount_ tells you how many were drawn vs 200 total
        gd.Present();
    }

private:
    GraphicsDeviceManager         graphics_;
    std::unique_ptr<BasicEffect>  effect_;
    std::unique_ptr<VertexBuffer> vb_;
    int                           primitiveCount_ = 0;
    std::vector<SceneObject>      objects_;
    Vector3                       camPos_;
    int                           visibleCount_ = 0;
};