OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / f8461c1

glsl: one graph, two products -- emit the density's cone, and rotate in it
The bridged voronoi mountain now compiles to a density function straight from the .blend: 100/100 on tests/test_emitted_density_parity.cpp against the real exported graph, where it previously refused outright. ONE GRAPH, TWO PRODUCTS. A tree bridged from Blender is a MESH tree that ALSO declares a density -- the mountain subdivides a cage, displaces it and solidifies it through an SDF grid, and separately says "this float is my density" on its group interface. compile_to_glsl_function emitted every node and refused on SubdivideMesh, a node that cannot affect the density and has no business in the density shader. When the graph DECLARES a density, only the backward cone that feeds it is emitted. This is NOT the silent drop the refusal exists to catch: a node INSIDE the cone that cannot emit still refuses loudly, and the excluded nodes are excluded because the density provably does not read them. With no declared density the whole graph is emitted exactly as before. The refusal then moved from SubdivideMesh to EulerToRotation -- which IS in the cone, because MTN_CellSpace rotates the voronoi lattice. So: EULER TO ROTATION AND ROTATE VECTOR GET GLSL. The emit context already maps Rotation to vec4 and Matrix to mat4, so this is arithmetic, not plumbing. ⚠ PN_EULER_ORDER is ZYX -- Blender's XYZ euler IS Godot's ZYX, one naming the order rotations are APPLIED and the other the order matrices are MULTIPLIED -- so the quaternion is qz * qy * qx. Held to the CPU by the density gate, which compares NUMBERS. That distinction is load-bearing: this repo has twice certified a wrong euler order with a check that asserted a SPELLING. Baselines held: op parity 770/770, golden 31/32, batch 7/8, and the three hand-built density shapes 100/100 each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f8461c1dfed34e4d9e6a9937261895e92cfdcfbe
by plankatron <fleeky@gmail.com> · 2026-09-08T14:44:57.000Z

Changed files (3) +123 -0

