OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / selection_nodes.cpp

719 lines · cpp ref:
#include "selection_nodes.h"
#include "../gpu/glsl_emit_context.h"
#include "../graph/graph_executor.h"
#include "../shared/blender_noise.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include <cmath>
#include "../gpu/blender_hash_glsl.gen.h"

namespace godot {

// --- DeleteGeometryNode ---

void DeleteGeometryNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_delete_domain", "domain"), &DeleteGeometryNode::set_delete_domain);
	ClassDB::bind_method(D_METHOD("get_delete_domain"), &DeleteGeometryNode::get_delete_domain);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "delete_domain"), "set_delete_domain", "get_delete_domain");
}

DeleteGeometryNode::DeleteGeometryNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	output_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
}

// THE SELECTION FILTER, shared by Delete Geometry and Separate Geometry.
//
// Separate Geometry is Delete Geometry run twice with the selection the other
// way up, so this is one text rather than two. Blender's Separate Geometry
// outputs the selected half AND the inverted half; ours used to be mapped onto
// SeparateComponents, which splits a geometry by COMPONENT TYPE and is a
// different node entirely.
//
// `p_invert` false keeps the UNSELECTED elements, which is what Delete does.
static Ref<ProcGeometry> _filter_by_selection(const Ref<ProcGeometry> &p_geo,
		const SocketValue &p_sel, int p_domain, bool p_invert) {
	if (p_geo.is_null()) {
		return p_geo;
	}
	Ref<ProcGeometry> geo = p_geo->duplicate_geometry();
	Ref<ProcMesh> mesh = geo->get_mesh();
	if (mesh.is_null() || mesh->positions.size() == 0) {
		return geo;
	}

	const int vcount = mesh->positions.size();
	const int tcount = mesh->indices.size() / 3;

	// Which VERTICES survive. On the point domain the selection says so
	// directly; on the face domain a vertex survives if any surviving triangle
	// still uses it, which is Blender's behaviour -- deleting faces takes with
	// them only the vertices nothing else needs.
	Vector<bool> keep_vert;
	Vector<bool> keep_tri;
	keep_vert.resize(vcount);
	keep_tri.resize(tcount);

	if (p_domain == 2) { // Face
		for (int i = 0; i < vcount; i++) {
			keep_vert.set(i, false);
		}
		const int32_t *idx = mesh->indices.ptr();
		// A selection arriving on the POINT domain has to be interpolated onto
		// faces before it can be read here. Blender's rule for a boolean is
		// all-corners: the face is selected only if every one of its corners is.
		const bool by_point = p_sel.is_field && p_sel.domain == AttrDomain::Point;
		for (int t = 0; t < tcount; t++) {
			bool selected;
			if (by_point) {
				selected = p_sel.bool_at(idx[t * 3]) && p_sel.bool_at(idx[t * 3 + 1]) &&
						p_sel.bool_at(idx[t * 3 + 2]);
			} else {
				selected = p_sel.bool_at(t);
			}
			bool keep = p_invert ? selected : !selected;
			keep_tri.set(t, keep);
			if (keep) {
				for (int k = 0; k < 3; k++) {
					keep_vert.set(idx[t * 3 + k], true);
				}
			}
		}
	} else { // Point
		for (int i = 0; i < vcount; i++) {
			keep_vert.set(i, p_invert ? p_sel.bool_at(i) : !p_sel.bool_at(i));
		}
		const int32_t *idx = mesh->indices.ptr();
		for (int t = 0; t < tcount; t++) {
			// A triangle cannot outlive any of its corners.
			keep_tri.set(t, keep_vert[idx[t * 3]] && keep_vert[idx[t * 3 + 1]] &&
							keep_vert[idx[t * 3 + 2]]);
		}
	}

	// old vertex index -> new, -1 for deleted
	PackedInt32Array remap;
	remap.resize(vcount);
	int32_t *rw = remap.ptrw();
	int kept = 0;
	for (int i = 0; i < vcount; i++) {
		rw[i] = keep_vert[i] ? kept++ : -1;
	}

	if (kept == vcount) {
		bool all_tris = true;
		for (int t = 0; t < tcount; t++) {
			if (!keep_tri[t]) {
				all_tris = false;
				break;
			}
		}
		if (all_tris) {
			return geo; // nothing removed
		}
	}

	// Compact every array that runs parallel to positions. Arrays that do not
	// (tangents and face_offsets are per CORNER and per FACE) are dropped rather
	// than left describing a topology that no longer exists.
	PackedVector3Array pos, nrm;
	PackedVector2Array uv;
	PackedColorArray col;
	const bool has_n = mesh->normals.size() == vcount;
	const bool has_uv = mesh->uvs.size() == vcount;
	const bool has_col = mesh->colors.size() == vcount;
	pos.resize(kept);
	if (has_n) {
		nrm.resize(kept);
	}
	if (has_uv) {
		uv.resize(kept);
	}
	if (has_col) {
		col.resize(kept);
	}
	for (int i = 0; i < vcount; i++) {
		int n = remap[i];
		if (n < 0) {
			continue;
		}
		pos.set(n, mesh->positions[i]);
		if (has_n) {
			nrm.set(n, mesh->normals[i]);
		}
		if (has_uv) {
			uv.set(n, mesh->uvs[i]);
		}
		if (has_col) {
			col.set(n, mesh->colors[i]);
		}
	}

	PackedInt32Array tris;
	const int32_t *idx = mesh->indices.ptr();
	for (int t = 0; t < tcount; t++) {
		if (!keep_tri[t]) {
			continue;
		}
		for (int k = 0; k < 3; k++) {
			tris.push_back(remap[idx[t * 3 + k]]);
		}
	}

	mesh->positions = pos;
	mesh->normals = nrm;
	mesh->uvs = uv;
	mesh->colors = col;
	mesh->indices = tris;
	mesh->tangents = PackedFloat32Array();
	mesh->face_offsets = PackedInt32Array();
	return geo;
}

