Skip to content

Commit f7b8b73

Browse files
committed
Add basic physics demo
1 parent e31d211 commit f7b8b73

File tree

5 files changed

+224
-7
lines changed

5 files changed

+224
-7
lines changed

src/components/physics.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ namespace components {
1818
struct AngularAcceleration {
1919
glm::vec3 value;
2020
};
21+
22+
// Box collider for collision detection
23+
struct BoxCollider {
24+
glm::vec3 halfExtents;
25+
};
2126
};

src/plugins/physics/physics.cpp

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,42 @@
1010
#include <glm/gtc/quaternion.hpp>
1111
#define GLM_ENABLE_EXPERIMENTAL
1212
#include <glm/gtx/quaternion.hpp>
13+
#include <vector>
14+
#include <print>
15+
16+
bool plugins::Physics::checkAABBCollision(const glm::vec3& pos1, const glm::vec3& halfExtents1, const glm::vec3& pos2,
17+
const glm::vec3& halfExtents2, glm::vec3& penetrationDepth) {
18+
// Calculate the distance between centers
19+
glm::vec3 distance = pos1 - pos2;
20+
21+
// Calculate the sum of half extents
22+
glm::vec3 totalHalfExtents = halfExtents1 + halfExtents2;
23+
24+
// Check for overlap on all axes
25+
glm::vec3 overlap = totalHalfExtents - glm::abs(distance);
26+
27+
// If any axis has no overlap, there's no collision
28+
if (overlap.x <= 0.0f || overlap.y <= 0.0f || overlap.z <= 0.0f) {
29+
return false;
30+
}
31+
32+
// Find the axis with minimum overlap (this is our separation axis)
33+
if (overlap.x <= overlap.y && overlap.x <= overlap.z) {
34+
penetrationDepth = glm::vec3(overlap.x * (distance.x < 0 ? -1.0f : 1.0f), 0.0f, 0.0f);
35+
} else if (overlap.y <= overlap.z) {
36+
penetrationDepth = glm::vec3(0.0f, overlap.y * (distance.y < 0 ? -1.0f : 1.0f), 0.0f);
37+
} else {
38+
penetrationDepth = glm::vec3(0.0f, 0.0f, overlap.z * (distance.z < 0 ? -1.0f : 1.0f));
39+
}
40+
41+
return true;
42+
}
1343

