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