OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / mesh_primitive_nodes.cpp

903 lines · cpp ref:
#include "mesh_primitive_nodes.h"

#include "../shared/blender_mesh_primitives.h"
#include "../graph/graph_executor.h"

namespace godot {

// --- MeshCubeNode ---

void MeshCubeNode::_bind_methods() {
}

MeshCubeNode::MeshCubeNode() {
	// Blender's four inputs. The three Vertices sockets were missing entirely,
	// so a subdivided cube lost them on import without a word.
	input_defs.push_back({ "Size", SocketType::Vector, Vector3(1, 1, 1) });
	input_defs.push_back({ "Vertices X", SocketType::Int, 2 });
	input_defs.push_back({ "Vertices Y", SocketType::Int, 2 });
	input_defs.push_back({ "Vertices Z", SocketType::Int, 2 });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "UV Map", SocketType::Vector, Variant() });
}


// ── Blender-framed primitives ───────────────────────────────────────────────
//
// A graph marked BLENDER is evaluated in Blender's frame, so its primitives
// must be BLENDER'S primitives -- not ours rotated. Measured with
// audit_mesh_primitives.py, only the grid had Blender's vertex count: ours
// split vertices per face where Blender welds (24 against 8 for a cube), and
// Mesh Line produced nothing at all. That is a different MESH, not a different
// frame, and it matters to the graph rather than only to a renderer: `Position`
// is evaluated per POINT, so a cube with 24 points and a cube with 8 give
// different answers to every field below them.
//
// A Godot-authored graph keeps the layouts it has always had, so nothing
// already on disk moves -- including the facets, which welding costs.
static bool _blender_frame(ProcEvalContext &p_ctx) {
	return p_ctx.graph != nullptr && p_ctx.graph->is_blender_frame();
}

// Fill a geometry from Blender-ordered positions plus our triangulation, and
// derive per-vertex normals from the faces.
static Ref<ProcGeometry> _blender_mesh(const PackedVector3Array &p_positions,
		const PackedInt32Array &p_indices) {
	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();
	mesh->positions = p_positions;
	mesh->indices = p_indices;
	mesh->uvs.resize(p_positions.size());
	mesh->normals.resize(p_positions.size());
	for (int i = 0; i < mesh->normals.size(); i++) {
		mesh->normals.ptrw()[i] = Vector3();
	}
	// Area-weighted vertex normals. Blender stores flat shading as a per-FACE
	// attribute over welded vertices and ProcMesh has no per-face normal, so a
	// welded cube is smooth here. That is the cost of matching the counts, and
	// splitting to avoid it is exactly what made them wrong.
	const int32_t *idx = p_indices.ptr();
	for (int t = 0; t + 2 < p_indices.size(); t += 3) {
		const Vector3 &a = p_positions[idx[t]];
		const Vector3 &b = p_positions[idx[t + 1]];
		const Vector3 &c = p_positions[idx[t + 2]];
		Vector3 n = (b - a).cross(c - a);
		mesh->normals.ptrw()[idx[t]] += n;
		mesh->normals.ptrw()[idx[t + 1]] += n;
		mesh->normals.ptrw()[idx[t + 2]] += n;
	}
	for (int i = 0; i < mesh->normals.size(); i++) {
		Vector3 n = mesh->normals[i];
		mesh->normals.ptrw()[i] = n.length_squared() > 0.0f ? n.normalized() : Vector3(0, 0, 1);
	}
	return geo;
}

// The same, for primitives that carry Blender's FACES rather than our
// triangulation: p_corners is the corner array and p_face_offsets groups it,
// with face_count + 1 entries. Normals are accumulated per POLYGON by Newell's
// method, so a quad contributes once rather than twice through a fan.
static Ref<ProcGeometry> _blender_mesh_poly(const PackedVector3Array &p_positions,
		const PackedInt32Array &p_corners, const PackedInt32Array &p_face_offsets) {
	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();
	mesh->positions = p_positions;
	mesh->indices = p_corners;
	mesh->face_offsets = p_face_offsets;
	mesh->uvs.resize(p_positions.size());
	mesh->normals.resize(p_positions.size());
	Vector3 *nor = mesh->normals.ptrw();
	for (int i = 0; i < mesh->normals.size(); i++) {
		nor[i] = Vector3();
	}
	const int32_t *c = p_corners.ptr();
	const int faces = p_face_offsets.size() - 1;
	for (int f = 0; f < faces; f++) {
		const int begin = p_face_offsets[f];
		const int n = p_face_offsets[f + 1] - begin;
		if (n < 3) {
			continue;
		}
		Vector3 normal;
		for (int k = 0; k < n; k++) {
			const Vector3 &a = p_positions[c[begin + k]];
			const Vector3 &b = p_positions[c[begin + (k + 1) % n]];
			normal.x += (a.y - b.y) * (a.z + b.z);
			normal.y += (a.z - b.z) * (a.x + b.x);
			normal.z += (a.x - b.x) * (a.y + b.y);
		}
		for (int k = 0; k < n; k++) {
			nor[c[begin + k]] += normal;
		}
	}
	for (int i = 0; i < mesh->normals.size(); i++) {
		const Vector3 n = mesh->normals[i];
		nor[i] = n.length_squared() > 0.0f ? n.normalized() : Vector3(0, 0, 1);
	}
	return geo;
}

