11using V = Rope3D::Vec3;
12V add(V
a, V
b) {
return {
a.x +
b.x,
a.y +
b.y,
a.z +
b.z}; }
13V sub(V
a, V
b) {
return {
a.x -
b.x,
a.y -
b.y,
a.z -
b.z}; }
14V mul(V
a,
float s) {
return {
a.x *
s,
a.y *
s,
a.z *
s}; }
15float dot(V
a, V
b) {
return a.x *
b.x +
a.y *
b.y +
a.z *
b.z; }
17bool finite(V
v) {
return std::isfinite(
v.x) && std::isfinite(
v.y) && std::isfinite(
v.z); }
24 throw Exception(
"Rope3D: requires at least two particles and distinct finite endpoints");
25 particles_.resize(
static_cast<std::size_t
>(
count));
26 elements_.resize(
static_cast<std::size_t
>(
count - 1));
27 for (
int i = 0; i <
count; ++i) {
28 const float t =
static_cast<float>(i) /
static_cast<float>(
count - 1);
29 auto&
p = particles_[
static_cast<std::size_t
>(i)];
32 auto& e = elements_[
static_cast<std::size_t
>(i)];
42 if (!std::isfinite(dt) || dt < 0.f)
throw Exception(
"Rope3D: dt must be finite and non-negative");
43 if (dt == 0.f)
return;
44 lastTornElements_.clear();
45 dt = std::clamp(dt, 0.f, 0.05f);
46 constexpr int substeps = 4;
47 for (
int substep = 0; substep < substeps; ++substep) {
48 const float h = dt /
static_cast<float>(substeps);
51 collideBorrowedSurfaces(
h);
52 if (bendConstraintsEnabled_) updatePlasticity(
h);
53 for (
int iteration = 0; iteration < 3; ++iteration) {
54 if (distanceConstraintsEnabled_) solveStretch(
h);
55 if (bendConstraintsEnabled_) solveBend(
h);
56 if (selfCollision_) solveSelfCollision();
57 solveExternalCollisions();
67 auto validation = detail::validateSimulationStep(
tick,
settings, observation_);
68 if (!validation)
return validation;
69 const float dt =
static_cast<float>(
tick.delta.seconds());
70 const auto oldParticles = particles_;
71 const auto oldElements = elements_;
72 const auto oldBendPlasticity = bendPlasticity_;
73 const auto oldTopologyRevision = topologyRevision_;
74 const auto oldTornElements = lastTornElements_;
75 lastTornElements_.clear();
76 for (
int substep = 0; substep <
settings.subStepCount; ++substep) {
77 const float h = dt /
static_cast<float>(
settings.subStepCount);
80 collideBorrowedSurfaces(
h);
81 if (bendConstraintsEnabled_) updatePlasticity(
h);
82 for (
int iteration = 0; iteration <
settings.positionIterations; ++iteration) {
83 if (distanceConstraintsEnabled_) solveStretch(
h);
84 if (bendConstraintsEnabled_) solveBend(
h);
85 if (selfCollision_) solveSelfCollision();
86 solveExternalCollisions();
93 auto next = detail::advanceSimulationObservation(observation_,
tick);
95 particles_ = oldParticles;
96 elements_ = oldElements;
97 bendPlasticity_ = oldBendPlasticity;
98 topologyRevision_ = oldTopologyRevision;
99 lastTornElements_ = oldTornElements;
102 observation_ = std::move(next).takeValue();
107 auto result = detail::validateSimulationObservation(
observation,
"physics.rope3d.observation");
108 if (!result)
return result;
115 gravity_ = {
x,
y,
z};
119 throw Exception(
"Rope3D: stretch compliance must be finite and non-negative");
120 stretchCompliance_ =
value;
124 throw Exception(
"Rope3D: bend compliance must be finite and non-negative");
125 bendCompliance_ =
value;
128 if (!std::isfinite(
value) || value < 0.f || value > 0.5f)
throw Exception(
"Rope3D: max bending must be in [0,0.5]");
132 if (!std::isfinite(yield) || yield < 0.f || yield > 0.5f || !std::isfinite(creep) || creep < 0.f)
133 throw Exception(
"Rope3D: plastic yield must be in [0,0.5] and creep must be non-negative");
134 plasticYield_ = yield;
135 plasticCreep_ = creep;
138 return index >= 0 &&
static_cast<std::size_t
>(
index) < bendPlasticity_.size()
139 ? bendPlasticity_[
static_cast<std::size_t
>(
index)]
143 if (!std::isfinite(
value) || value < 0.f || value > 1.f)
144 throw Exception(
"Rope3D: max compression must be in [0,1]");
145 maxCompression_ =
value;
148 if (!std::isfinite(
value) || value < 0.f || value > 1.f)
throw Exception(
"Rope3D: damping must be in [0,1]");
152 if (!std::isfinite(
value) ||
value <= 0.f)
throw Exception(
"Rope3D: particle mass must be positive");
153 particleMass_ =
value;
156 if (!std::isfinite(
value) ||
value <= 0.f)
throw Exception(
"Rope3D: radius must be positive");
161 throw Exception(
"Rope3D: bounds extents must be finite and positive");
162 boundsMin_ = {
x,
y,
z};
163 boundsMax_ = {
x +
w,
y +
h,
z +
d};
168 if (!validParticle(i))
171 const auto&
p = particles_[
static_cast<std::size_t
>(i)];
172 return attach(i,
p.position.x,
p.position.y,
p.position.z);
175 if (!validParticle(i) || !
finite({
x,
y,
z}))
178 "Invalid particle index or attachment position",
"physics.rope3d.attach"));
179 auto&
p = particles_[
static_cast<std::size_t
>(i)];
180 const bool changed = !
p.attached ||
p.attachment.x !=
x ||
p.attachment.y !=
y ||
p.attachment.z !=
z;
182 p.attachment =
p.position =
p.previous = {
x,
y,
z};
187 if (!validParticle(i) || !particles_[
static_cast<std::size_t
>(i)].attached || !
finite({
x,
y,
z}))
190 "Attachment does not exist or target is invalid",
"physics.rope3d.moveAttachment"));
194 if (!validParticle(i))
197 auto&
p = particles_[
static_cast<std::size_t
>(i)];
200 p.previous =
p.position;
203bool Rope3D::isAttached(
int i)
const {
return validParticle(i) && particles_[
static_cast<std::size_t
>(i)].attached; }
206 force_ = add(force_, {
x,
y,
z});
211 if (!std::isfinite(requested) || requested <= 0.f ||
current <= 0.f)
214 "physics.rope3d.setRestLength"));
215 if (std::abs(requested -
current) <= 1e-6f)
218 for (
auto& e : elements_)
219 if (e.active) e.restLength *=
scale;
225 for (
const auto& e : elements_)
226 if (e.active) sum += e.restLength;
231 for (
const auto& e : elements_)
232 if (e.active) sum +=
length(sub(particles_[e.b].position, particles_[e.a].position));
237 if (!validElement(i))
240 auto& e = elements_[
static_cast<std::size_t
>(i)];
249 if (!validElement(i))
252 auto& e = elements_[
static_cast<std::size_t
>(i)];
254 e.restLength = std::max(
length(sub(particles_[e.b].position, particles_[e.a].position)), 1e-5f);
261 return validElement(i) ? elements_[
static_cast<std::size_t
>(i)].force : 0.f;
264 if (!validElement(i) || !std::isfinite(multiplier) || multiplier <= 0.f)
267 "physics.rope3d.setElementTearResistance"));
268 auto&
element = elements_[
static_cast<std::size_t
>(i)];
269 if (
element.tearResistance == multiplier)
271 element.tearResistance = multiplier;
275 if (!std::isfinite(resistance) || resistance <= 0.f || maxTears < 1)
276 throw Exception(
"Rope3D: tearing requires positive resistance and tear count");
277 tearingEnabled_ =
true;
278 tearResistance_ = resistance;
279 maxTearsPerStep_ = maxTears;
286 return validParticle(i) && dt > 0 ? (particles_[i].position.x - particles_[i].previous.x) / dt : 0.f;
289 return validParticle(i) && dt > 0 ? (particles_[i].position.y - particles_[i].previous.y) / dt : 0.f;
292 return validParticle(i) && dt > 0 ? (particles_[i].position.z - particles_[i].previous.z) / dt : 0.f;
294bool Rope3D::validParticle(
int i)
const noexcept {
return i >= 0 && i < getParticleCount(); }
295bool Rope3D::validElement(
int i)
const noexcept {
return i >= 0 && i < getElementCount(); }
297void Rope3D::integrate(
float dt) {
298 const Vec3 accel = add(gravity_, mul(force_, 1.f / particleMass_));
299 for (
auto&
p : particles_) {
300 if (
p.attached)
continue;
301 const Vec3 velocity = mul(sub(
p.position,
p.previous), 1.f - damping_);
302 p.previous =
p.position;
303 p.position = add(add(
p.position, velocity), mul(accel, dt * dt));
306void Rope3D::solveStretch(
float dt) {
307 const float invMass = 1.f / particleMass_;
308 for (
auto& e : elements_) {
309 if (!e.active)
continue;
310 auto&
a = particles_[e.a];
311 auto&
b = particles_[e.b];
312 const Vec3 delta = sub(
b.position,
a.position);
313 const float l =
length(delta);
314 if (l < 1e-7f)
continue;
315 const float target = std::max(e.restLength * (1.f - maxCompression_), std::min(l, e.restLength));
317 const float wa =
a.attached ? 0.f : invMass, wb =
b.attached ? 0.f : invMass;
318 const float alpha = stretchCompliance_ / (dt * dt);
319 const float dl = (-
c - alpha * e.lambda) / (wa + wb + alpha);
321 const Vec3 correction = mul(delta, dl / l);
322 if (!
a.attached)
a.position = sub(
a.position, mul(correction, wa));
323 if (!
b.attached)
b.position = add(
b.position, mul(correction, wb));
324 e.force = std::abs(e.lambda) / (dt * dt);
327void Rope3D::solveBend(
float dt) {
328 const float invMass = 1.f / particleMass_, alpha = bendCompliance_ / (dt * dt);
329 for (std::size_t i = 1; i < particles_.size() - 1; ++i) {
330 if (!elements_[i - 1].
active || !elements_[i].
active)
continue;
331 auto&
a = particles_[i - 1];
332 auto&
c = particles_[i + 1];
333 const Vec3 delta = sub(
c.position,
a.position);
334 const float l =
length(delta);
335 const float baseSpan = elements_[i - 1].restLength + elements_[i].restLength;
336 const float rest = baseSpan * (1.f - std::max(maxBending_, bendPlasticity_[i - 1]));
337 if (l < 1e-7f)
continue;
338 if (l >= rest)
continue;
339 const float wa =
a.attached ? 0.f : invMass, wc =
c.attached ? 0.f : invMass;
340 const float dl = -(l - rest) / (wa + wc + alpha);
341 const Vec3 corr = mul(delta, dl / l);
342 if (!
a.attached)
a.position = sub(
a.position, mul(corr, wa));
343 if (!
c.attached)
c.position = add(
c.position, mul(corr, wc));
346void Rope3D::updatePlasticity(
float dt) {
347 if (plasticCreep_ <= 0.f)
return;
348 for (std::size_t i = 1; i + 1 < particles_.size(); ++i) {
349 if (!elements_[i - 1].
active || !elements_[i].
active)
continue;
350 const float baseSpan = elements_[i - 1].restLength + elements_[i].restLength;
352 const float deformation = std::clamp(1.f - currentSpan / baseSpan, 0.f, 0.5f);
353 if (deformation > plasticYield_)
354 bendPlasticity_[i - 1] = std::min(deformation, bendPlasticity_[i - 1] + plasticCreep_ * dt);
357void Rope3D::syncBendState() { bendPlasticity_.resize(particles_.size() > 2 ? particles_.size() - 2 : 0, 0.f); }
358void Rope3D::solveSelfCollision() {
359 const float minDist = 2.f * radius_;
360 for (std::size_t i = 0; i < particles_.size(); ++i)
361 for (std::size_t j = i + 2; j < particles_.size(); ++j) {
362 auto&
a = particles_[i];
363 auto&
b = particles_[j];
364 Vec3 d = sub(
b.position,
a.position);
366 if (l >= minDist || l < 1e-7f)
continue;
367 const float wa =
a.attached ? 0.f : 1.f, wb =
b.attached ? 0.f : 1.f, total = wa + wb;
368 if (total == 0)
continue;
369 Vec3 corr = mul(
d, (minDist - l) / (l * total));
370 if (!
a.attached)
a.position = sub(
a.position, mul(corr, wa));
371 if (!
b.attached)
b.position = add(
b.position, mul(corr, wb));
374void Rope3D::collideBounds() {
375 if (!hasBounds_)
return;
376 for (
auto&
p : particles_)
378 p.position.x = std::clamp(
p.position.x, boundsMin_.
x + radius_, boundsMax_.
x - radius_);
379 p.position.y = std::clamp(
p.position.y, boundsMin_.
y + radius_, boundsMax_.
y - radius_);
380 p.position.z = std::clamp(
p.position.z, boundsMin_.
z + radius_, boundsMax_.
z - radius_);
383void Rope3D::applyAttachments() {
384 for (
auto&
p : particles_)
387void Rope3D::applyTearing() {
388 if (!tearingEnabled_)
return;
389 std::vector<std::size_t> candidates;
390 candidates.reserve(elements_.size());
391 for (std::size_t i = 0; i < elements_.size(); ++i) {
392 const auto&
element = elements_[i];
393 if (
element.active &&
element.force > tearResistance_ *
element.tearResistance) candidates.push_back(i);
395 std::stable_sort(candidates.begin(), candidates.end(),
396 [&](std::size_t lhs, std::size_t rhs) { return elements_[lhs].force > elements_[rhs].force; });
398 for (std::size_t
index : candidates) {
403 lastTornElements_.push_back(
static_cast<int>(
index));
405 if (++tears >= maxTearsPerStep_)
break;
std::array< float, 3 > position
std::array< float, 3 > scale
graphics::Canvas * previous
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.
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.
eve::Result< void > restoreObservation(const SimulationObservation &observation) override
Restores progress metadata after the owner restores its state.
void update(float dt)
Advances using a compatibility-generated monotonic fixed tick.
eve::Result< RopeTopologyChange > cut(int elementIndex)
Deactivates one structural element and splits the simulated chain.
float getParticleVelocityY(int index, float dt) const
float getBendPlasticity(int bendIndex) const
Returns absorbed local bend fraction, or zero for an invalid bend index.
void setBendCompliance(float compliance)
Sets bend compliance; larger values make the rope easier to bend.
void applyForce(float x, float y, float z)
Applies a one-frame acceleration to every free particle.
void setDamping(float damping)
Sets Verlet velocity damping in [0,1].
bool isElementActive(int elementIndex) const
SimulationObservation observation() const noexcept override
Returns completed-step observables by value.
eve::Result< RopeTopologyChange > pin(int particleIndex)
Pins a particle at its current position.
void setMaxBending(float fraction)
Sets fractional bend slack in [0,0.5] before bend resistance activates.
void setParticleMass(float mass)
Sets each particle's mass in kilograms.
eve::Result< RopeTopologyChange > attach(int particleIndex, float x, float y, float z)
Pins a particle at an explicit animated target.
void setStretchCompliance(float compliance)
Sets stretch compliance in inverse newtons; zero is inextensible.
float getParticleZ(int index) const
eve::Result< RopeTopologyChange > moveAttachment(int particleIndex, float x, float y, float z)
Moves an existing attachment target.
void setMaxCompression(float fraction)
Sets the maximum fractional compression in [0,1].
float getRestLength() const
float getParticleVelocityX(int index, float dt) const
float calculateLength() const
void setPlasticity(float yield, float creep)
Configures local permanent bend absorption; zero creep disables plasticity.
float getParticleVelocityZ(int index, float dt) const
void setBounds(float x, float y, float z, float width, float height, float depth)
Constrains particles to an axis-aligned box (origin plus positive extents).
float getParticleX(int index) const
float getElementForce(int elementIndex) const
Rope3D(int particleCount, float startX, float startY, float startZ, float endX, float endY, float endZ)
Creates a straight rope including both endpoints.
eve::Result< RopeTopologyChange > setRestLength(float length)
Scales all active element rest lengths, supporting reel/winch behavior.
bool isAttached(int particleIndex) const
eve::Result< void > step(const eve::SimulationStep &step, const SimulationSettings &settings) override
Advances the rope atomically under the shared simulation contract.
void setTearing(float resistance, int maxTearsPerStep)
Enables automatic tearing above a tensile-force threshold in newtons.
float getParticleY(int index) const
eve::Result< RopeTopologyChange > detach(int particleIndex)
Releases a pinned particle.
eve::Result< RopeTopologyChange > setElementTearResistance(int elementIndex, float multiplier)
Sets a positive per-element multiplier for the global tearing threshold.
eve::Result< RopeTopologyChange > repair(int elementIndex)
Re-enables a cut element using its current endpoint distance.
void setRadius(float radius)
Sets collision radius in meters.
void setGravity(float x, float y, float z)
Sets uniform acceleration in meters per second squared.
Optional physics backend for vehicle mobility and body attach.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
One deterministic fixed-step emitted by SimulationClock.
Compact solver vector exposed only as a value type.
Observable backend progress shared by CPU and accelerator providers.
Validated solver policy for one simulation step.