Geometry classes BBox, Plane, BezierCurve
Three small geometric classes that build on vectors: bounding boxes for collision and culling, planes for tests like "which side am I on?", and Bézier curves for smooth paths.
BBox :- axis-aligned bounding boxes
A bounding box is the simplest useful approximation of an object: the smallest axis-aligned box the object fits inside. We can use this for what is know as "broad phase" collision detection where we first test the cheap box before (or instead of) the expensive mesh.
It can be used to quickly test whether a ray (e.g. from a mouse click) intersects with the mesh, without needing to test every vertex.
Creating a BBox
There are two ways to create a BBox depending on how you have the data:
from ncca.ngl import BBox, Vec3
# from a center point and dimensions
box = BBox(center=Vec3(0.0, 0.0, 0.0), width=2.0, height=2.0, depth=2.0)
# FROM min/max extents (note the from_ classmethod naming)
box = BBox.from_extents(-1.0, 1.0, # min_x, max_x
-1.0, 1.0, # min_y, max_y
-1.0, 1.0) # min_z, max_z
Properties
box.center # Vec3 property, no parentheses
box.width # 2.0
box.height # 2.0
box.depth # 2.0
box.min_x, box.max_x # extents on each axis
box.min_y, box.max_y
box.min_z, box.max_z
box.get_vertex_array() # the 8 corner Vec3s handy for drawing the box
box.get_normal_array() # the 6 face normals
Mutation
center, width, height, and depth are settable properties, and
set_extents(...) replaces the extents; the corner vertices and normals
are recalculated for you:
box.center = Vec3(5.0, 0.0, 0.0) # move the box
box.width = 4.0 # widen it
box.set_extents(0.0, 1.0, 0.0, 1.0, 0.0, 1.0)
Worked example — point-in-box and overlap tests
def contains(box: BBox, p: Vec3) -> bool:
return (box.min_x <= p.x <= box.max_x and
box.min_y <= p.y <= box.max_y and
box.min_z <= p.z <= box.max_z)
def overlaps(a: BBox, b: BBox) -> bool:
return (a.min_x <= b.max_x and a.max_x >= b.min_x and
a.min_y <= b.max_y and a.max_y >= b.min_y and
a.min_z <= b.max_z and a.max_z >= b.min_z)
These two tests are the backbone of simple collision detection.
Plane :- an infinite flat surface
A plane divides space in two. Graphics uses planes everywhere: the six faces of a camera's viewing volume (frustum culling), mirrors, floors, clipping.
Creating a Plane
from ncca.ngl import Plane, Vec3
# from three points ON the plane (their winding decides the normal direction)
p = Plane(Vec3(0.0, 0.0, 0.0), Vec3(1.0, 0.0, 0.0), Vec3(0.0, 0.0, -1.0))
# or from a normal and one point
p = Plane()
p.set_normal_point(Vec3(0.0, 1.0, 0.0), Vec3(0.0, 0.0, 0.0)) # the ground plane
# or from the four floats of the plane equation ax + by + cz + d = 0
p = Plane()
p.set_floats(0.0, 1.0, 0.0, 0.0)
p.normal # Vec3 — the plane's unit normal (property)
p.point # Vec3 — a point on the plane
p.d # float — the d of the plane equation
The signed distance from a plane
distance(point) returns how far a point is from the plane, with a
sign: positive on the side the normal points to, negative on the other
side, zero on the plane itself.
ground = Plane()
ground.set_normal_point(Vec3(0.0, 1.0, 0.0), Vec3(0.0, 0.0, 0.0))
ground.distance(Vec3(0.0, 5.0, 0.0)) # 5.0 — above the ground
ground.distance(Vec3(0.0, -2.0, 0.0)) # -2.0 — below it!
Worked example :- is a sphere visible?
Frustum culling in one line per plane: a sphere is completely outside a plane if its center is further than its radius behind it.
def sphere_outside(plane: Plane, center: Vec3, radius: float) -> bool:
return plane.distance(center) < -radius
Run that against the six planes of the camera frustum and you can skip drawing everything the camera cannot see.
BezierCurve :- smooth curves through control points
A Bézier curve turns a handful of control points into a smooth path for camera moves, animation paths, or modelling curved shapes. The curve starts at the first control point, ends at the last, and is pulled towards (but does not touch) the ones in between.
Building and evaluating a curve
from ncca.ngl import BezierCurve, Vec3
curve = BezierCurve()
curve.add_point(Vec3(0.0, 0.0, 0.0)) # add_point accepts a Vec3...
curve.add_point(1.0, 2.0, 0.0) # ...or three floats
curve.add_point(3.0, 2.0, 0.0)
curve.add_point(4.0, 0.0, 0.0)
curve.create_knots() # call once after adding all points
mid = curve.get_point_on_curve(0.5) # Vec3 at the halfway parameter
print(mid) # [2.0, 1.5, 0.0]
get_point_on_curve(u) takes a parameter u from 0.0 (start) to 1.0
(end) and returns the Vec3 on the curve at that parameter.
Worked example :- flying a camera along a path
from ncca.ngl import BezierCurve, Vec3, look_at
path = BezierCurve()
for p in [Vec3(10.0, 2.0, 10.0), Vec3(10.0, 8.0, -10.0),
Vec3(-10.0, 8.0, -10.0), Vec3(-10.0, 2.0, 10.0)]:
path.add_point(p)
path.create_knots()
frames = 300
for frame in range(frames + 1):
u = frame / frames
eye = path.get_point_on_curve(u)
view = look_at(eye, Vec3(0.0, 0.0, 0.0), Vec3(0.0, 1.0, 0.0))
# render the scene with this view matrix...
The camera glides smoothly around the scene, always looking at the origin.
Common mistakes
Mistake 1 — forgetting create_knots(). The curve cannot be evaluated
until the knot vector exists. Add all your points, call create_knots()
once, then evaluate.
Mistake 2 :- treating distance() as always positive. It is a signed
distance :- that sign is the useful part. Use abs() if you only want the
magnitude.
Mistake 3 :- expecting the curve to pass through the middle control points. Bézier curves only touch their first and last points; the middle ones shape the curve.
Next: Utility Functions :- the small helpers you will use everywhere.