OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 07e6964

density: the graph declares its own extent, beside its density
A Blender group interface takes any number of named, typed outputs, so a graph says HOW BIG IT IS the same way it says what its density is -- sockets on the Group Output, read by name: "Bounds Min", "Bounds Max", "Voxel Size". Nothing in the bridge needed changing; the Group Output node already crosses carrying its whole interface. ⚠ THE BOUNDS ARE IN THE GRAPH'S FRAME, and M NEGATES x, so a converted pair is no longer sorted. min/max are recomputed componentwise afterwards. Measured on the bridged voronoi mountain, authored in Blender as min (-20, -20, -2) / max (20, 20, 23.7648): without the re-sort x comes back min 20, max -20 -- inverted with it min (-20, -2, -20) max (20, 23.7648, 20) An inverted box meshes nothing and reads downstream as "the terrain is missing", not as a frame error, which is why this is a comment and an assertion rather than a line of arithmetic. Half a box is refused, not completed: a graph naming one corner and not the other is an authoring mistake, and a defaulted corner would put the world somewhere nobody asked for. It warns and reports unbounded. Bounds may be wired or left as the socket default; both are read. Baselines held: op parity 770/770, golden 31/32, batch 7/8, and all three density shapes -- displacement, Group-Output float, and bounded -- 100/100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
07e69649b14918a33867e17f56bf9e1b5c02167d
by plankatron <fleeky@gmail.com> · 2026-09-08T14:30:07.000Z

Changed files (3) +98 -0

