Separating Axis Theorem function fails when met with acute angles

215 views Asked by At

This is a little library I was making for the LOVE2D engine in Lua, which uses separating axis theorem to solve collisions.

I was so happy when I got my SAT program to work, and started testing it with a multitude of polygons. It works in most cases, and gives a correct minimum translation vector for them too. Oddly enough- if both shapes have acute angles, then those angles cause the program to fail, returning collisions when the shape isn't touching, or even more unusually, it gives a bizarre minimum translation vector. I have checked my function that returns normals- as I felt that this was my first point that could have failed, but it seems to be working fine.

This is the main function that handles my collision.

function findIntersection(shape1, shape2)
--Get axes to test.
--MTV means 'minimum translation vector' ie. the shortest vector of intersection
local axes1 = {}
local axes2 = {}
local overlap = false
local MTV = {direction = 0, magnitude = 99999}

for i, vert in pairs(shape1.hitbox) do
    nrm = getNormal(shape1.hitbox, i)
    table.insert(axes1, nrm)
end
for i, vert in pairs(shape2.hitbox)do
    nrm = getNormal(shape2.hitbox, i)
    table.insert(axes2, nrm)
end

--print(#axes1 .. '    ' .. #axes2)

--now that we have the axes, we have to project along each of them
for i, axis in pairs(axes1) do
    test1 = hitboxProj(shape1, vectorToCoord(axis.direction, axis.magnitude))
    test2 = hitboxProj(shape2, vectorToCoord(axis.direction, axis.magnitude))
    if test2.max > test1.min or test1.max > test2.min then
        if test2.max - test1.min < MTV.magnitude then
            MTV.direction = axes1[i].direction
            MTV.magnitude = test2.max - test1.min
        end
    else
        return false
    end
end

--now that we have the axes, we have to project along each of them
for i, axis in pairs(axes2) do
    test1 = hitboxProj(shape1, vectorToCoord(axis.direction, axis.magnitude))
    test2 = hitboxProj(shape2, vectorToCoord(axis.direction, axis.magnitude))
    if test2.max > test1.min or test1.max > test2.min then
        if test2.max - test1.min < MTV.magnitude then
            MTV.direction = axes2[i].direction
            MTV.magnitude = test2.max - test1.min
        end
    else
        return false
    end
end

return {MTV}
end

My project files are here on github https://github.com/ToffeeGoat/ToffeeCollision

2

There are 2 answers

0
2dengine On

It's a good start and your code is fairly clear. There are some things which can be improved, in particular I see that all of your functions are global. For starters you want to store all of your functions in a "module" to avoid polluting the _G space. You can use locals for everything else. Note that it's not robust to write things like x == 0 this check will only work for integers and may fail when floating point math is involved. I recommend writing a simple test script for each function in your library.

Also, it's not efficient to write return {x = xCoord, y = yCoord} when you can return multiple values with Lua return xCoord, yCoord. Creating a lot of intermediate tables puts a strain on the garbage collector. Some of your code needs reworking like the "getDirection" function. I mean there are already well-known techniques for this sort of thing. Check out my tutorial for examples: https://2dengine.com/?p=vectors#Angle_between_two_vectors

There is some silly stuff in there like function sq(x) return x*x end. Did you know you can write x^2 in Lua? addDeg can be replaced by the modulo operator: newAng = (angle + addition)%360 Also note that there is absolutely no benefit to working with degrees - I recommend using only radians. You are already using math.pi which is in radians. Either way you have to pick either radians or degrees and stick to one or the other. Don't use both units in your code.

I don't want to nitpick too much because your code is not bad, you just need to get used to some of the best practices. Here is another one of my tutorials: https://2dengine.com/?p=intersections

0
darkfrei On

Working function:

local function checkSATCollision(vertices1, vertices2)
    -- Separating Axis Theorem with Minimum Translation Vector
    local function projectVertices(vertices, dx, dy)
        local min, max = math.huge, -math.huge
        local indexMin, indexMax
        for i = 1, #vertices-1, 2 do
            local dotProduct = vertices[i]*dy-vertices[i+1]*dx
            if dotProduct < min then 
                min = dotProduct 
                indexMin = i
            end
            if dotProduct > max then 
                max = dotProduct 
                indexMax = i
            end
        end
        return min, max, indexMin, indexMax
    end
    
    local minDist = math.huge
    local dx, dy
    local x1, y1, x2, y2 = vertices1[#vertices1-1], vertices1[#vertices1],vertices1[1], vertices1[2]
    for i = 1, #vertices1-1, 2 do
    local nx, ny = x2-x1, y2-y1
        local length = math.sqrt(nx*nx+ny*ny)
    nx, ny = nx/length, ny/length
    local min1, max1, indexMin1, indexMax1 = projectVertices(vertices1, nx, ny)
    local min2, max2, indexMin2, indexMax2 = projectVertices(vertices2, nx, ny)
        local dist = math.min (max2-min1, max1-min2)
    if dist < 0 then
      return false -- no collision
        elseif minDist >= dist then
            minDist = dist
            dx, dy = ny, -nx
        end
        x1, y1, x2, y2 = x2, y2, vertices1[i+2], vertices1[i+3]
  end
    if minDist*2^45 < 1 then -- rounding almost 0 to 0
        minDist = 0
    end
  return minDist, minDist*dx, minDist*dy -- collision and direction
end

and:

-- example: 
local vertices1 = {0,0, 200,0, 0,100}
local vertices2 = {50,50, 60,80, 80,60}
local dist, dx, dy = checkSATCollision(vertices1, vertices2)
print (dist, dx, dy) -- 24.149534156998 10.8    21.6
local str = ''
for i = 1, #vertices2-1, 2 do
    vertices2[i] = vertices2[i] + dx
    vertices2[i+1] = vertices2[i+1] + dy
    str = str .. vertices2[i] .. ', ' .. vertices2[i+1] .. ', '
end
print (str) -- 58.8, 70.6, 70.8, 101.6, 90.8, 81.6, 
dist, dx, dy = checkSATCollision(vertices1, vertices2)
print (dist, dx, dy) -- 0   0.0 0.0