Skip to content

M2-C3: Box SDF and Half-Extents

Taught: 2026-04-30 Milestone: Phase 1 M2 — Pedestal SDF Result: PASS on retry, after geometric/number-line re-teach (first attempt anchored on the half-extent value as a position)

Why this concept matters here

M2 places a box pedestal under the sphere. The box SDF formula uses half-extents (vector from center to corner) rather than full extents (full dimensions), because the formula's abs(p) - he mirror fold collapses all 8 octants into one — but only if he represents per-axis "reach from center." This concept locks the parametric handle that positions the pedestal correctly: half-extents (1.5, 0.1, 1.5) + center (0, -1.1, 0) = sphere sits exactly tangent on top.

The walkthrough

Analogy

In Houdini, when you drop a Box SOP, the parameter is Size — the full dimensions (width, height, depth). But internally, when Houdini does any symmetric math against that box (bounding tests, deformations, transforms about the center), it converts to half-sizes immediately. Same reason a sphere primitive carries radius, not diameter — symmetric primitives prefer the half-form because every formula simplifies. Box SDF is the same story: the math wants half-extents, not full extents.

Key idea

A box has mirror symmetry across its center along each axis. The trick abs(p) - half_extents collapses all 8 octants into one: it folds the point into the +X+Y+Z corner, and now you only need to think about distance to ONE face per axis. The result vector tells you, per-axis, "how far past that axis's face am I?" — negative means inside the slab, positive means outside. Half-extents are the parametric handle that makes this fold work.

Step 1 — What "extents" means

In bounding-box vocabulary, the extent on an axis is the total span: a 3-meter-wide box has X-extent = 3. Half-extents are exactly what they sound like: the vector from the box center to one corner. A box centered at origin with half-extents (1.5, 0.1, 1.5) spans from (-1.5, -0.1, -1.5) to (+1.5, +0.1, +1.5) — it is 3 units wide, 0.2 tall, 3 deep. A flat slab. A pedestal.

Step 2 — Why the formula uses half-extents specifically

Look at the box SDF:

wgsl
fn sd_box(p: vec3f, he: vec3f) -> f32 {
    let q = abs(p) - he;                      // fold into +octant, signed distance per axis
    let outside = length(max(q, vec3f(0.0))); // Euclidean distance when outside
    let inside  = min(max(q.x, max(q.y, q.z)), 0.0); // signed distance when inside
    return outside + inside;
}

The abs(p) step is the mirror fold — it reflects the point into the all-positive octant. After the fold, the only face that matters on each axis is the positive one, and that face sits at +he.x, +he.y, +he.z. So abs(p) - he directly produces "how far past that face are we?" — one subtract, no branches, no octant logic. If the formula took full extents, every line would need a * 0.5 baked in, which means the same constant gets divided every shader invocation. Half-extents push that division to the CPU once, at construction time.

Step 3 — Reading the formula in two pieces

  • length(max(q, 0)) — when you're outside the box, this is the Euclidean distance to the nearest point on the surface. The max(q, 0) zeros out any axis where you're already inside the slab, so the length only counts axes where you've escaped.
  • min(max(q.x, max(q.y, q.z)), 0) — when you're inside the box, all three components of q are negative, and the largest (least negative) one is the distance to the nearest face. The min(..., 0) clamps this to zero when you're outside, so it doesn't double-count.

The two pieces are mutually exclusive — exactly one is non-zero at any point. Their sum is the signed distance. (Inside negative, outside positive, surface zero.)

Step 4 — Worked numerical example, centered at origin

Use he = (1.5, 0.1, 1.5) — the proposed pedestal half-extents.

Test point p = (0, 0, 0) (dead center, inside):

  • q = abs(p) - he = (-1.5, -0.1, -1.5)
  • max(q, 0) = (0, 0, 0) → outside term = 0
  • max(qx, qy, qz) = max(-1.5, -0.1, -1.5) = -0.1
  • min(-0.1, 0) = -0.1 → inside term = -0.1
  • SDF = -0.1. Reads as "0.1 units inside the nearest face." Top and bottom faces are 0.1 away (he.y), the X/Z faces are 1.5 away. Nearest wins.

Test point p = (2, 0, 0) (outside, +X side):

  • q = (0.5, -0.1, -1.5)
  • max(q, 0) = (0.5, 0, 0) → outside term = 0.5
  • inside term = min(max(0.5, -0.1, -1.5), 0) = min(0.5, 0) = 0
  • SDF = 0.5. You are 0.5 past the +X face.