// face_offsets for a corner array that is entirely quads.
static PackedInt32Array _quad_offsets(int p_corner_count) {
	PackedInt32Array fo;
	const int faces = p_corner_count / 4;
	fo.resize(faces + 1);
	int32_t *w = fo.ptrw();
	for (int f = 0; f <= faces; f++) {
		w[f] = f * 4;
	}
	return fo;
}

void MeshCubeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (_blender_frame(p_ctx)) {
		Vector3 size = p_ctx.read_vector(p_node_idx, 0);
		int vx = MAX(p_ctx.read_int(p_node_idx, 1), 2);
		int vy = MAX(p_ctx.read_int(p_node_idx, 2), 2);
		int vz = MAX(p_ctx.read_int(p_node_idx, 3), 2);
		// QUADS, at any subdivision. This was 2x2x2 only, triangulated, and
		// warned otherwise: Blender's cube is 6 quads / 24 corners where ours
		// was 12 triangles / 36, so every per-face and per-corner field had a
		// different length and a different order, and the UV Map output -- per
		// CORNER -- had nothing to be compared against.
		const PackedInt32Array corners = bprim::cuboid_corner_verts(vx, vy, vz);
		p_ctx.write_geometry(p_node_idx, 0,
				_blender_mesh_poly(bprim::cuboid(size, vx, vy, vz), corners,
						_quad_offsets(corners.size())));
		p_ctx.write_vec_field(p_node_idx, 1, bprim::cuboid_uvs(vx, vy, vz), AttrDomain::Corner);
		return;
	}
	Vector3 size = p_ctx.read_vector(p_node_idx, 0);
	Vector3 half = size * 0.5f;

	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();

	// 8 vertices
	mesh->positions.resize(24); // 4 per face * 6 faces (for proper normals)
	mesh->normals.resize(24);
	mesh->uvs.resize(24);
	mesh->indices.resize(36); // 6 faces * 2 triangles * 3 indices

	Vector3 *pos = mesh->positions.ptrw();
	Vector3 *nor = mesh->normals.ptrw();
	Vector2 *uv = mesh->uvs.ptrw();
	int32_t *idx = mesh->indices.ptrw();

	// Per-face vertices with correct normals and UVs
	struct FaceDef {
		Vector3 corners[4];
		Vector3 normal;
	};

	FaceDef faces[6] = {
		// +Z front
		{ { { -half.x, -half.y, half.z }, { half.x, -half.y, half.z }, { half.x, half.y, half.z }, { -half.x, half.y, half.z } }, { 0, 0, 1 } },
		// -Z back
		{ { { half.x, -half.y, -half.z }, { -half.x, -half.y, -half.z }, { -half.x, half.y, -half.z }, { half.x, half.y, -half.z } }, { 0, 0, -1 } },
		// +X right
		{ { { half.x, -half.y, half.z }, { half.x, -half.y, -half.z }, { half.x, half.y, -half.z }, { half.x, half.y, half.z } }, { 1, 0, 0 } },
		// -X left
		{ { { -half.x, -half.y, -half.z }, { -half.x, -half.y, half.z }, { -half.x, half.y, half.z }, { -half.x, half.y, -half.z } }, { -1, 0, 0 } },
		// +Y top
		{ { { -half.x, half.y, half.z }, { half.x, half.y, half.z }, { half.x, half.y, -half.z }, { -half.x, half.y, -half.z } }, { 0, 1, 0 } },
		// -Y bottom
		{ { { -half.x, -half.y, -half.z }, { half.x, -half.y, -half.z }, { half.x, -half.y, half.z }, { -half.x, -half.y, half.z } }, { 0, -1, 0 } },
	};

	Vector2 face_uvs[4] = { { 0, 1 }, { 1, 1 }, { 1, 0 }, { 0, 0 } };

	for (int f = 0; f < 6; f++) {
		int base = f * 4;
		for (int v = 0; v < 4; v++) {
			pos[base + v] = faces[f].corners[v];
			nor[base + v] = faces[f].normal;
			uv[base + v] = face_uvs[v];
		}
		// Two triangles per face (CW winding for Godot/Vulkan front-face)
		idx[f * 6 + 0] = base + 0;
		idx[f * 6 + 1] = base + 2;
		idx[f * 6 + 2] = base + 1;
		idx[f * 6 + 3] = base + 0;
		idx[f * 6 + 4] = base + 3;
		idx[f * 6 + 5] = base + 2;
	}

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

// --- MeshGridNode ---

void MeshGridNode::_bind_methods() {
}