src/graph/proc_node_graph.cpp +51 -0
……@@ -414,6 +414,53 @@ String ProcNodeGraph::compile_to_glsl_function(const String &p_function_name,
414414 }
415415 }
416416
417 // ONE GRAPH, TWO PRODUCTS.
418 //
419 // A tree bridged from Blender is a MESH tree that also declares a density:
420 // the mountain subdivides a cage, displaces it and solidifies it through an
421 // SDF grid, and separately says "this float is my density" on its group
422 // interface. Emitting every node then refuses on SubdivideMesh -- a node
423 // that cannot affect the density and has no business being in the density
424 // shader at all.
425 //
426 // So when the graph DECLARES a density, only the cone that feeds it is
427 // emitted. This is not the silent drop the refusal below exists to catch:
428 // a node INSIDE the cone that cannot emit still refuses loudly, and the
429 // nodes excluded are excluded because the density provably does not read
430 // them. With no declared density the whole graph is emitted exactly as
431 // before, so every existing heightfield graph is untouched.
432 bool cone_only = false;
433 Vector<bool> in_cone;
434 {
435 int dn = -1, ds = -1;
436 if (find_density_output(dn, ds)) {
437 const int src = get_input_source_node(dn, ds);
438 if (src >= 0) {
439 cone_only = true;
440 in_cone.resize(nodes.size());
441 for (int i = 0; i < in_cone.size(); i++) {
442 in_cone.write[i] = false;
443 }
444 // Backward reachability over connections, iterated to a fixed
445 // point. The graph is small and this runs once per compile.
446 in_cone.write[src] = true;
447 bool grew = true;
448 while (grew) {
449 grew = false;
450 for (int c = 0; c < connections.size(); c++) {
451 const ProcConnection &conn = connections[c];
452 if (conn.to_node < 0 || conn.to_node >= in_cone.size()) continue;
453 if (conn.from_node < 0 || conn.from_node >= in_cone.size()) continue;
454 if (in_cone[conn.to_node] && !in_cone[conn.from_node]) {
455 in_cone.write[conn.from_node] = true;
456 grew = true;
457 }
458 }
459 }
460 }
461 }
462 }
463
417464 Vector<Ref<ProcNode>> gpu_nodes;
418465 Vector<int> gpu_indices;
419466
……@@ -427,6 +474,10 @@ String ProcNodeGraph::compile_to_glsl_function(const String &p_function_name,
427474 if (p_skip_types.has(String(nodes[i]->get_node_type()))) {
428475 continue;
429476 }
477 // Outside the declared density's cone: it cannot change the answer.
478 if (cone_only && i < in_cone.size() && !in_cone[i]) {
479 continue;
480 }
430481 // ANYTHING ELSE THAT CANNOT BE EMITTED IS A REFUSAL, NOT A SKIP. This
431482 // used to drop the node and emit the rest: adding a Math node to
432483 // map_dc_erosion's graph moved the emitted text by one comment line and
src/nodes/rotation_nodes.cpp +52 -0
……@@ -9,6 +9,7 @@
99// is worse than a missing one.
1010
1111#include "rotation_nodes.h"
12#include "../gpu/glsl_emit_context.h"
1213
1314#include "../graph/graph_executor.h"
1415
……@@ -512,4 +513,55 @@ void TransposeMatrixNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
512513 });
513514}
514515
516// --- GLSL for the two rotation nodes the density cone needs ------------------
517//
518// PN_EULER_ORDER is ZYX, so the basis is Rz * Ry * Rx and the quaternion is
519// qz * qy * qx -- matrix product order, which is what "ZYX" names here. The
520// composition is written out rather than fused so the order is readable at the
521// point it matters.
522static const char *PN_QUAT_GLSL = R"(
523#ifndef PN_QUAT_ROT_HELPERS
524#define PN_QUAT_ROT_HELPERS
525vec4 pn_quat_axis(vec3 axis, float ang) {
526 float h = ang * 0.5;
527 return vec4(axis * sin(h), cos(h));
528}
529vec4 pn_quat_mul(vec4 a, vec4 b) {
530 return vec4(a.w * b.xyz + b.w * a.xyz + cross(a.xyz, b.xyz),
531 a.w * b.w - dot(a.xyz, b.xyz));
532}
533vec4 pn_quat_from_euler_zyx(vec3 e) {
534 vec4 qz = pn_quat_axis(vec3(0.0, 0.0, 1.0), e.z);
535 vec4 qy = pn_quat_axis(vec3(0.0, 1.0, 0.0), e.y);
536 vec4 qx = pn_quat_axis(vec3(1.0, 0.0, 0.0), e.x);
537 return pn_quat_mul(pn_quat_mul(qz, qy), qx);
538}
539vec3 pn_quat_xform(vec4 q, vec3 v) {
540 q = normalize(q);
541 vec3 t = 2.0 * cross(q.xyz, v);
542 return v + q.w * t + cross(q.xyz, t);
543}
544#endif
545)";
546
547String EulerToRotationNode::compile_glsl_prelude(int) const {
548 return String(PN_QUAT_GLSL);
549}
550
551String EulerToRotationNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
552 return p_ctx.declare(p_node_idx, 0) + String(" = pn_quat_from_euler_zyx(") +
553 p_ctx.in(p_node_idx, 0) + String(");");
554}
555
556String RotateVectorNode::compile_glsl_prelude(int) const {
557 return String(PN_QUAT_GLSL);
558}
559
560String RotateVectorNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
561 // execute() normalizes the quaternion before xform; pn_quat_xform does the
562 // same, so an unnormalized Rotation arriving from either side behaves alike.
563 return p_ctx.declare(p_node_idx, 0) + String(" = pn_quat_xform(") +
564 p_ctx.in(p_node_idx, 1) + String(", ") + p_ctx.in(p_node_idx, 0) + String(");");
565}
566
515567} // namespace godot
src/nodes/rotation_nodes.h +20 -0
……@@ -38,6 +38,16 @@ public:
3838 void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
3939
4040 EulerToRotationNode();
41 // GLSL, because MTN_CellSpace's lattice rotation sits inside the bridged
42 // mountain's DENSITY cone -- the emitter refused the whole graph on it.
43 // ⚠ The euler order is PN_EULER_ORDER (ZYX): Blender's XYZ euler is Godot's
44 // ZYX, one naming the order rotations are APPLIED and the other the order
45 // matrices are MULTIPLIED. This is held to the CPU by
46 // tests/test_emitted_density_parity.cpp, which compares NUMBERS -- a check
47 // that asserted the spelling of an order certified the wrong one twice.
48 bool supports_gpu() const override { return true; }
49 String compile_glsl_prelude(int p_node_idx) const override;
50 String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
4151};
4252
4353class QuaternionToRotationNode : public ProcNode {
……@@ -150,6 +160,16 @@ public:
150160 void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
151161
152162 RotateVectorNode();
163 // GLSL, because MTN_CellSpace's lattice rotation sits inside the bridged
164 // mountain's DENSITY cone -- the emitter refused the whole graph on it.
165 // ⚠ The euler order is PN_EULER_ORDER (ZYX): Blender's XYZ euler is Godot's
166 // ZYX, one naming the order rotations are APPLIED and the other the order
167 // matrices are MULTIPLIED. This is held to the CPU by
168 // tests/test_emitted_density_parity.cpp, which compares NUMBERS -- a check
169 // that asserted the spelling of an order certified the wrong one twice.
170 bool supports_gpu() const override { return true; }
171 String compile_glsl_prelude(int p_node_idx) const override;
172 String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
153173};
154174
155175class InputRotationNode : public ProcNode {