载入中...
搜索中...
未找到
PhysicalBalancePose.cpp
浏览该文件的文档.
2
7#include "common/Exception.h"
8
9#include <cmath>
10#include <string>
11
12namespace eve::animation {
13namespace {
14
15constexpr float kTwoPi = 6.283185307179586f;
16constexpr float kImpulseEps = 1e-8f;
17
18void quatMul(float ax, float ay, float az, float aw, float bx, float by, float bz, float bw, float& ox, float& oy,
19 float& oz, float& ow) {
20 multiplyQuat(ax, ay, az, aw, bx, by, bz, bw, ox, oy, oz, ow);
21}
22
23void quatFromAxisAngle(float ax, float ay, float az, float angle, float& qx, float& qy, float& qz, float& qw) {
24 const float half = 0.5f * angle;
25 const float s = std::sin(half);
26 qx = ax * s;
27 qy = ay * s;
28 qz = az * s;
29 qw = std::cos(half);
30}
31
32void applyParentRotation(TransformTRS& local, float qx, float qy, float qz, float qw) {
33 float ox = 0.f, oy = 0.f, oz = 0.f, ow = 1.f;
34 quatMul(qx, qy, qz, qw, local.qx, local.qy, local.qz, local.qw, ox, oy, oz, ow);
35 local.qx = ox;
36 local.qy = oy;
37 local.qz = oz;
38 local.qw = ow;
39 local.normalizeRotation();
40}
41
42void stepTowardZero(float dt, float omega, float zeta, float& y, float& yd) {
43 if (dt <= 0.f || omega <= 0.f) return;
44 const float kp = omega * omega;
45 const float kd = 2.f * zeta * omega;
46 yd += dt * (-kp * y - kd * yd);
47 y += dt * yd;
48}
49
50} // namespace
51
53 if (!skeleton_) throw Exception("PhysicalBalancePose: skeleton is null");
54 ensureBones();
55 skeleton_->applyBindPose(&pose_);
56 skeleton_->applyBindPose(&target_);
57 hasTarget_ = true;
58 writeOverlayPose();
59}
60
61void PhysicalBalancePose::ensureBones() {
62 const int n = skeleton_->getBoneCount();
63 if (pose_.getBoneCount() != n) pose_.resize(n);
64 if (target_.getBoneCount() != n) target_.resize(n);
65 if (static_cast<int>(masses_.size()) != n) {
66 masses_.assign(static_cast<size_t>(n), 1.f);
67 recoils_.assign(static_cast<size_t>(n), Recoil{});
68 }
69 if (n <= 0) {
70 supportBone_ = 0;
71 balanceBone_ = 0;
72 return;
73 }
74 if (!boneInRange(supportBone_)) supportBone_ = 0;
75 if (!boneInRange(balanceBone_)) balanceBone_ = n > 1 ? 1 : 0;
76}
77
78bool PhysicalBalancePose::boneInRange(int boneIndex) const {
79 return boneIndex >= 0 && boneIndex < skeleton_->getBoneCount();
80}
81
82eve::Result<void> PhysicalBalancePose::requireFinitePositive(const char* name, float value, bool allowZero) {
83 if (!std::isfinite(value) || value < 0.f || (!allowZero && value <= 0.f))
85 eve::DiagnosticCode::InvalidArgument, std::string("physical balance ") + name + " must be finite and " +
86 (allowZero ? "non-negative" : "positive")));
88}
89
91 if (!boneInRange(boneIndex))
93 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "physical balance support bone is out of range"));
94 supportBone_ = boneIndex;
96}
97
99 if (!boneInRange(boneIndex))
101 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "physical balance balance bone is out of range"));
102 balanceBone_ = boneIndex;
104}
105
107 if (!boneInRange(boneIndex))
109 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "physical balance bone mass index is out of range"));
110 auto ok = requireFinitePositive("bone mass", mass, true);
111 if (!ok) return ok;
112 masses_[static_cast<size_t>(boneIndex)] = mass;
114}
115
116float PhysicalBalancePose::getBoneMass(int boneIndex) const {
117 if (!boneInRange(boneIndex)) return 0.f;
118 return masses_[static_cast<size_t>(boneIndex)];
119}
120
121eve::Result<void> PhysicalBalancePose::setRecovery(float frequencyHz, float dampingZeta) {
122 auto freq = requireFinitePositive("recovery frequency", frequencyHz, false);
123 if (!freq) return freq;
124 if (!std::isfinite(dampingZeta) || dampingZeta < 0.f)
126 eve::DiagnosticCode::InvalidArgument, "physical balance recovery damping must be finite and non-negative"));
127 const float omega = kTwoPi * frequencyHz;
128 const float height = std::max(pendulumHeight_, 1e-4f);
129 if (omega * omega <= gravity_ / height)
132 "physical balance recovery frequency is too low to overcome gravity at the pendulum height"));
133 recoveryHz_ = frequencyHz;
134 recoveryZeta_ = dampingZeta;
136}
137
138eve::Result<void> PhysicalBalancePose::setRecoil(float frequencyHz, float dampingZeta) {
139 auto freq = requireFinitePositive("recoil frequency", frequencyHz, false);
140 if (!freq) return freq;
141 if (!std::isfinite(dampingZeta) || dampingZeta < 0.f)
143 eve::DiagnosticCode::InvalidArgument, "physical balance recoil damping must be finite and non-negative"));
144 recoilHz_ = frequencyHz;
145 recoilZeta_ = dampingZeta;
147}
148
150 auto ok = requireFinitePositive("gravity", metersPerSecondSquared, true);
151 if (!ok) return ok;
152 const float omega = kTwoPi * recoveryHz_;
153 const float height = std::max(pendulumHeight_, 1e-4f);
154 if (omega * omega <= metersPerSecondSquared / height)
157 "physical balance gravity would overcome the current recovery frequency"));
158 gravity_ = metersPerSecondSquared;
160}
161
163 auto ok = requireFinitePositive("pendulum height", meters, false);
164 if (!ok) return ok;
165 const float omega = kTwoPi * recoveryHz_;
166 if (omega * omega <= gravity_ / meters)
169 "physical balance pendulum height would make gravity stronger than recovery"));
170 pendulumHeight_ = meters;
172}
173
175 auto ok = requireFinitePositive("inertia", inertia, false);
176 if (!ok) return ok;
177 inertia_ = inertia;
179}
180
182 auto ok = requireFinitePositive("recoil inertia", inertia, false);
183 if (!ok) return ok;
184 recoilInertia_ = inertia;
186}
187
189 auto ok = requireFinitePositive("max lean", radians, false);
190 if (!ok) return ok;
191 maxLean_ = radians;
192 leanX_ = clampf(leanX_, -maxLean_, maxLean_);
193 leanZ_ = clampf(leanZ_, -maxLean_, maxLean_);
195}
196
198 if (!target)
200 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "physical balance target pose is null"));
201 if (target->getBoneCount() != skeleton_->getBoneCount())
203 eve::DiagnosticCode::InvalidArgument, "physical balance target pose bone count does not match the skeleton"));
204 target_.copyFrom(target);
205 hasTarget_ = true;
207}
208
209void PhysicalBalancePose::resetDynamics() {
210 leanX_ = leanZ_ = 0.f;
211 leanVelX_ = leanVelZ_ = 0.f;
212 pendingLeanTx_ = pendingLeanTz_ = 0.f;
213 for (Recoil& recoil : recoils_) recoil = Recoil{};
214}
215
217 if (!hasTarget_)
219 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "physical balance has no target pose to snap to"));
220 resetDynamics();
221 writeOverlayPose();
223}
224
225eve::Result<void> PhysicalBalancePose::applyImpulse(int boneIndex, float impulseX, float impulseY, float impulseZ,
226 float pointX, float pointY, float pointZ) {
227 if (!boneInRange(boneIndex))
229 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "physical balance impulse bone is out of range"));
230 if (!std::isfinite(impulseX) || !std::isfinite(impulseY) || !std::isfinite(impulseZ) || !std::isfinite(pointX) ||
231 !std::isfinite(pointY) || !std::isfinite(pointZ))
233 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "physical balance impulse values must be finite"));
234 const float mag2 = impulseX * impulseX + impulseY * impulseY + impulseZ * impulseZ;
235 if (mag2 < kImpulseEps)
237
238 pose_.computeWorld(skeleton_);
239 const TransformTRS& support = pose_.world(supportBone_);
240 const float rx = pointX - support.px;
241 const float ry = pointY - support.py;
242 const float rz = pointZ - support.pz;
243 pendingLeanTx_ += ry * impulseZ - rz * impulseY;
244 pendingLeanTz_ += rx * impulseY - ry * impulseX;
245
246 const TransformTRS& bone = pose_.world(boneIndex);
247 Recoil& rec = recoils_[static_cast<size_t>(boneIndex)];
248 const float brx = pointX - bone.px;
249 const float bry = pointY - bone.py;
250 const float brz = pointZ - bone.pz;
251 rec.pendingTx += bry * impulseZ - brz * impulseY;
252 rec.pendingTy += brz * impulseX - brx * impulseZ;
253 rec.pendingTz += brx * impulseY - bry * impulseX;
255}
256
257void PhysicalBalancePose::writeOverlayPose() {
258 pose_.copyFrom(&target_);
259 const int n = pose_.getBoneCount();
260 for (int i = 0; i < n; ++i) {
261 const Recoil& rec = recoils_[static_cast<size_t>(i)];
262 if (std::fabs(rec.x) < 1e-8f && std::fabs(rec.y) < 1e-8f && std::fabs(rec.z) < 1e-8f) continue;
263 float qx = 0.f, qy = 0.f, qz = 0.f, qw = 1.f;
264 float rx = 0.f, ry = 0.f, rz = 0.f, rw = 1.f;
265 float tx = 0.f, ty = 0.f, tz = 0.f, tw = 1.f;
266 quatFromAxisAngle(1.f, 0.f, 0.f, rec.x, qx, qy, qz, qw);
267 quatFromAxisAngle(0.f, 1.f, 0.f, rec.y, rx, ry, rz, rw);
268 quatMul(rx, ry, rz, rw, qx, qy, qz, qw, tx, ty, tz, tw);
269 quatFromAxisAngle(0.f, 0.f, 1.f, rec.z, qx, qy, qz, qw);
270 float ox = 0.f, oy = 0.f, oz = 0.f, ow = 1.f;
271 quatMul(qx, qy, qz, qw, tx, ty, tz, tw, ox, oy, oz, ow);
272 applyParentRotation(pose_.local(i), ox, oy, oz, ow);
273 }
274 if (boneInRange(balanceBone_) && (std::fabs(leanX_) > 1e-8f || std::fabs(leanZ_) > 1e-8f)) {
275 float qx = 0.f, qy = 0.f, qz = 0.f, qw = 1.f;
276 float rx = 0.f, ry = 0.f, rz = 0.f, rw = 1.f;
277 quatFromAxisAngle(1.f, 0.f, 0.f, leanX_, qx, qy, qz, qw);
278 quatFromAxisAngle(0.f, 0.f, 1.f, leanZ_, rx, ry, rz, rw);
279 float ox = 0.f, oy = 0.f, oz = 0.f, ow = 1.f;
280 quatMul(qx, qy, qz, qw, rx, ry, rz, rw, ox, oy, oz, ow);
281 applyParentRotation(pose_.local(balanceBone_), ox, oy, oz, ow);
282 }
283 refreshDiagnostics();
284}
285
286void PhysicalBalancePose::refreshDiagnostics() {
287 pose_.computeWorld(skeleton_);
288 const int n = pose_.getBoneCount();
289 float massSum = 0.f;
290 comX_ = comY_ = comZ_ = 0.f;
291 for (int i = 0; i < n; ++i) {
292 const float mass = masses_[static_cast<size_t>(i)];
293 if (mass <= 0.f) continue;
294 const TransformTRS& world = pose_.world(i);
295 comX_ += world.px * mass;
296 comY_ += world.py * mass;
297 comZ_ += world.pz * mass;
298 massSum += mass;
299 }
300 if (massSum > 1e-8f) {
301 const float inv = 1.f / massSum;
302 comX_ *= inv;
303 comY_ *= inv;
304 comZ_ *= inv;
305 }
306 if (boneInRange(supportBone_)) {
307 const TransformTRS& support = pose_.world(supportBone_);
308 supportX_ = support.px;
309 supportY_ = support.py;
310 supportZ_ = support.pz;
311 }
312}
313
314void PhysicalBalancePose::updateUnchecked(float dt) {
315 if (dt <= 0.f || !hasTarget_) return;
316 ensureBones();
317
318 leanVelX_ += pendingLeanTx_ / inertia_;
319 leanVelZ_ += pendingLeanTz_ / inertia_;
320 pendingLeanTx_ = pendingLeanTz_ = 0.f;
321
322 const float omega = kTwoPi * recoveryHz_;
323 const float height = std::max(pendulumHeight_, 1e-4f);
324 const float grav = gravity_ / height;
325 const float kp = std::max(omega * omega - grav, 1e-4f);
326 const float kd = 2.f * recoveryZeta_ * omega;
327 leanVelX_ += dt * (-kp * leanX_ - kd * leanVelX_);
328 leanVelZ_ += dt * (-kp * leanZ_ - kd * leanVelZ_);
329 leanX_ += dt * leanVelX_;
330 leanZ_ += dt * leanVelZ_;
331 const float clampedX = clampf(leanX_, -maxLean_, maxLean_);
332 const float clampedZ = clampf(leanZ_, -maxLean_, maxLean_);
333 if (clampedX != leanX_) leanVelX_ = 0.f;
334 if (clampedZ != leanZ_) leanVelZ_ = 0.f;
335 leanX_ = clampedX;
336 leanZ_ = clampedZ;
337
338 const float recoilOmega = kTwoPi * recoilHz_;
339 for (Recoil& rec : recoils_) {
340 rec.vx += rec.pendingTx / recoilInertia_;
341 rec.vy += rec.pendingTy / recoilInertia_;
342 rec.vz += rec.pendingTz / recoilInertia_;
343 rec.pendingTx = rec.pendingTy = rec.pendingTz = 0.f;
344 stepTowardZero(dt, recoilOmega, recoilZeta_, rec.x, rec.vx);
345 stepTowardZero(dt, recoilOmega, recoilZeta_, rec.y, rec.vy);
346 stepTowardZero(dt, recoilOmega, recoilZeta_, rec.z, rec.vz);
347 rec.x = clampf(rec.x, -maxLean_, maxLean_);
348 rec.y = clampf(rec.y, -maxLean_, maxLean_);
349 rec.z = clampf(rec.z, -maxLean_, maxLean_);
350 }
351
352 writeOverlayPose();
353}
354
356 auto seconds = detail::secondsForStep(step, hasLastTick_, lastTick_, "PhysicalBalancePose");
357 if (!seconds) return eve::Result<void>::failure(seconds.status());
358 updateUnchecked(std::move(seconds).takeValue());
359 lastTick_ = step.tick;
360 hasLastTick_ = true;
362}
363
365 auto step = detail::legacyStep(dt, hasLastTick_, lastTick_, "PhysicalBalancePose");
366 if (!step) {
367 step.ignore("legacy PhysicalBalancePose update");
368 return;
369 }
370 advance(std::move(step).takeValue()).ignore("legacy PhysicalBalancePose update");
371}
372
375
376AnimSkeleton* PhysicalBalancePose::getSkeleton() const { return skeleton_; }
377int PhysicalBalancePose::getSupportBone() const { return supportBone_; }
378int PhysicalBalancePose::getBalanceBone() const { return balanceBone_; }
379float PhysicalBalancePose::getRecoveryFrequency() const { return recoveryHz_; }
380float PhysicalBalancePose::getRecoveryDamping() const { return recoveryZeta_; }
381float PhysicalBalancePose::getRecoilFrequency() const { return recoilHz_; }
382float PhysicalBalancePose::getRecoilDamping() const { return recoilZeta_; }
383float PhysicalBalancePose::getGravity() const { return gravity_; }
384float PhysicalBalancePose::getPendulumHeight() const { return pendulumHeight_; }
385float PhysicalBalancePose::getInertia() const { return inertia_; }
386float PhysicalBalancePose::getRecoilInertia() const { return recoilInertia_; }
387float PhysicalBalancePose::getMaxLean() const { return maxLean_; }
388float PhysicalBalancePose::getLeanX() const { return leanX_; }
389float PhysicalBalancePose::getLeanZ() const { return leanZ_; }
390float PhysicalBalancePose::getLeanVelocityX() const { return leanVelX_; }
391float PhysicalBalancePose::getLeanVelocityZ() const { return leanVelZ_; }
392float PhysicalBalancePose::getCenterOfMassX() const { return comX_; }
393float PhysicalBalancePose::getCenterOfMassY() const { return comY_; }
394float PhysicalBalancePose::getCenterOfMassZ() const { return comZ_; }
395float PhysicalBalancePose::getSupportX() const { return supportX_; }
396float PhysicalBalancePose::getSupportY() const { return supportY_; }
397float PhysicalBalancePose::getSupportZ() const { return supportZ_; }
398
399} // namespace eve::animation
LogicalId target
double value
float y
Definition AnimClip.cpp:738
const std::string & s
int bz
Definition CaveMesh.cpp:114
int ax
Definition CaveMesh.cpp:113
int ay
Definition CaveMesh.cpp:113
int bx
Definition CaveMesh.cpp:114
int az
Definition CaveMesh.cpp:113
int by
Definition CaveMesh.cpp:114
glm::vec3 n
Definition Grass.cpp:63
std::uint32_t height
std::string local
std::uint32_t bone
std::string name
World3D * world
float inertia
Definition TreeMesh.cpp:309
float step
Definition TreeMesh.cpp:314
double oy
double ox
float qy
float qx
float qw
float angle
float qz
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.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
Definition AnimPose.h:17
int getBoneCount() const
Returns the bone count.
Definition AnimPose.h:36
const TransformTRS & world(int boneIndex) const
World.
Definition AnimPose.cpp:367
TransformTRS & local(int boneIndex)
Local.
Definition AnimPose.cpp:126
void copyFrom(const AnimPose *other)
Copies from.
Definition AnimPose.cpp:91
void computeWorld(const AnimSkeleton *skeleton)
Compute world transforms from local pose + skeleton hierarchy. World values readable via getWorld* af...
Definition AnimPose.cpp:203
void resize(int boneCount)
Resize.
Definition AnimPose.cpp:78
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int getBoneCount() const
Returns the bone count.
void applyBindPose(class AnimPose *pose) const
Fill pose locals with bind pose.
void update(float dt)
Legacy seconds facade; forwards to advance() and ignores its Result.
eve::Result< void > setInertia(float inertia)
Whole-body rotational inertia for lean (kg·m²).
eve::Result< void > setRecoilInertia(float inertia)
Per-bone recoil inertia (kg·m²).
eve::Result< void > advance(const eve::SimulationStep &step)
Advance the pendulum and recoil, then write the overlay pose.
eve::Result< void > snapToTarget()
Zero lean, recoil and pending impulses; pose snaps to the last target.
eve::Result< void > setSupportBone(int boneIndex)
Bone whose world XZ is the support point (typically pelvis/root).
eve::Result< void > setMaxLean(float radians)
Clamp for lean angle in radians.
eve::Result< void > setBalanceBone(int boneIndex)
Bone that receives whole-body lean (typically spine/chest).
eve::Result< void > setPendulumHeight(float meters)
Pendulum length used as g/h (meters).
eve::Result< void > setTargetPose(const AnimPose *target)
Copy the authored pose that recovery tracks.
eve::Result< void > setBoneMass(int boneIndex, float mass)
Mass used for center-of-mass; zero excludes the bone.
eve::Result< void > setRecovery(float frequencyHz, float dampingZeta)
Recovery frequency (Hz) and damping ζ for the balance pendulum.
AnimPose * getPose()
Overlay pose after the last successful advance (or snap). @ownership Borrowed; this object retains ow...
eve::Result< void > applyImpulse(int boneIndex, float impulseX, float impulseY, float impulseZ, float pointX, float pointY, float pointZ)
Queue a world-space linear impulse at a world point.
AnimPose * getTargetPose()
Last authored target, or bind pose before the first setTargetPose. @ownership Borrowed; this object r...
eve::Result< void > setGravity(float metersPerSecondSquared)
Gravity magnitude (m/s², Y-up). Zero disables the fall-away term.
PhysicalBalancePose(AnimSkeleton *skeleton)
Construct for a borrowed skeleton.
eve::Result< void > setRecoil(float frequencyHz, float dampingZeta)
Local-bone recoil frequency and damping toward the authored rotation.
AnimSkeleton * getSkeleton() const
Borrowed skeleton. @ownership Borrowed; ownership remains with the animation source....
eve::Result< eve::SimulationStep > legacyStep(float seconds, bool hasLastTick, eve::SimulationTick lastTick, const char *owner)
Convert a legacy seconds call to the next local scheduler step.
eve::Result< float > secondsForStep(const eve::SimulationStep &step, bool hasLastTick, eve::SimulationTick lastTick, const char *owner)
Validate a scheduler step before an animation object mutates state.
void multiplyQuat(float ax, float ay, float az, float aw, float bx, float by, float bz, float bw, float &ox, float &oy, float &oz, float &ow)
Hamilton product a * b for unit quaternions (xyzw).
Definition AnimMath.h:94
float clampf(float v, float lo, float hi)
Clampf.
Definition AnimMath.h:34
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
Local TRS used by skeletal animation (quaternion xyzw).
Definition AnimMath.h:9