FIRE-35686: Update BVH save state

- add optional unlock for mPelvis (for partial-pose BVH)
- add settings option for mPelvis BVH lock state
- T-pose now only BVH locks Body tab; face and hands are now BVH-unlocked by default
master
Angeldark Raymaker 2025-08-04 22:20:35 +01:00
parent d92e78ccc0
commit 743aca7ee4
10 changed files with 92 additions and 24 deletions

View File

@ -8100,6 +8100,17 @@
<key>Value</key>
<integer>0</integer>
</map>
<key>FSPoserPelvisUnlockedForBvhSave</key>
<map>
<key>Comment</key>
<string>Whether the mPelvis joint should be position/rotationally locked when a BVH is created.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>FSPoserOnSaveConfirmOverwrite</key>
<map>
<key>Comment</key>

View File

@ -62,6 +62,7 @@ constexpr std::string_view POSER_TRACKPAD_SENSITIVITY_SAVE_KEY = "FSPoserTrackpa
constexpr std::string_view POSER_STOPPOSINGWHENCLOSED_SAVE_KEY = "FSPoserStopPosingWhenClosed";
constexpr std::string_view POSER_SAVEEXTERNALFORMAT_SAVE_KEY = "FSPoserSaveExternalFileAlso";
constexpr std::string_view POSER_SAVECONFIRMREQUIRED_SAVE_KEY = "FSPoserOnSaveConfirmOverwrite";
constexpr std::string_view POSER_UNLOCKPELVISINBVH_SAVE_KEY = "FSPoserPelvisUnlockedForBvhSave";
constexpr char ICON_SAVE_OK[] = "icon_rotation_is_own_work";
constexpr char ICON_SAVE_FAILED[] = "icon_save_failed_button";
@ -212,6 +213,8 @@ bool FSFloaterPoser::postBuild()
mMiscJointsPnl = getChild<LLPanel>("misc_joints_panel");
mCollisionVolumesPnl = getChild<LLPanel>("collision_volumes_panel");
mUnlockPelvisInBvhSaveCbx = getChild<LLCheckBoxCtrl>("unlock_pelvis_for_bvh_save_checkbox");
mUnlockPelvisInBvhSaveCbx->setVisible(getSavingToBvh());
mAlsoSaveBvhCbx = getChild<LLCheckBoxCtrl>("also_save_bvh_checkbox");
mAlsoSaveBvhCbx->setCommitCallback([this](LLUICtrl*, const LLSD&) { onClickSavingToBvh(); });
@ -2587,7 +2590,9 @@ void FSFloaterPoser::writeBvhMotion(llofstream* fileStream, LLVOAvatar* avatar,
if (!joint)
return;
auto rotation = mPoserAnimator.getJointExportRotation(avatar, *joint);
bool lockPelvisJoint = gSavedSettings.getBOOL(POSER_UNLOCKPELVISINBVH_SAVE_KEY);
auto rotation = mPoserAnimator.getJointExportRotation(avatar, *joint, !lockPelvisJoint);
auto position = mPoserAnimator.getJointPosition(avatar, *joint);
switch (joint->boneType())
@ -2684,7 +2689,11 @@ bool FSFloaterPoser::getSavingToBvh()
return gSavedSettings.getBOOL(POSER_SAVEEXTERNALFORMAT_SAVE_KEY);
}
void FSFloaterPoser::onClickSavingToBvh() { refreshTextHighlightingOnJointScrollLists(); }
void FSFloaterPoser::onClickSavingToBvh()
{
mUnlockPelvisInBvhSaveCbx->setVisible(getSavingToBvh());
refreshTextHighlightingOnJointScrollLists();
}
void FSFloaterPoser::onClickLockWorldRotBtn()
{

View File

@ -513,6 +513,7 @@ public:
LLPanel* mPosesLoadSavePnl{ nullptr };
LLCheckBoxCtrl* mAlsoSaveBvhCbx{ nullptr };
LLCheckBoxCtrl* mUnlockPelvisInBvhSaveCbx{ nullptr };
LLUICtrl* mTrackpadSensitivitySpnr{ nullptr };
LLUICtrl* mYawSpnr{ nullptr };

View File

@ -62,7 +62,7 @@ void FSJointPose::setPublicRotation(bool zeroBase, const LLQuaternion& rot)
addStateToUndo(FSJointState(mCurrentState));
if (zeroBase)
zeroBaseRotation();
zeroBaseRotation(true);
mCurrentState.mRotation.set(rot);
}
@ -208,12 +208,13 @@ void FSJointPose::reflectRotation()
mCurrentState.reflectRotation();
}
void FSJointPose::zeroBaseRotation()
void FSJointPose::zeroBaseRotation(bool lockInBvh)
{
if (mIsCollisionVolume)
return;
mCurrentState.zeroBaseRotation();
mCurrentState.mUserSpecifiedBaseZero = lockInBvh;
}
bool FSJointPose::isBaseRotationZero() const
@ -233,12 +234,12 @@ void FSJointPose::purgeUndoQueue()
mLastSetJointStates.clear();
}
bool FSJointPose::userHaseSetBaseRotationToZero() const
bool FSJointPose::userHasSetBaseRotationToZero() const
{
if (mIsCollisionVolume)
return false;
return mCurrentState.userSetBaseRotationToZero();
return mCurrentState.mUserSpecifiedBaseZero;
}
bool FSJointPose::getWorldRotationLockState() const