demo/glsl_density_gate_dump.gd +9 -0
……@@ -60,6 +60,15 @@ func _init() -> void:
6060 String.num(pts[i].x, 9), String.num(pts[i].y, 9),
6161 String.num(pts[i].z, 9), String.num(cpu[i], 9)]
6262 FileAccess.open(dir + "/manifest.txt", FileAccess.WRITE).store_string(manifest)
63 # The graph's own extent, if it declares one. Reported here because a bounds
64 # error is silent downstream -- an inverted box reads as "the terrain is
65 # missing", not as a frame error.
66 var b: Dictionary = g.get_density_bounds()
67 if b.is_empty():
68 print("DENSITY_GATE_DUMP: no bounds declared (unbounded field)")
69 else:
70 print("DENSITY_GATE_DUMP: bounds min=%v max=%v voxel=%.4f"
71 % [b["min"], b["max"], b["voxel_size"]])
6372 print("DENSITY_GATE_DUMP: %d points, blender_frame=%s -> %s"
6473 % [pts.size(), str(g.get_source_frame() == 1), dir])
6574 quit()
src/graph/proc_node_graph.cpp +69 -0
……@@ -127,6 +127,7 @@ void ProcNodeGraph::_bind_methods() {
127127 ClassDB::bind_method(D_METHOD("disconnect_sockets", "from_node", "from_socket", "to_node", "to_socket"), &ProcNodeGraph::disconnect_sockets);
128128 ClassDB::bind_method(D_METHOD("evaluate_socket", "node_idx", "socket_idx"), &ProcNodeGraph::evaluate_socket);
129129 ClassDB::bind_method(D_METHOD("evaluate_density_batch", "positions"), &ProcNodeGraph::evaluate_density_batch);
130 ClassDB::bind_method(D_METHOD("get_density_bounds"), &ProcNodeGraph::get_density_bounds);
130131 ClassDB::bind_method(D_METHOD("evaluate_density_at", "pos"), &ProcNodeGraph::evaluate_density_at);
131132 ClassDB::bind_static_method("ProcNodeGraph",
132133 D_METHOD("debug_phacelle_noise", "p", "dir", "freq", "offset", "normalization"),
……@@ -516,6 +517,74 @@ bool ProcNodeGraph::find_density_output(int &r_node, int &r_socket) const {
516517 return false;
517518}
518519
520Dictionary ProcNodeGraph::get_density_bounds() const {
521 Dictionary out;
522 // The same terminal the density comes from: the graph's Group Output. Read
523 // by NAME, because a group interface's socket ORDER is the author's and
524 // changing it in Blender must not silently re-point a bound.
525 for (int i = 0; i < get_node_count(); i++) {
526 Ref<ProcNode> n = get_node(i);
527 if (n.is_null() || n->get_node_type() != StringName("GroupOutput")) {
528 continue;
529 }
530 bool have_min = false, have_max = false;
531 Vector3 bmin, bmax;
532 float voxel = 0.0f;
533 for (int s = 0; s < n->get_input_count(); s++) {
534 const StringName nm = n->get_input_def(s).name;
535 // A bound may be wired from a value node or left as the socket's
536 // own default; read whichever the author used.
537 Variant v;
538 const int src = get_input_source_node(i, s);
539 if (src >= 0) {
540 v = GraphExecutor::evaluate_socket_value(
541 *this, src, get_input_source_socket(i, s), Dictionary());
542 } else {
543 v = n->get_input_default(s);
544 }
545 if (nm == StringName("Bounds Min")) {
546 bmin = v;
547 have_min = true;
548 } else if (nm == StringName("Bounds Max")) {
549 bmax = v;
550 have_max = true;
551 } else if (nm == StringName("Voxel Size")) {
552 voxel = (float)(double)v;
553 }
554 }
555 if (!have_min || !have_max) {
556 // Half a box is not a box. An unbounded field is a legitimate
557 // state; a graph that names one corner and not the other is an
558 // authoring mistake, and answering with a default corner would
559 // place the world somewhere nobody asked for.
560 if (have_min != have_max) {
561 UtilityFunctions::push_warning(
562 "proc-nodes: the graph declares only one of Bounds Min / "
563 "Bounds Max on its Group Output. Both are needed; treating "
564 "the field as unbounded.");
565 }
566 continue;
567 }
568 if (is_blender_frame()) {
569 // M, self-inverse, the same conversion _to_godot_frame applies to
570 // geometry and the emitter applies to the query point.
571 bmin = Vector3(-bmin.x, bmin.z, bmin.y);
572 bmax = Vector3(-bmax.x, bmax.z, bmax.y);
573 // ⛔ M NEGATES x, so the converted pair is NOT sorted. Re-sort
574 // componentwise or the box comes out inverted on that axis.
575 const Vector3 lo(MIN(bmin.x, bmax.x), MIN(bmin.y, bmax.y), MIN(bmin.z, bmax.z));
576 const Vector3 hi(MAX(bmin.x, bmax.x), MAX(bmin.y, bmax.y), MAX(bmin.z, bmax.z));
577 bmin = lo;
578 bmax = hi;
579 }
580 out["min"] = bmin;
581 out["max"] = bmax;
582 out["voxel_size"] = voxel;
583 return out;
584 }
585 return out;
586}
587
519588static bool _density_output_socket(const ProcNodeGraph &p_graph, int &r_node, int &r_socket) {
520589 return p_graph.find_density_output(r_node, r_socket);
521590}
src/graph/proc_node_graph.h +20 -0
……@@ -185,6 +185,26 @@ public:
185185 // (hyper-games #65 NEXT 5).
186186 bool find_density_output(int &r_node, int &r_socket) const;
187187
188 // THE GRAPH'S OWN EXTENT, declared beside its density.
189 //
190 // A Blender group interface takes any number of named, typed outputs, so a
191 // graph says how big it is the same way it says what its density is: four
192 // sockets on the Group Output, read BY NAME -- "Bounds Min", "Bounds Max",
193 // "Voxel Size". Nothing in the bridge needed changing for this; the Group
194 // Output node already crosses carrying its whole interface.
195 //
196 // Returns an empty Dictionary when the graph declares no bounds, which is
197 // the honest answer for an unbounded field and is what
198 // DCGenerator::get_bounds already defaults to.
199 //
200 // ⚠ THE BOUNDS ARE IN THE GRAPH'S FRAME. A bridged graph is authored in
201 // Blender's, so the values are Blender coordinates and a consumer wants
202 // world. M = (x, y, z) -> (-x, z, y) is applied here, and because M NEGATES
203 // x a converted pair is no longer sorted -- min/max are recomputed
204 // componentwise afterwards. Skipping that yields an inverted box, which
205 // reads downstream as "the terrain is missing" rather than as a frame error.
206 Dictionary get_density_bounds() const;
207
188208 // THE BATCH PATH'S TEXT, FOR THE GATE.
189209 //
190210 // compile_batch's output is otherwise reachable only through a GPU