1444
void plugins::Physics::build(Game& game) {
1545
game.addSystem(Schedule::Update, [](std::shared_ptr<entt::registry>& registry, resources::Time& time) {
1646
float deltaTime = time.deltaTime;
1747

48+
// Update velocities first
1849
registry->view<components::AngularVelocity, components::AngularAcceleration>().each(
1950
[&registry, deltaTime](entt::entity entity, auto& angularVelocity, const auto& angularAcceleration) {
2051
angularVelocity.value += angularAcceleration.value * deltaTime;
@@ -27,6 +58,15 @@ void plugins::Physics::build(Game& game) {
2758
registry->replace<components::Velocity>(entity, velocity);
2859
});
2960

61+
// Store original positions for collision detection
62+
auto colliderView = registry->view<components::Position, components::BoxCollider>();
63+
std::vector<std::pair<entt::entity, glm::vec3>> originalPositions;
64+
65+
for (auto entity : colliderView) {
66+
auto& pos = colliderView.get<components::Position>(entity);
67+
originalPositions.emplace_back(entity, pos.value);
68+
}
69+
3070
// Update rotation based on angular velocity
3171
registry->view<components::Rotation, components::AngularVelocity>().each(
3272
[&registry, deltaTime](entt::entity entity, auto& rotation, const auto& angularVelocity) {
@@ -41,10 +81,117 @@ void plugins::Physics::build(Game& game) {
4181
}
4282
});
4383

84+
// Update positions based on velocity
4485
registry->view<components::Position, components::Velocity>().each(
4586
[&registry, deltaTime](entt::entity entity, auto& position, const auto& velocity) {
4687
position.value += velocity.value * deltaTime;
4788
registry->replace<components::Position>(entity, position);
4889
});
90+
91+
// Collision detection and response
92+
std::vector<plugins::physics::components::CollisionEvent> collisionEvents;
93+
94+
// Check all pairs of colliders
95+
for (auto entity1 : colliderView) {
96+
auto& pos1 = colliderView.get<components::Position>(entity1);
97+
auto& collider1 = colliderView.get<components::BoxCollider>(entity1);
98+
99+
for (auto entity2 : colliderView) {
100+
// Don't check collision with self
101+
if (entity1 >= entity2)
102+
continue;
103+
104+
auto& pos2 = colliderView.get<components::Position>(entity2);
105+
auto& collider2 = colliderView.get<components::BoxCollider>(entity2);
106+
107+
glm::vec3 penetrationDepth;
108+
if (checkAABBCollision(pos1.value, collider1.halfExtents, pos2.value, collider2.halfExtents, penetrationDepth)) {
109+
// Create collision event
110+
plugins::physics::components::CollisionEvent event;
111+
event.entity1 = entity1;
112+
event.entity2 = entity2;
113+
event.penetrationDepth = penetrationDepth;
114+
collisionEvents.push_back(event);
115+
116+
// Improved collision response
117+
glm::vec3 collisionNormal = glm::normalize(penetrationDepth);
118+
float penetrationMagnitude = glm::length(penetrationDepth);
119+
120+
// Separate objects by full penetration distance plus small margin
121+
glm::vec3 separation = collisionNormal * (penetrationMagnitude + 0.001f);
122+
123+
// Check if entities have velocity to determine mass-like behavior
124+
auto* vel1 = registry->try_get<components::Velocity>(entity1);
125+
auto* vel2 = registry->try_get<components::Velocity>(entity2);
126+
127+
if (vel1 && vel2) {
128+
// Both objects can move - split separation
129+
pos1.value += separation * 0.5f;
130+
pos2.value -= separation * 0.5f;
131+
132+
// Apply velocity changes for bouncing/energy loss
133+
float restitution = 0.3f; // Energy loss factor
134+
glm::vec3 relativeVelocity = vel1->value - vel2->value;
135+
float velocityAlongNormal = glm::dot(relativeVelocity, collisionNormal);
136+
137+
if (velocityAlongNormal > 0) {
138+
continue; // Objects separating
139+
}
140+
141+
float impulse = -(1 + restitution) * velocityAlongNormal;
142+
glm::vec3 impulseVector = impulse * collisionNormal;
143+
144+
vel1->value += impulseVector * 0.5f;
145+
vel2->value -= impulseVector * 0.5f;
146+
147+
registry->replace<components::Velocity>(entity1, *vel1);
148+
registry->replace<components::Velocity>(entity2, *vel2);
149+
} else if (vel1) {
150+
// Only entity1 can move (entity2 is static)
151+
pos1.value += separation;
152+
if (glm::dot(vel1->value, collisionNormal) < 0) {
153+
// Remove velocity component in collision direction
154+
vel1->value -= glm::dot(vel1->value, collisionNormal) * collisionNormal * 1.3f;
155+
registry->replace<components::Velocity>(entity1, *vel1);
156+
}
157+
} else if (vel2) {
158+
// Only entity2 can move (entity1 is static)
159+
pos2.value -= separation;
160+
if (glm::dot(vel2->value, collisionNormal) > 0) {
161+
// Remove velocity component in collision direction
162+
vel2->value -= glm::dot(vel2->value, collisionNormal) * collisionNormal * 1.3f;
163+
registry->replace<components::Velocity>(entity2, *vel2);
164+
}
165+
} else {
166+
// Both are static - just separate
167+
pos1.value += separation * 0.5f;
168+
pos2.value -= separation * 0.5f;
169+
}
170+
171+
// Update positions in registry
172+
registry->replace<components::Position>(entity1, pos1);
173+
registry->replace<components::Position>(entity2, pos2);
174+
}
175+
}
176+
}
177+
178+
// Process collision callbacks
179+
for (const auto& event : collisionEvents) {
180+
// Check if entities have collision callbacks
181+
auto* callback1 = registry->try_get<plugins::physics::components::CollisionCallback>(event.entity1);
182+
auto* callback2 = registry->try_get<plugins::physics::components::CollisionCallback>(event.entity2);
183+
184+
if (callback1 && callback1->onCollisionEnter) {
185+
callback1->onCollisionEnter(event);
186+
}
187+
if (callback2 && callback2->onCollisionEnter) {
188+
// Create event from entity2's perspective
189+
plugins::physics::components::CollisionEvent reverseEvent = event;
190+
reverseEvent.entity1 = event.entity2;
191+
reverseEvent.entity2 = event.entity1;
192+
reverseEvent.penetrationDepth = -event.penetrationDepth;
193+
callback2->onCollisionEnter(reverseEvent);
194+
}
195+
}
49196
});
50197
}

src/plugins/physics/physics.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,30 @@
22

33
class Game;
44

5+
#include <glm/glm.hpp>
6+
#include <functional>
7+
#include <entt/entt.hpp>
8+
59
namespace plugins {
10+
namespace physics::components {
11+
struct CollisionEvent {
12+
entt::entity entity1;
13+
entt::entity entity2;
14+
glm::vec3 penetrationDepth;
15+
};
16+
17+
struct CollisionCallback {
18+
std::function<void(const CollisionEvent&)> onCollisionEnter;
19+
std::function<void(const CollisionEvent&)> onCollisionStay;
20+
std::function<void(const CollisionEvent&)> onCollisionExit;
21+
};
22+
};
23+
624
struct Physics {
725
void build(Game& game);
26+
27+
private:
28+
static bool checkAABBCollision(const glm::vec3& pos1, const glm::vec3& halfExtents1, const glm::vec3& pos2,
29+
const glm::vec3& halfExtents2, glm::vec3& penetrationDepth);
830
};
931
};

src/scenes/nfs.cpp

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,27 @@ static void createLightsForEmissiveMaterials(const asset::Asset3D& cityAsset, st
106106
}
107107
}
108108

