27#include <Eigen/StdVector>
37using ProfilingClock = std::chrono::high_resolution_clock;
41 previous_dt_(config.max_dt),
42 G_(config.universal_g),
43 epsilon_(config.epsilon),
44 coeffRestitution_(config.coeff_restitution),
45 coeffFriction_(config.coeff_friction),
46 coeffStaticFriction_(config.coeff_static_friction),
47 positionalCorrectionFactor_(config.positional_correction_factor),
48 linearDamping_(config.linear_damping),
49 angularDamping_(config.angular_damping),
50 diagnosticsEnabled_(config.diagnostics_enabled),
51 logFreq_(config.log_freq) {
58 using Eigen::Vector3d;
59 using Eigen::Vector3i;
66 std::size_t operator()(
const Vector3i& v)
const {
68 std::hash<int> hasher;
69 return hasher(v.x()) ^ (hasher(v.y()) << 1) ^ (hasher(v.z()) << 2);
79 class PoissonDiskGrid {
82 std::vector<std::vector<std::vector<int>>> grid_;
94 PoissonDiskGrid(
double in_cell_size,
const Vector3d& in_dims)
95 : cell_size_(in_cell_size), dims_(in_dims) {
97 grid_shape_ = Vector3i(
98 static_cast<int>(std::ceil(in_dims.x() / in_cell_size)),
99 static_cast<int>(std::ceil(in_dims.y() / in_cell_size)),
100 static_cast<int>(std::ceil(in_dims.z() / in_cell_size)));
103 grid_.resize(grid_shape_.x());
104 for (
int i = 0; i < grid_shape_.x(); ++i) {
105 grid_[i].resize(grid_shape_.y());
106 for (
int j = 0; j < grid_shape_.y(); ++j) {
107 grid_[i][j].resize(grid_shape_.z(), -1);
117 Vector3i getCellCoords(
const Vector3d& pos)
const {
119 const Vector3d shifted = pos + dims_ / 2.0;
120 return Vector3i(
static_cast<int>(shifted.x() / cell_size_),
121 static_cast<int>(shifted.y() / cell_size_),
122 static_cast<int>(shifted.z() / cell_size_));
133 bool isValid(
const Vector3d& pos,
double min_dist_sq,
134 const std::vector<Vector3d>& existing_points)
const {
136 if (std::abs(pos.x()) >= dims_.x() / 2.0 ||
137 std::abs(pos.y()) >= dims_.y() / 2.0 ||
138 std::abs(pos.z()) >= dims_.z() / 2.0) {
143 const Vector3i cell = getCellCoords(pos);
146 if (cell.x() < 0 || cell.x() >= grid_shape_.x() || cell.y() < 0 ||
147 cell.y() >= grid_shape_.y() || cell.z() < 0 ||
148 cell.z() >= grid_shape_.z()) {
153 if (grid_[cell.x()][cell.y()][cell.z()] != -1) {
158 for (
int i = -1; i <= 1; ++i) {
159 for (
int j = -1; j <= 1; ++j) {
160 for (
int k = -1; k <= 1; ++k) {
161 const int x = cell.x() + i;
162 const int y = cell.y() + j;
163 const int z = cell.z() + k;
165 if (x >= 0 && x < grid_shape_.x() && y >= 0 &&
166 y < grid_shape_.y() && z >= 0 &&
167 z < grid_shape_.z()) {
168 const int point_idx = grid_[x][y][z];
169 if (point_idx != -1) {
170 const double dist_sq =
171 (pos - existing_points[point_idx])
173 if (dist_sq < min_dist_sq) {
190 void add(std::size_t point_idx,
const Vector3d& pos) {
191 const Vector3i cell = getCellCoords(pos);
192 if (cell.x() >= 0 && cell.x() < grid_shape_.x() && cell.y() >= 0 &&
193 cell.y() < grid_shape_.y() && cell.z() >= 0 &&
194 cell.z() < grid_shape_.z()) {
195 grid_[cell.x()][cell.y()][cell.z()] =
196 static_cast<int>(point_idx);
204 for (std::size_t i = 0; i < graphLocks_.size(); ++i) {
205 omp_destroy_lock(&graphLocks_[i]);
213 throw std::invalid_argument(
"Number of bodies must be greater than 0");
216 throw std::invalid_argument(
"Density must be greater than 0");
218 if (!std::isfinite(dens)) {
219 throw std::invalid_argument(
"Density must be a finite number");
224 std::random_device rd;
234 std::vector<double> masses(n);
235 std::vector<double> radii(n);
236 std::vector<Vector3d> velocities(n);
237 std::vector<Vector3d> angularVelocities(n);
242 for (std::size_t i = 0; i < n; ++i) {
243 masses[i] = massDist(gen);
244 radii[i] = masses[i] * dens;
245 velocities[i] = Vector3d(vDis(gen), vDis(gen), vDis(gen));
246 angularVelocities[i] =
247 Vector3d(omegaDis(gen), omegaDis(gen), omegaDis(gen));
255 const double avgRadius = avgMass * dens;
256 const double placementScale =
258 const Vector3d dims(2.0 * placementScale, 2.0 * placementScale,
259 2.0 * placementScale);
262 const double maxRadius = *std::max_element(radii.begin(), radii.end());
263 const double minDist = 2.0 * maxRadius;
264 const double minDistSq = minDist * minDist;
267 const double cellSize = minDist / std::sqrt(3.0);
268 PoissonDiskGrid grid(cellSize, dims);
271 std::vector<Vector3d> positions;
272 positions.reserve(n);
273 std::vector<std::size_t> activeList;
274 constexpr int kRejectionSamples = 30;
277 std::uniform_real_distribution<> initialDist(-placementScale,
279 Vector3d initialPoint(initialDist(gen), initialDist(gen), initialDist(gen));
280 positions.push_back(initialPoint);
281 grid.add(0, initialPoint);
282 activeList.push_back(0);
285 constexpr double TWO_PI = 2.0 * 3.14159265358979323846;
286 std::uniform_real_distribution<> angleDist(0.0, TWO_PI);
287 std::uniform_real_distribution<> radiusDist(minDist, 2.0 * minDist);
289 while (!activeList.empty() && positions.size() < n) {
291 std::uniform_int_distribution<std::size_t> activeDist(
292 0, activeList.size() - 1);
293 const std::size_t activeIdx = activeList[activeDist(gen)];
294 const Vector3d& activePoint = positions[activeIdx];
295 bool foundCandidate =
false;
298 for (
int attempt = 0; attempt < kRejectionSamples; ++attempt) {
301 const double theta = angleDist(gen);
302 const double phi = angleDist(gen);
303 const double radius = radiusDist(gen);
305 Vector3d offset(radius * std::sin(phi) * std::cos(theta),
306 radius * std::sin(phi) * std::sin(theta),
307 radius * std::cos(phi));
308 const Vector3d candidate = activePoint + offset;
311 if (grid.isValid(candidate, minDistSq, positions)) {
312 positions.push_back(candidate);
313 grid.add(positions.size() - 1, candidate);
314 activeList.push_back(positions.size() - 1);
315 foundCandidate =
true;
321 if (!foundCandidate) {
323 std::remove(activeList.begin(), activeList.end(), activeIdx),
329 const std::size_t nPlaced = positions.size();
332 <<
"Failed to generate" << n
333 <<
"non-overlapping positions. The density may be too high. Using"
334 << nPlaced <<
"bodies instead.";
338 bodies_.reserve(nPlaced);
339 for (std::size_t i = 0; i < nPlaced; ++i) {
340 bodies_.emplaceBack(masses[i], radii[i], positions[i], velocities[i],
341 angularVelocities[i]);
346 const std::size_t numBodies = nPlaced;
347 collisionGraph_.resize(numBodies);
348 graphLocks_.resize(numBodies);
349 for (std::size_t i = 0; i < numBodies; ++i) {
350 omp_init_lock(&graphLocks_[i]);
354 thread_local_collision_pairs_.resize(omp_get_max_threads());
359 const std::size_t reserve_size = numBodies / omp_get_max_threads();
360 for (
auto& vec : thread_local_collision_pairs_) {
361 vec.reserve(reserve_size);
370 const double originalDt = dt_;
371 kdTree_ = std::make_unique<KDTree>(bodies_);
372 previous_dt_ = epsilon_;
380 dt_ = previous_dt_ = originalDt;
383std::tuple<Model::Vector3dVec, Model::QuaterniondVec>
385 const std::size_t numBodies = n();
386 Vector3dVec positions;
387 QuaterniondVec quaternions;
389 positions.reserve(numBodies);
390 quaternions.reserve(numBodies);
392 positions.assign(bodies_.x_.begin(), bodies_.x_.begin() + numBodies);
393 quaternions.assign(bodies_.q_.begin(), bodies_.q_.begin() + numBodies);
395 return {positions, quaternions};
398std::tuple<Model::Vector3dVec, Model::QuaterniondVec, Model::Vector3dVec,
401 const std::size_t numBodies = n();
402 Vector3dVec positions;
403 QuaterniondVec quaternions;
407 positions.assign(bodies_.x_.begin(), bodies_.x_.begin() + numBodies);
408 quaternions.assign(bodies_.q_.begin(), bodies_.q_.begin() + numBodies);
409 torques.assign(bodies_.torque_.begin(),
410 bodies_.torque_.begin() + numBodies);
411 alphas.assign(bodies_.alpha_.begin(), bodies_.alpha_.begin() + numBodies);
413 return {positions, quaternions, torques, alphas};
416void Model::Space::printProfilingReport()
const {
417 const double totalTime = profIntegration_ + profCollisionGraph_ +
418 profCollisionResponse_ + profForceCalculation_;
420 if (totalTime < epsilon_) {
421 qDebug().noquote() <<
"\n--- Profiling Report (No data) ---";
427 "\n--- Profiling Report ---\nIntegration: %1s "
428 "(%2%%)\nCollision Graph: %3s (%4%%)\nCollision Response: "
429 "%5s (%6%%)\nForce Calculation: %7s "
430 "(%8%%)\n------------------------\nTotal Instrumented Time:%9s")
431 .arg(profIntegration_, 9,
'f', 4)
432 .arg(profIntegration_ / totalTime * 100.0, 5,
'f', 1)
433 .arg(profCollisionGraph_, 9,
'f', 4)
434 .arg(profCollisionGraph_ / totalTime * 100.0, 5,
'f', 1)
435 .arg(profCollisionResponse_, 9,
'f', 4)
436 .arg(profCollisionResponse_ / totalTime * 100.0, 5,
'f', 1)
437 .arg(profForceCalculation_, 9,
'f', 4)
438 .arg(profForceCalculation_ / totalTime * 100.0, 5,
'f', 1)
439 .arg(totalTime, 9,
'f', 4);
440 qDebug().noquote() << msg;
444 double transKe = 0.0;
446 double potential = 0.0;
450#pragma omp parallel for reduction(+ : transKe, rotKe)
451 for (std::size_t i = 0; i < n(); ++i) {
452 const auto bi = body(i);
454 transKe += 0.5 * bi.m() * bi.v().squaredNorm();
458 if (bi.getInverseInertia() > 0) {
460 0.5 * (1.0 / bi.getInverseInertia()) * bi.omega().squaredNorm();
468#pragma omp parallel for reduction(+ : potential) schedule(dynamic)
469 for (std::size_t i = 0; i < n(); ++i) {
470 const auto bi = body(i);
473 for (std::size_t j = i + 1; j < n(); ++j) {
474 const auto bj = body(j);
475 const double dSq = (bi.x() - bj.x()).squaredNorm();
476 if (dSq > epsilon_ * epsilon_) {
477 potential -= G_ * bi.m() * bj.m() / std::sqrt(dSq);
482 return {transKe, rotKe, potential};
488 const double totalInvMass =
490 if (totalInvMass < epsilon_) {
495 constexpr double PERCENT = 0.8;
496 const Vector3d correction =
508 const Vector3d& direction_vec) {
510 const double term1 = (ctx.
r1Vec_.cross(direction_vec)).squaredNorm() *
513 const double term2 = (ctx.
r2Vec_.cross(direction_vec)).squaredNorm() *
519 return effectiveMassInv > epsilon_ ? 1.0 / effectiveMassInv : 0.0;
522std::tuple<Model::Vector3d, double, double>
524 const double vRelNormal = ctx.
vRel_.dot(ctx.
nVec_);
525 if (vRelNormal > 0) {
527 return {Vector3d::Zero(), 0.0, 0.0};
529 const double effectiveMassN = getEffectiveMass(ctx, ctx.
nVec_);
533 const double bias_jn = (positionalCorrectionFactor_ / previous_dt_) *
534 std::max(0.0, ctx.
overlap_ - 0.01) * effectiveMassN;
538 const double restitution_jn =
539 -(1.0 + coeffRestitution_) * vRelNormal * effectiveMassN;
543 const double jn = std::max(0.0, restitution_jn) + bias_jn;
545 return {jn * ctx.
nVec_, jn, bias_jn};
550 const Vector3d& impulseN) {
555 const Vector3d vT_initial =
557 const double vT_initialNorm = vT_initial.norm();
560 if (vT_initialNorm < epsilon_) {
561 return Vector3d::Zero();
565 const Vector3d tVec = vT_initial / vT_initialNorm;
566 const double effectiveMassT = getEffectiveMass(ctx, tVec);
574 const Vector3d torque1 = ctx.
r1Vec_.cross(-impulseN);
575 const Vector3d torque2 = ctx.
r2Vec_.cross(impulseN);
582 const Vector3d vRelAfterRestitution =
583 ((ctx.
b2.
v() + delta_v2) +
585 ((ctx.
b1.
v() + delta_v1) +
590 const Vector3d vTAfterRestitution =
591 vRelAfterRestitution - vRelAfterRestitution.dot(ctx.
nVec_) * ctx.
nVec_;
592 const double vTAfterRestitutionAlongT = vTAfterRestitution.dot(tVec);
596 const double jtRequiredMagnitude =
597 std::abs(vTAfterRestitutionAlongT) * effectiveMassT;
603 const double vRelNormal = ctx.
vRel_.dot(ctx.
nVec_);
604 const double effectiveMassN = getEffectiveMass(ctx, ctx.
nVec_);
605 const double restitution_jn =
606 (vRelNormal <= 0) ? std::max(0.0, -(1.0 + coeffRestitution_) *
607 vRelNormal * effectiveMassN)
613 const double jn_physical = restitution_jn;
614 const double jn_for_friction = (jn_physical < epsilon_) ? jn : jn_physical;
615 const double staticFrictionLimit = coeffStaticFriction_ * jn_for_friction;
618 if (jtRequiredMagnitude < staticFrictionLimit) {
622 jt = jtRequiredMagnitude;
628 jt = coeffFriction_ * jn_for_friction;
644 const Vector3d dVec = b2.
x() - b1.
x();
645 const double distSq = dVec.squaredNorm();
646 const double rSum = b1.
r() + b2.
r();
648 if (distSq >= rSum * rSum || distSq < epsilon_ * epsilon_) {
652 const double dist = std::sqrt(distSq);
653 const Vector3d nVec = dVec / dist;
654 const Vector3d r1Vec = b1.
r() * nVec;
655 const Vector3d r2Vec = -b2.
r() * nVec;
656 const Vector3d vRel =
657 (b2.
v() + b2.
omega().cross(r2Vec)) - (b1.
v() + b1.
omega().cross(r1Vec));
658 const double overlap = (b1.
r() + b2.
r()) - dist;
661 r2Vec, vRel, overlap};
665 auto context_opt = createCollisionContext(b1, b2);
674 resolveInterpenetration(ctx);
678 auto context_opt_updated = createCollisionContext(b1, b2);
679 if (!context_opt_updated) {
684 const auto [impulseN, jn, bias_jn] = applyRestitutionImpulse(ctx_updated);
688 const Vector3d impulseT =
689 applyFrictionImpulse(ctx_updated, jn, impulseN);
690 const Vector3d totalImpulse = impulseN + impulseT;
691 const Vector3d torque1 =
693 const Vector3d torque2 =
701 const std::vector<std::vector<std::size_t>>& in_graph) {
704 const std::size_t numBodies = n();
705 colors_.assign(numBodies, 0);
706 std::vector<bool> conflicts(numBodies,
true);
707 bool hasConflicts =
true;
710 omp_lock_t color_lock;
711 omp_init_lock(&color_lock);
712 while (hasConflicts) {
713 hasConflicts =
false;
718 const int max_color = *std::max_element(colors_.begin(), colors_.end());
721#pragma omp parallel for
722 for (std::size_t i = 0; i < numBodies; ++i) {
723 if (!conflicts[i])
continue;
728 std::vector<int> neighbourColours;
729 neighbourColours.reserve(in_graph[i].size());
730 omp_set_lock(&color_lock);
731 for (
const auto& neighbourIdx : in_graph[i]) {
732 neighbourColours.push_back(colors_[neighbourIdx]);
734 omp_unset_lock(&color_lock);
739 std::vector<bool> usedColors(max_color + 2,
false);
740 for (
const int neighbourColour : neighbourColours) {
743 if (neighbourColour >=
static_cast<int>(usedColors.size())) {
744 usedColors.resize(neighbourColour + 2,
false);
746 usedColors[neighbourColour] =
true;
750 while (newColor <
static_cast<int>(usedColors.size()) &&
751 usedColors[newColor]) {
756 if (newColor >=
static_cast<int>(usedColors.size())) {
762 omp_set_lock(&color_lock);
763 colors_[i] = newColor;
764 omp_unset_lock(&color_lock);
772#pragma omp parallel for reduction(| : hasConflicts)
773 for (std::size_t i = 0; i < numBodies; ++i) {
774 conflicts[i] =
false;
775 for (
const auto& neighbourIdx : in_graph[i]) {
776 if (colors_[i] == colors_[neighbourIdx]) {
785 omp_destroy_lock(&color_lock);
789 if (!diagnosticsEnabled_ || iteration == 0 || iteration % logFreq_ != 0) {
793 const auto [transKe, rotKe, potential] = calculateSystemEnergy();
794 const double totalEnergy = transKe + rotKe + potential;
799 "\n--- Iteration %1: System Energy ---\n Translational KE: %2\n "
800 "Rotational KE: %3\n Potential Energy: %4\n Total Energy: "
801 "%5\n Time Step (dt): %6")
803 .arg(transKe, 12,
'e', 4)
804 .arg(rotKe, 12,
'e', 4)
805 .arg(potential, 12,
'e', 4)
806 .arg(totalEnergy, 12,
'e', 4)
807 .arg(getPreviousTimeStep(), 12,
'e', 4);
808 qDebug().noquote() << msg;
811 if (totalEnergy > lastTotalEnergy_ + epsilon_) {
813 <<
"\n========================================"
814 <<
"\n Total energy increased by "
815 << QString::asprintf(
"%.4e", totalEnergy - lastTotalEnergy_)
816 <<
"\n========================================";
818 lastTotalEnergy_ = totalEnergy;
824 const std::size_t numBodies = n();
826 if (collisionGraph_.size() != numBodies) {
827 collisionGraph_.resize(numBodies);
831#pragma omp parallel for
832 for (std::size_t i = 0; i < numBodies; ++i) {
833 collisionGraph_[i].clear();
839#pragma omp parallel for
841 tid < static_cast<int>(thread_local_collision_pairs_.size()); ++tid) {
842 thread_local_collision_pairs_[tid].clear();
846 kdTree_ = std::make_unique<KDTree>(bodies_);
851 if (cached_max_radius_ == 0.0) {
852 cached_max_radius_ = 0.0;
853#pragma omp parallel for reduction(max : cached_max_radius_)
854 for (std::size_t i = 0; i < numBodies; ++i) {
855 cached_max_radius_ = std::max(cached_max_radius_, bodies_.r_[i]);
858 const double max_radius = cached_max_radius_;
863 const int tid = omp_get_thread_num();
866#if defined(NANOFLANN_VERSION) && NANOFLANN_VERSION < 0x150
867 std::vector<std::pair<std::size_t, double>> results;
869 std::vector<nanoflann::ResultItem<std::size_t, double>> results;
873#pragma omp for schedule(dynamic)
874 for (std::size_t i = 0; i < numBodies; ++i) {
876 const double r_i = bodies_.r_[i];
877 const Vector3d& x_i = bodies_.x_[i];
878 const double search_radius = r_i + max_radius;
882 kdTree_->radiusSearch(x_i.data(), search_radius, results);
886 for (
const auto& result : results) {
887 const std::size_t j = result.first;
888 if (i >= j)
continue;
890 const double rSum = r_i + bodies_.r_[j];
891 const double distSq = (x_i - bodies_.x_[j]).squaredNorm();
892 if (distSq < rSum * rSum) {
893 thread_local_collision_pairs_[tid].push_back({i, j});
900 for (
const auto& localPairs : thread_local_collision_pairs_) {
901 for (
const auto& pair : localPairs) {
902 collisionGraph_[pair.first].emplace_back(pair.second);
903 collisionGraph_[pair.second].emplace_back(pair.first);
910 logSystemEnergy(iteration);
917 const double current_dt = dt_;
919 const auto startTime = ProfilingClock::now();
920 bodies_.integratePart1(dt_, previous_dt_);
922 std::chrono::duration<double>(ProfilingClock::now() - startTime)
928 const auto startTime = ProfilingClock::now();
929 buildCollisionGraph();
930 profCollisionGraph_ +=
931 std::chrono::duration<double>(ProfilingClock::now() - startTime)
937 const auto startTime = ProfilingClock::now();
939 profCollisionResponse_ +=
940 std::chrono::duration<double>(ProfilingClock::now() - startTime)
945 updateAdaptiveTimeStep();
949 const auto startTime = ProfilingClock::now();
951 profForceCalculation_ +=
952 std::chrono::duration<double>(ProfilingClock::now() - startTime)
960 bodies_.integratePart2(current_dt);
967 const std::size_t numBodies = n();
968 if (numBodies == 0)
return;
971 colorGraph(collisionGraph_);
972 const int numColours =
974 : *std::max_element(colors_.begin(), colors_.end()) + 1;
977 for (
int c = 0; c < numColours; ++c) {
978#pragma omp parallel for schedule(dynamic)
979 for (std::size_t i = 0; i < numBodies; ++i) {
980 if (colors_[i] != c)
continue;
982 for (
const auto& j : collisionGraph_[i]) {
987 handleCollision(b1, b2);
995 const std::size_t numBodies = n();
998#pragma omp parallel for
999 for (std::size_t i = 0; i < numBodies; ++i) {
1000 bodies_.v_[i] *= (1.0 - linearDamping_);
1001 bodies_.omega_[i] *= (1.0 - angularDamping_);
1011#pragma omp parallel for reduction(max : maxASq) schedule(static)
1012 for (std::size_t i = 0; i < n(); ++i) {
1013 const double aSq = body(i).a().squaredNorm();
1014 maxASq = std::max(maxASq, aSq);
1018 double maxVSq = 0.0;
1019 std::size_t fastestBodyIdx = 0;
1020 for (std::size_t i = 0; i < n(); ++i) {
1021 const double vSq = body(i).v().squaredNorm();
1029 double maxOmegaSq = 0.;
1030 double maxAlphaSq = 0.;
1031#pragma omp parallel for reduction(max : maxOmegaSq, maxAlphaSq) \
1033 for (std::size_t i = 0; i < n(); ++i) {
1034 const double omegaSq = body(i).omega().squaredNorm();
1035 const double alphaSq = body(i).alpha().squaredNorm();
1036 maxOmegaSq = std::max(maxOmegaSq, omegaSq);
1037 maxAlphaSq = std::max(maxAlphaSq, alphaSq);
1042 if (maxASq > epsilon_ * epsilon_) {
1043 dtA = std::sqrt(2.0 *
TARGET_DX / std::sqrt(maxASq));
1049 if (maxVSq > epsilon_ * epsilon_) {
1050 dtV = (0.5 * body(fastestBodyIdx).r()) / std::sqrt(maxVSq);
1058 constexpr double MAX_ANGLE = 0.1;
1061 if (maxOmegaSq > epsilon_ * epsilon_) {
1065 dtOmega = MAX_ANGLE / std::sqrt(maxOmegaSq);
1071 if (maxAlphaSq > epsilon_ * epsilon_) {
1075 dtAlpha = std::sqrt(2.0 * MAX_ANGLE / std::sqrt(maxAlphaSq));
1081 const double targetDt = std::min({
MAX_DT, dtA, dtV, dtOmega, dtAlpha});
1086#pragma omp parallel for
1087 for (std::size_t i = 0; i < n(); ++i) {
1088 body(i).resetAccelerations();
1094#pragma omp parallel for schedule(dynamic)
1095 for (std::size_t i = 0; i < n(); ++i) {
1096 Vector3d totalForceVec = Vector3d::Zero();
1097 for (std::size_t j = 0; j < n(); ++j) {
1098 if (i == j)
continue;
1099 const Vector3d dv = body(j).x() - body(i).x();
1100 const double dSq = dv.squaredNorm();
1101 if (dSq > epsilon_ * epsilon_) {
1102 const double force_mag =
1103 G_ * body(j).m() / (dSq * std::sqrt(dSq));
1104 totalForceVec += dv * force_mag;
1107 body(i).setAcceleration(totalForceVec);
1113#pragma omp parallel for
1114 for (std::size_t i = 0; i < n(); ++i) {
1115 body(i).updateAlphaFromTorque();
1120#pragma omp parallel for
1121 for (std::size_t i = 0; i < n(); ++i) {
1122 body(i).resetTorque();
const Vector3d & v() const noexcept
Access to body velocities.
double getInverseInertia() const
Access to the inverse of the body's moment of inertia.
double getInverseMass() const
Access to the inverse of the body masses.
double r() const noexcept
Access to body radii.
const Vector3d & x() const noexcept
Access to body positions.
A proxy object that provides an AoS-like interface to a body stored in the Bodies SoA container.
void addToX(const Vector3d &correction)
Add a displacement to a position.
Vector3d & omega()
Access to body angular velocities.
void accumulateTorque(const Vector3d &torque)
Accumulates a torque vector to the body's total torque.
Vector3d applyImpulse(const Vector3d &in_J, const Vector3d &in_r_vec)
Apply an impulse to a body.
Vector3d applyFrictionImpulse(const CollisionContext &ctx, double jn, const Vector3d &impulseN)
Calculates the tangential (frictional) impulse for a collision.
~Space()
Destructor to clean up OpenMP locks.
double previous_dt_
Previous time step (in s).
void initializeBodies(std::size_t n, double dens, unsigned int seed)
Initialises the bodies in the simulation with random properties.
void applyGravity()
Calculates and applies gravitational forces to all bodies.
std::tuple< Vector3dVec, QuaterniondVec, Vector3dVec, Vector3dVec > getDataForDisplay() const
Retrieves all data required for display for all bodies.
void resolveInterpenetration(const CollisionContext &ctx)
Resolves interpenetration by moving bodies apart along the collision normal.
double getEffectiveMass(const CollisionContext &ctx, const Vector3d &direction_vec)
Calculates the effective mass of two colliding bodies in a given direction.
void logSystemEnergy(std::size_t iteration)
Logs system energy and checks for instability if diagnostics are enabled.
void resolveCollisions()
Resolves all detected collisions using graph colouring.
void colorGraph(const std::vector< std::vector< std::size_t > > &graph)
A greedy graph colouring algorithm to assign a colour (integer) to each body such that no two adjacen...
void computeDynamics(std::size_t iteration)
Computes one full step of the simulation.
Space()
Default constructor.
std::optional< CollisionContext > createCollisionContext(BodyProxy &b1, BodyProxy &b2)
Factory method to create a CollisionContext if two bodies are colliding.
double dt_
Time step (in s).
void applyDamping()
Applies linear and angular damping to all bodies.
void updateAdaptiveTimeStep()
Determines the optimal time step for the next frame based on current velocities and accelerations.
std::tuple< Vector3d, double, double > applyRestitutionImpulse(CollisionContext &ctx)
Calculates and applies the normal impulse (restitution) for a collision.
void buildCollisionGraph()
Builds the collision graph using broad-phase and narrow-phase detection.
std::tuple< Vector3dVec, QuaterniondVec > getAllBodyTransforms() const
Retrieves the current positions and orientations of all bodies.
void handleCollision(BodyProxy &b1, BodyProxy &b2)
Handles collision detection and response between two bodies.
std::tuple< double, double, double > calculateSystemEnergy() const
Calculates the kinetic and potential energy of the system.
A 32-bit Permuted Congruential Generator (pcg32).
constexpr double MIN_BODY_MASS
Minimum mass for randomly generated bodies.
constexpr double MIN_INITIAL_VELOCITY
Minimum initial velocity component for randomly generated bodies.
constexpr double MAX_BODY_MASS
Maximum mass for randomly generated bodies.
constexpr double TARGET_DX
Heuristic for the maximum distance a body should travel in one step, used for adaptive time stepping.
constexpr double PLACEMENT_SCALE_FACTOR
Heuristic scaling factor for the initial placement volume of bodies.
constexpr double MAX_INITIAL_VELOCITY
Maximum initial velocity component for randomly generated bodies.
constexpr double DT_DAMPING_FACTOR
Damping factor for smoothing adaptive time step changes.
constexpr double MAX_INITIAL_ANGULAR_VELOCITY
Maximum initial angular velocity component for randomly generated bodies (in rad/s).
constexpr int SETTLING_STEPS
The number of "settling" steps to run at the start of the simulation.
constexpr double MAX_DT
Maximum time step allowed for the simulation to ensure stability.
A minimal C++ implementation of the PCG32 random number generator.
N-body simulation space with gravitational interaction and collision response.
A data container to hold all relevant information for a single collision event.
Vector3d vRel_
The relative velocity between the two bodies at the contact point.
BodyProxy & b1
A proxy to the first body in the collision.
BodyProxy & b2
A proxy to the second body in the collision.
Vector3d r1Vec_
The vector from the centre of body 1 to the contact point.
Vector3d r2Vec_
The vector from the centre of body 2 to the contact point.
Vector3d nVec_
The normalised vector pointing from body 1 to body 2.
double overlap_
The penetration depth of the two bodies.