2

I am coding a physic engine for a custom doom engine.

I don't aim to replicate the exact behavior of the original doom.

In doom, every "thing" (player, monsters etc) is an axis aligned bounding box. I want to keep that.

I already have a working tesselation algorithm for the sectors.

I already have a working quadtree, using AABB boxes of triangles of sectors.

The quad tree will return a list of candidate for intersection with a given AABB (the player, for example).

What I want : an algorithm to test if a triangle intersect an AABB in 2D.

What I can do : split the AABB into two triangles, then do triangle-triangle intersection check. What I can do : use a "triangle vs aabb" algorithm working with 3D and set the "z" to 0. What I can do :

1/ check if a point of the triangle is inside the AABB.

2/ check if the center of the AABB is inside the triangle (the center is the better candidate to avoid rounding problems).

3/ check if a line segment of the triangle intersects a segment of the AABB.

I don't want to do that because : I think that given this precise situation, there should be a more optimized way to do it. It is precisely something that the GPU has to face often : find if a triangle is inside the viewport. I think that question, more than any other question, has been solved to hell.

Note that the situation can be even simpler : I can translate everything so the AABB start at (0,O). I can resize everything so the question becomes: "how to determine if a triangle intersect [0,1][0,1]".

I already did some research but:

1/ most results are for 3D stuff.

2/ this is strangely enough a not often covered case. Even in the book "game physics cookbook" this question is not mentionned.

3/ answers I found goes the hard, generic, way, with SAT (separation axis theorem) or equivalent stuff.

I'll have to do this test for a lot of "thing" (player, monster) everyframe for every candidate triangle given by the quadtree.

There is one last option I have, but I really don't know where to start or even if it's a good idea. Here is a quick summary of what I have in my minds.

1/ as the gpu already has all these triangles.

2/ as it's massively parallelizable.

3/ as, passed the fixed cost, there'll be no additionnal cost.

=> ask the gpu.

But I don't know how to do that. The communication cpu/gpu will have a cost, but a fixed cost (it will roughly cost the same thing for 1 or 100.000 things). I prefer to avoid this solution (but as the website ask me to say the ideas I had, I am talking about it).

Please note that this is my first message on this website. Please note that english is not my native language. Please note that, right now right here, it's 3:32 am (in the night). Please note that I will not be able to answer before tomorrow at roughly the same hour (and this is true for every day actually).

Thanks for reading, thanks in advance for answers.

