Recent

Author Topic: Oriented Bounding Box (OBB) support for Raylib  (Read 791 times)

Guva

  • Full Member
  • ***
  • Posts: 207
  • 🌈 ZX-Spectrum !!!
Oriented Bounding Box (OBB) support for Raylib
« on: April 10, 2025, 04:39:42 pm »
Oriented Bounding Box (OBB) Collision System for raylib.

This is a lightweight, raylib-compatible OBB collision module. It provides high-performance functions for 3D collision detection using oriented bounding boxes, including:
  • OBB vs OBB (Separating Axis Theorem)
  • OBB vs BoundingBox
  • OBB vs Sphere
  • Ray vs OBB
  • Point inside OBB check
  • OBB corner generation
  • Wireframe OBB debug drawing

Code: Pascal  [Select][+][-]
  1. program OrientedBoxCollisions;
  2.  
  3. uses
  4.   raylib, raymath, OBB;
  5.  
  6. var
  7.   screenWidth, screenHeight: Integer;
  8.   camera: TCamera;
  9.   playerPosition, playerSize: TVector3;
  10.   playerRotation: TQuaternion;
  11.   playerColor: TColor;
  12.   playerMesh: TMesh;
  13.   playerModel: TModel;
  14.   enemyBoxPos, enemyBoxSize: TVector3;
  15.   enemyBoxRotation: TQuaternion;
  16.   enemyBoxMesh: TMesh;
  17.   enemyBoxModel: TModel;
  18.   enemySpherePos: TVector3;
  19.   enemySphereSize: Single;
  20.   collision: Boolean;
  21.   rotation: TQuaternion;
  22.   playerModelMatrix, enemyBoxModelMatrix: TMatrix;
  23.   playerObb, enemyBoxObb: TOBB;
  24.   mouseRay: TRay;
  25.   rayCollision: TRayCollision;
  26.  
  27. begin
  28.   screenWidth := 800;
  29.   screenHeight := 450;
  30.  
  31.   InitWindow(screenWidth, screenHeight, 'raylib [models] example - oriented box collisions');
  32.  
  33.   camera.position := Vector3Create(0.0, 10.0, 10.0);
  34.   camera.target := Vector3Create(0.0, 0.0, 0.0);
  35.   camera.up := Vector3Create(0.0, 1.0, 0.0);
  36.   camera.fovy := 45.0;
  37.   camera.projection := CAMERA_PERSPECTIVE;
  38.  
  39.   playerPosition := Vector3Create(0.0, 1.0, 2.0);
  40.   playerRotation := QuaternionIdentity();
  41.   playerSize := Vector3Create(1.0, 2.0, 1.0);
  42.   playerColor := GREEN;
  43.   playerMesh := GenMeshCube(1.0, 1.0, 1.0);
  44.   playerModel := LoadModelFromMesh(playerMesh);
  45.  
  46.   enemyBoxPos := Vector3Create(-4.0, 1.0, 0.0);
  47.   enemyBoxRotation := QuaternionIdentity();
  48.   enemyBoxSize := Vector3Create(2.0, 2.0, 2.0);
  49.   enemyBoxMesh := GenMeshCube(1.0, 1.0, 1.0);
  50.   enemyBoxModel := LoadModelFromMesh(enemyBoxMesh);
  51.  
  52.   enemySpherePos := Vector3Create(4.0, 0.0, 0.0);
  53.   enemySphereSize := 1.5;
  54.  
  55.   collision := False;
  56.  
  57.   SetTargetFPS(60);
  58.  
  59.   // Main game loop
  60.   while not WindowShouldClose() do
  61.   begin
  62.     // Update
  63.     //----------------------------------------------------------------------------------
  64.  
  65.     // Move player
  66.     if IsKeyDown(KEY_RIGHT) then
  67.       playerPosition.x := playerPosition.x + 0.2
  68.     else if IsKeyDown(KEY_LEFT) then
  69.       playerPosition.x := playerPosition.x - 0.2
  70.     else if IsKeyDown(KEY_DOWN) then
  71.       playerPosition.z := playerPosition.z + 0.2
  72.     else if IsKeyDown(KEY_UP) then
  73.       playerPosition.z := playerPosition.z - 0.2;
  74.  
  75.     collision := False;
  76.  
  77.     rotation := QuaternionFromEuler(0.0, GetTime(), 0.0);
  78.     playerRotation := rotation;
  79.     enemyBoxRotation := rotation;
  80.  
  81.     playerModelMatrix := MatrixIdentity();
  82.     playerModelMatrix := MatrixMultiply(playerModelMatrix, MatrixScale(playerSize.x, playerSize.y, playerSize.z));
  83.     playerModelMatrix := MatrixMultiply(playerModelMatrix, QuaternionToMatrix(playerRotation));
  84.     playerModelMatrix := MatrixMultiply(playerModelMatrix, MatrixTranslate(playerPosition.x, playerPosition.y, playerPosition.z));
  85.     playerModel.transform := playerModelMatrix;
  86.  
  87.     enemyBoxModelMatrix := MatrixIdentity();
  88.     enemyBoxModelMatrix := MatrixMultiply(enemyBoxModelMatrix, MatrixScale(enemyBoxSize.x, enemyBoxSize.y, enemyBoxSize.z));
  89.     enemyBoxModelMatrix := MatrixMultiply(enemyBoxModelMatrix, QuaternionToMatrix(enemyBoxRotation));
  90.     enemyBoxModelMatrix := MatrixMultiply(enemyBoxModelMatrix, MatrixTranslate(enemyBoxPos.x, enemyBoxPos.y, enemyBoxPos.z));
  91.     enemyBoxModel.transform := enemyBoxModelMatrix;
  92.  
  93.     playerObb.center := playerPosition;
  94.     playerObb.rotation := playerRotation;
  95.     playerObb.halfExtents := Vector3Scale(playerSize, 0.5);
  96.  
  97.     enemyBoxObb.center := enemyBoxPos;
  98.     enemyBoxObb.rotation := enemyBoxRotation;
  99.     enemyBoxObb.halfExtents := Vector3Scale(enemyBoxSize, 0.5);
  100.  
  101.     if CheckCollisionOBBvsOBB(@playerObb, @enemyBoxObb) then
  102.       collision := True;
  103.  
  104.     if CheckCollisionSphereVsOBB(enemySpherePos, enemySphereSize, @playerObb) then
  105.       collision := True;
  106.  
  107.     mouseRay := GetScreenToWorldRay(GetMousePosition(), camera);
  108.     rayCollision := GetRayCollisionOBB(mouseRay, @enemyBoxObb);
  109.  
  110.     if collision then
  111.       playerColor := RED
  112.     else
  113.       playerColor := GREEN;
  114.     //----------------------------------------------------------------------------------
  115.     // Draw
  116.     //----------------------------------------------------------------------------------
  117.     BeginDrawing();
  118.  
  119.       ClearBackground(RAYWHITE);
  120.  
  121.       BeginMode3D(camera);
  122.  
  123.         // Draw enemy-box
  124.         DrawModel(enemyBoxModel, Vector3Zero(), 1.0, GRAY);
  125.         OBB_DrawWireframe(@enemyBoxObb, DARKGRAY);
  126.  
  127.         // Draw enemy-sphere
  128.         DrawSphere(enemySpherePos, enemySphereSize, GRAY);
  129.         DrawSphereWires(enemySpherePos, enemySphereSize, 16, 16, DARKGRAY);
  130.  
  131.         // Draw player
  132.         DrawModel(playerModel, Vector3Zero(), 1.0, playerColor);
  133.         OBB_DrawWireframe(@playerObb, DARKGRAY);
  134.  
  135.         if rayCollision.hit then
  136.         begin
  137.           DrawPoint3D(rayCollision.point, RED);
  138.           DrawLine3D(rayCollision.point, Vector3Add(rayCollision.point,
  139.                      Vector3Scale(rayCollision.normal, rayCollision.distance)), ORANGE);
  140.         end;
  141.  
  142.         DrawGrid(10, 1.0); // Draw a grid
  143.  
  144.       EndMode3D();
  145.  
  146.       DrawText('Move player with arrow keys to collide', 220, 40, 20, GRAY);
  147.       DrawText('Place mouse on the grey box to test raycast', 180, 60, 20, GRAY);
  148.  
  149.       DrawFPS(10, 10);
  150.  
  151.     EndDrawing();
  152.     //----------------------------------------------------------------------------------
  153.   end;
  154.  
  155.   // De-Initialization
  156.   UnloadModel(playerModel);
  157.   UnloadModel(enemyBoxModel);
  158.  
  159.   CloseWindow(); // Close window and OpenGL context
  160. end.
  161.  

