Where do the missing intersections go?
Drag two conics on a screen. Sometimes you see four crossings. Sometimes two. Sometimes one. Sometimes none. A physics engine doing collision, a vector tool doing clipping, a font rasterizer doing outline cleanup, a ray tracer doing picking — all of them need a stable answer to “how many?” What you see on screen flickers; the math underneath does not.
Bezout’s theorem says the count is always 4 for two conics — counted in the right number system, on the right plane, with the right multiplicity. The widget below is the same one the Bezout module ships with; here it answers a different question: which subset of those 4 is what the screen actually shows?
What the screen sees vs. what is there
Two real curves on a 2D plane. The “visible count” is just the number of points your eye finds at the same pixel — real coordinates, both finite. That is the count a naive collision detector returns by raster sampling, or a CAD engine returns by approximate root-finding. It depends on where the curves happen to be drifting today and is not stable under tiny perturbations.
Try the widget. The preset shows 4 real crossings — visible count agrees with Bezout. The preset drops to 3 visible — turn on show multiplicity and one of those gets a tag. The preset shows 0 visible — turn on show complex and four open dots appear in the side panel. None of the missing intersections were lost. They just left the real-affine plane.
Three places they hide
Bezout’s repair (covered fully in the Bezout module) tells you exactly where the missing crossings went. Three corrections; three places to look.
- Tangency. Two crossings collapsed onto one geometric point — the algebra still says two, but the renderer sees one dot. The bookkeeping is
: a tangent intersection counts as 2.intersection multiplicity - Off the visible plane. The intersection points have complex coordinates — they exist as solutions of the algebra but cannot be plotted in the real grid. Two disjoint circles still intersect at ; the screen has nowhere to put those.
- At infinity. Some intersections live on the line at infinity. Every two distinct circles, no matter where they sit, share the two circular points and — that’s two of their four Bezout intersections, off-screen and complex, accounted for whether you like it or not. The fix is the
.projective plane
Why a graphics engine cares about the stable count
An engine that only watches “visible count” is fragile. Two near-tangent circles can have visible-count 0, 1, or 2 depending on a sub-pixel jitter. A renderer that reports “no intersection” one frame and “two intersections” the next is reporting numerical noise as physics. The fix is to compute the algebraic count — the one Bezout guarantees — and then classify each root.
Practically: solve the y-resultant (a quartic in x), find its 4 complex roots with multiplicity, then label each one as real-affine (visible), complex (off-plane), or at infinity (asymptotic). The visible count is the size of the first bucket. Tangencies are marked by repeated roots, not by “two roots that happen to be very close.” A multiplicity-2 real root and two distinct real roots within ε of each other are different geometric situations that look identical on screen — the algebraic classification keeps them apart.
What the engine actually computes
The pipeline is short. (1)
# Engine pipeline: solve, classify, render. Bezout count = 4 always
# for two conics; the picture only shows the real-affine subset.
import numpy as np
def conic_resultant(c1, c2):
"""y-resultant of two conics → quartic in x. (Same as bezout module.)"""
a1, b1, cc1, d1, e1, k1 = c1
a2, b2, cc2, d2, e2, k2 = c2
P = np.polynomial.Polynomial
A1, A2 = cc1, cc2
B1, B2 = P([e1, b1]), P([e2, b2])
C1, C2 = P([k1, d1, a1]), P([k2, d2, a2])
return (A1*C2 - A2*C1)**2 - (A1*B2 - A2*B1)*(B1*C2 - B2*C1)
def classify(roots, eps=1e-6):
"""Group complex roots into 'real-affine' (visible) and 'complex' (off-plane)."""
visible, off_plane = [], []
for r in roots:
if abs(r.imag) < eps:
visible.append(r.real)
else:
off_plane.append(r)
return visible, off_plane
# Two ellipses, perpendicular major axes — the "general" preset.
c1 = (1/4, 0, 1, 0, 0, -1)
c2 = (1, 0, 1/4, 0, 0, -1)
roots = conic_resultant(c1, c2).roots()
visible, hidden = classify(roots)
len(visible), len(hidden) # → (4, 0) four visible crossings
# Two disjoint ellipses — the "disjoint" preset.
c2_far = (9, 0, 1, 0, -10, 24)
roots2 = conic_resultant(c1, c2_far).roots()
v2, h2 = classify(roots2)
len(v2), len(h2) # → (0, 4) nothing on screen, all complex
# Bezout 4 = 0 visible + 4 hidden. The "disjoint" pair never lost the count;
# the renderer just had no place to draw the imaginary parts.Where this shows up in a graphics stack
The same Bezout-and-classify logic, in different costumes, runs in:
- Collision detection. Two convex shapes (circles, ellipses, capsules) meet how many times? Bezout gives the upper bound; tangency-with-multiplicity detects glancing contact; “0 real, 4 complex” reports clearly disjoint.
- Vector clipping. When you intersect two paths in Illustrator/Figma, the tool computes algebraic intersections of cubic Bezier segments — degree 3 × degree 3 = up to 9 algebraic, classified into visible / out-of-segment / tangent.
- Ray–curve picking. A line (degree 1) meets a conic (degree 2) in 2 algebraic points. The mouse picks the first real-affine one along the ray. If both are complex, the click misses.
- Font outline rasterization. A horizontal scanline (degree 1) crosses a glyph outline (Beziers up to degree 3) in d·1 = d points. Counting those with multiplicity gives the even-odd fill rule for free.
In every case the surface story is “count and classify”; the deep story is Bezout’s theorem and a root-classifier. The shape changes; the count protocol doesn’t.
Real intersections come and go. Bezout’s count never does. What the screen shows is the real-affine subset of a number that doesn’t change. A graphics engine keeps the count and classifies each member; the picture is just one bucket.
The line meets the unit circle in how many points, by Bezout? For what range of are all of those visible (real-affine), and for what range do the missing ones become complex? Sketch one example of each.
Take any two distinct circles. Bezout says they meet 4 times. Yet the picture on screen never shows more than 2 real-affine intersections. Where are the other 2 always? (Hint: the
An engine has two parameters: an angle and a tolerance . As sweeps through a tangent configuration, the visible-count goes 2 → 1 → 0 → 1 → 2. The “tangent” instant is one configuration; the surrounding “near-tangent” instants are different. State the difference algebraically (one root with multiplicity 2 vs two roots within ε), and explain why a robust engine should not dedupe based on raw -distance.
You’re computing intersections of a horizontal line with the cubic . Bezout predicts 1·3 = 3. How many real-affine? Where are the others? (No required for the visible ones; one for each of the others.)