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 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 literal 0 HcmV?d00001 diff --git a/src/models.c b/src/models.c index a2043913c..41e527dca 100644 --- a/src/models.c +++ b/src/models.c @@ -1918,3 +1918,41 @@ static Material LoadMTL(const char *fileName) return material; } + +RayHitInfo RaycastMesh( Ray ray, Mesh *mesh ) +{ + RayHitInfo result = {0}; + + // If mesh doesn't have vertex data on CPU, can't test it. + if (!mesh->vertices) { + 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; +} +