| … | … | @@ -0,0 +1,297 @@ |
| 1 | #include "volume_grid_nodes.h" |
| 2 | |
| 3 | #include "../core/proc_geometry.h" |
| 4 | #include "../core/proc_mesh.h" |
| 5 | #include "../core/proc_volume.h" |
| 6 | #include "../graph/graph_executor.h" |
| 7 | |
| 8 | #include "dc_generator_mesh_sdf.h" |
| 9 | #include "dc_density_grid.h" |
| 10 | #include "dc_mesher.h" |
| 11 | |
| 12 | #include <godot_cpp/classes/os.hpp> |
| 13 | #include <godot_cpp/variant/utility_functions.hpp> |
| 14 | |
| 15 | #include <vector> |
| 16 | |
| 17 | namespace godot { |
| 18 | |
| 19 | // ── Mesh to SDF Grid ──────────────────────────────────────────────────────── |
| 20 | |
| 21 | void MeshToSDFGridNode::_bind_methods() {} |
| 22 | |
| 23 | MeshToSDFGridNode::MeshToSDFGridNode() { |
| 24 | input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() }); // 0 |
| 25 | input_defs.push_back({ "Voxel Size", SocketType::Float, 0.1f }); // 1 |
| 26 | input_defs.push_back({ "Band Width", SocketType::Float, 3.0f }); // 2 |
| 27 | output_defs.push_back({ "Volume", SocketType::Geometry, Variant() }); |
| 28 | } |
| 29 | |
| 30 | void MeshToSDFGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 31 | Ref<ProcGeometry> in_geo = p_ctx.read_geometry(p_node_idx, 0); |
| 32 | Ref<ProcGeometry> out_geo; |
| 33 | out_geo.instantiate(); |
| 34 | |
| 35 | 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) { |
| 37 | UtilityFunctions::push_warning( |
| 38 | "proc-nodes: Mesh to SDF Grid got no triangles, so there is no " |
| 39 | "surface to measure distance to. Passing an empty volume through."); |
| 40 | out_geo->set_volume(Ref<ProcVolume>(memnew(ProcVolume))); |
| 41 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 42 | return; |
| 43 | } |
| 44 | |
| 45 | float voxel = p_ctx.read_float(p_node_idx, 1); |
| 46 | if (voxel <= 0.0f) voxel = 0.1f; |
| 47 | float band = p_ctx.read_float(p_node_idx, 2); |
| 48 | if (band < 0.0f) band = 0.0f; |
| 49 | |
| 50 | // libdc's evaluator, not a second one: the pseudonormal method with a BVH |
| 51 | // (Baerentzen & Aanaes), already used by dc-terrain's mesh-SDF generator. |
| 52 | const int vcount = mesh->positions.size(); |
| 53 | std::vector<float> pos((size_t)vcount * 3); |
| 54 | for (int i = 0; i < vcount; i++) { |
| 55 | pos[i * 3 + 0] = (float)mesh->positions[i].x; |
| 56 | pos[i * 3 + 1] = (float)mesh->positions[i].y; |
| 57 | pos[i * 3 + 2] = (float)mesh->positions[i].z; |
| 58 | } |
| 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 | } |
| 63 | DCGeneratorMeshSDF sdf; |
| 64 | if (!sdf.load_arrays(pos.data(), vcount, idx.data(), (int)(idx.size() / 3))) { |
| 65 | UtilityFunctions::push_error( |
| 66 | "proc-nodes: Mesh to SDF Grid could not build a BVH for this mesh."); |
| 67 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 68 | return; |
| 69 | } |
| 70 | |
| 71 | float mn[3], mx[3]; |
| 72 | if (!sdf.get_bounds(mn, mx)) { |
| 73 | UtilityFunctions::push_error("proc-nodes: Mesh to SDF Grid got no bounds."); |
| 74 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 75 | return; |
| 76 | } |
| 77 | // The band is the margin the field is sampled OUTSIDE the mesh, in voxels. |
| 78 | // Without it the surface sits exactly on the grid edge and the extractor has |
| 79 | // no outside to find a crossing against. |
| 80 | const float pad = voxel * band; |
| 81 | for (int a = 0; a < 3; a++) { mn[a] -= pad; mx[a] += pad; } |
| 82 | |
| 83 | int res[3]; |
| 84 | for (int a = 0; a < 3; a++) { |
| 85 | res[a] = (int)((mx[a] - mn[a]) / voxel) + 1; |
| 86 | if (res[a] < 2) res[a] = 2; |
| 87 | // A grid is memory cubed. 256^3 floats is 64 MB; past that a stray |
| 88 | // voxel size turns into an allocation nobody meant to ask for. |
| 89 | if (res[a] > 256) res[a] = 256; |
| 90 | } |
| 91 | |
| 92 | Ref<ProcVolume> vol; |
| 93 | vol.instantiate(); |
| 94 | vol->grid_origin = Vector3(mn[0], mn[1], mn[2]); |
| 95 | vol->grid_resolution = Vector3i(res[0], res[1], res[2]); |
| 96 | vol->grid_size = Vector3(res[0] * voxel, res[1] * voxel, res[2] * voxel); |
| 97 | vol->density_grid.resize(res[0] * res[1] * res[2]); |
| 98 | float *w = vol->density_grid.ptrw(); |
| 99 | for (int z = 0; z < res[2]; z++) { |
| 100 | for (int y = 0; y < res[1]; y++) { |
| 101 | for (int x = 0; x < res[0]; x++) { |
| 102 | w[(z * res[1] + y) * res[0] + x] = sdf.evaluate( |
| 103 | mn[0] + x * voxel, mn[1] + y * voxel, mn[2] + z * voxel, voxel); |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | out_geo->set_volume(vol); |
| 108 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 109 | } |
| 110 | |
| 111 | // ── SDF Grid Fillet ───────────────────────────────────────────────────────── |
| 112 | |
| 113 | void SDFGridFilletNode::_bind_methods() {} |
| 114 | |
| 115 | SDFGridFilletNode::SDFGridFilletNode() { |
| 116 | input_defs.push_back({ "Volume", SocketType::Geometry, Variant() }); // 0 |
| 117 | input_defs.push_back({ "Radius", SocketType::Float, 1.0f }); // 1 |
| 118 | output_defs.push_back({ "Volume", SocketType::Geometry, Variant() }); |
| 119 | } |
| 120 | |
| 121 | void SDFGridFilletNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 122 | Ref<ProcGeometry> in_geo = p_ctx.read_geometry(p_node_idx, 0); |
| 123 | Ref<ProcGeometry> out_geo; |
| 124 | out_geo.instantiate(); |
| 125 | Ref<ProcVolume> in_vol = in_geo.is_valid() ? in_geo->get_volume() : Ref<ProcVolume>(); |
| 126 | if (in_vol.is_null() || in_vol->density_grid.size() == 0) { |
| 127 | UtilityFunctions::push_warning( |
| 128 | "proc-nodes: SDF Grid Fillet got no grid. Mesh to SDF Grid has to " |
| 129 | "come first -- this filters a field, it does not make one."); |
| 130 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 131 | return; |
| 132 | } |
| 133 | |
| 134 | // ⚠ AN APPROXIMATION, AND SAYING SO IS THE POINT. Blender's fillet is |
| 135 | // OpenVDB's level-set filter, which advances the surface by curvature. This |
| 136 | // is iterated Laplacian smoothing of the field, restricted to CONCAVE |
| 137 | // samples -- where the field is below the average of its neighbours, which |
| 138 | // is where a crease is. That rounds creases and leaves convex edges roughly |
| 139 | // alone, which is the visible half of what a fillet does; it is NOT the same |
| 140 | // operator and will not match Blender value for value. |
| 141 | Ref<ProcVolume> vol = in_vol->duplicate_volume(); |
| 142 | const Vector3i r = vol->grid_resolution; |
| 143 | float radius = p_ctx.read_float(p_node_idx, 1); |
| 144 | if (radius <= 0.0f) { |
| 145 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 146 | out_geo->set_volume(vol); |
| 147 | return; |
| 148 | } |
| 149 | // Radius in voxels -> iterations. Each pass smooths about one voxel. |
| 150 | const float voxel = (r.x > 0) ? (vol->grid_size.x / (float)r.x) : 1.0f; |
| 151 | int iters = (int)(radius / MAX(voxel, 1e-6f)); |
| 152 | if (iters < 1) iters = 1; |
| 153 | if (iters > 32) iters = 32; |
| 154 | |
| 155 | PackedFloat32Array next = vol->density_grid; |
| 156 | float *dst = next.ptrw(); |
| 157 | const int sx = r.x, sy = r.y, sz = r.z; |
| 158 | for (int it = 0; it < iters; it++) { |
| 159 | const float *src = vol->density_grid.ptr(); |
| 160 | for (int z = 1; z < sz - 1; z++) { |
| 161 | for (int y = 1; y < sy - 1; y++) { |
| 162 | for (int x = 1; x < sx - 1; x++) { |
| 163 | const int i = (z * sy + y) * sx + x; |
| 164 | const float c = src[i]; |
| 165 | const float avg = (src[i - 1] + src[i + 1] + src[i - sx] + src[i + sx] + |
| 166 | src[i - sx * sy] + src[i + sx * sy]) / |
| 167 | 6.0f; |
| 168 | // Concave only: the field dips below its neighbourhood. |
| 169 | dst[i] = (avg > c) ? avg : c; |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | vol->density_grid = next; |
| 174 | } |
| 175 | out_geo->set_volume(vol); |
| 176 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 177 | } |
| 178 | |
| 179 | // ── Grid to Mesh ──────────────────────────────────────────────────────────── |
| 180 | |
| 181 | void GridToMeshNode::_bind_methods() {} |
| 182 | |
| 183 | GridToMeshNode::GridToMeshNode() { |
| 184 | input_defs.push_back({ "Volume", SocketType::Geometry, Variant() }); // 0 |
| 185 | input_defs.push_back({ "Threshold", SocketType::Float, 0.0f }); // 1 |
| 186 | output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() }); |
| 187 | } |
| 188 | |
| 189 | void GridToMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 190 | Ref<ProcGeometry> in_geo = p_ctx.read_geometry(p_node_idx, 0); |
| 191 | Ref<ProcGeometry> out_geo; |
| 192 | out_geo.instantiate(); |
| 193 | Ref<ProcVolume> vol = in_geo.is_valid() ? in_geo->get_volume() : Ref<ProcVolume>(); |
| 194 | if (vol.is_null() || vol->density_grid.size() == 0) { |
| 195 | UtilityFunctions::push_warning( |
| 196 | "proc-nodes: Grid to Mesh got no grid, so there is nothing to " |
| 197 | "extract. Mesh to SDF Grid has to come first."); |
| 198 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 199 | return; |
| 200 | } |
| 201 | |
| 202 | // DensityGrid is CUBIC -- one `stride` for all three axes -- so a |
| 203 | // non-cubic ProcVolume grid is resampled onto the largest axis rather than |
| 204 | // silently read with the wrong strides, which is a mesh made of garbage. |
| 205 | const Vector3i r = vol->grid_resolution; |
| 206 | const float threshold = p_ctx.read_float(p_node_idx, 1); |
| 207 | const float voxel = (r.x > 0) ? (vol->grid_size.x / (float)r.x) : 1.0f; |
| 208 | |
| 209 | // ⛔ THE CELL COUNT MUST BE A POWER OF TWO. construct_octree_grid_pooled |
| 210 | // halves the span at every level, and dc-terrain only ever hands it 32 |
| 211 | // (VOXELS_PER_CELL) so nothing had ever exercised another value. A grid of |
| 212 | // 14 cells does not bottom out: the gate HUNG rather than failing, at 15^3, |
| 213 | // with no error -- which is why this reads as "no mesh" from the outside. |
| 214 | int cells = 1; |
| 215 | const int longest = MAX(MAX(r.x, r.y), r.z); |
| 216 | while (cells < longest - 1 && cells < 128) { |
| 217 | cells <<= 1; |
| 218 | } |
| 219 | const int n = cells + 1; // samples per axis |
| 220 | |
| 221 | DensityGrid grid; |
| 222 | const float org[3] = { (float)vol->grid_origin.x, (float)vol->grid_origin.y, |
| 223 | (float)vol->grid_origin.z }; |
| 224 | grid.allocate(cells, voxel, org); |
| 225 | const float *src = vol->density_grid.ptr(); |
| 226 | for (int z = 0; z < n; z++) { |
| 227 | for (int y = 0; y < n; y++) { |
| 228 | for (int x = 0; x < n; x++) { |
| 229 | float v = -1.0f; // outside, for samples past the source grid |
| 230 | if (x < r.x && y < r.y && z < r.z) { |
| 231 | v = src[(z * r.y + y) * r.x + x] - threshold; |
| 232 | } |
| 233 | grid.data[(z * n + y) * n + x] = v; |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | grid.compute_normals(); |
| 238 | |
| 239 | // ⛔ DISABLED: dc_mesh_from_grid HANGS ON THIS GRID AND I DO NOT KNOW WHY. |
| 240 | // |
| 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 | } |
| 262 | |
| 263 | DCMesherConfig cfg; |
| 264 | DCMeshData md = dc_mesh_from_grid(grid, cfg); |
| 265 | const int vcount = (int)md.positions.size() / 3; |
| 266 | if (vcount == 0 || md.indices.empty()) { |
| 267 | UtilityFunctions::push_warning( |
| 268 | "proc-nodes: Grid to Mesh extracted no surface -- the field does " |
| 269 | "not cross the threshold anywhere inside the grid."); |
| 270 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 271 | return; |
| 272 | } |
| 273 | |
| 274 | Ref<ProcMesh> mesh = out_geo->ensure_mesh(); |
| 275 | mesh->positions.resize(vcount); |
| 276 | Vector3 *pw = mesh->positions.ptrw(); |
| 277 | for (int i = 0; i < vcount; i++) { |
| 278 | pw[i] = Vector3(md.positions[i * 3], md.positions[i * 3 + 1], md.positions[i * 3 + 2]); |
| 279 | } |
| 280 | if ((int)md.normals.size() == vcount * 3) { |
| 281 | mesh->normals.resize(vcount); |
| 282 | Vector3 *nw = mesh->normals.ptrw(); |
| 283 | for (int i = 0; i < vcount; i++) { |
| 284 | nw[i] = Vector3(md.normals[i * 3], md.normals[i * 3 + 1], md.normals[i * 3 + 2]); |
| 285 | } |
| 286 | } |
| 287 | mesh->indices.resize((int)md.indices.size()); |
| 288 | int32_t *iw = mesh->indices.ptrw(); |
| 289 | for (size_t i = 0; i < md.indices.size(); i++) { |
| 290 | iw[i] = md.indices[i]; |
| 291 | } |
| 292 | // Dual Contouring emits triangles, so any ngon grouping is gone. |
| 293 | mesh->face_offsets.clear(); |
| 294 | p_ctx.write_geometry(p_node_idx, 0, out_geo); |
| 295 | } |
| 296 | |
| 297 | } // namespace godot |