Bubuche
  • 33
  • 3
  • 2
    I would first test whether the AABB of the triangle intersects the other AABB: this is very easy to do, and if the answer is "no", you have no further work to do. If the answer is "yes", you will need a more accurate test. I would test the 3 triangle lines for intersection with the 4 lines x=0, x=1, y=0 and y=1, stopping at the first hit. If you have the parametric equations of the lines for the 3 triangle sides (e.g. x = x1*(1-t) + x2*t, etc.) this is very easy to do -- just apply the constraint (e.g. x=1), solve for t and see if it's in the range [0, 1]. – j_random_hacker Apr 17 '19 at 02:19
  • 1
    Whoops, forgot a couple of important cases: The AABB could completely contain the triangle, or vice versa. Testing the former is easy: just see if each triangle vertex is in the unit square. For the latter, it's enough to pick a single vertex of the AABB (e.g. (0, 0)) and test whether it's inside the triangle; one way is by computing, for each triangle vertex, the cross product of the vector from there to the next triangle vertex and the vector from there to the point. Only the signs of the 3rd components of these cross products are needed: iff they are all the same, the point is inside. – j_random_hacker Apr 17 '19 at 17:46
  • @j_random_hacker Good point. My algorithm covers the case where the triangle is inside the AABB but I'm not absolutely sure if it covers all cases where the triangle contains the whole AABB... – jdehesa Apr 17 '19 at 18:08
  • @j_random_hacker: As I only test collisions between a rectangle and a triangle =>got from the quadtree<= I already know that the aabbs intersect. If there was no intersection, I wouldn't have this triangle to check. Your solution is the generic solution I was talking about. Even if it may work, it's a pretty generic and bruteforce approach. It's not what I am looking for, as explained in the first post. Still : thanks for the answer, it may help someone else. Today, I started to work on an other solution (which I explain below). – Bubuche Apr 18 '19 at 00:24
  • It's best to state things you already know, like that the triangle's AABB and the given AABB intersect, in the question. I expect this generic approach will be faster than jdehesa's approach, but I think the main drag is the line segment intersection tests, which AFAICS any method needs to fall back in the worst case. Will be interesting to see which is actually faster. – j_random_hacker Apr 18 '19 at 12:09
  • Honestly I can't say because I haven't tested yet. Programming the doom engine is a work with a lot of "todos" that keeps adding. Yesterday I did the sky (which is NOT a skybox, as the sky can hide stuff. In doom the sky is just a wall/floor/ceiling with sky painted on it with some effect to give the illusion it's far etc. You can hide stuff behind a wall ? Then you can hide stuff behind the sky). Even the first map hides things behind the sky (you have screens here) https://hub.jmonkeyengine.org/t/portal-effect-with-depth-buffer/36398 (old projet, in java and only for the rendering) – Bubuche Apr 20 '19 at 00:40

1 Answers1

2

Here is my attempt. In principle you can always test for segment intersections, but if you want to save floating point operations, there are cases where you can take a shortcut. An AABB divides the plane in nine regions (top-left, top, top-right, left, inside, right, bottom-left, bottom and bottom-right). If you just look at the regions in which the points of the triangle fall, you may be able to determine that an intersection must or cannot take place. There are some cases which cannot be decided on this basis, though, and falling back to geometrical intersection is necessary. Here is my code, which I think is correct (as in, all the region-based tests are well defined), although I didn't test thoroughly. It is rather long, but most of it is bitwise operations so it should actually be quite fast. The entry point is the function intersects, and there is an example in the main function.

#include <math.h>
#include <stdio.h>

#define EPSILON 1e-6

typedef struct AABB {
    float x0, y0, x1, y1;
} AABB;

typedef struct Point {
    float x, y, z;
} Point;

typedef struct Triangle {
    Point p1, p2, p3;
} Triangle;

// Naming assumes (0, 0) is top-left corner
typedef enum Region {
    TOP_LEFT     = 1 << 0,
    TOP          = 1 << 1,
    TOP_RIGHT    = 1 << 2,
    LEFT         = 1 << 3,
    INSIDE       = 1 << 4,
    RIGHT        = 1 << 5,
    BOTTOM_LEFT  = 1 << 6,
    BOTTOM       = 1 << 7,
    BOTTOM_RIGHT = 1 << 8
} Region;

// Find the region in which a point is with respect to the AABB
Region aabb_region(const AABB* aabb, const Point* point) {
    if (point->x < aabb->x0) {
        if (point->y < aabb->y0) {
            return TOP_LEFT;
        } else if (point->y > aabb->y1) {
            return BOTTOM_LEFT;
        } else {
            return LEFT;
        }
    } else if (point->x > aabb->x1) {
        if (point->y < aabb->y0) {
            return TOP_RIGHT;
        } else if (point->y > aabb->y1) {
            return BOTTOM_RIGHT;
        } else {
            return RIGHT;
        }
    } else {
        if (point->y < aabb->y0) {
            return TOP;
        } else if (point->y > aabb->y1) {
            return BOTTOM;
        } else {
            return INSIDE;
        }
    }
}

// 1: There is intersection
// 0: There may or may not be intersection
int regions_intersect_2(Region r1, Region r2) {
    if ((((r1 | r2) & INSIDE) != 0) ||
        ((r1 | r2) == (LEFT | RIGHT)) ||
        ((r1 | r2) == (TOP | BOTTOM))) {
        return 1;
    } else {
        return 0;
    }
}

// 1: There is intersection
// 0: There may or may not be intersection
// -1: There is no intersection
// Does not check cases already covered by regions_intersect_2
int regions_intersect_3(Region r1, Region r2, Region r3) {
    Region r23 = r2 | r3;
    switch (r1) {
    case TOP_LEFT:
        if (r23 == (BOTTOM | RIGHT) ||
            r23 == (BOTTOM | TOP_RIGHT) ||
            r23 == (RIGHT | BOTTOM_LEFT)) {
            return 1;
        } else if ((r23 & (TOP_LEFT | LEFT | BOTTOM_LEFT)) == r23 ||
                   (r23 & (TOP_LEFT | TOP | TOP_RIGHT)) == r23) {
            return -1;
        }
    case TOP:
        if (r23 == (LEFT | BOTTOM_RIGHT) ||
            r23 == (RIGHT | BOTTOM_LEFT)) {
            return 1;
        } else if ((r23 & (TOP_LEFT | TOP | TOP_RIGHT)) == r23) {
            return -1;
        }
    case TOP_RIGHT:
        if (r23 == (BOTTOM | LEFT) ||
            r23 == (BOTTOM | TOP_LEFT) ||
            r23 == (LEFT | BOTTOM_RIGHT)) {
            return 1;
        } else if ((r23 & (TOP_RIGHT | RIGHT | BOTTOM_RIGHT)) == r23 ||
                   (r23 & (TOP_RIGHT | TOP | TOP_LEFT)) == r23) {
            return -1;
        }
    case LEFT:
        if (r23 == (TOP | BOTTOM_RIGHT) ||
            r23 == (BOTTOM | TOP_RIGHT)) {
            return 1;
        } else if ((r23 & (TOP_LEFT | LEFT | BOTTOM_LEFT)) == r23) {
            return -1;
        }
    case RIGHT:
        if (r23 == (TOP | BOTTOM_LEFT) ||
            r23 == (BOTTOM | TOP_LEFT)) {
            return 1;
        } else if ((r23 & (TOP_RIGHT | RIGHT | BOTTOM_RIGHT)) == r23) {
            return -1;
        }
    case BOTTOM_LEFT:
        if (r23 == (TOP | RIGHT) ||
            r23 == (TOP | BOTTOM_RIGHT) ||
            r23 == (RIGHT | TOP_LEFT)) {
            return 1;
        } else if ((r23 & (BOTTOM_LEFT | LEFT | TOP_LEFT)) == r23 ||
                   (r23 & (BOTTOM_LEFT | BOTTOM | BOTTOM_RIGHT)) == r23) {
            return -1;
        }
    case BOTTOM:
        if (r23 == (LEFT | TOP_RIGHT) ||
            r23 == (RIGHT | TOP_LEFT)) {
            return 1;
        } else if ((r23 & (BOTTOM_LEFT | BOTTOM | BOTTOM_RIGHT)) == r23) {
            return -1;
        }
    case BOTTOM_RIGHT:
        if (r23 == (TOP | LEFT) ||
            r23 == (TOP | BOTTOM_LEFT) ||
            r23 == (LEFT | TOP_RIGHT)) {
            return 1;
        } else if ((r23 & (BOTTOM_RIGHT | RIGHT | TOP_RIGHT)) == r23 ||
                   (r23 & (BOTTOM_RIGHT | BOTTOM | BOTTOM_LEFT)) == r23) {
            return -1;
        }
    default:
        return 0;
    }
    return 0;
}

// Check if a segment intersects with the AABB
// Does not check cases already covered by regions_intersect_2 or regions_intersect_3
int segment_intersects(const AABB* aabb, const Point* p1, const Point* p2, Region r1, Region r2) {
    // Skip if intersection is impossible
    Region r12 = r1 | r2;
    if ((r12 & (TOP_LEFT | TOP | TOP_RIGHT)) == r12 ||
        (r12 & (BOTTOM_LEFT | BOTTOM | BOTTOM_RIGHT)) == r12 ||
        (r12 & (TOP_LEFT | LEFT | BOTTOM_LEFT)) == r12 ||
        (r12 & (TOP_RIGHT | RIGHT | BOTTOM_RIGHT)) == r12) {
        return 0;
    }
    float dx = p2->x - p1->x;
    float dy = p2->y - p1->y;
    if (fabsf(dx) < EPSILON || fabs(dy) < EPSILON) {
        // Vertical or horizontal line (or zero-sized vector)
        // If there were intersection we would have already picked it up
        return 0;
    }
    float t = (aabb->x0 - p1->x) / dx;
    if (t >= 0.f && t <= 1.f) {
        return 1;
    }
    t = (aabb->x1 - p1->x) / dx;
    if (t >= 0.f && t <= 1.f) {
        return 1;
    }
    t = (aabb->y0 - p1->y) / dy;
    if (t >= 0.f && t <= 1.f) {
        return 1;
    }
    t = (aabb->y1 - p1->y) / dy;
    if (t >= 0.f && t <= 1.f) {
        return 1;
    }
    return 0;
}

int intersects(const AABB* aabb, const Triangle* triangle) {
    // Find plane regions for each point
    Region r1 = aabb_region(aabb, &triangle->p1);
    Region r2 = aabb_region(aabb, &triangle->p2);
    Region r3 = aabb_region(aabb, &triangle->p3);
    // Check if any pair of regions implies intersection
    if (regions_intersect_2(r1, r2) ||
        regions_intersect_2(r1, r3) ||
        regions_intersect_2(r2, r3)) {
        return 1;
    }
    // Check if the three regions imply or forbid intersection
    switch (regions_intersect_3(r1, r2, r3)) {
    case 1:
        return 1;
    case -1:
        return 0;
    }
    // Check segment intersections
    if (segment_intersects(aabb, &triangle->p1, &triangle->p2, r1, r2)) {
        return 1;
    } else if (segment_intersects(aabb, &triangle->p1, &triangle->p3, r1, r3)) {
        return 1;
    } else if (segment_intersects(aabb, &triangle->p2, &triangle->p3, r2, r3)) {
        return 1;
    }
    return 0;
}

int main(int argc, char* argv[]) {
    AABB aabb = {
        /* x0 = */ 2.f,
        /* y0 = */ 1.f,
        /* x1 = */ 5.f,
        /* y1 = */ 6.f };
    Triangle triangle = {
        {1.f, 0.f}, {2.f, 2.f}, {2.f, -3.f}
    };
    int inter = intersects(&aabb, &triangle);
    printf("Intersects: %s.\n", inter ? "yes" : "no");
    return 0;
}

Output:

Intersects: yes.

See it in Rextester

jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • 1
    For speed, you could encode the answer for each possible input to `regions_intersect_3()` using 2 bits each in a 128-byte constant array. Then that function becomes `return ((answer[(r1|r2|r3)>>2] >> (((r1|r2|r3) & 3) << 1)) & 3) - 1;`. This would also make `regions_intersect_2()` unnecessary. (I fixed a bug!) – j_random_hacker Apr 17 '19 at 18:28
  • Ok, the size for comments is very very short here. So : thanks I'll try your code. I already started to work on an other solution. Comment from my code for this "other solution" is here: https://pastebin.com/3RuSsbdf I am not sure it will work but I will be able to: 1) detect a thing is against a wall 2) have the linedef to adjust its movement. But I'll keep and test your code and mark it as better answer if it is working. Not before tomorrow evening (still at roughly the same hour) Thanks a lot ! – Bubuche Apr 18 '19 at 00:47
  • @j_random_hacker Cool idea! I don't have now the time to work out that array, but yea that would make things simpler and faster. – jdehesa Apr 18 '19 at 11:36
  • 1
    @Bubuche Thanks for your comments. Note you can post and accept an asnwer to your own question if you do reach to a satisfying solution. – jdehesa Apr 18 '19 at 11:37
  • I validated your answer. Note that it could be improved to fit the original request (for example, when you find the Regions, you could avoid passing the AABB, as it's supposed to be a 1*1 box starting at (0,0). Plus, side note: 1/ The action we have are either "reject" (no intersection), "accept" (intersection) or "do a segment test". 2/ These actions, paradoxally, does not require regions (they are only use to detect the situation) 3/ So => It would be possible to have ONE switch. 4/ But this switch will have a lot of cases so : (message in two part, sorry, limitation) – Bubuche Apr 19 '19 at 01:56
  • It would be "unordered sampling with replacement" with 9 element (top-left etc.) and 3 sampling. BUT : there are equivalent cases. For example "top-left, right, bottom-left" is equivalent to "top-left, bottom, top-right". How hard would it be to merge these cases ? For example, if we store each corner on an even index, and each middle on an odd index, by doing ">> 2" we are just rotating the square. With the rule "at most one trailing 0" and with the function __builtin_ctz (counting trailing zeros) or equivalent, the number of cases in the switch could drop dramatically. I don't know. – Bubuche Apr 19 '19 at 02:10
  • @Bubuche: That entire function can be replaced with the 9-operation branch-free array lookup in my comment. All you need to do is determine what the array should contain, which you can do by running the current version of the function over all 512 possible inputs once beforehand. – j_random_hacker Apr 19 '19 at 12:40
  • switch are also optimized by the compiler. It can use binary search ("if ( v > 5 ) jumpto blabla" and at blabla : "if ( v > 7 ) jump to blibli" etc) But it can also use jump tables, or even arithmetic ("jumpto value * 3" if cases are all separated with break and values are continuous etc). If we are talking about generating stuffs, generating the switch instead of the array would lead to better results. – Bubuche Apr 20 '19 at 00:32
  • If you're talking about a 512-case switch statement, probably the optimal way for the compiler to implement that is using the table I described (it's basically the same as your "jump to value * 3", but there's no need to run more code at some jump target, because we already have the answer). Binary search will almost certainly be slower because of hard-to-predict conditional branches. – j_random_hacker Apr 21 '19 at 02:39
  • 1
    So as I see it, `r1|r2|r3` may have one, two or three in nine bits set, which I think amounts to 129 possible combinations, so a 129-branch switch? (I suppose you can skip some with `default`). However, the biggest posible value would be `0b111000000`, that is 448, so you would need an array of 449. I think I'd still go with the array, neither is great to maintain but I feel more comfortable with a "magic string of numbers" than with a "magic switch-case statement". In any case thank you both for the comments. – jdehesa Apr 21 '19 at 18:50
  • The biggest value is actually: 0b100000000 Because all the three points can't be on the same location. And you don't have 448 values. At all. For example, you don't have the value 0b111100000 Because it has 4 "1". It's why I walk talking about "unordered sampling with replacement". => "I have 9 tokens, with numbers from 1 to 9. Three times, I pick a token, note its number, then put it back. How many combinations". (I don't know, way too far away ^^) – Bubuche Apr 22 '19 at 00:33
  • @Bubuche: The biggest value is not 0b100000000, because for example 0b110100000 is also valid and bigger than that. And you have misunderstood what jdehesa meant about 448. He didn't say there are 448 values; he said you would need an array of size 449 in order to use my single-array-lookup trick. (A slightly smaller array would suffice since no triangle with bitmap 0b111000000 could intersect a rectangle.) The number of combinations -- the number you couldn't be bothered calculating -- is the number he already stated in his comment, 129. – j_random_hacker Apr 22 '19 at 15:19
  • 1
    @jdehesa: You're right, not all 512 bit patterns are possible, and you only need a case statement for those that are. The switch statement could be faster, but my money's definitely on the array! Either way, I would definitely *generate* the source code (array declaration or switch statement), using your existing code to calculate the correct answer for each of the 129 cases. – j_random_hacker Apr 22 '19 at 15:25