void DeleteGeometryNode::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;
	}
	// Selection is a FIELD, and deleting is per element. This used to read it
	// with read_bool -- a scalar -- so the node deleted everything or nothing
	// while advertising a selection input, which is worse than not having one.
	SocketValue sel = p_ctx.read_input(p_node_idx, 1);
	p_ctx.write_geometry(p_node_idx, 0, _filter_by_selection(geo, sel, delete_domain, false));
}

void SeparateGeometryNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_domain", "domain"), &SeparateGeometryNode::set_domain);
	ClassDB::bind_method(D_METHOD("get_domain"), &SeparateGeometryNode::get_domain);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "domain"), "set_domain", "get_domain");
}

SeparateGeometryNode::SeparateGeometryNode() {
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Selection", SocketType::Bool, true });
	output_defs.push_back({ "Selection", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Inverted", SocketType::Geometry, Variant() });
}

void SeparateGeometryNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	SocketValue sel = p_ctx.read_input(p_node_idx, 1);
	// Both halves, from the same filter with the selection the other way up.
	p_ctx.write_geometry(p_node_idx, 0, _filter_by_selection(geo, sel, domain, true));
	p_ctx.write_geometry(p_node_idx, 1, _filter_by_selection(geo, sel, domain, false));
}

// --- BoundingBoxNode ---

void BoundingBoxNode::_bind_methods() {
}

BoundingBoxNode::BoundingBoxNode() {
	// BLENDER'S FIRST OUTPUT IS THE BOX ITSELF, as geometry. Ours began with
	// Min, so a graph wiring the bounding-box MESH landed on a Vector socket,
	// and Min and Max then drove our Center and Size. Use Radius is Blender's
	// second input and was absent entirely.
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	input_defs.push_back({ "Use Radius", SocketType::Bool, true });
	output_defs.push_back({ "Bounding Box", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Min", SocketType::Vector, Variant() });
	output_defs.push_back({ "Max", SocketType::Vector, Variant() });
	// Ours, kept AFTER Blender's three so existing Godot-authored graphs that
	// read them still resolve, and so no Blender socket index can reach them.
	output_defs.push_back({ "Center", SocketType::Vector, Variant() });
	output_defs.push_back({ "Size", SocketType::Vector, Variant() });
}

void BoundingBoxNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);
	// Use Radius includes point-cloud radii in the bounds. ProcPointCloud has
	// no radius attribute to include, so the input is READ (so a link to it
	// lands and round-trips) and has nothing to change yet.
	(void)p_ctx.read_bool(p_node_idx, 1);

	if (geo.is_null() || geo->get_mesh().is_null() || geo->get_mesh()->vertex_count() == 0) {
		Ref<ProcGeometry> empty;
		empty.instantiate();
		p_ctx.write_geometry(p_node_idx, 0, empty);
		for (int i = 1; i <= 4; i++) {
			p_ctx.write_vector(p_node_idx, i, Vector3());
		}
		return;
	}

	Ref<ProcMesh> mesh = geo->get_mesh();
	const Vector3 *pos = mesh->positions.ptr();
	int count = mesh->vertex_count();

	Vector3 bb_min = pos[0];
	Vector3 bb_max = pos[0];
	for (int i = 1; i < count; i++) {
		bb_min.x = MIN(bb_min.x, pos[i].x);
		bb_min.y = MIN(bb_min.y, pos[i].y);
		bb_min.z = MIN(bb_min.z, pos[i].z);
		bb_max.x = MAX(bb_max.x, pos[i].x);
		bb_max.y = MAX(bb_max.y, pos[i].y);
		bb_max.z = MAX(bb_max.z, pos[i].z);
	}

	// THE BOX AS GEOMETRY, which is Blender's first output and which ours did
	// not produce at all. Eight corners, twelve triangles, wound outwards.
	Ref<ProcMesh> box;
	box.instantiate();
	PackedVector3Array verts;
	verts.resize(8);
	Vector3 *vw = verts.ptrw();
	for (int i = 0; i < 8; i++) {
		vw[i] = Vector3((i & 1) ? bb_max.x : bb_min.x,
				(i & 2) ? bb_max.y : bb_min.y,
				(i & 4) ? bb_max.z : bb_min.z);
	}
	static const int TRIS[36] = {
		0, 2, 3, 0, 3, 1, // -Z
		4, 5, 7, 4, 7, 6, // +Z
		0, 1, 5, 0, 5, 4, // -Y
		2, 6, 7, 2, 7, 3, // +Y
		0, 4, 6, 0, 6, 2, // -X
		1, 3, 7, 1, 7, 5 // +X
	};
	PackedInt32Array idx;
	idx.resize(36);
	int *iw = idx.ptrw();
	for (int i = 0; i < 36; i++) {
		iw[i] = TRIS[i];
	}
	box->positions = verts;
	box->indices = idx;

	Ref<ProcGeometry> box_geo;
	box_geo.instantiate();
	box_geo->set_mesh(box);

	p_ctx.write_geometry(p_node_idx, 0, box_geo);
	p_ctx.write_vector(p_node_idx, 1, bb_min);
	p_ctx.write_vector(p_node_idx, 2, bb_max);
	p_ctx.write_vector(p_node_idx, 3, (bb_min + bb_max) * 0.5f);
	p_ctx.write_vector(p_node_idx, 4, bb_max - bb_min);
}

// --- SeparateComponentsNode ---

void SeparateComponentsNode::_bind_methods() {
}

SeparateComponentsNode::SeparateComponentsNode() {
	// BLENDER'S SIX, IN ORDER. Ours had four and skipped Grease Pencil and
	// Volume, so from index 2 on the outputs were displaced: a graph reading
	// Point Cloud got our Instances, and the link to Instances addressed a
	// socket past the end. "Curve" is Blender's spelling, not "Curves".
	input_defs.push_back({ "Geometry", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Curve", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Grease Pencil", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Point Cloud", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Volume", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Instances", SocketType::Geometry, Variant() });
}

void SeparateComponentsNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	Ref<ProcGeometry> geo = p_ctx.read_geometry(p_node_idx, 0);

	auto only = [&geo](auto getter, auto setter) {
		Ref<ProcGeometry> out;
		out.instantiate();
		if (geo.is_valid()) {
			auto part = (geo.ptr()->*getter)();
			if (part.is_valid()) {
				(out.ptr()->*setter)(part);
			}
		}
		return out;
	};

	p_ctx.write_geometry(p_node_idx, 0,
			only(&ProcGeometry::get_mesh, &ProcGeometry::set_mesh));
	p_ctx.write_geometry(p_node_idx, 1,
			only(&ProcGeometry::get_curves, &ProcGeometry::set_curves));
	// GREASE PENCIL has no ProcGeometry component, so this is always empty. It
	// exists because Blender's socket 2 is Grease Pencil: without it every
	// output after it is one place along, which is the bug this replaces.
	{
		Ref<ProcGeometry> empty;
		empty.instantiate();
		p_ctx.write_geometry(p_node_idx, 2, empty);
	}
	p_ctx.write_geometry(p_node_idx, 3,
			only(&ProcGeometry::get_pointcloud, &ProcGeometry::set_pointcloud));
	p_ctx.write_geometry(p_node_idx, 4,
			only(&ProcGeometry::get_volume, &ProcGeometry::set_volume));
	p_ctx.write_geometry(p_node_idx, 5,
			only(&ProcGeometry::get_instances, &ProcGeometry::set_instances));
}

// --- SwitchNode ---

void SwitchNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_input_type", "type"), &SwitchNode::set_input_type);
	ClassDB::bind_method(D_METHOD("get_input_type_prop"), &SwitchNode::get_input_type_prop);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "input_type"), "set_input_type", "get_input_type_prop");
}

