Skip to content

Commit eed6096

Browse files
committed
Add EPA
get excite
1 parent ba72d80 commit eed6096

2 files changed

Lines changed: 186 additions & 33 deletions

File tree

src/include/sndx/collision/gjk.hpp

Lines changed: 171 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
#include <span>
88
#include <stdexcept>
99
#include <utility>
10+
#include <vector>
1011

1112
namespace sndx::collision {
13+
// this is pretty much what is written in https://winter.dev/articles/gjk-algorithm and their epa article
1214
namespace detail {
1315
template <class SFnA, class SFnB> [[nodiscard]]
1416
glm::vec3 gjkMinkowski(SFnA&& supportA, SFnB&& supportB, glm::vec3 direction) {
@@ -52,13 +54,12 @@ namespace sndx::collision {
5254

5355
if (detail::similarDir(ab, ao)) { // we want to point towards the origin
5456
newDirection = glm::normalize(detail::tripleProduct(ab, ao));
55-
}
56-
else {
57-
// remove b from simplex
58-
size = 1;
59-
newDirection = glm::normalize(ao);
57+
return true;
6058
}
6159

60+
// remove b from simplex
61+
size = 1;
62+
newDirection = glm::normalize(ao);
6263
return false;
6364
}
6465

@@ -74,34 +75,35 @@ namespace sndx::collision {
7475
if (!detail::similarDir(ac, ao)) {
7576
// keep a and b
7677
size = 2;
77-
return lineOrigin(newDirection);
78-
79-
}
80-
else {
81-
// keep a and c
82-
points[1] = points[2];
83-
size = 2;
84-
newDirection = glm::normalize(detail::tripleProduct(ac, ao));
78+
lineOrigin(newDirection);
79+
return false;
8580
}
81+
82+
// keep a and c
83+
points[1] = points[2];
84+
size = 2;
85+
newDirection = glm::normalize(detail::tripleProduct(ac, ao));
86+
return false;
87+
}
88+
89+
if (detail::similarDir(glm::cross(ab, cross), ao)) {
90+
// keep a and b
91+
size = 2;
92+
lineOrigin(newDirection);
93+
return false;
8694
}
87-
else {
88-
if (detail::similarDir(glm::cross(ab, cross), ao)) {
89-
// keep a and b
90-
size = 2;
91-
return lineOrigin(newDirection);
92-
}
9395

94-
// check direction for tetrahedon
95-
if (detail::similarDir(cross, ao)) {
96-
newDirection = glm::normalize(cross);
97-
}
98-
else {
99-
// flip b and c
100-
std::swap(points[1], points[2]);
101-
newDirection = glm::normalize(-cross);
102-
}
96+
// check direction for tetrahedon
97+
if (detail::similarDir(cross, ao)) {
98+
newDirection = glm::normalize(cross);
10399
}
104-
return false;
100+
else {
101+
// flip b and c
102+
std::swap(points[1], points[2]);
103+
newDirection = glm::normalize(-cross);
104+
}
105+
106+
return true;
105107
}
106108

107109
bool tetrahedronOrigin(Vec& newDirection) {
@@ -136,10 +138,11 @@ namespace sndx::collision {
136138

137139
[[nodiscard]]
138140
bool gjkOrigin(Vec& newDirection) {
141+
// checks need to be done after since size can be modified
139142
switch (size) {
140-
case 2: return lineOrigin(newDirection);
141-
case 3: return triangleOrigin(newDirection);
142-
case 4: return tetrahedronOrigin(newDirection);
143+
case 2: return lineOrigin(newDirection) && size == dimensionality() + 1;
144+
case 3: return triangleOrigin(newDirection) && size == dimensionality() + 1;
145+
case 4: return tetrahedronOrigin(newDirection) && size == dimensionality() + 1;
143146
default:
144147
throw std::logic_error("GJK had weird number of points in simplex");
145148
}
@@ -167,6 +170,142 @@ namespace sndx::collision {
167170
}
168171
}
169172

173+
struct EpaResult {
174+
glm::vec3 normal;
175+
float depth;
176+
};
177+
178+
struct PolytopeEPA {
179+
using Vertices = std::vector<glm::vec3>;
180+
using Indices = std::vector<glm::vec<3, size_t>>;
181+
using Edges = std::vector<std::pair<size_t, size_t>>;
182+
183+
Vertices verts{};
184+
Indices indices{};
185+
186+
static void addUniqueEdge(Edges& out, const Indices& indices, size_t tri, size_t a, size_t b) {
187+
auto reversed = std::find(out.begin(), out.end(), std::make_pair(indices[tri][b], indices[tri][a]));
188+
189+
if (reversed != out.end()) {
190+
out.erase(reversed);
191+
}
192+
else {
193+
out.emplace_back(indices[tri][a], indices[tri][b]);
194+
}
195+
}
196+
197+
[[nodiscard]]
198+
static std::pair<std::vector<glm::vec4>, size_t> calcNormals(const Vertices& verts, const Indices& indices) {
199+
size_t mTri = 0;
200+
std::vector<glm::vec4> norms{};
201+
norms.reserve(indices.size());
202+
float minDist = std::numeric_limits<float>::max();
203+
for (size_t i = 0; i < indices.size(); ++i) {
204+
const auto& a = verts[indices[i].x];
205+
const auto& b = verts[indices[i].y];
206+
const auto& c = verts[indices[i].z];
207+
208+
auto normal = glm::normalize(glm::cross(b - a, c - a));
209+
auto dist = glm::dot(normal, a);
210+
if (dist < 0) {
211+
normal *= -1.0f;
212+
dist *= -1.0f;
213+
}
214+
215+
norms.emplace_back(normal, dist);
216+
if (dist < minDist) {
217+
mTri = i;
218+
minDist = dist;
219+
}
220+
}
221+
222+
return { std::move(norms), mTri };
223+
}
224+
225+
[[nodiscard]]
226+
auto calcNormals() const {
227+
return calcNormals(verts, indices);
228+
}
229+
230+
PolytopeEPA(const SimplexGJK& simplex):
231+
verts(simplex.points.begin(), simplex.points.begin() + simplex.size),
232+
indices{ {0, 1, 2}, {0, 3, 1}, {0, 2, 3}, {1, 3, 2} } {
233+
234+
assert(simplex.size == 4);
235+
}
236+
};
237+
238+
template <float epsilon = 0.00001f, class SFnA, class SFnB> [[nodiscard]]
239+
EpaResult epa(const SimplexGJK& simplex, const SFnA& supportA, const SFnB& supportB) {
240+
PolytopeEPA polytope{ simplex };
241+
242+
auto [normals, minTri] = polytope.calcNormals();
243+
244+
glm::vec3 minNorm{};
245+
float minDist = std::numeric_limits<float>::max();
246+
247+
while (minDist == std::numeric_limits<float>::max()) {
248+
minNorm = glm::vec3(normals[minTri]);
249+
minDist = normals[minTri].w;
250+
251+
auto support = detail::gjkMinkowski(supportA, supportB, minNorm);
252+
auto dist = glm::dot(minNorm, support);
253+
254+
if (std::abs(dist - minDist) > epsilon) {
255+
minDist = std::numeric_limits<float>::max();
256+
257+
PolytopeEPA::Edges edges{};
258+
edges.reserve(polytope.verts.size());
259+
260+
for (size_t i = 0; i < normals.size(); ++i) {
261+
if (detail::similarDir(glm::vec3(normals[i]), support - polytope.verts[polytope.indices[i].x])) {
262+
263+
PolytopeEPA::addUniqueEdge(edges, polytope.indices, i, 0, 1);
264+
PolytopeEPA::addUniqueEdge(edges, polytope.indices, i, 1, 2);
265+
PolytopeEPA::addUniqueEdge(edges, polytope.indices, i, 2, 0);
266+
267+
polytope.indices[i] = polytope.indices.back();
268+
polytope.indices.pop_back();
269+
270+
normals[i] = normals.back();
271+
normals.pop_back();
272+
273+
--i;
274+
}
275+
}
276+
277+
PolytopeEPA::Indices newIndices{};
278+
newIndices.reserve(edges.size() / 3);
279+
for (auto [a, b] : edges) {
280+
newIndices.emplace_back(a, b, polytope.verts.size());
281+
}
282+
polytope.verts.emplace_back(support);
283+
284+
auto [newNorms, newMinTri] = PolytopeEPA::calcNormals(polytope.verts, newIndices);
285+
286+
float oldMin = std::numeric_limits<float>::max();
287+
for (size_t i = 0; i < normals.size(); ++i) {
288+
if (normals[i].w < oldMin) {
289+
oldMin = normals[i].w;
290+
minTri = i;
291+
}
292+
}
293+
294+
if (newNorms[newMinTri].w < oldMin) {
295+
minTri = newMinTri + normals.size();
296+
}
297+
298+
polytope.indices.insert(polytope.indices.begin(), newIndices.begin(), newIndices.end());
299+
normals.insert(normals.begin(), newNorms.begin(), newNorms.end());
300+
}
301+
}
302+
303+
EpaResult out{};
304+
out.normal = minNorm;
305+
out.depth = minDist + epsilon;
306+
return out;
307+
}
308+
170309
template <VolumeN<3> T> [[nodiscard]]
171310
auto getSupportFn(const T& volume) {
172311
return [&](glm::vec3 dir) { return volume.supportPoint(dir); };

src/tests/collision/gjk.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,18 @@ TEST(GJK, circleAndTriangleCollide) {
8080

8181
result = gjk(getSupportFn(std::span{ triangle }), getSupportFn(circle));
8282
EXPECT_TRUE(result);
83-
}
83+
}
84+
85+
TEST(EPA, simpleBoxesGiveCorrectDirection) {
86+
Rect3D boxA{ glm::vec3{-1.0f}, glm::vec3{0.0f} };
87+
Rect3D boxB{ glm::vec3{-0.5f}, glm::vec3{0.5f} };
88+
89+
auto simplex = gjk(getSupportFn(boxA), getSupportFn(boxB));
90+
ASSERT_TRUE(simplex);
91+
92+
auto result = epa(*simplex, getSupportFn(boxA), getSupportFn(boxB));
93+
ASSERT_LE(std::abs(result.depth - 0.5f), 0.0001f);
94+
95+
boxB.translate(result.normal * (result.depth + 0.0001f));
96+
EXPECT_FALSE(gjk(getSupportFn(boxA), getSupportFn(boxB)));
97+
}

0 commit comments

Comments
 (0)