OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / geometry_nodes.cpp

318 lines · cpp ref:
#include "geometry_nodes.h"
#include "../graph/graph_executor.h"

namespace godot {

// --- TransformGeometryNode ---

void TransformGeometryNode::_bind_methods() {
}

TransformGeometryNode::TransformGeometryNode() {
	// BLENDER'S LAYOUT, and the indices matter: a MENU socket sits at 1, so
	// Translation is 2 and not 1. Ours was short by that socket and by the
	// Transform matrix, and its Rotation was a Vector of euler DEGREES where
	// Blender's is a quaternion Rotation socket -- a bridged rotation was
	// therefore read as degrees-as-radians on the wrong type.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	// Blender 5.x MENU socket: "Components" or "Matrix". Carried as an int
	// because proc-nodes has no menu socket type; 0 is Components.
	input_defs.push_back({ "Mode", SocketType::Int, 0 });
	input_defs.push_back({ "Translation", SocketType::Vector, Vector3() });
	input_defs.push_back({ "Rotation", SocketType::Rotation, Quaternion() });
	input_defs.push_back({ "Scale", SocketType::Vector, Vector3(1, 1, 1) });
	input_defs.push_back({ "Transform", SocketType::Matrix, Projection() });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void TransformGeometryNode::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;
	}

	const int mode = p_ctx.read_int(p_node_idx, 1);

	Transform3D xform;
	if (mode == 1) {
		// MATRIX mode: the Transform socket IS the transform.
		Projection m = p_ctx.read_matrix(p_node_idx, 5);
		Basis b(Vector3(m.columns[0][0], m.columns[0][1], m.columns[0][2]),
				Vector3(m.columns[1][0], m.columns[1][1], m.columns[1][2]),
				Vector3(m.columns[2][0], m.columns[2][1], m.columns[2][2]));
		// Godot's Basis takes ROWS; a Projection stores columns, so this is
		// transposed on the way in.
		b = b.transposed();
		xform = Transform3D(b,
				Vector3(m.columns[3][0], m.columns[3][1], m.columns[3][2]));
	} else {
		Vector3 translation = p_ctx.read_vector(p_node_idx, 2);
		Quaternion rotation = p_ctx.read_rotation(p_node_idx, 3);
		Vector3 scale = p_ctx.read_vector(p_node_idx, 4);
		Basis basis(rotation.is_normalized() ? rotation : Quaternion());
		basis.scale(scale);
		xform = Transform3D(basis, translation);
	}

	// Duplicate to avoid mutating upstream
	geo = geo->duplicate_geometry();

	// Transform mesh positions and normals
	if (geo->get_mesh().is_valid()) {
		Ref<ProcMesh> mesh = geo->get_mesh();
		Vector3 *pos = mesh->positions.ptrw();
		int count = mesh->positions.size();
		for (int i = 0; i < count; i++) {
			pos[i] = xform.xform(pos[i]);
		}

		if (mesh->normals.size() == count) {
			Vector3 *nor = mesh->normals.ptrw();
			// From the composed transform, not from a local `basis` -- in
			// MATRIX mode there is no separate basis to take.
			Basis normal_basis = xform.basis.inverse().transposed();
			for (int i = 0; i < count; i++) {
				nor[i] = normal_basis.xform(nor[i]).normalized();
			}
		}
	}

	// Transform curve positions
	if (geo->get_curves().is_valid()) {
		Ref<ProcCurves> curves = geo->get_curves();
		Vector3 *pos = curves->positions.ptrw();
		for (int i = 0; i < curves->positions.size(); i++) {
			pos[i] = xform.xform(pos[i]);
		}
	}

	// Transform pointcloud positions
	if (geo->get_pointcloud().is_valid()) {
		Ref<ProcPointCloud> pc = geo->get_pointcloud();
		Vector3 *pos = pc->positions.ptrw();
		for (int i = 0; i < pc->positions.size(); i++) {
			pos[i] = xform.xform(pos[i]);
		}
	}

	// Transform instance transforms
	if (geo->get_instances().is_valid()) {
		Ref<ProcInstances> inst = geo->get_instances();
		for (int i = 0; i < inst->transforms.size(); i++) {
			inst->transforms.ptrw()[i] = xform * inst->transforms[i];
		}
	}

	p_ctx.write_geometry(p_node_idx, 0, geo);
}

String TransformGeometryNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
	// Full matrix transform requires more push constant space than available
	// Fall back to CPU execution (returned empty means GpuCompiler skips this node)
	return "";
}

// --- SetPositionNode ---

void SetPositionNode::_bind_methods() {
}

