From 0715f43ab868da48b3ba4a180f482f95a0ae5e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 30 Sep 2026 13:20:01 -0600 Subject: [PATCH] Splines: Detect poles relative to the other derivative With animated control points the pole is only nearly degenerate, so the vanishing derivative is rounding noise rather than exactly zero, and the absolute threshold missed it. The resulting random normals still showed as dark patches on Pac-Man Arrangement's ghosts (#12354). Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Common/SplineCommon.cpp | 13 ++++++++----- unittest/TestSplineTessellation.cpp | 7 +++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/GPU/Common/SplineCommon.cpp b/GPU/Common/SplineCommon.cpp index cbd13f0dc9..f66125bbdc 100644 --- a/GPU/Common/SplineCommon.cpp +++ b/GPU/Common/SplineCommon.cpp @@ -393,13 +393,16 @@ public: const Vec3f derivV = tess_pos.SampleV(wv.deriv); Vec3f nrm = Cross(derivU, derivV); - if (nrm.Length2() < 1e-20f) { + const float lenU2 = derivU.Length2(); + const float lenV2 = derivV.Length2(); + if (std::min(lenU2, lenV2) <= 1e-8f * std::max(lenU2, lenV2)) { // A pole: a patch edge whose control points all meet at one point, like the top - // of a dome. One derivative vanishes there, so the cross product is zero and the - // normal would be NaN (dark spots on Pac-Man Arrangement's ghosts, #12354). Use - // the limit instead: next to an edge where dP/dv = 0, dP/dv ~ (u - u_edge) * d2P/dudv. + // of a dome. One derivative vanishes there, so the cross product is zero, or with + // animated control points just rounding noise, and the normal would be NaN or + // random (dark patches on Pac-Man Arrangement's ghosts, #12354). Use the limit + // instead: next to an edge where dP/dv = 0, dP/dv ~ (u - u_edge) * d2P/dudv. const Vec3f derivUV = tess_nrm.SampleV(wv.deriv); - if (derivV.Length2() <= derivU.Length2()) { + if (lenV2 <= lenU2) { nrm = Cross(derivU, derivUV); if (tile_u * 2 > surface.tess_u) { nrm = -nrm; diff --git a/unittest/TestSplineTessellation.cpp b/unittest/TestSplineTessellation.cpp index 6917d29583..91a6210d9f 100644 --- a/unittest/TestSplineTessellation.cpp +++ b/unittest/TestSplineTessellation.cpp @@ -193,6 +193,7 @@ struct TestCase { u32 vertType = GE_VTYPE_POS_FLOAT | GE_VTYPE_NRM_FLOAT | GE_VTYPE_TC_FLOAT | GE_VTYPE_COL_8888; bool patchFacing = false; int poleEdge = -1; // Collapse this edge to one point: 0 = first row (v = 0), 1 = first column (u = 0). + float poleNoise = 0.0f; // ...but only nearly, the way animated control points come out. }; // A bumpy, uneven grid of control points with varying UVs and colors, so that a mixed-up weight or @@ -214,11 +215,11 @@ std::vector MakeControlPoints(const TestCase &tc) { } if (tc.poleEdge == 0) { for (int i = 1; i < tc.pointsU; i++) { - points[i].pos = points[0].pos; + points[i].pos = Vec3Packedf(points[0].pos.x + i * tc.poleNoise, points[0].pos.y - i * tc.poleNoise, points[0].pos.z); } } else if (tc.poleEdge == 1) { for (int j = 1; j < tc.pointsV; j++) { - points[j * tc.pointsU].pos = points[0].pos; + points[j * tc.pointsU].pos = Vec3Packedf(points[0].pos.x, points[0].pos.y + j * tc.poleNoise, points[0].pos.z - j * tc.poleNoise); } } return points; @@ -439,6 +440,8 @@ bool TestSplineTessellation() { { "bezier, pole at v = 0", true, 4, 4, 8, 8, 0, 0, posNrm, false, 0 }, { "bezier, pole at u = 0", true, 4, 4, 8, 8, 0, 0, posNrm, false, 1 }, { "spline, pole at v = 0, open", false, 5, 6, 4, 4, 3, 3, posNrm, false, 0 }, + { "bezier, pole at u = 0 with rounding noise", true, 4, 4, 8, 8, 0, 0, posNrm, false, 1, 1e-7f }, + { "spline, pole at v = 0 with rounding noise", false, 5, 6, 4, 4, 3, 3, posNrm, false, 0, 1e-7f }, }; // Each edge type on each axis, one axis with three patches and the other with two. cases.push_back({ "spline, closed/open", false, 6, 5, 4, 6, 0, 3 });