OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / c1edeac

mesh: normals from FACES, and turn the cage round on the way IN
Three things, all one question: a face is a list of corners, and that order is the only thing saying which side is the front. Measured, not assumed -- an outward sphere is +4.1219 in Blender and Godot's own SphereMesh is -0.5216, so the two engines read that order oppositely. 1. recalculate_normals() read `indices` as a flat triangle list, tri_count = size / 3, exactly as Subdivide Mesh did. `indices` is the CORNER array and face_offsets groups it. Once real topology crossed the bridge and Subdivide Mesh started producing quads, it built normals from corner triples spanning three different faces: the mountain rendered near-black with bright specks where a nonsense normal happened to face the light. 2. It and to_array_mesh each had their own copy of the calculation and they disagreed about the SIGN, which is the entire question. One implementation now, compute_normals(), area-weighted and negated for Godot's clockwise front face -- the rule proc-nodes already had in one of the two copies. 3. The cage is turned round where it ENTERS the graph, not on the way out. Reversing at the end left every node computing normals that pointed into the surface, so they had to be negated back -- two wrongs cancelling, measured at mean dot(normal, -cross) = -0.9989 where Godot's own meshes are +1.0000. It was also wrong for geometry a graph BUILDS ITSELF: a primitive made here was never Blender's and has nothing to turn round. One reversal, at the boundary, and everything downstream follows the one rule. Corner-domain values are permuted with the corners; reversing one without the other shears them while the counts still match. Verified against Blender's own evaluated mesh, over the real transport: vertices 2582018 2582018 signed volume -13010.5708 +13010.5468 1.9e-06 relative on |v| normals vs faces +0.9986 (Godot's own primitives: +1.0000) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c1edeac34a128026d8d1b2dd11654cd28bfd383a
by plankatron <fleeky@gmail.com> · 2026-09-08T20:08:26.000Z

Changed files (4) +111 -111