SwitchNode::SwitchNode() {
	// Blender's socket names, so the bridge resolves them BY NAME rather than
	// falling back to the index. "Condition"/"Result" were two more fallbacks.
	input_defs.push_back({ "Switch", SocketType::Bool, false });
	input_defs.push_back({ "False", SocketType::Geometry, Variant() });
	input_defs.push_back({ "True", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Output", SocketType::Geometry, Variant() });
}

void SwitchNode::set_input_type(int p_type) {
	input_type = (SocketType)p_type;
	input_defs.ptrw()[1].type = input_type;
	input_defs.ptrw()[2].type = input_type;
	output_defs.ptrw()[0].type = input_type;
}

String SwitchNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
	// execute()'s scalar path is `cond ? t : f`, and its element-wise path is the
	// same choice per element -- which is what a per-vertex shader already is.
	// Socket 1 is False, socket 2 is True.
	return p_ctx.declare(p_node_idx, 0) + String(" = ") + p_ctx.in(p_node_idx, 0) +
			String(" ? ") + p_ctx.in(p_node_idx, 2) + String(" : ") +
			p_ctx.in(p_node_idx, 1) + String(";");
}

void SwitchNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (input_type == SocketType::Geometry) {
		bool condition = p_ctx.read_bool(p_node_idx, 0);
		p_ctx.write_geometry(p_node_idx, 0, p_ctx.read_geometry(p_node_idx, condition ? 2 : 1));
		return;
	}

	SocketValue cond = p_ctx.read_input(p_node_idx, 0);
	SocketValue f = p_ctx.read_input(p_node_idx, 1);
	SocketValue t = p_ctx.read_input(p_node_idx, 2);

	if (!cond.is_field && !f.is_field && !t.is_field) {
		p_ctx.write_output(p_node_idx, 0, cond.to_bool() ? t : f);
		return;
	}

	// Element-wise, which is Blender's `mode = ELEMENT`: the condition is a
	// field over the geometry and the branch is taken per vertex, not once.
	int n = MAX(cond.field_size, MAX(f.field_size, t.field_size));
	if (n == 0) {
		n = 1;
	}
	AttrDomain domain = cond.is_field ? cond.domain : (f.is_field ? f.domain : t.domain);

	switch (input_type) {
		case SocketType::Vector: {
			PackedVector3Array out;
			out.resize(n);
			for (int i = 0; i < n; i++) {
				out.ptrw()[i] = cond.bool_at(i) ? t.vec_at(i) : f.vec_at(i);
			}
			p_ctx.write_vec_field(p_node_idx, 0, out, domain);
			return;
		}
		case SocketType::Color: {
			PackedColorArray out;
			out.resize(n);
			for (int i = 0; i < n; i++) {
				out.ptrw()[i] = cond.bool_at(i) ? t.color_at(i) : f.color_at(i);
			}
			p_ctx.write_color_field(p_node_idx, 0, out, domain);
			return;
		}
		case SocketType::Int:
		case SocketType::Bool: {
			PackedInt32Array out;
			out.resize(n);
			for (int i = 0; i < n; i++) {
				out.ptrw()[i] = cond.bool_at(i) ? t.int_at(i) : f.int_at(i);
			}
			p_ctx.write_int_field(p_node_idx, 0, out, domain);
			return;
		}
		case SocketType::Rotation: {
			PackedVector4Array out;
			out.resize(n);
			for (int i = 0; i < n; i++) {
				Quaternion q = cond.bool_at(i) ? t.quat_at(i) : f.quat_at(i);
				out.ptrw()[i] = Vector4(q.x, q.y, q.z, q.w);
			}
			p_ctx.write_rot_field(p_node_idx, 0, out, domain);
			return;
		}
		default: {
			PackedFloat32Array out;
			out.resize(n);
			for (int i = 0; i < n; i++) {
				out.ptrw()[i] = cond.bool_at(i) ? t.float_at(i) : f.float_at(i);
			}
			p_ctx.write_float_field(p_node_idx, 0, out, domain);
			return;
		}
	}
}