View File

@ -107,7 +107,8 @@ class FSJointPose
/// <summary>
/// Sets the private rotation of the represented joint to zero.
/// </summary>
void zeroBaseRotation();
/// <param name="lockInBvh">Whether the joint should be locked if exported to BVH.</param>
void zeroBaseRotation(bool lockInBvh);
/// <summary>
/// Queries whether the represented joint is zero.
@ -173,7 +174,7 @@ class FSJointPose
/// Gets whether the user has specified the base rotation of a joint to be zero.
/// </summary>
/// <returns>True if the user performed some action to specify zero rotation as the base, otherwise false.</returns>
bool userHaseSetBaseRotationToZero() const;
bool userHasSetBaseRotationToZero() const;
/// <summary>
/// Gets whether the rotation of a joint has been 'locked' so that its world rotation can remain constant while parent joints change.
@ -230,11 +231,9 @@ class FSJointPose
{
mBaseRotation.set(otherState.mBaseRotation);
mRotation.set(otherState.mRotation);
mUserSpecifiedBaseZero = otherState.userSetBaseRotationToZero();
mUserSpecifiedBaseZero = otherState.mUserSpecifiedBaseZero;
}
bool userSetBaseRotationToZero() const { return mUserSpecifiedBaseZero; }
bool baseRotationIsZero() const { return mBaseRotation == LLQuaternion::DEFAULT; }
void resetJoint()
@ -247,11 +246,7 @@ class FSJointPose
mScale.setZero();
}
void zeroBaseRotation()
{
mBaseRotation = LLQuaternion::DEFAULT;
mUserSpecifiedBaseZero = true;
}
void zeroBaseRotation() { mBaseRotation = LLQuaternion::DEFAULT; }
void revertJointToBase(LLJoint* joint) const
{
@ -294,7 +289,7 @@ class FSJointPose
mRotation.set(state->mRotation);
mPosition.set(state->mPosition);
mScale.set(state->mScale);
mUserSpecifiedBaseZero = state->userSetBaseRotationToZero();
mUserSpecifiedBaseZero = state->mUserSpecifiedBaseZero;
mRotationIsWorldLocked = state->mRotationIsWorldLocked;
}
@ -304,12 +299,22 @@ class FSJointPose
LLVector3 mScale;
bool mRotationIsWorldLocked = false;
/// <summary>
/// A value indicating whether the user has explicitly set the base rotation to zero.
/// </summary>
/// <remarks>
/// The base-rotation, representing any 'current animation' state when posing starts, may become zero for several reasons.
/// Loading a Pose, editing a rotation intended to save to BVH, or setting to 'T-Pose' being examples.
/// If a user intends on creating a BVH, zero-rotation has a special meaning upon upload: the joint is free (is not animated by that BVH).
/// This value represents the explicit intent to have that joint be 'free' in BVH (which is sometimes undesireable).
/// </remarks>
bool mUserSpecifiedBaseZero = false;
private:
LLQuaternion mStartingRotation;
LLQuaternion mBaseRotation;
LLVector3 mBasePosition;
LLVector3 mBaseScale;
bool mUserSpecifiedBaseZero = false;
};
private:

View File

