OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / e051e5a

mesh: the two engines wind the other way, and a drawn surface needs normals
Both found by looking at the screen, not at a count: the bridged mountain reached Godot with exactly Blender's 2582018 vertices and rendered black, speckled and inside out. WINDING. _to_godot_frame moved positions and normals and never touched the corner order. It did not have to while the cage arrived as a draw buffer: the bridge reversed the winding building that ArrayMesh and the result inherited it. Handing the graph Blender's own mesh removed that accident, so the conversion whose whole job is "a Blender-authored result, in Godot's terms" has to say it. ⚠ MEASURED, not reasoned about -- signed volume, sum det(a,b,c)/6: Blender, a fresh outward uv_sphere r=1 +4.1219 Godot, its own SphereMesh / BoxMesh -0.5216 / -1.0000 An outward mesh is POSITIVE in Blender and NEGATIVE in Godot, and M = (-x,z,y) has determinant +1 -- a rotation, which preserves the sign. So the reversal is a front-face convention, exactly as the bridge's oldest comment claimed, and it belongs in the frame conversion. Corner-domain attributes are permuted with the corners; reversing one without the other shears them silently. NORMALS. to_array_mesh omitted ARRAY_NORMAL whenever normals did not match the vertex count, and Godot then shaded a surface that has no normal -- the speckles. That was survivable only because the old wire smuggled per-vertex normals in on a corner-expanded cage, which is data the cage had no business carrying. Normals are DERIVED: Blender recomputes them on demand and stores none either. They are now derived at the draw step, area-weighted, which is the same rule that puts triangulation and corner expansion there. Verified against Blender's own evaluated mesh, over the real transport: vertices 2582018 2582018 signed volume -13010.5772 +13010.5519 2.3e-06 relative on |v| Same solid, opposite sign by convention. Volume is the stronger statement: two meshes can share a vertex count and enclose different shapes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e051e5a46fcce03e3a6c145aa7b6f41d3abd2f9c
by plankatron <fleeky@gmail.com> · 2026-09-08T19:56:05.000Z

Changed files (2) +94 -0

src/core/proc_mesh.cpp +38 -0
……@@ -206,8 +206,46 @@ Ref<ArrayMesh> ProcMesh::to_array_mesh() const {
206206 arrays.resize(Mesh::ARRAY_MAX);
207207 arrays[Mesh::ARRAY_VERTEX] = positions;
208208
209 // ⛔ A DRAWN SURFACE NEEDS NORMALS, and normals are DERIVED data -- Blender
210 // recomputes them on demand and does not store them on a mesh either. This
211 // omitted ARRAY_NORMAL when the mesh had none, and Godot then shaded a
212 // surface that has no normal at all: the bridged mountain came out black
213 // with speckles the moment the cage stopped carrying per-vertex normals it
214 // had no business carrying. Deriving them here, at the draw step, is the
215 // same rule as triangulating here.
209216 if (normals.size() == positions.size()) {
210217 arrays[Mesh::ARRAY_NORMAL] = normals;
218 } else if (indices.size() >= 3 && positions.size() > 0) {
219 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();
224 }
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;
211249 }
212250
213251 if (tangents.size() > 0 && tangents.size() == positions.size() * 4) {
src/graph/graph_executor.cpp +56 -0
……@@ -1472,6 +1472,62 @@ 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 }
14751531 return p_geo;
14761532}
14771533