Skip to content

Commit b5370d9

Browse files
zfergusCopilot
andauthored
Adjust planar_filter_step to better handle parallel edges (#232)
* Adjust planar_filter_step to better handle parallel edges - Rename CMake presets: "test-build" -> "test", "python-build" -> "python" - Add unit test for stacked parallel cloth layers and helper maker - Relax assert beta to allow zero root in TrustRegion - Skip edge-edge planar filter for nearly-parallel edges with negligible approach velocity to avoid spurious truncation * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]>
1 parent e454375 commit b5370d9

5 files changed

Lines changed: 210 additions & 5 deletions

File tree

CMakePresets.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,13 @@
118118
"description": "Debug build with CUDA support"
119119
},
120120
{
121-
"name": "test-build",
121+
"name": "test",
122122
"configurePreset": "test",
123123
"displayName": "Unit Tests",
124124
"description": "Build for running tests"
125125
},
126126
{
127-
"name": "python-build",
127+
"name": "python",
128128
"configurePreset": "python",
129129
"displayName": "Python Bindings",
130130
"description": "Build with Python bindings enabled"

src/ipc/candidates/candidates.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,41 @@ const CollisionStencil& Candidates::operator[](size_t i) const
540540
throw std::out_of_range("Candidate index is out of range!");
541541
}
542542

543+
bool Candidates::is_vertex_vertex(size_t i) const
544+
{
545+
return i < vv_candidates.size();
546+
}
547+
548+
bool Candidates::is_edge_vertex(size_t i) const
549+
{
550+
return i >= vv_candidates.size()
551+
&& i < vv_candidates.size() + ev_candidates.size();
552+
}
553+
554+
bool Candidates::is_edge_edge(size_t i) const
555+
{
556+
return i >= vv_candidates.size() + ev_candidates.size()
557+
&& i
558+
< vv_candidates.size() + ev_candidates.size() + ee_candidates.size();
559+
}
560+
561+
bool Candidates::is_face_vertex(size_t i) const
562+
{
563+
return i
564+
>= vv_candidates.size() + ev_candidates.size() + ee_candidates.size()
565+
&& i < vv_candidates.size() + ev_candidates.size()
566+
+ ee_candidates.size() + fv_candidates.size();
567+
}
568+
569+
bool Candidates::is_plane_vertex(size_t i) const
570+
{
571+
return i >= vv_candidates.size() + ev_candidates.size()
572+
+ ee_candidates.size() + fv_candidates.size()
573+
&& i < vv_candidates.size() + ev_candidates.size()
574+
+ ee_candidates.size() + fv_candidates.size()
575+
+ pv_candidates.size();
576+
}
577+
543578
// == Convert to subelement candidates ========================================
544579