@ -318,7 +318,7 @@ bool FSPoserAnimator::exportRotationWillLockJoint(LLVOAvatar* avatar, const FSPo
if (!jointPose)
return false;
if (!jointPose->userHaseSetBaseRotationToZero())
if (!jointPose->userHasSetBaseRotationToZero())
return false;
F32 rot_threshold = ROTATION_KEYFRAME_THRESHOLD / llmax((F32)getChildJointDepth(&joint, 0) * 0.33f, 1.f);
@ -344,7 +344,7 @@ bool FSPoserAnimator::userSetBaseRotationToZero(LLVOAvatar* avatar, const FSPose
if (!jointPose)
return false;
return jointPose->userHaseSetBaseRotationToZero();
return jointPose->userHasSetBaseRotationToZero();
}
bool FSPoserAnimator::allBaseRotationsAreZero(LLVOAvatar* avatar) const
@ -373,6 +373,21 @@ void FSPoserAnimator::setAllAvatarStartingRotationsToZero(LLVOAvatar* avatar)
return;
posingMotion->setAllRotationsToZeroAndClearUndo();
for (size_t index = 0; index != PoserJoints.size(); ++index)
{
auto boneType = PoserJoints[index].boneType();
bool setBvhToLock = boneType == BODY || boneType == WHOLEAVATAR;
if (setBvhToLock)
continue; // setAllRotationsToZeroAndClearUndo specified this is the default behaviour
FSJointPose* jointPose = posingMotion->getJointPoseByJointName(PoserJoints[index].jointName());
if (!jointPose)
continue;
posingMotion->setJointBvhLock(jointPose, false);
}
}
void FSPoserAnimator::recaptureJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneAxisTranslation translation, S32 negation)
@ -434,7 +449,7 @@ void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoi
}
}
LLVector3 FSPoserAnimator::getJointExportRotation(LLVOAvatar* avatar, const FSPoserJoint& joint) const
LLVector3 FSPoserAnimator::getJointExportRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, bool lockWholeAvatar) const
{
auto rotation = getJointRotation(avatar, joint, SWAP_NOTHING, NEGATE_NOTHING);
if (exportRotationWillLockJoint(avatar, joint))
@ -452,10 +467,10 @@ LLVector3 FSPoserAnimator::getJointExportRotation(LLVOAvatar* avatar, const FSPo
if (!jointPose)
return vec3;
if (!jointPose->userHaseSetBaseRotationToZero())
if (!jointPose->userHasSetBaseRotationToZero())
return vec3;
if (joint.boneType() == WHOLEAVATAR)
if (lockWholeAvatar && joint.boneType() == WHOLEAVATAR)
return LLVector3(DEG_TO_RAD * 0.295f, 0.f, 0.f);
F32 minimumRotation = DEG_TO_RAD * 0.65f / llmax((F32)getChildJointDepth(&joint, 0) * 0.33f, 1.f);

View File

@ -497,12 +497,13 @@ public:
/// </summary>
/// <param name="avatar">The avatar whose joint is being queried.</param>
/// <param name="joint">The joint to determine the rotation for.</param>
/// <param name="lockWholeAvatar">Whether the whole avatar should be rotation/position locked in the BVH export.</param>
/// <returns>The rotation of the requested joint for export.</returns>
/// <remarks>
/// The BVH export format requires some minimal amount of rotation so it animates the joint on upload.
/// The WHOLEAVATAR joint (mPelvis) never exports as 'free'.
/// </remarks>
LLVector3 getJointExportRotation(LLVOAvatar* avatar, const FSPoserJoint& joint) const;
LLVector3 getJointExportRotation(LLVOAvatar* avatar, const FSPoserJoint& joint, bool lockWholeAvatar) const;
/// <summary>
/// Sets the rotation of a joint for the supplied avatar.

View File

@ -264,6 +264,11 @@ void FSPosingMotion::setAllRotationsToZeroAndClearUndo()
}
}
void FSPosingMotion::setJointBvhLock(FSJointPose* joint, bool lockInBvh)
{
joint->zeroBaseRotation(lockInBvh);
}
bool FSPosingMotion::vectorsNotQuiteEqual(LLVector3 v1, LLVector3 v2) const
{
if (vectorAxesAlmostEqual(v1.mV[VX], v2.mV[VX]) &&

View File

@ -121,8 +121,17 @@ public:
/// <summary>
/// Sets all of the non-Collision Volume base-and-delta rotations to zero, and clears the undo/redo queue.
/// </summary>
/// <remarks>
/// By default, sets the joint to lock in BVH export.
/// </remarks>
void setAllRotationsToZeroAndClearUndo();
/// <summary>
/// Sets the BVH export state for the supplied joint.
/// </summary>
/// <param name="lockInBvh">Whether the joint should be locked if exported to BVH.</param>
void setJointBvhLock(FSJointPose* joint, bool lockInBvh);
private:
/// <summary>
/// The axial difference considered close enough to be the same.

View File

@ -553,6 +553,17 @@ width="430">
function="Poser.CommitSpinner"
parameter="2"/>
</spinner>
<check_box
control_name="FSPoserPelvisUnlockedForBvhSave"
name="unlock_pelvis_for_bvh_save_checkbox"
visible="false"
height="16"
label="Unlock Pelvis in BVH"
follows="left|top"
left="5"
tool_tip="When you save this pose to BVH, the Pelvis will not be 'locked', meaning your pose could move in-world."
top_pad="15"
width="134" />
<!-- to make this panel behaves like the others in code-behind, it has an invisible list -->
<scroll_list
visible="false"