#include "proc_node_graph.h"
#include "graph_executor.h"
#include "../gpu/gpu_compiler.h"
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
namespace godot {
// --- Serialization ---
void ProcNodeGraph::_get_property_list(List<PropertyInfo> *p_list) const {
// Order matters: nodes first (connections and defaults reference node indices)
p_list->push_back(PropertyInfo(Variant::ARRAY, "nodes", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
p_list->push_back(PropertyInfo(Variant::DICTIONARY, "input_defaults", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
p_list->push_back(PropertyInfo(Variant::INT, "source_frame", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
}
bool ProcNodeGraph::_set(const StringName &p_name, const Variant &p_property) {
if (p_name == StringName("nodes")) {
Array arr = p_property;
nodes.clear();
for (int i = 0; i < arr.size(); i++) {
Ref<ProcNode> node = arr[i];
nodes.push_back(node);
}
return true;
}
if (p_name == StringName("connections")) {
PackedInt32Array arr = p_property;
connections.clear();
for (int i = 0; i + 3 < arr.size(); i += 4) {
ProcConnection c;
c.from_node = arr[i];
c.from_socket = arr[i + 1];
c.to_node = arr[i + 2];
c.to_socket = arr[i + 3];
connections.push_back(c);
}
return true;
}
if (p_name == StringName("source_frame")) {
source_frame = p_property;
return true;
}
if (p_name == StringName("input_defaults")) {
Dictionary dict = p_property;
Array keys = dict.keys();
for (int i = 0; i < keys.size(); i++) {
String key = keys[i];
int colon = key.find(":");
if (colon < 0) {
continue;
}
int node_idx = key.substr(0, colon).to_int();
int socket_idx = key.substr(colon + 1).to_int();
if (node_idx >= 0 && node_idx < nodes.size() && nodes[node_idx].is_valid()) {
nodes[node_idx]->set_input_default(socket_idx, dict[keys[i]]);
}
}
return true;
}
return false;
}
bool ProcNodeGraph::_get(const StringName &p_name, Variant &r_property) const {
if (p_name == StringName("nodes")) {
Array arr;
for (int i = 0; i < nodes.size(); i++) {
arr.push_back(nodes[i]);
}
r_property = arr;
return true;
}
if (p_name == StringName("connections")) {
PackedInt32Array arr;
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &c = connections[i];
arr.push_back(c.from_node);
arr.push_back(c.from_socket);
arr.push_back(c.to_node);
arr.push_back(c.to_socket);
}
r_property = arr;
return true;
}
if (p_name == StringName("source_frame")) {
r_property = source_frame;
return true;
}
if (p_name == StringName("input_defaults")) {
Dictionary dict;
for (int i = 0; i < nodes.size(); i++) {
const Ref<ProcNode> &node = nodes[i];
if (node.is_null()) {
continue;
}
for (int s = 0; s < node->get_input_count(); s++) {
if (node->is_input_default_modified(s)) {
dict[String::num_int64(i) + ":" + String::num_int64(s)] = node->get_input_def(s).default_value;
}
}
}
r_property = dict;
return true;
}
return false;
}
// --- Binding ---
Variant ProcNodeGraph::evaluate_socket(int p_node_idx, int p_socket_idx) const {
return GraphExecutor::evaluate_socket_value(*this, p_node_idx, p_socket_idx);
}
void ProcNodeGraph::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_source_frame", "frame"), &ProcNodeGraph::set_source_frame);
ClassDB::bind_method(D_METHOD("get_source_frame"), &ProcNodeGraph::get_source_frame);
ClassDB::bind_method(D_METHOD("add_node", "node"), &ProcNodeGraph::add_node);
ClassDB::bind_method(D_METHOD("remove_node", "index"), &ProcNodeGraph::remove_node);
ClassDB::bind_method(D_METHOD("get_node", "index"), &ProcNodeGraph::get_node);
ClassDB::bind_method(D_METHOD("get_node_count"), &ProcNodeGraph::get_node_count);
ClassDB::bind_method(D_METHOD("connect_sockets", "from_node", "from_socket", "to_node", "to_socket"), &ProcNodeGraph::connect_sockets);
ClassDB::bind_method(D_METHOD("disconnect_sockets", "from_node", "from_socket", "to_node", "to_socket"), &ProcNodeGraph::disconnect_sockets);
ClassDB::bind_method(D_METHOD("evaluate_socket", "node_idx", "socket_idx"), &ProcNodeGraph::evaluate_socket);
ClassDB::bind_method(D_METHOD("evaluate_density_batch", "positions"), &ProcNodeGraph::evaluate_density_batch);
ClassDB::bind_method(D_METHOD("get_density_bounds"), &ProcNodeGraph::get_density_bounds);
ClassDB::bind_method(D_METHOD("find_node_by_source_id", "source_id"), &ProcNodeGraph::find_node_by_source_id);
ClassDB::bind_method(D_METHOD("evaluate_density_at", "pos"), &ProcNodeGraph::evaluate_density_at);
ClassDB::bind_static_method("ProcNodeGraph",
D_METHOD("debug_phacelle_noise", "p", "dir", "freq", "offset", "normalization"),
&ProcNodeGraph::debug_phacelle_noise);
ClassDB::bind_static_method("ProcNodeGraph",
D_METHOD("debug_erosion_filter", "p", "height_and_slope", "fade_target",
"strength", "gully_weight", "detail", "rounding", "onset",
"assumed_slope", "scale", "octaves", "lacunarity", "gain",
"cell_scale", "normalization", "max_octaves"),
&ProcNodeGraph::debug_erosion_filter);
ClassDB::bind_static_method("ProcNodeGraph",
D_METHOD("debug_erosion_node_eval_full", "p", "gn", "height_scale",
"erosion_strength", "cell_size", "octaves", "gain",
"peak_height_divisor", "gully_band_scale",
"input_height_amplitude", "octave_lacunarity", "gully_depth",
"fine_gully_slope_restriction", "gully_normalize_threshold",
"height_offset_pair", "rounding_set", "onset_set",
"assumed_slope_pair", "max_octaves"),
&ProcNodeGraph::debug_erosion_node_eval_full);
ClassDB::bind_method(D_METHOD("get_connection_count"), &ProcNodeGraph::get_connection_count);
// Read-only, flat [from_node, from_socket, to_node, to_socket] quadruples --
// the same shape serialization already writes. Bound because a node whose
// socket TYPE depends on a property cannot be checked from values alone: a
// refused link and a narrowed one look identical downstream.
ClassDB::bind_method(D_METHOD("get_connections_flat"), &ProcNodeGraph::get_connections_flat);
ClassDB::bind_method(D_METHOD("get_input_link_count", "node_idx", "socket_idx"),
&ProcNodeGraph::get_input_link_count);
ClassDB::bind_method(D_METHOD("compile_to_glsl_function", "function_name", "clamp_octaves", "skip_types"), &ProcNodeGraph::compile_to_glsl_function, DEFVAL("proc_density"), DEFVAL(false), DEFVAL(PackedStringArray()));
ClassDB::bind_method(D_METHOD("compile_batch_glsl", "node_indices"), &ProcNodeGraph::compile_batch_glsl);
}
int ProcNodeGraph::add_node(const Ref<ProcNode> &p_node) {
_density_plan_dirty = true;
nodes.push_back(p_node);
return nodes.size() - 1;
}
void ProcNodeGraph::remove_node(int p_idx) {
_density_plan_dirty = true;
if (p_idx < 0 || p_idx >= nodes.size()) {
return;
}
// Remove connections involving this node
clear_connections_for_node(p_idx);
nodes.remove_at(p_idx);
// Fix up connection indices
for (int i = 0; i < connections.size(); i++) {
ProcConnection &c = connections.ptrw()[i];
if (c.from_node > p_idx) c.from_node--;
if (c.to_node > p_idx) c.to_node--;
}
}
Ref<ProcNode> ProcNodeGraph::get_node(int p_idx) const {
if (p_idx < 0 || p_idx >= nodes.size()) {
return Ref<ProcNode>();
}
return nodes[p_idx];
}
void ProcNodeGraph::connect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket) {
_density_plan_dirty = true;
// A CONNECTION TO A SOCKET THAT DOES NOT EXIST IS NOT MERELY DROPPED -- it
// used to be ACCEPTED, and `_read_input_raw` then computed
// `output_values[src_node * max_output_sockets + src_socket]` with no bound
// on src_socket. With max_output_sockets small, socket 2 of a one-output
// node lands on a DIFFERENT NODE'S slot and reads whatever is there; past
// the end of the array it is out of bounds outright.
//
// Found as a SEGFAULT: a bridged graph read Accumulate Field's Trailing and
// Total, which our node did not have, and the aliased slot happened to
// contain a lazy promise naming an unrelated producer. Before Index became
// a field the same read returned harmless garbage, so the defect had been
// there all along and was invisible.
//
// Refused and named, rather than silently dropped: a graph asking for a
// socket we do not implement is exactly what the missing-socket ledger is
// for, and it should be visible.
{
Ref<ProcNode> from = get_node(p_from_node);
Ref<ProcNode> to = get_node(p_to_node);
if (from.is_null() || to.is_null()) {
UtilityFunctions::push_warning(
"ProcNodeGraph: refusing a connection with a missing node (",
p_from_node, " -> ", p_to_node, ").");
return;
}
if (p_from_socket < 0 || p_from_socket >= from->get_output_count()) {
UtilityFunctions::push_warning(
"ProcNodeGraph: refusing ", from->get_node_type(), " output socket ",
p_from_socket, " -> ", to->get_node_type(), " input ", p_to_socket,
": the producer has only ", from->get_output_count(), " outputs.");
return;
}
if (p_to_socket < 0 || p_to_socket >= to->get_input_count()) {
UtilityFunctions::push_warning(
"ProcNodeGraph: refusing ", from->get_node_type(), " output ",
p_from_socket, " -> ", to->get_node_type(), " input socket ",
p_to_socket, ": the consumer has only ", to->get_input_count(),
" inputs.");
return;
}
}
// A MULTI-INPUT SOCKET KEEPS EVERY LINK, in the order they arrive. For an
// ordinary socket a second link still replaces the first, which is
// Blender's behaviour too.
{
Ref<ProcNode> to = get_node(p_to_node);
const bool multi = to.is_valid() && to->is_input_multi(p_to_socket);
if (multi) {
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &c = connections[i];
if (c.to_node == p_to_node && c.to_socket == p_to_socket &&
c.from_node == p_from_node && c.from_socket == p_from_socket) {
return; // already linked; do not duplicate
}
}
} else {
for (int i = connections.size() - 1; i >= 0; i--) {
const ProcConnection &c = connections[i];
if (c.to_node == p_to_node && c.to_socket == p_to_socket) {
connections.remove_at(i);
}
}
}
}
ProcConnection c;
c.from_node = p_from_node;
c.from_socket = p_from_socket;
c.to_node = p_to_node;
c.to_socket = p_to_socket;
connections.push_back(c);
}
void ProcNodeGraph::disconnect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket) {
_density_plan_dirty = true;
for (int i = connections.size() - 1; i >= 0; i--) {
const ProcConnection &c = connections[i];
if (c.from_node == p_from_node && c.from_socket == p_from_socket &&
c.to_node == p_to_node && c.to_socket == p_to_socket) {
connections.remove_at(i);
return;
}
}
}
void ProcNodeGraph::clear_connections_for_node(int p_node_idx) {
for (int i = connections.size() - 1; i >= 0; i--) {
const ProcConnection &c = connections[i];
if (c.from_node == p_node_idx || c.to_node == p_node_idx) {
connections.remove_at(i);
}
}
}
int ProcNodeGraph::get_input_source_node(int p_node_idx, int p_socket_idx) const {
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &c = connections[i];
if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
return c.from_node;
}
}
return -1;
}
int ProcNodeGraph::get_input_source_socket(int p_node_idx, int p_socket_idx) const {
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &c = connections[i];
if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
return c.from_socket;
}
}
return -1;
}
int ProcNodeGraph::find_output_node() const {
// Find the last node that has Geometry outputs but no downstream connections
for (int i = nodes.size() - 1; i >= 0; i--) {
const Ref<ProcNode> &node = nodes[i];
if (node.is_null()) continue;
bool has_geo_output = false;
for (int o = 0; o < node->get_output_count(); o++) {
if (node->get_output_def(o).type == SocketType::Geometry) {
has_geo_output = true;
break;
}
}
if (!has_geo_output) continue;
// Check if any connection uses this node as source
bool is_source = false;
for (int c = 0; c < connections.size(); c++) {
if (connections[c].from_node == i) {
is_source = true;
break;
}
}
if (!is_source) {
return i;
}
}
return nodes.size() > 0 ? nodes.size() - 1 : -1;
}
String ProcNodeGraph::compile_batch_glsl(const PackedInt32Array &p_node_indices) const {
Vector<Ref<ProcNode>> batch_nodes;
Vector<int> batch_indices;
for (int i = 0; i < p_node_indices.size(); i++) {
const int idx = p_node_indices[i];
if (idx < 0 || idx >= nodes.size() || nodes[idx].is_null()) {
continue;
}
batch_nodes.push_back(nodes[idx]);
batch_indices.push_back(idx);
}
return GpuCompiler::compile_batch(batch_nodes, batch_indices, this);
}
String ProcNodeGraph::compile_to_glsl_function(const String &p_function_name,
bool p_clamp_octaves, const PackedStringArray &p_skip_types) const {
// TOPOLOGICAL ORDER, not graph index order. A value cannot be read before it
// is written, and until inputs were resolved through connections the
// distinction did not exist -- every graph on disk happens to be in index
// order, which is what makes this change provable rather than hopeful.
Vector<int> order;
{
Vector<int> indegree;
indegree.resize(nodes.size());
for (int i = 0; i < nodes.size(); i++) {
indegree.write[i] = 0;
}
for (int c = 0; c < connections.size(); c++) {
const ProcConnection &conn = connections[c];
if (conn.to_node >= 0 && conn.to_node < indegree.size()) {
indegree.write[conn.to_node]++;
}
}
// Ties are broken by node index, so a graph with no ordering constraint
// emits exactly what it always did.
Vector<int> ready;
for (int i = 0; i < nodes.size(); i++) {
if (indegree[i] == 0) {
ready.push_back(i);
}
}
while (!ready.is_empty()) {
int best = 0;
for (int k = 1; k < ready.size(); k++) {
if (ready[k] < ready[best]) {
best = k;
}
}
const int idx = ready[best];
ready.remove_at(best);
order.push_back(idx);
for (int c = 0; c < connections.size(); c++) {
const ProcConnection &conn = connections[c];
if (conn.from_node != idx) {
continue;
}
if (conn.to_node < 0 || conn.to_node >= indegree.size()) {
continue;
}
if (--indegree.write[conn.to_node] == 0) {
ready.push_back(conn.to_node);
}
}
}
// A cycle leaves nodes unvisited. Emit them in index order afterwards
// rather than dropping them: a cycle is a graph bug, and losing nodes
// silently is the failure this whole change exists to remove.
if (order.size() != nodes.size()) {
for (int i = 0; i < nodes.size(); i++) {
if (!order.has(i)) {
order.push_back(i);
}
}
}
}
// 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. Emitting every node then refuses on SubdivideMesh -- a node
// that cannot affect the density and has no business being in the density
// shader at all.
//
// So when the graph DECLARES a density, only the cone that feeds it is
// emitted. This is not the silent drop the refusal below exists to catch:
// a node INSIDE the cone that cannot emit still refuses loudly, and the
// nodes excluded are excluded because the density provably does not read
// them. With no declared density the whole graph is emitted exactly as
// before, so every existing heightfield graph is untouched.
bool cone_only = false;
Vector<bool> in_cone;
{
int dn = -1, ds = -1;
if (find_density_output(dn, ds)) {
const int src = get_input_source_node(dn, ds);
if (src >= 0) {
cone_only = true;
in_cone.resize(nodes.size());
for (int i = 0; i < in_cone.size(); i++) {
in_cone.write[i] = false;
}
// Backward reachability over connections, iterated to a fixed
// point. The graph is small and this runs once per compile.
in_cone.write[src] = true;
bool grew = true;
while (grew) {
grew = false;
for (int c = 0; c < connections.size(); c++) {
const ProcConnection &conn = connections[c];
if (conn.to_node < 0 || conn.to_node >= in_cone.size()) continue;
if (conn.from_node < 0 || conn.from_node >= in_cone.size()) continue;
if (in_cone[conn.to_node] && !in_cone[conn.from_node]) {
in_cone.write[conn.from_node] = true;
grew = true;
}
}
}
}
}
}
Vector<Ref<ProcNode>> gpu_nodes;
Vector<int> gpu_indices;
for (int k = 0; k < order.size(); k++) {
const int i = order[k];
if (nodes[i].is_null()) {
continue;
}
// A SKIP LIST ENTRY IS DELIBERATE -- dc-terrain passes ErosionNoise when
// the erosion toggle is off -- and stays a skip.
if (p_skip_types.has(String(nodes[i]->get_node_type()))) {
continue;
}
// Outside the declared density's cone: it cannot change the answer.
if (cone_only && i < in_cone.size() && !in_cone[i]) {
continue;
}
// ANYTHING ELSE THAT CANNOT BE EMITTED IS A REFUSAL, NOT A SKIP. This
// used to drop the node and emit the rest: adding a Math node to
// map_dc_erosion's graph moved the emitted text by one comment line and
// the node's arithmetic was simply gone, so the GPU evaluated a
// different field from the CPU with nothing said. An empty return here
// leaves the consumer's previous shader running -- dc-terrain's
// _recompile_glsl already treats empty as "keep what you have" -- which
// is a stale world rather than a wrong one.
// A boundary node carries no operation, so emitting nothing is correct
// for it and is not the silent drop this refusal exists to catch.
//
// ⛔ IT IS STILL INCLUDED. Skipping it outright meant Group Input --
// which carries the modifier's exposed PARAMETERS on its outputs, and
// writes them in execute() -- never declared them, so every consumer
// referenced `n0_sN` and the shader would not compile:
//
// error: 'n0_s19' : undeclared identifier (x16, bridged mountain)
//
// compile_to_function accepts an empty emission from a transparent node
// and refuses it from any other, so a boundary node that has nothing to
// say still says nothing.
if (nodes[i]->gpu_transparent() && !nodes[i]->supports_gpu()) {
continue;
}
if (!nodes[i]->supports_gpu()) {
UtilityFunctions::push_error(
"proc-nodes: node ", i, " (", nodes[i]->get_node_type(),
") has no GPU implementation, so this graph cannot be compiled to GLSL. "
"Emission refused rather than silently dropping the node.");
return String();
}
gpu_nodes.push_back(nodes[i]);
gpu_indices.push_back(i);
}
if (gpu_nodes.size() == 0) {
return "float " + p_function_name + "(vec3 pos) { return -pos.y; }\n";
}
return GpuCompiler::compile_to_function(gpu_nodes, gpu_indices, p_function_name, p_clamp_octaves, this);
}
// --- The CPU side of proc_density ------------------------------------------
// Which value the graph's density IS, by the same rules gpu_compiler.cpp uses
// for the emitted function. Returns the node and socket of an explicit Group
// Output float, or false for "the displacement of pos.y".
bool ProcNodeGraph::find_density_output(int &r_node, int &r_socket) const {
for (int i = 0; i < get_node_count(); i++) {
Ref<ProcNode> n = get_node(i);
// TWO WAYS TO SAY IT, one rule. A Group Output float is the graph
// declaring its result through its interface; a Volume to Mesh's
// Density input is an authored VOLUME declaring the same thing at a
// terminal. Both are "this float is my density", and a volume graph
// should not have to carry a Group Output to be compilable.
if (n.is_null()) {
continue;
}
const StringName ty = n->get_node_type();
if (ty == StringName("VolumeToMesh")) {
// SOCKET 0 EXPLICITLY. Volume to Mesh has Resolution, Threshold and
// bounds sockets too, and "the first connected Float" would happily
// pick Threshold if Density were left unwired -- a density that is
// silently the iso level. Its Density input is socket 0 by
// construction, so name it.
if (n->get_input_count() > 0 && get_input_source_node(i, 0) >= 0) {
r_node = i;
r_socket = 0;
return true;
}
continue;
}
if (ty != StringName("GroupOutput")) {
continue;
}
for (int s = 0; s < n->get_input_count(); s++) {
if (n->get_input_def(s).type != SocketType::Float) {
continue;
}
if (get_input_source_node(i, s) < 0) {
continue;
}
r_node = i;
r_socket = s;
return true;
}
}
return false;
}
int ProcNodeGraph::find_node_by_source_id(int p_source_id) const {
if (p_source_id < 0) {
return -1;
}
for (int i = 0; i < get_node_count(); i++) {
Ref<ProcNode> n = get_node(i);
if (n.is_valid() && n->get_source_id() == p_source_id) {
return i;
}
}
return -1;
}
Dictionary ProcNodeGraph::get_density_bounds() const {
Dictionary out;
// The same terminal the density comes from: the graph's Group Output. Read
// by NAME, because a group interface's socket ORDER is the author's and
// changing it in Blender must not silently re-point a bound.
for (int i = 0; i < get_node_count(); i++) {
Ref<ProcNode> n = get_node(i);
if (n.is_null() || n->get_node_type() != StringName("GroupOutput")) {
continue;
}
bool have_min = false, have_max = false;
Vector3 bmin, bmax;
float voxel = 0.0f;
for (int s = 0; s < n->get_input_count(); s++) {
const StringName nm = n->get_input_def(s).name;
// A bound may be wired from a value node or left as the socket's
// own default; read whichever the author used.
Variant v;
const int src = get_input_source_node(i, s);
if (src >= 0) {
v = GraphExecutor::evaluate_socket_value(
*this, src, get_input_source_socket(i, s), Dictionary());
} else {
v = n->get_input_default(s);
}
if (nm == StringName("Bounds Min")) {
bmin = v;
have_min = true;
} else if (nm == StringName("Bounds Max")) {
bmax = v;
have_max = true;
} else if (nm == StringName("Voxel Size")) {
voxel = (float)(double)v;
}
}
if (!have_min || !have_max) {
// Half a box is not a box. An unbounded field is a legitimate
// state; a graph that names one corner and not the other is an
// authoring mistake, and answering with a default corner would
// place the world somewhere nobody asked for.
if (have_min != have_max) {
UtilityFunctions::push_warning(
"proc-nodes: the graph declares only one of Bounds Min / "
"Bounds Max on its Group Output. Both are needed; treating "
"the field as unbounded.");
}
continue;
}
if (is_blender_frame()) {
// M, self-inverse, the same conversion _to_godot_frame applies to
// geometry and the emitter applies to the query point.
bmin = Vector3(-bmin.x, bmin.z, bmin.y);
bmax = Vector3(-bmax.x, bmax.z, bmax.y);
// ⛔ M NEGATES x, so the converted pair is NOT sorted. Re-sort
// componentwise or the box comes out inverted on that axis.
const Vector3 lo(MIN(bmin.x, bmax.x), MIN(bmin.y, bmax.y), MIN(bmin.z, bmax.z));
const Vector3 hi(MAX(bmin.x, bmax.x), MAX(bmin.y, bmax.y), MAX(bmin.z, bmax.z));
bmin = lo;
bmax = hi;
}
out["min"] = bmin;
out["max"] = bmax;
out["voxel_size"] = voxel;
return out;
}
return out;
}
static bool _density_output_socket(const ProcNodeGraph &p_graph, int &r_node, int &r_socket) {
return p_graph.find_density_output(r_node, r_socket);
}
ProcNodeGraph::~ProcNodeGraph() {
delete _density_plan;
_density_plan = nullptr;
}
PackedFloat32Array ProcNodeGraph::evaluate_density_batch(const PackedVector3Array &p_positions) {
PackedFloat32Array out;
const int n = p_positions.size();
out.resize(n);
if (n == 0) {
return out;
}
float *w = out.ptrw();
for (int i = 0; i < n; i++) {
w[i] = 0.0f;
}
// One point of geometry per position. The graph displaces them exactly as
// it displaces a mesh's vertices, which is what the shader's `pos` is.
Ref<ProcGeometry> in_geo;
in_geo.instantiate();
Ref<ProcMesh> in_mesh = in_geo->ensure_mesh();
// ⛔ THE QUERY POINT IS IN THE CONSUMER'S FRAME, THE GRAPH IS IN ITS OWN.
//
// A bridged graph runs in Blender's frame and GraphExecutor converts its
// RESULT on the way out (_to_godot_frame). The input was never converted,
// so this compared an M-converted dst against an unconverted src and the
// two axes did not correspond: with zero displacement the density came back
// as src.z - src.y instead of 0.
//
// M on the way in, self-inverse, matching what compile_to_function now
// emits. src stays the caller's own point, so dst.y - src.y below is still
// the displacement along the CONSUMER's up.
if (is_blender_frame()) {
PackedVector3Array conv;
conv.resize(n);
Vector3 *cw = conv.ptrw();
const Vector3 *pr = p_positions.ptr();
for (int i = 0; i < n; i++) {
cw[i] = Vector3(-pr[i].x, pr[i].z, pr[i].y);
}
in_mesh->positions = conv;
} else {
in_mesh->positions = p_positions;
}
in_mesh->normals.resize(n);
for (int i = 0; i < n; i++) {
in_mesh->normals.ptrw()[i] = Vector3(0, 1, 0);
}
Dictionary overrides;
overrides["geometry"] = in_geo;
if (_density_plan == nullptr) {
_density_plan = new ExecutionPlan();
_density_plan_dirty = true;
}
if (_density_plan_dirty) {
*_density_plan = GraphExecutor::compile(*this);
_density_plan_dirty = false;
}
// use_gpu FALSE: this IS the CPU answer, and the point of it is to agree
// with the shader rather than to be the shader.
Ref<ProcGeometry> result = GraphExecutor::execute(*this, *_density_plan, overrides, false,
nullptr, 0.016f, nullptr);
// ⛔ A NULL GEOMETRY IS NOT A FAILURE FOR A GRAPH THAT DECLARES A DENSITY.
//
// This used to `return out` right here, BEFORE the density lookup below ever
// ran. A graph whose density is declared on its own Group Output interface
// has no geometry chain -- that is the point of it -- so execute()
// legitimately returns nothing and every point came back 0.0.
//
// Measured 2026-09-08 on the bridged voronoi mountain terminated that way:
// the emitted GLSL produced a correct signed 3D field while this returned
// zero at all 100 points, and find_density_output was finding the socket
// perfectly well a few lines further down than the function ever reached.
// The GPU and CPU halves of one graph disagreed completely, and the reason
// was an early return, not any arithmetic.
//
// Only the DISPLACEMENT path needs a mesh, so the check moves down to it.
int out_node = -1, out_socket = -1;
if (_density_output_socket(*this, out_node, out_socket)) {
// ⛔ TWO BUGS IN ONE LINE, both fixed here, in BOTH density entry points.
//
// out_socket is an INPUT socket of the terminal (Group Output's float,
// Volume to Mesh's Density). evaluate_socket_value reads an OUTPUT socket,
// so this read a different slot entirely. What the density IS lives on the
// node FEEDING that input.
//
// And it passed no overrides, so the fresh run evaluated against NO
// GEOMETRY -- discarding the points built immediately above and answering
// from an empty context. Measured 2026-09-06: a unit-sphere SDF returned
// 0.0 at every one of five points, inside and outside, through both the
// Group Output and the Volume to Mesh spelling. Nothing in proc-nodes,
// dc-terrain or hyper-tps calls these, which is why it stood.
const int src_node = get_input_source_node(out_node, out_socket);
const int src_socket = get_input_source_socket(out_node, out_socket);
if (src_node < 0) {
return out; // declared an output and wired nothing to it
}
const Variant v = GraphExecutor::evaluate_socket_value(
*this, src_node, src_socket, overrides);
// A scalar output is the same value at every point; a field is per point.
if (v.get_type() == Variant::PACKED_FLOAT32_ARRAY) {
PackedFloat32Array f = v;
for (int i = 0; i < n && i < f.size(); i++) {
w[i] = f[i];
}
} else {
const float c = (float)(double)v;
for (int i = 0; i < n; i++) {
w[i] = c;
}
}
return out;
}
if (result.is_null()) {
// No density terminal AND no geometry -- nothing to answer with.
return out;
}
Ref<ProcMesh> out_mesh = result->get_mesh();
if (out_mesh.is_null() || out_mesh->positions.size() != n) {
// A graph that changed the point COUNT has no per-input answer, and
// pairing them by index would invent one.
UtilityFunctions::push_warning(
"proc-nodes: evaluate_density_batch got ",
out_mesh.is_null() ? 0 : out_mesh->positions.size(),
" points back for ", n, " in. A graph that changes the point count "
"cannot answer a density per input point.");
return out;
}
const Vector3 *src = p_positions.ptr();
const Vector3 *dst = out_mesh->positions.ptr();
for (int i = 0; i < n; i++) {
w[i] = dst[i].y - src[i].y;
}
return out;
}
float ProcNodeGraph::evaluate_density_at(const Vector3 &p_pos) {
// The scratch geometry is built ONCE and its single position rewritten per
// query. Going through evaluate_density_batch instead allocates a fresh
// ProcGeometry and ProcMesh every call, and a collision query cannot pay
// for that.
if (_density_scratch.is_null()) {
_density_scratch.instantiate();
Ref<ProcMesh> m = _density_scratch->ensure_mesh();
m->positions.resize(1);
m->normals.resize(1);
m->normals.ptrw()[0] = Vector3(0, 1, 0);
}
Ref<ProcMesh> mesh = _density_scratch->get_mesh();
mesh->positions.ptrw()[0] = p_pos;
if (_density_plan == nullptr) {
_density_plan = new ExecutionPlan();
_density_plan_dirty = true;
}
if (_density_plan_dirty) {
*_density_plan = GraphExecutor::compile(*this);
_density_plan_dirty = false;
}
Dictionary overrides;
overrides["geometry"] = _density_scratch;
Ref<ProcGeometry> result = GraphExecutor::execute(*this, *_density_plan, overrides, false,
nullptr, 0.016f, nullptr);
if (result.is_null()) {
return 0.0f;
}
int out_node = -1, out_socket = -1;
if (_density_output_socket(*this, out_node, out_socket)) {
// ⛔ TWO BUGS IN ONE LINE, both fixed here, in BOTH density entry points.
//
// out_socket is an INPUT socket of the terminal (Group Output's float,
// Volume to Mesh's Density). evaluate_socket_value reads an OUTPUT socket,
// so this read a different slot entirely. What the density IS lives on the
// node FEEDING that input.
//
// And it passed no overrides, so the fresh run evaluated against NO
// GEOMETRY -- discarding the points built immediately above and answering
// from an empty context. Measured 2026-09-06: a unit-sphere SDF returned
// 0.0 at every one of five points, inside and outside, through both the
// Group Output and the Volume to Mesh spelling. Nothing in proc-nodes,
// dc-terrain or hyper-tps calls these, which is why it stood.
const int src_node = get_input_source_node(out_node, out_socket);
const int src_socket = get_input_source_socket(out_node, out_socket);
if (src_node < 0) {
return 0.0f; // declared an output and wired nothing to it
}
const Variant v = GraphExecutor::evaluate_socket_value(
*this, src_node, src_socket, overrides);
if (v.get_type() == Variant::PACKED_FLOAT32_ARRAY) {
PackedFloat32Array f = v;
return f.size() > 0 ? f[0] : 0.0f;
}
return (float)(double)v;
}
Ref<ProcMesh> out_mesh = result->get_mesh();
if (out_mesh.is_null() || out_mesh->positions.size() != 1) {
return 0.0f;
}
return out_mesh->positions[0].y - p_pos.y;
}
PackedInt32Array ProcNodeGraph::get_connections_flat() const {
PackedInt32Array out;
out.resize(connections.size() * 4);
int *w = out.ptrw();
for (int i = 0; i < connections.size(); i++) {
w[i * 4 + 0] = connections[i].from_node;
w[i * 4 + 1] = connections[i].from_socket;
w[i * 4 + 2] = connections[i].to_node;
w[i * 4 + 3] = connections[i].to_socket;
}
return out;
}
int ProcNodeGraph::get_input_link_count(int p_node_idx, int p_socket_idx) const {
int n = 0;
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &c = connections[i];
if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
n++;
}
}
return n;
}
bool ProcNodeGraph::get_input_link_at(int p_node_idx, int p_socket_idx, int p_k,
int &r_from_node, int &r_from_socket) const {
int n = 0;
for (int i = 0; i < connections.size(); i++) {
const ProcConnection &c = connections[i];
if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
if (n == p_k) {
r_from_node = c.from_node;
r_from_socket = c.from_socket;
return true;
}
n++;
}
}
return false;
}
} // namespace godot