#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