OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 5484d5f

volume: the SDF-grid family, and Grid to Mesh hangs
mountain_voronoi1.blend's MTN_Solidify is mesh -> SDF grid -> fillet -> mesh, and none of those three node types existed. They were SKIPPED at import, so the mountain would have arrived without its solidify pass -- thinner and sharper than in Blender, with only a log line saying so. audit of the blend against GEOMETRY_NODE_MAP before 5 types dropped: MeshToSDFGrid, SDFGridFillet, GridToMesh, GeometryNodeGroup, SetMaterial after 2: GeometryNodeGroup (a FALSE POSITIVE -- the serializer INLINES groups, _walk recurses into node_tree) and SetMaterial (cosmetic) The grid type was not new: ProcVolume has carried density_grid, grid_origin and grid_resolution since it was written and nothing had ever filled one. The evaluator and extractor are libdc's -- DCGeneratorMeshSDF (pseudonormal + BVH) and dc_mesh_from_grid -- so no algorithm is written twice. SConstruct now also compiles libdc/src/generators, which the first glob missed. Mapped BOTH WAYS. geometry_serializer's GEOMETRY_NODE_MAP, and graph_exporter's C++ inverse -- test_node_maps compares the two and went 4/6 when only the Python side had them. 6/6 now. ⚠ SIGN. proc-nodes is POSITIVE = SOLID (libdc's convention); Blender's SDF grids are negative inside. Mesh -> grid -> mesh round trips either way; a raw grid value does not compare across. --- ⛔ Grid to Mesh IS DISABLED AND I DO NOT KNOW WHY IT HANGS. demo/sdf_grid_gate.gd: a unit UV sphere -> Mesh to SDF Grid (voxel 0.25, a 15^3 grid) -> Grid to Mesh never returns. Measured, not inferred: * NOT slowness -- 15^3 is 3375 samples, and 32^3 hangs identically * NOT the fillet -- bypassing it changes nothing * NOT the octree's cell count -- snapping it to a power of two, on the theory that construct_octree_grid_pooled needs one (dc-terrain only ever passes 32), did not fix it So it REFUSES with an error instead of running. A node that locks the editor the moment someone drops it in a graph is worse than one that admits it is unfinished. PROC_NODES_GRID_TO_MESH=1 runs it anyway, for whoever picks up the debugging; the gate is the reproduction. Mesh to SDF Grid WORKS and is verified: 3375 and 32768 voxels at the expected resolutions. SDF Grid Fillet is untested -- it sits behind Grid to Mesh, and its implementation is an approximation of OpenVDB's level-set filter (iterated Laplacian on concave samples), documented as such at the call site. sdf_grid_gate 1/3, glsl_golden_gate 32/32, node maps 6/6, all three targets build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5484d5f2a7e59bde62a3c4199d733fb6dcdffb54
by plankatron <fleeky@gmail.com> · 2026-09-06T17:23:24.000Z

Changed files (7) +446 -2

SConstruct +7 -2
……@@ -64,6 +64,10 @@ env.Append(CPPPATH=[LIBDC_INC])
6464# Globbing them directly from here would have two builds writing the same object
6565# paths with different flags, and whichever ran last would win silently.
6666VariantDir("build/libdc", LIBDC_SRC, duplicate=0)
67# generators/ too: DCGeneratorMeshSDF is the mesh -> signed-distance evaluator
68# (pseudonormal method, BVH) that Mesh to SDF Grid needs. Missing it is a link
69# error rather than a silent gap, but only once something calls it.
70VariantDir("build/libdc_gen", LIBDC_SRC + "/generators", duplicate=0)
6771
6872# ⚠ EVERYTHING EXCEPT dc_seam. libdc is self-contained apart from one file:
6973# dc_seam.h includes dc_octree_clipmap.h, which lives in dc-terrain/src and
……@@ -81,8 +85,9 @@ VariantDir("build/libdc", LIBDC_SRC, duplicate=0)
8185# dc_mesher.cpp nor dc_contour.cpp mentions simplify. Taking it would mean
8286# vendoring a third-party library to compile a file we never call.
8387LIBDC_SKIP = ("dc_seam.cpp", "dc_simplify.cpp")
84LIBDC_SOURCES = [f for f in Glob("build/libdc/*.cpp")
85 if not str(f).endswith(LIBDC_SKIP)]
88LIBDC_SOURCES = ([f for f in Glob("build/libdc/*.cpp")
89 if not str(f).endswith(LIBDC_SKIP)]
90 + Glob("build/libdc_gen/*.cpp"))
8691
8792# THE erosion filter lives once, in src/shared/erosion_filter.inc, and is
8893# compiled two ways: as C++ (via erosion_cpp_shim.h, here and in dc-terrain)
demo/sdf_grid_gate.gd +46 -0
……@@ -0,0 +1,46 @@
1## The SDF-grid family, end to end: mesh -> grid -> fillet -> mesh.
2##
3## This is MTN_Solidify's chain, which mountain_voronoi1.blend is built from and
4## which was skipped at import until these three nodes existed.
5extends SceneTree
6var _fail := 0
7func _ck(n: String, ok: bool, d: String) -> void:
8 print(" %-34s %s %s" % [n, "PASS" if ok else "FAIL", d])
9 if not ok: _fail += 1
10
11func _init() -> void:
12 var g := ProcNodeGraph.new()
13 var sphere := g.add_node(MeshUVSphereNode.new())
14 g.get_node(sphere).set_input_default(2, 1.0) # radius
15 var to_grid := g.add_node(MeshToSDFGridNode.new())
16 g.get_node(to_grid).set_input_default(1, 0.25) # voxel size
17 g.connect_sockets(sphere, 0, to_grid, 0)
18 var fillet := g.add_node(SDFGridFilletNode.new())
19 g.get_node(fillet).set_input_default(1, 0.16) # radius
20 g.connect_sockets(to_grid, 0, fillet, 0)
21 var to_mesh := g.add_node(GridToMeshNode.new())
22 g.connect_sockets(fillet, 0, to_mesh, 0)
23
24 var grid_geo = g.evaluate_socket(to_grid, 0)
25 var vol = grid_geo.get_volume() if grid_geo != null else null
26 _ck("mesh -> SDF grid", vol != null and vol.get_density_grid().size() > 0,
27 "%d voxels, res %v" % [vol.get_density_grid().size() if vol != null else 0,
28 vol.get_grid_resolution() if vol != null else Vector3i.ZERO])
29
30 var out = g.evaluate_socket(to_mesh, 0)
31 var m = out.get_mesh() if out != null else null
32 _ck("grid -> mesh", m != null and m.vertex_count() > 0 and m.triangle_count() > 0,
33 "%d verts, %d tris" % [m.vertex_count() if m != null else -1,
34 m.triangle_count() if m != null else -1])
35 if m != null and m.vertex_count() > 0:
36 var worst := 0.0
37 for i in range(m.vertex_count()):
38 worst = max(worst, abs(m.positions[i].length() - 1.0))
39 # A filleted unit sphere is still about a unit sphere; the fillet rounds
40 # creases and a sphere has none, so this also says the fillet did not
41 # eat the field.
42 _ck("round trip is still a sphere", worst < 0.25, "worst |r-1| = %.4f" % worst)
43 else:
44 _ck("round trip is still a sphere", false, "no mesh")
45 print("\n%d/%d passed" % [3 - _fail, 3])
46 quit(1 if _fail > 0 else 0)
src/graph/graph_exporter.cpp +6 -0
……@@ -31,6 +31,12 @@ static HashMap<StringName, String> _build_reverse_type_map() {
3131
3232 // Mesh Primitives
3333 m["MeshGrid"] = "GeometryNodeMeshGrid";
34 // The SDF-grid family. Godot -> Blender must know these too, or the export
35 // side and the import side disagree about what crosses -- which is what
36 // test_node_maps asserts and what caught this.
37 m["MeshToSDFGrid"] = "GeometryNodeMeshToSDFGrid";
38 m["SDFGridFillet"] = "GeometryNodeSDFGridFillet";
39 m["GridToMesh"] = "GeometryNodeGridToMesh";
3440 m["MeshCube"] = "GeometryNodeMeshCube";
3541 m["MeshCircle"] = "GeometryNodeMeshCircle";
3642 m["MeshCylinder"] = "GeometryNodeMeshCylinder";
src/graph/graph_importer.cpp +4 -0
……@@ -17,6 +17,7 @@
1717#include "../nodes/selection_nodes.h"
1818#include "../nodes/utility_nodes.h"
1919#include "../nodes/volume_nodes.h"
20#include "../nodes/volume_grid_nodes.h"
2021#include "../nodes/zone_nodes.h"
2122
2223#include <godot_cpp/classes/json.hpp>
……@@ -159,6 +160,9 @@ static Ref<ProcNode> _create_node_by_type(const String &p_type) {
159160 if (p_type == "SphereSDF") return Ref<ProcNode>(memnew(SphereSdfNode));
160161 if (p_type == "VolumeBoolean") return Ref<ProcNode>(memnew(VolumeBooleanNode));
161162 if (p_type == "VolumeToMesh") return Ref<ProcNode>(memnew(VolumeToMeshNode));
163 if (p_type == "MeshToSDFGrid") return Ref<ProcNode>(memnew(MeshToSDFGridNode));
164 if (p_type == "SDFGridFillet") return Ref<ProcNode>(memnew(SDFGridFilletNode));
165 if (p_type == "GridToMesh") return Ref<ProcNode>(memnew(GridToMeshNode));
162166
163167 // Utility
164168 if (p_type == "AccumulateField") return Ref<ProcNode>(memnew(AccumulateFieldNode));
src/nodes/volume_grid_nodes.cpp +297 -0
……@@ -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
17namespace godot {
18
19// ── Mesh to SDF Grid ────────────────────────────────────────────────────────
20
21void MeshToSDFGridNode::_bind_methods() {}
22
23MeshToSDFGridNode::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
30void 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
113void SDFGridFilletNode::_bind_methods() {}
114
115SDFGridFilletNode::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
121void 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
181void GridToMeshNode::_bind_methods() {}
182
183GridToMeshNode::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
189void 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
src/nodes/volume_grid_nodes.h +80 -0
……@@ -0,0 +1,80 @@
1#ifndef PROC_NODES_VOLUME_GRID_NODES_H
2#define PROC_NODES_VOLUME_GRID_NODES_H
3
4#include "../graph/proc_node.h"
5
6namespace godot {
7
8// ── THE VOXEL-GRID FAMILY ───────────────────────────────────────────────────
9//
10// Blender's SDF-grid nodes, which mountain_voronoi1.blend's MTN_Solidify is
11// built from: mesh -> SDF grid -> fillet -> mesh. Without them that group is
12// SKIPPED at import and the mountain arrives without its solidify pass --
13// thinner and sharper than in Blender, with only a log line saying so.
14//
15// The grid itself is not new. ProcVolume has carried density_grid, grid_origin,
16// grid_size and grid_resolution since it was written; these three nodes are what
17// finally put something in it and take it out again. The evaluator and the
18// extractor are libdc's -- DCGeneratorMeshSDF and dc_mesh_from_grid -- so no
19// algorithm is written twice.
20//
21// ⚠ SIGN CONVENTION. libdc is POSITIVE = SOLID throughout, and these nodes keep
22// that. Blender's SDF grids are negative inside. A grid that crosses the bridge
23// is therefore sign-flipped relative to Blender's, which matters the moment
24// anyone compares a raw grid value across the two; it does not matter for
25// mesh -> grid -> mesh, which is what the mountain does.
26
27// Mesh to SDF Grid — sample a mesh's signed distance into a ProcVolume grid.
28class MeshToSDFGridNode : public ProcNode {
29 GDCLASS(MeshToSDFGridNode, ProcNode);
30
31protected:
32 static void _bind_methods();
33
34public:
35 StringName get_node_type() const override { return "MeshToSDFGrid"; }
36 String get_node_title() const override { return "Mesh to SDF Grid"; }
37 String get_node_category() const override { return "Volume"; }
38
39 void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
40
41 MeshToSDFGridNode();
42};
43
44// SDF Grid Fillet — round the field's concave creases.
45class SDFGridFilletNode : public ProcNode {
46 GDCLASS(SDFGridFilletNode, ProcNode);
47
48protected:
49 static void _bind_methods();
50
51public:
52 StringName get_node_type() const override { return "SDFGridFillet"; }
53 String get_node_title() const override { return "SDF Grid Fillet"; }
54 String get_node_category() const override { return "Volume"; }
55
56 void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
57
58 SDFGridFilletNode();
59};
60
61// Grid to Mesh — extract the isosurface, through libdc's Dual Contouring.
62class GridToMeshNode : public ProcNode {
63 GDCLASS(GridToMeshNode, ProcNode);
64
65protected:
66 static void _bind_methods();
67
68public:
69 StringName get_node_type() const override { return "GridToMesh"; }
70 String get_node_title() const override { return "Grid to Mesh"; }
71 String get_node_category() const override { return "Volume"; }
72
73 void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
74
75 GridToMeshNode();
76};
77
78} // namespace godot
79
80#endif // PROC_NODES_VOLUME_GRID_NODES_H
src/register_types.cpp +6 -0
……@@ -31,6 +31,7 @@
3131#include "nodes/instance_nodes.h"
3232#include "nodes/utility_nodes.h"
3333#include "nodes/volume_nodes.h"
34#include "nodes/volume_grid_nodes.h"
3435
3536// Scene
3637#include "scene/proc_geometry_node.h"
……@@ -228,6 +229,11 @@ void initialize_proc_nodes_module(ModuleInitializationLevel p_level) {
228229 GDREGISTER_CLASS(SphereSdfNode);
229230 GDREGISTER_CLASS(VolumeBooleanNode);
230231 GDREGISTER_CLASS(VolumeToMeshNode);
232 // The voxel-grid family: Blender's mesh -> SDF grid -> fillet -> mesh chain,
233 // which mountain_voronoi1.blend's MTN_Solidify is built from.
234 GDREGISTER_CLASS(MeshToSDFGridNode);
235 GDREGISTER_CLASS(SDFGridFilletNode);
236 GDREGISTER_CLASS(GridToMeshNode);
231237
232238 // Utility nodes
233239 GDREGISTER_CLASS(AccumulateFieldNode);