Test point p = (0, 1, 0) (above the slab):

  • q = (-1.5, 0.9, -1.5)
  • max(q, 0) = (0, 0.9, 0) → outside term = 0.9
  • inside term = 0
  • SDF = 0.9. Top face is at y = +0.1; you're at y = 1, so 0.9 above it.

Step 5 — Translating the box (the standard pattern)

The formula assumes box-centered-at-origin. To place a box at center, translate the input point and call sd_box(p - center, he). Same trick as length(p - sphere_center) - r for spheres.

For the M2 pedestal: center = (0, -1.1, 0), he = (1.5, 0.1, 1.5). The top face sits at center.y + he.y = -1.1 + 0.1 = -1.0. The sphere is at origin with radius 1, so its bottom is at y = -1.0. Tangent. The sphere sits on the pedestal, exactly touching, no gap and no intersection. That tangency is what the M2 acceptance test "pedestal pixel below sphere is lit" needs to be geometrically meaningful — if the pedestal were 0.05 too low, there'd be a visible gap; 0.05 too high, the sphere would clip into it.

Step 6 — Why this matters for M2

Half-extents are the parametric handle for "where the pedestal lives." The acceptance test depends on the pedestal being exactly under the sphere, with its top face at exactly y = -1.0. Three numbers — (1.5, 0.1, 1.5) — define the entire shape; one center vector positions it. Get either wrong and the lit-pixel test either passes for the wrong reason or fails for an off-by-0.1 reason.

The mental model in one sentence

Half-extents are to a box what radius is to a sphere — the symmetric parametric handle that makes the SDF formula collapse to its simplest form via the abs(p) mirror fold.

Explain-back question

Suppose I want the pedestal to sit perfectly tangent under a sphere of radius 0.5 (instead of 1.0), and I keep the half-extents at (1.5, 0.1, 1.5). What pedestal center y-coordinate should I use, and walk me through the geometry — where does the sphere's bottom land, where does the pedestal's top land, and why those two numbers must match?

First attempt — FAIL

User answered:

pedestal center y coordinate you use is +0.1. The spheres bottom should land on the pedestal top, both intersect tangentially at +0.1 along y-axis!

Wrong: with center.y = +0.1, top face is at +0.2 — well above the sphere (which has its bottom at -0.5 with radius 0.5 centered at origin). The user anchored on the half-extent value as if it were a world-space coordinate.

Re-teach — number-line / geometric angle

You nailed the concept (sphere bottom kisses pedestal top, tangentially). The arithmetic slipped. Let me redraw it on a number line so the offsets become visual instead of symbolic.

Analogy: Think of it like dropping a ball onto a table in Houdini. The ball's pivot is at its center, but the contact point is the bottom of the ball. The table's pivot is at its center, but the contact point is the top of the table. To make them touch, you align contact-point to contact-point — NOT pivot to pivot. The pivots are offset from the contacts by their respective half-sizes.

Key idea: Both shapes are positioned by their CENTER, but they touch at their SURFACES. So you have to walk from center → surface for the sphere (subtract radius going down), then from surface → center for the pedestal (subtract half-extent going down). Two hops, both downward.

   y = 0     •─── sphere center  (origin)

             │   ↓ subtract radius 0.5  (hop 1: center → bottom of sphere)

   y = -0.5  •─── sphere bottom  ═══  pedestal TOP  (tangency: same point)

             │   ↓ subtract half_extent.y = 0.1  (hop 2: top face → center of pedestal)

   y = -0.6  •─── pedestal CENTER  ← this is what you pass to sd_box

Two hops down. 0.5 then 0.1. Total = 0.6 below origin. The pedestal center sits at y = −0.6.

Why the +0.1 first answer was wrong: +0.1 is the half-extent value itself, not a position. Half-extents describe the box's size relative to its own center — they aren't a world-space coordinate. The user used the size as if it were a location.

The pattern generalizes:

pedestal_center.y = sphere_center.y − sphere_radius − pedestal_half_extent.y
                    └── hop 0 ──┘     └── hop 1 ──┘   └────── hop 2 ──────┘

Re-test — PASS

User re-answered:

  1. the sphere bottom is at -0.5 = 0 - 0.5
  2. the pedestal top face intersects sphere bottom face
  3. the pedestal center is -0.5 -0.1 = -0.6

Clean PASS. Traced all three hops correctly:

  • Sphere center 0 → sphere bottom −0.5 (subtract radius)
  • Sphere bottom = pedestal top (tangency)
  • Pedestal top −0.5 → pedestal center −0.6 (subtract half-extent)

Conversational walkthroughs of real-time graphics + Rust/WASM concepts.