MeshGridNode::MeshGridNode() {
	input_defs.push_back({ "Size X", SocketType::Float, 1.0f });
	input_defs.push_back({ "Size Y", SocketType::Float, 1.0f });
	input_defs.push_back({ "Vertices X", SocketType::Int, 10 });
	input_defs.push_back({ "Vertices Y", SocketType::Int, 10 });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	// BLENDER'S SECOND OUTPUT, and the reason it is per CORNER rather than per
	// vertex: on a seamed primitive one vertex carries several uvs (an ico
	// sphere's vertex 0 is u=0.181819 at one corner and u=0.909091 at another,
	// measured), so a per-vertex array cannot represent one. A link to a socket
	// we do not have is dropped SILENTLY on import, so a graph reading UV Map
	// baked as though the link had never been drawn.
	output_defs.push_back({ "UV Map", SocketType::Vector, Variant() });
}

void MeshGridNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	float size_x = p_ctx.read_float(p_node_idx, 0);
	float size_y = p_ctx.read_float(p_node_idx, 1);
	int verts_x = MAX(p_ctx.read_int(p_node_idx, 2), 2);
	int verts_y = MAX(p_ctx.read_int(p_node_idx, 3), 2);

	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();

	int vert_count = verts_x * verts_y;
	int quad_count = (verts_x - 1) * (verts_y - 1);

	mesh->positions.resize(vert_count);
	mesh->normals.resize(vert_count);
	mesh->uvs.resize(vert_count);
	// QUADS, as Blender's grid has. Four corners per face and a face_offsets
	// entry per face plus a terminator. This was two triangles per quad, so a
	// 2x2 grid was 2 faces / 6 corners / 5 edges here against Blender's 1 / 4 /
	// 4 -- every per-face and per-edge count disagreed, and a per-face value
	// had nowhere to agree at all.
	mesh->indices.resize(quad_count * 4);
	mesh->face_offsets.resize(quad_count + 1);

	Vector3 *pos = mesh->positions.ptrw();
	Vector3 *nor = mesh->normals.ptrw();
	Vector2 *uv = mesh->uvs.ptrw();
	int32_t *idx = mesh->indices.ptrw();

	// VERTEX ORDER IS PART OF THE MESH. Blender's grid loops x OUTER and y
	// inner, indexing x * verts_y + y; ours loops the other way. The two hold
	// the same vertices in a different order, which is the same shape and a
	// different input to everything below it -- a field is evaluated per point,
	// in index order.
	const bool blender_frame = (p_ctx.graph != nullptr && p_ctx.graph->is_blender_frame());
	auto IDX = [&](int x, int y) { return blender_frame ? (x * verts_y + y) : (y * verts_x + x); };

	// Generate vertices
	for (int y = 0; y < verts_y; y++) {
		for (int x = 0; x < verts_x; x++) {
			int i = IDX(x, y);
			float fx = static_cast<float>(x) / (verts_x - 1);
			float fy = static_cast<float>(y) / (verts_y - 1);
			// THE ONE PLACE A FRAME ENTERS. Blender's grid lies in XY with Z
			// up; ours lies in XZ with Y up, and that difference -- measured
			// as a Y/Z swap, our layout against Blender's -- is why a bridged
			// graph used to compute in two frames at once. A Blender-framed
			// graph gets Blender's layout, so the arithmetic downstream sees
			// exactly the numbers Blender's does; the executor converts the
			// finished mesh once.
			if (blender_frame) {
				pos[i] = Vector3((fx - 0.5f) * size_x, (fy - 0.5f) * size_y, 0.0f);
				nor[i] = Vector3(0, 0, 1);
			} else {
				pos[i] = Vector3((fx - 0.5f) * size_x, 0.0f, (fy - 0.5f) * size_y);
				nor[i] = Vector3(0, 1, 0);
			}
			uv[i] = Vector2(fx, fy);
		}
	}

	// Generate quads, wound bl -> br -> tr -> tl so a fan about corner 0 gives
	// back the two triangles this used to emit directly.
	// FACE ORDER IS PART OF THE MESH TOO. #62 moved the VERTEX order to
	// Blender's x-outer indexing and stopped there; the faces still ran y outer,
	// so on a 4x3 grid face 1 was the quad at x=1..2 where Blender's is the one
	// at y=1..2. Nothing could see it until a per-face value could be read back
	// -- both meshes have the same shape and the same six centroids, in a
	// different order -- and it moves every per-face and per-corner field.
	int32_t *fo = mesh->face_offsets.ptrw();
	int corner = 0;
	const int faces_x = verts_x - 1;
	const int faces_y = verts_y - 1;
	for (int face = 0; face < quad_count; face++) {
		const int x = blender_frame ? face / faces_y : face % faces_x;
		const int y = blender_frame ? face % faces_y : face / faces_x;
		fo[face] = corner;
		idx[corner++] = IDX(x, y);
		idx[corner++] = IDX(x + 1, y);
		idx[corner++] = IDX(x + 1, y + 1);
		idx[corner++] = IDX(x, y + 1);
	}
	const int face = quad_count;
	fo[face] = corner; // terminator: face_offsets has face_count + 1 entries

	// The grid's uv is a function of the VERTEX -- measured: corners 1 and 4 of
	// a 3x2 grid are both vertex 2 and both read (0.5, 0) -- so the corner field
	// is the vertex uv gathered through the corner array. It is a CORNER field
	// because that is the domain Blender's is on, and the domain a consumer
	// adapts FROM.
	PackedVector3Array corner_uv;
	corner_uv.resize(corner);
	Vector3 *cu = corner_uv.ptrw();
	for (int c = 0; c < corner; c++) {
		const Vector2 &u = uv[idx[c]];
		cu[c] = Vector3(u.x, u.y, 0.0f);
	}

	p_ctx.write_geometry(p_node_idx, 0, geo);
	p_ctx.write_vec_field(p_node_idx, 1, corner_uv, AttrDomain::Corner);
}

