Gilbert-Johnson-Keerthi (GJK)算法是一种实用且高效的算法,主要应用在计算机图形学和机器人学中,以判断两个凸多面体是否相交。这个算法由Elmer Gilbert、Daniel Johnson和Srinivasan Keerthi于1988年提出。由于它的高效性和可靠性,GJK算法在实时物理模拟、碰撞检测和避障算法等多种领域中都有广泛应用。
本文旨在详细解析如何在 C、C# 和 Matlab 语言中实现这一算法,既适用于初学者,也适用于希望深入了解这一算法的研究者和开发者。本文中的代码都经过精心调试和优化,以确保准确性和效率。请跟随我一步步学习和理解GJK算法的实现过程。
实战项目下载
在了解如何实现GJK算法之前,我们首先需要理解GJK算法的基本概念和工作原理。
GJK算法是一个迭代过程,基于几何概念——“支持映射”。对于两个凸形状A和B,我们可以定义一个Minkowski差集 C = A - B。若A和B相交,则原点存在于C中。GJK算法的基本思路就是通过迭代搜索Minkowski差集,判断原点是否在其中,从而判断两个凸形状是否相交。
GJK算法的关键步骤之一是计算支持映射。支持映射(support mapping)是一个从向量到点的映射,对于给定的方向向量d,支持映射返回的是沿着d方向最远的点。具体到凸形状A和B,我们可以定义A和B的支持映射为:
// C 语言示例代码
// 定义结构体Vector表示向量
typedef struct Vector {
double x;
double y;
double z;
} Vector;
// 定义凸形状A和B的支持映射函数
Vector supportMapA(Vector d);
Vector supportMapB(Vector d);
对于凸形状A和B的Minkowski差集C,其支持映射定义为:
// C 语言示例代码
Vector supportMapC(Vector d) {
Vector pointOnA = supportMapA(d);
Vector pointOnB = supportMapB(negate(d)); // 注意这里的d方向需要取反
return subtract(pointOnA, pointOnB); // C = A - B
}
GJK算法的基本流程可以总结为以下几个步骤:
初始化一个初始向量d(可以是任意值,但一般选取从原点指向A或B的中心的向量)以及一个空的简单形状(一开始是空集,可以是点、线、三角形或四面体)。
计算Minkowski差集C的支持映射,得到一个新的点p。
如果p和d的点乘小于0,那么原点不在C中,也就是说A和B不相交,算法结束。
否则,将p添加到简单形状中。
更新d为指向原点的最近方向。
如果d与上一步的d几乎相同(或者说,改进非常小),那么我们可以认为原点在C中,也就是说A和B相交,算法结束。
否则,返回第2步。
以上是GJK算法的基本流程,下一节我们将具体解析在C、C#和Matlab中如何实现这一算法。
在C语言中,我们可以定义以下数据结构和函数来实现GJK算法:
// C 语言示例代码
// 定义数据结构和函数
typedef struct Simplex {
Vector vertices[4];
int count;
} Simplex;
// 定义函数来计算Minkowski差集C的支持映射
Vector support(Vector d, Shape A, Shape B) {
Vector p = supportMapC(d);
return p;
}
// 定义函数来更新simplex和搜索方向d
bool updateSimplexAndDirection(Simplex* simplex, Vector* d);
// 定义GJK算法主函数
bool gjk(Shape A, Shape B) {
Vector d = {1, 0, 0}; // 初始方向可以随便选,这里我们选择x轴正方向
Simplex simplex = {0}; // 初始的simplex为空
// 主循环
while (true) {
Vector p = support(d, A, B);
if (dotProduct(p, d) < 0) {
return false; // A和B不相交
}
simplex.vertices[simplex.count++] = p; // 将p添加到simplex
if (updateSimplexAndDirection(&simplex, &d)) {
return true; // A和B相交
}
}
}
以上代码中省略了updateSimplexAndDirection
函数的实现,这个函数的具体实现取决于simplex的形状(点、线、三角形或四面体)。在下一节,我们将解析在C#和Matlab中如何实现GJK算法,同时我们也将详细解析updateSimplexAndDirection
函数的实现。
在C#语言中,我们可以定义一些类和方法来实现GJK算法:
// C# 示例代码
public struct Vector
{
public double x;
public double y;
public double z;
}
public class Simplex
{
public Vector[] vertices;
public int count;
}
public class GJK
{
// 定义支持映射方法
public static Vector SupportMapC(Vector d, Shape A, Shape B) {
Vector pointOnA = A.Support(d);
Vector pointOnB = B.Support(Negate(d));
return Subtract(pointOnA, pointOnB);
}
// 更新simplex和搜索方向
public static bool UpdateSimplexAndDirection(ref Simplex simplex, ref Vector d);
// GJK主算法
public static bool Run(Shape A, Shape B) {
Vector d = new Vector { x = 1, y = 0, z = 0 };
Simplex simplex = new Simplex();
simplex.vertices = new Vector[4];
simplex.count = 0;
while (true) {
Vector p = SupportMapC(d, A, B);
if (DotProduct(p, d) < 0) {
return false;
}
simplex.vertices[simplex.count++] = p;
if (UpdateSimplexAndDirection(ref simplex, ref d)) {
return true;
}
}
}
}
这里的C#实现基本与C语言实现相同,主要的区别在于C#中我们使用了类和方法的概念,而在C语言中我们使用了结构体和函数。另外,C#代码也更加简洁,更符合面向对象编程的风格。
在Matlab中,我们可以定义以下函数来实现GJK算法:
% Matlab 示例代码
function [isIntersect] = GJK(A, B)
% 初始化搜索方向和Simplex
d = [1; 0; 0];
Simplex = [];
while true
% 计算支持映射得到新点
p = SupportMapC(d, A, B);
if dot(p, d) < 0
isIntersect = false;
return
end
% 将新点添加到Simplex
Simplex = [Simplex, p];
% 更新Simplex和搜索方向
[updated, d] = UpdateSimplexAndDirection(Simplex, d);
if updated
isIntersect = true;
return
end
end
end
这里的Matlab实现与C语言和C#实现基本相同,但是因为Matlab是一种脚本语言,所以语法有所不同,例如我们使用了function
关键字定义函数,并且所有变量默认为矩阵,不需要像C和C#那样进行声明。
UpdateSimplexAndDirection
函数详解至此,我们已经在C,C#和Matlab三种语言中实现了GJK算法的主体部分,但我们还没有解释UpdateSimplexAndDirection
函数的实现。这个函数的主要任务是更新simplex和搜索方向d。具体来说,它需要执行以下操作:
检查最新添加到simplex的点p是否包含原点。如果是,那么A和B相交。
否则,更新simplex以使其更接近原点。
更新d为指向原点的新方向。
这个函数的具体实现取决于simplex的形状,由于篇幅限制,我们在本文中只介绍一维和二维的情况,即simplex为线段和三角形的情况。
UpdateSimplexAndDirection
函数在一维情况下的实现当simplex是一个线段时,我们有两个点,记为a和b(a是最新添加的点)。如果原点在a和b之间,那么我们保持simplex不变,并将d更新为原点在ab直线上的投影点。否则,我们将b从simplex中移除,并将d更新为指向原点的方向。
以下是C语言实现的示例代码:
// C 语言示例代码
bool updateSimplexAndDirection1D(Simplex* simplex, Vector* d) {
Vector a = simplex->vertices[simplex->count - 1]; // 最新的点
Vector b = simplex->vertices[simplex->count - 2]; // 上一个点
Vector ao = negate(a); // 指向原点的向量
if (dotProduct(subtract(b, a), ao) > 0) { // 原点在a和b之间
*d = tripleProduct(subtract(b, a), ao, subtract(b, a)); // 更新d为原点在ab直线上的投影点
} else { // 原点不在a和b之间
simplex->count--; // 移除b
*d = ao; // 更新d为指向原点的方向
}
return false;
}
在这里,tripleProduct
函数计算的是三个向量的混合积,它在更新搜索方向时非常有用。
UpdateSimplexAndDirection
函数在二维情况下的实现当simplex是一个三角形时,我们有三个点,记为a、b和c(a是最新添加的点)。我们需要检查原点是否在abc组成的三角形内或者在ab、ac边的外部。对于这三种情况,我们都需要相应地更新simplex和d。
以下是C语言实现的示例代码:
// C 语言示例代码
bool updateSimplexAndDirection2D(Simplex* simplex, Vector* d) {
Vector a = simplex->vertices[simplex->count - 1]; // 最新的点
Vector b = simplex->vertices[simplex->count - 2]; // 上一个点
Vector c = simplex->vertices[simplex->count - 3]; // 倒数第三个点
Vector ao = negate(a); // 指向原点的向量
Vector ab = subtract(b, a); // ab向量
Vector ac = subtract(c, a); // ac向量
if (dotProduct(tripleProduct(ab, ac, ac), ao) > 0) { // 原点在ac边的外部
if (dotProduct(ac, ao) > 0) { // 原点在ao和ac之间
simplex->vertices[simplex->count - 2] = a; // 移除b
*d = tripleProduct(ac, ao, ac); // 更新d为原点在ac直线上的投影点
} else { // 原点不在ao和ac之间
return updateSimplexAndDirection1D(simplex, d); // 转为一维情况处理
}
} else { // 原点在ab边的外部或abc三角形内
if (dotProduct(tripleProduct(ab, ab, ac), ao) > 0) { // 原点在ab边的外部
return updateSimplexAndDirection1D(simplex, d); // 转为一维情况处理
} else { // 原点在abc三角形内
return true; // A和B相交
}
}
return false;
}
此处的tripleProduct
函数同样是计算三个向量的混合积,它能帮助我们确定原点在三角形哪一侧。
以上就是在C、C#和Matlab三种编程语言中实现Gilbert-Johnson-Keerthi (GJK)算法的详细步骤和代码示例。我们希望通过这篇文章,您能对GJK算法有一个全面的理解,并能够在实际问题中应用这个强大的算法。
值得注意的是,我们只介绍了一维和二维情况下的UpdateSimplexAndDirection
函数实现,三维情况下的实现需要进一步探讨。如果您对此有进一步的想法或问题,欢迎在评论区留言,我们将很高兴与您进行讨论。
我们也提醒您,以上代码只是为了帮助您理解GJK算法的原理和实现,真正的实际应用可能需要考虑更多的因素,例如计算精度、性能优化等。因此,在实际应用中,您可能需要根据您的具体需求和环境进行适当的调整和优化。
感谢您的阅读,希望这篇文章对您有所帮助。