diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..d9061a6 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,72 @@ +name: C++ CI/CD + +on: [push, pull_request] + +jobs: + build-and-test: + runs-on: ubuntu-latest # You can also use windows-latest or macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Google Test (GTest) + run: | + sudo apt-get update + sudo apt-get install libgtest-dev cmake -y + cd /usr/src/gtest + sudo cmake . + sudo make + sudo cp lib/*.a /usr/lib + + - name: Create Build Directory + shell: bash + working-directory: ${{github.workspace}} + run: mkdir build + + - name: Configure CMake + shell: bash + working-directory: ${{github.workspace}}/build + run: cmake .. + + - name: Build Project + shell: bash + working-directory: ${{github.workspace}}/build + run: cmake --build . + + - name: Run Tests + shell: bash + working-directory: ${{github.workspace}}/build + run: ./run_test + + # release: + # needs: build-and-test + # if: github.event_name == 'push' && github.ref == 'refs/heads/master' + # runs-on: ubuntu-latest + + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Create Release + # id: create_release + # uses: actions/create-release@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # tag_name: v${{ github.run_number }} # Example: v1, v2, etc. Or use a more sophisticated versioning + # release_name: Release v${{ github.run_number }} + # body: | + # Automated release based on successful CI/CD build. + # draft: false + # prerelease: false + + # - name: Upload Release Asset + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_path: ${{ github.workspace }}/build/my_app # Replace with the actual path to your build artifact + # asset_name: my_app_${{ github.run_number }} # Name for your release asset + # asset_content_type: application/octet-stream \ No newline at end of file diff --git a/.gitignore b/.gitignore index 259148f..7723faa 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ *.exe *.out *.app + +# CMakes +build + +# IDEs +.vscode \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index b5db16d..81dcc18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,8 +9,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) include_directories( sources sources/math + sources/physic includes includes/math + includes/physic ) file(GLOB_RECURSE SRCS diff --git a/includes/math/geometry.hpp b/includes/math/geometry.hpp index 03b6e4d..e3a5660 100644 --- a/includes/math/geometry.hpp +++ b/includes/math/geometry.hpp @@ -2,11 +2,10 @@ #define GEOMETRY_HPP #include -#include "object.hpp" #include "vectors.hpp" #include "quaternions.hpp" -// class Object; +class Object; enum class GeometryType { @@ -15,6 +14,7 @@ enum class GeometryType class Geometry { + friend class Object; protected: Vec3 offset; Quat4 rotation; @@ -29,8 +29,10 @@ class Geometry const Vec3 &GetLocalOffset() const { return offset; } const Quat4 &GetLocalRotation() const { return rotation; } - Vec3 GetWorldCenter() const { return offset + object->GetPosition(); } - const Quat4 &GetWorldRotation() const { return (rotation * object->GetRotation()).Normalize(); } + Vec3 GetWorldCenter() const; + Quat4 GetWorldRotation() const; + + void SetOwner(Object *obj) { object = obj; } void SetLocalOffset(double _x, double _y, double _z) { @@ -38,9 +40,9 @@ class Geometry this->offset.y = _y; this->offset.z = _z; } - void SetLocalOffset(const Vec3 &new_offset) + void SetLocalOffset(const Vec3 &local_offset) { - this->offset = new_offset; + offset = local_offset; } void SetLocalRotation(double _w, double _x, double _y, double _z) { diff --git a/includes/math/quaternions.hpp b/includes/math/quaternions.hpp index a42703e..4c399df 100644 --- a/includes/math/quaternions.hpp +++ b/includes/math/quaternions.hpp @@ -7,10 +7,14 @@ struct Quat4 { double w, x, y, z; - Quat4() : w(0), x(0), y(0), z(0) {} + Quat4() : w(1), x(0), y(0), z(0) {} Quat4(double _w, Vec3 axis) : w(_w) { - Vec4 temp = Vec4(_w, axis.x, axis.y, axis.z).Normalize(); + Vec4 temp = Vec4(_w, axis.x, axis.y, axis.z); + if (temp.IsZero()) + temp.w = 1; + else + temp.Normalize(); w = temp.w; x = temp.x; y = temp.y; @@ -18,7 +22,11 @@ struct Quat4 } Quat4(double _w, double _x, double _y, double _z) { - Vec4 temp = Vec4(_w, _x, _y, _z).Normalize(); + Vec4 temp = Vec4(_w, _x, _y, _z); + if (temp.IsZero()) + temp.w = 1; + else + temp.Normalize(); w = temp.w; x = temp.x; y = temp.y; @@ -57,7 +65,7 @@ struct Quat4 inline Quat4 operator+(Quat4 l, const Quat4 &r) { return l += r; } inline Quat4 operator*(Quat4 l, const Quat4 &r) { return l *= r; } -inline Vec3 operator*(const Vec3& l, const Quat4 &Q) +inline Vec3 operator*(const Vec3 &l, const Quat4 &Q) { Quat4 P = Quat4(0, l); Quat4 R = Q * P * -Q; diff --git a/includes/math/vectors.hpp b/includes/math/vectors.hpp index 2402f14..57d8bf6 100644 --- a/includes/math/vectors.hpp +++ b/includes/math/vectors.hpp @@ -52,6 +52,10 @@ struct Vec3 } constexpr Vec3 &Normalize() { + if (this->IsZero()) + { + return *this; + } double length = sqrt(this->LengthSquared()); x /= length; y /= length; @@ -149,6 +153,8 @@ struct Vec4 } constexpr Vec4 &Normalize() { + if (this->IsZero()) + return *this; double length = sqrt(this->LengthSquared()); x /= length; y /= length; @@ -156,6 +162,18 @@ struct Vec4 w /= length; return *this; } + bool IsZero() const + { + if (!(-EPSILON <= w && w <= EPSILON)) + return false; + if (!(-EPSILON <= x && x <= EPSILON)) + return false; + if (!(-EPSILON <= y && y <= EPSILON)) + return false; + if (!(-EPSILON <= z && z <= EPSILON)) + return false; + return true; + } }; // Vector Product diff --git a/includes/object.hpp b/includes/object.hpp deleted file mode 100644 index 500ea99..0000000 --- a/includes/object.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef OBJECT_HPP -#define OBJECT_HPP - -#include -#include "vectors.hpp" -#include "quaternions.hpp" -// #include "geometry.hpp" - -class Geometry; - -class Object -{ -private: - double mass; - Quat4 rotation; - Vec3 position; - Vec3 velocity; - - std::unique_ptr geometry; - -public: - Object(double mass, Vec3 position, Quat4 rotation, std::unique_ptr geom) - : mass(mass), position(position), rotation(rotation), geometry(std::move(geom)), velocity(Vec3()) {} - - const double &GetMass() const { return mass; } - const Vec3 &GetVelocity() const { return velocity; } - const Vec3 &GetPosition() const { return position; } - const Quat4 &GetRotation() const { return rotation; } -}; - -#endif \ No newline at end of file diff --git a/includes/physic/engine.hpp b/includes/physic/engine.hpp new file mode 100644 index 0000000..e5f3151 --- /dev/null +++ b/includes/physic/engine.hpp @@ -0,0 +1,24 @@ +#ifndef ENGINE_HPP +#define ENGINE_HPP + +#include "object.hpp" +#include "config.hpp" +#include +class Engine +{ +private: + std::vector objectList; + +public: + // Getting + const Object &GetObject(int id) const + { + return objectList[id]; + } + + // Adding + int AddObject(Object &&obj); // Change signature to take by value + void Step(double dt = 1.0 / FPS); +}; + +#endif \ No newline at end of file diff --git a/includes/physic/force.hpp b/includes/physic/force.hpp new file mode 100644 index 0000000..a75a4e9 --- /dev/null +++ b/includes/physic/force.hpp @@ -0,0 +1,22 @@ +#ifndef FORCE_HPP +#define FORCE_HPP + +#include "vectors.hpp" + +struct Force +{ + Vec3 offset; + Vec3 direction; + + Force() : offset(Vec3()), direction(Vec3()) {} + Force(Vec3 direction) : offset(Vec3()), direction(direction) {} + Force(Vec3 offset, Vec3 direction) : offset(offset), direction(direction) {} + + Force &operator+=(const Force &other) + { + direction += other.direction; + return *this; + } +}; + +#endif \ No newline at end of file diff --git a/includes/physic/object.hpp b/includes/physic/object.hpp new file mode 100644 index 0000000..cf57fd9 --- /dev/null +++ b/includes/physic/object.hpp @@ -0,0 +1,106 @@ +#ifndef OBJECT_HPP +#define OBJECT_HPP + +#include +#include +#include "force.hpp" +#include "vectors.hpp" +#include "quaternions.hpp" + +class Geometry; + +class Object +{ +private: + double mass; + Quat4 rotation; + Vec3 position; + Vec3 velocity; + + std::vector forces; + std::unique_ptr geometry; + + void SetGeometryOwner(); + +public: + Object(double mass, std::unique_ptr geom) + : mass(mass), position(Vec3()), rotation(Quat4()), velocity(Vec3()), geometry(std::move(geom)) { SetGeometryOwner(); } + Object(double mass, Vec3 position, Quat4 rotation, std::unique_ptr geom) + : mass(mass), position(position), rotation(rotation), geometry(std::move(geom)), velocity(Vec3()) { SetGeometryOwner(); } + + double GetMass() const { return mass; } + const Vec3 &GetVelocity() const { return velocity; } + const Vec3 &GetPosition() const { return position; } + const Quat4 &GetRotation() const { return rotation; } + const std::vector &GetForces() const { return forces; } + + void SetMass(double m) + { + this->mass = m > 0 ? m : this->mass; + } + void SetVelocity(double _x, double _y, double _z) + { + this->velocity.x = _x; + this->velocity.y = _y; + this->velocity.z = _z; + } + void SetVelocity(const Vec3 &vector) + { + this->velocity = vector; + } + void SetPosition(double _x, double _y, double _z) + { + this->position.x = _x; + this->position.y = _y; + this->position.z = _z; + } + void SetPosition(const Vec3 &vector) + { + this->position = vector; + } + void SetRotation(double _w, double _x, double _y, double _z) + { + this->rotation.w = _w; + this->rotation.x = _x; + this->rotation.y = _y; + this->rotation.z = _z; + this->rotation.Normalize(); + } + void SetRotation(double w, const Vec3 &axis) + { + this->rotation.w = w; + this->rotation.x = axis.x; + this->rotation.y = axis.y; + this->rotation.z = axis.z; + this->rotation.Normalize(); + } + void SetRotation(const Quat4 &quaternion) + { + this->rotation = quaternion; + this->rotation.Normalize(); + } + + void AddForce(const Force &f) + { + forces.emplace_back(f); + } + void ClearForces() + { + forces.clear(); + } + + // Move constructor + Object(Object &&) noexcept = default; + // Move assignment + Object &operator=(Object &&) noexcept = default; + + // Delete copy constructor and copy assignment + Object(const Object &) = delete; + Object &operator=(const Object &) = delete; + + ~Object(); +}; + +#include "geometry.hpp" + +#endif \ No newline at end of file diff --git a/sources/math/geometry.cpp b/sources/math/geometry.cpp index 196257e..aa3aa54 100644 --- a/sources/math/geometry.cpp +++ b/sources/math/geometry.cpp @@ -14,7 +14,7 @@ void ProjectBox(const Vec3 ¢er, const std::vector &axis, maxProj = s + r; } -bool Overlap(double min1, const double &max1, double min2, const double &max2) +bool Overlap(double min1, double max1, double min2, double max2) { return (max1 >= min2 - EPSILON && max2 >= min1 - EPSILON); } @@ -69,7 +69,7 @@ bool Box::CollidesWith(const Geometry &other) const { if (other.GetType() == GeometryType::BOX) { - const Box* otherBox = dynamic_cast(&other); + const Box *otherBox = dynamic_cast(&other); if (otherBox) return BoxBoxCollision(*this, *otherBox); return false; @@ -87,3 +87,10 @@ std::vector Box::GetNormalAxis() const a.emplace_back(Vec3(0, 0, 1) * this->GetWorldRotation()); return a; } + +Vec3 Geometry::GetWorldCenter() const { return offset + object->GetPosition(); } +Quat4 Geometry::GetWorldRotation() const { + Quat4 result = rotation * object->GetRotation(); + result.Normalize(); + return result; +} \ No newline at end of file diff --git a/sources/physic/engine.cpp b/sources/physic/engine.cpp new file mode 100644 index 0000000..d11a7c6 --- /dev/null +++ b/sources/physic/engine.cpp @@ -0,0 +1,28 @@ +#include "engine.hpp" + +int Engine::AddObject(Object &&obj) // Take by rvalue reference +{ + objectList.emplace_back(std::move(obj)); // Move into vector + return objectList.size() - 1; +} +void Engine::Step(double dt) +{ + Vec3 temp_vector; + + for (Object &obj : objectList) + { + Force accumulated_force; + + // Process motions + for (const Force &f : obj.GetForces()) + accumulated_force += f; + temp_vector = obj.GetVelocity() + (dt * accumulated_force.direction / obj.GetMass()); + obj.SetVelocity(temp_vector); + + // Process rotations + // Apply force + temp_vector = obj.GetPosition() + obj.GetVelocity(); + obj.SetPosition(temp_vector); + obj.ClearForces(); + } +} \ No newline at end of file diff --git a/sources/physic/object.cpp b/sources/physic/object.cpp new file mode 100644 index 0000000..900432f --- /dev/null +++ b/sources/physic/object.cpp @@ -0,0 +1,9 @@ +#include "object.hpp" +#include "geometry.hpp" + +void Object::SetGeometryOwner() +{ + if (geometry) + geometry->SetOwner(this); +} +Object::~Object() = default; diff --git a/tests/test_movements.cpp b/tests/test_movements.cpp new file mode 100644 index 0000000..d22a220 --- /dev/null +++ b/tests/test_movements.cpp @@ -0,0 +1,84 @@ +// filepath: /home/MrUnknown850/Documents/cphysic/tests/test_movements.cpp +#include +#include "physic/engine.hpp" +#include "physic/object.hpp" +#include "math/geometry.hpp" +#include "math/vectors.hpp" +#include + +TEST(EngineTest, ObjectPositionAfterForce) +{ + std::unique_ptr geom = std::make_unique(Vec3(1, 1, 1), nullptr); + Object obj(2.0, std::move(geom)); + + obj.AddForce(Force{Vec3{4, 0, 0}}); + + Engine engine; + int id = engine.AddObject(std::move(obj)); + + // Step the engine with dt = 1.0 + engine.Step(1.0); + + // Get the updated object + const Object &updated = engine.GetObject(id); + + // The acceleration is F/m = (4,0,0)/2 = (2,0,0) + // New velocity = old velocity + a*dt = (0,0,0) + (2,0,0)*1 = (2,0,0) + // New position = old position + velocity = (0,0,0) + (2,0,0) = (2,0,0) + EXPECT_DOUBLE_EQ(updated.GetVelocity().x, 2.0); + EXPECT_DOUBLE_EQ(updated.GetVelocity().y, 0.0); + EXPECT_DOUBLE_EQ(updated.GetVelocity().z, 0.0); + + EXPECT_DOUBLE_EQ(updated.GetPosition().x, 2.0); + EXPECT_DOUBLE_EQ(updated.GetPosition().y, 0.0); + EXPECT_DOUBLE_EQ(updated.GetPosition().z, 0.0); +} + +TEST(EngineTest, MultipleForces) +{ + std::unique_ptr geom = std::make_unique(Vec3(1, 1, 1), nullptr); + Object obj(1.0, std::move(geom)); + + // Apply two forces: (1,0,0) and (0,2,0) + obj.AddForce(Force{Vec3{1, 0, 0}}); + obj.AddForce(Force{Vec3{0, 2, 0}}); + + Engine engine; + int id = engine.AddObject(std::move(obj)); + engine.Step(1.0); + + const Object &updated = engine.GetObject(id); + + // Total force = (1,2,0), mass = 1, acceleration = (1,2,0) + // New velocity = (1,2,0), new position = (1,2,0) + EXPECT_DOUBLE_EQ(updated.GetVelocity().x, 1.0); + EXPECT_DOUBLE_EQ(updated.GetVelocity().y, 2.0); + EXPECT_DOUBLE_EQ(updated.GetVelocity().z, 0.0); + + EXPECT_DOUBLE_EQ(updated.GetPosition().x, 1.0); + EXPECT_DOUBLE_EQ(updated.GetPosition().y, 2.0); + EXPECT_DOUBLE_EQ(updated.GetPosition().z, 0.0); +} + +TEST(EngineTest, NoForceNoMovement) +{ + std::unique_ptr geom = std::make_unique(Vec3(1, 1, 1), nullptr); + Object obj(5.0, std::move(geom)); + obj.SetPosition(1, 2, 3); + obj.SetVelocity(0, 0, 0); + + Engine engine; + int id = engine.AddObject(std::move(obj)); + engine.Step(1.0); + + const Object &updated = engine.GetObject(id); + + // No force, so no movement + EXPECT_DOUBLE_EQ(updated.GetVelocity().x, 0.0); + EXPECT_DOUBLE_EQ(updated.GetVelocity().y, 0.0); + EXPECT_DOUBLE_EQ(updated.GetVelocity().z, 0.0); + + EXPECT_DOUBLE_EQ(updated.GetPosition().x, 1.0); + EXPECT_DOUBLE_EQ(updated.GetPosition().y, 2.0); + EXPECT_DOUBLE_EQ(updated.GetPosition().z, 3.0); +} \ No newline at end of file diff --git a/tests/test_quat4.cpp b/tests/test_quat4.cpp index c41de13..d437bf3 100644 --- a/tests/test_quat4.cpp +++ b/tests/test_quat4.cpp @@ -4,7 +4,7 @@ TEST(Quat4Test, DefaultConstructor) { Quat4 a; - EXPECT_DOUBLE_EQ(a.w, 0); + EXPECT_DOUBLE_EQ(a.w, 1); EXPECT_DOUBLE_EQ(a.x, 0); EXPECT_DOUBLE_EQ(a.y, 0); EXPECT_DOUBLE_EQ(a.z, 0);