GJK算法创建具有两个相对点的单纯形

GJK algorithm creates simplex with two opposite points

我在做物理引擎,我用的是GJK算法。现在我已经根据 this blog post 编写了它,这是我的代码:

class CollManager:
    @staticmethod
    def supportPoint(a, b, direction):
        return a.supportPoint(direction) - \
            b.supportPoint(-direction)

    @staticmethod
    def nextSimplex(args):
        length = len(args[0])
        if length == 2:
            return CollManager.lineSimplex(args)
        if length == 3:
            return CollManager.triSimplex(args)
        if length == 4:
            return CollManager.tetraSimplex(args)
        return False
    
    @staticmethod
    def lineSimplex(args):
        a, b = args[0]
        ab = b - a
        ao = -a
        if ab.dot(ao) > 0:
            args[1] = ab.cross(ao).cross(ab)
        else:
            args[0] = [a]
            args[1] = ao
    
    @staticmethod
    def triSimplex(args):
        a, b, c = args[0]
        ab = b - a
        ac = c - a
        ao = -a
        abc = ab.cross(ac)
        if abc.cross(ac).dot(ao) > 0:
            if ac.dot(ao) > 0:
                args[0] = [a, c]
                args[1] = ac.cross(ao).cross(ac)
            else:
                args[0] = [a, b]
                return CollManager.lineSimplex(args)
        elif ab.cross(abc).dot(ao) > 0:
            args[0] = [a, b]
            return CollManager.lineSimplex(args)
        else:
            if abc.dot(ao) > 0:
                args[1] = abc
            else:
                args[0] = [a, c, b]
                args[1] = -abc
        return False
    
    @staticmethod
    def tetraSimplex(args):
        a, b, c, d = args[0]
        ab = b - a
        ac = c - a
        ad = d - a
        ao = -a
        abc = ab.cross(ac)
        acd = ac.cross(ad)
        adb = ad.cross(ab)
        if abc.dot(ao) > 0:
            args[0] = [a, b, c]
            return CollManager.triSimplex(args)
        if acd.dot(ao) > 0:
            args[0] = [a, c, d]
            return CollManager.triSimplex(args)
        if adb.dot(ao) > 0:
            args[0] = [a, d, b]
            return CollManager.triSimplex(args)
        return True

    @staticmethod
    def gjk(a, b):
        ab = a.pos - b.pos
        c = Vector3(ab.z, ab.z, -ab.x - ab.y)
        if c == Vector3.zero():
            c = Vector3(-ab.y - ab.z, ab.x, ab.x)

        support = CollManager.supportPoint(a, b, ab.cross(c))
        points = [support]
        direction = -support
        while True:
            support = CollManager.supportPoint(a, b, direction)
            if support.dot(direction) <= 0:
                return None
            points.insert(0, support)
            args = [points, direction]
            if CollManager.nextSimplex(args):
                return args[0]
            points, direction = args

我已经检查了支持点函数是否按预期工作,因为我已经计算出它背后的数学原理。我所做的是我创建了两个立方体,边长均为 2,位置为 (0, 0, 0) 和 (1, 0, 0)。计算的第一个方向是 Vector3(0, 1, 0),因此单纯形上的第一个点是 Vector3(1, -2, 0)。第二点,换个方向看,刚好相反,Vector3(-1, 2, 0)。这就产生了一个问题,因为创建的下一个方向使用了两者的叉积,得到 Vector3(0, 0, 0)。显然没有矢量与此方向相同,因此行 if support.dot(direction) <= 0: 失败。

但是,当 x 为 -1、0 或 1 时会发生这种情况,但不会发生在 -2 和 2 时。

如何确保在单纯形上找到的第二个点与第一个点不完全相反,从而提前进行检查 return?

最小可重现示例(使用完全相同的代码):

# main.py
from vector3 import Vector3
import math

class BoxCollider:
    def __init__(self, position, side_length):
        self.pos = position
        self.side_length = side_length
    
    def supportPoint(self, direction):
        maxDistance = -math.inf
        min, max = self.pos - self.side_length // 2, self.pos + self.side_length // 2
        for x in (min.x, max.x):
            for y in (min.y, max.y):
                for z in (min.z, max.z):
                    distance = Vector3(x, y, z).dot(direction)
                    if distance > maxDistance:
                        maxDistance = distance
                        maxVertex = Vector3(x, y, z)
        return maxVertex

def supportPoint(a, b, direction):
    return a.supportPoint(direction) - \
        b.supportPoint(-direction)

def nextSimplex(args):
    length = len(args[0])
    if length == 2:
        return lineSimplex(args)
    if length == 3:
        return triSimplex(args)
    if length == 4:
        return tetraSimplex(args)
    return False

def lineSimplex(args):
    a, b = args[0]
    ab = b - a
    ao = -a
    if ab.dot(ao) > 0:
        args[1] = ab.cross(ao).cross(ab)
    else:
        args[0] = [a]
        args[1] = ao

def triSimplex(args):
    a, b, c = args[0]
    ab = b - a
    ac = c - a
    ao = -a
    abc = ab.cross(ac)
    if abc.cross(ac).dot(ao) > 0:
        if ac.dot(ao) > 0:
            args[0] = [a, c]
            args[1] = ac.cross(ao).cross(ac)
        else:
            args[0] = [a, b]
            return lineSimplex(args)
    elif ab.cross(abc).dot(ao) > 0:
        args[0] = [a, b]
        return lineSimplex(args)
    else:
        if abc.dot(ao) > 0:
            args[1] = abc
        else:
            args[0] = [a, c, b]
            args[1] = -abc
    return False

def tetraSimplex(args):
    a, b, c, d = args[0]
    ab = b - a
    ac = c - a
    ad = d - a
    ao = -a
    abc = ab.cross(ac)
    acd = ac.cross(ad)
    adb = ad.cross(ab)
    if abc.dot(ao) > 0:
        args[0] = [a, b, c]
        return triSimplex(args)
    if acd.dot(ao) > 0:
        args[0] = [a, c, d]
        return triSimplex(args)
    if adb.dot(ao) > 0:
        args[0] = [a, d, b]
        return triSimplex(args)
    return True

def gjk(a, b):
    ab = a.pos - b.pos
    c = Vector3(ab.z, ab.z, -ab.x - ab.y)
    if c == Vector3.zero():
        c = Vector3(-ab.y - ab.z, ab.x, ab.x)

    support = supportPoint(a, b, ab.cross(c))
    points = [support]
    direction = -support
    while True:
        support = supportPoint(a, b, direction)
        if support.dot(direction) <= 0:
            return None
        points.insert(0, support)
        args = [points, direction]
        if nextSimplex(args):
            return args[0]
        points, direction = args

a = BoxCollider(Vector3(0, 0, 0), 2)
b = BoxCollider(Vector3(1, 0, 0), 2)
print(gjk(a, b))

我的 vector3.py 文件太长了,下面是一个同样有效的等效文件:https://github.com/pyunity/pyunity/blob/07fed7871ace0c1b1b3cf8051d08d6677fe18209/pyunity/vector3.py

解决方法是将 ab.cross(ao).cross(ab) 更改为 ab / 2,因为这给出了从 AB 的中点到原点的向量,并且比两个叉积要便宜得多。