LearnOpenGL

Translation in progress of learnopengl.com.
git clone https://git.mtkn.jp/LearnOpenGL
Log | Files | Refs

Frustum-Culling.html (26244B)


      1     <div id="content">
      2     <h1 id="content-title">Frustum Culling</h1>
      3 <h1 id="content-url" style='display:none;'>Guest-Articles/2021/Scene/Frustum-Culling</h1>
      4 <p>
      5         Now we know how to create a Scene graph and organize your object in a scene, we are going to see how to limit your GPU usage thanks to a technical name's the frustum culling.
      6         This technique is simple to understand.
      7         Instead of sending all information to your GPU, you will sort visible and invisible elements and render only visible elements.
      8         Thanks to this technique, you will earn GPU compute time.
      9         You need to know that when information travels toward another unit in your computer, it takes a long time.
     10         For example, information from your GPU to your ram takes time.
     11         It's the same if you want to send information from your CPU to your GPU like a model matrice.
     12         It's for this reason that the "draw instance" is so powerful.
     13         You send a large block to your GPU instead of sending elements one by one.
     14         But this technique isn’t free.
     15         To sort your element, you need to create a physical scene to compute some stuff with math.
     16         This chapter will start with an introduction to the mathematical concept that will allow us to understand how frustum culling works.
     17         Next, we are going to implement it.
     18         Finally, we are going to study possible optimizations and talk about the balance of the technical.
     19     </p>
     20 
     21     <video width="850" controls>
     22       <source src="/img\guest\2021\Frustum_culling\frustumExample.mp4" type="video/mp4">
     23         Your browser does not support the video tag.
     24     </video>
     25 
     26       <p>
     27     In this video illustrating frustum culling in a forest, the yellow and red shape on the left side is the bounding volume that contains the mesh.
     28     Red color means that the mesh is not visible and not sent to the GPU.
     29     Yellow means that the mesh is rendered.
     30     As you can see lots of things are rendered and few are visible for the player.
     31       </p>
     32 
     33     <h2>Mathematical concept</h2>
     34 
     35     <p>
     36         Let's start the mathematical parts from top to bottom.
     37         Firstly, what is a frustum?
     38         As we can see in <a href="https://en.wikipedia.org/wiki/Frustum" target="_blank"> Wikipedia</a>, frustum is a portion of a solid like a cone or pyramid.
     39         The frustum is usually used in game engine to speak about the camera frustum.
     40         Camera frustum represents the zone of vision of a camera.
     41         Without limit, we have a pyramid but with near and far we have a frustum. 
     42     </p>
     43 
     44       <img src="/img/guest/2021/Frustum_culling/VisualCameraFrustum.png" alt="Camera frustum shape"/>
     45 
     46     <p>
     47         How to mathematically represent a frustum?
     48         Thanks to 6 plans: near, far, right, left top and bottom plans.
     49         So, an object is visible if it is forward or on the 6 plans.
     50         Mathematically a plan is represented with a normal vector and distance to the origin.
     51         A plan doesn't have any size or limit as a quad. 
     52     </p>
     53 
     54       <img src="/img/guest/2021/Frustum_culling/plan.png"  width="600" alt="Plan representation"/>
     55 
     56     <p>
     57         So, create a struct to represent a plan:
     58     </p>
     59     <pre><code>
     60 struct Plan
     61 {
     62     // unit vector
     63     glm::vec3 normal = { 0.f, 1.f, 0.f };
     64 
     65     // distance from origin to the nearest point in the plan
     66     float     distance = 0.f;             
     67 
     68     [...]
     69 };
     70     </code></pre>
     71 
     72     <p>
     73      We can now create <fun>Frustum</fun> structure:
     74     </p>
     75 
     76     <pre><code>
     77 struct Frustum
     78 {
     79     Plan topFace;
     80     Plan bottomFace;
     81 
     82     Plan rightFace;
     83     Plan leftFace;
     84 
     85     Plan farFace;
     86     Plan nearFace;
     87 };
     88     </code></pre>
     89 
     90     <p>
     91         Reminder: a plan can be built with a point and a normal. 
     92         For the near, the normal is the front vector of the camera. 
     93         For the far plan, it's the opposite. 
     94         The normal of the right face we will need to do a cross product.
     95         The cross product is the second wonderful tool for the programmer who likes vectors.
     96         It allows you to get a perpendicular vector to a plan created with two vectors.
     97         To go forward, we need to do the cross product of the right axis per up.
     98         We will use it like that:
     99     </p>
    100 
    101       <img src="/img/guest/2021/Frustum_culling/RightNormal.png" alt="Plan representation"/>
    102 
    103     <p>
    104         But to know the direction of each vector from the camera to the far plan we will know the side length of the far quad:
    105     </p>
    106       <img src="/img/guest/2021/Frustum_culling/hAndVSide.png" alt="Plan representation"/>
    107 
    108     <p>
    109         hSide and vSide are the far quad limited by the other plans of the camera frustum.
    110         To compute its edge, we will need of trigonometry.
    111         As you can see in the image above, we have two rectangle triangles and we can apply the trigonometric functions.
    112         So, we would like to obtain vSide which is the opposite side and we have zFar that is the adjacent side of the camera.
    113         Tan of fovY is equal to the opposite side (vSide) divided by the adjacent side (zFar).
    114         In conclusion, if I move the adjacent side on the left on our equation, tan of fovY multiplied by the zFar is equal to the vSide.
    115         We now need to compute hSide.
    116         Thanks to the aspect that is a ratio of the width by the height, we can easily obtain it.
    117         So, hSide is equal to the vSide multiplied by the aspect as you can see on the right side of the image above.
    118         We can now implement our function:
    119     </p>
    120 
    121     <pre><code>
    122 Frustum createFrustumFromCamera(const Camera& cam, float aspect, float fovY,
    123                                                                 float zNear, float zFar)
    124 {
    125     Frustum     frustum;
    126     const float halfVSide = zFar * tanf(fovY * .5f);
    127     const float halfHSide = halfVSide * aspect;
    128     const glm::vec3 frontMultFar = zFar * cam.Front;
    129 
    130     frustum.nearFace = { cam.Position + zNear * cam.Front, cam.Front };
    131     frustum.farFace = { cam.Position + frontMultFar, -cam.Front };
    132     frustum.rightFace = { cam.Position,
    133                             <function id='61'>glm::cross</function>(cam.Up,frontMultFar + cam.Right * halfHSide) };
    134     frustum.leftFace = { cam.Position,
    135                             <function id='61'>glm::cross</function>(frontMultFar - cam.Right * halfHSide, cam.Up) };
    136     frustum.topFace = { cam.Position,
    137                             <function id='61'>glm::cross</function>(cam.Right, frontMultFar - cam.Up * halfVSide) };
    138     frustum.bottomFace = { cam.Position,
    139                             <function id='61'>glm::cross</function>(frontMultFar + cam.Up * halfVSide, cam.Right) };
    140 
    141     return frustum;
    142 }
    143 </code></pre>
    144 <note>
    145     In this example, the camera doesn't know the near, aspect but I encourage you to include this variable inside your Camera class.
    146 </note>
    147 
    148     <h3>Bounding volume</h3>
    149 
    150     <p>
    151         Let's take a minute to imagine an algorithm that can detect collisions with your mesh (with all types of polygons in general) and a plan.
    152         You will start to say that image is an algorithm that checks if a triangle is on or outside the plane.
    153         This algorithm looks pretty and fast! But now imagine that you have hundreds of mesh with thousands of triangles each one.
    154         Your algorithm will sign the death of your frame rate fastly.
    155         Another method is to wrap your objects in another geometrical object with simplest properties such as a sphere, a box, a capsule...
    156         Now our algorithm looks possible without creating a framerate black hole.
    157         Its shape is called bounding volume and allows us to create a simpler shape than our mesh to simplify the process.
    158         All shapes have their own properties and can correspond plus or minus to our mesh. 
    159     </p>
    160       <img src="/img/guest/2021/Frustum_culling/boundingVolumeQuality.png" alt="Bounding volume quality vs computation speed"/>
    161 
    162     <p>
    163         All shapes also have their own compute complexity.
    164         The <a href="https://en.wikipedia.org/wiki/Bounding_volume" target="_blank">article</a> on Wikipedia is very nice and describes some bounding volumes with their balance and application.
    165         In this article, we are going to see 2 bounding volumes: the sphere and the AABB.
    166         Let's create a simple abstract struct Volume that represent all our bounding volumes: 
    167     </p>
    168 
    169     <pre><code>
    170 struct Volume
    171 {
    172     virtual bool isOnFrustum(const Frustum& camFrustum,
    173                                             const Transform& modelTransform) const = 0;
    174 };
    175     </code></pre>
    176 
    177     <h4>Sphere</h4>
    178 
    179       <img src="/img/guest/2021/Frustum_culling/boundingSphere.png" alt="Bounding sphere example"/>
    180 
    181     <p>
    182         The bounding sphere is the simplest shape to represent a bounding volume.
    183         It is represented by center and radius.
    184         A sphere is ideal to encapsulate mesh with any rotation.
    185         It must be adjusted with the scale and position of the object.
    186         We can create struct Sphere that inheritance from volume struct:
    187     </p>
    188 
    189     <pre><code>
    190 struct Sphere : public Volume
    191 {
    192     glm::vec3 center{ 0.f, 0.f, 0.f };
    193     float radius{ 0.f };
    194 
    195     [...]
    196 }
    197     </code></pre>
    198 
    199     <p>
    200         This struct doesn't compile because we haven't defined the function isOnFrustum.
    201         Let's make it.
    202         Remember that our bounding volume is processed thanks to our meshes.
    203         That assumes that we will need to apply a transform to our bounding volume to apply it.
    204         As we have seen in the previous chapter, we will apply the transformation to a scene graph.
    205     </p>
    206 
    207     <pre><code>
    208 bool isOnFrustum(const Frustum& camFrustum, const Transform& transform) const final
    209 {
    210     //Get global scale is computed by doing the magnitude of
    211     //X, Y and Z model matrix's column.
    212     const glm::vec3 globalScale = transform.getGlobalScale();
    213 
    214     //Get our global center with process it with the global model matrix of our transform
    215     const glm::vec3 globalCenter{ transform.getModelMatrix() * glm::vec4(center, 1.f) };
    216 
    217     //To wrap correctly our shape, we need the maximum scale scalar.
    218     const float maxScale = std::max(std::max(globalScale.x, globalScale.y), globalScale.z);
    219 
    220     //Max scale is assuming for the diameter. So, we need the half to apply it to our radius
    221     Sphere globalSphere(globalCenter, radius * (maxScale * 0.5f));
    222 
    223     //Check Firstly the result that have the most chance
    224     //to faillure to avoid to call all functions.
    225     return (globalSphere.isOnOrForwardPlan(camFrustum.leftFace) &&
    226         globalSphere.isOnOrForwardPlan(camFrustum.rightFace) &&
    227         globalSphere.isOnOrForwardPlan(camFrustum.farFace) &&
    228         globalSphere.isOnOrForwardPlan(camFrustum.nearFace) &&
    229         globalSphere.isOnOrForwardPlan(camFrustum.topFace) &&
    230         globalSphere.isOnOrForwardPlan(camFrustum.bottomFace));
    231 };
    232     </code></pre>
    233     <note>
    234         To compute the globalCenter we can’t only add the current center with the global position because we need to apply translation caused by rotation and scale.
    235         This is the reason why we use the model matrix. 
    236     </note>
    237 
    238     <p>
    239         As you can see, we used a function undefined for now called <fun>isOnOrForwardPlan</fun>.
    240         This implementation method is called top/down programming and consists to create a high-level function to determine which kind of function need to be implemented.
    241         It avoids to implement too many unused functions that can be the case in "bottom/up".
    242         So to understand how this function works, let's make a drawing :
    243     </p>
    244 
    245       <img src="/img/guest/2021/Frustum_culling/SpherePlanDetection.png" width="400" height="400" alt="Sphere plan collision shema"/>
    246 
    247     <p>
    248         We can see 3 possible cases: Sphere is inside the plan, back or forward.
    249         To detect when a sphere is colliding with a plan we need to compute the nearest distance from the center of the sphere to the plan.
    250         When we have this distance, we need to compare this distance with radius.
    251     </p>
    252 
    253     <pre><code>
    254 bool isOnOrForwardPlan(const Plan& plan) const
    255 {
    256     return plan.getSignedDistanceToPlan(center) > -radius;
    257 }
    258     </code></pre>
    259 
    260     <note>
    261         We can see the problem in the other way and create a function called <fun>isOnBackwardPlan</fun>.
    262         To use it we simply need to check if bounding volume IS NOT on the backward plan
    263     </note>
    264 
    265     <p>
    266         Now we need to create the function <fun>getSignedDistanceToPlan</fun> in the </un>Plan</fun> structure.
    267         Let me realize my most beautiful paint for you :
    268     </p>
    269 
    270 <img src="/img/guest/2021/Frustum_culling/SignedDistanceDraw.png" width="400" height="400" alt="Signed distance to plan shema"/>
    271     
    272     <p>
    273         Signed distance is a positive distance from a point if this point is forward the plan.
    274         Otherwise this distance will be negative.
    275         To obtain it, we will need to call a friend: The dot product.
    276         Dot product allows us to obtain the projection from a vector to another.
    277         The result of the dot product is a scale and this scalar is a distance.
    278         If both vectors go oppositely, the dot product will be negative.
    279         Thanks to it, we will obtain the horizontal scale component of a vector in the same direction as the normal of the plan.
    280         Next, we will need to subtract this dot product by the nearest distance from the plan to the origin.
    281         Hereafter you will find the implementation of this function :
    282     </p>
    283 
    284     <pre><code>
    285 float getSignedDistanceToPlan(const glm::vec3& point) const
    286 {
    287     return glm::dot(normal, point) - distance;
    288 }
    289     </code></pre>
    290 
    291     <h4>AABB</h4>
    292 <img src="/img/guest/2021/Frustum_culling/boundingAABB.png" alt="Bounding AABB example"/>
    293 
    294     <p>
    295         AABB is the acronym of Axis aligned bounding box.
    296         It means that this volume has the same orientation as the world.
    297         It can be constructed as different can be we generally create it with its center and its half extension.
    298         The half extension is a distance from center to the edge in the direction of an axis.
    299         The half extension can be called Ii, Ij, Ik. In this chapter, we will call it Ix, Iy, Iz.
    300     </p>
    301 
    302 <img src="/img/guest/2021/Frustum_culling/AABBRepresentation.png" width="400" alt="AABB representation"/>
    303 
    304     <p>
    305         Let's make the base of this structure with few constructors to made its creation the simplest
    306     </p>
    307 
    308     <pre><code>
    309 struct AABB : public BoundingVolume
    310 {
    311     glm::vec3 center{ 0.f, 0.f, 0.f };
    312     glm::vec3 extents{ 0.f, 0.f, 0.f };
    313 
    314     AABB(const glm::vec3& min, const glm::vec3& max)
    315         : BoundingVolume{},
    316         center{ (max + min) * 0.5f },
    317         extents{ max.x - center.x, max.y - center.y, max.z - center.z }
    318     {}
    319 
    320     AABB(const glm::vec3& inCenter, float iI, float iJ, float iK)
    321         : BoundingVolume{}, center{ inCenter }, extents{ iI, iJ, iK }
    322     {}
    323 
    324     [...]
    325 };
    326     </code></pre>
    327 
    328     <p>
    329         We now need to add the function <fun>isOnFrustum</fun> and <fun>isOnOrForwardPlan</fun>.
    330         The problem is not easy as a bounding sphere because if I rotate my mesh, the AABB will need to be adjusted.
    331         An image talks much than a text :
    332     </p>
    333 
    334 <img src="/img/guest/2021/Frustum_culling/AABBProblem.png" alt="AABB rotation probleme"/>
    335 
    336     <p>
    337         To solve this problem lets draw it :
    338     </p>
    339 
    340 <img src="/img/guest/2021/Frustum_culling/AABB orientation.png" alt="AABB orientation problem explication"/>
    341 
    342     <p>
    343         Crazy guys want to rotate our beautiful Eiffel tower but we can see that after its rotation, the AABB is not the same.
    344         To make the Shema more readable, assume that referential is not a unit and represented the half extension with the orientation of the mesh.
    345         To adjust it, we can see in the third picture that the new extension is the sum of the dot product with the world axis and the scaled referential of our mesh.
    346         The problem is seen in 2D but in 3D it's the same thing. Let's implement the function to do it. 
    347     </p>
    348 
    349     <pre><code>
    350 bool isOnFrustum(const Frustum& camFrustum, const Transform& transform) const final
    351 {
    352     //Get global scale thanks to our transform
    353     const glm::vec3 globalCenter{ transform.getModelMatrix() * glm::vec4(center, 1.f) };
    354 
    355     // Scaled orientation
    356     const glm::vec3 right = transform.getRight() * extents.x;
    357     const glm::vec3 up = transform.getUp() * extents.y;
    358     const glm::vec3 forward = transform.getForward() * extents.z;
    359 
    360     const float newIi = std::abs(glm::dot(glm::vec3{ 1.f, 0.f, 0.f }, right)) +
    361         std::abs(glm::dot(glm::vec3{ 1.f, 0.f, 0.f }, up)) +
    362         std::abs(glm::dot(glm::vec3{ 1.f, 0.f, 0.f }, forward));
    363 
    364     const float newIj = std::abs(glm::dot(glm::vec3{ 0.f, 1.f, 0.f }, right)) +
    365         std::abs(glm::dot(glm::vec3{ 0.f, 1.f, 0.f }, up)) +
    366         std::abs(glm::dot(glm::vec3{ 0.f, 1.f, 0.f }, forward));
    367 
    368     const float newIk = std::abs(glm::dot(glm::vec3{ 0.f, 0.f, 1.f }, right)) +
    369         std::abs(glm::dot(glm::vec3{ 0.f, 0.f, 1.f }, up)) +
    370         std::abs(glm::dot(glm::vec3{ 0.f, 0.f, 1.f }, forward));
    371 
    372     //We not need to divise scale because it's based on the half extention of the AABB
    373     const AABB globalAABB(globalCenter, newIi, newIj, newIk);
    374 
    375     return (globalAABB.isOnOrForwardPlan(camFrustum.leftFace) &&
    376         globalAABB.isOnOrForwardPlan(camFrustum.rightFace) &&
    377         globalAABB.isOnOrForwardPlan(camFrustum.topFace) &&
    378         globalAABB.isOnOrForwardPlan(camFrustum.bottomFace) &&
    379         globalAABB.isOnOrForwardPlan(camFrustum.nearFace) &&
    380         globalAABB.isOnOrForwardPlan(camFrustum.farFace));
    381 };
    382     </code></pre>
    383 
    384     <p>
    385         For the function <fun>isOnOrForwardPlan</fun>, I have taken an algorithm that I found in a wonderful <a href="https://gdbooks.gitbooks.io/3dcollisions/content/Chapter2/static_aabb_plan.html" target="_blank">article</a>.
    386         I invite you to have a look at it if you want to understand how it works.
    387         I just modify the result of its algorithm to check if the AABB is on or forward my plan.
    388     </p>
    389 
    390     <pre><code>
    391 bool isOnOrForwardPlan(const Plan& plan) const
    392 {
    393     // Compute the projection interval radius of b onto L(t) = b.c + t * p.n
    394     const float r = extents.x * std::abs(plan.normal.x) +
    395             extents.y * std::abs(plan.normal.y) + extents.z * std::abs(plan.normal.z);
    396 
    397     return -r <= plan.getSignedDistanceToPlan(center);
    398 }
    399     </code></pre>
    400 
    401     <p>
    402         To check if our algorithm works, we need to check that every object disappeared in front of our camera when we moved.
    403         Then, we can add a counter that is incremented if an object is displayed and another for the total displayed in our console.
    404     </p>
    405 
    406     <pre><code>
    407 // in main.cpp main lopp
    408 unsigned int total = 0, display = 0;
    409 ourEntity.drawSelfAndChild(camFrustum, ourShader, display, total);
    410 std::cout << "Total process in CPU : " << total;
    411 std::cout << " / Total send to GPU : " << display << std::endl;
    412 
    413 // In the drawSelfAndChild function of entity
    414 void drawSelfAndChild(const Frustum& frustum, Shader& ourShader,
    415                                             unsigned int& display, unsigned int& total)
    416 {
    417     if (boundingVolume->isOnFrustum(frustum, transform))
    418     {
    419         ourShader.setMat4("model", transform.getModelMatrix());
    420         pModel->Draw(ourShader);
    421         display++;
    422     }
    423     total++;
    424 
    425     for (auto&& child : children)
    426     {
    427         child->drawSelfAndChild(frustum, ourShader, display, total);
    428     }
    429 }
    430     </code></pre>
    431 
    432 <img src="/img/guest/2021/Frustum_culling/result.png" alt="Result"/>
    433 
    434     <p>
    435         Ta-dah ! The average of objects sent to our GPUrepresents now about 15% of the total and is only divided by 6.
    436         A wonderful result if your GPU process is the bottleneck because of your shader or number of polygons.
    437         You can find the code <a href="https://learnopengl.com/code_viewer_gh.php?code=src/8.guest/2021/1.scene/2.frustum_culling/frustum_culling.cpp" target="_blank">here</a>.
    438     </p>
    439 
    440     <h2>Optimization</h2>
    441     <p>
    442         Now you know how to make your frustum culling.
    443         Frustum culling can be useful to avoid computation of things that are not visible.
    444         You can use it to not compute the animation state of your entity, simplify its AI...
    445         For this reason, I advise you to add a IsInFrustum flag in your entity and do a frustum culling pass that fills this variable.
    446     </p>
    447     <h3>Space partitionning</h3>
    448     <p>
    449         In our example, frustum culling is a good balance with a small number of entities in the CPU.
    450         If you want to optimize your detection, you now will need to partition your space. 
    451         To do it, a lot of algorithms exist and each has interesting properties which depend on your usage :
    452         - BSH (Bounding sphere hierarchy or tree) : 
    453             Different kinds exist. The simplest implementation is to wrap both nearest objects in a sphere.
    454             Wrap this sphere with another group or objetc etc...
    455       <img src="/img/guest/2021/Frustum_culling/BSH.png"  width="400" height="400" alt="BSH example"/>
    456     </p>
    457     <note>
    458         In this example, only 2 checks allow us to know that 3 objects are in frustum instead of 6 because if the bounding sphere is totally inside the frustum all its content is also inside.
    459         If the bounding sphere is not inside when needed to inter and check its content.
    460     </note>
    461     <p>
    462         - <a href="https://en.wikipedia.org/wiki/Quadtree" target="_blank">Quadtree</a> : 
    463             The main idea is that you will split space into 4 zones that can be split into four zones etc... until an object wasn't wrapped alone.
    464             Your object will be the leaf of this diagram.
    465             The quadtree is very nice to partition 2D spaces but also if you don't need to partition height. It can be very useful in strategy games like 4x (like age of empire, war selection...) because you don't need height partitioning.
    466       <img src="/img/guest/2021/Frustum_culling/quadtree.png" width="400" height="400" alt="Quatree example"/>
    467         - <a href="https://en.wikipedia.org/wiki/Octree" target="_blank">Octree</a> : 
    468             It's like a quadtree but with 8 nodes. It's nice if you have a 3D game with elements in different height levels.
    469       <img src="/img/guest/2021/Frustum_culling/octree.png"  width="500" alt="Octree example"/>
    470         - <a href="https://en.wikipedia.org/wiki/Binary_space_partitioning" target="_blank">BSP (binary space partitioning)</a> : 
    471             It's a very fast algorithm that allows you to split space with segments. You will define a segment and the algorithm will sort if an object is in front of this segment or behind.
    472             It's very useful with a map, city, dungeon... The segments can be created at the same time if you generate a map and can be fast forward.
    473       <img src="/img/guest/2021/Frustum_culling/BSP.png" alt="BSP example"/>
    474         - Lot of other methods exist, be curious.
    475         I don't implement each of these methods, I just learn it to know that they exist if one day I need specific space partitioning.
    476         Some algorithm is great to parallelize like octree of quadtree if you use multithread and must also balance on your decision.
    477     </p>
    478 
    479     <h3>Compute shader</h3>
    480     <p>
    481         Compute shader allows you to process computation on shader.
    482         This technique must be used only if you have a high parallelized task like check collision with a simple list of bounds.
    483         I never implemented this technique for the frustum culling but it can be used in this case to avoid updating space partitioning if you have a lot of objects that move.
    484     </p>
    485 
    486     <h2>Additional resources</h2>
    487     <ul>
    488 		<li><a href="http://www.cs.otago.ac.nz/postgrads/alexis/planExtraction.pdf" target="_blank">
    489 			Article about camera frustum extraction</a>: Fast Extraction of Viewing Frustum Plans from the WorldView-Projection Matrix by Gil Gribb and Klaus Hartmann</li>    
    490 
    491         <li><a href="https://gdbooks.gitbooks.io/3dcollisions/content/Chapter1/aabb.html" target="_blank">
    492             Article about collisions detection</a>: A wonderful resource for collision detection and another approach about volume, culling and mathematic concept</li>   
    493             
    494         <li><a href="https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/frustum-culling-r4613/" target="_blank">
    495             Article to go further</a>: A good article to go further on GPU culling process, multithreading and OBB</li>
    496         </ul>
    497 
    498 	
    499       <author>
    500 			<strong>Article by: </strong>Six Jonathan<br>
    501 			<strong>Contact: </strong><a href="Six-Jonathan@orange.fr" target="_blank">e-mail</a><br>
    502 			<strong>Date: </strong> 09/2021<br>
    503 			<div>
    504 				<a href="https://github.com/Renardjojo">
    505 					<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle">
    506 						<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
    507 					</svg>
    508 				</a>
    509 				<a href="https://www.linkedin.com/in/jonathan-six-4553611a9/">
    510 					<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 34 34" class="global-nav__logo">
    511 						<path d="M34,2.5v29A2.5,2.5,0,0,1,31.5,34H2.5A2.5,2.5,0,0,1,0,31.5V2.5A2.5,2.5,0,0,1,2.5,0h29A2.5,2.5,0,0,1,34,2.5ZM10,13H5V29h5Zm.45-5.5A2.88,2.88,0,0,0,7.59,4.6H7.5a2.9,2.9,0,0,0,0,5.8h0a2.88,2.88,0,0,0,2.95-2.81ZM29,19.28c0-4.81-3.06-6.68-6.1-6.68a5.7,5.7,0,0,0-5.06,2.58H17.7V13H13V29h5V20.49a3.32,3.32,0,0,1,3-3.58h.19c1.59,0,2.77,1,2.77,3.52V29h5Z" fill="currentColor"></path>
    512 					</svg>
    513 				</a>
    514 			</div>
    515       </author>       
    516 
    517     </div>
    518