#include "attribute_nodes.h"
#include "../graph/domain_adapt.h"
#include "../graph/graph_executor.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/templates/hash_set.hpp>
namespace godot {
// --- PositionNode ---
void PositionNode::_bind_methods() {
}
String PositionNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// `pos` is the density function's argument and the batch shader's per-vertex
// position -- the same name in both, which is why this one line serves both
// paths. See the note in the header for what its absence cost.
return p_ctx.declare(p_node_idx, 0) + " = pos;";
}
PositionNode::PositionNode() {
// Blender's Position has NO inputs -- it is a field, evaluated on whatever
// domain the consumer is working over. Ours took a Geometry socket only
// because it had no other way to know what to be a field of; that is what
// ProcEvalContext::context_geometry is for now.
output_defs.push_back({ "Position", SocketType::Vector, Variant() });
}
void PositionNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.context_geometry;
if (geo.is_null()) {
// Running in topological order, with no consumer asking yet. Leave a
// promise; whoever reads this will redeem it against its own geometry.
SocketValue sv;
sv.type = SocketType::Vector;
sv.is_lazy = true;
sv.lazy_node = p_node_idx;
sv.lazy_socket = 0;
p_ctx.write_output(p_node_idx, 0, sv);
return;
}
if (geo->get_mesh().is_null() || geo->get_mesh()->vertex_count() == 0) {
p_ctx.write_vector(p_node_idx, 0, Vector3());
return;
}
p_ctx.write_vec_field(p_node_idx, 0, geo->get_mesh()->positions, AttrDomain::Point);
}
// --- NormalNode ---
void NormalNode::_bind_methods() {
}
NormalNode::NormalNode() {
// Blender's Normal has no inputs, for the same reason Position has none.
// TWO outputs: the shading normal, which may be smoothed or custom, and the
// TRUE normal, which is the face's own geometry. A graph that needs to know
// which way a face actually points has to ask for the second one, and ours
// had no way to.
output_defs.push_back({ "Normal", SocketType::Vector, Variant() });
output_defs.push_back({ "True Normal", SocketType::Vector, Variant() });
}
void NormalNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.context_geometry;
if (geo.is_null()) {
SocketValue sv;
sv.type = SocketType::Vector;
sv.is_lazy = true;
sv.lazy_node = p_node_idx;
sv.lazy_socket = 0;
p_ctx.write_output(p_node_idx, 0, sv);
return;
}
Ref<ProcMesh> mesh = geo->get_mesh();
if (mesh.is_null() || mesh->normals.size() == 0) {
p_ctx.write_vector(p_node_idx, 0, Vector3(0, 1, 0));
p_ctx.write_vector(p_node_idx, 1, Vector3(0, 1, 0));
return;
}
p_ctx.write_vec_field(p_node_idx, 0, mesh->normals, AttrDomain::Point);
// TRUE NORMAL is recomputed from the triangles rather than read from the
// stored normals, which is the whole distinction: the stored ones can be
// smoothed, custom, or flipped by a node, and the true one is what the
// geometry says. Area-weighted from the faces touching each vertex.
PackedVector3Array truth;
truth.resize(mesh->positions.size());
for (int i = 0; i < truth.size(); i++) {
truth.ptrw()[i] = Vector3();
}
const int32_t *idx = mesh->indices.ptr();
for (int t = 0; t + 2 < mesh->indices.size(); t += 3) {
const Vector3 &a = mesh->positions[idx[t]];
const Vector3 &b = mesh->positions[idx[t + 1]];
const Vector3 &c = mesh->positions[idx[t + 2]];
Vector3 n = (b - a).cross(c - a);
truth.ptrw()[idx[t]] += n;
truth.ptrw()[idx[t + 1]] += n;
truth.ptrw()[idx[t + 2]] += n;
}
for (int i = 0; i < truth.size(); i++) {
Vector3 n = truth[i];
truth.ptrw()[i] = n.length_squared() > 0.0f ? n.normalized() : mesh->normals[i];
}
p_ctx.write_vec_field(p_node_idx, 1, truth, AttrDomain::Point);
}
// --- IndexNode ---
void IndexNode::_bind_methods() {
}
IndexNode::IndexNode() {
output_defs.push_back({ "Index", SocketType::Int, Variant() });
}
void IndexNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
// A CONSTANT 0 IS NOT AN INDEX. This wrote one with a TODO saying a field
// "requires knowing element count from a geometry consumer downstream" --
// which is exactly what the lazy promise PositionNode already leaves, so
// the mechanism it was waiting for had arrived.
//
// It matters beyond the node: Index is the canonical source for Random
// Value's ID, so with a constant 0 every element drew the SAME random
// number, and the node whose whole purpose is per-element variation had
// none. That read as a Random Value bug.
Ref<ProcGeometry> geo = p_ctx.context_geometry;
if (geo.is_null()) {
// Running in topological order, with no consumer asking yet. Leave a
// promise; whoever reads this will redeem it against its own geometry.
SocketValue sv;
sv.type = SocketType::Int;
sv.is_lazy = true;
sv.lazy_node = p_node_idx;
sv.lazy_socket = 0;
p_ctx.write_output(p_node_idx, 0, sv);
return;
}
if (geo->get_mesh().is_null() || geo->get_mesh()->vertex_count() == 0) {
p_ctx.write_int(p_node_idx, 0, 0);
return;
}
// ON THE CONSUMER'S DOMAIN. Index is not a point attribute that gets
// adapted -- it is the index of the element being evaluated, so on the
// corner domain it runs 0..corner_count-1. Adapting the point version
// instead would hand back each corner's VERTEX index, which is a different
// sequence that looks equally plausible.
const AttrDomain dom = p_ctx.context_domain;
const int n = geo->get_mesh()->domain_size(dom);
if (n <= 0) {
p_ctx.write_int(p_node_idx, 0, 0);
return;
}
PackedInt32Array idx;
idx.resize(n);
int32_t *w = idx.ptrw();
for (int i = 0; i < n; i++) {
w[i] = i;
}
p_ctx.write_int_field(p_node_idx, 0, idx, dom);
}
// --- StoreNamedAttributeNode ---
// --- UVNode ---
void UVNode::_bind_methods() {}
UVNode::UVNode() {
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
output_defs.push_back({ "UV", SocketType::Vector, Variant() });
}
void UVNode::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() || geo->get_mesh()->uvs.size() == 0) {
p_ctx.write_vector(p_node_idx, 0, Vector3());
return;
}
// Convert UV2 to vec3 field (Z=0)
Ref<ProcMesh> mesh = geo->get_mesh();
int count = mesh->uvs.size();
PackedVector3Array uv_field;
uv_field.resize(count);
Vector3 *out = uv_field.ptrw();
const Vector2 *uvs = mesh->uvs.ptr();
for (int i = 0; i < count; i++) {
out[i] = Vector3(uvs[i].x, 0.0f, uvs[i].y);
}
p_ctx.write_vec_field(p_node_idx, 0, uv_field);
}
String UVNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// In GPU context, UV is derived from position on XZ plane.
// For standalone function mode (DC terrain), pos IS the world position
// and UV-like 2D coords are just pos.xz. The upstream MeshGrid sets
// UV metadata; in GLSL we pass pos through since noise samples pos directly.
return "// UV: position passthrough (noise samples pos.xz directly)";
}
// --- StoreNamedAttributeNode ---
void StoreNamedAttributeNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_attr_name", "name"), &StoreNamedAttributeNode::set_attr_name);
ClassDB::bind_method(D_METHOD("get_attr_name"), &StoreNamedAttributeNode::get_attr_name);
ClassDB::bind_method(D_METHOD("set_attr_domain", "domain"), &StoreNamedAttributeNode::set_attr_domain);
ClassDB::bind_method(D_METHOD("get_attr_domain"), &StoreNamedAttributeNode::get_attr_domain);
ClassDB::bind_method(D_METHOD("set_attr_type", "type"), &StoreNamedAttributeNode::set_attr_type);
ClassDB::bind_method(D_METHOD("get_attr_type"), &StoreNamedAttributeNode::get_attr_type);
ClassDB::bind_method(D_METHOD("set_data_type", "type"), &StoreNamedAttributeNode::set_data_type);
ClassDB::bind_method(D_METHOD("get_data_type"), &StoreNamedAttributeNode::get_data_type);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "attr_name"), "set_attr_name", "get_attr_name");
ADD_PROPERTY(PropertyInfo(Variant::INT, "attr_domain"), "set_attr_domain", "get_attr_domain");
ADD_PROPERTY(PropertyInfo(Variant::INT, "attr_type"), "set_attr_type", "get_attr_type");
ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
}
// Blender's data_type is a CD_PROP_* code, and it decides BOTH the Value
// socket's type and the type of the attribute written. Measured off Blender
// 5.2 rather than transcribed -- the codes are not 0..n and the set is larger
// than the six the serializer's comment lists.
//
// An unrecognised code leaves the node as it was and says so: silently
// treating an unknown type as Float is how a stored attribute becomes its own
// red channel.
void StoreNamedAttributeNode::set_data_type(int p_type) {
SocketType sock;
AttrType attr;
switch (p_type) {
case 10: sock = SocketType::Float; attr = AttrType::Float; break; // FLOAT
case 11: sock = SocketType::Int; attr = AttrType::Int; break; // INT
case 45: sock = SocketType::Int; attr = AttrType::Int; break; // INT8
case 50: sock = SocketType::Bool; attr = AttrType::Bool; break; // BOOLEAN
case 48: sock = SocketType::Vector; attr = AttrType::Float3; break; // FLOAT_VECTOR
case 49: sock = SocketType::Vector; attr = AttrType::Float2; break; // FLOAT2
case 47: sock = SocketType::Color; attr = AttrType::Color; break; // FLOAT_COLOR
case 17: sock = SocketType::Color; attr = AttrType::ColorByte; break; // BYTE_COLOR
case 52: sock = SocketType::Rotation; attr = AttrType::Quaternion; break; // QUATERNION
case 20: sock = SocketType::Matrix; attr = AttrType::Float4x4; break; // FLOAT4X4
default:
UtilityFunctions::push_warning(
"StoreNamedAttribute: unrecognised Blender data_type ", p_type,
"; leaving the Value socket as it was rather than guessing Float.");
return;
}
data_type = p_type;
attr_type = (int)attr;
input_defs.ptrw()[3].type = sock;
}
StoreNamedAttributeNode::StoreNamedAttributeNode() {
// BLENDER'S FOUR INPUTS. Ours had two, so Selection did not exist and Value
// sat where Blender puts the NAME -- a graph wiring a value drove the name
// socket's index and a graph wiring a name was dropped.
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Name", SocketType::String, String() });
input_defs.push_back({ "Value", SocketType::Float, 0.0f });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
void StoreNamedAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
// SOCKET 3 -- Value moved when Selection and Name took 1 and 2.
//
// READ ON THE TARGET DOMAIN. `read_input` redeems a promise against the
// context geometry on the POINT domain, and the value was then indexed
// straight into the destination: on a 3x2 grid, corner 1 took vertex 1's
// value where Blender gives vertex 2's, and corners past the end of the
// point array came back ZERO. materialize_on evaluates the field on this
// node's own domain and adapts anything that arrives on another.
const AttrDomain target_domain = static_cast<AttrDomain>(attr_domain);
SocketValue val = p_ctx.materialize_on(
p_ctx._read_input_raw(p_node_idx, 3), geo, target_domain);
// The name comes from the SOCKET when a graph supplies one, and falls back
// to the node property so graphs authored in the Godot editor keep working.
{
String socket_name = p_ctx.read_input(p_node_idx, 2).str;
if (!socket_name.is_empty()) {
attr_name = StringName(socket_name);
}
}
geo = geo->duplicate_geometry();
AttrDomain domain = target_domain;
AttrType type = static_cast<AttrType>(attr_type);
if (geo->get_mesh().is_valid()) {
Ref<ProcMesh> mesh = geo->get_mesh();
int count = mesh->domain_size(domain);
if (count > 0) {
// Special case: writing to "color" attribute sets vertex colors directly
if (attr_name == StringName("color") && domain == AttrDomain::Point) {
mesh->colors.resize(count);
Color *colors = mesh->colors.ptrw();
if (val.is_field && val.type == SocketType::Color) {
for (int i = 0; i < count; i++) {
colors[i] = val.color_at(i);
}
} else if (val.is_field) {
// Float field → grayscale color
for (int i = 0; i < count; i++) {
float v = val.float_at(i);
colors[i] = Color(v, v, v, 1.0f);
}
} else {
float v = val.to_float();
Color c(v, v, v, 1.0f);
for (int i = 0; i < count; i++) {
colors[i] = c;
}
}
} else {
// General named attribute storage
Ref<AttributeArray> attr;
attr.instantiate();
attr->name = attr_name;
attr->type = type;
attr->domain = domain;
attr->resize(count);
// EVERY TYPE data_type CAN SELECT. Float and Int were the only
// two written, so a FLOAT_VECTOR or FLOAT_COLOR attribute was
// allocated at the right size and left as zeros -- which reads
// back as a black colour or no offset, not as an error.
switch (type) {
case AttrType::Float: {
float *data = attr->data_ptrw<float>();
for (int i = 0; i < count; i++) {
data[i] = val.float_at(i);
}
} break;
case AttrType::Int: {
int32_t *data = attr->data_ptrw<int32_t>();
for (int i = 0; i < count; i++) {
data[i] = val.int_at(i);
}
} break;
case AttrType::Bool: {
// One BYTE per element, not one int.
uint8_t *data = attr->data_ptrw<uint8_t>();
for (int i = 0; i < count; i++) {
data[i] = val.bool_at(i) ? 1 : 0;
}
} break;
case AttrType::Float2: {
Vector2 *data = attr->data_ptrw<Vector2>();
for (int i = 0; i < count; i++) {
Vector3 v = val.vec_at(i);
data[i] = Vector2(v.x, v.y);
}
} break;
case AttrType::Float3: {
Vector3 *data = attr->data_ptrw<Vector3>();
for (int i = 0; i < count; i++) {
data[i] = val.vec_at(i);
}
} break;
case AttrType::Color: {
Color *data = attr->data_ptrw<Color>();
for (int i = 0; i < count; i++) {
data[i] = val.color_at(i);
}
} break;
case AttrType::ColorByte: {
// Four bytes, so the float channels are quantised here
// rather than stored wide and truncated on read.
uint8_t *data = attr->data_ptrw<uint8_t>();
for (int i = 0; i < count; i++) {
Color c = val.color_at(i);
data[i * 4 + 0] = (uint8_t)CLAMP(Math::round(c.r * 255.0f), 0.0f, 255.0f);
data[i * 4 + 1] = (uint8_t)CLAMP(Math::round(c.g * 255.0f), 0.0f, 255.0f);
data[i * 4 + 2] = (uint8_t)CLAMP(Math::round(c.b * 255.0f), 0.0f, 255.0f);
data[i * 4 + 3] = (uint8_t)CLAMP(Math::round(c.a * 255.0f), 0.0f, 255.0f);
}
} break;
case AttrType::Quaternion: {
Quaternion *data = attr->data_ptrw<Quaternion>();
for (int i = 0; i < count; i++) {
data[i] = val.quat_at(i);
}
} break;
case AttrType::Float4x4: {
// 16 floats per element, column-major, matching
// SocketValue::mat_at.
float *data = attr->data_ptrw<float>();
for (int i = 0; i < count; i++) {
Projection m = val.mat_at(i);
for (int c = 0; c < 4; c++) {
for (int r = 0; r < 4; r++) {
data[i * 16 + c * 4 + r] = m.columns[c][r];
}
}
}
} break;
default:
UtilityFunctions::push_warning(
"StoreNamedAttribute: no writer for attribute type ",
attr_type_name(type), "; the attribute is allocated and empty.");
break;
}
switch (domain) {
case AttrDomain::Point:
mesh->point_attrs.set(attr_name, attr);
break;
case AttrDomain::Face:
mesh->face_attrs.set(attr_name, attr);
break;
case AttrDomain::Corner:
mesh->corner_attrs.set(attr_name, attr);
break;
case AttrDomain::Edge:
mesh->edge_attrs.set(attr_name, attr);
break;
default:
break;
}
}
}
}
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- NamedAttributeNode ---
void NamedAttributeNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_attr_name", "name"), &NamedAttributeNode::set_attr_name);
ClassDB::bind_method(D_METHOD("get_attr_name"), &NamedAttributeNode::get_attr_name);
ClassDB::bind_method(D_METHOD("set_data_type", "type"), &NamedAttributeNode::set_data_type);
ClassDB::bind_method(D_METHOD("get_data_type"), &NamedAttributeNode::get_data_type);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "attr_name"), "set_attr_name", "get_attr_name");
ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
}
// The READ side of the same typing. Blender's Named Attribute output socket is
// chosen by data_type just as Store's Value input is, and ours was a fixed
// Float that returned 0 for every attribute that was not one.
void NamedAttributeNode::set_data_type(int p_type) {
SocketType sock;
switch (p_type) {
case 10: sock = SocketType::Float; break; // FLOAT
case 11:
case 45: sock = SocketType::Int; break; // INT, INT8
case 50: sock = SocketType::Bool; break; // BOOLEAN
case 48:
case 49: sock = SocketType::Vector; break; // FLOAT_VECTOR, FLOAT2
case 47:
case 17: sock = SocketType::Color; break; // FLOAT_COLOR, BYTE_COLOR
case 52: sock = SocketType::Rotation; break; // QUATERNION
case 20: sock = SocketType::Matrix; break; // FLOAT4X4
default:
UtilityFunctions::push_warning(
"NamedAttribute: unrecognised Blender data_type ", p_type,
"; leaving the output socket as it was rather than guessing Float.");
return;
}
data_type = p_type;
output_defs.ptrw()[0].type = sock;
}
NamedAttributeNode::NamedAttributeNode() {
// Blender's Name is a SOCKET, not a property -- it can be computed. And
// there are two outputs: the attribute and whether it EXISTS, which is the
// only way a graph can branch on a missing attribute rather than silently
// reading zeros.
input_defs.push_back({ "Name", SocketType::String, String() });
output_defs.push_back({ "Attribute", SocketType::Float, Variant() });
output_defs.push_back({ "Exists", SocketType::Bool, false });
}
void NamedAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
{
String socket_name = p_ctx.read_input(p_node_idx, 0).str;
if (!socket_name.is_empty()) {
attr_name = StringName(socket_name);
}
}
Ref<ProcGeometry> geo = p_ctx.context_geometry;
if (geo.is_null()) {
SocketValue sv;
sv.type = SocketType::Float;
sv.is_lazy = true;
sv.lazy_node = p_node_idx;
sv.lazy_socket = 0;
p_ctx.write_output(p_node_idx, 0, sv);
return;
}
if (geo->get_mesh().is_null()) {
p_ctx.write_float(p_node_idx, 0, 0.0f);
return;
}
// Look up attribute and return average value (scalar context)
Ref<ProcMesh> mesh = geo->get_mesh();
Ref<AttributeArray> attr = mesh->point_attrs.get(attr_name);
// Blender's Named Attribute is a FIELD, and it is a field OF THE ATTRIBUTE'S
// OWN TYPE. Only Float was handled here, so an Int, Bool, vector or colour
// attribute fell through to the averaging below and came out as 0 -- which
// reads as "the attribute is not there", not as "this node cannot read it".
if (attr.is_valid() && attr->size() > 0) {
const int n = attr->size();
switch (attr->type) {
case AttrType::Float: {
PackedFloat32Array vals;
vals.resize(n);
const float *src = attr->data_ptr<float>();
float *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
p_ctx.write_float_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::Int: {
PackedInt32Array vals;
vals.resize(n);
const int32_t *src = attr->data_ptr<int32_t>();
int32_t *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
p_ctx.write_int_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::Bool: {
// Stored one BYTE per element; the field carries ints.
PackedInt32Array vals;
vals.resize(n);
const uint8_t *src = attr->data_ptr<uint8_t>();
int32_t *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = src[i] ? 1 : 0;
}
p_ctx.write_int_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::Float2: {
PackedVector3Array vals;
vals.resize(n);
const Vector2 *src = attr->data_ptr<Vector2>();
Vector3 *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = Vector3(src[i].x, src[i].y, 0.0f);
}
p_ctx.write_vec_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::Float3: {
PackedVector3Array vals;
vals.resize(n);
const Vector3 *src = attr->data_ptr<Vector3>();
Vector3 *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
p_ctx.write_vec_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::Color: {
PackedColorArray vals;
vals.resize(n);
const Color *src = attr->data_ptr<Color>();
Color *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
p_ctx.write_color_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::ColorByte: {
PackedColorArray vals;
vals.resize(n);
const uint8_t *src = attr->data_ptr<uint8_t>();
Color *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = Color(src[i * 4 + 0] / 255.0f, src[i * 4 + 1] / 255.0f,
src[i * 4 + 2] / 255.0f, src[i * 4 + 3] / 255.0f);
}
p_ctx.write_color_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
case AttrType::Quaternion: {
PackedVector4Array vals;
vals.resize(n);
const Quaternion *src = attr->data_ptr<Quaternion>();
Vector4 *dst = vals.ptrw();
for (int i = 0; i < n; i++) {
dst[i] = Vector4(src[i].x, src[i].y, src[i].z, src[i].w);
}
p_ctx.write_rot_field(p_node_idx, 0, vals, AttrDomain::Point);
return;
}
default:
// Float4x4 and the 2D integer types have no field writer to
// hand; fall through to the averaging path, which says 0.
break;
}
}
if (attr.is_null()) {
attr = mesh->face_attrs.get(attr_name);
}
if (attr.is_null()) {
attr = mesh->corner_attrs.get(attr_name);
}
// The EDGE domain is reachable now that ProcMesh derives edges; before,
// nothing could be stored there so nothing could be read back either.
if (attr.is_null()) {
attr = mesh->edge_attrs.get(attr_name);
}
if (attr.is_null() || attr->size() == 0) {
p_ctx.write_float(p_node_idx, 0, 0.0f);
return;
}
// Return average for scalar context
if (attr->type == AttrType::Float) {
const float *data = attr->data_ptr<float>();
float sum = 0.0f;
for (int i = 0; i < attr->size(); i++) {
sum += data[i];
}
p_ctx.write_float(p_node_idx, 0, sum / static_cast<float>(attr->size()));
} else {
p_ctx.write_float(p_node_idx, 0, 0.0f);
}
}
// --- CaptureAttributeNode ---
void CaptureAttributeNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_item_names", "n"), &CaptureAttributeNode::set_item_names);
ClassDB::bind_method(D_METHOD("get_item_names"), &CaptureAttributeNode::get_item_names);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "item_names"),
"set_item_names", "get_item_names");
ClassDB::bind_method(D_METHOD("set_item_types", "t"), &CaptureAttributeNode::set_item_types);
ClassDB::bind_method(D_METHOD("get_item_types"), &CaptureAttributeNode::get_item_types);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "item_types"),
"set_item_types", "get_item_types");
ClassDB::bind_method(D_METHOD("set_domain", "d"), &CaptureAttributeNode::set_domain);
ClassDB::bind_method(D_METHOD("get_domain"), &CaptureAttributeNode::get_domain);
ADD_PROPERTY(PropertyInfo(Variant::INT, "domain"), "set_domain", "get_domain");
}
// Blender's CD_PROP_* -> our socket type, for an item.
static SocketType _capture_socket_type(int p_cd, Variant &r_zero) {
switch (p_cd) {
case 11: case 45: r_zero = 0; return SocketType::Int;
case 50: r_zero = false; return SocketType::Bool;
case 48: case 49: r_zero = Vector3(); return SocketType::Vector;
case 47: case 17: r_zero = Color(0, 0, 0, 1); return SocketType::Color;
case 52: r_zero = Quaternion(); return SocketType::Rotation;
case 20: r_zero = Projection(); return SocketType::Matrix;
default: r_zero = 0.0f; return SocketType::Float;
}
}
// THE ITEM LIST BUILDS THE SOCKETS. Geometry and Selection are fixed; each item
// contributes one input and one output of its own type, in list order, exactly
// as Blender lays them out.
void CaptureAttributeNode::_rebuild_sockets() {
input_defs.clear();
output_defs.clear();
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
output_defs.push_back({ "Selection", SocketType::Bool, Variant() });
const int n = item_names.size();
for (int i = 0; i < n; i++) {
Variant zero;
const int cd = (i < item_types.size()) ? item_types[i] : 10;
const SocketType st = _capture_socket_type(cd, zero);
const StringName nm = StringName(item_names[i]);
input_defs.push_back({ nm, st, zero });
output_defs.push_back({ nm, st, Variant() });
}
}
CaptureAttributeNode::CaptureAttributeNode() {
// No items until the graph says so, which is Blender's empty node.
_rebuild_sockets();
}
void CaptureAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
p_ctx.write_geometry(p_node_idx, 0, geo);
p_ctx.write_output(p_node_idx, 1, p_ctx.read_input(p_node_idx, 1));
// CAPTURING IS EVALUATING NOW. Each item's field is materialised against
// THIS geometry and handed on as a concrete field, so a later change of
// topology cannot re-evaluate it -- which is the whole point of the node,
// and what a pass-through could never do.
const int n = item_names.size();
for (int i = 0; i < n; i++) {
SocketValue v = p_ctx.read_input(p_node_idx, 2 + i);
if (geo.is_valid()) {
v = p_ctx.materialize(v, geo);
}
p_ctx.write_output(p_node_idx, 2 + i, v);
}
}
// --- RemoveNamedAttributeNode ---
void RemoveNamedAttributeNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_attr_name", "name"), &RemoveNamedAttributeNode::set_attr_name);
ClassDB::bind_method(D_METHOD("get_attr_name"), &RemoveNamedAttributeNode::get_attr_name);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "attr_name"), "set_attr_name", "get_attr_name");
}
RemoveNamedAttributeNode::RemoveNamedAttributeNode() {
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
// Blender 5.x MENU socket: Exact or Wildcard. Carried as an int; only
// Exact is implemented, and a Wildcard pattern says so rather than
// quietly matching nothing.
input_defs.push_back({ "Pattern Mode", SocketType::Int, 0 });
input_defs.push_back({ "Name", SocketType::String, String() });
output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}
void RemoveNamedAttributeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
{
String socket_name = p_ctx.read_input(p_node_idx, 2).str;
if (!socket_name.is_empty()) {
attr_name = StringName(socket_name);
}
}
if (p_ctx.read_int(p_node_idx, 1) != 0) {
UtilityFunctions::push_warning(
"RemoveNamedAttribute: only the Exact pattern mode is implemented; "
"a wildcard removes nothing.");
}
geo = geo->duplicate_geometry();
if (geo->get_mesh().is_valid()) {
Ref<ProcMesh> mesh = geo->get_mesh();
mesh->point_attrs.remove(attr_name);
mesh->face_attrs.remove(attr_name);
mesh->corner_attrs.remove(attr_name);
mesh->edge_attrs.remove(attr_name);
}
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- DomainSizeNode ---
void DomainSizeNode::_bind_methods() {
}
DomainSizeNode::DomainSizeNode() {
// BLENDER'S SEVEN, IN BLENDER'S ORDER. Ours had five and no Edge Count, so
// from index 1 on every output was one place along: a graph reading Edge
// Count got Face Count, Face Count got Face Corner Count, and the link to
// Layer Count addressed a socket that did not exist. The two renames are
// Blender's spellings -- "Face Corner Count", not "Corner Count", and
// "Spline Count", not "Curve Count" -- so they resolve by name as well.
input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
output_defs.push_back({ "Point Count", SocketType::Int, Variant() });
output_defs.push_back({ "Edge Count", SocketType::Int, Variant() });
output_defs.push_back({ "Face Count", SocketType::Int, Variant() });
output_defs.push_back({ "Face Corner Count", SocketType::Int, Variant() });
output_defs.push_back({ "Spline Count", SocketType::Int, Variant() });
output_defs.push_back({ "Instance Count", SocketType::Int, Variant() });
output_defs.push_back({ "Layer Count", SocketType::Int, Variant() });
}
void DomainSizeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
if (geo.is_null()) {
for (int i = 0; i < 7; i++) {
p_ctx.write_int(p_node_idx, i, 0);
}
return;
}
// ProcMesh derives the edge domain now, so this asks the mesh rather than
// counting pairs here. It will still NOT always equal Blender's, and that
// is not an error: where Blender keeps quads and we carry triangles the two
// meshes genuinely have different edges.
int edge_count = 0;
if (geo->get_mesh().is_valid()) {
edge_count = geo->get_mesh()->edge_count();
}
p_ctx.write_int(p_node_idx, 0, geo->domain_size(AttrDomain::Point));
p_ctx.write_int(p_node_idx, 1, edge_count);
p_ctx.write_int(p_node_idx, 2, geo->domain_size(AttrDomain::Face));
p_ctx.write_int(p_node_idx, 3, geo->domain_size(AttrDomain::Corner));
p_ctx.write_int(p_node_idx, 4, geo->domain_size(AttrDomain::Curve));
p_ctx.write_int(p_node_idx, 5, geo->domain_size(AttrDomain::Instance));
// Layers are Grease Pencil, which ProcGeometry has no component for.
p_ctx.write_int(p_node_idx, 6, 0);
}
} // namespace godot