Skip to content

Lipschitz gyroid#90

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

Lipschitz gyroid#90
snowbldr wants to merge 2 commits into
deadsy:masterfrom
snowbldr:lipschitz_gyroid

Conversation

@snowbldr
Copy link
Copy Markdown
Contributor

Fix Gyroid3D Lipschitz under-correction

Fixes one of the bugs in #85.

The bug

Gyroid3D returns the gyroid implicit function

g(p) = sin(kx·x)·cos(ky·y) + sin(ky·y)·cos(kz·z) + sin(kz·z)·cos(kx·x)

which is not a signed distance function — |∇g| is bounded by
√3·max(kᵢ) (tight for isotropic scales), not by 1. The octree
marching-cubes renderer treats Evaluate as a Lipschitz-1 distance
estimator and prunes cubes via |sdf(center)| ≥ half-diagonal. Because
g is up to √3·max(kᵢ) larger than the true distance, the pruning
rule wrongly skips cubes that contain the gyroid surface, producing
holes.

The uniform renderer is fine — it evaluates every cube — so the bug
only shows up under octree.

The fix

GyroidSDF3 grows an invStretch float64 field set at construction:

kMax := math.Max(math.Max(math.Abs(k.X), math.Abs(k.Y)), math.Abs(k.Z))
s.invStretch = 1 / (math.Sqrt(3) * kMax)

Evaluate multiplies its result by invStretch, producing a
Lipschitz-1 scalar field with the same zero level set — the gyroid
surface is unchanged, just the magnitudes scale down enough that
octree pruning becomes sound.

Tests

render/gyroid_test.go clips the (infinite) gyroid with a 10×10×10
box and renders via the octree at 80 cells, asserting zero boundary
edges across four scale configurations:

  • isotropic_2, isotropic_3 — symmetric scales
  • anisotropic_2_3_4 — non-isotropic, exercises kMax selection
  • fine_1 — high-frequency case where kMax is large

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 isEmpty rule wrongly trusts.
The conservative √3·max(kᵢ) bound is loose enough to absorb 1-ulp
FMA/FTZ rounding differences between architectures, so this should
pass equally on x86_64 and Apple Silicon.

snowbldr added 2 commits May 8, 2026 20:00
The gyroid implicit function
    g(p) = sin(kx·x)·cos(ky·y) + sin(ky·y)·cos(kz·z) + sin(kz·z)·cos(kx·x)
is not a true SDF: its gradient has magnitude up to √3·max(kᵢ), so
unrescaled it overstates 3D distance by that factor. The octree
marching-cubes renderer's |sdf(center)| ≥ half-diagonal pruning then
skips cubes that contain the gyroid surface, producing holes.

GyroidSDF3 grows an invStretch field set at construction:

    invStretch = 1 / (√3 · max(|kx|, |ky|, |kz|))

Evaluate multiplies its result by invStretch, giving a Lipschitz-1
scalar field with the same zero level set — safe for octree pruning.

render/gyroid_test.go: watertight tests on box-clipped gyroid at four
scale configurations (isotropic 2 and 3, anisotropic 2-3-4, fine 1) at
80 cells, asserting zero boundary edges.
8 scale configurations (isotropic from 0.7 to 5, plus 3 anisotropic
including an extreme case where one axis dominates kMax) clipped by
a Box3D — exercises the √3·max(kᵢ) bound across the full range of
gyroid frequencies.

Plus a separate test that fixes the gyroid scale and varies the
clipping shape (sphere, rounded box, cylinder, shell-via-difference)
so the gyroid surface meets different boundary geometries.

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