From d5d391faaf69027b8fecb26f30754c3bff83c311 Mon Sep 17 00:00:00 2001 From: Joel Davis Date: Mon, 2 Jan 2017 21:56:25 -0800 Subject: [PATCH] Added RaycastMesh function and example test case --- examples/core_3d_raypick.c | 165 ++++++-- examples/resources/model/lowpoly-tower.obj | 456 +++++++++++++++++++++ examples/resources/model/lowpoly-tower.png | Bin 0 -> 24939 bytes src/models.c | 38 ++ src/raylib.h | 3 + src/raymath.h | 26 ++ src/shapes.c | 75 +++- 7 files changed, 712 insertions(+), 51 deletions(-) create mode 100644 examples/resources/model/lowpoly-tower.obj create mode 100644 examples/resources/model/lowpoly-tower.png diff --git a/examples/core_3d_raypick.c b/examples/core_3d_raypick.c index c1c327718..cf56b2773 100644 --- a/examples/core_3d_raypick.c +++ b/examples/core_3d_raypick.c @@ -1,15 +1,21 @@ /******************************************************************************************* * -* raylib [core] example - Ray-Picking in 3d mode, also ground plane +* raylib [core] example - Ray-Picking in 3d mode, ground plane, triangle, mesh * * This example has been created using raylib 1.3 (www.raylib.com) * raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) * * Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example contributed by Joel Davis (@joeld42) * ********************************************************************************************/ #include "raylib.h" +#include "raymath.h" + +#include +#include + int main() { @@ -22,24 +28,36 @@ int main() // Define the camera to look into our 3d world Camera camera; - camera.position = (Vector3){ 10.0f, 10.0f, 10.0f }; // Camera position - camera.target = (Vector3){ 0.0f, 0.0f, 0.0f }; // Camera looking at point - camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target) + camera.position = (Vector3){ 10.0f, 8.0f, 10.0f }; // Camera position + camera.target = (Vector3){ 0.0f, 2.3f, 0.0f }; // Camera looking at point + camera.up = (Vector3){ 0.0f, 1.6f, 0.0f }; // Camera up vector (rotation towards target) camera.fovy = 45.0f; // Camera field-of-view Y Vector3 cubePosition = { 0.0f, 1.0f, 0.0f }; Vector3 cubeSize = { 2.0f, 2.0f, 2.0f }; - Vector3 groundCursorPos = { 0 }; Ray ray; // Picking line ray - bool collision = false; - + Model tower = LoadModel("resources/model/lowpoly-tower.obj"); // Load OBJ model + Texture2D texture = LoadTexture("resources/model/lowpoly-tower.png"); // Load model texture + tower.material.texDiffuse = texture; // Set model diffuse texture + Vector3 towerPos = { 0.0f, 0.0f, 0.0f }; // Set model position + BoundingBox towerBBox = CalculateBoundingBox( tower.mesh ); + bool hitMeshBBox; + bool hitTriangle; + + // Test triangle + Vector3 ta = (Vector3){ -25.0, 0.5, 0.0 }; + Vector3 tb = (Vector3){ -4.0, 2.5, 1.0 }; + Vector3 tc = (Vector3){ -8.0, 6.5, 0.0 }; + + Vector3 bary = {0}; + SetCameraMode(camera, CAMERA_FREE); // Set a free camera mode SetTargetFPS(60); // Set our game to run at 60 frames-per-second - //-------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------- // Main game loop while (!WindowShouldClose()) // Detect window close button or ESC key { @@ -47,22 +65,52 @@ int main() //---------------------------------------------------------------------------------- UpdateCamera(&camera); // Update camera - // if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) - // { - // // NOTE: This function is NOT WORKING properly! - // ray = GetMouseRay(GetMousePosition(), camera); - - // // Check collision between ray and box - // collision = CheckCollisionRayBox(ray, - // (BoundingBox){(Vector3){ cubePosition.x - cubeSize.x/2, cubePosition.y - cubeSize.y/2, cubePosition.z - cubeSize.z/2 }, - // (Vector3){ cubePosition.x + cubeSize.x/2, cubePosition.y + cubeSize.y/2, cubePosition.z + cubeSize.z/2 }}); - // } + // Display information about closest hit + RayHitInfo nearestHit; + char *hitObjectName = "None"; + nearestHit.distance = FLT_MAX; + nearestHit.hit = false; + Color cursorColor = WHITE; + + // Get ray and test against ground, triangle, and mesh ray = GetMouseRay(GetMousePosition(), camera); - RayHitInfo hitinfo = RaycastGroundPlane( ray, 0.0 ); + + RayHitInfo groundHitInfo = RaycastGroundPlane( ray, 0.0 ); + if ((groundHitInfo.hit) && (groundHitInfo.distance < nearestHit.distance)) { + nearestHit = groundHitInfo; + cursorColor = GREEN; + hitObjectName = "Ground"; + } + + RayHitInfo triHitInfo = RaycastTriangle( ray, ta, tb, tc ); + if ((triHitInfo.hit) && (triHitInfo.distance < nearestHit.distance)) { + nearestHit = triHitInfo; + cursorColor = PURPLE; + hitObjectName = "Triangle"; + + bary = Barycentric( nearestHit.hitPosition, ta, tb, tc ); + hitTriangle = true; + } else { + hitTriangle = false; + } + + RayHitInfo meshHitInfo; + + // check the bounding box first, before trying the full ray/mesh test + if (CheckCollisionRayBox( ray, towerBBox )) { + hitMeshBBox = true; + meshHitInfo = RaycastMesh( ray, &tower.mesh ); + if ((meshHitInfo.hit) && (meshHitInfo.distance < nearestHit.distance)) { + nearestHit = meshHitInfo; + cursorColor = ORANGE; + hitObjectName = "Mesh"; + } + } else { + hitMeshBBox = false; + } //---------------------------------------------------------------------------------- - // Draw //---------------------------------------------------------------------------------- BeginDrawing(); @@ -71,37 +119,66 @@ int main() Begin3dMode(camera); - if (collision) - { - DrawCube(cubePosition, cubeSize.x, cubeSize.y, cubeSize.z, RED); - DrawCubeWires(cubePosition, cubeSize.x, cubeSize.y, cubeSize.z, MAROON); - - DrawCubeWires(cubePosition, cubeSize.x + 0.2f, cubeSize.y + 0.2f, cubeSize.z + 0.2f, GREEN); - } - else - { - DrawCube(cubePosition, cubeSize.x, cubeSize.y, cubeSize.z, GRAY); - DrawCubeWires(cubePosition, cubeSize.x, cubeSize.y, cubeSize.z, DARKGRAY); - } - - if (hitinfo.hit) { - - groundCursorPos = hitinfo.hitPosition; - groundCursorPos.y += 0.25; // Offset so the cube rests on the ground - printf("Hit: groundpos %3.2f %3.2f %3.2f\n", - groundCursorPos.x, groundCursorPos.y, groundCursorPos.z ); - DrawCubeWires( groundCursorPos, 0.5, 0.5, 0.5, RED ); - } + // Draw the tower + DrawModel( tower, towerPos, 1.0, WHITE ); + // Draw the test triangle + DrawLine3D( ta, tb, PURPLE ); + DrawLine3D( tb, tc, PURPLE ); + DrawLine3D( tc, ta, PURPLE ); + + // Draw the mesh bbox if we hit it + if (hitMeshBBox) { + DrawBoundingBox( towerBBox, LIME ); + } + + // If we hit something, draw the cursor at the hit point + if (nearestHit.hit) { + DrawCube( nearestHit.hitPosition, 0.5, 0.5, 0.5, cursorColor ); + DrawCubeWires( nearestHit.hitPosition, 0.5, 0.5, 0.5, YELLOW ); + + Vector3 normalEnd; + normalEnd.x = nearestHit.hitPosition.x + nearestHit.hitNormal.x; + normalEnd.y = nearestHit.hitPosition.y + nearestHit.hitNormal.y; + normalEnd.z = nearestHit.hitPosition.z + nearestHit.hitNormal.z; + DrawLine3D( nearestHit.hitPosition, normalEnd, YELLOW ); + } + DrawRay(ray, MAROON); DrawGrid(10, 1.0f); End3dMode(); - //DrawText("Try selecting the box with mouse!", 240, 10, 20, DARKGRAY); - - //if(collision) DrawText("BOX SELECTED", (screenWidth - MeasureText("BOX SELECTED", 30)) / 2, screenHeight * 0.1f, 30, GREEN); + // Show some debug text + char line[1024]; + sprintf( line, "Hit Object: %s\n", hitObjectName ); + DrawText( line, 10, 30, 15, BLACK ); + + if (nearestHit.hit) { + int ypos = 45; + sprintf( line, "Distance: %3.2f", nearestHit.distance ); + DrawText( line, 10, ypos, 15, BLACK ); + ypos += 15; + + sprintf( line, "Hit Pos: %3.2f %3.2f %3.2f", + nearestHit.hitPosition.x, nearestHit.hitPosition.y, nearestHit.hitPosition.z ); + DrawText( line, 10, ypos, 15, BLACK ); + ypos += 15; + + sprintf( line, "Hit Norm: %3.2f %3.2f %3.2f", + nearestHit.hitNormal.x, nearestHit.hitNormal.y, nearestHit.hitNormal.z ); + DrawText( line, 10, ypos, 15, BLACK ); + ypos += 15; + + if (hitTriangle) { + sprintf( line, "Barycentric: %3.2f %3.2f %3.2f", + bary.x, bary.y, bary.z ); + DrawText( line, 10, ypos, 15, BLACK ); + } + } + + DrawText( "Use Mouse to Move Camera", 10, 420, 15, LIGHTGRAY ); DrawFPS(10, 10); diff --git a/examples/resources/model/lowpoly-tower.obj b/examples/resources/model/lowpoly-tower.obj new file mode 100644 index 000000000..ea03a9fc4 --- /dev/null +++ b/examples/resources/model/lowpoly-tower.obj @@ -0,0 +1,456 @@ +# Blender v2.78 (sub 0) OBJ File: 'lowpoly-tower.blend' +# www.blender.org +o Grid +v -4.000000 0.000000 4.000000 +v -2.327363 0.000000 4.654725 +v 0.000000 0.000000 4.654725 +v 2.327363 0.000000 4.654725 +v 4.000000 0.000000 4.000000 +v -4.654725 0.955085 2.327363 +v -2.000000 0.815050 2.000000 +v 0.000000 0.476341 2.423448 +v 2.000000 0.476341 2.000000 +v 4.654725 0.000000 2.327363 +v -4.654725 1.649076 0.000000 +v -2.423448 1.092402 0.000000 +v 2.423448 0.198579 0.000000 +v 4.654725 0.000000 0.000000 +v -4.654725 1.649076 -2.327363 +v -2.000000 1.092402 -2.000000 +v 0.000000 0.476341 -2.423448 +v 2.000000 -0.012791 -2.000000 +v 4.654725 0.000000 -2.612731 +v -4.000000 0.955085 -4.000000 +v -2.327363 0.955085 -4.654725 +v 0.000000 0.955085 -4.654725 +v 2.327363 0.000000 -4.654725 +v 4.000000 0.000000 -4.000000 +v 2.423448 0.682825 0.000000 +v 2.000000 0.565423 -2.000000 +v -4.654725 -0.020560 2.327363 +v -4.654725 0.000000 0.000000 +v -4.654725 0.000000 -2.327363 +v -4.000000 0.000000 -4.000000 +v -2.327363 0.000000 -4.654725 +v 0.000000 -0.020560 -4.654725 +v 0.000000 0.709880 -1.230535 +v -0.000000 7.395413 0.000000 +v 0.962071 0.709880 -0.767226 +v -0.533909 0.709880 1.108674 +v -1.199683 0.709880 0.273820 +v -0.962071 0.709880 -0.767226 +v 1.506076 0.859071 1.325337 +v 1.199683 0.709880 0.273820 +v 0.533909 0.709880 1.108674 +v 0.000000 1.875340 -1.177842 +v -0.000000 2.293973 -0.649884 +v -0.000000 4.365648 -0.627970 +v 0.000000 6.167194 -0.942957 +v 0.000000 6.232434 -1.708677 +v 1.335898 6.232434 -1.065343 +v 0.737233 6.167195 -0.587924 +v 0.490966 4.365648 -0.391533 +v 0.508100 2.293973 -0.405196 +v 0.920874 1.875340 -0.734372 +v -0.741367 6.232434 1.539465 +v -0.409133 6.167195 0.849574 +v -0.272466 4.365648 0.565781 +v -0.281974 2.293973 0.585526 +v -0.511047 1.875340 1.061199 +v -1.665837 6.232434 0.380217 +v -0.919314 6.167195 0.209828 +v -0.612225 4.365648 0.139736 +v -0.633590 2.293973 0.144613 +v -1.148311 1.875340 0.262095 +v -1.335898 6.232434 -1.065343 +v -0.737233 6.167195 -0.587924 +v -0.490967 4.365648 -0.391533 +v -0.508100 2.293973 -0.405196 +v -0.920874 1.875340 -0.734372 +v 1.665837 6.232434 0.380216 +v 0.919315 6.167195 0.209828 +v 0.612225 4.365648 0.139736 +v 0.633590 2.293973 0.144613 +v 1.148311 1.875340 0.262095 +v 0.741367 6.232434 1.539465 +v 0.409133 6.167195 0.849575 +v 0.272466 4.365648 0.565781 +v 0.281974 2.293973 0.585526 +v 0.511046 1.875340 1.061199 +v 0.000000 5.012550 -0.969733 +v 0.758168 5.012550 -0.604618 +v -0.420751 5.012550 0.873699 +v -0.945419 5.012550 0.215786 +v -0.758168 5.012550 -0.604618 +v 0.945419 5.012550 0.215786 +v 0.420751 5.012550 0.873699 +vt 0.0523 0.5444 +vt 0.1817 0.4284 +vt 0.1641 0.5859 +vt 0.0177 0.4451 +vt 0.1526 0.3090 +vt 0.0189 0.1737 +vt 0.0188 0.3088 +vt 0.0561 0.0762 +vt 0.1757 0.1924 +vt 0.3024 0.4534 +vt 0.3071 0.5902 +vt 0.3413 0.2459 +vt 0.2906 0.1614 +vt 0.4116 0.1801 +vt 0.2834 0.3774 +vt 0.1526 0.0362 +vt 0.2917 0.1622 +vt 0.4446 0.5865 +vt 0.4443 0.2989 +vt 0.3711 0.3021 +vt 0.4396 0.0275 +vt 0.4094 0.1829 +vt 0.4219 0.4255 +vt 0.5474 0.5381 +vt 0.5811 0.4376 +vt 0.5715 0.1505 +vt 0.5811 0.2997 +vt 0.5272 0.0533 +vt 0.2208 0.2194 +vt 0.3456 0.3610 +vt 0.2878 0.0321 +vt 0.2321 0.3392 +vt 0.4432 0.0177 +vt 0.7347 0.7934 +vt 0.7382 0.7595 +vt 0.8982 0.7768 +vt 0.6169 0.7595 +vt 0.6139 0.7879 +vt 0.4951 0.7634 +vt 0.1551 0.6832 +vt 0.2925 0.6268 +vt 0.2925 0.6832 +vt 0.7795 0.6832 +vt 0.6421 0.6268 +vt 0.7795 0.6255 +vt 0.5046 0.7241 +vt 0.6421 0.7241 +vt 0.3986 0.6268 +vt 0.3986 0.6832 +vt 0.5046 0.6268 +vt 0.0177 0.6268 +vt 0.1551 0.6255 +vt 0.8856 0.6268 +vt 0.1899 0.9579 +vt 0.1194 0.8696 +vt 0.2324 0.8696 +vt 0.1899 0.7813 +vt 0.0943 0.7595 +vt 0.0177 0.8206 +vt 0.0177 0.9186 +vt 0.0943 0.9797 +vt 0.2793 0.2349 +vt 0.2304 0.2758 +vt 0.6597 0.0177 +vt 0.6954 0.0993 +vt 0.6367 0.0768 +vt 0.7558 0.0777 +vt 0.7238 0.0440 +vt 0.8840 0.1330 +vt 0.7385 0.1141 +vt 0.9157 0.0886 +vt 0.9781 0.1232 +vt 0.9224 0.1276 +vt 0.2677 0.8141 +vt 0.3463 0.8037 +vt 0.3086 0.8339 +vt 0.6387 0.3550 +vt 0.7130 0.3801 +vt 0.6596 0.4053 +vt 0.7245 0.3245 +vt 0.6919 0.3383 +vt 0.8655 0.3566 +vt 0.7351 0.3577 +vt 0.9770 0.3365 +vt 0.9078 0.3751 +vt 0.9174 0.3282 +vt 0.2677 0.9018 +vt 0.3086 0.8821 +vt 0.6803 0.2948 +vt 0.6251 0.3035 +vt 0.7194 0.2854 +vt 0.8764 0.2832 +vt 0.9221 0.2861 +vt 0.3363 0.9565 +vt 0.3464 0.9122 +vt 0.6751 0.2482 +vt 0.6178 0.2499 +vt 0.7179 0.2431 +vt 0.9823 0.2484 +vt 0.9247 0.2452 +vt 0.3935 0.9014 +vt 0.6755 0.1996 +vt 0.6164 0.1941 +vt 0.7201 0.1992 +vt 0.8793 0.2446 +vt 0.9823 0.2060 +vt 0.9257 0.2051 +vt 0.4598 0.8580 +vt 0.4144 0.8579 +vt 0.6819 0.1498 +vt 0.6222 0.1361 +vt 0.7266 0.1555 +vt 0.8831 0.1684 +vt 0.9252 0.1659 +vt 0.4218 0.7790 +vt 0.3934 0.8145 +vt 0.3363 0.7595 +vt 0.8815 0.2060 +vt 0.8720 0.3208 +vt 0.8825 0.1012 +vt 0.9735 0.0816 +vt 0.9718 0.3817 +vt 0.9807 0.2918 +vt 0.4218 0.9370 +vt 0.9810 0.1644 +vn 0.1035 0.8806 0.4623 +vn 0.0964 0.9481 0.3030 +vn 0.0000 0.9780 0.2088 +vn 0.0659 0.9835 0.1683 +vn 0.2325 0.9320 0.2779 +vn 0.0553 0.9960 -0.0702 +vn 0.2827 0.9564 0.0728 +vn 0.1873 0.9776 -0.0961 +vn 0.2421 0.9703 0.0000 +vn 0.0921 0.9772 -0.1913 +vn -0.0277 0.9947 -0.0993 +vn 0.2308 0.9274 -0.2944 +vn 0.2771 0.9572 -0.0837 +vn 0.3724 0.9074 0.1947 +vn 0.0777 0.9770 -0.1985 +vn -0.1094 0.9539 0.2794 +vn 0.0364 0.9844 0.1721 +vn 0.1683 0.9835 0.0659 +vn 0.0674 0.9901 0.1230 +vn 0.4338 0.8823 0.1829 +vn 0.2845 0.9565 0.0649 +vn 0.0886 0.9961 0.0000 +vn 0.2000 0.9789 0.0424 +vn 0.1417 0.9830 0.1171 +vn 0.3021 0.9524 0.0412 +vn -0.0193 0.9986 -0.0493 +vn 0.0000 0.9777 0.2098 +vn 0.0005 0.9781 -0.2083 +vn 0.1879 0.9782 -0.0887 +vn 0.2249 0.0000 0.9744 +vn 0.9783 0.0000 -0.2071 +vn 0.9783 0.0000 0.2071 +vn 0.0000 0.0000 -1.0000 +vn -1.0000 0.0000 0.0000 +vn -0.3645 0.0000 -0.9312 +vn -0.9312 0.0000 -0.3645 +vn -0.9312 0.0000 0.3645 +vn 0.2615 0.7979 -0.5431 +vn 0.5877 0.7979 -0.1341 +vn 0.4713 0.7979 0.3758 +vn -0.0000 0.7979 0.6028 +vn -0.4713 0.7979 0.3758 +vn -0.5877 0.7979 -0.1341 +vn -0.2615 0.7979 -0.5431 +vn -0.1285 0.9864 -0.1025 +vn 0.0929 0.8937 0.4389 +vn -0.4335 0.0407 -0.9002 +vn -0.2867 0.7507 -0.5952 +vn -0.4339 0.0095 -0.9009 +vn -0.4338 0.0209 -0.9008 +vn -0.0408 -0.9956 -0.0848 +vn -0.9741 0.0407 -0.2223 +vn -0.6441 0.7507 -0.1470 +vn -0.9749 0.0095 -0.2225 +vn -0.9747 0.0209 -0.2225 +vn -0.0918 -0.9956 -0.0209 +vn -0.7812 0.0407 0.6230 +vn -0.5165 0.7507 0.4119 +vn -0.7818 0.0095 0.6235 +vn -0.7817 0.0209 0.6234 +vn -0.0736 -0.9956 0.0587 +vn -0.0000 0.0407 0.9992 +vn 0.0000 0.7507 0.6607 +vn 0.0000 0.0095 1.0000 +vn -0.0000 0.0209 0.9998 +vn -0.0000 -0.9956 0.0941 +vn 0.7812 0.0407 0.6230 +vn 0.5165 0.7507 0.4119 +vn 0.7818 0.0095 0.6235 +vn 0.7817 0.0209 0.6234 +vn 0.0736 -0.9956 0.0587 +vn 0.9741 0.0407 -0.2223 +vn 0.6441 0.7507 -0.1470 +vn 0.9749 0.0095 -0.2225 +vn 0.9747 0.0209 -0.2225 +vn 0.0918 -0.9956 -0.0209 +vn 0.4335 0.0407 -0.9002 +vn 0.2867 0.7507 -0.5952 +vn 0.4339 0.0095 -0.9009 +vn 0.4338 0.0209 -0.9008 +vn 0.0408 -0.9956 -0.0848 +vn 0.3918 -0.4298 -0.8135 +vn 0.8803 -0.4298 -0.2009 +vn 0.7059 -0.4298 0.5630 +vn -0.0000 -0.4298 0.9029 +vn -0.7059 -0.4298 0.5630 +vn -0.8803 -0.4298 -0.2009 +vn -0.3918 -0.4298 -0.8135 +vn 0.0210 0.9998 -0.0048 +vn 0.0482 0.9981 -0.0385 +vn -0.0166 0.9914 -0.1301 +vn -0.0090 0.9904 -0.1379 +vn 0.2820 0.9576 0.0597 +vn -0.0000 0.9846 0.1749 +vn -0.0921 0.9772 -0.1913 +vn -0.1734 0.9794 0.1036 +s off +f 1/1/1 7/2/1 6/3/1 +f 2/4/2 8/5/2 7/2/2 +f 4/6/3 8/5/3 3/7/3 +f 5/8/4 9/9/4 4/6/4 +f 6/3/5 12/10/5 11/11/5 +f 35/12/6 25/13/6 26/14/6 +f 7/2/7 37/15/7 12/10/7 +f 10/16/8 13/17/8 9/9/8 +f 12/10/9 15/18/9 11/11/9 +f 35/12/10 17/19/10 33/20/10 +f 13/17/11 19/21/11 18/22/11 +f 16/23/12 20/24/12 15/18/12 +f 17/19/13 21/25/13 16/23/13 +f 17/19/14 23/26/14 22/27/14 +f 26/14/15 24/28/15 23/26/15 +f 1/1/16 2/4/16 7/2/16 +f 2/4/3 3/7/3 8/5/3 +f 4/6/17 9/9/17 8/5/17 +f 5/8/18 10/16/18 9/9/18 +f 6/3/19 7/2/19 12/10/19 +f 25/13/20 39/29/20 9/9/20 +f 38/30/21 12/10/21 37/15/21 +f 10/16/22 14/31/22 13/17/22 +f 12/10/23 16/23/23 15/18/23 +f 8/5/24 36/32/24 7/2/24 +f 38/30/25 17/19/25 16/23/25 +f 13/17/22 14/31/22 19/21/22 +f 16/23/26 21/25/26 20/24/26 +f 17/19/27 22/27/27 21/25/27 +f 17/19/28 26/14/28 23/26/28 +f 26/14/29 19/33/29 24/28/29 +f 26/34/30 18/35/30 19/36/30 +f 26/34/31 13/37/31 18/35/31 +f 25/38/32 9/39/32 13/37/32 +f 22/40/33 31/41/33 21/42/33 +f 6/43/34 28/44/34 27/45/34 +f 15/46/34 28/44/34 11/47/34 +f 21/42/35 30/48/35 20/49/35 +f 20/49/36 29/50/36 15/46/36 +f 22/40/33 23/51/33 32/52/33 +f 6/43/37 27/45/37 1/53/37 +f 46/54/38 34/55/38 47/56/38 +f 47/56/39 34/55/39 67/57/39 +f 67/57/40 34/55/40 72/58/40 +f 72/58/41 34/55/41 52/59/41 +f 52/59/42 34/55/42 57/60/42 +f 57/60/43 34/55/43 62/61/43 +f 62/61/44 34/55/44 46/54/44 +f 40/62/45 41/63/45 39/29/45 +f 39/29/46 8/5/46 9/9/46 +f 38/64/47 42/65/47 33/66/47 +f 65/67/48 42/65/48 66/68/48 +f 65/67/49 44/69/49 43/70/49 +f 81/71/50 45/72/50 77/73/50 +f 62/74/51 45/75/51 63/76/51 +f 37/77/52 66/78/52 38/79/52 +f 60/80/53 66/78/53 61/81/53 +f 60/80/54 64/82/54 65/83/54 +f 58/84/55 81/85/55 80/86/55 +f 57/87/56 63/76/56 58/88/56 +f 56/89/57 37/77/57 36/90/57 +f 55/91/58 61/81/58 56/89/58 +f 54/92/59 60/80/59 55/91/59 +f 79/93/60 58/84/60 80/86/60 +f 52/94/61 58/88/61 53/95/61 +f 76/96/62 36/90/62 41/97/62 +f 75/98/63 56/89/63 76/96/63 +f 75/98/64 54/92/64 55/91/64 +f 73/99/65 79/93/65 83/100/65 +f 73/101/66 52/94/66 53/95/66 +f 71/102/67 41/97/67 40/103/67 +f 70/104/68 76/96/68 71/102/68 +f 70/104/69 74/105/69 75/98/69 +f 68/106/70 83/100/70 82/107/70 +f 67/108/71 73/101/71 68/109/71 +f 51/110/72 40/103/72 35/111/72 +f 50/112/73 71/102/73 51/110/73 +f 49/113/74 70/104/74 50/112/74 +f 78/114/75 68/106/75 82/107/75 +f 47/115/76 68/109/76 48/116/76 +f 42/65/77 35/111/77 33/66/77 +f 43/70/78 51/110/78 42/65/78 +f 44/69/79 50/112/79 43/70/79 +f 45/72/80 78/114/80 77/73/80 +f 46/117/81 48/116/81 45/75/81 +f 44/69/82 78/114/82 49/113/82 +f 49/113/83 82/107/83 69/118/83 +f 82/107/84 74/105/84 69/118/84 +f 83/100/85 54/92/85 74/105/85 +f 79/93/86 59/119/86 54/92/86 +f 80/86/87 64/82/87 59/119/87 +f 64/120/88 77/73/88 44/69/88 +f 35/12/89 40/62/89 25/13/89 +f 7/2/90 36/32/90 37/15/90 +f 35/12/91 26/14/91 17/19/91 +f 25/13/92 40/62/92 39/29/92 +f 38/30/93 16/23/93 12/10/93 +f 8/5/94 41/63/94 36/32/94 +f 38/30/95 33/20/95 17/19/95 +f 26/34/31 25/38/31 13/37/31 +f 22/40/33 32/52/33 31/41/33 +f 6/43/34 11/47/34 28/44/34 +f 15/46/34 29/50/34 28/44/34 +f 21/42/35 31/41/35 30/48/35 +f 20/49/36 30/48/36 29/50/36 +f 39/29/96 41/63/96 8/5/96 +f 38/64/47 66/68/47 42/65/47 +f 65/67/48 43/70/48 42/65/48 +f 65/67/49 64/120/49 44/69/49 +f 81/71/50 63/121/50 45/72/50 +f 62/74/51 46/117/51 45/75/51 +f 37/77/52 61/81/52 66/78/52 +f 60/80/53 65/83/53 66/78/53 +f 60/80/54 59/119/54 64/82/54 +f 58/84/55 63/122/55 81/85/55 +f 57/87/56 62/74/56 63/76/56 +f 56/89/57 61/81/57 37/77/57 +f 55/91/58 60/80/58 61/81/58 +f 54/92/59 59/119/59 60/80/59 +f 79/93/60 53/123/60 58/84/60 +f 52/94/61 57/87/61 58/88/61 +f 76/96/62 56/89/62 36/90/62 +f 75/98/63 55/91/63 56/89/63 +f 75/98/64 74/105/64 54/92/64 +f 73/99/65 53/123/65 79/93/65 +f 73/101/66 72/124/66 52/94/66 +f 71/102/67 76/96/67 41/97/67 +f 70/104/68 75/98/68 76/96/68 +f 70/104/69 69/118/69 74/105/69 +f 68/106/70 73/99/70 83/100/70 +f 67/108/71 72/124/71 73/101/71 +f 51/110/72 71/102/72 40/103/72 +f 50/112/73 70/104/73 71/102/73 +f 49/113/74 69/118/74 70/104/74 +f 78/114/75 48/125/75 68/106/75 +f 47/115/76 67/108/76 68/109/76 +f 42/65/77 51/110/77 35/111/77 +f 43/70/78 50/112/78 51/110/78 +f 44/69/79 49/113/79 50/112/79 +f 45/72/80 48/125/80 78/114/80 +f 46/117/81 47/115/81 48/116/81 +f 44/69/82 77/73/82 78/114/82 +f 49/113/83 78/114/83 82/107/83 +f 82/107/84 83/100/84 74/105/84 +f 83/100/85 79/93/85 54/92/85 +f 79/93/86 80/86/86 59/119/86 +f 80/86/87 81/85/87 64/82/87 +f 64/120/88 81/71/88 77/73/88 diff --git a/examples/resources/model/lowpoly-tower.png b/examples/resources/model/lowpoly-tower.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9239e2d5944db7f7a67c52d35cb4b45a97711a GIT binary patch literal 24939 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_R+SkfJR9T^xl_H+M9WMyDr zP)PO&@?~JCQe$9fXklRZ1ycWlfuYoZf#FpG1B2BJ1_tqhIlBUF7#JAXlDyqr7{K7C z^X_^E1_sUokH}&M25un`X1sK_?hgY)!g)^@$B>F!Z~vCp$XvbpqksSVxi@3is)&2O z+uKyAKO@eETeW3^o6~0Bd`WO=X zy^l_NR`-90@#fu=a(pW1J^A^k?)leETdi~7-zk3g@_Ee0Z1NRP?^szYoqYvAX~3*hbC! z#(FXHKh^%WHU4w{@w2tBO7{P75@d+^)ZzO3;8O;RjT{V_mf{QoD{Hl%S}zf5ZO>8M z?e=e(@9ce3I0S+cW*oaN#u%hB<9@xCpo;SS<`ktD9X0$mN*-?e*kwB(aJwE*?O(E~ zR?_FQef0_de=;j;yWju&V_NrT!qWd|Hwp$RExjW1AbZUpWyd!Y9gIFX7B~GjdBOFw z-TI#3E~mQb#f3X2pFF|;-SYbXIeTtRUZ~wPf$^ljT&an;z}$)J=O{<7t(3oEGx@ui zzh2c``}687*EDQoKW&G#?=1~mwzv#n*gU<8n zwZ8KnSh#ep+!Uqc32F!Qd;^w7Y&rC-vvvK)CyTCE7F~S(WN*Iu-WTH1HXr66=G+zZ zgRlJ0>ZUJ|#!^4!RFrosyJQJnS3UhNrD?^j?^>$S|GoC#i{yB4$-dM~QTF(vwQKjD zzJK9F`rwVt2`cG31b$TE0bNn`;aC%T|EW3)p{59NP&b(lB z$o1v5+*`G=zIs}a+~9LR^~Zm@&Wk-6@jH)s z{H!}zzt;2TziavaUO)9dZLe9j?riaktLiSXs~0XHZ=q<+Sd@R;0wQ}lz+vVZ< z46DBF_1ZB%ZtdHt+6)qQ58bEa{MBZ#WuNy}*r7OBd3AWbkGD6szfMhW^txRWT;IE% z0ZW=mTR^0yY?^ecyBNH)J4$9=XN7+=VRtR)|q~{8%^y5Q+TYzTN)jTzV2Kp zth8?lr&aZ}(7Ank|Nr9>U|K4&P4xK2Tp9I)2Wt$USlsA4v?(_r!CGuvZnIa9qm&xs zWf!l`DQrO;EsBB%IyS$5SLDMTJ|!^j>XC&9l$-iayzk=hIm{v89~@$?{p)GT;!+-y z={X9?A1-crch~p#!Ka#P4<-~&|25zCTD-T@&+PRvjyvw(Z23LccmKUeyHAW#-_}3q zI(|OwWv<%YUEde1wEQUKuUqBpf2ZN{36Adzw%R)VIxso&_J2bS*V~P~Ex&u+SMKPq z5nsCH?SB^j-%){6qT&S{#q_rCd}iZY@`=Od{>cP3o_lq_WiMU69K&);<7tN0@1OQXJZ|BQXhl@tHf9BOwyxcTrw z*JGvl|B8AZ4a*+!{%c&D9ey$Qhspoy?GGYb| z=}@@yJ|~A=ui7X3^G!?3xqGX*qQ2L69AaAD`h_u=yV;_U9^+#c7JkofdWU{c?N}ftsptM7b)&$ak307(vc|uf!zxQ2!`ceCrO2@v%hpt~W+*o{Et<-bgW96ztoW`J75%!zmm>F8k#mJ!PaxpG) zQq!B14@Vq6RlbjK+rPQ6A^e>|FKp#0vqFH2QTKs2EkJtUaa8|TmPe0Jt-)5!?f;w#VLl&^BHVB z+;$x2-L@|Fsq+2ZYQ;Y)7>@P4I=-y`%aL+<)5eK^jsL!vE3uJVyH?@O_lke7zr0=Z zH^=sL*wK?duNQVYY+XD@<5cEl7REUyr&Jlr!{P+Fv{lPEl^R(`8 z{q_HINS^oUm*-!1@A`Oecht@7yW5+t9l8A?JcP^Unnlu{us2iHnj-ETGGLnFlz3Zg zo44%$nsix(6B0Fir!t*S_!zO*AD=49wzfLnR_RH5Q)bEgiwApk@5~gx*RIdmzROip z;8Okee4)7NzrFe@Oq?rM--~{`AiO3s)HqW3_mBS$%C%puTXU=zglFvd|6xkIpQWiI z%UyS0h6O7YsXm>NI74R5I>raTw9onbOq$*JwDh?4wcAfW?v3G`t#@4`-eA*TEzZ0Uh4Th@9xW+o86~Rp87$t#?oh{$nntSoBuC9 z^)P$Ayj|IarHL_7SADiWynJHT4R+TlADNy0?cDWeujc&C>U`h!gW_>fmspop)C;?; zrv5(@{~Bj%-FYQn{ZyTe!_hg}HmK=~o1)O>sdgVM4*fmeEHI-j?Q5F}hlHBjffpue z?wXf=`+wZO{at*ofFetfb<4Sl5!+f*k8jkGzFipfp?}JgFYkOUWbEFwaM~o<7%2s4 z)(5};IA2jD-t73Nj)Q@Fe!a?Wx6y6%OPjy_mTk)Vy5$C~9Fr3NGzcu8Yu?rPd>&gy zO8=YrFX#JD`|H~p{r9o-wfbjiQd~R}1GX5k&GVZdc%MzM3p}ou9?yt#22KZGwE>S^fQ+Xr+@4YoUYLP zr24DGKlaOyWLh8Vt#B4>yrREM(EiEp=Zia*ttsNSiJZUZk-v5R6n#F1M{8%D&Hwcx zSpKH0=%MhsMH6F#qi37=wf8tapKSO-$LMnPx;-o}@#?!L7uJb? zC=B4+vpe*3$G5H;fkY#=xi6L1tUeOFRAk$xt?B0jTP_`Hxx}~sldZtN`jtEWIw=Ko zJvpJ>AhhoA%X%j^6~+6_>pC2~G8GQ1J^VMbJ@x%fVRqv>hmMcTPAUE}KHrWU)Z|)F z{(jw|Q&Z$W9C0{)zO=M-ANz$e5drqZfY2Z1Z<%lWH@xclUL`I1z$;rb?Q+c}3(p=j z56gKH|9&mIqKA9|6G!KSnF>5cY;#|4zPkFn;TGmahwYb&9NeN{yP0qP_6Lv7`{(&C zYtTQuOL(GJc=7jtfAtmaH}|A*H=d$v$rH1(AXw>Attf3?oo_U zp^E(K)Azl1{4ZdVoM|WiLE!z`x`xKT>;bW{y^j}&Zm;G1zLD?qugz}18XgC3<5S%p zp7V6R=mfh1!qHvZm|uIyS7>oKzTWr!q{N!XjBzm!0(Q1AN-ijWzt7!feN?dcl1T?9 zHW%G@Ur->^y7<74Wx2D%p9fnlkKYwDo!NG|75rxe@`**byl=wb6Nw3QjZ+R;^;|hQzqDbP`Q3Y zm?Lq5;Rhavi@WkIon~+(7F@NAUB1@a`O@le{U_618!jn%aQsP{shrBx_gJAdGq6%#(xXZQs6UO8LFY_G{O!O?iA?zxePuua{HTJkk*ToF3`^Poeg~Ne)530zSnP z5*2J~e}}(X&tb$?AKl)(JAvVD^%n(eZEM5weFpw9ITFkK^KTsr+4KFMR*OKyG*=a& zgPVkQd4}^zF11NDPJGA8#CRxCHpR&I@>VUC`?l3WmnQ7l=A=JExNrWl_r>!|Bit`c zcc?S!U}irxRna=SIo&qwUMGW%g-l_I&9%6|Nd|wd4rKrTrGE72(Yfkp1JBgIj5T7b z<(|T8p}=JI+THQkfy)ldUKSr(QaWF4R!?JkgGaGx>5gA_RO0J|1Y3*`eR+F#{pI&o z8TL&w<y(LmygoTH|seSl)avu_+vc_k&STr-3B^NXhMmWdtB69hKO%IMzS@yz{lp-jYw>!mlgxo%E< zY~i)LJ$8}B=O2MHmZkhWzI&sNYjXB%zsbP@{m~L_Dkphg3SYgob-pmi3=6+3gO@il zgMSz)tkU{ktN60yDDN(%j=tmnjGRC2t#_&TdHdFtVj1;T4!er~v7bI$zh2dkN976g-``)AMc(AR+q*j{^;G%s z!U;i(raYSE?U(fYxx8@a%_~>72j9CUD^@Z(g6Utl9ZpG4J7$ z&V$A>BFANyi#}X`m~o#GV@b>Yc{d|xZnx;Ow?UVG|z2CCy;$ruU*KT2BOiz~VbF5k`09+Pd~yY}v#t7~^U`0t+e>6(BO$NbRh)qxx*U&s}%y0z%WsfGJ= zdmi7ov90;>jUMTHy%{E6=ikjd^45a!@`HrzEW4#*rcIv0$AvlAI9i?T^gU!hU%$ZE z*TOKV^MG?*ILG@g2ASj6x_sD`R@g;2`kr;<;_lMWc@@R)aiRIa&(zZ=B}(*+&-*Bx zSuN^w^3Pj^i){Q-M=mUM-u>nM_O08JZkv7O?KLso_QT3SM@(66`vfN?-h-?7uP-Uk zTPpH#hpUOy!zIGY7t6FRmbt$1$%b#6egBop9juemO%cig}u^n9CxnJ=3 z@;%+(ie44%ayXzO!KK)0QDXAJTj=0X@%WM_EFVH0G+WVmJO zU#Co+x>TUaogpr3+LV${uWAJuWooCyeRXHbUAxxA)LgLtwu_QrR@SQ;rVH;UNlbql zaU{u5N4qcgrbU?$qejiU)SG4>BYJj-s*7#!a9QMXJNX9JrAwDQ{P~4>SUA|4e-#@P zR4{R{2)d=u)pgx&<)rlC4}*J%+`m$X>{XNg{_g7Bvc1%D$C@`fn>KBhuqx3o-MZDp z#3U#x=#}#S(@xuaeAN78{KLw=3ErPPZ=z$Bo6_^TFE8}}uS;FJbc_3;pe*Jo`s?u6AV({%yJU9mCGgbLw>Mv3I)SB$k*P62eGw?0uU$)f^wHw~^au6KFWZ}x z72Em(A_81$orNcQSZ6*o_*lWC*rTk@@S$^o_7i7umJ3f^JzW%8{wr($t>@au@$!27 zn%2Lc*Nc@{mEV6;`0u%8_jdVjlCgK|HGXIvnZUDjY3de}?HA-acY1w$7+A4m%LZko zhX39lQp0|J74Kgb?*G<(MP|oRg^SL0zRqhd*WN2_-~Hd?;cFpghsa3Fiy2qGC#oD? zxqM#Tjq3e@svlM^pRX*|{qx+^CkOA8z4q^aA=&QyV4>RU)oVUIx%yUd=VPyTTt8(W zcCY)X^zK68oA_3RHa^*+6UA4rPW@un@>+VIBHxD}^XI!Tv}G~~ExqH!_w@LUT{>@m z+-KnbDW|gjUSGhJ`5t|;mW8(z=g;i9;H-G#F`ryx$)07(?Jq0-&3?dhvOq)WN0iKi zkM{O2>$l0a%Kpx(W$93oyo;kM`doA2HIJ#*#rO6B=} z#vifVcVf+-O$8c$AEsJ3gzK1BPMSP%N{@vCYvsxBGI>h;U*9|_j(7c67rMym&gQ-p znf6Cs?M?ZU1gwM(PFHGBp1Sqq2|J-Ds{V%_Mtpnt?){HHhySi)Pw(}Zvb@~pzUXxe zk=6f=uFG=Wlr5dKZr$%?9EvkJ_64xWaJ_WjRmIOP``%h)^|!wMITpPWh4)t+EvxC9 z_Iz&V;>Yi+SqzIFnLOgP=J+D>`v2E=uQmSf-u12^*)rQD_|y&CBjHo>{C4iwzI*8K z(d()$DTN7V=9S*8FE70~U#_}oYxeb@%`?r^+Ie>i?Rz_KW6-AGd>`zmUul0Ksl9Zc zSNgOn{g<+<*RS$$Jg~*k(nosocT9=dG=FjIjSzPz`=;FWUv#+mvd_=hKf0WCGYuB9I+|ttd zCak$3%QtE3lRoV;&Fhut^KU&hS#;aFea-GQ(z`6!HXM}y{m`N_OwKfj+4gu@kjQ=g zYtL<;lrQxtGd=gj^@Ho3#gA>&id*xWq(j2P)k+#aa-EM45=KuP&^vL6tQnl{W zrkv5^H=WrYwCSAG#Vb#1?|odi?!DB>cwRpB-~6R_7p}9NlwNsV=H(1)y*bK}9`REJ z_*SyWb-J%z&H0J#)AHlA|HS)}9=?D1 zJ-=Tc`d_^6!GC)h%P;TlzMp-b-GJxr+eaR+WhWh;8h)o-ok9Oxb&q3n`^!!F#~*gF z=c(V5v;EU-b>;QnkI(lPyngo~`Q86xe>ML-KCqc-pAqAXwoVhCceM{Wm({=5G0Thb z&ppU^?oVT$^2YTxsVe(xzE!?DwaVh*o68;U z$q^SNHh$yyT`uzZcl7%!n(6=Ty#(S<@qgIdF0Z0%ENd*HFk{}a$@_P<-to2m^ZD}r z@~~51wg3GStK`3X?9y4+xz)2v8X|-H70Z*a&vunK$WWHdZP9n%VZpjJtAcgzD4Ozr zITh?y_;>I5m-9oF%E`Bp7GH*3;{ z@M80~7q*r-O)`$Mj?3caYiL*=zV7id$;&71?&yvT?oa&K_V&!IDxSUW-`pAf?mT<- zSZzbW-?D&!;3qkK6JbTgzROU2S=Mw`{LWxM^%tag#SCt;}81H9PzBf8&;d6&VX|%=*7m z$SG--+TC4WFL2FixWE24`{v~`hu?BfW0=Lkptt|IV38aDf^82NEElr}#1f)V!FI-n~Yeks3Ygg}-F3+s7=8N8zvuxLjrlrMoe)o>q zo|#p~>XF6HL*frc- z?5xJIw-`{B*G*I2P_Rl@Ls@GC8 zYA%(8e^VCIxP?Z2}X+51!+T{30Y zyz>)kKAqE=@>s#=o z?D<|!=kM3|AFV5QF1acaVFe*wO@f{#zBTOg9pk#x3PLV9++>- zRM7uC?^fd>(~n|$U*`+wXD;8iqbp@*&XUZj$v5xpW;~dypK&fQIE1m!&FX;YcRR23 z7yove?AXiP;vIHs;l4i#8hbw-yc!#G=;qey<@-0UEbv}2i~q`wT=r6X#g=RKLPx*l zS+<=%8^vd~C}*}|aB2ADo!9^9u>QOkzf@#n#1s2z0(~q$HCYO7PX7LbUEOcaoApPe z(`DS>J&T>i5yRp?*XXML0~OArTsOAVF-tU^`V+(;c4jZ@(wukm{`3DU_{+aD|K#h= zYSwqB+L!X*Klq4u_e|Tj8G8G__ctir6LaOT__@dUZ=Q^GdH(WsUl!Qa9lYsLV!BR{ zV?*uluyUDBUsb8^uWUPIPP^UQrG9aNWAn|eiP=uFZzhCVr=6-;q8Qkh^lt0+vd)_Q z@*9JWBwbY2zVU)Z>yy_I!E$Ju;)50}%vF;yt&#t?tdZ_l?ge?oVrAGcYQ~I&# z#CsQp27eZfqgTJ%%}jXDC-HDWee!Xmn)kmiS(b0C_^fwJD(c6F`1egMt;Kl<3v;He z(Y{|U?Md;Hy>Q&PtNru0^ymX##H&@v9 zU+@I}b;>8A|5j#iO`E$t&D_}Ert6L1-o(km9ys!#dQt5^R$x1}#Nuy(_#`wyaSnr-~1;dbD_#*&(Y`gv~~KJ$Y6L%puc-+CZ87E1o#ONUmg=WYcZGa< zbv`hhIc2nE;hvnDn`c>zP{W`KS^>dnvLX%pWM#q$0r`=);m#Wz)T)J`VWva@B4f~I6 zXR%55U88$w+u7MGwXIM5csaRq%a)5VvHnm0G1rOa`IYgV3cPaVTE@4xU5nqW$$9nq z_1|kHhhE=&Z{B+4v|awa_rlDcSqyJ$Z7Mc&h5!4-UvDlPdp}>mfai>Zz+r(qg~#XK zw_vQ;ka?MH$%Sj%QzIh^yFE;r-dgUAxj5B_CFo^^rUhSRcG>Iw+1w6Zo7%sAo>+M7 z`*~*ZpA7!%qs}LX$G52*{Fm-4xtf>x`0KWn>MQmw>3aRHdRnBq{M-K<1PufoC;ytH zwW{ph(p?W2wjPaAyDk>`;fAUGvxl$s65f3kH~RjIJ5$5jCe6kTilwp90xwoYn&q2$CiF{R$MF0*e1^ex)9 z&24&ywdY(Po$^aLxB0zae|~4bbZ66Jp-Dkqn=V}mvvTq%U$6A?oAorCf6wjfA9uv> zualKFUt|C1^>lszH(E-kPH|mTULE&||BTH?jZb#kEXO}uHZYyr@gn-4>AW-LQ zm=7dh|1+cYb=k#;(C)+;8po5q%}n0${O5HC$G>rJxh=l_*?i*uCeQ6TnR&;z<-9t@ zwB%vyW)ueK*@d`)lOh;E;Cr`dOY5ur; z_j`dx#rwVAH8r%pST>lbv?U60%wjpqaLB-5iGmE@W#iTA4^7r z!<@xQYTx!NpAT5ca+L_*4ca$@xuW3R&gaQ-|DNpq*SITb`}y@#p5&iauXeG2)unCu zcv1dRg}btx8Wle_aqq}~Is1x8c)?BE{hyuwTX1P@pEk*K&-~k!p7+yko9*BIEot4p z=bAi+AME=5toT%AdeZS6t?g5VINAd&^5)xZ$nAD8IPqv|wM6-u3cI@J%zq0sf4n#= zy35pCrSGxDjf)niHhp2aSykTuW=&50RQK7p8D7|@OlZ&Fw7F=->eZ$8>A#OG6uELe z^z~d1j~!fmOCQRwvGZ^YOkDmrDC6rD=Y5G1yRI7svG4rXoA&tA)BJKi_VrbN_xkls zthp}seMQ2bADzDzi~LFYXJ0Vo5hzb8H}9NXwxRGb+ale!SH#t4xu389;ihBlc5fC_ zRj*8g?Dj3`=W}<@GTqXDU)n=+>35ExglMt0%uTt`c9G7%e=J*3Fh$;VjzKNgTVLHN zA|i>d$vhG^9U0ZT%l=vLMMmDdqUpM3XYBtr5srSn$%PLx+nj#LIjrSW`Kt4Z>D#-z z*MBY$kowr>;kZxW-nEkW-00i+LTBt_mVdvnGyC8Dn#%Xh)`9gq-nH*LRC2wzKKb^x z8uOgh>QDR4|8-3JX7z8|;z^OgeQuVZ3i{gG$jLK8Si0@*v`s@cbUrR)v6%EjN3o{c59FM2vBbsVvC z|CGJ1?!3{jD3%G|S5Dh4r&w|1f$tUhb^iq`v&+nCMl!@{v!(7X zI(MF{$l~W6NjcYz#ozt1xW8>K{F*LfoXQkt(zi+G(ysQ-<4Nn5_1`{zUh#&m&zhaG zjoYpZf1ckFUJ(!;|9rmXpK1C5p`kMm{ac#t=f&7ILBdIGLcWB=x;5`D=EjTtY*%hl zzH4&(%dI}$$nX=q%d@8EAIMFy`QF7{JO4n^^nhdi^IzQGb2j(W3Q_)v28!mQ4{n%6 zZ_jmQ@!iz+{pmaLe+x_7R_i_3_dNdd^Upuq*QGu@b@$DkopaYH)~?XE_Q)-4-EY`z zz%ym;9?wnUZP#W91~rN7Qn|aSFlY9+Utv`#ujV<%mY!d+dbR4UJANyoXJmB$-oCQ8 zYuUCdeP{A+%!uQBdU2C=xWoH9S6%n*TYh5a>if?73Z_~u`NQVoaJ&b(`Mb4TG``CDJb+}~eh26Z&v?_IP#p>UFtLz&NXtLHqgS1UIJEzi%? z?>_u~-G9b%`~T&86N8PMrpT>*{7ouLWBs!!+~?KgdVdy4JlHI{`tR$#8VjxovfTds zdF8#$^A{iQU%GT%CT>!ZulFM< z|DV-g-nCrf2?!28YajPF{FI@y#Edis;|w9bc22hDC9KUH&iUNpTuU$Rjb=+Tn9;OP zKbiH$w6|sMtQWplhB3S-`WvC+R$W~^J*=TfCTHn{f6J`(PIs7W4|tvSbC1P$@jVvr z#lMtY-fNb3%j4XNe@}Yk9NsTxyHFicU)BGvUyvt>rP-$Tb?Kj{{@*WspRtgoiBYLn z_ss5hNn*8IW>;RXxjsMk_@)VM2R7==iHH$yS{r`Tef{3!Pu2N93hGTPk-xy5s@-S* z+(h?z?$hNp*QdX%vCsc+mukJ`s(H5VzI}bac3Hi8^}1%-%&W@U8&AmoogHbr`Q)in zirU)kZ{ElxoqZb>9}p0@%Rm0Tf848W%Dc;C*{@%POYm-$#e{J0waW8s72>-OEb z-|=x9t9Qwyb~I9&UbI--IR;nx%tWYu%N1jMfWE(Ub%Yxr`O}gPk(s| zEFFzx)~rkXzV0r^l5=5R&zRhpF1GD|J@Z_f5l`^;`pH4Z-S#O}xV-gf4B%_K?H1(0 zx$Rw?Uh4tBo$8H$cH}18Rozu(IJ570VSN$HhsoCR&-DB6`M+BxoA)U_>6KXijm8Nd zeq5bid~HnY z<%Hwg)_#$=(S2;MX-wTIfjZN8ckbEWuKyI)xf6R}@5LJ%B`&s^t;ygyzKL@Q>twZ1 zejRJ=tN-P)^V}JyKK=dDydvS$=Sg1=CTurg>e%YGV)bROhoRs9O>$l5y<^D|mJ5^L zXn1-*jBZd;P_X$av?Tqr^dqE@V`R^<5?9W~l% zipBPqPsP65bNhtkz4>ve5tvBI%TsXgfhiy}-MY!hN5XtPURFD4$XZ_v%u}nKJ(RFcD_V%1z-_JkV9~aSA zB*Odjs>|$XN$WzJ$$j?IUZ&4G@7EA4MT?WlCwgcOx(l@pJAtzXzyDQvgf z-QpR%EP>5u&&Dfa;Y|~2w*7J5)c*bR;-cwuO{b*3e|c5dgL~1G!}I3u&b$*pyTZ^t z(KTtyRkKU0%k#gjx#^@Jqok|s<5tG6rNy6cT<6hR!CmPkhYg)~{W!cWsApH#r`j*l zMh2P3^?W5B9Ia!S`t-N(m=N+5y8ET+p8c=SUtM@1-9E}cDy_<>c4OvexB634n75{~-;_G@ z{pNbv%`cx?O_N%(?d8&i&inV*ezR@kmFCM!Ub$!yQ&jfsLY?-L%VpYw_*%0+XZE_i zoOg3>g^z!Tjo9+ND_f7fvYIt(mOz`!O}>u~v&=GMzL%7RiGAh$`cF$d!#L2xvo=;% z@w)s3AB|IYuNYp@T@$|VxMhpWEY5$gI=fe`S|!3BbUQCsR+7nyiAis|!JYlO@BcSW zXjmhEUUkZ{4EAQ4WD7n1O<<`zA9LF!twAXuIs?*-{ zg=g06*@8^3va?^_G(KIf*}NdVe_qJj%XyEivTB_KIF$6*U)ZNCufjNU9?t&_ji_Q;Eld>4=e-Len0ck zX|vvQ0k3?CmEXCGgbn^q(Q$NkZg&6iZ__u62DdrWS}#5`o4%L7z_R&}!Pjm2nRhQc zzI+?svM@LQ@aev90&kVHg*uZYocuqXtKV_^MCOv?qAaX8yo6aY`Yzb7h%C&$r@P|4 z{iEAYV;9CNZ_hsTfjNlJWZ$Q5mOPe}KF)--S+|>+in({X6^48o37JeV=vn<`~LOIsAzCR_5>JH|^6NKEJ)Q=#++! z@d@tvd0)GA#cV2+cg`0x%K5?ZC|9tpU&8U?JfFx6DlU`CW0X$J`WwQHo*gdUGL}LoWFnX_W9qIiJe`QJ9k#;@3ZM2zRhaZIDW9}sBrZsmHs2$(|0#t zyJ#cebw1+zJKG6C?l+~Rq#Yd2%h{G*>g;}VaB+Kc(3NXfg);LlKNrwgxjo0(p(;%6 zZSPtcjoZg3EOtw1V`6D?Y48TGK8oPE2ZLYI=H9O#?`KG6W`5oJ_qLc`^2N4T zh0^ZI4>}AxKDX{S`h6sD&1#;-IqcSX#w8yD-c-yvV`n?#|0lNiB^8l}SN_pE_-)BU zHQO0%ZJEEWHKhr4s&FyDkmg6a#cIi{zww|HfURcZP-Jl%%KorzN)t1wDzj|HC)kPv+Q}>VHo*|6l#Snk`pTqoqi~B!gqA>(M1& z`WfEnZ>m%RZlJ%+_5{tsrRJz`=*Yqwd*>UY~0DIoRF}rRl=Y^ z!R`>{-knQkTNESImEGX>c%zJ7$^Tc5?W>e?e0H|@W9 zt)9`y|MK6~BP;j++4w1JpZQLv6O#ADCtR5pvV_g?@2A`YfjV=Fxxeex9@hW1^48Me z4dV7S+&i+iyxr`xMNRoq@oe|yz-_N~9{o!fS?Jcvu4v21t! z1P=q&?LV8pU*9JCJ*qjNYQ{4U?X)Ndpj{@$qtPPDa4{+FSoaeqq`_F8q8MXa>VP_=o z-FGkOy?sXU`^4zKwgrww-?{4-FMGkj{rt|MN$0tCzPXY3=hf2k6?(V&yR3EQRZDl< z-+j8d_;%N3(|Yc{pet`~wTt#LHZd~)3JwgM7;vs(1INXX*yq9xi4rFoW=`n#d>Ex) z9Im!LfYJ8%ceV6@c)lwkyLK^NnHv6hv&hp6YvaXyMLh+bmd>cSw9!M;(Aja$4p##i zB?-R3h?s`Q8pnDhPyc-$f71H>o`ZdJcQa<(pYeKA&Wp5HQza(-n$MuOPna*S#Zdf< zs$BRDK9T(Vr;nGem0${+&bozPY2}M!C4wafIl32w>>^A1;>g&fz2|p5KYvRpV)NZ=5Tn-fztn9ZMvWR znc|vV_Upuk$6^W+=7~%{rMEW(#eDqRx#xeTsh8vax>+1sGTGO?Z6SzP^V|m_@|I6dRZE?yTar5WW=4DLQGE@DZ6@ zAudKn=CVD3tw92vj{l-8zMkUYVN7Q^(B*8QbLWl5-@7~qn!mp?_Y*ESb#|st=G9~? zNrN7R*=E)(JRR?QE-X7B(qs{^-O|sMyXVzH%ZWl&Iv4f+oO|@_Neu&g+We9&W{el- z9khP_f9n1Czs^P1-?>%PoDR#myQ}qn{C8)uwHM=DG*wo|#rE+?=+wLrE3(%v=yq5A zxI%BhuN%8Ejngzv-1sQG;j!V~tE-yx_0~miWt-AGq3ykRMcz4Ho!H<* z>+atEyRdKmg2{%axcTinYXsLNRT*1_zL$JD3MSs{J3f@Hxc6jMf?3WV58+Pr z`L<0rL*!B(CF~8~q-Z+b>fbG%NyUj~`dJrUloa;IglG2tnD%9={rO)3T)*atsqI=e zcgJt*wd?-mZ>R~GTzTwm6z81MCbwPBx#pD~x~Snkad#en^!8`L!664F#plnS!me>+ zSE;e7>(ny^mygQcPP_X0{jW7|zPzvDK5TMxlk4uDsZSG*>Sld^bG$2>L;RwgnW^}N zcs4$%4oQv;_3nn-a+j|8(|06c);#0j1Qw>0ukT&A-;1<6{=z(qBm3UZhBpuY{`~8E zKmDPW@14G-n|pJ$`*N*C#NHlsoPPB8H_2OfR%@>FIk|3U$)ik;gp3b`yGqSp-1GhL zV|^5d`pi(Zg6Hy&*VJn@E2?h`+NIX)czo-o$m!NXC9W;$$KT(PJ|a+XWr2m`^{E@P z^BW4CllDbhKKm&l^SAQ+K83^QChjXYO5kW=Y-=gtNSeP@xO~e`3HR;>D{tZW7{_BL z&8s&y+~rW*`ty0+_s={z?!Ot|xVX;wa3$P4*K2|4*EtXWnociBc)d;c*8hB|y4Za@ z5>I-h4ji1jt6|ZiO-=v)va4)(=^LluprGsV{vq=UojbQRy>IT1seG5zC}{BVqWbJJ zbLQ>39N-Dwtk+L-UyRRxin$M`%Asr)2C|p{+L_c+#|oJTtSp6DO&l~p@3Pl z8B)%1{JwMI8V~Dn+xrcsu(l(RJ^Fx=jF^J+@rg9bm9l`GbGoZKU0l45Xh z!@Fg&@8)?bh^UCYT;#cAaY^0W)4!Z06EE#rp3LI&*3L|Ivfr$>#~Qo7zwUf{AoRHG z(M6@z3@1a6%O8F8mGwbJxBITNGxFXqq$e@83Dqjdsuf{#mqh`vXJk+kGFqTmQS^eHdBY;Z`+pAWZ@%;=Oe%```vC*qDt>8)qw_XR z@sgO!rvA4f(873b&zg5FyUQYeYxmb~|@t2g}F zYNj;1bXjqXLqgPy3tJ2hIyjv#i1AC{$k@2$$HkX3gg#$zW?Wp`=S zEZdWD?);3}`FV9~);8UZsr;ky$?opqGs68zkA8SYF-~FYOD=v|sC@GGmI;;DYo@<` zSAS0TG*jW3h_mx}nHvvm)BW}3-D9QDgk`KdfAlc!`RTd!eB0&f6rocd%9rj;yR_8V z%QLuGIP_2X5iayH~aYf{2h~ArOPh{ME2fYFJ9=bd}>P6Gp#S_ z(=C6tJany|dZ_=bHtX*A^_P=NtNkp!iuvBlX(k-EFA*u;{rh@;#-i8x8aL$y`rLEqbIZuBv7d^lCDtOj%o`3&d zy`TI&lF_(d-uGU5Yu(>H-`iHlRr4R;HsR8PE!^j?&9#`4bo*M(tJ8c;d}VhQu3aXW zdNbmp#o>O*rISrt{P;ZI-@H4Q3M;p_gFW2n4yt`uS*p=P;7OhwzAme(igVAS}jikVn{JU>1GSu(+7gQj_V|*g_ zysq;Mw?*}eyAEDmT7LHF^7PvI-}vkMm`nVNBUi7kaeVatW8TWv>2ucP{CMiCX8&ti z+OuUP;q^f)A{p8^S)~v4%v*N(>69X2U*YLNyDaA>PQHC3{ngdH+q0UFn7s=4{=&Pe z;KN+EMQ3Lv+{#&NA98%F>H~(m{yQT-e~euB?Y-l>J<%us=9ury{=Q?)t@Jd7*8A=U zq_@6J+jeHh3z;IfFY475@wF=_1}v}Ud+FNGxc5(xc8kJhe}BHP(7@6SlUA+Lx_&%= zN7{r>H^Zacr0)Jsxp?K0PjImE-gf>c``FK=zu7o>qNA{}jMI+O@AudJR^zw-sd!ZN zsNU43?&%^^R8-z8{3ueZHA~w|=(&j4b!RQ@Q7LnG-%OIXUO=-_Iut zyCd}WzyHpm$uY~($NBdEIF<9!Kif_VF8i;ran@Pc+M4oKmZo=mU)%X#ezxLwga7qq z3mbN{TC=|s+0CqTYsuX+M`xRS;x=y!%+QZ73AVo2`*z0UtsEJ*uXrddslR+pXIe+} z*4*EZeOGbMDE)I$A?Eq{7+;3XefG2NoG)7}lly&f!l4$=cPl1wsWqORZ5|jM^fYTr zRhUe4dztKMCY9r7Ed&K*ZUlQ(W@?@ayHUuUdXwX@!UVxbjq~Rn_CD>s@lm09&g$aZ z-`?pj-@C7M%Nw6}yFdEGRo2g6x393-dEzxQFK=%Lj#=8T{+z6T^052o>D#|I=KlSb zTJ0U!cX-3X!;)5&JZ-mAqP#i{Sfb5xoo292R`)H-jQf?u`r!9o^-bB&^%l+lBhuoQ zcA;U5!jxP-2D`tXmaC;G z*Lg8Jr9SQM>du6Z>3ZQGQq=gOx95Hh-?}*A(h<*`llPr!Jw=1K#Ln$xZ9Q!I-Ol-2 zfUMg(-IYiBB5S@KRF|oLcG4r$Amv4X%;|&Ma=*R5-JX1t<+Agi#FOegudGkR+X-pt z>R8UT*SP!HpPoIX z)juA{pRVP6z}m2G-+N7m4V!+%PT$TccJ{9(xBV?fKAnmmZenZhwF+#wTK4A||AO4I zzsJlDP2iCaO&Rv8co%YGhBmRWtmQ_zr(?Ex)g($c0~dwsH32rm!D&zf2A^ z#F_5+r(NZDdQoMvt?lykz}@TQf}Or+MsCSj<1RJ9GPJ%d#zbL#Xl zuiFX;@MN%GX!<7n^Z0%LrCaOFpU2ou7G|>lG})=a#Y@8BUHa~s2AN9HvG)(Wsh;;i zaK82TuwH@VYVS8QiJkkaxqrrgbD5I(#@6EcQ*-kIUn+;!Pv0H?L;dlw-s648OShc8 zs2OYeTZ_>(+F7_S*Z9MXi4u%+=N5mNk$hvHro3@N!=<^FK9?_Cn>D2&;an5{&i}uR zXY4N6djHEo@2mU1Kf7Gz4+{@BePrJz5n$W1_9m#={Z~ zf?Ns5A4wc+xUauaS%dX=*|LeXIY-PH?>-bTiS@0HmcO#3`Gjum2K5)q#O@vWK4+Ti z-(OYt*#o{;?md4gdi#~fx#<_aRMs~Aoy~cE;bIeaLto#Nv#)P+oN&2w-_rHD>H?9i zOXVkAOIDEIyKT zYEa4h=~IImm0Lbo9(Xc&x%#51+nhZ!H9Zba{cd}#yZ=sW?$L-z@uxyBAK&y~bM6_t zZyEK^1s|G~fB2HTzI1{4M#IylPh;FCGHlq&el^g_>-)3$w=AEUWZVkqpR(cMWRqoc z*VO*9U6y)z!#cj>UIkO$2yb2)zx?Q|w|2(w8eT5@S25G+US+<|_4IX5r)2Xi+Sq&K zaX_$s%{#@`D=ZTVJ{`E^{_aTGqkRUuKJVPWNapC9$;UTToYiA7`26GIlXbcYx6i%( zS8MDqarRMvxl!gPk(P$3|F4{XW4VN%_bGel_AL|dEmyyE_T}zhdkiJoPWr!(KQo#C zJllit&-cFtZ&<21v-Dp^+%JVA{eJQ%PHTTx;_*yOZ1flKdRYF2#pCrf$0bXrp5A9W zqkW#vvZb5)^8S`u&Jbu?^hjwJ|A}u~vu~H(5z;%kVxqEy%}b4KPoEdxE)n`N+@N@O!JVtGbXhGk1GWX$>)#Y`?E1noQB&i#1mmT<<%O^FroOJ8 z!L{(@ONlh6_8(79MqPWy`{8b8;i>q`@1BI2Yaa2s7M>Sr7XI{k{<3W=nvSmfX8(D| z;uF!^+ar(5pIuXZcbQvWRmA#->)J1D{~*Hs{NI#4TUjlp9X%uLK6$sB`P7)7-?wMp zs-AGB?xsxnxs?)^65g`QD^9FSU43lsHQsr31!DVa{)D>qTXr?wtb0@*?DK}(s&{gu zAEydwF5RFrTk!I+S2q~)#Ed5VzL0yz@PF#GJ6{Z1(kkNzV_HU1!_BShjx%Td#Z|s*mPL%b}W?|ywR#7l5 zH{|*IZ`-s}sfYdtIT{NL0A8?ToqAO4e8HjPEn+-BMZMh5kD zdna!6eApsBX?iIi4pIQ1R@XWsFh4$JU4|?9u|h#t(^ig>UFTV@Nr0r0>|q#f4el!xD}@`XX&Aa=YvG z5uWfeufq>s-anUi@>SB3r<%u%QAVhtb3n~>%!UM zD`Z^Ptd(ti&^UkYxn}=0Oe|-oe%<#lrf0fb-~C_5p6dh!MMt?_+py0^&RkS6Hh$m1Z0TpO&!0-&QRmsv`hG$7?FF^`X+a0nU(K49HYxL^>Lj(bvXA?nELOMbthI4M!qHp@>-Ma!RUVAjSN*umY=5v!hbMDW#!tB^n>Ic+ z>w7BD@AA?*$>4Tb&eDBv6wce%$X3dW{*+OY>wI*BZ+H3oMTPae^;I8&7^bb$czmk3 z?ein{R1p!;fZzq$zkYptxA^iYWkW4(ZO*SJe5CrW&zD#>r$%PBU8MVWMoSaf6`x)C z|A}zlU$FnhG3_s1T$$bv)(LNoFaP&vIb+7p2jy#4yl8vKRq)QK84AJm&qFTbH_%DBL`BhIjM`=!s_%`=$R%@O|8nV21T;q>{VVODof zJKJPTEd2FCVy)rdH!EH@uIY82%=x-NnxBRb_)qCqcnSQrB>o0%eX=i)drYFrwOa~dJEiM13u)=_e z?X`WJ^Wl3^u@X@`ggKOa=dPCTKT`5PzC5k&-|FcB)7A;x+VU&7{wS*xW6o(cUE|o} zoKqH+zrW{W^py=t-_BnT1-#`ulA?d@2`KoP-}FMux^?k_U17KX$_Xki2oIaE)ql&2 z=h}aLPM_BPAl`l{%;BEZ+mENCx6hsv`1fxp|E+iXKmVN0s4F~mskCF>-Cd#X7oXid zcgTO+>lY8AV&6?Q-lISD_URutnY}9?E!3`fu+qF_y}bUH)go>uT-bQzeqL4G^QF$x z-eRk~e*K2)$v$t+-G8>JX6+ftg$?}`6GgNqFg7&DOzbo$H8a**BXO(=G)El2xTJ!X&iI;o)wCn?!Z~?sG24b5>!Ueaa=W$rq+QqDcHv@ttnR0D>5|*w zds%hg9OV4%Ot^1vERboBx8E((vY;@ZH^{5*=Lh>Am-FinUHa}dqw-*eTvZ)wkkpae zk6WsjEuQk|dHCET_Tgb4nv2;!eJ}t1yJ+{8H7@%XXf`g;<13tP&Al+2SynrZ_`$kRH*?6FIiHy5Ml<$qE=4h@%htvz1PJ2~r>sn2;UGuKRHyYQL96GV%AmR^>p~hCwC%s{w{D)FHl*lpq-aEgDw01UjcVbx&LpM z{z*H?zK`$XhV5;~>#8kFeYfuUzZHkmYF?v zwv02%PnTDIS}z@QG^G5$o}Q-DO|xG1R~FxTCjZ%JJ?-qW{_CY&hkPu*R@8ky{93^2 z!^KyjZ|?7pFFv@mUvNs)Hz~bIdONNi;c+rhkf@#F&A^v)Cig&KK>W(X8}DVAdZaK{ zt`29CKGozSE%lk(Zre2WnFcDy&h2$P#@qL@#;K$#V(U_iGLJj{f~RLl9Xpf%J-mMQ z+Ro*6d|Shm@(us4nXhisDEE53?m8#0=9!n>CvVT&?l&ofFYUkq#%(!wTzorcYc3JF ze(%4?^4JQ#&3-mrceb)NBpp4S|LXcDqq4~vhqvX3bUAozFM4UYL!9?_){e*f<2}#* zoGz3(W6cdY)hUwCs+WIzmal#HNZ{cMjY^jfY|N=EI%x3kQ<-g`*z^dth_3h=h zcm6gdtV{*4B(2S3dnHD7&u^)BAu)`DA+ zcizwAYI>H7?e+IY`zc5yoL?7f5_awK;`nL*GPf`85bYJt&QqRpckY~pwJWvlSHxa) zO4uptdG}ZP#hAeRC)on>B*YSK^#}(ZI2Jnp*XK{a=8J!L)NK{M;Ok%azmJz4(C7Jj zqjc9o_4+N>Kb&dvVBX^L_3PJZ=U#pN!x*=z-JOT=;z_%UcN}+!?t53xEn(*T+S9V_ zTetc9p2@;jLJ!6*4Lox3)+4Sb()sEy~7}8 zQ)1k0f47-M&2P?;Q{nf&8m|!%$iMvU_~l}gz`NhS%;sR^kE^%a`CioU^PGt~zcaTL1^W8jgdt2VY z?s;u~`=>q&tv|+i$VohQiL{>J4()UAZ>Bp}3u}0%^X2OB2IXt7+a0f6dhdL6UV*yU zWuJ50qS9pLA4r+TP%{e%JM8(w|YriVaT70Vf<+&OI2OYlC^K-v;JKOpt?Ou6XPW@o*xh*#(pFH38>xgKP zbn{Jz(}9^DD_1x3GD=Q9+_G)^@0$*R*S~F_a*fgEh1uh<9_x2I{_fw)?0PZk*Vnhl z>(B4%==bvSIRDPAI+mMT{+}kVq^)Gxm4az|_D+h;-fr;a#l$*ezV+MkkKfYWV!q`; zgqip}+fL8J)sy0Ht>|3kre0OIVPC_pUsh75_=PgF9?z`2zA^25+&Qan8QZ2@Gkkp} zFh_Ic=e=Efe}7qeG)w)`l~d8$=9?cazPshBxo~bKyJE%nX4yIA-zv6=nKlW0Iq}BP z+1d8vw6c4@GNhZ``7S?9`z|XjC2erINya#niP??k+!cTz&SE|GZuQrAt9;7b|QybzN`*=TDgu``@0^51jkUnD?``)JHF3Y*Y`lRTgu_67&rSv_4!r0@gf(3QWaP0HVI!c_%*}jlX6|= z&g&BnxfM5-x!0ewd7373-15r1hkehO9Nz7*KJmMzylKDn1lvXNPKt%b5udj|oowu5 z-n!~eV3)w8J>R=)?JUyHM3lX~*J#4~>`uYN`CRk9FN|Aget-Xj<#PMF_DprHTJC@P zqh*5uZx!Fm({rTvpPdwX)Wi8w%v}e^i*i!muYbC<+v@glo~+=47aI~q@~+pmrOh|( zQk56`J-NFfAZ^A?ao+Tk(W!N;MqY+hS2DO_3@7m9+iVnQ=#{p+KHGDL_P*ycw_o1# z$?(6G{wcLaTTwe}?^ckxFq7k>#e_-^~VZJAl-j%ADY-EZ!Rv-Mbga=}Xn zStqX83oq5?vK&@4*z*0|vTE_7OO?hOblrb*-)T%+_LL!&<3ZPFw#{GO7IATLTPfO0 zM%}*UVU+cUb-{&OU)M1+=AL7G9xth&%P~7X|M7cK#&fb0rH++exaMH&*=nID$m+W2 zatF({cib7W>;ZA+0pI0ri|*h3ja}ft7sbN;bpecT%T~obY7G?OioQA5vVo!ebolgD z$B&)7Kg~b3P}NfQoA3(lbtzw8zgPVxSs*gi`unFddu~428-M1EvHOJYjqjFjUY@c2 zcDeih1ES0O74lOBgSd|M$eunYYyHA-ssv1*xt6*A9L`TW= z{^N~I4+W;rw{A&&Kjrsq8%f8>3lFyoo^9B*sd?9fOWiwm7?-~fUT^sP8Fyyywarh~ z8hJ$hynN2$Wc~5`HUFdj7Y7Fit1r@6#q;CSubyt7UFDZ8tE2Ddet!Lb&#C1po9X|_l1-U;-0<)yXr<-JxLTlem^ndv<#FJC3aCe{7o z$cx&?#dA>q`MzWC-~X4Z%oh{pm~mw4qE*Yf=1JDpI=y0f=WVm+gYa5^m9**3juvy= zdUsb&VK@6QSANzO_k`=W%Hjp5yzBBVc0O{to4F=^O5UvmZQGBG3#+--=wE;PecAnQ z(L9=*EWS%F=%)TWwD14A*AXw$t}OraOp|F@-nGozX46syGxFP)sqy`Ld;R44b@tVD z3=OMZh^5{8K4YHo?RO@H+|OSv@V}Sdx~A%@>|BmVVeGp9O89SYdn%lpw|w88Tl?K- z1%mnznZZ1CWw|~duiL>>NZFb#rGi`*9iLX>+a11#&Z&pL`qgDw1u&DaN?Ur>at~q)Ws7rbH*byRakAaNFV& z1}4qlYMA0)PhZgTsEgrw--Wlg7xgJ9Y<=@%kIji6(eg*ibl0upIB;8Fl`{K<+{cfP zJQCw-Z;(`x;S)CCozL)Sqs7~cX0xQCSFBv8DAfDoeBcTe#+ZL0N#;7o1Fwk5c_`fd zdvK3w=Oe*<_l>Qe>z>KSf4#N)$StJ`zsBd!Vw#qOrtHLJc=E+A(Ur$@_#89}M zNn65zbE&FugR4#2#e4VMl8cSEUfrt6GT{*8xxJsY>YrBU$R_+#zg;^|P^fXrY}>mD z$BsUKc!Tfuw&%e+_sVdd`Tw_9=ch;d!yD7i_RYPWS+ci!SHLCFfNe_I5%SOO6bOFb z|AFmP*^KJPt{SY~;o^l)Un&>Mw9kGY&A_q$fe2@D-VP3DubktL{yo3HWZfTy8+>6V zeaY4?(oq)QrDA{Yc=mRx9pjlfhLi40ZQb|nz2L#)`D+|3yOPuAoLIlVy7bg;Ih%;1 zxAXV=dV3dheX`BCyMv`utlx3_%jWA$ev4ck6I~lVyB%RXxNR?^U)>AKq>U11*XkZg z7Tfl?CY6bSV+NyB=0Xvs4!5t%1kd{VUb%LyT!|r`{e|fw{j03I-uvHMUfp5U>2^#} z`N>AMT}?C9d#;^RbWeX{WBol!Z2g_XZ;bnu%1Z?oltkTNxv?!*x>R*)0>`|{7g9cl z50vFhtBH6aeYh=g!;j{>F_phG_U5k1oou^DzI18mm8l^UZ8ypH-}w+*Wbd`4(zfXH z(LbMV*6(zAW3c4B!ma4ryQciFT-{~jTmItmY|}ORr^ z-Z%2z@Ji@7;xL(8~*pLQI`{zH(I*$WHoJWP|MljkhC5eXHBKKh z@XhglZc)AI-^})P50{!Q)mR$wt5EZhiKKm<&7lbq(|EZ+W%nlCbvCt2;=Zn9vV0Gk z=8HXYCfIFB&&q{cx^^w{sw@Be{luAfepTC#++zXdWkr?nd3JlUmkLOj*D=-qJo^6c zvdJfJl_fhj@BZ5LYD2n5&|{PGt;c2GR)23Y6g+M!c=4v?vZuq-r@fWQ!QybdE{HO&ls2sU^i9iv<@58n_eEkg zA5|K){bf(;&##=i>r=DM-v>HH_8P}HGR~hmH`jVb^=H>7Yqc)9G=#C(@_ktSd7Y2X zoZBUr3NN3W5~LHiXTq*uzs|j9`*&w+&QIvYh45^{(+UlR?O0>C1Oxm)=WMST3-Xn`P zPT4rS-P!JdetJw-_%e(9tKI+l7Rg-^4KMn-*Pm&E?H2jI-Nnn>XI7cNF8>!J>v#1{ z-08CUt>p^8s^{1CZam)e)@0h6#M%}2qvsy_#m~D=yr7iRy8NAo<>r*o!c*TU9J zG3_XjY1fT53kZ+C{?z8@?QJ{w7bP@|O5>JEokDJ*qEh)3id<=D=qDIOlg-a}_?FysgdH zTf?7uDl}qRO+<^wtACX$0r6!5OA@ZFR2G&|;4HG&y40nhu+;J1szV}A?*IOheP(au z?uFCWXgzEEyw2uH*oKoX7jE46(t0A?VoC3B#iiQ+PX^xqwJ^JnwM0DFd471`fr&!5 zw-uO2UAFFRxpXIMvGVa&d%u}4rp)5GSFxXCf70!1UspwcpVz+s#B0HhM}MrhZ2j;} zUi|0ty~%x#KOB4i?6+q0$;mJIbvmU|+r`#6ap?WNY%Ars{oG5FKJEWC@?Wc0SN`tj z4)iiiKiaLSbbzre=k}`Os~*fSJYEv@m!UXs*Mxg7_=;R~iby8ASLbKT*#!%Fu* zaxphHWM<{sNGOPKEzOa5n$MZ;)zrxFuwX`k#hY{am+r_cvertices) { + return result; + } + + // mesh->triangleCount may not be set, vertexCount is more reliable + int triangleCount = mesh->vertexCount / 3; + + // Test against all triangles in mesh + for (int i=0; i < triangleCount; i++) { + Vector3 a, b, c; + Vector3 *vertdata = (Vector3*)mesh->vertices; + if (mesh->indices) { + a = vertdata[ mesh->indices[i*3+0] ]; + b = vertdata[ mesh->indices[i*3+1] ]; + c = vertdata[ mesh->indices[i*3+2] ]; + } else { + a = vertdata[i*3+0]; + b = vertdata[i*3+1]; + c = vertdata[i*3+2]; + } + + RayHitInfo triHitInfo = RaycastTriangle( ray, a, b, c ); + if (triHitInfo.hit) { + // Save the closest hit triangle + if ((!result.hit)||(result.distance > triHitInfo.distance)) { + result = triHitInfo; + } + } + } + + return result; +} diff --git a/src/raylib.h b/src/raylib.h index f291ce858..7252ba4e5 100644 --- a/src/raylib.h +++ b/src/raylib.h @@ -497,6 +497,7 @@ typedef struct Ray { // Information returned from a raycast typedef struct RayHitInfo { bool hit; // Did the ray hit something? + float distance; // Distance to nearest hit Vector3 hitPosition; // Position of nearest hit Vector3 hitNormal; // Surface normal of hit } RayHitInfo; @@ -924,6 +925,8 @@ RLAPI bool CheckCollisionRayBox(Ray ray, BoundingBox box); // Ray Casts //------------------------------------------------------------------------------------ RLAPI RayHitInfo RaycastGroundPlane( Ray ray, float groundHeight ); +RLAPI RayHitInfo RaycastTriangle( Ray ray, Vector3 a, Vector3 b, Vector3 c ); +RLAPI RayHitInfo RaycastMesh( Ray ray, Mesh *mesh ); //------------------------------------------------------------------------------------ // Shaders System Functions (Module: rlgl) diff --git a/src/raymath.h b/src/raymath.h index 3cd1394ee..5871e350b 100644 --- a/src/raymath.h +++ b/src/raymath.h @@ -130,6 +130,7 @@ RMDEF void VectorTransform(Vector3 *v, Matrix mat); // Transforms a Ve RMDEF Vector3 VectorZero(void); // Return a Vector3 init to zero RMDEF Vector3 VectorMin(Vector3 vec1, Vector3 vec2); // Return min value for each pair of components RMDEF Vector3 VectorMax(Vector3 vec1, Vector3 vec2); // Return max value for each pair of components +RMDEF Vector3 Barycentric(Vector3 p, Vector3 a, Vector3 b, Vector3 c); // Barycentric coords for p in triangle abc //------------------------------------------------------------------------------------ // Functions Declaration to work with Matrix @@ -382,6 +383,31 @@ RMDEF Vector3 VectorMax(Vector3 vec1, Vector3 vec2) return result; } +// Compute barycentric coordinates (u, v, w) for +// point p with respect to triangle (a, b, c) +// Assumes P is on the plane of the triangle +RMDEF Vector3 Barycentric(Vector3 p, Vector3 a, Vector3 b, Vector3 c) +{ + + //Vector v0 = b - a, v1 = c - a, v2 = p - a; + Vector3 v0 = VectorSubtract( b, a ); + Vector3 v1 = VectorSubtract( c, a ); + Vector3 v2 = VectorSubtract( p, a ); + float d00 = VectorDotProduct(v0, v0); + float d01 = VectorDotProduct(v0, v1); + float d11 = VectorDotProduct(v1, v1); + float d20 = VectorDotProduct(v2, v0); + float d21 = VectorDotProduct(v2, v1); + float denom = d00 * d11 - d01 * d01; + + Vector3 result; + result.y = (d11 * d20 - d01 * d21) / denom; + result.z = (d00 * d21 - d01 * d20) / denom; + result.x = 1.0f - (result.z + result.y); + + return result; +} + //---------------------------------------------------------------------------------- // Module Functions Definition - Matrix math //---------------------------------------------------------------------------------- diff --git a/src/shapes.c b/src/shapes.c index 4b2de4f20..74480c83e 100644 --- a/src/shapes.c +++ b/src/shapes.c @@ -544,13 +544,74 @@ RayHitInfo RaycastGroundPlane( Ray ray, float groundHeight ) { float t = (ray.position.y - groundHeight) / -ray.direction.y; if (t >= 0.0) { - Vector3 camDir = ray.direction; - VectorScale( &camDir, t ); - result.hit = true; - result.hitNormal = (Vector3){ 0.0, 1.0, 0.0}; - result.hitPosition = VectorAdd( ray.position, camDir ); + Vector3 rayDir = ray.direction; + VectorScale( &rayDir, t ); + result.hit = true; + result.distance = t; + result.hitNormal = (Vector3){ 0.0, 1.0, 0.0}; + result.hitPosition = VectorAdd( ray.position, rayDir ); } } - return result; -} \ No newline at end of file +} +// Adapted from: +// https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm +RayHitInfo RaycastTriangle( Ray ray, Vector3 a, Vector3 b, Vector3 c ) +{ + Vector3 e1, e2; //Edge1, Edge2 + Vector3 p, q, tv; + float det, inv_det, u, v; + float t; + RayHitInfo result = {0}; + + //Find vectors for two edges sharing V1 + e1 = VectorSubtract( b, a); + e2 = VectorSubtract( c, a); + + //Begin calculating determinant - also used to calculate u parameter + p = VectorCrossProduct( ray.direction, e2); + + //if determinant is near zero, ray lies in plane of triangle or ray is parallel to plane of triangle + det = VectorDotProduct(e1, p); + + //NOT CULLING + if(det > -EPSILON && det < EPSILON) return result; + inv_det = 1.f / det; + + //calculate distance from V1 to ray origin + tv = VectorSubtract( ray.position, a ); + + //Calculate u parameter and test bound + u = VectorDotProduct(tv, p) * inv_det; + + //The intersection lies outside of the triangle + if(u < 0.f || u > 1.f) return result; + + //Prepare to test v parameter + q = VectorCrossProduct( tv, e1 ); + + //Calculate V parameter and test bound + v = VectorDotProduct( ray.direction, q) * inv_det; + + //The intersection lies outside of the triangle + if(v < 0.f || (u + v) > 1.f) return result; + + t = VectorDotProduct(e2, q) * inv_det; + + + if(t > EPSILON) { + // ray hit, get hit point and normal + result.hit = true; + result.distance = t; + + result.hit = true; + result.hitNormal = VectorCrossProduct( e1, e2 ); + VectorNormalize( &result.hitNormal ); + Vector3 rayDir = ray.direction; + VectorScale( &rayDir, t ); + result.hitPosition = VectorAdd( ray.position, rayDir ); + } + + return result; +} +