OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 9aa63da

sdf: sign the band from the flood, not from pseudonormals -- 9.6 s -> 3.6 s
Mesh bit-identical: 2,582,018 verts, x -20..20 y -2..23.765 z -20..20, sum(z)=1792386.7043, sumsq(z)=28382638.9998 -- the same digits as every step since the dense build. load_arrays (pseudonormals + bvh) 7306 ms -> 1445 ms MeshToSDFGrid 9652 ms -> 3630 ms whole bake 23 s -> 17 s The band now takes UNSIGNED distance and this node applies the sign it already had. The barrier -- voxels within half a voxel of the surface -- is the only place a sign is genuinely ambiguous at grid resolution, and it takes the side of a labelled neighbour, preferring outside so a voxel touching air is air rather than closing the surface over a thin gap. Everything else is more than half a voxel from the surface and the flood labels it exactly. ⚠ Gated on a CLOSED mesh. The flood is the sign; a hole leaks and an open mesh keeps the pseudonormal path. This is the fourth attempt at this node's cost and the first one aimed at the right thing. The three before -- threading the BVH build, precomputing the split comparator, replacing the whole builder with an LBVH -- were all aimed at a timer that bracketed load_arrays rather than build_bvh, so they optimised a fifth of the cost and returned 1.42x, 0.4% and nothing. The lesson is the same one this repo keeps paying for: instrument at the granularity of the thing you are about to change, BEFORE changing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9aa63dab6ead30731fb588369532961e2034ee1f
by plankatron <fleeky@gmail.com> · 2026-09-08T17:41:18.000Z

Changed files (1) +69 -10