109-
static std::expected<void, std::string> startup(/* clang-format off */
109+
static std::expected<void, std::string>
110+
startup(/* clang-format off */
110111
std::shared_ptr<entt::registry> registry,
111-
std::shared_ptr<Renderer> renderer
112-
) { /* clang-format on */
112+
std::shared_ptr<Renderer> renderer,
113+
std::shared_ptr<scenes::nfs::resources::CrateAsset> crateAsset /* clang-format on */
114+
) {
115+
{
116+
auto crateResult = asset::loader::Gltf::tryFromFile("resources/CrateBox.glb", *renderer->textureManager3D);
117+
if (!crateResult.has_value()) {
118+
return std::unexpected{std::format("Failed to load crate asset: {}", util::error::indent(crateResult.error()))};
119+
}
120+
121+
crateAsset->asset = renderer->createAsset3D(crateResult.value());
122+
}
123+
124+
{ // baseplate 1000x1000 (invisible)
125+
auto baseplateEnt = registry->create();
126+
registry->emplace<components::Position>(baseplateEnt, glm::vec3(0.0f, 0.0f, -0.2f));
127+
registry->emplace<components::BoxCollider>(baseplateEnt, glm::vec3(500.0f, 500.0f, 0.05f));
128+
}
129+
113130
{
114131
auto asset = asset::loader::Gltf::tryFromFile("resources/CarConcept/CarConcept.gltf", *renderer->textureManager3D);
115132
if (!asset.has_value()) {
@@ -133,18 +150,19 @@ static std::expected<void, std::string> startup(/* clang-format off */
133150
registry->emplace<components::Velocity>(ent, glm::vec3(0.0f, 0.0f, 0.0f));
134151
registry->emplace<components::AngularVelocity>(ent, glm::vec3(0.0f, 0.0f, 0.0f));
135152
registry->emplace<scenes::nfs::components::Car>(ent); // mark as car entity
153+
registry->emplace<components::BoxCollider>(ent, glm::vec3(0.5f, 0.5f, 0.20f));
136154

137155
auto cubeModel = std::make_shared<model::Cube>(glm::vec3(1.0f));
138156

139157
auto cubeEnt = registry->create();
140-
registry->emplace<components::Position>(cubeEnt, -constants::WORLD_FORWARD * 5.0f);
158+
registry->emplace<components::Position>(cubeEnt, constants::WORLD_RIGHT * 5.0f);
141159
registry->emplace<components::Child>(cubeEnt, ent); // make it a child of the car
142160
registry->emplace<components::Model3D>(cubeEnt, cubeModel);
143161

144162
std::vector<entt::entity> carChildren;
145163
carChildren.push_back(cubeEnt);
146164

147-
registry->emplace<components::Parent>(ent, carChildren); // make car a parent of the cube
165+
registry->emplace<components::Parent>(ent, carChildren);
148166
}
149167

150168
{ // city
@@ -188,10 +206,24 @@ static std::expected<void, std::string> startup(/* clang-format off */
188206
static std::expected<void, std::string> update(/* clang-format off */
189207
std::shared_ptr<entt::registry> registry,
190208
std::shared_ptr<Renderer> renderer,
191-
std::shared_ptr<scenes::nfs::components::CameraState> cameraState,
209+
std::shared_ptr<scenes::nfs::resources::CameraState> cameraState,
210+
std::shared_ptr<scenes::nfs::resources::CrateAsset> crateAsset,
192211
resources::Time& time
193212
) { /* clang-format on */
194213

214+
if (input::Mouse::wasJustPressed(input::MouseButton::Left)) {
215+
auto boxAsset = std::make_shared<model::Cube>(glm::vec3(1.0f));
216+
217+
// Create the box entity
218+
auto boxEntity = registry->create();
219+
registry->emplace<components::Position>(boxEntity, glm::vec3(0.0f, 0.0f, 20.0f));
220+
// registry->emplace<components::Scale>(boxEntity, glm::vec3(0.03f));
221+
registry->emplace<components::Model3D>(boxEntity, boxAsset);
222+
registry->emplace<components::Velocity>(boxEntity, glm::vec3(0.0f, 0.0f, 0.0f));
223+
registry->emplace<components::Acceleration>(boxEntity, constants::WORLD_GRAVITY);
224+
registry->emplace<components::BoxCollider>(boxEntity, glm::vec3(0.5f));
225+
}
226+
195227
// Car controller system
196228
auto carView = registry->view<components::Position, components::Rotation, components::Velocity, components::AngularVelocity,
197229
scenes::nfs::components::Car>();
@@ -361,9 +393,12 @@ static std::expected<void, std::string> update(/* clang-format off */
361393
}
362394

363395
void scenes::nfs::NFS::build(Game& game) {
364-
auto cameraState = std::make_shared<scenes::nfs::components::CameraState>();
396+
auto cameraState = std::make_shared<scenes::nfs::resources::CameraState>();
365397
game.addResource(cameraState);
366398

399+
auto crateAsset = std::make_shared<scenes::nfs::resources::CrateAsset>();
400+
game.addResource(crateAsset);
401+
367402
game.addSystem(Schedule::Startup, startup);
368403
game.addSystem(Schedule::Update, update);
369404
}

src/scenes/nfs.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
class Game;
55

6+
#include "render/model/3d/asset.hpp"
7+
68
namespace scenes::nfs {
79
namespace components {
810
struct Car {};
11+
};
912

13+
namespace resources {
1014
struct CameraState {
1115
float yaw = 0.0f; // Horizontal rotation around the car
1216
float pitch = -0.1f; // Vertical angle (looking down slightly at the car)
@@ -24,6 +28,10 @@ namespace scenes::nfs {
2428
float targetYaw = 0.0f; // Target yaw (behind the car)
2529
float targetPitch = -0.1f; // Target pitch (slightly down)
2630
};
31+
32+
struct CrateAsset {
33+
std::shared_ptr<model::Asset> asset; // The crate 3D model asset
34+
};
2735
};
2836

2937
struct NFS {

0 commit comments

Comments
 (0)