// --- MeshCircleNode ---

void MeshCircleNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_fill_type", "fill_type"), &MeshCircleNode::set_fill_type);
	ClassDB::bind_method(D_METHOD("get_fill_type"), &MeshCircleNode::get_fill_type);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "fill_type", PROPERTY_HINT_ENUM, "None,N-gon,Triangle Fan"),
			"set_fill_type", "get_fill_type");
}

MeshCircleNode::MeshCircleNode() {
	input_defs.push_back({ "Vertices", SocketType::Int, 32 });
	input_defs.push_back({ "Radius", SocketType::Float, 1.0f });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}

void MeshCircleNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (_blender_frame(p_ctx)) {
		int verts = MAX(p_ctx.read_int(p_node_idx, 0), 3);
		float radius = p_ctx.read_float(p_node_idx, 1);
		// FILL TYPE CHANGES THE MESH, not just the faces: a triangle fan appends
		// the centre as the LAST vertex, and NONE has no faces at all. This
		// always emitted a fan of the ring vertices without a centre -- 5 verts
		// and 3 triangles where Blender gives 5/1 ngon, 6/5 fan or 5/0.
		PackedVector3Array pos = bprim::circle(verts, radius);
		if (fill_type == 2) {
			pos.push_back(Vector3());
		}
		PackedInt32Array corners, offsets;
		bprim::circle_faces(verts, fill_type, corners, offsets);
		p_ctx.write_geometry(p_node_idx, 0, _blender_mesh_poly(pos, corners, offsets));
		return;
	}
	int verts = MAX(p_ctx.read_int(p_node_idx, 0), 3);
	float radius = p_ctx.read_float(p_node_idx, 1);
	// A triangle mesh cannot distinguish N-gon from Triangle Fan, so both fill.
	bool fill = fill_type != 0;

	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();

	if (fill) {
		// Fan triangulation: center + ring vertices
		mesh->positions.resize(verts + 1);
		mesh->normals.resize(verts + 1);
		mesh->uvs.resize(verts + 1);
		mesh->indices.resize(verts * 3);

		Vector3 *pos = mesh->positions.ptrw();
		Vector3 *nor = mesh->normals.ptrw();
		Vector2 *uv = mesh->uvs.ptrw();
		int32_t *idx = mesh->indices.ptrw();

		// Center vertex
		pos[0] = Vector3(0, 0, 0);
		nor[0] = Vector3(0, 1, 0);
		uv[0] = Vector2(0.5f, 0.5f);

		for (int i = 0; i < verts; i++) {
			float angle = static_cast<float>(i) / verts * Math_TAU;
			float x = cosf(angle) * radius;
			float z = sinf(angle) * radius;
			pos[i + 1] = Vector3(x, 0, z);
			nor[i + 1] = Vector3(0, 1, 0);
			uv[i + 1] = Vector2(cosf(angle) * 0.5f + 0.5f, sinf(angle) * 0.5f + 0.5f);
		}

		for (int i = 0; i < verts; i++) {
			int next = (i + 1) % verts;
			// CW winding for Godot front-face (top view)
			idx[i * 3 + 0] = 0;
			idx[i * 3 + 1] = next + 1;
			idx[i * 3 + 2] = i + 1;
		}
	} else {
		// Edge ring only (no fill) — still output as degenerate triangles for visibility
		mesh->positions.resize(verts);
		mesh->normals.resize(verts);
		mesh->uvs.resize(verts);

		Vector3 *pos = mesh->positions.ptrw();
		Vector3 *nor = mesh->normals.ptrw();
		Vector2 *uv = mesh->uvs.ptrw();

		for (int i = 0; i < verts; i++) {
			float angle = static_cast<float>(i) / verts * Math_TAU;
			pos[i] = Vector3(cosf(angle) * radius, 0, sinf(angle) * radius);
			nor[i] = Vector3(0, 1, 0);
			uv[i] = Vector2(cosf(angle) * 0.5f + 0.5f, sinf(angle) * 0.5f + 0.5f);
		}
	}

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

// --- MeshCylinderNode ---

void MeshCylinderNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_fill_type", "fill_type"), &MeshCylinderNode::set_fill_type);
	ClassDB::bind_method(D_METHOD("get_fill_type"), &MeshCylinderNode::get_fill_type);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "fill_type", PROPERTY_HINT_ENUM, "None,N-gon,Triangle Fan"),
			"set_fill_type", "get_fill_type");
}