// --- RandomValueNode ---

void RandomValueNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_data_type", "t"), &RandomValueNode::set_data_type);
	ClassDB::bind_method(D_METHOD("get_data_type"), &RandomValueNode::get_data_type);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
}

RandomValueNode::RandomValueNode() {
	// Blender's order: Min, Max, ID, SEED. Ours put Seed where ID goes, so a
	// graph wiring a seed drove the ID and a graph wiring an ID was dropped --
	// and the two are not interchangeable: ID varies per element, seed does not.
	input_defs.push_back({ "Min", SocketType::Float, 0.0f });
	input_defs.push_back({ "Max", SocketType::Float, 1.0f });
	input_defs.push_back({ "ID", SocketType::Int, 0 });
	input_defs.push_back({ "Seed", SocketType::Int, 0 });
	output_defs.push_back({ "Value", SocketType::Float, Variant() });
}

// Blender's data_type, and the SOCKET SET that goes with it. Defaults measured
// off Blender 5.2 rather than assumed -- INT's Max is 100, not 1, and
// BOOLEAN's Probability is 0.5.
//
// Only four of the fourteen CD_PROP codes are supported by this node; the rest
// cannot be selected in Blender at all, so an unrecognised one leaves the node
// alone rather than inventing a layout.
void RandomValueNode::set_data_type(int p_type) {
	Vector<SocketDef> in;
	Vector<SocketDef> out;
	switch (p_type) {
		case 10: // FLOAT
			in.push_back({ "Min", SocketType::Float, 0.0f });
			in.push_back({ "Max", SocketType::Float, 1.0f });
			in.push_back({ "ID", SocketType::Int, 0 });
			in.push_back({ "Seed", SocketType::Int, 0 });
			out.push_back({ "Value", SocketType::Float, Variant() });
			break;
		case 11: // INT
			in.push_back({ "Min", SocketType::Int, 0 });
			in.push_back({ "Max", SocketType::Int, 100 });
			in.push_back({ "ID", SocketType::Int, 0 });
			in.push_back({ "Seed", SocketType::Int, 0 });
			out.push_back({ "Value", SocketType::Int, Variant() });
			break;
		case 48: // FLOAT_VECTOR
			in.push_back({ "Min", SocketType::Vector, Vector3(0, 0, 0) });
			in.push_back({ "Max", SocketType::Vector, Vector3(1, 1, 1) });
			in.push_back({ "ID", SocketType::Int, 0 });
			in.push_back({ "Seed", SocketType::Int, 0 });
			out.push_back({ "Value", SocketType::Vector, Variant() });
			break;
		case 50: // BOOLEAN -- THREE inputs, and ID/Seed shift to 1 and 2.
			in.push_back({ "Probability", SocketType::Float, 0.5f });
			in.push_back({ "ID", SocketType::Int, 0 });
			in.push_back({ "Seed", SocketType::Int, 0 });
			out.push_back({ "Value", SocketType::Bool, Variant() });
			break;
		default:
			UtilityFunctions::push_warning(
					"RandomValue: Blender supports only FLOAT, INT, FLOAT_VECTOR and "
					"BOOLEAN; data_type ", p_type,
					" is not one of them and the node is left as it was.");
			return;
	}
	data_type = p_type;
	input_defs = in;
	output_defs = out;
}

