OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / point_nodes.cpp

287 lines · cpp ref:
#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