545580
namespace {

src/ipc/candidates/candidates.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,31 @@ class Candidates {
6464
/// @return A const reference to the collision stencil.
6565
const CollisionStencil& operator[](size_t i) const;
6666

67+
/// @brief Get if the collision at i is a vertex-vertex collision.
68+
/// @param i The index of the collision.
69+
/// @return If the collision at i is a vertex-vertex collision.
70+
bool is_vertex_vertex(size_t i) const;
71+
72+
/// @brief Get if the collision at i is an edge-vertex collision.
73+
/// @param i The index of the collision.
74+
/// @return If the collision at i is an edge-vertex collision.
75+
bool is_edge_vertex(size_t i) const;
76+
77+
/// @brief Get if the collision at i is an edge-edge collision.
78+
/// @param i The index of the collision.
79+
/// @return If the collision at i is an edge-edge collision.
80+
bool is_edge_edge(size_t i) const;
81+
82+
/// @brief Get if the collision at i is a face-vertex collision.
83+
/// @param i The index of the collision.
84+
/// @return If the collision at i is a face-vertex collision.
85+
bool is_face_vertex(size_t i) const;
86+
87+
/// @brief Get if the collision at i is a plane-vertex collision.
88+
/// @param i The index of the collision.
89+
/// @return If the collision at i is a plane-vertex collision.
90+
bool is_plane_vertex(size_t i) const;
91+
6792
/// @brief Determine if the step is collision free from the set of candidates.
6893
/// @note Assumes the trajectory is linear.
6994
/// @param mesh The collision mesh.

src/ipc/ogc/trust_region.cpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ namespace {
165165
beta = (-b + sqrt_d) / (2 * a);
166166
}
167167
// β should be the positive root
168-
assert(beta > 0);
168+
assert(beta >= 0);
169169

170170
return beta;
171171
}
@@ -258,8 +258,11 @@ void TrustRegion::planar_filter_step(
258258

259259
const VectorMax3d n = dv / dist;
260260

261+
// Compute the closest points on the two primitives: c_first/c_second.
261262
VectorMax3d c_first = VectorMax3d::Zero(d);
262263
VectorMax3d c_second = VectorMax3d::Zero(d);
264+
// Compute the approach velocity of the vertices relative to the
265+
// division plane: max(-Δxⱼ⋅n)
263266
double delta_first = 0, delta_second = 0;
264267

265268
for (int j = 0; j < nv; ++j) {
@@ -277,12 +280,29 @@ void TrustRegion::planar_filter_step(
277280
delta_first = std::max(delta_first, 0.0);
278281
delta_second = std::max(delta_second, 0.0);
279282

283+
// Skip if the approach velocity is negligible relative to the distance.
284+
// This prevents numerical noise in n from nearly-parallel edges from
285+
// truncating uniform displacements.
286+
constexpr double APPROACH_TOL = 1e-3;
287+
if (candidates.is_edge_edge(i)
288+
&& delta_first + delta_second <= APPROACH_TOL * dist) {
289+
// Check that the edges are nearly parallel:
290+
constexpr double EE_PARALLEL_TOL_SQ = 1e-6; // sin of ~0.057°
291+
const Eigen::Vector3d ea = pos.segment<3>(3) - pos.segment<3>(0);
292+
const Eigen::Vector3d eb = pos.segment<3>(9) - pos.segment<3>(6);
293+
const double sin_angle_sq = ea.cross(eb).squaredNorm()
294+
/ (ea.squaredNorm() * eb.squaredNorm());
295+
if (sin_angle_sq < EE_PARALLEL_TOL_SQ) {
296+
return;
297+
}
298+
}
299+
280300
const double lambda = (delta_first == 0 && delta_second == 0)
281301
? 0.5
282302
: delta_second / (delta_first + delta_second);
283303

284-
// Division plane: p = c_second + λ·dv (λ=0 → plane at
285-
// second prim, λ=1 → plane at first prim).
304+
// Point on the division plane: p = c_second + λ·dv
305+
// (λ=0 → plane at second prim, λ=1 → plane at first prim).
286306
const VectorMax3d p = c_second + lambda * dv;
287307

288308
for (int j = 0; j < nv; ++j) {

tests/src/tests/ogc/test_trust_region.cpp

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <ipc/collision_mesh.hpp>
55
#include <ipc/collisions/normal/normal_collisions.hpp>
66

7+
#include <igl/edges.h>
8+
79
#include <Eigen/Dense>
810

911
using namespace ipc;
@@ -588,3 +590,126 @@ TEST_CASE(
588590
CHECK(final_y > 0.0);
589591
CHECK(std::abs(dx(2, 1)) < 0.6);
590592
}
593+
594+
// Helper: (N+1)×(N+1) cloth grid in the xz-plane at y = y_offset.
595+
// Vertices are on a regular grid; edges are extracted from a triangulation.
596+
static std::tuple<Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXi>
597+
make_grid_layer(int N, double spacing, double y_offset)
598+
{
599+
const int nv = (N + 1) * (N + 1);
600+
Eigen::MatrixXd V(nv, 3);
601+
for (int i = 0; i <= N; ++i) {
602+
for (int j = 0; j <= N; ++j) {
603+
V.row(i * (N + 1) + j) << j * spacing, y_offset, i * spacing;
604+
}
605+
}
606+
607+
Eigen::MatrixXi F(2 * N * N, 3);
608+
int f = 0;
609+
for (int i = 0; i < N; ++i) {
610+
for (int j = 0; j < N; ++j) {
611+
const int a = i * (N + 1) + j, b = a + 1;
612+
const int c = (i + 1) * (N + 1) + j, d = c + 1;
613+
F.row(f++) << a, b, d;
614+
F.row(f++) << a, d, c;
615+
}
616+
}
617+
618+
Eigen::MatrixXi E;
619+
igl::edges(F, E);
620+
return { V, E, F };
621+
}
622+
623+
TEST_CASE(
624+
"planar_filter_step: stacked parallel cloth layers", "[ogc][planar_dat]")
625+
{
626+
// Flat cloth grids stacked at y = gap * i (layer 0 at bottom).
627+
// Even-indexed layers (0, 2, ...) move downward (−y);
628+
// odd-indexed layers (1, 3, ...) move upward (+y).
629+
// Adjacent pairs where an odd layer is directly below an even layer
630+
// (e.g. layers 1 and 2) approach each other and must be truncated.
631+
// Adjacent pairs where an even layer is below an odd layer (e.g. 0 and 1)
632+
// move apart and must not be spuriously truncated.
633+
634+
const int N_LAYERS = GENERATE(range(1, 5)); // test 1 to 4 layers
635+
636+
#ifdef NDEBUG
637+
const int N = GENERATE(2, 10);
638+
#else
639+
const int N = 2; // smaller N for debug mode to keep test fast
640+
#endif
641+
const int NV = (N + 1) * (N + 1);
642+
constexpr double spacing = 0.1;
643+
constexpr double gap = 0.3; // y-separation between adjacent layers
644+
constexpr double dhat = 0.35; // > gap so adjacent layers are active
645+
constexpr double step = 0.2; // approach speed; combined 0.4 > gap
646+
647+
Eigen::MatrixXd V;
648+
Eigen::MatrixXi E, F;
649+
for (int i = 0; i < N_LAYERS; ++i) {
650+
auto [Vi, Ei, Fi] = make_grid_layer(N, spacing, gap * i);
651+
const int prev_nv = static_cast<int>(V.rows());
652+
V.conservativeResize(V.rows() + Vi.rows(), 3);
653+
E.conservativeResize(E.rows() + Ei.rows(), 2);
654+
F.conservativeResize(F.rows() + Fi.rows(), 3);
655+
V.bottomRows(Vi.rows()) = Vi;
656+
E.bottomRows(Ei.rows()) = Ei.array() + prev_nv;
657+
F.bottomRows(Fi.rows()) = Fi.array() + prev_nv;
658+
}
659+
660+
CollisionMesh mesh(V, E, F);
661+
662+
ipc::ogc::TrustRegion tr(dhat);
663+
ipc::NormalCollisions collisions;
664+
collisions.set_collision_set_type(
665+
ipc::NormalCollisions::CollisionSetType::OGC);
666+
tr.update(mesh, V, collisions);
667+
668+
if (N_LAYERS > 1) {
669+
REQUIRE(!collisions.empty());
670+
}
671+
672+
Eigen::MatrixXd x = V;
673+
Eigen::MatrixXd dx = Eigen::MatrixXd::Zero(V.rows(), 3);
674+
for (int i = 0; i < N_LAYERS; ++i) {
675+
const double dir = (i % 2 == 0) ? -1.0 : 1.0; // even down, odd up
676+
dx.middleRows(i * NV, NV).col(1).array() = dir * step;
677+
}
678+
679+
tr.planar_filter_step(mesh, x, dx);
680+
681+
// Every layer must still move in its original direction — intra-layer
682+
// parallel-edge pairs must not produce spurious truncations.
683+
for (int i = 0; i < N_LAYERS; ++i) {
684+
for (int j = 0; j < NV; ++j) {
685+
const int k = NV * i + j;
686+
CHECK(dx(k, 0) == Catch::Approx(0.0));
687+
CHECK(dx(k, 2) == Catch::Approx(0.0));
688+
if (i % 2 == 0) {
689+
CHECK(dx(k, 1) < 0.0); // even: downward
690+
} else {
691+
CHECK(dx(k, 1) > 0.0); // odd: upward
692+
}
693+
}
694+
}
695+
696+
// Single layer: no collision pairs, so the step must be completely
697+
// unmodified.
698+
if (N_LAYERS == 1) {
699+
for (int j = 0; j < NV; ++j) {
700+
CHECK(dx(j, 1) == Catch::Approx(-step));
701+
}
702+
}
703+
704+
// Approaching pairs (odd layer i below even layer i+1): step must be
705+
// truncated and the result must be penetration-free.
706+
const Eigen::MatrixXd x_new = x + dx;
707+
for (int i = 1; i + 1 < N_LAYERS; i += 2) {
708+
CHECK(dx(i * NV, 1) < step - 1e-10);
709+
710+
const double max_yi = x_new.middleRows(i * NV, NV).col(1).maxCoeff();
711+
const double min_yi1 =
712+
x_new.middleRows((i + 1) * NV, NV).col(1).minCoeff();
713+
CHECK(min_yi1 > max_yi);
714+
}
715+
}

0 commit comments

Comments
 (0)