#include "point_nodes.h"
#include "../core/proc_geometry.h"
#include "../core/proc_mesh.h"
#include "../core/proc_pointcloud.h"
#include "../graph/graph_executor.h"
#include <cmath>
#include <cstdlib>
namespace godot {
// Simple LCG for deterministic random
static uint32_t _lcg_next(uint32_t &state) {
state = state * 1664525u + 1013904223u;
return state;
}
static float _lcg_float(uint32_t &state) {
return (float)(_lcg_next(state) & 0x00FFFFFF) / (float)0x00FFFFFF;
}
// --- PointsNode ---
void PointsNode::_bind_methods() {
}
PointsNode::PointsNode() {
// Blender's three. Position and Radius are FIELDS over the points being
// created, and without them every point sat at the origin with a fixed
// radius -- the node made a count of points and nothing else.
input_defs.push_back({ "Count", SocketType::Int, 10 });
input_defs.push_back({ "Position", SocketType::Vector, Vector3() });
input_defs.push_back({ "Radius", SocketType::Float, 0.1f });
output_defs.push_back({ "Points", SocketType::Geometry, Variant() });
}
void PointsNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
int count = p_ctx.read_int(p_node_idx, 0);
count = CLAMP(count, 0, 100000);
Ref<ProcPointCloud> pc;
pc.instantiate();
pc->positions.resize(count);
pc->radii.resize(count);
Vector3 *pos = pc->positions.ptrw();
float *rad = pc->radii.ptrw();
SocketValue pos_v = p_ctx.read_input(p_node_idx, 1);
SocketValue rad_v = p_ctx.read_input(p_node_idx, 2);
for (int i = 0; i < count; i++) {
pos[i] = pos_v.vec_at(i);
rad[i] = rad_v.float_at(i);
}
Ref<ProcGeometry> geo;
geo.instantiate();
geo->set_pointcloud(pc);
p_ctx.write_geometry(p_node_idx, 0, geo);
}
// --- PointsToVerticesNode ---
void PointsToVerticesNode::_bind_methods() {
}
PointsToVerticesNode::PointsToVerticesNode() {
input_defs.push_back({ "Points", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}
void PointsToVerticesNode::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_pointcloud().is_null()) {
p_ctx.write_geometry(p_node_idx, 0, geo);
return;
}
Ref<ProcPointCloud> pc = geo->get_pointcloud();
// Selection decides which points become vertices; unselected ones are
// dropped, so the vertex COUNT depends on it.
SocketValue sel = p_ctx.read_input(p_node_idx, 1);
Ref<ProcMesh> mesh;
mesh.instantiate();
for (int i = 0; i < pc->positions.size(); i++) {
if (sel.bool_at(i)) {
mesh->positions.push_back(pc->positions[i]);
}
}
// No indices = point cloud as vertices only
Ref<ProcGeometry> out;
out.instantiate();
out->set_mesh(mesh);
p_ctx.write_geometry(p_node_idx, 0, out);
}
// --- DistributePointsOnFacesNode ---
void DistributePointsOnFacesNode::_bind_methods() {
}
DistributePointsOnFacesNode::DistributePointsOnFacesNode() {
// BLENDER'S SEVEN INPUTS AND THREE OUTPUTS. Ours were Mesh, Count, Seed --
// so the invented Count sat on Blender's SELECTION and Seed on Distance
// Min, and Normal and Rotation did not exist. Defaults measured off
// Blender 5.2: Distance Min 0, Density Max 10, Density 10, Factor 1.
input_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
input_defs.push_back({ "Selection", SocketType::Bool, true });
input_defs.push_back({ "Distance Min", SocketType::Float, 0.0f });
input_defs.push_back({ "Density Max", SocketType::Float, 10.0f });
input_defs.push_back({ "Density", SocketType::Float, 10.0f });
input_defs.push_back({ "Density Factor", SocketType::Float, 1.0f });
input_defs.push_back({ "Seed", SocketType::Int, 0 });
// OURS, after Blender's. A direct count is not a thing Blender's node can
// express, and dropping it would break Godot-authored graphs.
input_defs.push_back({ "Count", SocketType::Int, 0 });
output_defs.push_back({ "Points", SocketType::Geometry, Variant() });
output_defs.push_back({ "Normal", SocketType::Vector, Variant() });
output_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
}
void DistributePointsOnFacesNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
const float density = p_ctx.read_float(p_node_idx, 4);
const float density_factor = p_ctx.read_float(p_node_idx, 5);
const int seed = p_ctx.read_int(p_node_idx, 6);
int count = p_ctx.read_int(p_node_idx, 7);
count = CLAMP(count, 0, 100000);
// Validate input has a mesh with triangles
if (geo.is_null() || geo->get_mesh().is_null()) {
Ref<ProcGeometry> empty;
empty.instantiate();
p_ctx.write_geometry(p_node_idx, 0, empty);
return;
}
Ref<ProcMesh> mesh = geo->get_mesh();
int tri_count = mesh->triangle_count();
if (tri_count == 0) {
Ref<ProcGeometry> empty;
empty.instantiate();
p_ctx.write_geometry(p_node_idx, 0, empty);
return;
}
if (count == 0) {
Ref<ProcGeometry> empty;
empty.instantiate();
p_ctx.write_geometry(p_node_idx, 0, empty);
return;
}
const Vector3 *verts = mesh->positions.ptr();
const int32_t *indices = mesh->indices.ptr();
// Step 1: Compute area of each triangle and build cumulative area array
Vector<float> cumulative_area;
cumulative_area.resize(tri_count);
float *cum = cumulative_area.ptrw();
float total_area = 0.0f;
for (int t = 0; t < tri_count; t++) {
Vector3 v0 = verts[indices[t * 3]];
Vector3 v1 = verts[indices[t * 3 + 1]];
Vector3 v2 = verts[indices[t * 3 + 2]];
Vector3 e1 = v1 - v0;
Vector3 e2 = v2 - v0;
float area = e1.cross(e2).length() * 0.5f;
total_area += area;
cum[t] = total_area;
}
if (total_area <= 0.0f) {
Ref<ProcGeometry> empty;
empty.instantiate();
p_ctx.write_geometry(p_node_idx, 0, empty);
return;
}
// COUNT COMES FROM DENSITY x AREA, which is what Blender's node expresses;
// the explicit Count is ours and wins when it is set.
//
// NOT positionally identical to Blender. Blender scatters per TRIANGLE,
// taking each triangle's point count from
// `round_probabilistic(area * density * factor)` seeded by
// `noise::hash(tri_i, seed)`, so matching its actual points needs its
// RandomNumberGenerator ported too. That is a separate job from the socket
// shape this commit is fixing, and claiming parity here would be the
// plausible-wrong-answer this project keeps finding.
if (count == 0) {
const float wanted = total_area * MAX(density, 0.0f) * MAX(density_factor, 0.0f);
count = CLAMP((int)(wanted + 0.5f), 0, 100000);
}
if (count == 0) {
Ref<ProcGeometry> empty;
empty.instantiate();
p_ctx.write_geometry(p_node_idx, 0, empty);
return;
}
// Step 2: Generate points
uint32_t rng_state = (uint32_t)seed;
// Warm up the LCG a few rounds to decorrelate from seed
_lcg_next(rng_state);
_lcg_next(rng_state);
_lcg_next(rng_state);
Ref<ProcPointCloud> pc;
pc.instantiate();
pc->positions.resize(count);
pc->radii.resize(count);
Vector3 *out_pos = pc->positions.ptrw();
float *out_rad = pc->radii.ptrw();
PackedVector3Array normals;
normals.resize(count);
for (int i = 0; i < count; i++) {
// Step 3a: Select face by area-weighted random (binary search in cumulative array)
float target = _lcg_float(rng_state) * total_area;
// Binary search for the face
int lo = 0;
int hi = tri_count - 1;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (cum[mid] < target) {
lo = mid + 1;
} else {
hi = mid;
}
}
int face = lo;
// Step 3b: Random barycentric coordinates
float u = _lcg_float(rng_state);
float v = _lcg_float(rng_state);
if (u + v > 1.0f) {
u = 1.0f - u;
v = 1.0f - v;
}
float w = 1.0f - u - v;
// Step 3c: Compute point position
Vector3 v0 = verts[indices[face * 3]];
Vector3 v1 = verts[indices[face * 3 + 1]];
Vector3 v2 = verts[indices[face * 3 + 2]];
out_pos[i] = v0 * u + v1 * v + v2 * w;
out_rad[i] = 0.01f;
normals.ptrw()[i] = (v1 - v0).cross(v2 - v0).normalized();
}
Ref<ProcGeometry> out;
out.instantiate();
out->set_pointcloud(pc);
p_ctx.write_geometry(p_node_idx, 0, out);
// Blender's Normal and Rotation are FIELDS over the points it made. The
// rotation aligns +Z to the normal, which is what its "align_rotations"
// does for the default axis.
p_ctx.write_vec_field(p_node_idx, 1, normals, AttrDomain::Point);
{
PackedVector4Array rots;
rots.resize(count);
for (int i = 0; i < count; i++) {
const Vector3 n = normals[i];
Quaternion q;
const Vector3 up(0, 0, 1);
const float d = up.dot(n);
if (d < -0.9999f) {
q = Quaternion(1, 0, 0, 0); // opposite: half turn about X
} else {
const Vector3 axis = up.cross(n);
q = Quaternion(axis.x, axis.y, axis.z, 1.0f + d).normalized();
}
rots.ptrw()[i] = Vector4(q.x, q.y, q.z, q.w);
}
p_ctx.write_rot_field(p_node_idx, 2, rots, AttrDomain::Point);
}
}
} // namespace godot