5#include <glm/gtc/quaternion.hpp>
17float tetraVolume(
const V3&
a,
const V3&
b,
const V3&
c,
const V3&
d) {
18 return std::fabs(glm::dot(
b -
a, glm::cross(
c -
a,
d -
a))) / 6.f;
21glm::quat bestFitRotation(
const glm::mat3& covariance) {
22 glm::quat
rotation(1.f, 0.f, 0.f, 0.f);
23 for (
int i = 0; i < 8; ++i) {
24 const glm::mat3 basis = glm::mat3_cast(
rotation);
25 const V3 omega = (glm::cross(basis[0], covariance[0]) + glm::cross(basis[1], covariance[1]) +
26 glm::cross(basis[2], covariance[2])) /
27 (std::fabs(glm::dot(basis[0], covariance[0]) + glm::dot(basis[1], covariance[1]) +
28 glm::dot(basis[2], covariance[2])) +
30 const float magnitude = glm::length(omega);
31 if (magnitude < 1e-6f)
break;
32 rotation = glm::normalize(glm::angleAxis(magnitude, omega / magnitude) *
rotation);
40 float originX,
float originY,
float originZ) {
42 definition.cols =
cols;
43 definition.rows =
rows;
44 definition.layers = layers;
46 definition.originX = originX;
47 definition.originY = originY;
48 definition.originZ = originZ;
55 auto valid = definition.validate();
57 auto result = std::unique_ptr<SoftBody3D>(
new SoftBody3D(definition.cols, definition.rows, definition.layers,
58 definition.spacing, definition.originX, definition.originY,
60 result->setGravity(definition.gravityX, definition.gravityY, definition.gravityZ);
61 result->setDeformationResistance(definition.deformationResistance);
62 result->setIterations(definition.iterations);
63 result->setDamping(definition.damping);
64 result->setParticleRadius(definition.particleRadius);
65 result->setParticleMass(definition.particleMass);
66 result->setPlasticity(definition.plasticYield, definition.plasticCreep, definition.plasticRecovery,
67 definition.maxDeformation);
68 result->setSelfCollision(definition.selfCollision);
74 particles_.reserve(
static_cast<size_t>(cols_) * rows_ * layers_);
75 for (
int z = 0;
z < layers_; ++
z) {
76 for (
int y = 0;
y < rows_; ++
y) {
77 for (
int x = 0;
x < cols_; ++
x) {
78 const float px = originX + float(
x) * spacing_;
79 const float py = originY + float(
y) * spacing_;
80 const float pz = originZ + float(
z) * spacing_;
85 clusters_.reserve(
static_cast<size_t>(cols_ - 1) * (rows_ - 1) * (layers_ - 1));
86 for (
int z = 0;
z + 1 < layers_; ++
z) {
87 for (
int y = 0;
y + 1 < rows_; ++
y) {
88 for (
int x = 0;
x + 1 < cols_; ++
x) {
90 cluster.indices[0] = indexOf(
x,
y,
z);
91 cluster.indices[1] = indexOf(
x + 1,
y,
z);
92 cluster.indices[2] = indexOf(
x,
y + 1,
z);
93 cluster.indices[3] = indexOf(
x + 1,
y + 1,
z);
94 cluster.indices[4] = indexOf(
x,
y,
z + 1);
95 cluster.indices[5] = indexOf(
x + 1,
y,
z + 1);
96 cluster.indices[6] = indexOf(
x,
y + 1,
z + 1);
97 cluster.indices[7] = indexOf(
x + 1,
y + 1,
z + 1);
98 clusters_.push_back(cluster);
102 particleRadius_ = spacing_ * 0.35f;
110 collisionWorld_ =
nullptr;
111 ownedCollisionWorld_.reset();
114bool SoftBody3D::validIndex(
int index)
const noexcept {
return index >= 0 &&
index < getParticleCount(); }
116int SoftBody3D::indexOf(
int x,
int y,
int z)
const noexcept {
return (
z * rows_ +
y) * cols_ +
x; }
119 gravityX_ = std::isfinite(
x) ?
x : 0.f;
120 gravityY_ = std::isfinite(
y) ?
y : 0.f;
121 gravityZ_ = std::isfinite(
z) ?
z : 0.f;
131 if (std::isfinite(
value)) particleRadius_ = std::max(0.f,
value);
135 if (std::isfinite(
value)) particleMass_ = std::max(1e-5f,
value);
139 plasticYield_ = std::max(0.f, std::isfinite(yield) ? yield : 0.f);
140 plasticCreep_ = std::clamp(std::isfinite(creep) ? creep : 0.f, 0.f, 1.f);
141 plasticRecovery_ = std::max(0.f, std::isfinite(recovery) ? recovery : 0.f);
142 maxDeformation_ = std::max(0.f, std::isfinite(maxDeformation) ? maxDeformation : 0.f);
155 boundW_ = std::max(0.f,
width);
156 boundH_ = std::max(0.f,
height);
157 boundD_ = std::max(0.f,
depth);
158 hasBounds_ = std::isfinite(
x) && std::isfinite(
y) && std::isfinite(
z) && std::isfinite(
width) &&
163 if (!validIndex(
index))
throw Exception(
"SoftBody3D.pin: index out of range");
164 particles_[
static_cast<size_t>(
index)].
pinned =
true;
167 if (!validIndex(
index))
throw Exception(
"SoftBody3D.unpin: index out of range");
168 particles_[
static_cast<size_t>(
index)].
pinned =
false;
171 if (!validIndex(
index))
throw Exception(
"SoftBody3D.isPinned: index out of range");
172 return particles_[
static_cast<size_t>(
index)].
pinned;
179 const Particle&
p = particles_[
static_cast<size_t>(i)];
181 const float distanceSquared =
dx *
dx +
dy *
dy +
dz *
dz;
182 if (!
p.pinned && distanceSquared <=
best) {
183 best = distanceSquared;
195 if (grabIndex_ < 0)
return;
199 Particle&
p = particles_[
static_cast<size_t>(grabIndex_)];
206 if (std::isfinite(
x)) forceX_ +=
x;
207 if (std::isfinite(
y)) forceY_ +=
y;
208 if (std::isfinite(
z)) forceZ_ +=
z;
221 if (!validIndex(
index))
throw Exception(
"SoftBody3D.getParticleX: index out of range");
222 return particles_[
static_cast<size_t>(
index)].
x;
225 if (!validIndex(
index))
throw Exception(
"SoftBody3D.getParticleY: index out of range");
226 return particles_[
static_cast<size_t>(
index)].
y;
229 if (!validIndex(
index))
throw Exception(
"SoftBody3D.getParticleZ: index out of range");
230 return particles_[
static_cast<size_t>(
index)].
z;
233 if (!validIndex(
index))
throw Exception(
"SoftBody3D.setParticlePosition: index out of range");
234 if (!std::isfinite(
x) || !std::isfinite(
y) || !std::isfinite(
z))
235 throw Exception(
"SoftBody3D.setParticlePosition: coordinates must be finite");
236 Particle&
p = particles_[
static_cast<size_t>(
index)];
243 for (Particle&
p : particles_) {
249 for (Cluster& cluster : clusters_)
252 forceX_ = forceY_ = forceZ_ = 0.f;
253 hasInteraction_ =
false;
256void SoftBody3D::integrate(
float dt) {
257 const float velocityScale = std::max(0.f, 1.f - damping_);
259 Particle&
p = particles_[
static_cast<size_t>(i)];
260 if (
p.pinned || i == grabIndex_)
continue;
261 float ax = gravityX_ + forceX_ / particleMass_;
262 float ay = gravityY_ + forceY_ / particleMass_;
263 float az = gravityZ_ + forceZ_ / particleMass_;
264 if (hasInteraction_) {
265 const V3 delta = V3(interactX_ -
p.x, interactY_ -
p.y, interactZ_ -
p.z);
266 const float distance = glm::length(delta);
268 const float falloff = 1.f -
distance / interactRadius_;
269 const V3 field = delta * (interactStrength_ * falloff /
distance);
275 const float vx = (
p.x -
p.px) * velocityScale;
276 const float vy = (
p.y -
p.py) * velocityScale;
277 const float vz = (
p.z -
p.pz) * velocityScale;
281 p.x +=
vx +
ax * dt * dt;
282 p.y +=
vy +
ay * dt * dt;
283 p.z +=
vz +
az * dt * dt;
287void SoftBody3D::solveShapeMatching(
float dt) {
288 const float stiffness = 1.f - std::pow(1.f - deformationResistance_, 1.f /
float(iterations_));
289 for (Cluster& cluster : clusters_) {
290 V3 currentCom(0.f), restCom(0.f);
291 for (
int i = 0; i < 8; ++i) {
292 const Particle&
p = particles_[
static_cast<size_t>(cluster.indices[i])];
293 currentCom += V3(
p.x,
p.y,
p.z);
294 restCom += V3(
p.rx + cluster.plastic[i][0],
p.ry + cluster.plastic[i][1],
p.rz + cluster.plastic[i][2]);
298 glm::mat3 covariance(0.f);
299 for (
int i = 0; i < 8; ++i) {
300 const Particle&
p = particles_[
static_cast<size_t>(cluster.indices[i])];
301 const V3
current = V3(
p.x,
p.y,
p.z) - currentCom;
303 V3(
p.rx + cluster.plastic[i][0],
p.ry + cluster.plastic[i][1],
p.rz + cluster.plastic[i][2]) - restCom;
304 covariance += glm::outerProduct(
current, rest);
306 const glm::mat3
rotation = glm::mat3_cast(bestFitRotation(covariance));
307 float deformation = 0.f;
309 for (
int i = 0; i < 8; ++i) {
310 Particle&
p = particles_[
static_cast<size_t>(cluster.indices[i])];
311 restCurrent[i] = glm::transpose(
rotation) * (V3(
p.x,
p.y,
p.z) - currentCom);
313 V3(
p.rx + cluster.plastic[i][0],
p.ry + cluster.plastic[i][1],
p.rz + cluster.plastic[i][2]) - restCom;
314 deformation += glm::length(restCurrent[i] - rest) / spacing_;
315 if (!
p.pinned && cluster.indices[i] != grabIndex_) {
316 const V3 goal = currentCom +
rotation * rest;
317 p.x += (goal.x -
p.x) * stiffness;
318 p.y += (goal.y -
p.y) * stiffness;
319 p.z += (goal.z -
p.z) * stiffness;
323 const float creep = deformation > plasticYield_ ? plasticCreep_ * dt : 0.f;
324 const float recovery = plasticRecovery_ * dt;
325 if (maxDeformation_ > 0.f && (creep > 0.f || recovery > 0.f)) {
326 for (
int i = 0; i < 8; ++i) {
327 const Particle&
p = particles_[
static_cast<size_t>(cluster.indices[i])];
328 const V3 original = V3(
p.rx,
p.ry,
p.rz) - restCom;
329 V3
offset(cluster.plastic[i][0], cluster.plastic[i][1], cluster.plastic[i][2]);
330 offset += (restCurrent[i] - original -
offset) * std::min(1.f, creep);
332 const float limit = maxDeformation_ * spacing_;
335 cluster.plastic[i][0] =
offset.x;
336 cluster.plastic[i][1] =
offset.y;
337 cluster.plastic[i][2] =
offset.z;
343void SoftBody3D::solveSelfCollision() {
344 if (!selfCollision_ || particleRadius_ <= 0.f)
return;
345 const float minimum = particleRadius_ * 2.f;
349 Particle&
a = particles_[
static_cast<size_t>(i)];
350 Particle&
b = particles_[
static_cast<size_t>(j)];
351 V3 delta(
b.x -
a.x,
b.y -
a.y,
b.z -
a.z);
353 if (distanceSquared >= minimumSquared || distanceSquared < 1e-12f)
continue;
354 const float distance = std::sqrt(distanceSquared);
356 if (!
a.pinned && i != grabIndex_) {
361 if (!
b.pinned && j != grabIndex_) {
370void SoftBody3D::collideWorld(
float dt) {
372 softbody::SoftBodyContact contact;
373 const float inverseDt = dt > 1e-6f ? 1.f / dt : 0.f;
374 for (Particle&
p : particles_) {
379 const V3 velocity = V3(
p.x -
p.px,
p.y -
p.py,
p.z -
p.pz) * inverseDt;
380 p.x += contact.nx * contact.depth;
381 p.y += contact.ny * contact.depth;
382 p.z += contact.nz * contact.depth;
383 if (contact.dynamicBody) {
384 const float normalVelocity = glm::dot(velocity, V3(contact.nx, contact.ny, contact.nz));
385 if (normalVelocity < 0.f) {
386 const float impulse = std::min(-normalVelocity * particleMass_, particleMass_ * 8.f);
387 collisionWorld_->
applySoftBodyImpulse(contact.bodyId, -contact.nx * impulse, -contact.ny * impulse,
388 -contact.nz * impulse);
394void SoftBody3D::collideBounds() {
395 if (!hasBounds_)
return;
396 const V3
minimum(boundX_, boundY_, boundZ_);
397 const V3
maximum(boundX_ + boundW_, boundY_ + boundH_, boundZ_ + boundD_);
398 for (Particle&
p : particles_) {
399 if (
p.pinned)
continue;
407 if (destroyed_)
return;
408 updateSubsteps(std::clamp(dt, 0.f, 0.05f), 2);
411void SoftBody3D::updateSubsteps(
float dt,
int substeps) {
412 if (destroyed_ || substeps < 1)
return;
413 const float h = dt / float(substeps);
414 for (
int substep = 0; substep < substeps; ++substep) {
416 for (
int iteration = 0; iteration < iterations_; ++iteration) {
417 solveShapeMatching(
h);
418 solveSelfCollision();
422 if (grabIndex_ >= 0)
moveGrab(grabX_, grabY_, grabZ_);
424 forceX_ = forceY_ = forceZ_ = 0.f;
425 hasInteraction_ =
false;
431 "Cannot step a destroyed soft body",
432 "physics.softbody3d.step"));
433 auto valid = detail::validateSimulationStep(stepValue,
settings, observation_);
435 auto next = detail::advanceSimulationObservation(observation_, stepValue);
438 observation_ = std::move(next).takeValue();
443 auto valid = detail::validateSimulationObservation(
observation,
"physics.softbody3d.restoreObservation");
447 "Cannot restore a destroyed soft body",
448 "physics.softbody3d.restoreObservation"));
454 if (clusters_.empty())
return 1.f;
456 for (
const Cluster& cluster : clusters_) {
458 for (
int i = 0; i < 8; ++i) {
459 const Particle& particle = particles_[
static_cast<size_t>(cluster.indices[i])];
460 p[i] = V3(particle.x, particle.y, particle.z);
462 volume += tetraVolume(
p[0],
p[1],
p[3],
p[7]) + tetraVolume(
p[0],
p[3],
p[2],
p[7]) +
463 tetraVolume(
p[0],
p[2],
p[6],
p[7]) + tetraVolume(
p[0],
p[6],
p[4],
p[7]) +
464 tetraVolume(
p[0],
p[4],
p[5],
p[7]) + tetraVolume(
p[0],
p[5],
p[1],
p[7]);
466 const float rest = float(clusters_.size()) * spacing_ * spacing_ * spacing_;
467 return rest > 0.f ?
volume / rest : 1.f;
std::array< float, 4 > rotation
std::map< Cell, int > best
TerrainThermalSettings settings
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
EVENGINE_API_FOUNDATION public API.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Volumetric particle soft body using overlapping shape-matching clusters.
int getParticleCount() const
void applyForce(float x, float y, float z)
Applies force.
void setParticleMass(float value)
Sets the particle mass.
void update(float dt)
Advances using a legacy variable timestep clamped to 50 ms.
void setBounds(float x, float y, float z, float width, float height, float depth)
Sets the bounds.
SimulationObservation observation() const noexcept override
Returns completed fixed-step metadata by value.
float getParticleZ(int index) const
void setIterations(int value)
Sets the iterations.
void setDeformationResistance(float value)
Sets the deformation resistance.
float getPlasticCreep() const
Returns the plastic creep.
eve::Result< void > step(const eve::SimulationStep &step, const SimulationSettings &settings) override
Advances through the checked fixed-step backend contract.
~SoftBody3D() override
Soft body 3 d.
float getMaxDeformation() const
Returns the max deformation.
float getVolumeRatio() const
Returns current cell volume divided by undeformed rest volume.
float getPlasticYield() const
Returns the plastic yield.
void moveGrab(float x, float y, float z)
Moves grab.
void setGravity(float x, float y, float z)
Sets the gravity.
void setPlasticity(float yield, float creep, float recovery, float maxDeformation)
Sets the plasticity.
void setParticlePosition(int index, float x, float y, float z)
float getDeformationResistance() const
Returns the deformation resistance.
static eve::Result< std::unique_ptr< SoftBody3D > > create(const SoftBody3DDefinition &definition)
Creates a soft body from the canonical versioned definition.
void interactAt(float x, float y, float z, float radius, float strength)
Interact at.
SoftBody3D(const SoftBody3D &)=delete
float getPlasticRecovery() const
Returns the plastic recovery.
eve::Result< void > restoreObservation(const SimulationObservation &observation) override
Restores checked progress metadata; particle state is unchanged.
void setParticleRadius(float value)
Sets the particle radius.
void setDamping(float value)
Sets the damping.
bool isPinned(int index) const
True when pinned.
float getParticleX(int index) const
float getParticleY(int index) const
int grabAt(float x, float y, float z, float radius)
Grab at.
void unpin(int index)
Unpin.
float getParticleRadius() const
Returns the particle radius.
virtual void applySoftBodyImpulse(int bodyId, float x, float y, float z)=0
Apply a world-space impulse to a previously reported dynamic body id.
virtual SoftBodyContactState probeSoftBodyParticle(float x, float y, float z, float radius, SoftBodyContact &contact) const =0
Probe the deepest non-sensor contact for a spherical particle.
virtual bool softBodyCollisionAvailable() const noexcept=0
Whether the provider can currently answer contacts.
Optional physics backend for vehicle mobility and body attach.
float distanceSquared(float ax, float ay, float bx, float by)
One deterministic fixed-step emitted by SimulationClock.
Duration delta
Fixed simulation duration for this step.
Observable backend progress shared by CPU and accelerator providers.
Validated solver policy for one simulation step.
Versioned, owning creation definition for one volumetric soft body.
static eve::Result< void > ensureSchemaRegistered()
Idempotently register version 1 in the process schema registry.