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),
59 using Eigen::Vector3d;
60 using Eigen::Vector3i;
67 std::size_t operator()(
const Vector3i& v)
const {
69 std::hash<int> hasher;
70 return hasher(v.x()) ^ (hasher(v.y()) << 1) ^ (hasher(v.z()) << 2);
80 class PoissonDiskGrid {
83 std::vector<std::vector<std::vector<int>>> grid_;
95 PoissonDiskGrid(
double in_cell_size,
const Vector3d& in_dims)
96 : cell_size_(in_cell_size), dims_(in_dims) {
98 grid_shape_ = Vector3i(
99 static_cast<int>(std::ceil(in_dims.x() / in_cell_size)),
100 static_cast<int>(std::ceil(in_dims.y() / in_cell_size)),
101 static_cast<int>(std::ceil(in_dims.z() / in_cell_size)));
104 grid_.resize(grid_shape_.x());
105 for (
int i = 0; i < grid_shape_.x(); ++i) {
106 grid_[i].resize(grid_shape_.y());
107 for (
int j = 0; j < grid_shape_.y(); ++j) {
108 grid_[i][j].resize(grid_shape_.z(), -1);
118 Vector3i getCellCoords(
const Vector3d& pos)
const {
120 const Vector3d shifted = pos + dims_ / 2.0;
121 return Vector3i(
static_cast<int>(shifted.x() / cell_size_),
122 static_cast<int>(shifted.y() / cell_size_),
123 static_cast<int>(shifted.z() / cell_size_));
134 bool isValid(
const Vector3d& pos,
double min_dist_sq,
135 const std::vector<Vector3d>& existing_points)
const {
137 if (std::abs(pos.x()) >= dims_.x() / 2.0 ||
138 std::abs(pos.y()) >= dims_.y() / 2.0 ||
139 std::abs(pos.z()) >= dims_.z() / 2.0) {
144 const Vector3i cell = getCellCoords(pos);
147 if (cell.x() < 0 || cell.x() >= grid_shape_.x() || cell.y() < 0 ||
148 cell.y() >= grid_shape_.y() || cell.z() < 0 ||
149 cell.z() >= grid_shape_.z()) {
154 if (grid_[cell.x()][cell.y()][cell.z()] != -1) {
159 for (
int i = -1; i <= 1; ++i) {
160 for (
int j = -1; j <= 1; ++j) {
161 for (
int k = -1; k <= 1; ++k) {
162 const int x = cell.x() + i;
163 const int y = cell.y() + j;
164 const int z = cell.z() + k;
166 if (x >= 0 && x < grid_shape_.x() && y >= 0 &&
167 y < grid_shape_.y() && z >= 0 &&
168 z < grid_shape_.z()) {
169 const int point_idx = grid_[x][y][z];
170 if (point_idx != -1) {
171 const double dist_sq =
172 (pos - existing_points[point_idx])
174 if (dist_sq < min_dist_sq) {
191 void add(std::size_t point_idx,
const Vector3d& pos) {
192 const Vector3i cell = getCellCoords(pos);
193 if (cell.x() >= 0 && cell.x() < grid_shape_.x() && cell.y() >= 0 &&
194 cell.y() < grid_shape_.y() && cell.z() >= 0 &&
195 cell.z() < grid_shape_.z()) {
196 grid_[cell.x()][cell.y()][cell.z()] =
197 static_cast<int>(point_idx);
205 for (std::size_t i = 0; i < graphLocks_.size(); ++i) {
206 omp_destroy_lock(&graphLocks_[i]);
214 throw std::invalid_argument(
"Number of bodies must be greater than 0");
217 throw std::invalid_argument(
"Density must be greater than 0");
219 if (!std::isfinite(dens)) {
220 throw std::invalid_argument(
"Density must be a finite number");
225 std::random_device rd;
235 std::vector<double> masses(n);
236 std::vector<double> radii(n);
237 std::vector<Vector3d> velocities(n);
238 std::vector<Vector3d> angularVelocities(n);
243 for (std::size_t i = 0; i < n; ++i) {
244 masses[i] = massDist(gen);
245 radii[i] = masses[i] * dens;
246 velocities[i] = Vector3d(vDis(gen), vDis(gen), vDis(gen));
247 angularVelocities[i] =
248 Vector3d(omegaDis(gen), omegaDis(gen), omegaDis(gen));
256 const double avgRadius = avgMass * dens;
257 const double placementScale =
259 const Vector3d dims(2.0 * placementScale, 2.0 * placementScale,
260 2.0 * placementScale);
263 const double maxRadius = *std::max_element(radii.begin(), radii.end());
264 const double minDist = 2.0 * maxRadius;
265 const double minDistSq = minDist * minDist;
268 const double cellSize = minDist / std::sqrt(3.0);
269 PoissonDiskGrid grid(cellSize, dims);
272 std::vector<Vector3d> positions;
273 positions.reserve(n);
274 std::vector<std::size_t> activeList;
275 constexpr int kRejectionSamples = 30;
278 std::uniform_real_distribution<> initialDist(-placementScale,
280 Vector3d initialPoint(initialDist(gen), initialDist(gen), initialDist(gen));
281 positions.push_back(initialPoint);
282 grid.add(0, initialPoint);
283 activeList.push_back(0);
286 constexpr double TWO_PI = 2.0 * 3.14159265358979323846;
287 std::uniform_real_distribution<> angleDist(0.0, TWO_PI);
288 std::uniform_real_distribution<> radiusDist(minDist, 2.0 * minDist);
290 while (!activeList.empty() && positions.size() < n) {
292 std::uniform_int_distribution<std::size_t> activeDist(
293 0, activeList.size() - 1);
294 const std::size_t activeIdx = activeList[activeDist(gen)];
295 const Vector3d& activePoint = positions[activeIdx];
296 bool foundCandidate =
false;
299 for (
int attempt = 0; attempt < kRejectionSamples; ++attempt) {
302 const double theta = angleDist(gen);
303 const double phi = angleDist(gen);
304 const double radius = radiusDist(gen);
306 Vector3d offset(radius * std::sin(phi) * std::cos(theta),
307 radius * std::sin(phi) * std::sin(theta),
308 radius * std::cos(phi));
309 const Vector3d candidate = activePoint + offset;
312 if (grid.isValid(candidate, minDistSq, positions)) {
313 positions.push_back(candidate);
314 grid.add(positions.size() - 1, candidate);
315 activeList.push_back(positions.size() - 1);
316 foundCandidate =
true;
322 if (!foundCandidate) {
324 std::remove(activeList.begin(), activeList.end(), activeIdx),
330 const std::size_t nPlaced = positions.size();
333 <<
"Failed to generate" << n
334 <<
"non-overlapping positions. The density may be too high. Using"
335 << nPlaced <<
"bodies instead.";
339 bodies_.reserve(nPlaced);
340 for (std::size_t i = 0; i < nPlaced; ++i) {
341 bodies_.emplaceBack(masses[i], radii[i], positions[i], velocities[i],
342 angularVelocities[i]);
347 const std::size_t numBodies = nPlaced;
348 collisionGraph_.resize(numBodies);
349 graphLocks_.resize(numBodies);
350 for (std::size_t i = 0; i < numBodies; ++i) {
351 omp_init_lock(&graphLocks_[i]);
353 conflicts_.resize(numBodies);
356 thread_local_collision_pairs_.resize(omp_get_max_threads());
357 thread_local_search_results_.resize(omp_get_max_threads());
358 thread_local_used_colors_.resize(omp_get_max_threads());
359 thread_local_neighbour_colours_.resize(omp_get_max_threads());
364 const std::size_t reserve_size = (numBodies * 10) / omp_get_max_threads();
365 for (
auto& vec : thread_local_collision_pairs_) {
366 vec.reserve(reserve_size);
368 for (
auto& vec : thread_local_search_results_) {
371 for (
auto& vec : thread_local_neighbour_colours_) {
381 const double originalDt = dt_;
382 kdTree_.buildIndex();
383 previous_dt_ = epsilon_;
391 dt_ = previous_dt_ = originalDt;
394std::tuple<Model::Vector3dVec, Model::QuaterniondVec>
396 const std::size_t numBodies = n();
397 Vector3dVec positions;
398 QuaterniondVec quaternions;
400 positions.reserve(numBodies);
401 quaternions.reserve(numBodies);
403 positions.assign(bodies_.x_.begin(), bodies_.x_.begin() + numBodies);
404 quaternions.assign(bodies_.q_.begin(), bodies_.q_.begin() + numBodies);
406 return {positions, quaternions};
410 QuaterniondVec& out_quaternions,
411 Vector3dVec& out_torques,
412 Vector3dVec& out_alphas)
const {
413 const std::size_t numBodies = n();
415 out_positions.assign(bodies_.x_.begin(), bodies_.x_.begin() + numBodies);
416 out_quaternions.assign(bodies_.q_.begin(), bodies_.q_.begin() + numBodies);
417 out_torques.assign(bodies_.torque_.begin(),
418 bodies_.torque_.begin() + numBodies);
419 out_alphas.assign(bodies_.alpha_.begin(),
420 bodies_.alpha_.begin() + numBodies);
423void Model::Space::printProfilingReport()
const {
424 const double totalTime = profIntegration_ + profCollisionGraph_ +
425 profCollisionResponse_ + profForceCalculation_;
427 if (totalTime < epsilon_) {
428 qDebug().noquote() <<
"\n--- Profiling Report (No data) ---";
434 "\n--- Profiling Report ---\nIntegration: %1s "
435 "(%2%%)\nCollision Graph: %3s (%4%%)\nCollision Response: "
436 "%5s (%6%%)\nForce Calculation: %7s "
437 "(%8%%)\n------------------------\nTotal Instrumented Time:%9s")
438 .arg(profIntegration_, 9,
'f', 4)
439 .arg(profIntegration_ / totalTime * 100.0, 5,
'f', 1)
440 .arg(profCollisionGraph_, 9,
'f', 4)
441 .arg(profCollisionGraph_ / totalTime * 100.0, 5,
'f', 1)
442 .arg(profCollisionResponse_, 9,
'f', 4)
443 .arg(profCollisionResponse_ / totalTime * 100.0, 5,
'f', 1)
444 .arg(profForceCalculation_, 9,
'f', 4)
445 .arg(profForceCalculation_ / totalTime * 100.0, 5,
'f', 1)
446 .arg(totalTime, 9,
'f', 4);
447 qDebug().noquote() << msg;
451 double transKe = 0.0;
453 double potential = 0.0;
454 const std::size_t numBodies = n();
455 const double epsSq = epsilon_ * epsilon_;
462#pragma omp for simd reduction(+ : transKe, rotKe) nowait
463 for (std::size_t i = 0; i < numBodies; ++i) {
466 transKe += 0.5 * bodies_.m_[i] * bodies_.v_[i].squaredNorm();
468 const double iInv = bodies_.iInv_[i];
469 rotKe += (iInv > 0.0)
470 ? 0.5 * (1.0 / iInv) * bodies_.omega_[i].squaredNorm()
478#pragma omp for reduction(+ : potential) schedule(dynamic)
479 for (std::size_t i = 0; i < numBodies; ++i) {
480 const Vector3d& xi = bodies_.x_[i];
481 const double G_mi = G_ * bodies_.m_[i];
482 double local_potential = 0.0;
486#pragma omp simd reduction(+ : local_potential)
487 for (std::size_t j = i + 1; j < numBodies; ++j) {
488 const double dSq = (xi - bodies_.x_[j]).squaredNorm();
489 local_potential += (dSq > epsSq)
490 ? (G_mi * bodies_.m_[j] / std::sqrt(dSq))
493 potential -= local_potential;
497 return {transKe, rotKe, potential};
503 const double totalInvMass =
505 if (totalInvMass < epsilon_) {
510 constexpr double PERCENT = 0.8;
511 const Vector3d correction =
523 const Vector3d& direction_vec) {
525 const double term1 = (ctx.
r1Vec_.cross(direction_vec)).squaredNorm() *
528 const double term2 = (ctx.
r2Vec_.cross(direction_vec)).squaredNorm() *
534 return effectiveMassInv > epsilon_ ? 1.0 / effectiveMassInv : 0.0;
537std::tuple<Model::Vector3d, double, double>
539 const double vRelNormal = ctx.
vRel_.dot(ctx.
nVec_);
540 if (vRelNormal > 0) {
542 return {Vector3d::Zero(), 0.0, 0.0};
544 const double effectiveMassN = getEffectiveMass(ctx, ctx.
nVec_);
548 const double bias_jn = (positionalCorrectionFactor_ / previous_dt_) *
549 std::max(0.0, ctx.
overlap_ - 0.01) * effectiveMassN;
553 const double restitution_jn =
554 -(1.0 + coeffRestitution_) * vRelNormal * effectiveMassN;
558 const double jn = std::max(0.0, restitution_jn) + bias_jn;
560 return {jn * ctx.
nVec_, jn, bias_jn};
565 const Vector3d& impulseN) {
570 const Vector3d vT_initial =
572 const double vT_initialNorm = vT_initial.norm();
575 if (vT_initialNorm < epsilon_) {
576 return Vector3d::Zero();
580 const Vector3d tVec = vT_initial / vT_initialNorm;
581 const double effectiveMassT = getEffectiveMass(ctx, tVec);
590 const Vector3d torque1 = ctx.
r1Vec_.cross(-impulseN);
591 const Vector3d torque2 = ctx.
r2Vec_.cross(impulseN);
598 const Vector3d vRelAfterRestitution =
599 ((ctx.
b2.
v() + delta_v2) +
601 ((ctx.
b1.
v() + delta_v1) +
606 const Vector3d vTAfterRestitution =
607 vRelAfterRestitution - vRelAfterRestitution.dot(ctx.
nVec_) * ctx.
nVec_;
608 const double vTAfterRestitutionAlongT = vTAfterRestitution.dot(tVec);
612 const double jtRequiredMagnitude =
613 std::abs(vTAfterRestitutionAlongT) * effectiveMassT;
620 const double vRelNormal = ctx.
vRel_.dot(ctx.
nVec_);
621 const double effectiveMassN = getEffectiveMass(ctx, ctx.
nVec_);
622 const double restitution_jn =
623 (vRelNormal <= 0) ? std::max(0.0, -(1.0 + coeffRestitution_) *
624 vRelNormal * effectiveMassN)
630 const double jn_physical = restitution_jn;
631 const double jn_for_friction = (jn_physical < epsilon_) ? jn : jn_physical;
632 const double staticFrictionLimit = coeffStaticFriction_ * jn_for_friction;
635 if (jtRequiredMagnitude < staticFrictionLimit) {
639 jt = jtRequiredMagnitude;
645 jt = coeffFriction_ * jn_for_friction;
661 const Vector3d dVec = b2.
x() - b1.
x();
662 const double distSq = dVec.squaredNorm();
663 const double rSum = b1.
r() + b2.
r();
665 if (distSq >= rSum * rSum || distSq < epsilon_ * epsilon_) {
669 const double dist = std::sqrt(distSq);
670 const Vector3d nVec = dVec / dist;
671 const Vector3d r1Vec = b1.
r() * nVec;
672 const Vector3d r2Vec = -b2.
r() * nVec;
673 const Vector3d vRel =
674 (b2.
v() + b2.
omega().cross(r2Vec)) - (b1.
v() + b1.
omega().cross(r1Vec));
675 const double overlap = (b1.
r() + b2.
r()) - dist;
678 r2Vec, vRel, overlap};
682 auto initial_ctx = createCollisionContext(b1, b2);
690 resolveInterpenetration(*initial_ctx);
694 auto context_opt = createCollisionContext(b1, b2);
700 const auto [impulseN, jn, bias_jn] = applyRestitutionImpulse(ctx);
704 const Vector3d impulseT = applyFrictionImpulse(ctx, jn, impulseN);
705 const Vector3d totalImpulse = impulseN + impulseT;
714 const std::vector<std::vector<std::size_t>>& in_graph) {
717 const std::size_t numBodies = n();
718 colors_.assign(numBodies, 0);
719 std::fill(conflicts_.begin(), conflicts_.end(), 1);
720 bool hasConflicts =
true;
722 while (hasConflicts) {
723 hasConflicts =
false;
727 const int tid = omp_get_thread_num();
728 auto& usedColors = thread_local_used_colors_[tid];
729 auto& neighbourColours = thread_local_neighbour_colours_[tid];
734 for (std::size_t i = 0; i < numBodies; ++i) {
735 if (!conflicts_[i])
continue;
737 neighbourColours.clear();
738 for (
const auto& neighbourIdx : in_graph[i]) {
740#pragma omp atomic read
741 neighbor_color = colors_[neighbourIdx];
742 neighbourColours.push_back(neighbor_color);
746 std::fill(usedColors.begin(), usedColors.end(), 0);
748 for (
const int neighbourColour : neighbourColours) {
751 if (neighbourColour >=
752 static_cast<int>(usedColors.size())) {
753 usedColors.resize(neighbourColour + 2, 0);
755 usedColors[neighbourColour] = 1;
759 while (newColor <
static_cast<int>(usedColors.size()) &&
760 usedColors[newColor]) {
766 if (newColor >=
static_cast<int>(usedColors.size())) {
771#pragma omp atomic write
772 colors_[i] = newColor;
776#pragma omp for reduction(| : hasConflicts)
777 for (std::size_t i = 0; i < numBodies; ++i) {
779 for (
const auto& neighbourIdx : in_graph[i]) {
780 if (colors_[i] == colors_[neighbourIdx]) {
792 if (!diagnosticsEnabled_ || iteration == 0 || iteration % logFreq_ != 0) {
796 const auto [transKe, rotKe, potential] = calculateSystemEnergy();
797 const double totalEnergy = transKe + rotKe + potential;
800 const QString msg = QString(
801 "\n--- Iteration %1: System Energy ---\n "
802 "Translational KE: %2\n "
803 "Rotational KE: %3\n Potential Energy: "
804 "%4\n Total Energy: "
805 "%5\n Time Step (dt): %6")
807 .arg(transKe, 12,
'e', 4)
808 .arg(rotKe, 12,
'e', 4)
809 .arg(potential, 12,
'e', 4)
810 .arg(totalEnergy, 12,
'e', 4)
811 .arg(getPreviousTimeStep(), 12,
'e', 4);
812 qDebug().noquote() << msg;
815 if (totalEnergy > lastTotalEnergy_ + epsilon_) {
817 <<
"\n========================================"
818 <<
"\n Total energy increased by "
819 << QString::asprintf(
"%.4e", totalEnergy - lastTotalEnergy_)
820 <<
"\n========================================";
822 lastTotalEnergy_ = totalEnergy;
828 const std::size_t numBodies = n();
830 if (collisionGraph_.size() != numBodies) {
831 collisionGraph_.resize(numBodies);
836 kdTree_.buildIndex();
842 if (cached_max_radius_ == 0.0) {
843 cached_max_radius_ = 0.0;
844#pragma omp parallel for reduction(max : cached_max_radius_)
845 for (std::size_t i = 0; i < numBodies; ++i) {
846 cached_max_radius_ = std::max(cached_max_radius_, bodies_.r_[i]);
849 const double max_radius = cached_max_radius_;
854 const int tid = omp_get_thread_num();
858 thread_local_collision_pairs_[tid].clear();
863#pragma omp for schedule(static) nowait
864 for (std::size_t i = 0; i < numBodies; ++i) {
865 collisionGraph_[i].clear();
869 auto& results = thread_local_search_results_[tid];
871#pragma omp for schedule(dynamic)
872 for (std::size_t i = 0; i < numBodies; ++i) {
874 const double r_i = bodies_.r_[i];
875 const Vector3d& x_i = bodies_.x_[i];
876 const double search_radius = r_i + max_radius;
880 kdTree_.radiusSearch(x_i.data(), search_radius, results);
885 for (
const auto& result : results) {
886 const std::size_t j = result.first;
887 if (i >= j)
continue;
889 const double rSum = r_i + bodies_.r_[j];
890 const double distSq = result.second;
891 if (distSq < rSum * rSum) {
892 thread_local_collision_pairs_[tid].push_back({i, j});
899 for (
const auto& localPairs : thread_local_collision_pairs_) {
900 for (
const auto& pair : localPairs) {
901 collisionGraph_[pair.first].emplace_back(pair.second);
902 collisionGraph_[pair.second].emplace_back(pair.first);
909 logSystemEnergy(iteration);
916 const double current_dt = dt_;
918 const auto startTime = ProfilingClock::now();
919 bodies_.integratePart1(dt_, previous_dt_);
921 std::chrono::duration<double>(ProfilingClock::now() - startTime)
927 const auto startTime = ProfilingClock::now();
928 buildCollisionGraph();
929 profCollisionGraph_ +=
930 std::chrono::duration<double>(ProfilingClock::now() - startTime)
936 const auto startTime = ProfilingClock::now();
938 profCollisionResponse_ +=
939 std::chrono::duration<double>(ProfilingClock::now() - startTime)
944 updateAdaptiveTimeStep();
948 const auto startTime = ProfilingClock::now();
949 applyGravity(current_dt, linearDamping_, angularDamping_);
950 profForceCalculation_ +=
951 std::chrono::duration<double>(ProfilingClock::now() - startTime)
957 const std::size_t numBodies = n();
958 if (numBodies == 0)
return;
961 colorGraph(collisionGraph_);
962 const int numColours =
964 : *std::max_element(colors_.begin(), colors_.end()) + 1;
969 for (
int c = 0; c < numColours; ++c) {
970#pragma omp for schedule(dynamic)
971 for (std::size_t i = 0; i < numBodies; ++i) {
972 if (colors_[i] != c)
continue;
974 for (
const auto& j : collisionGraph_[i]) {
980 handleCollision(b1, b2);
993 double maxOmegaSq = 0.0;
994 double maxAlphaSq = 0.0;
997 std::size_t fastestBodyIdx = 0;
998 const std::size_t numBodies = n();
1002 double localMaxVSq = 0.0;
1003 std::size_t localFastestBodyIdx = 0;
1005#pragma omp for reduction(max : maxASq, maxOmegaSq, maxAlphaSq) schedule(static)
1006 for (std::size_t i = 0; i < numBodies; ++i) {
1008 const double aSq = bodies_.a_[i].squaredNorm();
1009 const double vSq = bodies_.v_[i].squaredNorm();
1010 const double omegaSq = bodies_.omega_[i].squaredNorm();
1011 const double alphaSq = bodies_.alpha_[i].squaredNorm();
1013 maxASq = std::max(maxASq, aSq);
1014 maxOmegaSq = std::max(maxOmegaSq, omegaSq);
1015 maxAlphaSq = std::max(maxAlphaSq, alphaSq);
1017 if (vSq > localMaxVSq) {
1019 localFastestBodyIdx = i;
1025 if (localMaxVSq > maxVSq) {
1026 maxVSq = localMaxVSq;
1027 fastestBodyIdx = localFastestBodyIdx;
1035 if (maxASq > epsilon_ * epsilon_) {
1036 dtA = std::sqrt(2.0 *
TARGET_DX / std::sqrt(maxASq));
1042 if (maxVSq > epsilon_ * epsilon_) {
1043 dtV = (0.5 * bodies_.r_[fastestBodyIdx]) / std::sqrt(maxVSq);
1051 constexpr double MAX_ANGLE = 0.1;
1054 if (maxOmegaSq > epsilon_ * epsilon_) {
1058 dtOmega = MAX_ANGLE / std::sqrt(maxOmegaSq);
1064 if (maxAlphaSq > epsilon_ * epsilon_) {
1068 dtAlpha = std::sqrt(2.0 * MAX_ANGLE / std::sqrt(maxAlphaSq));
1074 const double targetDt = std::min({
MAX_DT, dtA, dtV, dtOmega, dtAlpha});
1079 double angular_damping) {
1080 const std::size_t numBodies = n();
1081 const double epsSq = epsilon_ * epsilon_;
1083#pragma omp parallel for schedule(dynamic)
1084 for (std::size_t i = 0; i < numBodies; ++i) {
1088 const Vector3d& xi = bodies_.x_[i];
1092#pragma omp simd reduction(+ : fx, fy, fz)
1093 for (std::size_t j = 0; j < numBodies; ++j) {
1094 const Vector3d dv = bodies_.x_[j] - xi;
1095 const double dSq = dv.squaredNorm();
1099 const double safe_dSq = (dSq > epsSq) ? dSq : 1.0;
1100 const double force_mag =
1102 ? (G_ * bodies_.m_[j] / (safe_dSq * std::sqrt(safe_dSq)))
1105 fx += dv.x() * force_mag;
1106 fy += dv.y() * force_mag;
1107 fz += dv.z() * force_mag;
1111 bodies_.a_[i] = Vector3d(fx, fy, fz);
1116 bodies_.alpha_[i] = bodies_.torque_[i] * bodies_.iInv_[i];
1117 bodies_.torque_[i].setZero();
1121 (bodies_.omega_[i] + 0.5 * bodies_.alpha_[i] * current_dt) *
1122 (1.0 - angular_damping);
1123 bodies_.v_[i] = (bodies_.v_[i] + 0.5 * bodies_.a_[i] * current_dt) *
1124 (1.0 - linear_damping);
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 resolveInterpenetration(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 getDataForDisplay(Vector3dVec &out_positions, QuaterniondVec &out_quaternions, Vector3dVec &out_torques, Vector3dVec &out_alphas) const
Retrieves all data required for display for all bodies.
void computeDynamics(std::size_t iteration)
Computes one full step of the simulation.
void applyGravity(double current_dt, double linear_damping, double angular_damping)
Calculates and applies gravitational forces, and completes Velocity Verlet part 2.
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 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.