src/nodes/volume_grid_nodes.cpp +69 -10
……@@ -150,6 +150,10 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
150150 }
151151 signed_volume /= 6.0;
152152 const float sdf_sign = (signed_volume > 0.0) ? -1.0f : 1.0f;
153 // Closed means the flood from the grid boundary is a trustworthy sign, which
154 // is what lets the pseudonormals go. Decided here because load_arrays has to
155 // know before it builds anything.
156 const bool closed_precheck = Math::abs(signed_volume) >= 1e-9;
153157 if (Math::abs(signed_volume) < 1e-9) {
154158 // Flat or open: there is no inside, so a "signed" distance is only the
155159 // side the faces happen to point. Say so rather than pretend.
……@@ -161,6 +165,17 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
161165
162166 const uint64_t t_setup = Time::get_singleton()->get_ticks_usec();
163167 DCGeneratorMeshSDF sdf;
168 // ⛔ NO PSEUDONORMALS. They exist only to sign a query, and they cost 5554 ms
169 // of a 6663 ms load_arrays here -- five times the BVH build -- because they
170 // need per-vertex AND per-EDGE angle-weighted normals over 5.16M triangles.
171 // This node already determines inside from outside globally: it floods the
172 // outside from the grid boundary, which is outside by construction because
173 // the bounds are padded. Asking for unsigned distance and applying that sign
174 // is the same answer without the 5.5 s.
175 //
176 // Only for a CLOSED mesh -- the flood is what supplies the sign, and it
177 // leaks through a hole. An open mesh keeps the pseudonormal path.
178 sdf.set_unsigned_only(closed_precheck);
164179 if (!sdf.load_arrays(pos.data(), vcount, idx.data(), (int)(idx.size() / 3))) {
165180 UtilityFunctions::push_error(
166181 "proc-nodes: Mesh to SDF Grid could not build a BVH for this mesh.");
……@@ -225,7 +240,7 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
225240 const uint64_t t_bvh = Time::get_singleton()->get_ticks_usec();
226241 const uint64_t t_start = t_bvh;
227242 uint64_t t_stamp = t_start, t_dilate = t_start, t_query = t_start;
228 const bool closed = Math::abs(signed_volume) >= 1e-9;
243 const bool closed = closed_precheck;
229244 const int bandv = MAX(1, (int)band);
230245
231246 std::vector<uint8_t> in_band;
……@@ -324,9 +339,11 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
324339 if (banded && !in_band[(size_t)o]) {
325340 continue; // filled by the flood below
326341 }
327 w[o] = sdf_sign * sdf.evaluate(
328 mn[0] + x * voxel, mn[1] + y * voxel,
329 mn[2] + z * voxel, voxel);
342 const float d = sdf.evaluate(mn[0] + x * voxel,
343 mn[1] + y * voxel, mn[2] + z * voxel, voxel);
344 // banded => unsigned; the flood signs it. Otherwise
345 // libdc signed it with pseudonormals as before.
346 w[o] = banded ? d : (sdf_sign * d);
330347 }
331348 }
332349 }
……@@ -356,14 +373,27 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
356373 // `pad` above, so the whole grid boundary is outside the mesh and a flood
357374 // from it reaches exactly the outside.
358375 if (closed && !in_band.empty()) {
359 // positive = solid after sdf_sign (see the winding note above)
376 // positive = solid after this pass (see the winding note above)
360377 const float far_v = voxel * (float)(bandv + 1);
378
379 // THE BARRIER is the shell the surface actually lies in: voxels within
380 // half a voxel of it. Everything else is unambiguously one side or the
381 // other at this resolution, so a flood from the grid boundary -- which
382 // is outside, because the bounds are padded -- labels it exactly.
383 //
384 // Only the barrier itself is decided by a neighbour, and a shell one
385 // voxel thick is precisely the width at which a grid cannot tell the
386 // two sides apart anyway.
387 const float half = voxel * 0.5f;
361388 std::vector<uint8_t> outside((size_t)total, 0);
362389 std::vector<int64_t> stack;
363390 stack.reserve(4096);
391 auto passable = [&](int64_t o) {
392 return !in_band[(size_t)o] || w[o] >= half;
393 };
364394 auto push_if_open = [&](int64_t x, int64_t y, int64_t z) {
365395 const int64_t o = at(x, y, z);
366 if (in_band[(size_t)o] || outside[(size_t)o]) {
396 if (outside[(size_t)o] || !passable(o)) {
367397 return;
368398 }
369399 outside[(size_t)o] = 1;
……@@ -391,11 +421,40 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
391421 if (z > 0) push_if_open(x, y, z - 1);
392422 if (z < nz - 1) push_if_open(x, y, z + 1);
393423 }
394 for (int64_t o = 0; o < total; o++) {
395 if (in_band[(size_t)o]) {
396 continue;
424
425 for (int64_t z = 0; z < nz; z++) {
426 for (int64_t y = 0; y < ny; y++) {
427 for (int64_t x = 0; x < nx; x++) {
428 const int64_t o = at(x, y, z);
429 if (!in_band[(size_t)o]) {
430 w[o] = outside[(size_t)o] ? -far_v : far_v;
431 continue;
432 }
433 const float d = w[o];
434 if (d >= half) {
435 // Outside the barrier: the flood knows the side.
436 w[o] = outside[(size_t)o] ? -d : d;
437 continue;
438 }
439 // In the barrier. Take the side from a labelled neighbour;
440 // prefer an OUTSIDE one, so a voxel touching air is air --
441 // the alternative closes the surface over thin gaps.
442 bool any_out = false, any_in = false;
443 const int64_t nbr[6][3] = {{x - 1, y, z}, {x + 1, y, z}, {x, y - 1, z},
444 {x, y + 1, z}, {x, y, z - 1}, {x, y, z + 1}};
445 for (int k = 0; k < 6; k++) {
446 const int64_t bx = nbr[k][0], by = nbr[k][1], bz = nbr[k][2];
447 if (bx < 0 || bx >= nx || by < 0 || by >= ny || bz < 0 || bz >= nz) {
448 continue;
449 }
450 const int64_t no = at(bx, by, bz);
451 if (!in_band[(size_t)no] || w[no] >= half) {
452 if (outside[(size_t)no]) any_out = true; else any_in = true;
453 }
454 }
455 w[o] = (any_out && !any_in) ? -d : (any_in && !any_out ? d : -d);
456 }
397457 }
398 w[o] = outside[(size_t)o] ? -far_v : far_v;
399458 }
400459 }
401460