String RandomValueNode::compile_glsl_prelude(int p_node_idx) const {
	return String(BLENDER_HASH_GLSL);
}

String RandomValueNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
	// Mirrors execute() case for case, including the two things about this
	// node that cannot be guessed:
	//
	//   1. BOOLEAN HAS THREE INPUTS, so ID and Seed sit at 1 and 2 rather
	//      than 2 and 3.
	//   2. THE HASH ARGUMENT ORDER IS NOT THE SAME FOR ALL FOUR TYPES. Float
	//      and vector hash (seed, id); int and boolean hash (id, seed). Read
	//      off node_fn_random_value.cc, not inferred -- one order agrees with
	//      Blender on half the types and looks plausible on the rest.
	//
	// ⚠ EVERY LITERAL AFTER A NAMED String IS WRAPPED IN String(). A named
	// `const String` plus a bare "literal" is an AMBIGUOUS overload in
	// godot-cpp -- the char array converts equally well to String and to
	// StringName -- while the prvalue `p_ctx.declare(...) + " = "` that every
	// other node in this tree happens to use does not. Same expression, and
	// the only difference is whether the left side was given a name.
	const bool is_bool = (data_type == 50);
	const String id = "uint(" + p_ctx.in(p_node_idx, is_bool ? 1 : 2) + ")";
	const String seed = "uint(" + p_ctx.in(p_node_idx, is_bool ? 2 : 3) + ")";
	const String a = "(" + p_ctx.in(p_node_idx, 0) + ")";
	const String decl = p_ctx.declare(p_node_idx, 0);

	switch (data_type) {
		case 10: { // FLOAT
			const String b = "(" + p_ctx.in(p_node_idx, 1) + ")";
			return decl + String(" = bn_hash_to_float(") + seed + String(", ") + id +
					String(") * (") + b + String(" - ") + a + String(") + ") + a + String(";");
		}
		case 48: { // FLOAT_VECTOR -- three hashes, one per component.
			const String b = "(" + p_ctx.in(p_node_idx, 1) + ")";
			String e = "vec3(";
			const char *c[3] = { "x", "y", "z" };
			for (int i = 0; i < 3; i++) {
				if (i > 0) {
					e += ", ";
				}
				const String comp = String(".") + c[i];
				e += String("bn_hash_to_float(") + seed + String(", ") + id + String(", ") +
						itos(i) + String("u) * (") + b + comp + String(" - ") + a + comp +
						String(") + ") + a + comp;
			}
			return decl + String(" = ") + e + String(");");
		}
		case 11: { // INT
			const String b = "(" + p_ctx.in(p_node_idx, 1) + ")";
			const String pre = String("rv") + itos(p_node_idx);
			// UNSIGNED throughout, so the whole 32-bit span fits and the range
			// wraps to 0 for INT_MIN..INT_MAX, where a modulo would divide by
			// zero. Blender SWAPS rather than erroring when Min > Max.
			return String("int ") + pre + String("_mn = min(") + a + String(", ") + b +
					String(");\n\tint ") + pre + String("_mx = max(") + a + String(", ") + b +
					String(");\n\tuint ") + pre + String("_h = bn_hash(") + id + String(", ") +
					seed + String(");\n\tuint ") + pre + String("_r = uint(") + pre +
					String("_mx) - uint(") + pre + String("_mn) + 1u;\n\t") + decl +
					String(" = int(uint(") + pre + String("_mn) + ((") + pre +
					String("_r == 0u) ? ") + pre + String("_h : (") + pre + String("_h % ") +
					pre + String("_r)));");
		}
		case 50: // BOOLEAN
			// `<=`, not `<`, so a probability of exactly 0 can still be true
			// when the hash is exactly 0.
			return decl + String(" = bn_hash_to_float(") + id + String(", ") + seed +
					String(") <= ") + a + String(";");
		default:
			// set_data_type refuses anything else, so this is unreachable
			// rather than a silent fallback.
			return String();
	}
}

void RandomValueNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	// BOOLEAN HAS THREE INPUTS, so ID and Seed are not where they are for the
	// other three. Retyping the sockets without moving them would have ID
	// reading the seed's link and Seed reading nothing.
	const bool is_bool = (data_type == 50);
	const int id_sock = is_bool ? 1 : 2;
	const int seed_sock = is_bool ? 2 : 3;

	SocketValue a = p_ctx.read_input(p_node_idx, 0); // Min, or Probability
	SocketValue b = is_bool ? SocketValue() : p_ctx.read_input(p_node_idx, 1); // Max
	SocketValue idv = p_ctx.read_input(p_node_idx, id_sock);
	SocketValue seedv = p_ctx.read_input(p_node_idx, seed_sock);

	const bool is_field = a.is_field || b.is_field || idv.is_field || seedv.is_field;
	int n = MAX(MAX(a.field_size, b.field_size), MAX(idv.field_size, seedv.field_size));
	if (n == 0) {
		n = 1;
	}
	const AttrDomain domain = a.is_field ? a.domain
										 : (idv.is_field ? idv.domain
														 : (seedv.is_field ? seedv.domain : b.domain));

	// THE ARGUMENT ORDER IS NOT THE SAME FOR ALL FOUR TYPES. Float and vector
	// hash (seed, id); int and boolean hash (id, seed). Read off
	// node_fn_random_value.cc, not inferred -- one order would agree with
	// Blender on half the types and look plausible on the rest.
	auto float_at = [&](int e) -> float {
		const uint32_t id = (uint32_t)idv.int_at(e);
		const uint32_t seed = (uint32_t)seedv.int_at(e);
		const float t = bnoise::hash_to_float(seed, id);
		const float mn = a.float_at(e);
		const float mx = b.float_at(e);
		return t * (mx - mn) + mn;
	};
	auto vec_at = [&](int e) -> Vector3 {
		const uint32_t id = (uint32_t)idv.int_at(e);
		const uint32_t seed = (uint32_t)seedv.int_at(e);
		const Vector3 mn = a.vec_at(e);
		const Vector3 mx = b.vec_at(e);
		return Vector3(bnoise::hash_to_float(seed, id, 0) * (mx.x - mn.x) + mn.x,
				bnoise::hash_to_float(seed, id, 1) * (mx.y - mn.y) + mn.y,
				bnoise::hash_to_float(seed, id, 2) * (mx.z - mn.z) + mn.z);
	};
	auto int_at = [&](int e) -> int32_t {
		int32_t mn = a.int_at(e);
		int32_t mx = b.int_at(e);
		if (mn > mx) {
			SWAP(mn, mx);
		}
		const uint32_t h = bnoise::hash((uint32_t)idv.int_at(e), (uint32_t)seedv.int_at(e));
		// UNSIGNED, so the whole 32-bit span fits. The range wraps to 0 for
		// INT_MIN..INT_MAX, where a modulo would divide by zero.
		const uint32_t range = (uint32_t)mx - (uint32_t)mn + 1u;
		const uint32_t modulo_result = (range == 0u) ? h : (h % range);
		return (int32_t)((uint32_t)mn + modulo_result);
	};
	auto bool_at = [&](int e) -> bool {
		// `<=`, not `<`, so a probability of exactly 0 can still be true when
		// the hash is exactly 0.
		return bnoise::hash_to_float((uint32_t)idv.int_at(e), (uint32_t)seedv.int_at(e)) <=
				a.float_at(e);
	};

	if (!is_field) {
		switch (data_type) {
			case 11: p_ctx.write_int(p_node_idx, 0, int_at(0)); break;
			case 48: p_ctx.write_vector(p_node_idx, 0, vec_at(0)); break;
			case 50: p_ctx.write_bool(p_node_idx, 0, bool_at(0)); break;
			default: p_ctx.write_float(p_node_idx, 0, float_at(0)); break;
		}
		return;
	}

	// ID IS THE FIELD INPUT THAT MATTERS. Ours ignored it entirely and hashed
	// the seed alone, so every element of a field got the same number -- which
	// is the one thing this node exists not to do.
	switch (data_type) {
		case 11: {
			PackedInt32Array out;
			out.resize(n);
			for (int e = 0; e < n; e++) {
				out.ptrw()[e] = int_at(e);
			}
			p_ctx.write_int_field(p_node_idx, 0, out, domain);
		} break;
		case 48: {
			PackedVector3Array out;
			out.resize(n);
			for (int e = 0; e < n; e++) {
				out.ptrw()[e] = vec_at(e);
			}
			p_ctx.write_vec_field(p_node_idx, 0, out, domain);
		} break;
		case 50: {
			PackedInt32Array out;
			out.resize(n);
			for (int e = 0; e < n; e++) {
				out.ptrw()[e] = bool_at(e) ? 1 : 0;
			}
			p_ctx.write_int_field(p_node_idx, 0, out, domain);
		} break;
		default: {
			PackedFloat32Array out;
			out.resize(n);
			for (int e = 0; e < n; e++) {
				out.ptrw()[e] = float_at(e);
			}
			p_ctx.write_float_field(p_node_idx, 0, out, domain);
		} break;
	}
}

} // namespace godot