diff --git a/documentation/source/development/reference/main.rst b/documentation/source/development/reference/main.rst index fb21d1b1f..983c5b47e 100644 --- a/documentation/source/development/reference/main.rst +++ b/documentation/source/development/reference/main.rst @@ -8,3 +8,4 @@ Reference gpu-selection binary-format-specification octree-file-format + octree-collision diff --git a/documentation/source/development/reference/octree-collision.rst b/documentation/source/development/reference/octree-collision.rst new file mode 100644 index 000000000..5371c9039 --- /dev/null +++ b/documentation/source/development/reference/octree-collision.rst @@ -0,0 +1,167 @@ +Multi octree collision detection +================================ + +Octree collision detection allows us to find intersections between octree geometry and a ray, for example the camera view ray. This is an essential part for the octree editor. Inexor engine allows to have multiple octrees with arbitrary position and relative size (no support for rotations yet), making collision detection significantly more complex than single octree traversal: + +.. image:: octree_collision_sizes.jpg + :width: 500 + :alt: Multiple octree worlds with different relative size. + +In the following screenshot, you can see three octrees of different types and different sizes. One octree is completely filled (we call this an ``Cube::Type::OCTANT`` in the engine). The one in the right has empty sub-cubes in it. You can see that these cubes are not indented at all, because indentation is not taken into account yet for octree collision. [#f1]_ + +.. image:: octree_collision_multiple_octrees.jpg + :width: 500 + :alt: Multiple octrees with different relative size. + +How to find collisions between octree geometry and a rays in this scene now? For simplicity, we assume that the octrees are not intersecting each other. Let's assume we want to write an octree editor. We are obviously only interested in the intersection which is closest to the camera's position: if there is another octree world behind the current selection, we must move the camera to it: [#f3]_ + +.. image:: octree_collision_camera_view_blocked.jpg + :width: 500 + :alt: We are interested in the collision closest to the camera position. + +We are also only interested in collisions which are in front of our camera view. A ray has infinite length, but only the collisions which we are facing are of interest. + +.. image:: octree_collision_only_front_collisions.jpg + :width: 500 + :alt: We are interested in the collision in front of the camera. + +Let's imagine we have now :math:`N` octrees, we want to find all those which collide with the ray and we want to know the one which is closest to the camera. Furthermore, since we want to write an octree editor, we want not only the cube which is in selection, but we also want to know which one of the 6 faces of the cube is in selection. In addition, we want to know the coordinates or the intersection between camera ray and the plane of the selected face. We also need the closest corner on the selected face and the closest edge, just so we have all that we could possibly need. + +- How do we determine if there are even any collisions occuring at all? +- How do we now find out which of the :math:`N` octrees is in selection? +- How do we determine the closest world to our camera's position? + +Assuming we have :math:`N` octrees, the first thing we do is to iterate through every one of the :math:`N` octrees and to check for collision of the camera ray with the bounding sphere of the octree world. This is a quick way to optimize the collision in the beginning and to save a lot of computation time. This is a common trick in computer graphics. If we would check for every possible collision without this step, the algorithm would be way too slow. So we need to iterate through the :math:`N` octrees we have and calculate the distance :math:`d` between the ray and the center of the world's bounding sphere. In our engine, the center of the octree world is also the center of the bounding sphere. We are using `glm::intersectRaySphere `__ to check if a collision is happening. + +.. note:: + Simply iterating through all :math:`N` octrees is a naive approach. This only works for small number of octrees. Much better would be to use a hierarchical data structure like a `bounding volume hierarchy `__, which groups objects which are close to each other into a unified bounding sphere. This hierarchical bounding sphere check is much faster than iterating through all :math:`N` octrees. There are libraries which could help implement this for Inexor in the future. + +After this step, we have :math:`0` to :math:`N` octrees which collide with the ray. The following screenshot shows the possible situations for :math:`N=3`: + +.. image:: octree_collision_cases.jpg + :width: 500 + :alt: Possible collision cases (examples). + +If we have :math:`0` collisions, we can already stop collision detection here because there are no collisions possible: if a camera ray intersects an octree, it must also intersect the bounding sphere. The reverse statement is not true: if a ray collides with a bounding sphere, that does not mean it collides with the octree. It could be a false positive: + +.. image:: octree_collision_boundin_sphere_false_positive.jpg + :width: 500 + :alt: False positive intersection of a bounding sphere. + +We now need to find the world which is closest the camera. The first thing which comes to our mind is sorting the octrees by distance to the camera: we could calculate the distance :math:`d` between camera's position and bounding sphere's center (= the world's center) for every one world which intersects with the camera ray and order them by distance: + +:math:`d = \sqrt{(x_1 - x_2)^2 +(y_1 - y_2)^2 +(z_1 - z_2)^2}` + +The one with the lowest distance will be the one which is closest to the camera. This should be the world we will perform any further detailed collision checks on. However, there are two things we can already optimize here. + + +The square of the distance +-------------------------- + +First, we do not need to sort the octrees by distance. Sorting would mean we need all of the data sorted by distance. We are only interested in the world with the smallest distance though. Since we iterate through all of them, we check if the calculated distance :math:`d` between bounding sphere's center and camera position is smaller than the stored value, and if that is the case, store it as the new closest world. [#f4]_ This is significantly faster than sorting all octrees. We also lose information about the distance to all the other octrees in selection, but that's not important at the moment (at least for the octree editor that is irrelevant for now). As a second optimization, we should not calculate the distance :math:`d` between the bounding sphere's center and the camera's center, as we are not interested in the exact value of the distance. The reason we should avoid this is because distance calculation using `glm::distance `__ makes an expensive `sqrt `__ call, as it needs to calculate the distance like this: + +:math:`d = \sqrt{(x_1 - x_2)^2 +(y_1 - y_2)^2 +(z_1 - z_2)^2}` + +If we take this equation and square both sides, we obtain :math:`{d}^2`, the squared distance: + +:math:`{d}^2 = {(x_1 - x_2)^2+ (y_1 - y_2)^2+ (z_1 - z_2)^2}` + +This way, we do **perform no square root calculation**. The squared distance :math:`{d}^2` will serve as our value for determination of the closest world. Think about it: if the distance :math:`d` is the value which allows us to find the closest world, the square of the distance :math:`{d}^2` will work as well. If you take :math:`N` octrees, each one having a distance :math:`d` to the camera's position, the order will not change if we square the distance. + +.. note:: + For simplicity, we assume that the octrees have a variable position and size, but are **not intersecting each other**. If that is the case, the determination of the world which is closest to the camera would be more complicated. For example if there would be two octrees, one being closer to the camera than the other, but the one further away has a bigger size, maybe resulting in faces which are closer to the camera than the other cube. We will implement support for this in the future. + +Finding the leaf node +--------------------- + +Now that we have found the octree which is closest to the camera, we need to find a leaf node in the octree which is being intersected. The most simple case would be if the octree's root is of type ``Cube::Type::SOLID``, as completely filled octrees are leaf nodes by definition: + +.. image:: octree_collision_filled.jpg + :width: 500 + :alt: An octree of type ``Cube::Type::SOLID``. + +If the octree's root is of type ``Cube::Type::OCTANT``, we need to iterate through 8 all sub-cubes. This is described in the next section. + +.. image:: octree_collision_octant.jpg + :width: 500 + :alt: An octree of type ``Cube::Type::OCTANT``. + +Please note that every octant has 8 sub-cubes, even if some (or even all) of them are of type ``Cube::Type::EMPTY``. [#f5]_ + +.. note:: + Technically, the octree's root could also be of type ``Cube::Type::EMPTY``. In this case, there also no collision possible. However, such octrees will be skipped when iterating through all possible sub-cubes which could possibly collide with the ray. + +Subcube iteration +----------------- + +So we found the octree which is closest to the camera, but it's neither completely empty (``Cube::Type::EMPTY``) or completely filled (``Cube::Type::OCTANT``). We now simply iterate through all 8 sub-cubes and repeat the bounding sphere collision check for every subcube. If a subcube is empty, no collision with it is possible and it will be excluded from detailed collision checks. We now need to find the sub-cube which is closest to the camera gain. We therefore perform the same search by squared distance as we already did for the octree octrees. We simply calculate the squared distance from the center of the sub-cube to the camera and if the distance is lower than the one which is currently stored, we accept it as new closest sub-cube. Imagine a cube is an octant and it has 8 sub-cubes which are all not empty. If a ray goes through that cube, no more than 4 sub-cubes can be intersected. Therefore we abort the hit collection after 4 hits. Once we determined the sub-cube which is closest to the camera, we recursively perform this algorithm until we found a leaf node of type ``Cube::Type::SOLID``. Once a leaf cube was found (either directly or by recursively iterating through sub-cubes until we found one), we proceed to calculate the selected face, as described in the following section. + +.. note:: + Every cube of type ``Cube::Type::OCTANT`` has 8 subcubes. Iterating through all subcubes from index :math:`0` to :math:`7` is a naive approach as well. Inexor should use a fast octree traversal algorithm in the future. For more information, check out `this paper `__. Also check out the `hero algorithm `__. + + +Determination of selected face +------------------------------ + +Now that we have found a leaf node of type ``Cube::Type::SOLID``, we need to determine on which one of the 6 faces (left, right, top, bottom, front, back) the collision takes place. We are only interested in the intersection which is facing the camera. That is also the intersection which is closer to the camera position. There is also a backside intersection from the outgoing ray, but we are not interested in this for now. There are several ways how to determine which face is in collision. We decided to use the following approach: first we filter out all sides of the cube which are not facing the camera. In order to do so, let's take a look at the following equation which describes the angle :math:`\alpha` of two vectors :math:`\vec{a}` and :math:`\vec{a}`: + +:math:`cos(\alpha) = \frac{\vec{a}\cdot\vec{b}}{|a| \cdot |b|}` + +If we define :math:`\vec{a}` as the normal vector on the face and :math:`\vec{b}` as the camera direction vector, we realize that the normal vector on the cube's face is no longer facing the camera if the angle :math:`\alpha` becomes greater than ``90 degrees``. We now think we should rearrange for the angle: + +:math:`\alpha = cos^{-1}\left(\frac{\vec{a}\cdot\vec{b}}{|a| \cdot |b|}\right)` + +However, we can simplify this: If the angle is slightly greater than ``90 degrees``, the value of :math:`cos(\alpha)` becomes smaller than ``0``. If the angle is a little less than ``90 degrees``, :math:`cos(\alpha)` becomes greater than ``0``. If we take a look at the right side of the equation we started with, we can see that the dot product of :math:`\vec{a}` and :math:`\vec{b}` is in the nominator while the product of the magnitudes is in the denominator. Since the magnitude of a vector is never negative, the product of two magnitudes will always be positive. We now see that the sign change is entirely dictated by the nominator. Furthermore, we already elaborated that it's comparably expensive to calculate the square root. We can simplify all this to the following condition: the face on a cube is visible, if the dot product of the two vectors :math:`\vec{a}` and :math:`\vec{b}` is smaller than zero: + +:math:`\alpha < 0` for :math:`\vec{a}\cdot\vec{b} < 0` + +This is quite nice, because the dot product of :math:`\vec{a}` and :math:`\vec{b}` is a cheap calculation. This is another very popular trick in computer graphics. [#f6]_ + +We now simply iterate through all 6 faces of the cube, take the normal vector on that cube face and check if it's facing the camera. We are only interested in the planes which are facing the camera. [#f7]_ If you look at a cube, no more than 3 sides can be visible at the same time. This means we can stop after we found 3 cube sides which are facing the camera. It could be less than 3 sides though. Imagine you are right on top of a solid cube and your look down on it, only the top side is visible. If you look from a certain position, only 2 sides are visible. + +.. image:: octree_collision_cube_facing_camera.jpg + :width: 500 + :alt: No more than 3 sides of a cube can be seen. It could be less though. + +.. note:: + We could optimize this in the future by doing some coordinate checks of the camera and the octree. For example if the ``x`` and ``y`` coordinates are inside the square of the cube, we could only see top or bottom of the cube. However, since Inexor wants to account for arbitrary rotations around all 3 axis, this is more complex than for unrotated octrees. We think our current solution is sufficiently performant. + +We now have 3 or less sides of the cube facing the camera. We calculate the intersection point between the ray and every plane which represents a cube face. In order to determine the real intersection, we come back to searching the lowest squared distance again. However, it is important to state that we can't use the squared distance to the camera position in this case. We must calculate the squared distance between the intersection point on every plane and the center of the cube's face which is associated to this plane. This way, we find the real intersection point and the selected corner: + +.. image:: octree_collision_real_face.jpg + :width: 500 + :alt: Possible intersections. + +Calculation of closest corner +----------------------------- + +We now successfully determined the selected face and the intersection point. We already know the coordinates of every one of the 4 corners on that face. In order to determine the nearest corner, we come back to calculating the squared distance between the intersection point and every corner point. The corner with the lowest squared distance is the nearest. + +.. image:: octree_collision_nearest_corner.jpg + :width: 500 + :alt: Possible intersections. + +Calculation of closest edge +--------------------------- + +The determination of the closest edge works the same way as the determination of the closest corner: searching the lowest squared distance between intersection point and center of the four edges on the selected face. + +Closing remarks +--------------- + +With this algorithm, we have a good starting point writing an octree editor. However, we know that this is not the fastest solution possible. Nevertheless, it is a solution which is easy to understand, easy to improve and easy to optimize for sure. Furthermore, it will be easy to parallelize it. All the aspects which could be improved have been listed on this page. + + +.. rubric:: Footnotes + +.. [f1] The current implementation of octree-ray intersection only checks for intersections with completely filled cubes and does not take into account indentations of cubes, as this is not required for an octree editor. The bounding box of an octree is always unchanged, even if the octree geometry itself has indentations. Taking into account indentations will be required for physics calculations in the future, for example to check collisions between particles and octree. + +.. [f2] We could also make the layer which is blocking view invisible for a moment in the future. + +.. [f4] To do so, we need to set the initial value of the distance to a maximum value. We use ``std::numeric_limits::max()`` + +.. [f5] This has to do with the way the engine lays out memory for the octree data structure. The engine will allocate memory for the empty sub-cube because it's faster to change the sub-cube's data if it gets modified. However, empty sub-cubes will not result in additional vertex or index data being generated. + +.. [f6] In fact this is used during the rasterization step in rendering to discard all triangles which are not facing the camera. + +.. [f7] For some reasons we might be interested in those sides of a cube which are not facing the camera in the future? diff --git a/documentation/source/development/reference/octree_collision_boundin_sphere_false_positive.jpg b/documentation/source/development/reference/octree_collision_boundin_sphere_false_positive.jpg new file mode 100644 index 000000000..1e37b7669 Binary files /dev/null and b/documentation/source/development/reference/octree_collision_boundin_sphere_false_positive.jpg differ diff --git a/documentation/source/development/reference/octree_collision_camera_view_blocked.jpg b/documentation/source/development/reference/octree_collision_camera_view_blocked.jpg new file mode 100644 index 000000000..e32dc68fe Binary files /dev/null and b/documentation/source/development/reference/octree_collision_camera_view_blocked.jpg differ diff --git a/documentation/source/development/reference/octree_collision_cases.jpg b/documentation/source/development/reference/octree_collision_cases.jpg new file mode 100644 index 000000000..595afea90 Binary files /dev/null and b/documentation/source/development/reference/octree_collision_cases.jpg differ diff --git a/documentation/source/development/reference/octree_collision_cube_facing_camera.jpg b/documentation/source/development/reference/octree_collision_cube_facing_camera.jpg new file mode 100644 index 000000000..1e74fa60d Binary files /dev/null and b/documentation/source/development/reference/octree_collision_cube_facing_camera.jpg differ diff --git a/documentation/source/development/reference/octree_collision_filled.jpg b/documentation/source/development/reference/octree_collision_filled.jpg new file mode 100644 index 000000000..bee0c93e6 Binary files /dev/null and b/documentation/source/development/reference/octree_collision_filled.jpg differ diff --git a/documentation/source/development/reference/octree_collision_multiple_octrees.jpg b/documentation/source/development/reference/octree_collision_multiple_octrees.jpg new file mode 100644 index 000000000..d3a2a3d66 Binary files /dev/null and b/documentation/source/development/reference/octree_collision_multiple_octrees.jpg differ diff --git a/documentation/source/development/reference/octree_collision_nearest_corner.jpg b/documentation/source/development/reference/octree_collision_nearest_corner.jpg new file mode 100644 index 000000000..722ae00ad Binary files /dev/null and b/documentation/source/development/reference/octree_collision_nearest_corner.jpg differ diff --git a/documentation/source/development/reference/octree_collision_octant.jpg b/documentation/source/development/reference/octree_collision_octant.jpg new file mode 100644 index 000000000..3b0a3ba64 Binary files /dev/null and b/documentation/source/development/reference/octree_collision_octant.jpg differ diff --git a/documentation/source/development/reference/octree_collision_only_front_collisions.jpg b/documentation/source/development/reference/octree_collision_only_front_collisions.jpg new file mode 100644 index 000000000..6c6d38419 Binary files /dev/null and b/documentation/source/development/reference/octree_collision_only_front_collisions.jpg differ diff --git a/documentation/source/development/reference/octree_collision_real_face.jpg b/documentation/source/development/reference/octree_collision_real_face.jpg new file mode 100644 index 000000000..2fadb05bb Binary files /dev/null and b/documentation/source/development/reference/octree_collision_real_face.jpg differ diff --git a/documentation/source/development/reference/octree_collision_sizes.jpg b/documentation/source/development/reference/octree_collision_sizes.jpg new file mode 100644 index 000000000..006035dae Binary files /dev/null and b/documentation/source/development/reference/octree_collision_sizes.jpg differ