MeshCylinderNode::MeshCylinderNode() {
	input_defs.push_back({ "Vertices", SocketType::Int, 32 });
	input_defs.push_back({ "Side Segments", SocketType::Int, 1 });
	input_defs.push_back({ "Fill Segments", SocketType::Int, 1 });
	input_defs.push_back({ "Radius", SocketType::Float, 1.0f });
	input_defs.push_back({ "Depth", SocketType::Float, 2.0f });
	// BLENDER'S FIVE OUTPUTS. Top, Side and Bottom are per-FACE selections and
	// UV Map is per corner; ours had only Mesh, so four of the five links a
	// graph could draw were dropped on import without a word.
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "Top", SocketType::Bool, false });
	output_defs.push_back({ "Side", SocketType::Bool, false });
	output_defs.push_back({ "Bottom", SocketType::Bool, false });
	output_defs.push_back({ "UV Map", SocketType::Vector, Variant() });
}

void MeshCylinderNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (_blender_frame(p_ctx)) {
		int segs = MAX(p_ctx.read_int(p_node_idx, 0), 3);
		int side = MAX(p_ctx.read_int(p_node_idx, 1), 1);
		int fill = MAX(p_ctx.read_int(p_node_idx, 2), 1);
		float radius = p_ctx.read_float(p_node_idx, 3);
		float depth = p_ctx.read_float(p_node_idx, 4);
		// ANY side and fill segments, and Blender's FACES: a fan or an n-gon
		// per cap according to fill_type, quad rings between -- including the
		// concentric rings inside a multi-segment fill. This was 1x1 only,
		// triangulated, with a warning otherwise; and the fill type could not
		// cross the bridge at all, so a bridged cylinder took whatever fill
		// this node happened to default to.
		// height is HALF the depth, as in Blender's ConeConfig.
		const bprim::ConeConfig cfg(segs, side, fill, radius, radius, fill_type);
		PackedVector3Array pos = bprim::cone(segs, side, fill, radius, radius,
				depth * 0.5f, fill_type);
		PackedInt32Array corners, offsets;
		bprim::cone_faces(cfg, corners, offsets);
		p_ctx.write_geometry(p_node_idx, 0, _blender_mesh_poly(pos, corners, offsets));
		// Top, Side and Bottom, on the domain Blender puts them on -- FACE when
		// the end has a fill, POINT when it is a tip or unfilled. Declaring the
		// sockets without writing them would hand a graph the socket DEFAULT,
		// which is the silent-wrong-answer shape this whole series keeps finding.
		for (int which = 0; which < 3; which++) {
			bool is_face = true;
			int size = 0;
			const PackedInt32Array sel = bprim::cone_selection(cfg, which, is_face, size);
			PackedInt32Array bools = sel;
			SocketValue sv = SocketValue::from_int_field(bools);
			sv.type = SocketType::Bool;
			sv.domain = is_face ? AttrDomain::Face : AttrDomain::Point;
			sv.field_size = size;
			// socket 1 Top, 2 Side, 3 Bottom
			p_ctx.write_output(p_node_idx, which == 0 ? 1 : (which == 1 ? 2 : 3), sv);
		}
		p_ctx.write_vec_field(p_node_idx, 4, bprim::cone_uvs(cfg), AttrDomain::Corner);
		return;
	}
	// Blender subdivides a cylinder twice over: SIDE SEGMENTS are rings up the
	// wall, FILL SEGMENTS are concentric rings in each cap. Ours had neither, so
	// a cylinder was always one quad tall and its caps always a single fan --
	// fine to look at, useless to displace, because there were no interior
	// vertices to move.
	int verts_around = MAX(p_ctx.read_int(p_node_idx, 0), 3);
	int side_segments = MAX(p_ctx.read_int(p_node_idx, 1), 1);
	int fill_segments = MAX(p_ctx.read_int(p_node_idx, 2), 1);
	float radius = p_ctx.read_float(p_node_idx, 3);
	float depth = p_ctx.read_float(p_node_idx, 4);
	bool fill_caps = fill_type != 0;
	float half_depth = depth * 0.5f;

	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();

	const int side_rings = side_segments + 1;
	const int side_verts = verts_around * side_rings;
	const int side_tris = verts_around * side_segments * 2;
	// centre plus one ring per fill segment
	const int cap_verts_each = fill_caps ? (1 + verts_around * fill_segments) : 0;
	// innermost ring fans to the centre; each ring beyond it is a band of quads
	const int cap_tris_each = fill_caps
			? (verts_around + verts_around * 2 * (fill_segments - 1))
			: 0;

	const int total_verts = side_verts + cap_verts_each * 2;
	const int total_tris = side_tris + cap_tris_each * 2;

	mesh->positions.resize(total_verts);
	mesh->normals.resize(total_verts);
	mesh->uvs.resize(total_verts);
	mesh->indices.resize(total_tris * 3);

	Vector3 *pos = mesh->positions.ptrw();
	Vector3 *nor = mesh->normals.ptrw();
	Vector2 *uv = mesh->uvs.ptrw();
	int32_t *idx = mesh->indices.ptrw();
	int tri = 0;

	// --- wall ---
	for (int r = 0; r < side_rings; r++) {
		float v = static_cast<float>(r) / side_segments;
		float y = -half_depth + depth * v;
		for (int i = 0; i < verts_around; i++) {
			float angle = static_cast<float>(i) / verts_around * Math_TAU;
			int at = r * verts_around + i;
			pos[at] = Vector3(cosf(angle) * radius, y, sinf(angle) * radius);
			nor[at] = Vector3(cosf(angle), 0, sinf(angle));
			uv[at] = Vector2(static_cast<float>(i) / verts_around, 1.0f - v);
		}
	}
	for (int r = 0; r < side_segments; r++) {
		for (int i = 0; i < verts_around; i++) {
			int nxt = (i + 1) % verts_around;
			int b0 = r * verts_around + i;
			int b1 = r * verts_around + nxt;
			int t0 = (r + 1) * verts_around + i;
			int t1 = (r + 1) * verts_around + nxt;
			idx[tri++] = b0; idx[tri++] = b1; idx[tri++] = t0;
			idx[tri++] = t0; idx[tri++] = b1; idx[tri++] = t1;
		}
	}

	if (fill_caps) {
		for (int cap = 0; cap < 2; cap++) {
			const bool top = (cap == 1);
			const float y = top ? half_depth : -half_depth;
			const Vector3 n = top ? Vector3(0, 1, 0) : Vector3(0, -1, 0);
			const int base = side_verts + cap * cap_verts_each;

			pos[base] = Vector3(0, y, 0);
			nor[base] = n;
			uv[base] = Vector2(0.5f, 0.5f);

			for (int k = 1; k <= fill_segments; k++) {
				float rr = radius * static_cast<float>(k) / fill_segments;
				for (int i = 0; i < verts_around; i++) {
					float angle = static_cast<float>(i) / verts_around * Math_TAU;
					int at = base + 1 + (k - 1) * verts_around + i;
					pos[at] = Vector3(cosf(angle) * rr, y, sinf(angle) * rr);
					nor[at] = n;
					uv[at] = Vector2(0.5f + cosf(angle) * 0.5f * k / fill_segments,
							0.5f + sinf(angle) * 0.5f * k / fill_segments);
				}
			}

			// fan from the centre to the innermost ring
			for (int i = 0; i < verts_around; i++) {
				int nxt = (i + 1) % verts_around;
				int a = base + 1 + i;
				int b = base + 1 + nxt;
				if (top) {
					idx[tri++] = base; idx[tri++] = a; idx[tri++] = b;
				} else {
					idx[tri++] = base; idx[tri++] = b; idx[tri++] = a;
				}
			}
			// bands between successive rings
			for (int k = 1; k < fill_segments; k++) {
				for (int i = 0; i < verts_around; i++) {
					int nxt = (i + 1) % verts_around;
					int i0 = base + 1 + (k - 1) * verts_around + i;
					int i1 = base + 1 + (k - 1) * verts_around + nxt;
					int o0 = base + 1 + k * verts_around + i;
					int o1 = base + 1 + k * verts_around + nxt;
					if (top) {
						idx[tri++] = i0; idx[tri++] = o0; idx[tri++] = o1;
						idx[tri++] = i0; idx[tri++] = o1; idx[tri++] = i1;
					} else {
						idx[tri++] = i0; idx[tri++] = o1; idx[tri++] = o0;
						idx[tri++] = i0; idx[tri++] = i1; idx[tri++] = o1;
					}
				}
			}
		}
	}

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

