| … | … | @@ -160,13 +160,27 @@ def auto_bounds(shader, doc, ubos, alias_tex, probe_size=64, dispatches=8, |
| 160 | 160 | continue |
| 161 | 161 | |
| 162 | 162 | edge = ((hit == 0) | (hit == probe_size - 1)).any(axis=1).mean() |
| 163 | | if edge <= 0.01 or extent >= max_extent: |
| 163 | |
| 164 | # Grow while the BODY is clipped, not while stragglers touch the edge. |
| 165 | # |
| 166 | # The old test grew whenever >1% of occupied voxels sat on the boundary. A |
| 167 | # chaos game throws samples arbitrarily far, so for a heavy-tailed document |
| 168 | # that fraction never falls: growing the box just captures more outliers. |
| 169 | # light_rods_sample doubled all the way to the max_extent of 256 and still |
| 170 | # reported 4.3% on the boundary, so it fitted a 655-unit box - roughly 6.8 |
| 171 | # units per voxel at size 96, smearing the fractal into a featureless blob. |
| 172 | # |
| 173 | # The percentile fit below already excludes those outliers deliberately; the |
| 174 | # growth decision simply was not using it. Now it asks the same question the |
| 175 | # fit does - is the 99% body inside the probe? - so the loop stops once the |
| 176 | # part we are actually going to keep fits. |
| 177 | body_lo, body_hi = _mass_bounds(density, probe_size, percentile) |
| 178 | clipped = bool((body_lo <= 0.5).any() or (body_hi >= probe_size - 1.5).any()) |
| 179 | if not clipped or extent >= max_extent: |
| 164 | 180 | break |
| 165 | 181 | extent *= 2.0 |
| 166 | | |
| 167 | | clipped = edge > 0.01 |
| 168 | 182 | box = ((-extent, -extent, -extent), (extent, extent, extent)) |
| 169 | | lo_w, hi_w = _fit(hit, box, probe_size, percentile, margin) |
| 183 | lo_w, hi_w = _fit(density, box, probe_size, percentile, margin) |
| 170 | 184 | |
| 171 | 185 | # Re-probe any axis that collapsed to a handful of probe voxels. |
| 172 | 186 | # |
| … | … | @@ -182,7 +196,8 @@ def auto_bounds(shader, doc, ubos, alias_tex, probe_size=64, dispatches=8, |
| 182 | 196 | # toward a zero-size voxel, so this stops when an axis is thinner than one voxel |
| 183 | 197 | # of the LARGEST axis: past that it is planar in any sense the bake can represent. |
| 184 | 198 | for _ in range(refine_passes): |
| 185 | | occupied = [len(np.unique(hit[:, 2 - axis])) for axis in range(3)] |
| 199 | occupied = [int((density.sum(axis=tuple(a for a in range(3) |
| 200 | if a != 2 - axis)) > 0).sum()) for axis in range(3)] |
| 186 | 201 | span = hi_w - lo_w |
| 187 | 202 | floor = float(span.max()) / probe_size |
| 188 | 203 | collapsed = [a for a in range(3) |
| … | … | @@ -201,20 +216,49 @@ def auto_bounds(shader, doc, ubos, alias_tex, probe_size=64, dispatches=8, |
| 201 | 216 | |
| 202 | 217 | finer = accumulate(shader, doc, ubos, alias_tex, probe_size, |
| 203 | 218 | (probe_lo, probe_hi), dispatches) |
| 204 | | finer_hit = np.argwhere(finer[:, :, :, 3] > 0) |
| 205 | | if finer_hit.size == 0: |
| 219 | finer_density = finer[:, :, :, 3] |
| 220 | if not (finer_density > 0).any(): |
| 206 | 221 | break # the tighter box found nothing; keep what we had |
| 207 | 222 | |
| 208 | 223 | box = (probe_lo, probe_hi) |
| 209 | | hit = finer_hit |
| 210 | | lo_w, hi_w = _fit(hit, box, probe_size, percentile, margin) |
| 224 | density = finer_density |
| 225 | lo_w, hi_w = _fit(density, box, probe_size, percentile, margin) |
| 211 | 226 | |
| 212 | 227 | return (tuple(float(v) for v in lo_w), tuple(float(v) for v in hi_w), |
| 213 | 228 | {"probe_extent": extent, "edge_fraction": float(edge), |
| 214 | 229 | "clipped": bool(clipped)}) |
| 215 | 230 | |
| 216 | 231 | |
| 217 | | def _fit(hit, box, probe_size, percentile, margin): |
| 232 | def _mass_bounds(density, probe_size, percentile): |
| 233 | """ |
| 234 | Index bounds holding the central `percentile` of the fractal's MASS, per world axis. |
| 235 | |
| 236 | The fit used to percentile over `np.argwhere(density > 0)` - the INDICES of occupied |
| 237 | voxels - which counts a voxel holding one sample the same as one holding a million. |
| 238 | A chaos game's halo is enormous in voxel count and negligible in mass, so that |
| 239 | statistic tracked the halo rather than the fractal. light_rods_sample grew its probe |
| 240 | to the 256 cap and still reported the body clipped, fitting a 655-unit box. |
| 241 | |
| 242 | Summing the marginal along each axis and cutting the cumulative distribution asks |
| 243 | where the mass is instead, which is what the bake should spend its resolution on. |
| 244 | """ |
| 245 | lo_idx, hi_idx = [], [] |
| 246 | for array_axis in (2, 1, 0): # array is [z][y][x]; iterate world x, y, z |
| 247 | others = tuple(a for a in range(3) if a != array_axis) |
| 248 | marginal = density.sum(axis=others) |
| 249 | total = float(marginal.sum()) |
| 250 | if total <= 0: |
| 251 | lo_idx.append(0.0) |
| 252 | hi_idx.append(float(probe_size - 1)) |
| 253 | continue |
| 254 | cumulative = np.cumsum(marginal) / total |
| 255 | tail = (100.0 - percentile) / 100.0 |
| 256 | lo_idx.append(float(np.searchsorted(cumulative, tail))) |
| 257 | hi_idx.append(float(np.searchsorted(cumulative, 1.0 - tail))) |
| 258 | return np.array(lo_idx), np.array(hi_idx) |
| 259 | |
| 260 | |
| 261 | def _fit(density, box, probe_size, percentile, margin): |
| 218 | 262 | """ |
| 219 | 263 | Percentile-fits world bounds to probe hits, for an ARBITRARY probe box. |
| 220 | 264 | |
| … | … | @@ -223,10 +267,10 @@ def _fit(hit, box, probe_size, percentile, margin): |
| 223 | 267 | an argument so a refinement pass can re-fit inside a tighter one. |
| 224 | 268 | """ |
| 225 | 269 | lo, hi = np.array(box[0], dtype=float), np.array(box[1], dtype=float) |
| 226 | | zyx_lo = np.percentile(hit, 100 - percentile, axis=0) |
| 227 | | zyx_hi = np.percentile(hit, percentile, axis=0) |
| 228 | | lo_n = np.array([zyx_lo[2], zyx_lo[1], zyx_lo[0]]) / probe_size |
| 229 | | hi_n = np.array([zyx_hi[2], zyx_hi[1], zyx_hi[0]]) / probe_size |
| 270 | # Already in world-axis order (x, y, z) - _mass_bounds does the reordering. |
| 271 | lo_idx, hi_idx = _mass_bounds(density, probe_size, percentile) |
| 272 | lo_n = lo_idx / probe_size |
| 273 | hi_n = hi_idx / probe_size |
| 230 | 274 | |
| 231 | 275 | lo_w = lo + lo_n * (hi - lo) |
| 232 | 276 | hi_w = lo + hi_n * (hi - lo) |