From 508c3a8160b592723a66bf9847b168ef698f06bc Mon Sep 17 00:00:00 2001 From: joaomanita Date: Sun, 7 Jul 2024 17:53:44 +0100 Subject: [PATCH] feat(engine): add voxel collision shape --- core/include/cubos/core/geom/box.hpp | 2 +- core/src/geom/intersections.cpp | 1 - engine/CMakeLists.txt | 1 + .../cubos/engine/collisions/shapes/voxel.hpp | 97 +++++ engine/samples/CMakeLists.txt | 1 + .../voxel-shape-collisions/assets/car.grd | Bin 0 -> 10572 bytes .../assets/car.grd.meta | 3 + .../voxel-shape-collisions/assets/main.pal | Bin 0 -> 162 bytes .../assets/main.pal.meta | 3 + .../samples/voxel-shape-collisions/main.cpp | 169 +++++++++ engine/src/assets/asset.cpp | 4 +- engine/src/collisions/interface/plugin.cpp | 2 + .../src/collisions/interface/shapes/voxel.cpp | 10 + engine/src/collisions/narrow_phase/plugin.cpp | 352 +++++++++++++++--- engine/src/collisions/plugin.cpp | 194 ++++++++++ engine/tests/assets/useless.meta | 0 engine/tests/raycast.cpp | 8 + 17 files changed, 785 insertions(+), 62 deletions(-) create mode 100644 engine/include/cubos/engine/collisions/shapes/voxel.hpp create mode 100644 engine/samples/voxel-shape-collisions/assets/car.grd create mode 100644 engine/samples/voxel-shape-collisions/assets/car.grd.meta create mode 100644 engine/samples/voxel-shape-collisions/assets/main.pal create mode 100644 engine/samples/voxel-shape-collisions/assets/main.pal.meta create mode 100644 engine/samples/voxel-shape-collisions/main.cpp create mode 100644 engine/src/collisions/interface/shapes/voxel.cpp create mode 100644 engine/tests/assets/useless.meta diff --git a/core/include/cubos/core/geom/box.hpp b/core/include/cubos/core/geom/box.hpp index f7551239b5..2036c9ec59 100644 --- a/core/include/cubos/core/geom/box.hpp +++ b/core/include/cubos/core/geom/box.hpp @@ -29,7 +29,7 @@ namespace cubos::core::geom } /// @brief Computes four corners of the box, one for each diagonal. - /// @param corners Array to store the three corners in. + /// @param corners Array to store the four corners in. void corners4(glm::vec3 corners[4]) const { corners[0] = {halfSize.x, -halfSize.y, -halfSize.z}; diff --git a/core/src/geom/intersections.cpp b/core/src/geom/intersections.cpp index 1a8ca92ca2..06a00c6d54 100644 --- a/core/src/geom/intersections.cpp +++ b/core/src/geom/intersections.cpp @@ -147,7 +147,6 @@ bool cubos::core::geom::intersects(const Box& box1, const glm::mat4& localToWorl } } } - return true; } diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index e29913c7ee..c9eeea548c 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -73,6 +73,7 @@ set(CUBOS_ENGINE_SOURCE "src/collisions/interface/shapes/box.cpp" "src/collisions/interface/raycast.cpp" "src/collisions/interface/shapes/capsule.cpp" + "src/collisions/interface/shapes/voxel.cpp" "src/collisions/broad_phase/plugin.cpp" "src/collisions/broad_phase/sweep_and_prune.cpp" "src/collisions/broad_phase/potentially_colliding_with.cpp" diff --git a/engine/include/cubos/engine/collisions/shapes/voxel.hpp b/engine/include/cubos/engine/collisions/shapes/voxel.hpp new file mode 100644 index 0000000000..d6e983efe1 --- /dev/null +++ b/engine/include/cubos/engine/collisions/shapes/voxel.hpp @@ -0,0 +1,97 @@ +/// @file +/// @brief Component @ref cubos::engine::VoxelCollisionShape. +/// @ingroup collisions-plugin + +#pragma once + +#include + +#include +#include +#include + +#include +#include +#include + +namespace cubos::engine +{ + /// @brief Component which adds a collision shape corresponding to a given voxel grid to an entity, used with a @ref + /// Collider component. + /// @ingroup collisions-plugin + class CUBOS_ENGINE_API VoxelCollisionShape + { + + public: + CUBOS_REFLECT; + + /// @brief Struct which holds a sub-box of the voxel collision shape, and its shift from the center of the + /// shape. + /// @ingroup collisions-plugin + struct BoxShiftPair + { + cubos::core::geom::Box box; + glm::vec3 shift; + }; + + /// @brief Entities voxel grid. + Asset grid; + + /// @brief Constructs voxel shape with no grid. + VoxelCollisionShape() = default; + + /// @brief Constructs voxel shape with voxel grid. + /// @param grid VoxelGrid given in constructor. + VoxelCollisionShape(Asset grid) + { + setGrid(grid); + } + + /// @brief Default destructor. + ~VoxelCollisionShape() = default; + + /// @brief Move constructor. + /// @param other VoxelCollisionShape to move. + VoxelCollisionShape(VoxelCollisionShape&& other) noexcept + { + this->grid = std::move(other.grid); + this->mBoxes = std::move(other.mBoxes); + } + + /// @brief Copy constructor. + /// @param shape VoxelCollisionSHape to copy. + VoxelCollisionShape(const VoxelCollisionShape& shape) + { + this->grid = shape.grid; + this->mBoxes = shape.mBoxes; + } + + /// @brief Sets the grid. + /// @param grid to set. + void setGrid(Asset& grid) + { + this->grid = grid; + } + + /// @brief Inserts a new @ref BoxShiftPair to the list of the class. + /// @param box Box to insert. + /// @param shift Shift vector of the box. + void insertBox(const cubos::core::geom::Box& box, const glm::vec3& shift) + { + BoxShiftPair pair; + pair.box = box; + pair.shift = shift; + this->mBoxes.push_back(pair); + } + + /// @brief Getter for the list of @ref BoxShiftPair of the class. + std::vector getBoxes() const + { + return this->mBoxes; + } + + private: + /// @brief List of pairs composing the shape. + std::vector mBoxes; ///< List of boxes. + }; +} // namespace cubos::engine diff --git a/engine/samples/CMakeLists.txt b/engine/samples/CMakeLists.txt index d789b5b6b4..c0b746a043 100644 --- a/engine/samples/CMakeLists.txt +++ b/engine/samples/CMakeLists.txt @@ -42,6 +42,7 @@ make_sample(DIR "render/main" ASSETS) make_sample(DIR "render/shadows" ASSETS) make_sample(DIR "imgui") make_sample(DIR "collisions" ASSETS) +make_sample(DIR "voxel-shape-collisions" ASSETS) make_sample(DIR "scene" ASSETS) make_sample(DIR "voxels" ASSETS) make_sample(DIR "gizmos") diff --git a/engine/samples/voxel-shape-collisions/assets/car.grd b/engine/samples/voxel-shape-collisions/assets/car.grd new file mode 100644 index 0000000000000000000000000000000000000000..cfaab6f0511848ed779f352cdbcc5d7df9b4d8c8 GIT binary patch literal 10572 zcmeI2;a0;S4279ix9JaG!S{bkd&%xeLMZgKPOGCmmLwMne1$SOO(}h(l-}pR&-rir zaZ20I?X~{LlBe{vc!Kfsd@_Zt-f6}$-OiW9lM%3bT~J>$NUAKsRMed1k6o5Yq9fDH z4t*4gxQaLBUb6Qwm9jA|k-nO5f4EDV)svy5yq@QgD*Yurr6LhgHR`RsYOefnmo_U= zJqNHFB;$iTFO0f#&U#MJJxRn=_1cs*QAZ`eFzi^R$#fl|T)26;?|TyMP)797AVld)4tjkRIrp$?m&M)V*@T$I_S#+cR{JmgjWr^ozb9k; zxPz3H)aO{IMiPwRZnR_CSL^*jw0{heBo0=OMzS`y!Gmu?vhnFUb&2J1$K>TU%Y6%{ z+wUvR@=}#If+u7>hsHa3^?$>5N=PgoOpV;eEzm8b1zoyvS-bUHjB1WLLkNv+Vl_KfrnDiPw zw1{?0RpVZ}q?*$cupVl*zPC}Yk5~^Xxy2M}`{o{ literal 0 HcmV?d00001 diff --git a/engine/samples/voxel-shape-collisions/assets/car.grd.meta b/engine/samples/voxel-shape-collisions/assets/car.grd.meta new file mode 100644 index 0000000000..c7afcf5e7c --- /dev/null +++ b/engine/samples/voxel-shape-collisions/assets/car.grd.meta @@ -0,0 +1,3 @@ +{ + "id": "059c16e7-a439-44c7-9bdc-6e069dba0c75" +} \ No newline at end of file diff --git a/engine/samples/voxel-shape-collisions/assets/main.pal b/engine/samples/voxel-shape-collisions/assets/main.pal new file mode 100644 index 0000000000000000000000000000000000000000..e9433b867c154d485b415e97c98b3fd2ec3cd373 GIT binary patch literal 162 zcmZSJvU~C3B@Ej)FfiC#?AQs%Ab!r9w{Q&P+c_LKXfH8iroGRZvmo_gK8RiO=Py`4 oP(1?_faNCpJJcfow7zU{a09uwmA^-pY literal 0 HcmV?d00001 diff --git a/engine/samples/voxel-shape-collisions/assets/main.pal.meta b/engine/samples/voxel-shape-collisions/assets/main.pal.meta new file mode 100644 index 0000000000..509d07f886 --- /dev/null +++ b/engine/samples/voxel-shape-collisions/assets/main.pal.meta @@ -0,0 +1,3 @@ +{ + "id": "1aa5e234-28cb-4386-99b4-39386b0fc215" +} \ No newline at end of file diff --git a/engine/samples/voxel-shape-collisions/main.cpp b/engine/samples/voxel-shape-collisions/main.cpp new file mode 100644 index 0000000000..49199534c5 --- /dev/null +++ b/engine/samples/voxel-shape-collisions/main.cpp @@ -0,0 +1,169 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using cubos::core::geom::Box; +using cubos::core::io::Key; +using cubos::core::io::Modifiers; +using cubos::core::io::MouseButton; + +using namespace cubos::engine; + +static CUBOS_DEFINE_TAG(collisionsSampleUpdated); + +struct State +{ + CUBOS_ANONYMOUS_REFLECT(State); + + bool collided = false; + + Entity a; + Entity b; + + glm::vec3 aRotationAxis; + glm::vec3 bRotationAxis; +}; + +/// [Get handles to assets] +static const Asset CarAsset = AnyAsset("059c16e7-a439-44c7-9bdc-6e069dba0c75"); +static const Asset PaletteAsset = AnyAsset("1aa5e234-28cb-4386-99b4-39386b0fc215"); +/// [Get handles to assets] + +int main() +{ + auto cubos = Cubos(); + + cubos.plugin(defaultsPlugin); + cubos.tag(gizmosDrawTag).after(toneMappingTag); + + cubos.resource(); + + cubos.startupSystem("setup camera").call([](Commands commands) { + auto targetEnt = commands.create().add(RenderTargetDefaults{}).add(GizmosTarget{}).entity(); + commands.create() + .relatedTo(targetEnt, DrawsTo{}) + .add(Camera{.zNear = 0.1F, .zFar = 100.0F}) + .add(PerspectiveCamera{.fovY = 60.0F}) + .add(LocalToWorld{}) + .add(Position{{-35.0F, 1.5F, 0.0F}}) + .add(Rotation::lookingAt({3.0F, -1.0F, 0.0F}, glm::vec3{0.0F, 1.0F, 0.0F})); + }); + + cubos.startupSystem("configure Assets").tagged(settingsTag).call([](Settings& settings) { + settings.setString("assets.io.path", SAMPLE_ASSETS_FOLDER); + }); + + /// [Set palette] + cubos.startupSystem("set palette").call([](RenderPalette& palette) { palette.asset = PaletteAsset; }); + /// [Set palette] + + cubos.startupSystem("create colliders").tagged(assetsTag).call([](State& state, Commands commands, Assets& assets) { + auto car = assets.read(CarAsset); + glm::vec3 offset = glm::vec3(car->size().x, car->size().y, car->size().z) / -2.0F; + state.a = commands.create() + .add(Collider{}) + .add(RenderVoxelGrid{CarAsset, offset}) + .add(VoxelCollisionShape(CarAsset)) + .add(LocalToWorld{}) + .add(Position{glm::vec3{0.0F, 0.0F, -30.0F}}) + .add(Rotation{}) + .add(PhysicsBundle{.mass = 500.0F, .velocity = {0.0F, 0.0F, 1.0F}}) + .entity(); + state.aRotationAxis = glm::sphericalRand(1.0F); + + state.b = commands.create() + .add(Collider{}) + .add(RenderVoxelGrid{CarAsset, offset}) + .add(VoxelCollisionShape(CarAsset)) + .add(LocalToWorld{}) + .add(Position{glm::vec3{0.0F, 0.0F, 10.0F}}) + .add(Rotation{}) + .add(PhysicsBundle{.mass = 500.0F, .velocity = {0.0F, 0.0F, -1.0F}}) + .entity(); + state.bRotationAxis = glm::sphericalRand(1.0F); + }); + + cubos.system("move colliders") + .before(transformUpdateTag) + .call([](State& state, Query query) { + auto [aPos, aRot, aVel] = *query.at(state.a); + auto [bPos, bRot, bVel] = *query.at(state.b); + + aRot.quat = glm::rotate(aRot.quat, 0.001F, state.aRotationAxis); + aVel.vec += glm::vec3{0.0F, 0.0F, 0.01F}; + + bRot.quat = glm::rotate(bRot.quat, 0.001F, state.bRotationAxis); + bVel.vec -= glm::vec3{0.0F, 0.0F, 0.01F}; + }); + + cubos.tag(collisionsSampleUpdated); + + cubos.system("render voxel") + .after(collisionsSampleUpdated) + .call([](Gizmos& gizmos, Query query) { + for (auto [localToWorld, collider, shape] : query) + { + for (const auto box : shape.getBoxes()) + { + // Get the current position from the localToWorld matrix + glm::mat4 pos = localToWorld.mat; // Store the matrix + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix = glm::translate(glm::mat4(1.0F), -box.shift); + + // Combine the matrices (note: order matters) + pos = pos * shiftMatrix; + auto size = box.box.halfSize * 2.0F; + glm::mat4 transform = glm::scale(pos * collider.transform, size); + gizmos.drawWireBox("subboxes", transform); + } + } + }); + + cubos.system("render") + .after(collisionsSampleUpdated) + .call([](Gizmos& gizmos, Query query) { + for (auto [localToWorld, collider] : query) + { + auto size = collider.localAABB.box().halfSize * 2.0F; + glm::mat4 transform = glm::scale(localToWorld.mat * collider.transform, size); + gizmos.color({1.0F, 1.0F, 1.0F}); + gizmos.drawWireBox("local AABB", transform); + + gizmos.color({1.0F, 0.0F, 0.0F}); + gizmos.drawWireBox("world AABB", collider.worldAABB.min(), collider.worldAABB.max()); + } + }); + + cubos.run(); + return 0; +} diff --git a/engine/src/assets/asset.cpp b/engine/src/assets/asset.cpp index c7ab5f4f2d..73cfce0cd4 100644 --- a/engine/src/assets/asset.cpp +++ b/engine/src/assets/asset.cpp @@ -63,7 +63,7 @@ AnyAsset::AnyAsset(const AnyAsset& other) } AnyAsset::AnyAsset(AnyAsset&& other) noexcept - : pathOrId(other.pathOrId) + : pathOrId(std::move(other.pathOrId)) , mId(other.mId) , mRefCount(other.mRefCount) , mVersion(other.mVersion) @@ -95,7 +95,7 @@ AnyAsset& AnyAsset::operator=(AnyAsset&& other) noexcept } this->decRef(); - pathOrId = other.pathOrId; + pathOrId = std::move(other.pathOrId); mId = other.mId; mRefCount = other.mRefCount; mVersion = other.mVersion; diff --git a/engine/src/collisions/interface/plugin.cpp b/engine/src/collisions/interface/plugin.cpp index ce5e03afa9..c82a3b71db 100644 --- a/engine/src/collisions/interface/plugin.cpp +++ b/engine/src/collisions/interface/plugin.cpp @@ -5,12 +5,14 @@ #include #include #include +#include void cubos::engine::interfaceCollisionsPlugin(Cubos& cubos) { cubos.component(); cubos.component(); cubos.component(); + cubos.component(); cubos.relation(); cubos.relation(); diff --git a/engine/src/collisions/interface/shapes/voxel.cpp b/engine/src/collisions/interface/shapes/voxel.cpp new file mode 100644 index 0000000000..0d97379917 --- /dev/null +++ b/engine/src/collisions/interface/shapes/voxel.cpp @@ -0,0 +1,10 @@ +#include + +#include + +CUBOS_REFLECT_IMPL(cubos::engine::VoxelCollisionShape) +{ + return core::ecs::TypeBuilder("cubos::engine::VoxelCollisionShape") + .withField("grid", &VoxelCollisionShape::grid) + .build(); +} diff --git a/engine/src/collisions/narrow_phase/plugin.cpp b/engine/src/collisions/narrow_phase/plugin.cpp index 01dc8c6d9d..0902627f9b 100644 --- a/engine/src/collisions/narrow_phase/plugin.cpp +++ b/engine/src/collisions/narrow_phase/plugin.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,90 @@ CUBOS_DEFINE_TAG(cubos::engine::collisionsNarrowCleanTag); CUBOS_DEFINE_TAG(cubos::engine::collisionsNarrowTag); CUBOS_DEFINE_TAG(cubos::engine::collisionsManifoldTag); +using cubos::engine::CollidingWith; +using cubos::engine::ContactPointData; +using cubos::engine::LocalToWorld; + +std::vector computePoints(const cubos::core::geom::Box* matchedShape1, + const LocalToWorld* matchedLocalToWorld1, + const cubos::core::geom::Box* matchedShape2, + const LocalToWorld* matchedLocalToWorld2, CollidingWith& collidingWith) +{ + // Calculate incident and reference face + std::vector polygon1; + std::vector polygon2; + glm::vec3 normal1; + glm::vec3 normal2; + std::vector adjPlanes1; + std::vector adjPlanes2; + getIncidentReferencePolygon(*matchedShape1, collidingWith.normal, polygon1, normal1, adjPlanes1, + matchedLocalToWorld1->mat, matchedLocalToWorld1->worldScale()); + getIncidentReferencePolygon(*matchedShape2, -collidingWith.normal, polygon2, normal2, adjPlanes2, + matchedLocalToWorld2->mat, matchedLocalToWorld2->worldScale()); + + // Each face will always have more than 1 point so we proceed to clipping + // See which one of the normals is the reference one by checking which has the highest dot product + bool flipped = fabs(glm::dot(collidingWith.normal, normal1)) < fabs(glm::dot(collidingWith.normal, normal2)); + + if (flipped) + { + std::swap(polygon1, polygon2); + std::swap(normal1, normal2); + std::swap(adjPlanes1, adjPlanes2); + } + + // Clip the incident face to the adjacent edges of the reference face + polygon2 = cubos::core::geom::sutherlandHodgmanClipping(polygon2, (int)adjPlanes1.size(), adjPlanes1.data(), false); + + // Finally clip (and remove) any contact points that are above the reference face + cubos::core::geom::Plane refPlane = + cubos::core::geom::Plane{.normal = -normal1, .d = -glm::dot(-normal1, polygon1.front())}; + polygon2 = cubos::core::geom::sutherlandHodgmanClipping(polygon2, 1, &refPlane, true); + + // Use the remaining contact point on the manifold + std::vector points; + + for (const glm::vec3& point : polygon2) + { + // Compute distance to reference plane + glm::vec3 pointDiff = point - cubos::core::geom::getClosestPointPolygon(point, polygon1); + float contactPenetration = -glm::dot(pointDiff, collidingWith.normal); // is this positive + + // Set Contact data + glm::vec3 globalOn1 = point; // world coordinates + glm::vec3 globalOn2 = point + collidingWith.normal * contactPenetration; // world coordinates + + // If we flipped incident and reference planes, we will + // need to flip it back before sending it to the manifold. + if (flipped) + { + contactPenetration = -contactPenetration; + globalOn1 = point - collidingWith.normal * contactPenetration; + globalOn2 = point; + } + + glm::vec3 localOn1 = glm::vec3(matchedLocalToWorld1->inverse() * glm::vec4(globalOn1, 1.0F)); + glm::vec3 localOn2 = glm::vec3(matchedLocalToWorld2->inverse() * glm::vec4(globalOn2, 1.0F)); + + // Just make a final sanity check that the contact point + // is actual a point of contact not just a clipping bug + // and consider only points with positive penetration + if (contactPenetration >= 0.0F) + { + auto contact = ContactPointData{.entity = collidingWith.entity, + .globalOn1 = globalOn1, + .globalOn2 = globalOn2, + .localOn1 = localOn1, + .localOn2 = localOn2, + .penetration = collidingWith.penetration, + .id = 0}; + + points.push_back(contact); + } + } + return points; +} + void cubos::engine::narrowPhaseCollisionsPlugin(Cubos& cubos) { cubos.depends(transformPlugin); @@ -113,78 +198,229 @@ void cubos::engine::narrowPhaseCollisionsPlugin(Cubos& cubos) std::swap(matchedLocalToWorld1, matchedLocalToWorld2); } - // Calculate incident and reference face - std::vector polygon1; - std::vector polygon2; - glm::vec3 normal1; - glm::vec3 normal2; - std::vector adjPlanes1; - std::vector adjPlanes2; - getIncidentReferencePolygon(*matchedShape1, collidingWith.normal, polygon1, normal1, adjPlanes1, - matchedLocalToWorld1->mat, matchedLocalToWorld1->worldScale()); - getIncidentReferencePolygon(*matchedShape2, -collidingWith.normal, polygon2, normal2, adjPlanes2, - matchedLocalToWorld2->mat, matchedLocalToWorld2->worldScale()); - - // Each face will always have more than 1 point so we proceed to clipping - // See which one of the normals is the reference one by checking which has the highest dot product - bool flipped = - fabs(glm::dot(collidingWith.normal, normal1)) < fabs(glm::dot(collidingWith.normal, normal2)); - - if (flipped) + auto points = computePoints(matchedShape1, matchedLocalToWorld1, matchedShape2, matchedLocalToWorld2, + collidingWith); + + cmds.relate( + ent1, ent2, + ContactManifold{.entity = collidingWith.entity, .normal = collidingWith.normal, .points = points}); + } + }); + + cubos.system("find colliding voxel-box pairs") + .tagged(collisionsNarrowTag) + .after(collisionsBroadTag) + .call( + [](Commands cmds, + Query + query) { + for (auto [ent1, position1, localToWorld1, boxShape, potentiallyCollidingWith, ent2, position2, + localToWorld2, voxelShape] : query) { - std::swap(polygon1, polygon2); - std::swap(normal1, normal2); - std::swap(adjPlanes1, adjPlanes2); + cubos::core::geom::Intersection intersectionInfo{}; + for (const auto box : voxelShape.getBoxes()) + { + // Get the current position from the localToWorld matrix + glm::mat4 pos2 = localToWorld2.mat; // Store the matrix + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix = glm::translate(glm::mat4(1.0F), -box.shift); + + pos2 = pos2 * shiftMatrix; + + bool intersects = cubos::core::geom::intersects(boxShape.box, localToWorld1.mat, box.box, pos2, + intersectionInfo); + if (!intersects) + { + continue; + } + cmds.relate(ent1, ent2, + CollidingWith{.entity = ent1, + .entity1OriginalPosition = position1.vec, + .entity2OriginalPosition = position2.vec, + .penetration = intersectionInfo.penetration, + .position = {0.0F, 0.0F, 0.0F}, + .normal = intersectionInfo.normal}); + } } + }); - // Clip the incident face to the adjacent edges of the reference face - polygon2 = cubos::core::geom::sutherlandHodgmanClipping(polygon2, (int)adjPlanes1.size(), - adjPlanes1.data(), false); + cubos.system("find colliding voxel-voxel pairs") + .tagged(collisionsNarrowTag) + .after(collisionsBroadTag) + .call([](Commands cmds, Query + query) { + for (auto [ent1, position1, localToWorld1, voxelShape1, potentiallyCollidingWith, ent2, position2, + localToWorld2, voxelShape2] : query) + { + cubos::core::geom::Intersection intersectionInfo{}; + intersectionInfo.normal = glm::vec3(0.0F); + intersectionInfo.penetration = std::numeric_limits::infinity(); - // Finally clip (and remove) any contact points that are above the reference face - cubos::core::geom::Plane refPlane = - cubos::core::geom::Plane{.normal = -normal1, .d = -glm::dot(-normal1, polygon1.front())}; - polygon2 = cubos::core::geom::sutherlandHodgmanClipping(polygon2, 1, &refPlane, true); + for (const auto box1 : voxelShape1.getBoxes()) + { + // Get the current position from the localToWorld matrix + glm::mat4 pos1 = localToWorld1.mat; // Store the matrix - // Use the remaining contact point on the manifold - std::vector points; + // Create a translation matrix for the shift + glm::mat4 shiftMatrix = glm::translate(glm::mat4(1.0F), -box1.shift); + + pos1 = pos1 * shiftMatrix; + + bool intersects = false; + for (const auto box2 : voxelShape2.getBoxes()) + { + // Get the current position from the localToWorld matrix + glm::mat4 pos2 = localToWorld2.mat; // Store the matrix + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix = glm::translate(glm::mat4(1.0F), -box2.shift); + + pos2 = pos2 * shiftMatrix; + + intersects = cubos::core::geom::intersects(box1.box, pos1, box2.box, pos2, intersectionInfo); + + if (intersects) + { + cmds.relate(ent1, ent2, + CollidingWith{.entity = ent1, + .entity1OriginalPosition = position1.vec, + .entity2OriginalPosition = position2.vec, + .penetration = intersectionInfo.penetration, + .position = {0.0F, 0.0F, 0.0F}, + .normal = intersectionInfo.normal}); + } + } + } + } + }); - for (const glm::vec3& point : polygon2) + cubos.system("collision manifolds voxel-box") + .tagged(collisionsManifoldTag) + .after(collisionsNarrowTag) + .call([](Commands cmds, Query + query) { + for (auto [ent1, localToWorld1, boxShape, collidingWith, ent2, localToWorld2, voxelShape] : query) + { + // If penetration not bigger than 0 continue + if (collidingWith.penetration < 0) { - // Compute distance to reference plane - glm::vec3 pointDiff = point - cubos::core::geom::getClosestPointPolygon(point, polygon1); - float contactPenetration = -glm::dot(pointDiff, collidingWith.normal); // is this positive + continue; + } - // Set Contact data - glm::vec3 globalOn1 = point; // world coordinates - glm::vec3 globalOn2 = point + collidingWith.normal * contactPenetration; // world coordinates + std::vector points; + // Make sure that shape1 corresponds to the entity refered to in collidingWith + if (ent1 == collidingWith.entity) + { + const cubos::core::geom::Box* matchedShape1 = &boxShape.box; + const LocalToWorld* matchedLocalToWorld1 = &localToWorld1; + for (auto box : voxelShape.getBoxes()) + { + const cubos::core::geom::Box* matchedShape2 = &box.box; + LocalToWorld matchedLocalToWorld2 = localToWorld2; + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix = glm::translate(glm::mat4(1.0F), -box.shift); + matchedLocalToWorld2.mat *= shiftMatrix; - // If we flipped incident and reference planes, we will - // need to flip it back before sending it to the manifold. - if (flipped) + auto newPoints = computePoints(matchedShape1, matchedLocalToWorld1, matchedShape2, + &matchedLocalToWorld2, collidingWith); + points.insert(points.end(), newPoints.begin(), newPoints.end()); + } + } + else + { + const cubos::core::geom::Box* matchedShape2 = &boxShape.box; + const LocalToWorld* matchedLocalToWorld2 = &localToWorld1; + for (auto box : voxelShape.getBoxes()) { - contactPenetration = -contactPenetration; - globalOn1 = point - collidingWith.normal * contactPenetration; - globalOn2 = point; + const cubos::core::geom::Box* matchedShape1 = &box.box; + LocalToWorld matchedLocalToWorld1 = localToWorld2; + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix = glm::translate(glm::mat4(1.0F), -box.shift); + matchedLocalToWorld1.mat *= shiftMatrix; + + auto newPoints = computePoints(matchedShape1, &matchedLocalToWorld1, matchedShape2, + matchedLocalToWorld2, collidingWith); + points.insert(points.end(), newPoints.begin(), newPoints.end()); } + } + + cmds.relate( + ent1, ent2, + ContactManifold{.entity = collidingWith.entity, .normal = collidingWith.normal, .points = points}); + } + }); - glm::vec3 localOn1 = glm::vec3(matchedLocalToWorld1->inverse() * glm::vec4(globalOn1, 1.0F)); - glm::vec3 localOn2 = glm::vec3(matchedLocalToWorld2->inverse() * glm::vec4(globalOn2, 1.0F)); + cubos.system("collision manifolds voxel-voxel") + .tagged(collisionsManifoldTag) + .after(collisionsNarrowTag) + .call([](Commands cmds, Query + query) { + for (auto [ent1, localToWorld1, voxelShape1, collidingWith, ent2, localToWorld2, voxelShape2] : query) + { + // If penetration not bigger than 0 continue + if (collidingWith.penetration < 0) + { + continue; + } - // Just make a final sanity check that the contact point - // is actual a point of contact not just a clipping bug - // and consider only points with positive penetration - if (contactPenetration >= 0.0F) + std::vector points; + // Make sure that shape1 corresponds to the entity refered to in collidingWith + if (ent1 == collidingWith.entity) + { + for (auto box1 : voxelShape1.getBoxes()) + { + const cubos::core::geom::Box* matchedShape1 = &box1.box; + LocalToWorld matchedLocalToWorld1 = localToWorld1; + // Create a translation matrix for the shift + glm::mat4 shiftMatrix1 = glm::translate(glm::mat4(1.0F), -box1.shift); + matchedLocalToWorld1.mat *= shiftMatrix1; + + for (auto box2 : voxelShape2.getBoxes()) + { + const cubos::core::geom::Box* matchedShape2 = &box2.box; + LocalToWorld matchedLocalToWorld2 = localToWorld2; + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix2 = glm::translate(glm::mat4(1.0F), -box2.shift); + matchedLocalToWorld2.mat *= shiftMatrix2; + + auto newPoints = computePoints(matchedShape1, &matchedLocalToWorld1, matchedShape2, + &matchedLocalToWorld2, collidingWith); + points.insert(points.end(), newPoints.begin(), newPoints.end()); + } + } + } + else + { + for (auto box1 : voxelShape2.getBoxes()) { - auto contact = ContactPointData{.entity = collidingWith.entity, - .globalOn1 = globalOn1, - .globalOn2 = globalOn2, - .localOn1 = localOn1, - .localOn2 = localOn2, - .penetration = collidingWith.penetration, - .id = 0}; - - points.push_back(contact); + const cubos::core::geom::Box* matchedShape1 = &box1.box; + LocalToWorld matchedLocalToWorld1 = localToWorld1; + // Create a translation matrix for the shift + glm::mat4 shiftMatrix1 = glm::translate(glm::mat4(1.0F), -box1.shift); + matchedLocalToWorld1.mat *= shiftMatrix1; + + for (auto box2 : voxelShape1.getBoxes()) + { + const cubos::core::geom::Box* matchedShape2 = &box2.box; + LocalToWorld matchedLocalToWorld2 = localToWorld2; + + // Create a translation matrix for the shift + glm::mat4 shiftMatrix2 = glm::translate(glm::mat4(1.0F), -box2.shift); + matchedLocalToWorld2.mat *= shiftMatrix2; + + auto newPoints = computePoints(matchedShape1, &matchedLocalToWorld1, matchedShape2, + &matchedLocalToWorld2, collidingWith); + points.insert(points.end(), newPoints.begin(), newPoints.end()); + } } } diff --git a/engine/src/collisions/plugin.cpp b/engine/src/collisions/plugin.cpp index 5746dbe5d9..fd383fa1f9 100644 --- a/engine/src/collisions/plugin.cpp +++ b/engine/src/collisions/plugin.cpp @@ -1,19 +1,186 @@ #include "interface/plugin.hpp" +#include + +#include #include #include #include #include +#include #include +#include #include "broad_phase/plugin.hpp" #include "narrow_phase/plugin.hpp" CUBOS_DEFINE_TAG(cubos::engine::collisionsTag); +using namespace std; + +inline void voxelGridToAABB(const cubos::engine::VoxelGrid& grid, cubos::core::geom::AABB& aabb) +{ + glm::ivec3 size = grid.size(); + glm::ivec3 min = glm::ivec3({size.x - 1, size.y - 1, size.z - 1}); + glm::ivec3 max = glm::ivec3({0, 0, 0}); + + for (int x = 0; x < size.x; x++) + { + for (int y = 0; y < size.y; y++) + { + for (int z = 0; z < size.z; z++) + { + if (grid.get({x, y, z}) != 0U) + { + min = glm::min(min, glm::ivec3({x, y, z})); + max = glm::max(max, glm::ivec3({x, y, z})); + } + } + } + } + max = glm::ivec3(max.x + 1, max.y + 1, max.z + 1); + glm::vec3 dist = glm::vec3(max) - glm::vec3(min); + aabb.min({-dist.x / 2.0F, -dist.y / 2.0F, -dist.z / 2.0F}); + aabb.max({dist.x / 2.0F, dist.y / 2.0F, dist.z / 2.0F}); +} + +pair findLargestBox(const cubos::engine::VoxelGrid& grid, + vector>>& processed, const glm::uvec3& start) +{ + glm::uvec3 gridSize = grid.size(); + glm::uvec3 end = start; + + // Expand X axis + while (end.x + 1 < gridSize.x) + { + bool canExpand = true; + for (uint y = start.y; y <= end.y; y++) + { + for (uint z = start.z; z <= end.z; z++) + { + // Convert to ivec3 when calling grid.get + glm::ivec3 pos(static_cast(end.x) + 1, static_cast(y), static_cast(z)); + if (grid.get(pos) == 0U || processed.at(static_cast(end.x) + 1).at(y).at(z)) + { + canExpand = false; + break; + } + } + if (!canExpand) + { + break; // Break outer loop if not expandable + } + } + if (canExpand) + { + end.x++; + } + else + { + break; // Exit while loop if cannot expand + } + } + + // Expand Y axis + while (end.y + 1 < gridSize.y) + { + bool canExpand = true; + for (uint x = start.x; x <= end.x; x++) + { + for (uint z = start.z; z <= end.z; z++) + { + glm::ivec3 pos(static_cast(x), static_cast(end.y) + 1, static_cast(z)); + if (grid.get(pos) == 0U || processed.at(x).at(static_cast(end.y) + 1).at(z)) + { + canExpand = false; + break; + } + } + if (!canExpand) + { + break; // Break outer loop if not expandable + } + } + if (canExpand) + { + end.y++; + } + else + { + break; // Exit while loop if cannot expand + } + } + + // Expand Z axis + while (end.z + 1 < gridSize.z) + { + bool canExpand = true; + for (uint x = start.x; x <= end.x; x++) + { + for (uint y = start.y; y <= end.y; y++) + { + glm::ivec3 pos(static_cast(x), static_cast(y), static_cast(end.z) + 1); + if (grid.get(pos) == 0U || processed.at(x).at(y).at(static_cast(end.z) + 1)) + { + canExpand = false; + break; + } + } + if (!canExpand) + { + break; // Break outer loop if not expandable + } + } + if (canExpand) + { + end.z++; + } + else + { + break; // Exit while loop if cannot expand + } + } + + // Mark voxels within the box as processed + for (uint x = start.x; x <= end.x; x++) + { + for (uint y = start.y; y <= end.y; y++) + { + for (uint z = start.z; z <= end.z; z++) + { + processed[x][y][z] = true; + } + } + } + + return std::make_pair(start, end); +} + +vector> greedy3dMeshing(const cubos::engine::VoxelGrid& grid) +{ + const glm::uvec3 gridSize = grid.size(); + + // Vector of processed voxels + vector>> processed(gridSize.x, + vector>(gridSize.y, vector(gridSize.z, false))); + + // Vector with min and max of boxes found + vector> boxes; + + for (uint x = 0; x < gridSize.x; x++) + for (uint y = 0; y < gridSize.y; y++) + for (uint z = 0; z < gridSize.z; z++) + if (grid.get({x, y, z}) != 0U && !processed[x][y][z]) + { + boxes.push_back(findLargestBox(grid, processed, {x, y, z})); + } + return boxes; +} + void cubos::engine::collisionsPlugin(Cubos& cubos) { cubos.depends(transformPlugin); + cubos.depends(assetsPlugin); cubos.plugin(interfaceCollisionsPlugin); cubos.plugin(broadPhaseCollisionsPlugin); @@ -42,4 +209,31 @@ void cubos::engine::collisionsPlugin(Cubos& cubos) cubos.observer("setup Capsule Colliders").onAdd().call(initializeCapsuleColliders); cubos.observer("setup Capsule Colliders").onAdd().call(initializeCapsuleColliders); + + auto initializeVoxelColliders = [](const Assets& assets, Query query) { + for (auto [shape, collider] : query) + { + // load the voxel grid + const VoxelGrid& grid = assets.read(shape.grid).get(); + // set the local AABB + voxelGridToAABB(grid, collider.localAABB); + collider.margin = 0.0F; + + glm::uvec3 size = grid.size(); + glm::vec3 center = glm::vec3(size) / 2.0F; + auto boxCoords = greedy3dMeshing(grid); + + for (auto corners : boxCoords) + { + cubos::core::geom::Box box; + auto boxCenter = glm::vec3(glm::ivec3(corners.first + corners.second) + glm::ivec3(1, 1, 1)) / 2.0F; + glm::vec3 shift = center - boxCenter; + box.halfSize = glm::vec3(glm::ivec3(corners.second - corners.first) + glm::ivec3(1, 1, 1)) / 2.0F; + shape.insertBox(box, shift); + } + } + }; + + cubos.observer("setup Voxel Colliders").onAdd().call(initializeVoxelColliders); + cubos.observer("setup Voxel Colliders").onAdd().call(initializeVoxelColliders); } diff --git a/engine/tests/assets/useless.meta b/engine/tests/assets/useless.meta new file mode 100644 index 0000000000..e69de29bb2 diff --git a/engine/tests/raycast.cpp b/engine/tests/raycast.cpp index ef0c5e6c5e..29a6ff1617 100644 --- a/engine/tests/raycast.cpp +++ b/engine/tests/raycast.cpp @@ -2,10 +2,12 @@ #include +#include #include #include #include #include +#include #include #include #include @@ -19,10 +21,16 @@ using namespace cubos::engine; TEST_CASE("cubos::engine::Raycast") { Cubos cubos{}; + cubos.plugin(settingsPlugin); cubos.plugin(transformPlugin); cubos.plugin(fixedStepPlugin); + cubos.plugin(assetsPlugin); cubos.plugin(collisionsPlugin); + cubos.startupSystem("configure Assets").tagged(settingsTag).call([](Settings& settings) { + settings.setBool("assets.io.enable", false); + }); + SUBCASE("get the right scalar from multiple boxes") { cubos.startupSystem("create resources").call([](Commands cmds) {