// --- MeshUVSphereNode ---

void MeshUVSphereNode::_bind_methods() {
}

MeshUVSphereNode::MeshUVSphereNode() {
	input_defs.push_back({ "Segments", SocketType::Int, 32 });
	input_defs.push_back({ "Rings", SocketType::Int, 16 });
	input_defs.push_back({ "Radius", SocketType::Float, 1.0f });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "UV Map", SocketType::Vector, Variant() });
}

void MeshUVSphereNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (_blender_frame(p_ctx)) {
		int segments = MAX(p_ctx.read_int(p_node_idx, 0), 3);
		int rings = MAX(p_ctx.read_int(p_node_idx, 1), 2);
		float radius = p_ctx.read_float(p_node_idx, 2);
		// A FAN AT EACH POLE AND QUADS BETWEEN. This banded every ring into
		// triangles, so a 4x3 sphere was 16 faces / 48 corners against
		// Blender's 12 / 40 -- the same surface, a different corner domain, and
		// nothing for a per-corner UV to line up with.
		PackedVector3Array pos = bprim::uv_sphere(segments, rings, radius);
		PackedInt32Array corners, offsets;
		bprim::uv_sphere_faces(segments, rings, corners, offsets);
		p_ctx.write_geometry(p_node_idx, 0, _blender_mesh_poly(pos, corners, offsets));
		p_ctx.write_vec_field(p_node_idx, 1, bprim::uv_sphere_uvs(segments, rings),
				AttrDomain::Corner);
		return;
	}
	int segments = MAX(p_ctx.read_int(p_node_idx, 0), 3);
	int rings = MAX(p_ctx.read_int(p_node_idx, 1), 2);
	float radius = p_ctx.read_float(p_node_idx, 2);

	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();

	int vert_count = (rings + 1) * (segments + 1);
	int tri_count = rings * segments * 2;

	mesh->positions.resize(vert_count);
	mesh->normals.resize(vert_count);
	mesh->uvs.resize(vert_count);
	mesh->indices.resize(tri_count * 3);

	Vector3 *pos = mesh->positions.ptrw();
	Vector3 *nor = mesh->normals.ptrw();
	Vector2 *uv = mesh->uvs.ptrw();
	int32_t *idx = mesh->indices.ptrw();

	// Generate vertices
	int v = 0;
	for (int r = 0; r <= rings; r++) {
		float phi = static_cast<float>(r) / rings * Math_PI;
		float y = cosf(phi) * radius;
		float ring_radius = sinf(phi) * radius;

		for (int s = 0; s <= segments; s++) {
			float theta = static_cast<float>(s) / segments * Math_TAU;
			float x = cosf(theta) * ring_radius;
			float z = sinf(theta) * ring_radius;

			Vector3 n = Vector3(x, y, z).normalized();
			pos[v] = n * radius;
			nor[v] = n;
			uv[v] = Vector2(static_cast<float>(s) / segments, static_cast<float>(r) / rings);
			v++;
		}
	}

	// Generate triangles (CW winding)
	int t = 0;
	for (int r = 0; r < rings; r++) {
		for (int s = 0; s < segments; s++) {
			int bl = r * (segments + 1) + s;
			int br = bl + 1;
			int tl = bl + (segments + 1);
			int tr = tl + 1;

			idx[t++] = bl; idx[t++] = br; idx[t++] = tl;
			idx[t++] = br; idx[t++] = tr; idx[t++] = tl;
		}
	}

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