SetPositionNode::SetPositionNode() {
	// Blender's Set Position exactly: Geometry, Selection, Position, Offset.
	// Geometry nodes is the source of truth, so the layout matches even though
	// nothing forces it to -- the bridge resolves sockets by name.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	input_defs.push_back({ "Position", SocketType::Vector, Vector3() });
	input_defs.push_back({ "Offset", SocketType::Vector, Vector3() });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void SetPositionNode::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;
	}

	// new = Selection ? (Position + Offset) : old.
	//
	// Position is an IMPLICIT FIELD: unconnected it means "keep the position
	// this element already has", not "move everything to (0,0,0)". A default
	// cannot say that, so ask whether it is wired.
	bool has_position = p_ctx.is_input_connected(p_node_idx, 2);
	geo = geo->duplicate_geometry();
	// Redeem any context fields against THIS node's geometry -- that is what
	// makes a Position with no Geometry input mean "the positions of whatever
	// this node is operating on".
	SocketValue sel_val = p_ctx.materialize(p_ctx.read_input(p_node_idx, 1), geo);
	SocketValue pos_val = p_ctx.materialize(p_ctx.read_input(p_node_idx, 2), geo);
	SocketValue offset_val = p_ctx.materialize(p_ctx.read_input(p_node_idx, 3), geo);

	if (geo->get_mesh().is_valid()) {
		Ref<ProcMesh> mesh = geo->get_mesh();
		Vector3 *pos = mesh->positions.ptrw();
		int count = mesh->positions.size();
		for (int i = 0; i < count; i++) {
			if (!sel_val.bool_at(i)) {
				continue;
			}
			Vector3 base = has_position ? pos_val.vec_at(i) : pos[i];
			pos[i] = base + offset_val.vec_at(i);
		}
		mesh->recalculate_normals();
	}

	if (geo->get_curves().is_valid()) {
		Ref<ProcCurves> curves = geo->get_curves();
		Vector3 *pos = curves->positions.ptrw();
		Vector3 offset = offset_val.to_vector(); // curves use uniform offset
		for (int i = 0; i < curves->positions.size(); i++) {
			pos[i] += offset;
		}
	}

	if (geo->get_pointcloud().is_valid()) {
		Ref<ProcPointCloud> pc = geo->get_pointcloud();
		Vector3 *pos = pc->positions.ptrw();
		Vector3 pc_offset = offset_val.to_vector();
		for (int i = 0; i < pc->positions.size(); i++) {
			pos[i] += pc_offset;
		}
	}

	p_ctx.write_geometry(p_node_idx, 0, geo);
}

String SetPositionNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
	// Offset is socket 3. In the batch path param() spells it
	// `vec3(pc.param0, pc.param1, pc.param2)` -- the same text as before -- and
	// in the density path it becomes the socket's literal or the producer's
	// variable.
	//
	// ⛔ THE DENSITY PATH EMITTED `pos += vec3(0.0, 0.0, 0.0);`. The old text
	// named push-constant SLOTS, and compile_to_function replaced `pc.paramN`
	// with socket N's default: socket 0 is Geometry and sockets 1 and 2 are
	// Bool and Vector, none of which has a float literal, so all three came out
	// 0.0. Set Position was a no-op in every emitted density shader.
	//
	// It matches execute() only while Selection and Position are unconnected --
	// `base = has_position ? pos_val : pos[i]`, and unconnected Position means
	// "keep the position this element has", which is what `pos +=` does.
	// GraphExecutor refuses the batch when either is wired.
	return String("pos += ") + p_ctx.param(p_node_idx, 3) + ";";
}

// --- JoinGeometryNode ---

void JoinGeometryNode::_bind_methods() {
}

JoinGeometryNode::JoinGeometryNode() {
	// ONE MULTI-INPUT SOCKET, as Blender has. Ours was "Geometry A" plus
	// "Geometry B": two discrete sockets, so a three-way join lost one input,
	// and "Geometry B" matched nothing on Blender's node -- which is what put
	// it in the unresolved list and produced the claim that a multi-input could
	// not be represented.
	SocketDef geo;
	geo.name = "Geometry";
	geo.type = SocketType::Geometry;
	geo.multi = true;
	input_defs.push_back(geo);
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void JoinGeometryNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	// Every link on the socket, in the order they were made.
	Vector<SocketValue> parts = p_ctx.read_input_multi(p_node_idx, 0);

	Ref<ProcGeometry> result;
	for (int i = 0; i < parts.size(); i++) {
		Ref<ProcGeometry> g = parts[i].geometry;
		if (g.is_null()) {
			continue;
		}
		if (result.is_null()) {
			result = g->duplicate_geometry();
		} else {
			result->join(g);
		}
	}
	if (result.is_null()) {
		result.instantiate();
	}

	p_ctx.write_geometry(p_node_idx, 0, result);
}

// --- ConvexHullNode ---

void ConvexHullNode::_bind_methods() {
}

ConvexHullNode::ConvexHullNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void ConvexHullNode::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;
	}

	// Placeholder: pass through input geometry.
	// Full implementation would compute convex hull via Quickhull.
	geo = geo->duplicate_geometry();
	p_ctx.write_geometry(p_node_idx, 0, geo);
}

// --- GeometryToInstanceNode ---

void GeometryToInstanceNode::_bind_methods() {
}

GeometryToInstanceNode::GeometryToInstanceNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void GeometryToInstanceNode::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;
	}

	// Store the input geometry as a single instance with identity transform.
	Ref<ProcGeometry> result;
	result.instantiate();

	Ref<ProcInstances> inst = result->ensure_instances();
	inst->references.push_back(geo);
	inst->reference_indices.push_back(0);
	inst->transforms.push_back(Transform3D());

	p_ctx.write_geometry(p_node_idx, 0, result);
}

// --- ViewerNode ---

void ViewerNode::_bind_methods() {
}

ViewerNode::ViewerNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

void ViewerNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	// Debug pass-through: just forward the geometry unchanged.
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	p_ctx.write_geometry(p_node_idx, 0, geo);
}

} // namespace godot