Lipschitz scale extrude#92
Open
snowbldr wants to merge 2 commits into
Open
Conversation
Both Evaluate paths un-scale (and un-twist) the query before forwarding
to a 2D SDF. The Jacobian of the inverse map is non-orthonormal — its
2x2 JJᵀ has off-diagonal contributions from ∂/∂z of the linearly-
varying scale factor — so the returned 2D distance overstates true 3D
distance by σ_max(J) > 1. The octree marching-cubes renderer's
|sdf(center)| ≥ half-diagonal pruning then drops cubes that contain
surface, producing holes.
ExtrudeSDF3 grows an invStretch field. For ScaleExtrude3D:
A = s_x² + m_x²·x², C = s_y² + m_y²·y², B² = m_x²·m_y²·x²·y²
λ_max = (A+C)/2 + √((A-C)²/4 + B²)
λ_max is monotone in x², y² and in s_x², s_y², so its max over the
volume is attained at one of the two z endpoints with |x|, |y| at
their bbox extreme.
For ScaleTwistExtrude3D the rotation column adds j13 = m_x·x − k·y·s_y
and j23 = k·x·s_x + m_y·y to the Jacobian. λ_max(x,y) at fixed z is a
convex quadratic form in (x,y), so its max over the 2D bbox is also
attained at a corner. We scan the 2 (z) × 4 (xy corners) extreme cells.
Plain Extrude3D and TwistExtrude3D set invStretch = 1; their separate
Lipschitz issues are out of scope for this PR (TwistExtrude3D is
fixed in a sibling PR).
render/scale_extrude_test.go: watertight tests for both functions
across uniform shrink, anisotropic shrink, and grow scales (plus a
sweep of twist angles for the twisted variant). All assert zero
boundary edges from the octree at 80 cells.
…one configs
5 2D profiles (square, thin rect, rounded box, circle, triangle) ×
ScaleExtrude3D: 9 configs covering identity, modest shrink/grow,
anisotropic, extreme 4× ratios, tall (small m_x) and short (large
m_x) heights.
ScaleTwistExtrude3D: 11 configs covering twist=0 reduction to scale,
modest twist (30/90/180), full and 2-turn rotations, anisotropic
scale + twist, negative twist, short-extrusion stress.
Per-axis Lipschitz bound is exercised across the full range. All
assert zero boundary edges from the octree at cells=80.
b7486b1 to
bd3223b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix ScaleExtrude3D / ScaleTwistExtrude3D Lipschitz under-correction
Fixes one of the bugs in #85.
The bug
Both
Evaluatepaths un-scale (and un-twist) the 3D query beforeforwarding to a 2D SDF. The inverse map's Jacobian is non-orthonormal —
the linearly-varying scale factor contributes off-diagonal entries from
∂/∂z— so the returned 2D distance overstates true 3D distance byσ_max(J) > 1. The octree marching-cubes renderer's|sdf(center)| ≥ half-diagonalpruning then skips cubes that contain surface, producingholes.
For
ScaleExtrude3Dalone the σ_max comes from a 2×2 JJᵀ with diagonalscale entries plus a z-column. For
ScaleTwistExtrude3Dthe rotationcolumn adds shear that couples (x, y) into the z-derivative.
The fix
ExtrudeSDF3grows aninvStretch float64field. BothScaleExtrude3Dand
ScaleTwistExtrude3Dcompute σ_max² at construction and storeinvStretch = 1/√σ²;Evaluatemultiplies by it.ScaleExtrude3D. The 2×2 JJᵀ is
with
λ_max = (A+C)/2 + √((A-C)²/4 + B²). λ_max is monotone inx², y²and in
s_x², s_y², so its maximum over the volume is attained at oneof the two z endpoints with
|x|,|y|at their bbox extremes — checkedexplicitly.
ScaleTwistExtrude3D. The rotation matrix is orthogonal so
σ_maxisinvariant to it; the rotation-free Jacobian's z-column becomes
λ_max(x,y)at fixed z is a convex quadratic form in(x,y), so itsmax over the 2D bbox is also attained at a corner. We scan the
2 z-endpoints × 4 (x,y) corners — 8 evaluations per construction.
Plain
Extrude3DandTwistExtrude3DsetinvStretch = 1here; theirseparate Lipschitz issues are out of scope for this PR (
TwistExtrude3Dis fixed in a sibling PR — same
invStretchfield).Tests
render/scale_extrude_test.go:Test_ScaleExtrude3D_Watertight— uniform shrink (2x, 3x), anisotropicshrink (0.4, 0.7), uniform grow (2x). Octree at 80 cells, zero
boundary edges.
Test_ScaleTwistExtrude3D_Watertight— twist 30°/90°/180°/360° crossedwith shrink + an anisotropic case.
All 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
isEmptypruning rule wronglytrusts. Borderline configurations may render holes on x86_64 (FMA /
FTZ rounding differences) but not on Apple Silicon. The closed-form
λ_max bound has plenty of slack either way.
Dependency note
The
invStretchplumbing onExtrudeSDF3overlaps with the siblingTwistExtrude3DLipschitz fix PR. Whichever lands first sets up thefield; the second needs a trivial rebase to drop the duplicate
declaration. The Scale-specific math here is otherwise independent.