| … | … | @@ -9,7 +9,6 @@ |
| 9 | 9 | #include "dc_density_grid.h" |
| 10 | 10 | #include "dc_mesher.h" |
| 11 | 11 | |
| 12 | | #include <godot_cpp/classes/os.hpp> |
| 13 | 12 | #include <godot_cpp/variant/utility_functions.hpp> |
| 14 | 13 | |
| 15 | 14 | #include <vector> |
| … | … | @@ -33,7 +32,11 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 33 | 32 | out_geo.instantiate(); |
| 34 | 33 | |
| 35 | 34 | Ref<ProcMesh> mesh = in_geo.is_valid() ? in_geo->get_mesh() : Ref<ProcMesh>(); |
| 36 | | if (mesh.is_null() || mesh->positions.size() == 0 || mesh->indices.size() < 3) { |
| 35 | // `indices` is the CORNER array whenever the mesh carries ngons, so reading |
| 36 | // it three at a time is a triangle list only by accident. Blender's grid, |
| 37 | // circle and cylinder fills all arrive as ngons. |
| 38 | PackedInt32Array tris = mesh.is_valid() ? mesh->triangulated_indices() : PackedInt32Array(); |
| 39 | if (mesh.is_null() || mesh->positions.size() == 0 || tris.size() < 3) { |
| 37 | 40 | UtilityFunctions::push_warning( |
| 38 | 41 | "proc-nodes: Mesh to SDF Grid got no triangles, so there is no " |
| 39 | 42 | "surface to measure distance to. Passing an empty volume through."); |
| … | … | @@ -56,10 +59,96 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 56 | 59 | pos[i * 3 + 1] = (float)mesh->positions[i].y; |
| 57 | 60 | pos[i * 3 + 2] = (float)mesh->positions[i].z; |
| 58 | 61 | } |
| 59 | | std::vector<int> idx(mesh->indices.size()); |
| 60 | | for (int i = 0; i < mesh->indices.size(); i++) { |
| 61 | | idx[i] = mesh->indices[i]; |
| 62 | // ⛔ DROP THE SLIVER TRIANGLES, AND NOT ONLY THE EXACTLY DEGENERATE ONES. |
| 63 | // Blender's uv-sphere cap is a QUAD with the pole vertex written twice, so |
| 64 | // fanning it about corner 0 leaves one zero-area triangle per segment per |
| 65 | // pole. libdc takes a face normal as normalize((v1-v0)x(v2-v0)) and its |
| 66 | // normalize() only zeroes out below 1e-12 -- so what it does with the sliver |
| 67 | // depends on whether the two pole copies are bit-identical: |
| 68 | // |
| 69 | // north pole (0, 1, 0) exactly, twice |cross| = 0 -> (0,0,0) |
| 70 | // south pole (-8.7e-08, -1, -0) and |
| 71 | // (-8.6e-08, -1, -1.7e-08) |cross| = 3.3e-09 -> a UNIT |
| 72 | // vector of pure rounding noise |
| 73 | // |
| 74 | // and the angle that noise normal is weighted by, at a corner of a sliver, is |
| 75 | // nearly pi -- against ~0.038 rad for the real cap triangle beside it. So the |
| 76 | // noise WINS the vertex pseudonormal: measured 2026-09-06, the south pole's |
| 77 | // pseudonormal came out (-0.108, +0.994, +0.001), pointing INTO the sphere, |
| 78 | // and the field below the south pole read +0.75/+0.50/+0.25 where it should |
| 79 | // read -0.75/-0.50/-0.25. The north pole, whose copies are bit-identical, was |
| 80 | // correct. That asymmetry is the whole tell. |
| 81 | // |
| 82 | // The test is the triangle's HEIGHT -- twice the area over its longest edge -- |
| 83 | // against the mesh's own size, so it says "this has no thickness, therefore no |
| 84 | // normal" at any scale. The same argument applies to libdc's own loaders, |
| 85 | // which is where this would belong if that library were only ours to change. |
| 86 | Vector3 bb_min = mesh->positions[0]; |
| 87 | Vector3 bb_max = mesh->positions[0]; |
| 88 | for (int i = 1; i < vcount; i++) { |
| 89 | bb_min = bb_min.min(mesh->positions[i]); |
| 90 | bb_max = bb_max.max(mesh->positions[i]); |
| 62 | 91 | } |
| 92 | const double min_height = (double)(bb_max - bb_min).length() * 1e-6; |
| 93 | |
| 94 | std::vector<int> idx; |
| 95 | idx.reserve(tris.size()); |
| 96 | for (int i = 0; i + 2 < tris.size(); i += 3) { |
| 97 | const Vector3 &a = mesh->positions[tris[i]]; |
| 98 | const Vector3 &b = mesh->positions[tris[i + 1]]; |
| 99 | const Vector3 &c = mesh->positions[tris[i + 2]]; |
| 100 | const double twice_area = (double)(b - a).cross(c - a).length(); |
| 101 | const double longest = (double)MAX(MAX((b - a).length(), (c - b).length()), |
| 102 | (a - c).length()); |
| 103 | if (longest <= 0.0 || twice_area / longest < min_height) { |
| 104 | continue; |
| 105 | } |
| 106 | idx.push_back(tris[i]); |
| 107 | idx.push_back(tris[i + 1]); |
| 108 | idx.push_back(tris[i + 2]); |
| 109 | } |
| 110 | if (idx.size() < 3) { |
| 111 | UtilityFunctions::push_warning( |
| 112 | "proc-nodes: Mesh to SDF Grid got no triangle with any area. " |
| 113 | "Passing an empty volume through."); |
| 114 | out_geo->set_volume(Ref<ProcVolume>(memnew(ProcVolume))); |
| 115 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | // ⛔ THE WINDING DECIDES THE SIGN, and proc-nodes' primitives do not agree on |
| 120 | // one. libdc calls the side its pseudonormals point to the OUTSIDE, so its |
| 121 | // stated convention -- positive = solid -- holds only for a CLOCKWISE mesh, |
| 122 | // which is Godot's. Measured 2026-09-06 as the enclosed volume under that same |
| 123 | // cross product: cube -1.000 and cylinder -6.243 (clockwise), uv sphere +4.122 |
| 124 | // and ico sphere +4.047 (counter-clockwise, because they carry Blender's |
| 125 | // faces). The same call therefore returns distance-outside as POSITIVE for one |
| 126 | // and distance-inside as positive for the other, and a unit sphere came out |
| 127 | // inside out -- 3112 of 3375 samples "solid", most solid at the box corners -- |
| 128 | // with the extractor dutifully meshing the box. |
| 129 | // |
| 130 | // A closed mesh's own enclosed volume says which it is: sum v0 . (v1 x v2) is |
| 131 | // positive exactly when that cross product points out of it. Deriving the sign |
| 132 | // per mesh is what makes this node winding-independent, which it has to be: |
| 133 | // the graphs that reach it come from Blender. |
| 134 | double signed_volume = 0.0; |
| 135 | for (size_t t = 0; t + 2 < idx.size(); t += 3) { |
| 136 | const Vector3 &a = mesh->positions[idx[t]]; |
| 137 | const Vector3 &b = mesh->positions[idx[t + 1]]; |
| 138 | const Vector3 &c = mesh->positions[idx[t + 2]]; |
| 139 | signed_volume += (double)a.dot(b.cross(c)); |
| 140 | } |
| 141 | signed_volume /= 6.0; |
| 142 | const float sdf_sign = (signed_volume > 0.0) ? -1.0f : 1.0f; |
| 143 | if (Math::abs(signed_volume) < 1e-9) { |
| 144 | // Flat or open: there is no inside, so a "signed" distance is only the |
| 145 | // side the faces happen to point. Say so rather than pretend. |
| 146 | UtilityFunctions::push_warning( |
| 147 | "proc-nodes: Mesh to SDF Grid got a mesh enclosing no volume (flat " |
| 148 | "or not closed). The sign of the field is then whichever side the " |
| 149 | "faces point, and the extracted surface will follow it."); |
| 150 | } |
| 151 | |
| 63 | 152 | DCGeneratorMeshSDF sdf; |
| 64 | 153 | if (!sdf.load_arrays(pos.data(), vcount, idx.data(), (int)(idx.size() / 3))) { |
| 65 | 154 | UtilityFunctions::push_error( |
| … | … | @@ -99,7 +188,7 @@ void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 99 | 188 | for (int z = 0; z < res[2]; z++) { |
| 100 | 189 | for (int y = 0; y < res[1]; y++) { |
| 101 | 190 | for (int x = 0; x < res[0]; x++) { |
| 102 | | w[(z * res[1] + y) * res[0] + x] = sdf.evaluate( |
| 191 | w[(z * res[1] + y) * res[0] + x] = sdf_sign * sdf.evaluate( |
| 103 | 192 | mn[0] + x * voxel, mn[1] + y * voxel, mn[2] + z * voxel, voxel); |
| 104 | 193 | } |
| 105 | 194 | } |
| … | … | @@ -236,29 +325,19 @@ void GridToMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 236 | 325 | } |
| 237 | 326 | grid.compute_normals(); |
| 238 | 327 | |
| 239 | | // ⛔ DISABLED: dc_mesh_from_grid HANGS ON THIS GRID AND I DO NOT KNOW WHY. |
| 328 | // ⛔ IT NEVER HUNG. `demo/sdf_grid_gate.gd` called `vertex_count()` and |
| 329 | // `triangle_count()`, whose BOUND names are `get_vertex_count` / |
| 330 | // `get_triangle_count`, and `m.positions`, which is a C++ member and not a |
| 331 | // property at all. A script error aborts `_init` before it reaches `quit()`, |
| 332 | // and a headless SceneTree with nothing to quit it runs for ever. Disabling |
| 333 | // this node "fixed" the hang only because the empty geometry it wrote made |
| 334 | // `m` null, so the bad call was short-circuited and never evaluated -- which |
| 335 | // is also why 32^3 "hung identically" and why bypassing the fillet changed |
| 336 | // nothing. Extraction takes 0.375 s end to end for the gate's 15^3 grid. |
| 240 | 337 | // |
| 241 | | // Reproduced 2026-09-06 with demo/sdf_grid_gate.gd: a unit UV sphere -> |
| 242 | | // Mesh to SDF Grid (voxel 0.25, a 15^3 grid) -> Grid to Mesh never returns. |
| 243 | | // Not slowness -- 15^3 is 3375 samples -- and not the fillet, which bypassing |
| 244 | | // does not help. Snapping the cell count to a power of two, on the theory |
| 245 | | // that construct_octree_grid_pooled needs one (dc-terrain only ever passes |
| 246 | | // 32), did not fix it either. Cause unknown. |
| 247 | | // |
| 248 | | // It REFUSES rather than hangs, because a node that locks the editor the |
| 249 | | // moment someone drops it into a graph is worse than one that says it is not |
| 250 | | // finished. Mesh to SDF Grid works and is verified; this is the half that is |
| 251 | | // not. Delete this block and the gate is the reproduction. |
| 252 | | if (OS::get_singleton()->get_environment("PROC_NODES_GRID_TO_MESH") != "1") { |
| 253 | | UtilityFunctions::push_error( |
| 254 | | "proc-nodes: Grid to Mesh is DISABLED -- extraction hangs on a " |
| 255 | | "ProcVolume grid and the cause is not yet known. The node exists " |
| 256 | | "and maps so a Blender graph imports without losing nodes, but it " |
| 257 | | "produces nothing. Set PROC_NODES_GRID_TO_MESH=1 to run it anyway " |
| 258 | | "(it will hang). See demo/sdf_grid_gate.gd."); |
| 259 | | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 260 | | return; |
| 261 | | } |
| 338 | // What WAS broken was the field going in, in two ways that only bit the |
| 339 | // extractor: the sign, and the sliver triangles at the sphere's south pole. |
| 340 | // Both are fixed in Mesh to SDF Grid above, and both are documented there. |
| 262 | 341 | |
| 263 | 342 | DCMesherConfig cfg; |
| 264 | 343 | DCMeshData md = dc_mesh_from_grid(grid, cfg); |