// --- MeshIcoSphereNode ---

void MeshIcoSphereNode::_bind_methods() {
}

MeshIcoSphereNode::MeshIcoSphereNode() {
	input_defs.push_back({ "Radius", SocketType::Float, 1.0f });
	input_defs.push_back({ "Subdivisions", SocketType::Int, 2 });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
	output_defs.push_back({ "UV Map", SocketType::Vector, Variant() });
}

void MeshIcoSphereNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (_blender_frame(p_ctx)) {
		float radius = p_ctx.read_float(p_node_idx, 0);
		int subdiv = p_ctx.read_int(p_node_idx, 1);
		if (subdiv <= 1) {
			// The base icosahedron is all triangles, so the corner array IS the
			// triangle list and face_offsets groups it three at a time.
			const PackedVector3Array pos = bprim::ico_sphere_base(radius);
			const PackedInt32Array corners = bprim::ico_sphere_base_indices();
			PackedInt32Array fo;
			fo.resize(corners.size() / 3 + 1);
			for (int f = 0; f <= corners.size() / 3; f++) {
				fo.set(f, f * 3);
			}
			p_ctx.write_geometry(p_node_idx, 0, _blender_mesh_poly(pos, corners, fo));
			p_ctx.write_vec_field(p_node_idx, 1, bprim::ico_sphere_uvs(), AttrDomain::Corner);
			return;
		}
		// Blender subdivides the base icosahedron through a BMesh edge-subdivide
		// with `use_sphere`, whose vertex ORDER is a property of that operator.
		UtilityFunctions::push_warning(
				"MeshIcoSphere: Blender parity is implemented for 1 subdivision only; "
				"higher ones fall back to the Godot-native layout.");
	}
	float radius = p_ctx.read_float(p_node_idx, 0);
	int subdivisions = CLAMP(p_ctx.read_int(p_node_idx, 1), 0, 6);

	Ref<ProcGeometry> geo;
	geo.instantiate();
	Ref<ProcMesh> mesh = geo->ensure_mesh();

	// Start with icosahedron
	const float t = (1.0f + sqrtf(5.0f)) / 2.0f;

	Vector<Vector3> verts;
	verts.push_back(Vector3(-1, t, 0).normalized());
	verts.push_back(Vector3(1, t, 0).normalized());
	verts.push_back(Vector3(-1, -t, 0).normalized());
	verts.push_back(Vector3(1, -t, 0).normalized());
	verts.push_back(Vector3(0, -1, t).normalized());
	verts.push_back(Vector3(0, 1, t).normalized());
	verts.push_back(Vector3(0, -1, -t).normalized());
	verts.push_back(Vector3(0, 1, -t).normalized());
	verts.push_back(Vector3(t, 0, -1).normalized());
	verts.push_back(Vector3(t, 0, 1).normalized());
	verts.push_back(Vector3(-t, 0, -1).normalized());
	verts.push_back(Vector3(-t, 0, 1).normalized());

	Vector<int> tris;
	// 20 faces of icosahedron
	int ico_tris[] = {
		0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
		1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
		3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
		4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1
	};
	for (int i = 0; i < 60; i++) {
		tris.push_back(ico_tris[i]);
	}

	// Subdivide
	for (int sub = 0; sub < subdivisions; sub++) {
		HashMap<uint64_t, int> edge_midpoints;
		Vector<int> new_tris;

		auto get_midpoint = [&](int a, int b) -> int {
			uint64_t key = (a < b) ? (static_cast<uint64_t>(a) << 32 | b) : (static_cast<uint64_t>(b) << 32 | a);
			if (edge_midpoints.has(key)) {
				return edge_midpoints[key];
			}
			Vector3 mid = ((verts[a] + verts[b]) * 0.5f).normalized();
			int idx_val = verts.size();
			verts.push_back(mid);
			edge_midpoints[key] = idx_val;
			return idx_val;
		};

		for (int i = 0; i < tris.size(); i += 3) {
			int v0 = tris[i], v1 = tris[i + 1], v2 = tris[i + 2];
			int m01 = get_midpoint(v0, v1);
			int m12 = get_midpoint(v1, v2);
			int m20 = get_midpoint(v2, v0);

			new_tris.push_back(v0); new_tris.push_back(m01); new_tris.push_back(m20);
			new_tris.push_back(v1); new_tris.push_back(m12); new_tris.push_back(m01);
			new_tris.push_back(v2); new_tris.push_back(m20); new_tris.push_back(m12);
			new_tris.push_back(m01); new_tris.push_back(m12); new_tris.push_back(m20);
		}
		tris = new_tris;
	}

	// Build mesh
	mesh->positions.resize(verts.size());
	mesh->normals.resize(verts.size());
	mesh->uvs.resize(verts.size());
	mesh->indices.resize(tris.size());

	Vector3 *pos = mesh->positions.ptrw();
	Vector3 *nor = mesh->normals.ptrw();
	Vector2 *uv_w = mesh->uvs.ptrw();
	int32_t *idx_w = mesh->indices.ptrw();

	for (int i = 0; i < verts.size(); i++) {
		pos[i] = verts[i] * radius;
		nor[i] = verts[i];
		// Spherical UV
		uv_w[i] = Vector2(
				0.5f + atan2f(verts[i].z, verts[i].x) / Math_TAU,
				0.5f - asinf(CLAMP(verts[i].y, -1.0f, 1.0f)) / Math_PI);
	}

	for (int i = 0; i < tris.size(); i++) {
		idx_w[i] = tris[i];
	}

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

