Skip to content

Lipschitz loft#93

Open
snowbldr wants to merge 2 commits into
deadsy:masterfrom
snowbldr:lipschitz_loft
Open

Lipschitz loft#93
snowbldr wants to merge 2 commits into
deadsy:masterfrom
snowbldr:lipschitz_loft

Conversation

@snowbldr
Copy link
Copy Markdown
Contributor

Fix Loft3D Lipschitz under-correction

Fixes one of the bugs in #85.

The bug

Loft3D linearly interpolates between two 2D SDFs along z:

a = Mix(a0(x,y), a1(x,y), k),    k = 0.5·z/h + 0.5
b = |z| − h
d = √(a² + b²)    when outside both axes

The xy gradient of a is a convex combination of the two inner
Lipschitz-1 gradients (so still ≤ 1), but the z gradient grows
with max|a1 − a0| / (2·h_inner) and the outer d = √(a² + b²)
end-cap composition inflates further. Without correction Evaluate
overstates 3D distance and the octree's |sdf(center)| ≥ half-diagonal
pruning skips cubes that contain surface, producing holes.

The fix

LoftSDF3 grows an invStretch float64 field set at construction:

L_z   = max|a1(p) − a0(p)|_{pbb} / (2·h_inner)
L_end² = 1 + L_z·(L_z + √(L_z² + 4))/2
s.invStretch = 1 /L_end²       (clamped to1)

The L_end² ≥ √(1 + L_z²) factor is the joint Lipschitz of the
(a, b) → √(a² + b²) end-cap composition.

max|a1 − a0| is sampled on a 16×16 xy grid plus a Lipschitz-2
between-samples safety term (a0 − a1 is Lipschitz-2 since each
inner SDF is Lipschitz-1). This is conservative; in practice it's
within a few percent of the true max for typical input shapes.

Evaluate multiplies its result by invStretch so the SDF stays
Lipschitz-1.

Tests

render/loft_test.go:

  • small_to_big_box_h2 / small_to_big_box_h5 — Box2D(1×1) →
    Box2D(5×5), short and tall heights (short → larger L_z).
  • box_to_circle — exercises a topology-changing loft.
  • circle_to_circle_growing / circle_to_circle_short — same shape
    family, varying L_z by changing height.

All assert zero boundary edges from the octree at 80 cells. Pass on
macOS.

Architecture-specific note

Same family of bug as the high-taper screw rendering issue from #84
non-1-Lipschitz SDF that the octree's isEmpty rule wrongly trusts.
Borderline configurations may render holes on x86_64 (FMA / FTZ
rounding differences) but not on Apple Silicon. The closed-form L_end
bound has plenty of slack either way.

snowbldr added 2 commits May 8, 2026 20:12
Loft3D linearly interpolates between two 2D SDFs along z. The xy
gradient is a convex combination of the two Lipschitz-1 inner
gradients (so still ≤ 1), but the z gradient grows with
max|a1−a0|/(2·h_inner). The outer d = √(a² + b²) end-cap composition
inflates the Lipschitz constant further. Without correction the result
overstates 3D distance and the octree marching-cubes renderer's
|sdf(center)| ≥ half-diagonal pruning skips cubes that contain
surface, producing holes.

LoftSDF3 grows an invStretch field set at construction:

    L_z = max|a1(p) − a0(p)|_{p ∈ bb} / (2·h_inner)
    L_end² = 1 + L_z·(L_z + √(L_z² + 4))/2
    invStretch = 1/√L_end²    (clamped to ≤ 1)

The L_z² ≥ √(1 + L_z²) inflation factor comes from the joint
Lipschitz of (a, b) → √(a² + b²). The max|a1 − a0| upper bound is
sampled on a 16×16 xy grid plus a Lipschitz-2 between-samples safety
term (a0 − a1 is Lipschitz-2 since each sdf is Lipschitz-1).

render/loft_test.go: watertight tests across small-to-big box loft
(short and tall heights), box-to-circle loft, and growing circle
loft. All assert zero boundary edges from the octree at 80 cells.
15 loft configurations covering:
  - identity (sdf0 == sdf1) — must be a no-op
  - same-shape size change at varied heights (h=5, 2, 0.5)
  - cross-shape lofts (box↔circle, triangle→circle, star↔box, star→circle)
  - extreme size ratio (circle r=0.5 → r=3)
  - thin-shape loft (3×1 box, narrower bbox stresses L_z)
  - extra rounding

9 distinct 2D shapes (boxes from 1×1 to 5×5, thin rect, circles,
triangle, star) so max|a1−a0| varies widely.

All assert zero boundary edges from the octree at cells=80.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant