OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / blender / test_auto_bounds.py

132 lines · python ref:
"""
Proves the bake box is fitted to the fractal, not to the probe's rounding.

`auto_bounds` can only resolve an axis to the PROBE's voxel size. When a fractal is
flat - spherical_gasket_base is planar, 2 probe voxels thick out of 64 - both edges of
that axis are rounding artifacts, so the fitted box came out barely thicker than one
probe voxel AND offset by up to one. Measured, the plane ended up at ~88% of the box
height: the bake spent its z resolution on empty space and pressed the fractal against
a face. The arithmetic reproduces it exactly, so it was never random.

That is invisible in review, because a fractal squashed against one side of its box is
still a fractal. It shows only as a bake whose proportions and placement disagree with
the still render.

The fix re-probes collapsed axes inside the fitted box, where the voxel size is smaller
by the ratio of the two spans. A truly planar attractor would refine forever, each pass
halving the thickness toward a zero-size voxel, so refinement stops once an axis is
thinner than one voxel of the largest axis.

SUPERSEDED IN PART: the fit later became mass-weighted - cutting the cumulative density
along each axis instead of percentiling occupied-voxel INDICES - and that centres a
planar fractal on its own. Refinement now buys resolution on a collapsed axis rather
than placement.

Needs a GPU. --factory-startup is REQUIRED, or an enabled add-on has already imported
ifs_volume from the INSTALLED copy and sys.path cannot displace it:

    blender -b --factory-startup -noaudio --python blender/test_auto_bounds.py
"""

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TPL = os.path.join(REPO, "WpfDisplay", "Library", "Templates")

PLANAR = "spherical_gasket_base.ifsjson"    # 2 probe voxels thick in z
SOLID = "nebula_sample.ifsjson"             # fully 3D in all axes
SIZE = 32
DISPATCHES = 10


class AutoBoundsCentring(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        try:
            import gpu
            import numpy  # noqa: F401
            import ifs_volume
        except ImportError as ex:
            raise unittest.SkipTest(f"needs Blender's gpu module ({ex})")
        if not os.path.isdir(TPL):
            raise unittest.SkipTest("templates not found")
        gpu.init()
        cls.shader, cls.library = ifs_volume.build_shader()

    def _occupancy(self, template, refine_passes):
        """Returns the z extent of the baked density, in bake voxels."""
        import gpu
        import numpy as np
        import ifs_document
        import ifs_host
        import ifs_volume

        doc = ifs_document.load(os.path.join(TPL, template), self.library)
        ubos = ifs_volume._document_buffers(doc)
        alias = ifs_host.build_alias_tables(doc.iterators)
        n = len(doc.iterators)
        tex = gpu.types.GPUTexture((n, n), format='RGBA32F',
                                   data=gpu.types.Buffer('FLOAT', len(alias), alias))
        lo, hi, _fit = ifs_volume.auto_bounds(
            self.shader, doc, ubos, tex, refine_passes=refine_passes)
        grid, _colour = ifs_volume.accumulate(self.shader, doc, ubos, tex, SIZE,
                                              (lo, hi), DISPATCHES)
        hit = np.argwhere(grid > 0)                 # columns are (z, y, x)
        self.assertGreater(hit.size, 0, f"{template} produced an empty bake")
        z = hit[:, 0]
        return int(z.min()), int(z.max())

    def _offset(self, lo, hi):
        """How far the fractal's centre sits from the box's, in bake voxels."""
        return abs((lo + hi) / 2.0 - (SIZE - 1) / 2.0)

    def test_a_planar_fractal_is_centred_in_its_box(self):
        """
        The one that matters. Without refinement the gasket's plane sat 20 voxels off
        centre in a 64-voxel bake - two thirds of the way up its own box.
        """
        lo, hi = self._occupancy(PLANAR, refine_passes=2)
        offset = self._offset(lo, hi)
        self.assertLess(offset, SIZE * 0.10,
                        f"the plane sits {offset:.1f} voxels off centre; the box is "
                        "still fitted to the probe's rounding rather than the fractal")

    def test_refinement_does_not_decentre_it(self):
        """
        This asserted the OPPOSITE until the fit became mass-weighted.

        Refinement was introduced because a collapsed axis was fitted at the probe's
        resolution and came out ~20 voxels off centre, so the guard checked that
        refinement was what centred it. Weighting the fit by MASS rather than by
        occupied-voxel count centres it outright - measured, the unrefined offset is
        now 0.0 - so refinement is no longer the mechanism and that guard could only
        fail.

        Refinement still earns its place by tightening a collapsed axis, which buys
        resolution; it simply is not what centres the box any more. What matters here
        is that it does not make placement worse.
        """
        before = self._offset(*self._occupancy(PLANAR, refine_passes=0))
        after = self._offset(*self._occupancy(PLANAR, refine_passes=2))
        self.assertLessEqual(after, max(before, 1.0) + 0.5,
                             f"refinement moved the fractal off centre "
                             f"(before {before:.1f}, after {after:.1f})")

    def test_a_solid_fractal_is_left_alone(self):
        """
        Refinement must not disturb a fractal that already fills its box - no axis
        collapses, so nothing should trigger.
        """
        before = self._occupancy(SOLID, refine_passes=0)
        after = self._occupancy(SOLID, refine_passes=2)
        self.assertEqual(before, after,
                         "refinement altered a fully 3D bake, which it should skip")


if __name__ == "__main__":
    unittest.main(argv=sys.argv[:1], exit=False, verbosity=2)