// --- MeshLineNode ---

void MeshLineNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_mode", "mode"), &MeshLineNode::set_mode);
	ClassDB::bind_method(D_METHOD("get_mode"), &MeshLineNode::get_mode);
	ClassDB::bind_method(D_METHOD("set_count_mode", "count_mode"), &MeshLineNode::set_count_mode);
	ClassDB::bind_method(D_METHOD("get_count_mode"), &MeshLineNode::get_count_mode);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "mode", PROPERTY_HINT_ENUM, "Offset,End Points"),
			"set_mode", "get_mode");
	ADD_PROPERTY(PropertyInfo(Variant::INT, "count_mode", PROPERTY_HINT_ENUM, "Total,Resolution"),
			"set_count_mode", "get_count_mode");
}

MeshLineNode::MeshLineNode() {
	input_defs.push_back({ "Count", SocketType::Int, 10 });
	input_defs.push_back({ "Resolution", SocketType::Float, 1.0f });
	input_defs.push_back({ "Start Location", SocketType::Vector, Vector3() });
	input_defs.push_back({ "Offset", SocketType::Vector, Vector3(0, 0, 1) });
	output_defs.push_back({ "Mesh", SocketType::Geometry, Variant() });
}

void MeshLineNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
	if (_blender_frame(p_ctx)) {
		int count = p_ctx.read_int(p_node_idx, 0);
		Vector3 start = p_ctx.read_vector(p_node_idx, 2);
		Vector3 offset = p_ctx.read_vector(p_node_idx, 3);
		// Blender's line is vertices and EDGES, with no faces. ProcMesh has no
		// edge array, so this carries the points -- which is what every field in
		// the graph is evaluated over -- and no triangles. Ours used to emit a
		// point CLOUD, which baked to nothing at all.
		p_ctx.write_geometry(p_node_idx, 0,
				_blender_mesh(bprim::line(start, offset, count), PackedInt32Array()));
		return;
	}
	int count = MAX(p_ctx.read_int(p_node_idx, 0), 1);
	float resolution = MAX(p_ctx.read_float(p_node_idx, 1), 0.0001f);
	Vector3 start = p_ctx.read_vector(p_node_idx, 2);
	Vector3 offset = p_ctx.read_vector(p_node_idx, 3);

	// End Points reads Offset as the far end; Offset reads it as the step.
	Vector3 step = offset;
	if (mode == 1) {
		if (count_mode == 1) {
			// Resolution: spacing along the line decides how many points fit.
			float len = (offset - start).length();
			count = MAX(static_cast<int>(len / resolution) + 1, 1);
		}
		step = (count > 1) ? (offset - start) / static_cast<float>(count - 1) : Vector3();
	}

	Ref<ProcGeometry> geo;
	geo.instantiate();

	// Output as point cloud (lines don't have triangles)
	Ref<ProcPointCloud> pc = geo->ensure_pointcloud();
	pc->positions.resize(count);
	pc->radii.resize(count);

	Vector3 *pos = pc->positions.ptrw();
	float *rad = pc->radii.ptrw();

	for (int i = 0; i < count; i++) {
		pos[i] = start + step * static_cast<float>(i);
		rad[i] = 0.01f;
	}

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

} // namespace godot