| … | … | @@ -323,9 +323,29 @@ func _volume_density_via_group_output() -> void: |
| 323 | 323 | # VolumeToMesh stays GPU-off, and a graph containing it still REFUSES -- |
| 324 | 324 | # case B depends on that, so assert the property rather than leave it |
| 325 | 325 | # implied by one other case. |
| 326 | # M. AN AUTHORED VOLUME COMPILES. This is the thing VolumeToMesh is FOR and | |
| 327 | # it has never worked: the node emits nothing, and `!supports_gpu()` is a | |
| 328 | # REFUSAL, so a graph ending in it could not be compiled to GLSL at all. | |
| 329 | # It is structurally transparent now -- it names the density rather than | |
| 330 | # computing one -- so the chain emits and the terminal carries it. | |
| 331 | var vg := ProcNodeGraph.new() | |
| 332 | var vs1 := vg.add_node(SphereSdfNode.new()) | |
| 333 | var vs2 := vg.add_node(SphereSdfNode.new()) | |
| 334 | var vbo := vg.add_node(VolumeBooleanNode.new()) | |
| 335 | vg.connect_sockets(vs1, 0, vbo, 0) | |
| 336 | vg.connect_sockets(vs2, 0, vbo, 1) | |
| 337 | var vtm_idx := vg.add_node(VolumeToMeshNode.new()) | |
| 338 | vg.connect_sockets(vbo, 0, vtm_idx, 0) | |
| 339 | var vt: String = vg.compile_to_glsl_function("proc_density", false, PackedStringArray()) | |
| 340 | _check("volume graph emits at all", not vt.is_empty(), | |
| 341 | "%d chars" % vt.length() if not vt.is_empty() else "REFUSED -- the gap this closes") | |
| 342 | _check("VolumeToMesh names the density", | |
| 343 | vt.find("return n%d_s0;" % vbo) != -1, | |
| 344 | "returns the SDF chain, with no Group Output in the graph") | |
| 326 | 345 | var vtm := VolumeToMeshNode.new() |
| 327 | _check("VolumeToMesh stays GPU-off", not vtm.supports_gpu(), | |
| 328 | "supports_gpu() false -- it hands on geometry, it names no float") | |
| 346 | _check("VolumeToMesh emits nothing itself", | |
| 347 | not vtm.supports_gpu() and vtm.gpu_transparent(), | |
| 348 | "transparent, not refused: it names a density, it does not compute one") | |
| 329 | 349 | |
| 330 | 350 | |
| 331 | 351 | func _init() -> void: |
| … | … | @@ -353,14 +373,19 @@ func _init() -> void: |
| 353 | 373 | "erosion call gone, gradient noise call kept") |
| 354 | 374 | |
| 355 | 375 | # B. A node with no GLSL must make emission FAIL, not vanish from the shader. |
| 356 | # VolumeToMesh, and the choice matters. This case used Math, then Map Range, | |
| 357 | # and each time that node got an implementation the case stopped testing | |
| 358 | # refusal and started testing emission -- it went red twice for being | |
| 359 | # OBSOLETE rather than wrong. VolumeToMesh's output is a Geometry socket, | |
| 360 | # which has no GLSL spelling at all, so it cannot acquire one by someone | |
| 361 | # doing the next item on the list. | |
| 376 | # JOIN GEOMETRY, and the choice matters. This case used Math, then Map | |
| 377 | # Range, then VolumeToMesh, and each time that node got an implementation | |
| 378 | # the case stopped testing refusal and started testing emission -- it has | |
| 379 | # now gone obsolete THREE times rather than wrong. VolumeToMesh was retired | |
| 380 | # from this role deliberately: it became a legitimate terminal that names a | |
| 381 | # volume's density, which is exactly the "someone did the next item on the | |
| 382 | # list" this case keeps losing its subject to. | |
| 383 | # | |
| 384 | # Join Geometry combines GEOMETRIES. A density function is a float of a | |
| 385 | # position -- there is no geometry in it to join -- so this one cannot | |
| 386 | # acquire a GLSL implementation by anybody finishing anything. | |
| 362 | 387 | var g2 := _erosion_graph() |
| 363 | g2.add_node(VolumeToMeshNode.new()) | |
| 388 | g2.add_node(JoinGeometryNode.new()) | |
| 364 | 389 | var with_math: String = g2.compile_to_glsl_function("proc_density", true, empty) |
| 365 | 390 | _check("a node with no GLSL refuses", with_math.is_empty(), |
| 366 | 391 | "emitted %d chars (a silent drop is the bug)" % with_math.length() |
| … | … | @@ -476,5 +501,22 @@ func _init() -> void: |
| 476 | 501 | _batch_still_uses_push_constants() |
| 477 | 502 | _volume_density_via_group_output() |
| 478 | 503 | |
| 504 | # ⛔ HOW MANY CHECKS SHOULD HAVE RUN. | |
| 505 | # | |
| 506 | # Without this the gate reports "N/N passed" where N is however many checks | |
| 507 | # happened to execute -- so a check that THROWS is silently dropped rather | |
| 508 | # than failed. Measured 2026-09-06: asserting on gpu_transparent(), which | |
| 509 | # was not bound to script, errored, vanished from the run, and the gate | |
| 510 | # printed "28/28 passed" and exited 0. The count moved and nothing went red. | |
| 511 | # | |
| 512 | # Bumping this by hand when a case is added is the point: an intentional | |
| 513 | # change is a one-line edit, an accidental drop is a failure. | |
| 514 | const EXPECTED_CHECKS := 29 | |
| 515 | if _ran != EXPECTED_CHECKS: | |
| 516 | _fail += 1 | |
| 517 | print(" %-34s FAIL %d checks ran, expected %d -- a check was DROPPED, " | |
| 518 | % ["all checks ran", _ran, EXPECTED_CHECKS] | |
| 519 | + "probably by throwing. Look for SCRIPT ERROR above.") | |
| 520 | ||
| 479 | 521 | print("\n%d/%d passed" % [_ran - _fail, _ran]) |
| 480 | 522 | quit(1 if _fail > 0 else 0) |
| … | … | @@ -6,6 +6,11 @@ void ProcNode::_bind_methods() { |
| 6 | 6 | ClassDB::bind_method(D_METHOD("get_node_type"), &ProcNode::get_node_type); |
| 7 | 7 | ClassDB::bind_method(D_METHOD("get_node_title"), &ProcNode::get_node_title); |
| 8 | 8 | ClassDB::bind_method(D_METHOD("supports_gpu"), &ProcNode::supports_gpu); |
| 9 | // Bound because the two are a PAIR and only one was askable. A node can | |
| 10 | // emit nothing for two different reasons -- missing arithmetic, which must | |
| 11 | // refuse, or structural transparency, which must skip -- and a gate that | |
| 12 | // can see supports_gpu() but not gpu_transparent() cannot tell them apart. | |
| 13 | ClassDB::bind_method(D_METHOD("gpu_transparent"), &ProcNode::gpu_transparent); | |
| 9 | 14 | // Bound so glsl_golden_gate.gd can assert that the slot a node's GLSL reads |
| 10 | 15 | // is the slot GraphExecutor packs -- the drift this map exists to end is |
| 11 | 16 | // only visible if the gate can see the map. |
| … | … | @@ -465,7 +465,16 @@ String ProcNodeGraph::compile_to_glsl_function(const String &p_function_name, |
| 465 | 465 | bool ProcNodeGraph::find_density_output(int &r_node, int &r_socket) const { |
| 466 | 466 | for (int i = 0; i < get_node_count(); i++) { |
| 467 | 467 | Ref<ProcNode> n = get_node(i); |
| 468 | if (n.is_null() || n->get_node_type() != StringName("GroupOutput")) { | |
| 468 | // TWO WAYS TO SAY IT, one rule. A Group Output float is the graph | |
| 469 | // declaring its result through its interface; a Volume to Mesh's | |
| 470 | // Density input is an authored VOLUME declaring the same thing at a | |
| 471 | // terminal. Both are "this float is my density", and a volume graph | |
| 472 | // should not have to carry a Group Output to be compilable. | |
| 473 | if (n.is_null()) { | |
| 474 | continue; | |
| 475 | } | |
| 476 | const StringName ty = n->get_node_type(); | |
| 477 | if (ty != StringName("GroupOutput") && ty != StringName("VolumeToMesh")) { | |
| 469 | 478 | continue; |
| 470 | 479 | } |
| 471 | 480 | for (int s = 0; s < n->get_input_count(); s++) { |
| … | … | @@ -1,5 +1,8 @@ |
| 1 | 1 | #include "volume_nodes.h" |
| 2 | 2 | |
| 3 | #include "../graph/graph_executor.h" | |
| 4 | #include "../graph/proc_node_graph.h" | |
| 5 | ||
| 3 | 6 | #include <godot_cpp/variant/utility_functions.hpp> |
| 4 | 7 | #include "../graph/graph_executor.h" |
| 5 | 8 | |
| … | … | @@ -94,24 +97,32 @@ VolumeToMeshNode::VolumeToMeshNode() { |
| 94 | 97 | } |
| 95 | 98 | |
| 96 | 99 | void VolumeToMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 97 | // ⛔ THIS IS A PLACEHOLDER AND IT FAILS SILENTLY. It writes an EMPTY | |
| 98 | // ProcVolume on an empty ProcGeometry -- no mesh, no density, no message -- | |
| 99 | // so a graph ending here bakes to nothing and looks like a graph that did | |
| 100 | // not run. That is the actual debt behind this node, not its | |
| 101 | // supports_gpu() == false (see the header). | |
| 100 | // FILL THE VOLUME. This used to write an EMPTY ProcVolume on an empty | |
| 101 | // ProcGeometry -- no mesh, no density, no message -- so a graph ending here | |
| 102 | // baked to nothing and looked like a graph that never ran. | |
| 102 | 103 | // |
| 103 | // Say so. The working way to express a volume's density is an explicit | |
| 104 | // Group Output float, which _density_output_socket reads and the emitter | |
| 105 | // returns; this node is not that mechanism. | |
| 106 | UtilityFunctions::push_warning( | |
| 107 | "proc-nodes: Volume to Mesh is a PLACEHOLDER -- it produces an empty " | |
| 108 | "geometry with an empty volume, so this graph will bake to nothing. " | |
| 109 | "To give a graph a density, wire the SDF chain into a Group Output " | |
| 110 | "FLOAT socket instead; that is what the emitter returns."); | |
| 104 | // ProcVolume has carried `glsl_source` with the signature | |
| 105 | // `float proc_density(vec3 pos)` since it was written; nothing ever put one | |
| 106 | // in it. The graph can emit exactly that, and now does. | |
| 111 | 107 | Ref<ProcGeometry> geo; |
| 112 | 108 | geo.instantiate(); |
| 113 | 109 | Ref<ProcVolume> vol; |
| 114 | 110 | vol.instantiate(); |
| 111 | if (p_ctx.graph != nullptr) { | |
| 112 | vol->glsl_source = p_ctx.graph->compile_to_glsl_function( | |
| 113 | "proc_density", false, PackedStringArray()); | |
| 114 | if (vol->glsl_source.is_empty()) { | |
| 115 | // The emitter has already said which node refused. Say what it | |
| 116 | // costs here, the way dc-terrain's _recompile_glsl does: an empty | |
| 117 | // volume is a cloud that does not render, and silence about it is | |
| 118 | // what made this node look finished for as long as it did. | |
| 119 | UtilityFunctions::push_error( | |
| 120 | "proc-nodes: Volume to Mesh could not compile this graph to a " | |
| 121 | "density function, so its ProcVolume carries no GLSL and will " | |
| 122 | "render nothing. See the emission error above for the node " | |
| 123 | "that refused."); | |
| 124 | } | |
| 125 | } | |
| 115 | 126 | geo->set_volume(vol); |
| 116 | 127 | p_ctx.write_geometry(p_node_idx, 0, geo); |
| 117 | 128 | } |
| … | … | @@ -83,46 +83,24 @@ public: |
| 83 | 83 | |
| 84 | 84 | void execute(ProcEvalContext &p_ctx, int p_node_idx) override; |
| 85 | 85 | |
| 86 | // ⛔ FALSE ON PURPOSE, AND THE REASON WAS RE-CHECKED 2026-09-06 (hyper-games | |
| 87 | // #65 NEXT 5, which asked whether this last supports_gpu() == false has a | |
| 88 | // reason or is the next debt). It has one, and half of what stood here was | |
| 89 | // already out of date: | |
| 86 | // ⛔ THIS NODE NAMES THE DENSITY. It does not compute one. | |
| 90 | 87 | // |
| 91 | // 1. Its output is a GEOMETRY socket, which has no GLSL spelling. | |
| 88 | // It emits nothing, so supports_gpu() stays false -- but it is | |
| 89 | // STRUCTURALLY TRANSPARENT, the same category as Group Input and Group | |
| 90 | // Output: it carries the graph's boundary, not an operation. Without that | |
| 91 | // distinction `!supports_gpu()` is a REFUSAL (proc_node_graph.cpp), so a | |
| 92 | // graph ending in this node could not be compiled to GLSL AT ALL -- which | |
| 93 | // is the "volume node GLSL connectivity" the dc-terrain-nubis notes list as | |
| 94 | // missing, and the reason nothing has ever built a ProcVolume. | |
| 92 | 95 | // |
| 93 | // 2. ⚠ "the density function has no way to return a node's value" is NO | |
| 94 | // LONGER TRUE. _density_output_socket (proc_node_graph.cpp) picks an | |
| 95 | // explicit Group Output FLOAT, and gpu_compiler emits `return n2_s0;` | |
| 96 | // for it. Measured: an SDF chain wired into a Group Output float emits | |
| 97 | // 486 characters returning the chain, with no VolumeToMesh anywhere. | |
| 98 | // So a volume graph CAN say what its density is -- through a mechanism | |
| 99 | // this node is not, and cannot become, because it hands on geometry | |
| 100 | // rather than naming a float. | |
| 101 | // | |
| 102 | // 3. NOTHING BUILDS A VOLUME THROUGH IT -- but it IS REACHABLE, and the | |
| 103 | // first version of this note said "no consumers", which was wrong. It | |
| 104 | // came from a grep truncated at six lines, and the line it cut off was | |
| 105 | // the one that matters: | |
| 106 | // | |
| 107 | // graph_importer.cpp:161 the FACTORY. Any imported or serialized | |
| 108 | // graph naming "VolumeToMesh" instantiates | |
| 109 | // one, and then bakes to nothing. | |
| 110 | // | |
| 111 | // What is true: no graph ON DISK uses it -- searched .tres, .tscn and | |
| 112 | // .json across proc-nodes, dc-terrain and hyper-tps, zero hits -- and | |
| 113 | // nothing constructs a meaningful ProcVolume anywhere. So it is | |
| 114 | // unused, not unreachable, and the reachable path is exactly the one | |
| 115 | // that fails silently. That makes execute()'s warning below load | |
| 116 | // bearing rather than decorative. | |
| 117 | // | |
| 118 | // blender-bridge does NOT map it, deliberately, and says why | |
| 119 | // (geometry_serializer.py:151): "ours takes a Density FIELD, Blender's | |
| 120 | // takes a volume GEOMETRY". They are different operations that share a | |
| 121 | // name, so a Blender graph can never produce this node. | |
| 122 | // | |
| 123 | // Turning this true would require it to emit something, and there is | |
| 124 | // nothing for it to emit. The debt here is not the flag -- see execute(). | |
| 96 | // Its Density input is the graph saying what its density IS, exactly as a | |
| 97 | // Group Output float does, and find_density_output() reads both. So an | |
| 98 | // authored volume -- a cloud formation, a fog body, an SDF blob -- compiles | |
| 99 | // to `float proc_density(vec3 pos)` and rides in the ProcVolume that | |
| 100 | // execute() now fills, for a GPU consumer (a FogVolume fog shader, or | |
| 101 | // dc-terrain) to raymarch. It is not meshed, despite the name. | |
| 125 | 102 | bool supports_gpu() const override { return false; } |
| 103 | bool gpu_transparent() const override { return true; } | |
| 126 | 104 | String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override; |
| 127 | 105 | }; |
| 128 | 106 |