Code: Pascal  [Select][+][-]
  1. unit OBB;
  2. (*
  3. Translate from c to pascal by Gunko Vadim
  4. Original c code by Aiman Azees
  5. https://github.com/AimanGameDev/Raylib-OBB-Oriented-Bounding-Box
  6. *)
  7. interface
  8.  
  9. uses
  10.   raylib, raymath, math;
  11.  
  12. const Infinity =  1.0 / 0.0;
  13.  
  14. type
  15.   POBB = ^TOBB;
  16.   TOBB = record
  17.     rotation: TQuaternion;
  18.     center: TVector3;
  19.     halfExtents: TVector3;
  20.   end;
  21.  
  22. procedure OBB_GetAxes(obb: POBB; out right, up, forward: TVector3);
  23. procedure OBB_GetCorners(obb: POBB; out corners: array of TVector3);
  24. procedure OBB_DrawWireframe(obb: POBB; color: TColor);
  25. function OBB_ContainsPoint(obb: POBB; point: TVector3): Boolean;
  26. procedure ProjectBoundingBoxOntoAxis(box: PBoundingBox; axis: TVector3; out outMin, outMax: Single);
  27. procedure ProjectOBBOntoAxis(obb: POBB; axis: TVector3; out outMin, outMax: Single);
  28. function CheckCollisionBoundingBoxVsOBB(box: PBoundingBox; obb: POBB): Boolean;
  29. function CheckCollisionOBBvsOBB(a, b: POBB): Boolean;
  30. function GetRayCollisionOBB(ray: TRay; obb: POBB): TRayCollision;
  31. function CheckCollisionSphereVsOBB(sphereCenter: TVector3; radius: Single; obb: POBB): Boolean;
  32.  
  33. implementation
  34.  
  35. procedure OBB_GetAxes(obb: POBB; out right, up, forward: TVector3);
  36. var
  37.   rot: TMatrix;
  38. begin
  39.   rot := QuaternionToMatrix(obb^.rotation);
  40.  
  41.   right := Vector3Create(rot.m0, rot.m1, rot.m2);
  42.   up := Vector3Create(rot.m4, rot.m5, rot.m6);
  43.   forward := Vector3Create(rot.m8, rot.m9, rot.m10);
  44. end;
  45.  
  46. procedure OBB_GetCorners(obb: POBB; out corners: array of TVector3);
  47. var
  48.   right, up, forward: TVector3;
  49. begin
  50.   OBB_GetAxes(obb, right, up, forward);
  51.  
  52.   right := Vector3Scale(right, obb^.halfExtents.x);
  53.   up := Vector3Scale(up, obb^.halfExtents.y);
  54.   forward := Vector3Scale(forward, obb^.halfExtents.z);
  55.  
  56.   corners[0] := Vector3Add(Vector3Add(Vector3Add(obb^.center, right), up), forward);
  57.   corners[1] := Vector3Add(Vector3Add(Vector3Subtract(obb^.center, right), up), forward);
  58.   corners[2] := Vector3Add(Vector3Add(Vector3Subtract(obb^.center, right), up), Vector3Negate(forward));
  59.   corners[3] := Vector3Add(Vector3Add(Vector3Add(obb^.center, right), up), Vector3Negate(forward));
  60.  
  61.   corners[4] := Vector3Add(Vector3Add(Vector3Add(obb^.center, right), Vector3Negate(up)), forward);
  62.   corners[5] := Vector3Add(Vector3Add(Vector3Subtract(obb^.center, right), Vector3Negate(up)), forward);
  63.   corners[6] := Vector3Add(Vector3Add(Vector3Subtract(obb^.center, right), Vector3Negate(up)), Vector3Negate(forward));
  64.   corners[7] := Vector3Add(Vector3Add(Vector3Add(obb^.center, right), Vector3Negate(up)), Vector3Negate(forward));
  65. end;
  66.  
  67. procedure OBB_DrawWireframe(obb: POBB; color: TColor);
  68. var
  69.   c: array[0..7] of TVector3;
  70. begin
  71.   OBB_GetCorners(obb, c);
  72.  
  73.   DrawLine3D(c[0], c[1], color);
  74.   DrawLine3D(c[1], c[2], color);
  75.   DrawLine3D(c[2], c[3], color);
  76.   DrawLine3D(c[3], c[0], color);
  77.  
  78.   DrawLine3D(c[4], c[5], color);
  79.   DrawLine3D(c[5], c[6], color);
  80.   DrawLine3D(c[6], c[7], color);
  81.   DrawLine3D(c[7], c[4], color);
  82.  
  83.   DrawLine3D(c[0], c[4], color);
  84.   DrawLine3D(c[1], c[5], color);
  85.   DrawLine3D(c[2], c[6], color);
  86.   DrawLine3D(c[3], c[7], color);
  87. end;
  88.  
  89. function OBB_ContainsPoint(obb: POBB; point: TVector3): Boolean;
  90. var
  91.   local: TVector3;
  92.   inverseRot: TQuaternion;
  93. begin
  94.   local := Vector3Subtract(point, obb^.center);
  95.  
  96.   inverseRot := QuaternionInvert(obb^.rotation);
  97.   local := Vector3RotateByQuaternion(local, inverseRot);
  98.  
  99.   Result := (abs(local.x) <= obb^.halfExtents.x) and
  100.             (abs(local.y) <= obb^.halfExtents.y) and
  101.             (abs(local.z) <= obb^.halfExtents.z);
  102. end;
  103.  
  104. procedure ProjectBoundingBoxOntoAxis(box: PBoundingBox; axis: TVector3; out outMin, outMax: Single);
  105. var
  106.   corners: array[0..7] of TVector3;
  107.   i: Integer;
  108.   projection, min, max: Single;
  109. begin
  110.   corners[0] := Vector3Create(box^.min.x, box^.min.y, box^.min.z);
  111.   corners[1] := Vector3Create(box^.max.x, box^.min.y, box^.min.z);
  112.   corners[2] := Vector3Create(box^.max.x, box^.max.y, box^.min.z);
  113.   corners[3] := Vector3Create(box^.min.x, box^.max.y, box^.min.z);
  114.   corners[4] := Vector3Create(box^.min.x, box^.min.y, box^.max.z);
  115.   corners[5] := Vector3Create(box^.max.x, box^.min.y, box^.max.z);
  116.   corners[6] := Vector3Create(box^.max.x, box^.max.y, box^.max.z);
  117.   corners[7] := Vector3Create(box^.min.x, box^.max.y, box^.max.z);
  118.  
  119.   min := Vector3DotProduct(corners[0], axis);
  120.   max := min;
  121.  
  122.   for i := 1 to 7 do
  123.   begin
  124.     projection := Vector3DotProduct(corners[i], axis);
  125.     if projection < min then
  126.       min := projection;
  127.     if projection > max then
  128.       max := projection;
  129.   end;
  130.  
  131.   outMin := min;
  132.   outMax := max;
  133. end;
  134.  
  135. procedure ProjectOBBOntoAxis(obb: POBB; axis: TVector3; out outMin, outMax: Single);
  136. var
  137.   right, up, forward: TVector3;
  138.   r, centerProj: Single;
  139. begin
  140.   OBB_GetAxes(obb, right, up, forward);
  141.  
  142.   r := abs(Vector3DotProduct(right, axis)) * obb^.halfExtents.x +
  143.        abs(Vector3DotProduct(up, axis)) * obb^.halfExtents.y +
  144.        abs(Vector3DotProduct(forward, axis)) * obb^.halfExtents.z;
  145.  
  146.   centerProj := Vector3DotProduct(obb^.center, axis);
  147.   outMin := centerProj - r;
  148.   outMax := centerProj + r;
  149. end;
  150.  
  151. function CheckCollisionBoundingBoxVsOBB(box: PBoundingBox; obb: POBB): Boolean;
  152. var
  153.   aabbAxes: array[0..2] of TVector3;
  154.   obbAxes: array[0..2] of TVector3;
  155.   testAxes: array[0..14] of TVector3;
  156.   axisCount, i, j: Integer;
  157.   cross: TVector3;
  158.   axis: TVector3;
  159.   minA, maxA, minB, maxB: Single;
  160. begin
  161.   aabbAxes[0] := Vector3Create(1, 0, 0);
  162.   aabbAxes[1] := Vector3Create(0, 1, 0);
  163.   aabbAxes[2] := Vector3Create(0, 0, 1);
  164.  
  165.   OBB_GetAxes(obb, obbAxes[0], obbAxes[1], obbAxes[2]);
  166.  
  167.   axisCount := 0;
  168.  
  169.   for i := 0 to 2 do
  170.   begin
  171.     testAxes[axisCount] := aabbAxes[i];
  172.     Inc(axisCount);
  173.   end;
  174.  
  175.   for i := 0 to 2 do
  176.   begin
  177.     testAxes[axisCount] := obbAxes[i];
  178.     Inc(axisCount);
  179.   end;
  180.  
  181.   for i := 0 to 2 do
  182.   begin
  183.     for j := 0 to 2 do
  184.     begin
  185.       cross := Vector3CrossProduct(aabbAxes[i], obbAxes[j]);
  186.       if Vector3LengthSqr(cross) > 0.000001 then
  187.       begin
  188.         testAxes[axisCount] := Vector3Normalize(cross);
  189.         Inc(axisCount);
  190.       end;
  191.     end;
  192.   end;
  193.  
  194.   for i := 0 to axisCount - 1 do
  195.   begin
  196.     axis := testAxes[i];
  197.     ProjectBoundingBoxOntoAxis(box, axis, minA, maxA);
  198.     ProjectOBBOntoAxis(obb, axis, minB, maxB);
  199.  
  200.     if (maxA < minB) or (maxB < minA) then
  201.     begin
  202.       Result := False;
  203.       Exit;
  204.     end;
  205.   end;
  206.  
  207.   Result := True;
  208. end;
  209.  
  210. function CheckCollisionOBBvsOBB(a, b: POBB): Boolean;
  211. var
  212.   axesA, axesB: array[0..2] of TVector3;
  213.   testAxes: array[0..14] of TVector3;
  214.   axisCount, i, j: Integer;
  215.   cross: TVector3;
  216.   len: Single;
  217.   axis: TVector3;
  218.   minA, maxA, minB, maxB: Single;
  219. begin
  220.   OBB_GetAxes(a, axesA[0], axesA[1], axesA[2]);
  221.   OBB_GetAxes(b, axesB[0], axesB[1], axesB[2]);
  222.  
  223.   axisCount := 0;
  224.  
  225.   for i := 0 to 2 do
  226.   begin
  227.     testAxes[axisCount] := axesA[i];
  228.     Inc(axisCount);
  229.   end;
  230.  
  231.   for i := 0 to 2 do
  232.   begin
  233.     testAxes[axisCount] := axesB[i];
  234.     Inc(axisCount);
  235.   end;
  236.  
  237.   for i := 0 to 2 do
  238.   begin
  239.     for j := 0 to 2 do
  240.     begin
  241.       cross := Vector3CrossProduct(axesA[i], axesB[j]);
  242.       len := Vector3Length(cross);
  243.       if len > 0.0001 then
  244.       begin
  245.         testAxes[axisCount] := Vector3Scale(cross, 1.0 / len);
  246.         Inc(axisCount);
  247.       end;
  248.     end;
  249.   end;
  250.  
  251.   for i := 0 to axisCount - 1 do
  252.   begin
  253.     axis := testAxes[i];
  254.     ProjectOBBOntoAxis(a, axis, minA, maxA);
  255.     ProjectOBBOntoAxis(b, axis, minB, maxB);
  256.  
  257.     if (maxA < minB) or (maxB < minA) then
  258.     begin
  259.       Result := False;
  260.       Exit;
  261.     end;
  262.   end;
  263.  
  264.   Result := True;
  265. end;
  266.  
  267. function GetRayCollisionOBB(ray: TRay; obb: POBB): TRayCollision;
  268. var
  269.   localOrigin, localRayOrigin, localRayDir: TVector3;
  270.   inverseRot: TQuaternion;
  271.   boxMin, boxMax: TVector3;
  272.   tmin, tmax: Single;
  273.   normal: TVector3;
  274.   i: Integer;
  275.   origin, dir, min, max, ood, t1, t2: Single;
  276.   axis: Integer;
  277.   temp: Single;
  278. begin
  279.   Result.hit := False;
  280.   Result.distance := 0;
  281.   Result.normal := Vector3Zero();
  282.   Result.point := Vector3Zero();
  283.  
  284.   // Move ray into OBB's local space
  285.   localOrigin := Vector3Subtract(ray.position, obb^.center);
  286.   inverseRot := QuaternionInvert(obb^.rotation);
  287.   localRayOrigin := Vector3RotateByQuaternion(localOrigin, inverseRot);
  288.   localRayDir := Vector3RotateByQuaternion(ray.direction, inverseRot);
  289.  
  290.   boxMin := Vector3Negate(obb^.halfExtents);
  291.   boxMax := obb^.halfExtents;
  292.  
  293.   // Ray vs AABB in OBB-local space
  294.   tmin := -Infinity;
  295.   tmax := Infinity;
  296.   normal := Vector3Zero();
  297.  
  298.   for i := 0 to 2 do
  299.   begin
  300.     case i of
  301.       0: begin origin := localRayOrigin.x; dir := localRayDir.x; min := boxMin.x; max := boxMax.x; end;
  302.       1: begin origin := localRayOrigin.y; dir := localRayDir.y; min := boxMin.y; max := boxMax.y; end;
  303.       2: begin origin := localRayOrigin.z; dir := localRayDir.z; min := boxMin.z; max := boxMax.z; end;
  304.     end;
  305.  
  306.     if abs(dir) < 0.0001 then
  307.     begin
  308.       if (origin < min) or (origin > max) then
  309.         Exit;
  310.     end
  311.     else
  312.     begin
  313.       ood := 1.0 / dir;
  314.       t1 := (min - origin) * ood;
  315.       t2 := (max - origin) * ood;
  316.       axis := i;
  317.  
  318.       if t1 > t2 then
  319.       begin
  320.         temp := t1;
  321.         t1 := t2;
  322.         t2 := temp;
  323.         axis := -axis;
  324.       end;
  325.  
  326.       if t1 > tmin then
  327.       begin
  328.         tmin := t1;
  329.         normal := Vector3Zero();
  330.         case abs(axis) of
  331.           0: normal.x := IfThen(axis >= 0, -1.0, 1.0);
  332.           1: normal.y := IfThen(axis >= 0, -1.0, 1.0);
  333.           2: normal.z := IfThen(axis >= 0, -1.0, 1.0);
  334.         end;
  335.       end;
  336.  
  337.       if t2 < tmax then
  338.       begin
  339.         tmax := t2;
  340.       end;
  341.  
  342.       if tmin > tmax then
  343.         Exit;
  344.     end;
  345.   end;
  346.  
  347.   // Convert result to world space
  348.   Result.hit := True;
  349.   Result.distance := tmin;
  350.   Result.point := Vector3Add(ray.position, Vector3Scale(ray.direction, tmin));
  351.   Result.normal := Vector3RotateByQuaternion(normal, obb^.rotation);
  352. end;
  353.  
  354. function CheckCollisionSphereVsOBB(sphereCenter: TVector3; radius: Single; obb: POBB): Boolean;
  355. var
  356.   localCenter, clamped, worldClamped: TVector3;
  357.   invRot: TQuaternion;
  358.   distSq: Single;
  359. begin
  360.   localCenter := Vector3Subtract(sphereCenter, obb^.center);
  361.   invRot := QuaternionInvert(obb^.rotation);
  362.   localCenter := Vector3RotateByQuaternion(localCenter, invRot);
  363.  
  364.   clamped.x := Clamp(localCenter.x, -obb^.halfExtents.x, obb^.halfExtents.x);
  365.   clamped.y := Clamp(localCenter.y, -obb^.halfExtents.y, obb^.halfExtents.y);
  366.   clamped.z := Clamp(localCenter.z, -obb^.halfExtents.z, obb^.halfExtents.z);
  367.  
  368.   worldClamped := Vector3RotateByQuaternion(clamped, obb^.rotation);
  369.   worldClamped := Vector3Add(worldClamped, obb^.center);
  370.  
  371.   distSq := Vector3DistanceSqr(sphereCenter, worldClamped);
  372.   Result := distSq <= radius * radius;
  373. end;
  374.  
  375. end.
  376.  

Lulu

  • Sr. Member
  • ****
  • Posts: 405
Re: Oriented Bounding Box (OBB) support for Raylib
« Reply #1 on: April 10, 2025, 08:11:21 pm »
very nice !  :)
wishing you a nice life!
GitHub repositories https://github.com/Lulu04

 

TinyPortal © 2005-2018