#include "volume_nodes.h"
#include "../graph/graph_executor.h"
#include "../graph/proc_node_graph.h"
#include "../core/proc_mesh.h"
#include "dc_density_grid.h"
#include "dc_mesher.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include "../graph/graph_executor.h"
namespace godot {
// ── SphereSdfNode ──────────────────────────────────────────────────────────────
void SphereSdfNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_radius", "radius"), &SphereSdfNode::set_radius);
ClassDB::bind_method(D_METHOD("get_radius"), &SphereSdfNode::get_radius);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radius"), "set_radius", "get_radius");
}
SphereSdfNode::SphereSdfNode() {
input_defs.push_back({ "Center", SocketType::Vector, Vector3(0, 0, 0) });
input_defs.push_back({ "Radius", SocketType::Float, 1.0f });
output_defs.push_back({ "Density", SocketType::Float, 0.0f });
}
void SphereSdfNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Vector3 center = p_ctx.read_vector(p_node_idx, 0);
float r = p_ctx.read_float(p_node_idx, 1);
// For CPU evaluation, density at pos = r - length(pos - center)
// Since we don't have a single eval position in the mesh context,
// output the radius as a density proxy. Volume evaluation uses GLSL.
p_ctx.write_float(p_node_idx, 0, r);
}
String SphereSdfNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Centre is a VECTOR socket. This emitted `length(pos - pc.param0)`, where
// pc.param0 is one float: GLSL broadcasts it, so the sphere sat on the
// diagonal x=y=z at the radius' value rather than at the centre the graph
// asked for. It compiled, which is why nothing caught it.
return p_ctx.declare(p_node_idx, 0) + " = " + p_ctx.in(p_node_idx, 1) +
" - length(pos - " + p_ctx.in(p_node_idx, 0) + ");";
}
// ── VolumeBooleanNode ──────────────────────────────────────────────────────────
void VolumeBooleanNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "operation"), &VolumeBooleanNode::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &VolumeBooleanNode::get_operation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation", PROPERTY_HINT_ENUM, "Union,Intersect,Subtract"), "set_operation", "get_operation");
BIND_ENUM_CONSTANT(OP_UNION);
BIND_ENUM_CONSTANT(OP_INTERSECT);
BIND_ENUM_CONSTANT(OP_SUBTRACT);
}
VolumeBooleanNode::VolumeBooleanNode() {
input_defs.push_back({ "A", SocketType::Float, 0.0f });
input_defs.push_back({ "B", SocketType::Float, 0.0f });
output_defs.push_back({ "Density", SocketType::Float, 0.0f });
}
void VolumeBooleanNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
float a = p_ctx.read_float(p_node_idx, 0);
float b = p_ctx.read_float(p_node_idx, 1);
float result = 0.0f;
switch (operation) {
case OP_UNION: result = Math::max(a, b); break;
case OP_INTERSECT: result = Math::min(a, b); break;
case OP_SUBTRACT: result = Math::min(a, -b); break;
}
p_ctx.write_float(p_node_idx, 0, result);
}
String VolumeBooleanNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Read density_%d_a and density_%d_b -- names NOTHING declared, so any graph
// with this node emitted a shader that could not compile. The operands are
// its two inputs, resolved through the graph.
const String a = p_ctx.in(p_node_idx, 0);
const String b = p_ctx.in(p_node_idx, 1);
String expr;
switch (operation) {
case OP_UNION: expr = "max(" + a + ", " + b + ")"; break;
case OP_INTERSECT: expr = "min(" + a + ", " + b + ")"; break;
case OP_SUBTRACT: expr = "min(" + a + ", -(" + b + "))"; break;
default: expr = a; break;
}
return p_ctx.declare(p_node_idx, 0) + " = " + expr + ";";
}
// ── VolumeToMeshNode ───────────────────────────────────────────────────────────
void VolumeToMeshNode::_bind_methods() {
}
VolumeToMeshNode::VolumeToMeshNode() {
// Socket 0 is the DENSITY and find_density_output() names it by index, so
// it must stay first. The rest are the meshing controls; Resolution 0 means
// "carry the volume, do not mesh", which is what every existing graph gets.
input_defs.push_back({ "Density", SocketType::Float, 0.0f }); // 0
input_defs.push_back({ "Resolution", SocketType::Int, 0 }); // 1
input_defs.push_back({ "Bounds Min", SocketType::Vector, Vector3(-1, -1, -1) }); // 2
input_defs.push_back({ "Bounds Size", SocketType::Vector, Vector3(2, 2, 2) }); // 3
input_defs.push_back({ "Threshold", SocketType::Float, 0.0f }); // 4
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
namespace {
// THE GRAPH AS A DENSITY FIELD, for libdc's extractor.
//
// dc_mesh_from_grid wants a filled DensityGrid, and DensityGrid::fill_generator
// wants a DCGenerator. The graph's density at a POSITION is only obtainable by
// running the graph, so this runs it -- once per batch, not once per corner,
// through evaluate_density_batch and the evaluate_batch override added to
// DCGenerator for exactly this shape.
class GraphDensityGenerator : public DCGenerator {
ProcNodeGraph *graph_;
float threshold_;
public:
GraphDensityGenerator(ProcNodeGraph *g, float t) : graph_(g), threshold_(t) {}
float evaluate(float x, float y, float z, float) override {
return (float)graph_->evaluate_density_at(Vector3(x, y, z)) - threshold_;
}
void evaluate_batch(const float *p_xyz, int p_count, float, float *r_out) override {
PackedVector3Array pts;
pts.resize(p_count);
Vector3 *w = pts.ptrw();
for (int i = 0; i < p_count; i++) {
w[i] = Vector3(p_xyz[i * 3], p_xyz[i * 3 + 1], p_xyz[i * 3 + 2]);
}
PackedFloat32Array d = graph_->evaluate_density_batch(pts);
const int n = MIN(p_count, d.size());
for (int i = 0; i < n; i++) {
r_out[i] = d[i] - threshold_;
}
for (int i = n; i < p_count; i++) {
r_out[i] = -1.0f; // outside, rather than uninitialised
}
}
};
} // namespace
void VolumeToMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
// FILL THE VOLUME. This used to write an EMPTY ProcVolume on an empty
// ProcGeometry -- no mesh, no density, no message -- so a graph ending here
// baked to nothing and looked like a graph that never ran.
//
// ProcVolume has carried `glsl_source` with the signature
// `float proc_density(vec3 pos)` since it was written; nothing ever put one
// in it. The graph can emit exactly that, and now does.
Ref<ProcGeometry> geo;
geo.instantiate();
if (meshing_) {
// THE NESTED PASS. Meshing runs the graph per grid batch, which runs
// this node again; without this it recurses until the stack ends.
// Upstream still computes the density -- that is what the extractor is
// reading -- so returning a bare geometry here costs nothing.
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
Ref<ProcVolume> vol;
vol.instantiate();
if (p_ctx.graph != nullptr) {
vol->glsl_source = p_ctx.graph->compile_to_glsl_function(
"proc_density", false, PackedStringArray());
if (vol->glsl_source.is_empty()) {
// The emitter has already said which node refused. Say what it
// costs here, the way dc-terrain's _recompile_glsl does: an empty
// volume is a cloud that does not render, and silence about it is
// what made this node look finished for as long as it did.
UtilityFunctions::push_error(
"proc-nodes: Volume to Mesh could not compile this graph to a "
"density function, so its ProcVolume carries no GLSL and will "
"render nothing. See the emission error above for the node "
"that refused.");
}
}
geo->set_volume(vol);
// ── AND THE MESH, which is what the node is named for ────────────────────
//
// Isosurface extraction: density field -> constructed mesh. The extractor is
// dc-terrain's libdc -- Dual Contouring, octree, QEF -- compiled into this
// library rather than reimplemented, because a second copy of an algorithm
// is the failure this repo keeps paying to undo.
const int resolution = p_ctx.read_int(p_node_idx, 1);
if (resolution > 0 && p_ctx.graph != nullptr) {
const Vector3 bmin = p_ctx.read_vector(p_node_idx, 2);
const Vector3 bsize = p_ctx.read_vector(p_node_idx, 3);
const float threshold = p_ctx.read_float(p_node_idx, 4);
const float span = MAX(MAX(bsize.x, bsize.y), bsize.z);
if (span > 0.0f) {
// const_cast: evaluate_density_batch caches a plan and a scratch
// geometry, so it is non-const for reasons that are bookkeeping.
// The graph is not modified.
ProcNodeGraph *g = const_cast<ProcNodeGraph *>(p_ctx.graph);
GraphDensityGenerator gen(g, threshold);
const float voxel = span / (float)resolution;
const float origin[3] = { (float)bmin.x, (float)bmin.y, (float)bmin.z };
meshing_ = true;
DensityGrid grid;
grid.allocate(resolution, voxel, origin);
grid.fill_generator(&gen, voxel);
grid.compute_normals();
DCMesherConfig cfg;
DCMeshData md = dc_mesh_from_grid(grid, cfg);
meshing_ = false;
const int vcount = (int)md.positions.size() / 3;
if (vcount == 0 || md.indices.empty()) {
// ⛔ THE EXTRACTOR IS FINE. THE CPU DENSITY IS NOT.
//
// SphereSdfNode::execute writes its RADIUS as a "density proxy"
// -- its own comment says so -- and VolumeBooleanNode combines
// those proxies, so the field handed to Dual Contouring is a
// constant and a constant has no zero crossing. The volume
// nodes have a real compile_glsl and a stub execute(); only the
// GPU half was ever written.
//
// Measured 2026-09-06: a unit-sphere graph returns 1.0 at every
// point, inside and outside.
//
// Fixing it means the volume nodes evaluating per POSITION on
// the CPU -- taking a Vector input that defaults to the implicit
// Position field, the way the texture nodes do -- not anything
// about the extractor, which is wired and reachable.
UtilityFunctions::push_warning(
"proc-nodes: Volume to Mesh extracted no surface. The volume "
"nodes have no CPU implementation -- SphereSDF writes its "
"radius as a placeholder -- so the density field is constant "
"and has no zero crossing. The GLSL path is unaffected.");
}
if (vcount > 0 && !md.indices.empty()) {
Ref<ProcMesh> mesh = geo->ensure_mesh();
mesh->positions.resize(vcount);
Vector3 *pw = mesh->positions.ptrw();
for (int i = 0; i < vcount; i++) {
pw[i] = Vector3(md.positions[i * 3], md.positions[i * 3 + 1],
md.positions[i * 3 + 2]);
}
if ((int)md.normals.size() == vcount * 3) {
mesh->normals.resize(vcount);
Vector3 *nw = mesh->normals.ptrw();
for (int i = 0; i < vcount; i++) {
nw[i] = Vector3(md.normals[i * 3], md.normals[i * 3 + 1],
md.normals[i * 3 + 2]);
}
}
mesh->indices.resize((int)md.indices.size());
int32_t *iw = mesh->indices.ptrw();
for (size_t i = 0; i < md.indices.size(); i++) {
iw[i] = md.indices[i];
}
// Dual Contouring emits TRIANGLES, so any ngon grouping the
// input had is gone -- clearing face_offsets is what every
// topology-changing operation here does.
mesh->face_offsets.clear();
}
}
}
p_ctx.write_geometry(p_node_idx, 0, geo);
}
String VolumeToMeshNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Read density_%d_input, which nothing declared. It is a pass-through, and
// its output is a GEOMETRY socket with no GLSL spelling, so there is nothing
// to declare: the value it forwards is its input, and the density function
// has no way to return it. See docs/glsl-emitter-design.md -- choosing the
// graph's OUTPUT node is not something this emitter can express yet.
return String();
}
} // namespace godot