#include "mesh_ops_nodes.h"
#include <godot_cpp/templates/hash_set.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include "../graph/graph_executor.h"
#include <cmath>
namespace godot {
// --- FlipFacesNode ---
void FlipFacesNode::_bind_methods() {
}
FlipFacesNode::FlipFacesNode() {
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
// Blender's Selection, which decides WHICH faces flip. Without it the node
// flipped every face while advertising no choice about it.
input_defs.push_back({ "Selection", SocketType::Bool, true });
output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}
void FlipFacesNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
// SELECTION IS PER FACE, and it is a field. A selection arriving on the
// POINT domain is interpolated the way Blender interpolates a boolean onto
// faces: the face is selected only if EVERY one of its corners is.
SocketValue sel = p_ctx.read_input(p_node_idx, 1);
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
const bool by_point = sel.is_field && sel.domain == AttrDomain::Point;
int32_t *idx = mesh->indices.ptrw();
const int tcount = mesh->indices.size() / 3;
Vector<bool> flipped_vert;
flipped_vert.resize(mesh->normals.size());
for (int i = 0; i < flipped_vert.size(); i++) {
flipped_vert.set(i, false);
}
for (int t = 0; t < tcount; t++) {
int32_t *tri = idx + t * 3;
bool selected = by_point
? (sel.bool_at(tri[0]) && sel.bool_at(tri[1]) && sel.bool_at(tri[2]))
: sel.bool_at(t);
if (!selected) {
continue;
}
int32_t tmp = tri[1];
tri[1] = tri[2];
tri[2] = tmp;
// Only the normals of vertices belonging to a flipped face turn, and
// each turns once however many flipped faces share it.
for (int k = 0; k < 3; k++) {
if (tri[k] < flipped_vert.size()) {
flipped_vert.set(tri[k], true);
}
}
}
Vector3 *nor = mesh->normals.ptrw();
for (int i = 0; i < mesh->normals.size(); i++) {
if (flipped_vert[i]) {
nor[i] = -nor[i];
}
}
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- SetShadeSmoothNode ---
void SetShadeSmoothNode::_bind_methods() {
}
SetShadeSmoothNode::SetShadeSmoothNode() {
// Blender's order: Geometry, Selection, Shade Smooth. Ours had the flag
// where the SELECTION goes, so a graph wiring a selection set the flag and
// a graph wiring the flag set nothing.
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Shade Smooth", SocketType::Bool, true });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
void SetShadeSmoothNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
// SOCKET 2. Selection took socket 1 to match Blender's order, and reading
// the old index here would have made a wired selection set the flag.
bool smooth = p_ctx.read_bool(p_node_idx, 2);
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
if (smooth) {
// Smooth: recalculate vertex normals (area-weighted average)
mesh->recalculate_normals();
} else {
// Flat: split vertices so each triangle has its own normals
int tri_count = mesh->indices.size() / 3;
PackedVector3Array new_pos;
PackedVector3Array new_nor;
PackedVector2Array new_uv;
PackedInt32Array new_idx;
new_pos.resize(tri_count * 3);
new_nor.resize(tri_count * 3);
new_uv.resize(tri_count * 3);
new_idx.resize(tri_count * 3);
const Vector3 *old_pos = mesh->positions.ptr();
const int32_t *old_idx = mesh->indices.ptr();
const Vector2 *old_uv = mesh->uvs.size() > 0 ? mesh->uvs.ptr() : nullptr;
Vector3 *np = new_pos.ptrw();
Vector3 *nn = new_nor.ptrw();
Vector2 *nu = new_uv.ptrw();
int32_t *ni = new_idx.ptrw();
for (int t = 0; t < tri_count; t++) {
int i0 = old_idx[t * 3];
int i1 = old_idx[t * 3 + 1];
int i2 = old_idx[t * 3 + 2];
Vector3 p0 = old_pos[i0], p1 = old_pos[i1], p2 = old_pos[i2];
Vector3 fn = -(p1 - p0).cross(p2 - p0).normalized();
int base = t * 3;
np[base] = p0; np[base + 1] = p1; np[base + 2] = p2;
nn[base] = fn; nn[base + 1] = fn; nn[base + 2] = fn;
if (old_uv) {
nu[base] = old_uv[i0];
nu[base + 1] = old_uv[i1];
nu[base + 2] = old_uv[i2];
}
ni[base] = base; ni[base + 1] = base + 1; ni[base + 2] = base + 2;
}
mesh->positions = new_pos;
mesh->normals = new_nor;
mesh->uvs = new_uv;
mesh->indices = new_idx;
}
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- SubdivideMeshNode ---
void SubdivideMeshNode::_bind_methods() {
}
SubdivideMeshNode::SubdivideMeshNode() {
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Level", SocketType::Int, 1 });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
void SubdivideMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
int levels = CLAMP(p_ctx.read_int(p_node_idx, 1), 0, 5);
if (geo.is_null() || geo->get_mesh().is_null() || levels == 0) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
// ⛔ THIS NODE USED TO READ `indices` AS A FLAT TRIANGLE LIST.
//
// `indices` is the CORNER array; `face_offsets` groups it into faces. On a
// triangle mesh those coincide, and for as long as every mesh arrived here
// triangulated nobody could tell. The moment the bridge started carrying
// Blender's own topology, the mountain's 10086-QUAD cage was read as 13448
// triangles cut out of quad corners three at a time -- 1520981 vertices of
// plausible nonsense against Blender's 2582018.
//
// Blender's Subdivide Mesh is SIMPLE (linear) subdivision, and its topology
// step is Catmull-Clark's: every face becomes N QUADS, one per corner, each
// spanning corner -> next edge midpoint -> face centre -> previous edge
// midpoint. A quad becomes 4 quads; a TRIANGLE BECOMES 3 QUADS, not 4
// triangles, which is what this produced before and is a different mesh.
for (int level = 0; level < levels; level++) {
const int face_count = mesh->face_count();
const int32_t *idx = mesh->indices.ptr();
const Vector3 *pos = mesh->positions.ptr();
const bool has_nor = mesh->normals.size() == mesh->positions.size();
const bool has_uv = mesh->uvs.size() == mesh->positions.size();
PackedVector3Array new_positions = mesh->positions;
PackedVector3Array new_normals = has_nor ? mesh->normals : PackedVector3Array();
PackedVector2Array new_uvs = has_uv ? mesh->uvs : PackedVector2Array();
PackedInt32Array new_corners;
PackedInt32Array new_faces;
HashMap<uint64_t, int> edge_midpoints;
auto midpoint = [&](int a, int b) -> int {
const uint64_t key = (a < b) ? ((uint64_t)a << 32 | (uint32_t)b)
: ((uint64_t)b << 32 | (uint32_t)a);
HashMap<uint64_t, int>::ConstIterator it = edge_midpoints.find(key);
if (it != edge_midpoints.end()) {
return it->value;
}
const int n = new_positions.size();
new_positions.push_back((pos[a] + pos[b]) * 0.5f);
if (has_nor) {
new_normals.push_back(((mesh->normals[a] + mesh->normals[b]) * 0.5f).normalized());
}
if (has_uv) {
new_uvs.push_back((mesh->uvs[a] + mesh->uvs[b]) * 0.5f);
}
edge_midpoints[key] = n;
return n;
};
new_faces.push_back(0);
for (int f = 0; f < face_count; f++) {
const int begin = mesh->face_corner_begin(f);
const int n = mesh->face_size(f);
if (n < 3) {
continue;
}
// The face centre, once per face.
Vector3 c_pos;
Vector3 c_nor;
Vector2 c_uv;
for (int k = 0; k < n; k++) {
const int v = idx[begin + k];
c_pos += pos[v];
if (has_nor) {
c_nor += mesh->normals[v];
}
if (has_uv) {
c_uv += mesh->uvs[v];
}
}
const float inv = 1.0f / (float)n;
const int centre = new_positions.size();
new_positions.push_back(c_pos * inv);
if (has_nor) {
new_normals.push_back((c_nor * inv).normalized());
}
if (has_uv) {
new_uvs.push_back(c_uv * inv);
}
for (int k = 0; k < n; k++) {
const int v = idx[begin + k];
const int v_next = idx[begin + (k + 1) % n];
const int v_prev = idx[begin + (k + n - 1) % n];
const int m_next = midpoint(v, v_next);
const int m_prev = midpoint(v_prev, v);
new_corners.push_back(v);
new_corners.push_back(m_next);
new_corners.push_back(centre);
new_corners.push_back(m_prev);
new_faces.push_back(new_corners.size());
}
}
mesh->positions = new_positions;
mesh->normals = new_normals;
mesh->uvs = new_uvs;
mesh->indices = new_corners;
mesh->face_offsets = new_faces;
}
// Attributes cannot ride through this untouched: every domain changed size.
// Dropping the ones that no longer fit is not a loss of information -- they
// were already meaningless at the new size -- and it is said out loud rather
// than left as an array that still reads as data.
{
const AttrDomain doms[4] = { AttrDomain::Point, AttrDomain::Edge,
AttrDomain::Face, AttrDomain::Corner };
AttributeStorage *st[4] = { &mesh->point_attrs, &mesh->edge_attrs,
&mesh->face_attrs, &mesh->corner_attrs };
for (int d = 0; d < 4; d++) {
Vector<StringName> stale;
for (const KeyValue<StringName, Ref<AttributeArray>> &kv : st[d]->attributes) {
if (kv.value.is_valid() && kv.value->size() != mesh->domain_size(doms[d])) {
stale.push_back(kv.key);
}
}
for (int i = 0; i < stale.size(); i++) {
UtilityFunctions::push_warning(
"proc-nodes: Subdivide Mesh dropped attribute '", String(stale[i]),
"' on the ", attr_domain_name(doms[d]),
" domain -- subdivision changes that domain and this node does "
"not interpolate yet.");
st[d]->remove(stale[i]);
}
}
}
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- ScaleElementsNode ---
void ScaleElementsNode::_bind_methods() {
}
ScaleElementsNode::ScaleElementsNode() {
// BLENDER'S SIX INPUTS, and SCALE IS A FLOAT. Ours had it as a Vector at
// index 1 -- where Blender puts Selection -- so the type and the index were
// both wrong: a graph wiring a selection drove the scale, and a graph
// wiring a scale was a vector into a float socket.
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Scale", SocketType::Float, 1.0f });
input_defs.push_back({ "Center", SocketType::Vector, Vector3(0, 0, 0) });
// Blender 5.x MENU: Uniform or Single Axis.
input_defs.push_back({ "Scale Mode", SocketType::Int, 0 });
input_defs.push_back({ "Axis", SocketType::Vector, Vector3(1, 0, 0) });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
void ScaleElementsNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
SocketValue sel = p_ctx.read_input(p_node_idx, 1);
SocketValue scale_v = p_ctx.read_input(p_node_idx, 2);
SocketValue center_v = p_ctx.read_input(p_node_idx, 3);
const int scale_mode = p_ctx.read_int(p_node_idx, 4);
SocketValue axis_v = p_ctx.read_input(p_node_idx, 5);
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
Vector3 *pos = mesh->positions.ptrw();
// Blender scales each selected FACE about its own centre by default, the
// Center socket carrying an implicit per-face field. proc-nodes has no
// per-face implicit input, so an UNCONNECTED Center means the face's
// centroid here -- which is what Blender does -- and a connected one is
// used as given.
const bool center_given = p_ctx.is_input_connected(p_node_idx, 3);
const bool by_point = sel.is_field && sel.domain == AttrDomain::Point;
const int32_t *idx = mesh->indices.ptr();
const int tcount = mesh->indices.size() / 3;
// A vertex shared by several selected faces would otherwise be scaled once
// per face; Blender moves it once.
Vector<bool> moved;
moved.resize(mesh->positions.size());
for (int i = 0; i < moved.size(); i++) {
moved.set(i, false);
}
for (int t = 0; t < tcount; t++) {
const int32_t *tri = idx + t * 3;
bool selected = by_point
? (sel.bool_at(tri[0]) && sel.bool_at(tri[1]) && sel.bool_at(tri[2]))
: sel.bool_at(t);
if (!selected) {
continue;
}
Vector3 center = center_given
? center_v.vec_at(t)
: (pos[tri[0]] + pos[tri[1]] + pos[tri[2]]) / 3.0f;
const float sc = scale_v.float_at(t);
for (int k = 0; k < 3; k++) {
const int v = tri[k];
if (moved[v]) {
continue;
}
moved.set(v, true);
Vector3 offset = pos[v] - center;
if (scale_mode == 1) { // SINGLE_AXIS
Vector3 axis = axis_v.vec_at(t);
if (axis.length_squared() > 0.0f) {
axis = axis.normalized();
const float along = offset.dot(axis);
offset += axis * (along * (sc - 1.0f));
pos[v] = center + offset;
continue;
}
}
pos[v] = center + offset * sc;
}
}
mesh->recalculate_normals();
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- ExtrudeMeshNode ---
void ExtrudeMeshNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_mode", "m"), &ExtrudeMeshNode::set_mode);
ClassDB::bind_method(D_METHOD("get_mode"), &ExtrudeMeshNode::get_mode);
ADD_PROPERTY(PropertyInfo(Variant::INT, "mode"), "set_mode", "get_mode");
}
ExtrudeMeshNode::ExtrudeMeshNode() {
// BLENDER'S FIVE. Ours was Geometry and a FLOAT "Offset" at index 1 --
// which is where SELECTION is -- so a bridged selection drove the offset
// distance, Blender's vector Offset landed nowhere, and Offset Scale,
// Individual, Top and Side did not exist.
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Offset", SocketType::Vector, Vector3() });
input_defs.push_back({ "Offset Scale", SocketType::Float, 1.0f });
input_defs.push_back({ "Individual", SocketType::Bool, true });
output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
output_defs.push_back({ "Top", SocketType::Bool, Variant() });
output_defs.push_back({ "Side", SocketType::Bool, Variant() });
}
void ExtrudeMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
const int orig_verts = mesh->vertex_count();
const int orig_tris = mesh->triangle_count();
if (orig_verts == 0 || orig_tris == 0) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
SocketValue selection = p_ctx.materialize(p_ctx.read_input(p_node_idx, 1), geo);
// OFFSET IS AN IMPLICIT-FIELD SOCKET: unconnected it is the NORMAL, and its
// default_value is never read. Measured -- setting the default to (1, 0, 0)
// changes nothing in Blender, while leaving it unconnected still extrudes
// along +Z on an XY grid. Same trap as Random Value's ID.
const bool offset_linked = p_ctx.is_input_connected(p_node_idx, 2);
SocketValue offset_field;
if (offset_linked) {
offset_field = p_ctx.materialize(p_ctx.read_input(p_node_idx, 2), geo);
}
const float scale = p_ctx.read_float(p_node_idx, 3);
const bool have_normals = mesh->normals.size() == orig_verts;
const Vector3 *pos = mesh->positions.ptr();
const int32_t *idx = mesh->indices.ptr();
PackedVector3Array out_pos = mesh->positions;
PackedInt32Array out_idx;
PackedInt32Array top_face; // 1 per output triangle
PackedInt32Array side_face;
// Existing faces stay, and are neither top nor side.
for (int t = 0; t < orig_tris; t++) {
const bool sel = selection.bool_at(t);
if (sel) {
continue; // replaced by its extruded top
}
for (int c = 0; c < 3; c++) {
out_idx.push_back(idx[t * 3 + c]);
}
top_face.push_back(0);
side_face.push_back(0);
}
// INDIVIDUAL is Blender's default (true): each face extrudes on its own,
// so its vertices are not shared with a neighbour's.
for (int t = 0; t < orig_tris; t++) {
if (!selection.bool_at(t)) {
continue;
}
const int v0 = idx[t * 3 + 0], v1 = idx[t * 3 + 1], v2 = idx[t * 3 + 2];
Vector3 face_n = (pos[v1] - pos[v0]).cross(pos[v2] - pos[v0]).normalized();
int base = out_pos.size();
for (int c = 0; c < 3; c++) {
const int v = idx[t * 3 + c];
Vector3 off = offset_linked
? offset_field.vec_at(v)
: (have_normals ? mesh->normals[v] : face_n);
out_pos.push_back(pos[v] + off * scale);
}
// The top face, wound as the original.
out_idx.push_back(base + 0);
out_idx.push_back(base + 1);
out_idx.push_back(base + 2);
top_face.push_back(1);
side_face.push_back(0);
// Three side quads, two triangles each.
const int bot[3] = { v0, v1, v2 };
for (int e = 0; e < 3; e++) {
const int b0 = bot[e], b1 = bot[(e + 1) % 3];
const int t0 = base + e, t1 = base + ((e + 1) % 3);
out_idx.push_back(b0);
out_idx.push_back(b1);
out_idx.push_back(t1);
top_face.push_back(0);
side_face.push_back(1);
out_idx.push_back(b0);
out_idx.push_back(t1);
out_idx.push_back(t0);
top_face.push_back(0);
side_face.push_back(1);
}
}
mesh->positions = out_pos;
mesh->indices = out_idx;
// EVERY PER-VERTEX ARRAY describes the OLD topology once the vertex count
// changes, and a stale one is not merely wrong -- to_array_mesh refuses the
// surface outright ("array.size() != p_vertex_array_len") and the bake
// produces nothing. Dropping them follows DeleteGeometry's existing rule
// here: wrong data is harder to notice than absent data.
mesh->normals = PackedVector3Array();
mesh->tangents = PackedFloat32Array();
mesh->uvs = PackedVector2Array();
mesh->colors = PackedColorArray();
mesh->face_offsets = PackedInt32Array();
p_ctx.write_geometry(p_node_idx, 0, geo);
p_ctx.write_int_field(p_node_idx, 1, top_face, AttrDomain::Face);
p_ctx.write_int_field(p_node_idx, 2, side_face, AttrDomain::Face);
}
// --- MergeByDistanceNode ---
void MergeByDistanceNode::_bind_methods() {
}
MergeByDistanceNode::MergeByDistanceNode() {
// Blender's four. Distance sat at index 1, which is Selection -- so a graph
// wiring a selection drove the merge DISTANCE.
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
// Blender 5.x MENU: All or Connected. Only All is implemented.
input_defs.push_back({ "Mode", SocketType::Int, 0 });
input_defs.push_back({ "Distance", SocketType::Float, 0.001f });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
void MergeByDistanceNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
// SOCKET 3 -- Distance moved when Selection and Mode took 1 and 2. Reading
// the old index made the merge distance whatever the selection evaluated
// to, which is 1.0 for the default `true`.
float dist = p_ctx.read_float(p_node_idx, 3);
if (p_ctx.read_int(p_node_idx, 2) != 0) {
UtilityFunctions::push_warning(
"MergeByDistance: only the All mode is implemented; Connected "
"needs an edge domain, which ProcMesh has no array for.");
}
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
int count = mesh->vertex_count();
float dist_sq = dist * dist;
// Build vertex merge map
PackedInt32Array merge_map;
merge_map.resize(count);
int32_t *mm = merge_map.ptrw();
for (int i = 0; i < count; i++) mm[i] = i;
const Vector3 *pos = mesh->positions.ptr();
// O(n^2) — fine for moderate meshes
for (int i = 0; i < count; i++) {
if (mm[i] != i) continue; // already merged
for (int j = i + 1; j < count; j++) {
if (mm[j] != j) continue;
if (pos[i].distance_squared_to(pos[j]) < dist_sq) {
mm[j] = i;
}
}
}
// Compact: build new vertex list + remap
PackedInt32Array new_indices_map;
new_indices_map.resize(count);
int32_t *nim = new_indices_map.ptrw();
PackedVector3Array new_pos;
PackedVector3Array new_nor;
// EVERY array parallel to positions, not just the normals. Compacting two
// of four left uvs and colors at the OLD length, and an ArrayMesh whose
// arrays disagree produces no surface at all -- a merged mesh came back
// with zero vertices rather than the one it had merged to.
PackedVector2Array new_uv;
PackedColorArray new_col;
const bool has_uv = mesh->uvs.size() == count;
const bool has_col = mesh->colors.size() == count;
int new_count = 0;
// THE MERGED VERTEX SITS AT THE GROUP'S CENTROID, not at its first member.
// Measured: a 0.02 grid of 16 points merged with distance 0.05 gives
// Blender ONE vertex at (0, 0, 0), where keeping the first would put it at
// the corner (-0.01, -0.01, 0).
PackedVector3Array sum_pos;
PackedInt32Array group_size;
sum_pos.resize(count);
group_size.resize(count);
for (int i = 0; i < count; i++) {
sum_pos.ptrw()[i] = Vector3();
group_size.ptrw()[i] = 0;
}
for (int i = 0; i < count; i++) {
const int root = mm[i];
sum_pos.ptrw()[root] += pos[i];
group_size.ptrw()[root] += 1;
}
for (int i = 0; i < count; i++) {
if (mm[i] == i) {
nim[i] = new_count;
new_pos.push_back(sum_pos[i] / (float)MAX(group_size[i], 1));
if (mesh->normals.size() > i) {
new_nor.push_back(mesh->normals[i]);
}
if (has_uv) {
new_uv.push_back(mesh->uvs[i]);
}
if (has_col) {
new_col.push_back(mesh->colors[i]);
}
new_count++;
} else {
nim[i] = -1; // resolved later
}
}
// Resolve merged vertices
for (int i = 0; i < count; i++) {
if (mm[i] != i) {
nim[i] = nim[mm[i]];
}
}
// Remap indices
int32_t *idx = mesh->indices.ptrw();
for (int i = 0; i < mesh->indices.size(); i++) {
idx[i] = nim[idx[i]];
}
// Remove degenerate triangles
PackedInt32Array clean_idx;
for (int i = 0; i < mesh->indices.size(); i += 3) {
if (idx[i] != idx[i + 1] && idx[i + 1] != idx[i + 2] && idx[i] != idx[i + 2]) {
clean_idx.push_back(idx[i]);
clean_idx.push_back(idx[i + 1]);
clean_idx.push_back(idx[i + 2]);
}
}
mesh->positions = new_pos;
mesh->normals = new_nor;
mesh->uvs = new_uv;
mesh->colors = new_col;
mesh->indices = clean_idx;
// Per-corner and per-face arrays describe a topology that no longer
// exists, as in Delete Geometry.
mesh->tangents = PackedFloat32Array();
mesh->face_offsets = PackedInt32Array();
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- DualMeshNode ---
void DualMeshNode::_bind_methods() {
}
DualMeshNode::DualMeshNode() {
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
// Accepted and not yet acted on: keeping the boundary needs an edge domain
// to find the boundary WITH, and ProcMesh has no edge array.
input_defs.push_back({ "Keep Boundaries", SocketType::Bool, false });
output_defs.push_back({ "Dual Mesh", SocketType::Geometry, Variant() });
}
void DualMeshNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
int tri_count = mesh->triangle_count();
if (tri_count == 0) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
// Dual mesh: each triangle becomes a vertex (at centroid)
// Each original vertex becomes a face connecting its surrounding triangle centroids
const Vector3 *pos = mesh->positions.ptr();
const int32_t *idx = mesh->indices.ptr();
// Compute centroids
PackedVector3Array centroids;
centroids.resize(tri_count);
Vector3 *cw = centroids.ptrw();
for (int t = 0; t < tri_count; t++) {
cw[t] = (pos[idx[t * 3]] + pos[idx[t * 3 + 1]] + pos[idx[t * 3 + 2]]) / 3.0f;
}
// Build adjacency: for each vertex, which triangles use it
int vert_count = mesh->vertex_count();
Vector<Vector<int>> vert_tris;
vert_tris.resize(vert_count);
for (int t = 0; t < tri_count; t++) {
vert_tris.ptrw()[idx[t * 3]].push_back(t);
vert_tris.ptrw()[idx[t * 3 + 1]].push_back(t);
vert_tris.ptrw()[idx[t * 3 + 2]].push_back(t);
}
// Output: centroids are new vertices, each original vertex becomes fan of triangles
PackedVector3Array dual_pos = centroids;
PackedInt32Array dual_idx;
for (int v = 0; v < vert_count; v++) {
const Vector<int> &tris = vert_tris[v];
if (tris.size() < 3) continue;
// Fan triangulate around the vertex
for (int i = 1; i < tris.size() - 1; i++) {
dual_idx.push_back(tris[0]);
dual_idx.push_back(tris[i]);
dual_idx.push_back(tris[i + 1]);
}
}
mesh->positions = dual_pos;
mesh->indices = dual_idx;
mesh->normals.clear();
mesh->uvs.clear();
mesh->recalculate_normals();
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- DuplicateElementsNode ---
void DuplicateElementsNode::_bind_methods() {
}
DuplicateElementsNode::DuplicateElementsNode() {
// Blender's order: Geometry, SELECTION, Amount -- and a second output.
// Ours had Amount at index 1, so a graph wiring a selection set the count.
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Amount", SocketType::Int, 1 });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
// Which copy an element came from, which is how a graph tells the copies
// apart at all.
output_defs.push_back({ "Duplicate Index", SocketType::Int, Variant() });
}
void DuplicateElementsNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
// SOCKET 2 -- Amount moved when Selection took socket 1.
int amount = CLAMP(p_ctx.read_int(p_node_idx, 2), 0, 100);
if (geo.is_null() || geo->get_mesh().is_null() || amount <= 0) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
// Duplicate the mesh N times using the Join pattern (offset indices).
Ref<ProcGeometry> result = geo->duplicate_geometry();
Ref<ProcMesh> src_mesh = geo->get_mesh();
int src_vert_count = src_mesh->vertex_count();
int src_idx_count = src_mesh->indices.size();
if (src_vert_count == 0) {
p_ctx.write_geometry(p_node_idx, 0, result);
return;
}
Ref<ProcMesh> dst_mesh = result->get_mesh();
// Pre-allocate for total copies (original + amount duplicates)
int total_copies = amount + 1;
int total_verts = src_vert_count * total_copies;
int total_indices = src_idx_count * total_copies;
PackedVector3Array new_pos;
PackedVector3Array new_nor;
PackedVector2Array new_uvs;
PackedInt32Array new_idx;
new_pos.resize(total_verts);
new_idx.resize(total_indices);
bool has_normals = src_mesh->normals.size() == src_vert_count;
bool has_uvs = src_mesh->uvs.size() == src_vert_count;
if (has_normals) new_nor.resize(total_verts);
if (has_uvs) new_uvs.resize(total_verts);
const Vector3 *sp = src_mesh->positions.ptr();
const Vector3 *sn = has_normals ? src_mesh->normals.ptr() : nullptr;
const Vector2 *su = has_uvs ? src_mesh->uvs.ptr() : nullptr;
const int32_t *si = src_mesh->indices.ptr();
Vector3 *dp = new_pos.ptrw();
Vector3 *dn = has_normals ? new_nor.ptrw() : nullptr;
Vector2 *du = has_uvs ? new_uvs.ptrw() : nullptr;
int32_t *di = new_idx.ptrw();
for (int copy = 0; copy < total_copies; copy++) {
int vert_offset = copy * src_vert_count;
int idx_offset = copy * src_idx_count;
for (int v = 0; v < src_vert_count; v++) {
dp[vert_offset + v] = sp[v];
if (dn) dn[vert_offset + v] = sn[v];
if (du) du[vert_offset + v] = su[v];
}
for (int i = 0; i < src_idx_count; i++) {
di[idx_offset + i] = si[i] + vert_offset;
}
}
dst_mesh->positions = new_pos;
dst_mesh->normals = new_nor;
dst_mesh->uvs = new_uvs;
dst_mesh->indices = new_idx;
p_ctx.write_geometry(p_node_idx, 0, result);
}
// --- MeshBooleanNode ---
void MeshBooleanNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "o"), &MeshBooleanNode::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &MeshBooleanNode::get_operation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation"), "set_operation", "get_operation");
}
MeshBooleanNode::MeshBooleanNode() {
// Blender's shape: Mesh 1, then a MULTI-INPUT socket taking any number of
// operands, then the two flags. Ours was "A" and "B", which matched neither
// by name nor by count -- a third operand had nowhere to go.
input_defs.push_back({ "Mesh 1", SocketType::Geometry, Variant() });
SocketDef operands;
operands.name = "Mesh 2";
operands.type = SocketType::Geometry;
operands.multi = true;
input_defs.push_back(operands);
input_defs.push_back({ "Self Intersection", SocketType::Bool, false });
input_defs.push_back({ "Hole Tolerant", SocketType::Bool, false });
output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
output_defs.push_back({ "Intersecting Edges", SocketType::Bool, Variant() });
}
void MeshBooleanNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
// THE BOOLEAN ITSELF IS STILL NOT IMPLEMENTED, and this says so out loud
// rather than returning something that looks like a result.
//
// A real CSG needs exact predicates and a robust intersection pass;
// Blender's uses one. Joining the operands would LOOK right for disjoint
// meshes and be silently wrong for the overlapping ones the node exists
// for, which is the plausible-wrong-answer this project keeps finding. The
// sockets are Blender's now so a graph crosses intact and nothing is
// mis-wired; the geometry waits for the algorithm.
Ref<ProcGeometry> a = p_ctx.read_geometry(p_node_idx, 0);
const int operands = p_ctx.graph ? p_ctx.graph->get_input_link_count(p_node_idx, 1) : 0;
if (operands > 0) {
UtilityFunctions::push_warning(
"MeshBoolean: no CSG implementation -- returning Mesh 1 unchanged. ",
operands, " operand(s) ignored, operation ", operation, ".");
}
if (a.is_valid()) {
a = a->duplicate_geometry();
}
p_ctx.write_geometry(p_node_idx, 0, a);
// No intersection is computed, so nothing is an intersecting edge.
p_ctx.write_bool(p_node_idx, 1, false);
}
// --- SplitEdgesNode ---
void SplitEdgesNode::_bind_methods() {
}
SplitEdgesNode::SplitEdgesNode() {
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}
void SplitEdgesNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null() || geo->get_mesh().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
geo = geo->duplicate_geometry();
Ref<ProcMesh> mesh = geo->get_mesh();
if (mesh->triangle_count() == 0) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
// IT WAS A PASS-THROUGH because there was no edge domain to select on.
// There is one now, so the Selection field is read per EDGE and the node
// does the work: a split edge stops being shared, so the faces either side
// get their own copies of its vertices.
const PackedInt32Array edges = mesh->compute_edges();
const int n_edges = edges.size() / 2;
SocketValue selection = p_ctx.materialize(p_ctx.read_input(p_node_idx, 1), geo);
// Which undirected pairs are split.
HashSet<int64_t> split;
for (int e = 0; e < n_edges; e++) {
if (selection.bool_at(e)) {
split.insert(((int64_t)edges[e * 2] << 32) | (uint32_t)edges[e * 2 + 1]);
}
}
if (split.is_empty()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
PackedVector3Array out_pos = mesh->positions;
PackedInt32Array out_idx = mesh->indices;
const int tris = mesh->triangle_count();
const int32_t *src = mesh->indices.ptr();
int32_t *dst = out_idx.ptrw();
// EACH ORIGINAL VERTEX IS CLAIMED BY THE FIRST FACE THAT SEPARATES AT IT;
// later faces get copies. Duplicating for every face instead leaves the
// originals unreferenced -- which is how the first version produced 72
// vertices where Blender gives 60: 12 orphans nothing pointed at.
PackedInt32Array claimed;
claimed.resize(mesh->positions.size());
for (int i = 0; i < claimed.size(); i++) {
claimed.set(i, -1);
}
auto is_split = [&](int a, int b) {
const int lo = MIN(a, b), hi = MAX(a, b);
return split.has(((int64_t)lo << 32) | (uint32_t)hi);
};
for (int t = 0; t < tris; t++) {
for (int k = 0; k < 3; k++) {
const int v = src[t * 3 + k];
const int prev = src[t * 3 + (k + 2) % 3];
const int next = src[t * 3 + (k + 1) % 3];
// A vertex separates for this face only if an edge of THIS face
// meeting it is split.
if (!is_split(v, next) && !is_split(prev, v)) {
continue;
}
if (claimed[v] == -1) {
claimed.set(v, t); // this face keeps the original
continue;
}
if (claimed[v] == t) {
continue;
}
out_pos.push_back(mesh->positions[v]);
dst[t * 3 + k] = out_pos.size() - 1;
}
}
mesh->positions = out_pos;
mesh->indices = out_idx;
// Per-vertex arrays describe the old vertex count once it grows.
mesh->normals = PackedVector3Array();
mesh->tangents = PackedFloat32Array();
mesh->uvs = PackedVector2Array();
mesh->colors = PackedColorArray();
mesh->face_offsets = PackedInt32Array();
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- TriangulateNode ---
void TriangulateNode::_bind_methods() {
}
TriangulateNode::TriangulateNode() {
// Blender's four. Every proc-nodes mesh is already triangles, so this is a
// pass-through whatever the methods say -- the sockets exist so a Blender
// graph's links land instead of being dropped.
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Quad Method", SocketType::Int, 0 });
input_defs.push_back({ "N-gon Method", SocketType::Int, 0 });
output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}
void TriangulateNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
// All meshes in proc-nodes are already triangulated, so pass through.
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- EdgePathsToCurvesNode ---
void EdgePathsToCurvesNode::_bind_methods() {
}
EdgePathsToCurvesNode::EdgePathsToCurvesNode() {
// Blender's three. Both field inputs are accepted and not yet acted on:
// walking an edge path needs an edge domain, and ProcMesh has no edge
// array -- declared so the links land instead of being dropped.
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
input_defs.push_back({ "Start Vertices", SocketType::Bool, true });
input_defs.push_back({ "Next Vertex Index", SocketType::Int, -1 });
output_defs.push_back({ "Curves", SocketType::Geometry, Variant() });
}
void EdgePathsToCurvesNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
// Placeholder: pass through.
// Full implementation would extract boundary/feature edges and convert to ProcCurves.
p_ctx.write_geometry(p_node_idx, 0, geo);
}
} // namespace godot