Skip to content

Lipschitz transform#89

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

Lipschitz transform#89
snowbldr wants to merge 2 commits into
deadsy:masterfrom
snowbldr:lipschitz_transform

Conversation

@snowbldr
Copy link
Copy Markdown
Contributor

Fix Transform3D Lipschitz under-correction for non-rigid matrices

Fixes one of the bugs in #85.

The bug

Transform3D evaluates by applying M⁻¹ to the query point and
forwarding to the inner SDF. The inner SDF returns a distance in the
untransformed space; if M is non-orthonormal (any axis scaling),
that distance is stretched by the largest singular value of M⁻¹:

|sdf_outer(p) − sdf_inner(M⁻¹·p)| ≤ σ_max(M⁻¹) · |p − q|

so Evaluate overstates true 3D distance by up to σ_max(M⁻¹). The
octree marching-cubes renderer's |sdf(center)| ≥ half-diagonal
pruning then skips cubes that contain surface, producing holes.

Pure rotation+translation has σ_max = 1 and the bug doesn't fire
there; it only matters once you compose with Scale3d or any
non-uniform scaling.

The fix

TransformSDF3 grows invStretch float64, set at construction:

sigma2 := m44LinearSigmaMax2(&s.inverse)
s.invStretch = 1
if sigma2 > 1 {
    s.invStretch = 1 / math.Sqrt(sigma2)
}

Evaluate multiplies its result by invStretch so the SDF stays a
valid Lipschitz-1 distance estimator. m44LinearSigmaMax2 computes
σ_max² of the 3×3 linear block via the closed-form symmetric-matrix
eigenvalue formula (Smith 1961) — no iterative SVD on the construction
path.

Diagonal matrices (which pure scaling produces) hit the p1 == 0
short-circuit returning max(a00, a11, a22). Pure rotation gives
σ_max² = 1 so invStretch = 1 (no behavior change).

Tests

render/transform_test.go:

  • Test_Transform3D_Watertight — non-uniform scales {2,2,2},
    {3,1,1}, {1,1,3}, {2,3,4}, {5,1,5} on a unit sphere, octree
    at 80 cells, asserts zero boundary edges.
  • Test_Transform3D_RigidPreservesDistance — pins that a rotated
    sphere's SDF at distance 1 equals 1 to ≤ 1e-9, so a future tightening
    of the σ_max calc can't accidentally apply a < 1 factor to rigid
    transforms (which would be safe but slow).

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 pruning rule wrongly
trusts. Borderline configurations may render holes on x86_64 (FMA /
FTZ rounding differences) but not on Apple Silicon. The conservative
invStretch constant is tight enough to absorb 1-ulp wobble either way.

snowbldr added 2 commits May 8, 2026 19:57
TransformSDF3.Evaluate forwards the inner SDF at M⁻¹·p but doesn't
account for the linear map's stretch. When M is rotation+translation
(orthonormal) σ_max(M⁻¹) = 1 and the SDF stays Lipschitz-1; for any
matrix with non-uniform scaling, σ_max(M⁻¹) > 1 and the result
overstates true 3D distance by that factor. The octree marching-cubes
isEmpty check (|sdf(center)| ≥ half-diagonal) then skips cubes that
contain surface, producing holes.

Adds invStretch = 1/σ_max(M⁻¹_3x3) to TransformSDF3, computed at
construction via a closed-form symmetric-3×3 eigenvalue (Smith 1961).
Pure rotation/translation hits the diagonal short-circuit and gets
σ_max = 1 → invStretch = 1, no behavior change.

render/transform_test.go: watertight test with non-uniform scale
factors {2,2,2}, {3,1,1}, {1,1,3}, {2,3,4}, {5,1,5} on a unit sphere
at 80 cells; plus a rigid-transform distance-preservation pin so a
future tightening can't accidentally apply a < 1 factor to rotations.
…nfigs

3 inner SDFs (sphere, rounded box, cylinder) × 16 transform matrices:

  - pure scale: uniform 2x/3x, anisotropic, extreme ratios (10x, 0.5x)
  - reflection (negative scale on one or all axes)
  - rotation × scale composition (45° z, 30° y, oblique XY)
  - shear (single off-diagonal)
  - translation × scale composition

Plus rigid-transform pin (rotation/translation only must give SDF
exactly 1 at distance 1) across 7 rigid matrices, and an
extruded-inner-SDF watertight pass.

Each combination asserts 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