src/core/proc_mesh.cpp +84 -54
……@@ -41,6 +41,7 @@ void ProcMesh::_bind_methods() {
4141 ClassDB::bind_method(D_METHOD("to_array_mesh"), &ProcMesh::to_array_mesh);
4242 ClassDB::bind_method(D_METHOD("update_array_mesh", "mesh"), &ProcMesh::update_array_mesh);
4343 ClassDB::bind_method(D_METHOD("recalculate_normals"), &ProcMesh::recalculate_normals);
44 ClassDB::bind_method(D_METHOD("reverse_faces"), &ProcMesh::reverse_faces);
4445 ClassDB::bind_method(D_METHOD("get_indices"), &ProcMesh::get_indices);
4546 ClassDB::bind_method(D_METHOD("get_face_offsets"), &ProcMesh::get_face_offsets);
4647 ClassDB::bind_method(D_METHOD("get_corner_count"), &ProcMesh::get_corner_count);
……@@ -217,35 +218,10 @@ Ref<ArrayMesh> ProcMesh::to_array_mesh() const {
217218 arrays[Mesh::ARRAY_NORMAL] = normals;
218219 } else if (indices.size() >= 3 && positions.size() > 0) {
219220 PackedVector3Array derived;
220 derived.resize(positions.size());
221 Vector3 *nw = derived.ptrw();
222 for (int i = 0; i < derived.size(); i++) {
223 nw[i] = Vector3();
221 compute_normals(derived);
222 if (derived.size() == positions.size()) {
223 arrays[Mesh::ARRAY_NORMAL] = derived;
224224 }
225 const Vector3 *pr = positions.ptr();
226 const int fc = face_count();
227 for (int f = 0; f < fc; f++) {
228 const int b = face_corner_begin(f);
229 const int n = face_size(f);
230 for (int k = 1; k + 1 < n; k++) {
231 const int32_t i0 = indices[b];
232 const int32_t i1 = indices[b + k];
233 const int32_t i2 = indices[b + k + 1];
234 if (i0 >= derived.size() || i1 >= derived.size() || i2 >= derived.size()) {
235 continue;
236 }
237 // Area-weighted: the cross product's LENGTH is twice the triangle's
238 // area, so there is no per-face normalisation.
239 const Vector3 fn = (pr[i1] - pr[i0]).cross(pr[i2] - pr[i0]);
240 nw[i0] += fn;
241 nw[i1] += fn;
242 nw[i2] += fn;
243 }
244 }
245 for (int i = 0; i < derived.size(); i++) {
246 nw[i] = nw[i].length_squared() > 0.0f ? nw[i].normalized() : Vector3(0, 1, 0);
247 }
248 arrays[Mesh::ARRAY_NORMAL] = derived;
249225 }
250226
251227 if (tangents.size() > 0 && tangents.size() == positions.size() * 4) {
……@@ -327,42 +303,96 @@ bool ProcMesh::update_array_mesh(Ref<ArrayMesh> p_mesh) const {
327303 return true;
328304}
329305
330void ProcMesh::recalculate_normals() {
331 int vert_count = positions.size();
332 if (vert_count == 0 || indices.size() == 0) {
306void ProcMesh::reverse_faces() {
307 const int fc = face_count();
308 if (fc == 0 || indices.size() == 0) {
333309 return;
334310 }
311 PackedInt32Array perm;
312 perm.resize(indices.size());
313 int32_t *cw = indices.ptrw();
314 int32_t *pw = perm.ptrw();
315 for (int f = 0; f < fc; f++) {
316 const int b = face_corner_begin(f);
317 const int n = face_size(f);
318 // The face keeps its first corner and reverses the rest, so it still
319 // starts at the same vertex and only its orientation changes.
320 pw[b] = b;
321 for (int k = 1; k < n; k++) {
322 pw[b + k] = b + (n - k);
323 }
324 for (int k = 1; k < (n + 1) / 2; k++) {
325 const int32_t tmp = cw[b + k];
326 cw[b + k] = cw[b + n - k];
327 cw[b + n - k] = tmp;
328 }
329 }
330 // Corner values are indexed by corner, so they move with them. Reversing
331 // one without the other shears them, silently, since the counts still match.
332 for (const KeyValue<StringName, Ref<AttributeArray>> &kv : corner_attrs.attributes) {
333 Ref<AttributeArray> a = kv.value;
334 if (a.is_null() || a->size() != indices.size()) {
335 continue;
336 }
337 const int esz = attr_type_size(a->type);
338 const PackedByteArray src = a->data;
339 uint8_t *dst = a->data.ptrw();
340 const uint8_t *sp = src.ptr();
341 for (int c = 0; c < indices.size(); c++) {
342 memcpy(dst + (int64_t)pw[c] * esz, sp + (int64_t)c * esz, esz);
343 }
344 }
345}
335346
336 normals.resize(vert_count);
337 Vector3 *nw = normals.ptrw();
347void ProcMesh::recalculate_normals() {
348 compute_normals(normals);
349}
350
351// ⛔ ONE PLACE. recalculate_normals() and to_array_mesh() each had their own
352// copy of this and they disagreed about the SIGN, which is the whole of the
353// question: proc-nodes stores meshes in GODOT's winding, where the front face
354// is clockwise, so the outward normal is the NEGATED cross product. One of the
355// two copies knew that.
356//
357// ⛔ AND IT READ `indices` AS A FLAT TRIANGLE LIST -- tri_count = size / 3 --
358// exactly as Subdivide Mesh did. `indices` is the CORNER array; face_offsets
359// groups it. Once the bridge started carrying real topology and Subdivide Mesh
360// started producing quads, this computed normals from corner triples that span
361// three different faces: the bridged mountain rendered near-black with bright
362// specks where a nonsense normal happened to face the light.
363void ProcMesh::compute_normals(PackedVector3Array &r_out) const {
364 const int vert_count = positions.size();
365 if (vert_count == 0 || indices.size() < 3) {
366 return;
367 }
368 r_out.resize(vert_count);
369 Vector3 *nw = r_out.ptrw();
338370 const Vector3 *pw = positions.ptr();
339371 const int32_t *iw = indices.ptr();
340
341 // Zero normals
342372 for (int i = 0; i < vert_count; i++) {
343373 nw[i] = Vector3();
344374 }
345
346 // Accumulate face normals
347 int tri_count = indices.size() / 3;
348 for (int t = 0; t < tri_count; t++) {
349 int i0 = iw[t * 3];
350 int i1 = iw[t * 3 + 1];
351 int i2 = iw[t * 3 + 2];
352
353 Vector3 e1 = pw[i1] - pw[i0];
354 Vector3 e2 = pw[i2] - pw[i0];
355 // Negate: CW winding (Godot front-face) produces inverted cross product
356 Vector3 fn = -e1.cross(e2);
357
358 nw[i0] += fn;
359 nw[i1] += fn;
360 nw[i2] += fn;
375 const int fc = face_count();
376 for (int f = 0; f < fc; f++) {
377 const int b = face_corner_begin(f);
378 const int n = face_size(f);
379 for (int k = 1; k + 1 < n; k++) {
380 const int32_t i0 = iw[b];
381 const int32_t i1 = iw[b + k];
382 const int32_t i2 = iw[b + k + 1];
383 if (i0 >= vert_count || i1 >= vert_count || i2 >= vert_count) {
384 continue;
385 }
386 // Area-weighted -- the cross product's length is twice the area --
387 // and NEGATED for Godot's clockwise front face.
388 const Vector3 fn = -(pw[i1] - pw[i0]).cross(pw[i2] - pw[i0]);
389 nw[i0] += fn;
390 nw[i1] += fn;
391 nw[i2] += fn;
392 }
361393 }
362
363 // Normalize
364394 for (int i = 0; i < vert_count; i++) {
365 nw[i].normalize();
395 nw[i] = nw[i].length_squared() > 0.0f ? nw[i].normalized() : Vector3(0, 1, 0);
366396 }
367397}
368398
src/core/proc_mesh.h +9 -1
……@@ -121,8 +121,16 @@ public:
121121 // The existing mesh must have exactly one surface with matching vertex/index counts.
122122 bool update_array_mesh(Ref<ArrayMesh> p_mesh) const;
123123
124 // Recalculate flat normals from triangles
124 // Reverse every face's corner order, permuting corner-domain values with
125 // it. A face is a list of corners and that order is the only thing that
126 // says which side is the front -- Blender and Godot read it oppositely,
127 // so a mesh authored in one has to be turned round to enter the other.
128 void reverse_faces();
129
130 // Recalculate normals from the FACES, in Godot's winding.
125131 void recalculate_normals();
132 // The one implementation both that and to_array_mesh() use.
133 void compute_normals(PackedVector3Array &r_out) const;
126134
127135 // Pack positions + normals (+ UVs if present) into Godot's internal vertex
128136 // buffer format for use with surface_update_vertex_region().
src/graph/graph_executor.cpp +0 -56
……@@ -1472,62 +1472,6 @@ static Ref<ProcGeometry> _to_godot_frame(const Ref<ProcGeometry> &p_geo,
14721472 for (int i = 0; i < mesh->normals.size(); i++) {
14731473 nor[i] = Vector3(-nor[i].x, nor[i].z, nor[i].y);
14741474 }
1475
1476 // ⛔ AND THE WINDING. M = (-x, z, y) has determinant +1 -- it is a ROTATION,
1477 // so it does not flip anything by itself. What differs is the FRONT-FACE
1478 // convention: Blender's counter-clockwise is Godot's clockwise under the
1479 // Vulkan Y-flip, and blender-bridge has always reversed corner order when
1480 // building a Godot mesh from a Blender one for exactly that reason.
1481 //
1482 // It used to happen by accident on this path: the bridge reversed the cage's
1483 // winding while turning it into an ArrayMesh, proc-nodes inherited that, and
1484 // the result came out facing the right way. Now the cage arrives as Blender's
1485 // own mesh, in Blender's own order, so the conversion has to say it -- and
1486 // this is the function whose whole job is "a Blender-authored graph's result,
1487 // in Godot's terms". Without it the mountain renders inside out.
1488 //
1489 // Corner-domain attributes are permuted with the corners; they are indexed by
1490 // corner, so reversing one without the other silently shears them.
1491 PackedInt32Array &corners = mesh->indices;
1492 const int fc = mesh->face_count();
1493 PackedInt32Array perm;
1494 perm.resize(corners.size());
1495 int32_t *cw = corners.ptrw();
1496 int32_t *pw = perm.ptrw();
1497 for (int f = 0; f < fc; f++) {
1498 const int b = mesh->face_corner_begin(f);
1499 const int n = mesh->face_size(f);
1500 if (n < 2) {
1501 for (int k = 0; k < n; k++) {
1502 pw[b + k] = b + k;
1503 }
1504 continue;
1505 }
1506 // Keep the first corner where it is and reverse the rest, so the face
1507 // still starts at the same vertex and only its orientation changes.
1508 pw[b] = b;
1509 for (int k = 1; k < n; k++) {
1510 pw[b + k] = b + (n - k);
1511 }
1512 for (int k = 1; k < (n + 1) / 2; k++) {
1513 const int32_t tmp = cw[b + k];
1514 cw[b + k] = cw[b + n - k];
1515 cw[b + n - k] = tmp;
1516 }
1517 }
1518 for (const KeyValue<StringName, Ref<AttributeArray>> &kv : mesh->corner_attrs.attributes) {
1519 Ref<AttributeArray> a = kv.value;
1520 if (a.is_null() || a->size() != corners.size()) {
1521 continue;
1522 }
1523 const int esz = attr_type_size(a->type);
1524 PackedByteArray src = a->data;
1525 uint8_t *dst = a->data.ptrw();
1526 const uint8_t *sp = src.ptr();
1527 for (int c = 0; c < corners.size(); c++) {
1528 memcpy(dst + (int64_t)pw[c] * esz, sp + (int64_t)c * esz, esz);
1529 }
1530 }
15311475 return p_geo;
15321476}
15331477
src/scene/proc_geometry_node.cpp +18 -0
……@@ -17,6 +17,24 @@ Dictionary ProcGeometryNode::_build_overrides() const {
1717 Ref<ProcMesh> pm;
1818 if (input_mesh_data.has("points")) {
1919 pm = ProcMesh::from_mesh_data(input_mesh_data);
20 // ⛔ TURN IT ROUND ON THE WAY IN, ONCE.
21 //
22 // A face is a list of corners, and that order is the only thing saying
23 // which side is the front. The two engines read it oppositely --
24 // measured, an outward sphere is +4.1219 in Blender and Godot's own is
25 // -0.5216 -- so a mesh Blender wrote has to be reversed to mean the same
26 // thing here. Everything downstream then follows ONE rule, which is the
27 // rule proc-nodes already had: compute_normals negates the cross product
28 // because Godot's front face is clockwise.
29 //
30 // Doing it on the way OUT instead, in _to_godot_frame, was wrong twice:
31 // it left the graph computing normals that pointed into the surface (so
32 // they had to be negated back, two wrongs cancelling), and it would have
33 // reversed geometry the graph BUILT ITSELF -- a primitive made here was
34 // never Blender's and has nothing to turn round.
35 if (pm.is_valid() && graph.is_valid() && graph->is_blender_frame()) {
36 pm->reverse_faces();
37 }
2038 } else if (input_mesh.is_valid()) {
2139 pm = ProcMesh::from_array_mesh(input_mesh);
2240 }