FIRE-35769: Add 'World lock joints' option

master
Angeldark Raymaker 2025-08-02 00:53:06 +01:00
parent 3bd58efda2
commit 00ed27bf14
7 changed files with 287 additions and 29 deletions

View File

@ -192,6 +192,8 @@ bool FSFloaterPoser::postBuild()
mFlipJointBtn = getChild<LLButton>("FlipJoint_avatar");
mRecaptureBtn = getChild<LLButton>("button_RecaptureParts");
mTogglePosingBonesBtn = getChild<LLButton>("toggle_PosingSelectedBones");
mToggleLockWorldRotBtn = getChild<LLButton>("toggle_LockWorldRotation");
mToggleLockWorldRotBtn->setClickedCallback([this](LLUICtrl*, const LLSD&) { onClickLockWorldRotBtn(); });
mToggleMirrorRotationBtn = getChild<LLButton>("button_toggleMirrorRotation");
mToggleSympatheticRotationBtn = getChild<LLButton>("button_toggleSympatheticRotation");
@ -517,6 +519,7 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi
{
std::string bone_name = pj.jointName();
bool posingThisJoint = mPoserAnimator.isPosingAvatarJoint(avatar, pj);
bool jointRotLocked = mPoserAnimator.getRotationIsWorldLocked(avatar, pj);
record[bone_name] = bone_name;
record[bone_name]["enabled"] = posingThisJoint;
@ -532,9 +535,10 @@ bool FSFloaterPoser::savePoseToXml(LLVOAvatar* avatar, const std::string& poseFi
continue;
record[bone_name]["jointBaseRotationIsZero"] = baseRotationIsZero;
record[bone_name]["rotation"] = rotation.getValue();
record[bone_name]["position"] = position.getValue();
record[bone_name]["scale"] = scale.getValue();
record[bone_name]["rotation"] = rotation.getValue();
record[bone_name]["position"] = position.getValue();
record[bone_name]["scale"] = scale.getValue();
record[bone_name]["worldLocked"] = jointRotLocked;
}
std::string fullSavePath =
@ -1041,6 +1045,7 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose
LLQuaternion quat;
bool enabled;
bool setJointBaseRotationToZero;
bool worldLocked;
S32 version = 0;
bool startFromZeroRot = true;
@ -1116,6 +1121,9 @@ void FSFloaterPoser::loadPoseFromXml(LLVOAvatar* avatar, const std::string& pose
vec3.setValue(control_map["scale"]);
mPoserAnimator.loadJointScale(avatar, poserJoint, loadPositionsAndScalesAsDeltas, vec3);
}
worldLocked = control_map.has("worldLocked") ? control_map["worldLocked"].asBoolean() : false;
mPoserAnimator.setRotationIsWorldLocked(avatar, *poserJoint, worldLocked);
}
}
}
@ -1449,8 +1457,7 @@ void FSFloaterPoser::onResetJoint(const LLSD data)
refreshScaleSlidersAndSpinners();
refreshTrackpadCursor();
enableOrDisableRedoAndUndoButton();
if (getSavingToBvh())
refreshTextHighlightingOnJointScrollLists();
refreshTextHighlightingOnJointScrollLists();
}
void FSFloaterPoser::onRedoLastChange()
@ -2373,10 +2380,7 @@ void FSFloaterPoser::addBoldToScrollList(LLScrollListCtrl* list, LLVOAvatar* ava
if (!poserJoint)
continue;
if (considerExternalFormatSaving)
((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue(getScrollListIconForJoint(avatar, *poserJoint));
else
((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue("");
((LLScrollListText*)listItem->getColumn(COL_ICON))->setValue(getScrollListIconForJoint(avatar, *poserJoint));
if (mPoserAnimator.isPosingAvatarJoint(avatar, *poserJoint))
((LLScrollListText *) listItem->getColumn(COL_NAME))->setFontStyle(LLFontGL::BOLD);
@ -2393,6 +2397,9 @@ std::string FSFloaterPoser::getScrollListIconForJoint(LLVOAvatar* avatar, FSPose
if (mPoserAnimator.getRotationIsWorldLocked(avatar, joint))
return tryGetString("icon_rotation_is_world_locked");
if (!getSavingToBvh())
return "";
if (joint.boneType() == COL_VOLUMES)
return tryGetString("icon_rotation_does_not_export");
@ -2678,3 +2685,29 @@ bool FSFloaterPoser::getSavingToBvh()
}
void FSFloaterPoser::onClickSavingToBvh() { refreshTextHighlightingOnJointScrollLists(); }
void FSFloaterPoser::onClickLockWorldRotBtn()
{
auto selectedJoints = getUiSelectedPoserJoints();
if (selectedJoints.size() < 1)
return;
LLVOAvatar* avatar = getUiSelectedAvatar();
if (!avatar)
return;
if (!mPoserAnimator.isPosingAvatar(avatar))
return;
for (auto item : selectedJoints)
{
bool currentlyPosingJoint = mPoserAnimator.isPosingAvatarJoint(avatar, *item);
if (!currentlyPosingJoint)
continue;
bool newLockState = !mPoserAnimator.getRotationIsWorldLocked(avatar, *item);
mPoserAnimator.setRotationIsWorldLocked(avatar, *item, newLockState);
}
refreshTextHighlightingOnJointScrollLists();
}

View File

@ -268,6 +268,7 @@ public:
void onCommitSpinner(const LLUICtrl* spinner, const S32 ID);
void onCommitSlider(const LLUICtrl* slider, const S32 id);
void onClickSymmetrize(const S32 ID);
void onClickLockWorldRotBtn();
// UI Refreshments
void refreshRotationSlidersAndSpinners();
@ -490,6 +491,7 @@ public:
LLButton* mFlipJointBtn{ nullptr };
LLButton* mRecaptureBtn{ nullptr };
LLButton* mTogglePosingBonesBtn{ nullptr };
LLButton* mToggleLockWorldRotBtn{ nullptr };
LLButton* mToggleMirrorRotationBtn{ nullptr };
LLButton* mToggleSympatheticRotationBtn{ nullptr };
LLButton* mToggleDeltaModeBtn{ nullptr };

View File

@ -155,14 +155,14 @@ void FSJointPose::recaptureJoint()
mCurrentState = FSJointState(joint);
}
void FSJointPose::recaptureJointAsDelta(bool zeroBase)
LLQuaternion FSJointPose::recaptureJointAsDelta(bool zeroBase)
{
LLJoint* joint = mJointState->getJoint();
if (!joint)
return;
return LLQuaternion::DEFAULT;
addStateToUndo(FSJointState(mCurrentState));
mCurrentState.updateFromJoint(joint, zeroBase);
return mCurrentState.updateFromJoint(joint, zeroBase);
}
void FSJointPose::swapRotationWith(FSJointPose* oppositeJoint)
@ -241,6 +241,22 @@ bool FSJointPose::userHaseSetBaseRotationToZero() const
return mCurrentState.userSetBaseRotationToZero();
}
bool FSJointPose::getWorldRotationLockState() const
{
if (mIsCollisionVolume)
return false;
return mCurrentState.mRotationIsWorldLocked;
}
void FSJointPose::setWorldRotationLockState(bool newState)
{
if (mIsCollisionVolume)
return;
mCurrentState.mRotationIsWorldLocked = newState;
}
bool FSJointPose::canPerformUndo() const
{
switch (mLastSetJointStates.size())

View File

@ -161,7 +161,8 @@ class FSJointPose
/// Recalculates the delta reltive to the base for a new rotation.
/// </summary>
/// <param name="zeroBase">Whether to zero the base rotation on setting the supplied rotation.</param>
void recaptureJointAsDelta(bool zeroBase);
/// <returns>The rotation of the public difference between before and after recapture.</returns>
LLQuaternion recaptureJointAsDelta(bool zeroBase);
/// <summary>
/// Clears the undo/redo deque.
@ -174,6 +175,18 @@ class FSJointPose
/// <returns>True if the user performed some action to specify zero rotation as the base, otherwise false.</returns>
bool userHaseSetBaseRotationToZero() const;
/// <summary>
/// Gets whether the rotation of a joint has been 'locked' so that its world rotation can remain constant while parent joints change.
/// </summary>
/// <returns>True if the joint is rotationally locked to the world, otherwise false.</returns>
bool getWorldRotationLockState() const;
/// <summary>
/// Sets whether the world-rotation of a joint has been 'locked' so that as its parent joints change rotation or position, this joint keeps a constant world rotation.
/// </summary>
/// <param name="newState">The new state for the world-rotation lock.</param>
void setWorldRotationLockState(bool newState);
/// <summary>
/// Reverts the position/rotation/scale to their values when the animation begun.
/// This treatment is required for certain joints, particularly Collision Volumes and those bones not commonly animated by an AO.
@ -201,16 +214,9 @@ class FSJointPose
}
FSJointState() = default;
LLQuaternion mDeltaRotation;
LLQuaternion getTargetRotation() const { return mRotation * mBaseRotation; }
LLVector3 getTargetPosition() const { return mPosition + mBasePosition; }
LLVector3 getTargetScale() const { return mScale + mBaseScale; }
void updateRotation(const LLQuaternion& newRotation)
{
auto inv_base = mBaseRotation;
inv_base.conjugate();
mDeltaRotation = newRotation * inv_base;
};
void reflectRotation()
{
@ -234,6 +240,7 @@ class FSJointPose
void resetJoint()
{
mUserSpecifiedBaseZero = false;
mRotationIsWorldLocked = false;
mBaseRotation.set(mStartingRotation);
mRotation.set(LLQuaternion::DEFAULT);
mPosition.setZero();
@ -256,20 +263,24 @@ class FSJointPose
joint->setScale(mBaseScale);
}
void updateFromJoint(LLJoint* joint, bool zeroBase)
LLQuaternion updateFromJoint(LLJoint* joint, bool zeroBase)
{
if (!joint)
return;
return LLQuaternion::DEFAULT;
LLQuaternion initalPublicRot = mRotation;
LLQuaternion invRot = mBaseRotation;
invRot.conjugate();
mRotation = joint->getRotation() * invRot;
LLQuaternion newPublicRot = joint->getRotation() * invRot;
if (zeroBase)
zeroBaseRotation();
mRotation.set(newPublicRot);
mPosition.set(joint->getPosition() - mBasePosition);
mScale.set(joint->getScale() - mBaseScale);
return newPublicRot *= ~initalPublicRot;
}
private:
@ -284,12 +295,14 @@ class FSJointPose
mPosition.set(state->mPosition);
mScale.set(state->mScale);
mUserSpecifiedBaseZero = state->userSetBaseRotationToZero();
mRotationIsWorldLocked = state->mRotationIsWorldLocked;
}
public:
LLQuaternion mRotation;
LLVector3 mPosition;
LLVector3 mScale;
bool mRotationIsWorldLocked = false;
private:
LLQuaternion mStartingRotation;

View File

@ -95,6 +95,7 @@ void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint
return;
jointPose->undoLastChange();
undoOrRedoWorldLockedDescendants(joint, posingMotion, false);
if (style == NONE || style == DELTAMODE)
return;
@ -104,6 +105,10 @@ void FSPoserAnimator::undoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint
return;
oppositeJointPose->undoLastChange();
auto oppositePoserJoint = getPoserJointByName(joint.mirrorJointName());
if (oppositePoserJoint)
undoOrRedoWorldLockedDescendants(*oppositePoserJoint, posingMotion, false);
}
void FSPoserAnimator::resetJoint(LLVOAvatar* avatar, const FSPoserJoint& joint, E_BoneDeflectionStyles style)
@ -173,6 +178,7 @@ void FSPoserAnimator::redoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint
return;
jointPose->redoLastChange();
undoOrRedoWorldLockedDescendants(joint, posingMotion, true);
if (style == NONE || style == DELTAMODE)
return;
@ -182,6 +188,10 @@ void FSPoserAnimator::redoLastJointChange(LLVOAvatar* avatar, const FSPoserJoint
return;
oppositeJointPose->redoLastChange();
auto oppositePoserJoint = getPoserJointByName(joint.mirrorJointName());
if (oppositePoserJoint)
undoOrRedoWorldLockedDescendants(*oppositePoserJoint, posingMotion, true);
}
LLVector3 FSPoserAnimator::getJointPosition(LLVOAvatar* avatar, const FSPoserJoint& joint) const
@ -274,9 +284,23 @@ bool FSPoserAnimator::getRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoser
if (!jointPose)
return false;
// TODO: FIRE-35769
return jointPose->getWorldRotationLockState();
}
return false;
void FSPoserAnimator::setRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState)
{
if (!isAvatarSafeToUse(avatar))
return;
FSPosingMotion* posingMotion = getPosingMotion(avatar);
if (!posingMotion)
return;
FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName());
if (!jointPose)
return;
jointPose->setWorldRotationLockState(newState);
}
bool FSPoserAnimator::exportRotationWillLockJoint(LLVOAvatar* avatar, const FSPoserJoint& joint) const
@ -382,7 +406,9 @@ void FSPoserAnimator::recaptureJointAsDelta(LLVOAvatar* avatar, const FSPoserJoi
if (!jointPose)
return;
jointPose->recaptureJointAsDelta(resetBaseRotationToZero);
LLQuaternion deltaRot = jointPose->recaptureJointAsDelta(resetBaseRotationToZero);
deRotateWorldLockedDescendants(joint, posingMotion, deltaRot);
if (style == NONE || style == DELTAMODE)
return;
@ -491,6 +517,7 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j
case DELTAMODE:
jointPose->setPublicRotation(resetBaseRotationToZero, deltaRot * jointPose->getPublicRotation());
deRotateWorldLockedDescendants(joint, posingMotion, deltaRot);
return;
case NONE:
@ -500,31 +527,42 @@ void FSPoserAnimator::setJointRotation(LLVOAvatar* avatar, const FSPoserJoint* j
else
jointPose->setPublicRotation(resetBaseRotationToZero, absRot);
deRotateWorldLockedDescendants(joint, posingMotion, deltaRot);
return;
}
deRotateWorldLockedDescendants(joint, posingMotion, deltaRot);
auto oppositePoserJoint = getPoserJointByName(joint->mirrorJointName());
FSJointPose* oppositeJointPose = posingMotion->getJointPoseByJointName(joint->mirrorJointName());
if (!oppositeJointPose)
return;
LLQuaternion inv_quat;
LLQuaternion inv_quat = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]);
switch (deflectionStyle)
{
case SYMPATHETIC:
oppositeJointPose->cloneRotationFrom(jointPose);
if (oppositePoserJoint)
deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, deltaRot);
break;
case SYMPATHETIC_DELTA:
oppositeJointPose->setPublicRotation(resetBaseRotationToZero, deltaRot * oppositeJointPose->getPublicRotation());
if (oppositePoserJoint)
deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, deltaRot);
break;
case MIRROR:
oppositeJointPose->mirrorRotationFrom(jointPose);
if (oppositePoserJoint)
deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, inv_quat);
break;
case MIRROR_DELTA:
inv_quat = LLQuaternion(-deltaRot.mQ[VX], deltaRot.mQ[VY], -deltaRot.mQ[VZ], deltaRot.mQ[VW]);
oppositeJointPose->setPublicRotation(resetBaseRotationToZero, inv_quat * oppositeJointPose->getPublicRotation());
if (oppositePoserJoint)
deRotateWorldLockedDescendants(oppositePoserJoint, posingMotion, inv_quat);
break;
default:
@ -987,3 +1025,98 @@ int FSPoserAnimator::getChildJointDepth(const FSPoserJoint* joint, int depth) co
return depth;
}
void FSPoserAnimator::deRotateWorldLockedDescendants(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion rotationChange)
{
size_t numberOfBvhChildNodes = joint->bvhChildren().size();
if (numberOfBvhChildNodes < 1)
return;
FSJointPose* parentJoint = posingMotion->getJointPoseByJointName(joint->jointName());
if (!parentJoint)
return;
LLJoint* pJoint = parentJoint->getJointState()->getJoint();
for (size_t index = 0; index != numberOfBvhChildNodes; ++index)
{
auto nextJoint = getPoserJointByName(joint->bvhChildren()[index]);
if (!nextJoint)
continue;
deRotateJointOrFirstLockedChild(nextJoint, posingMotion, pJoint->getWorldRotation(), rotationChange);
}
}
void FSPoserAnimator::deRotateJointOrFirstLockedChild(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion rotatedParentWorldRot, LLQuaternion rotationChange)
{
FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint->jointName());
if (!jointPose)
return;
if (jointPose->getWorldRotationLockState())
{
LLQuaternion worldRotOfThisJoint = jointPose->getJointState()->getJoint()->getWorldRotation();
LLQuaternion differenceInWorldRot = worldRotOfThisJoint * ~rotatedParentWorldRot;
LLQuaternion rotDiffInChildFrame = differenceInWorldRot * rotationChange * ~differenceInWorldRot;
rotDiffInChildFrame.conjugate();
jointPose->setPublicRotation(false, rotDiffInChildFrame * jointPose->getPublicRotation());
return;
}
size_t numberOfBvhChildNodes = joint->bvhChildren().size();
if (numberOfBvhChildNodes < 1)
return;
for (size_t index = 0; index != numberOfBvhChildNodes; ++index)
{
auto nextJoint = getPoserJointByName(joint->bvhChildren()[index]);
if (!nextJoint)
continue;
deRotateJointOrFirstLockedChild(nextJoint, posingMotion, rotatedParentWorldRot, rotationChange);
}
}
void FSPoserAnimator::undoOrRedoWorldLockedDescendants(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo)
{
size_t numberOfBvhChildNodes = joint.bvhChildren().size();
if (numberOfBvhChildNodes < 1)
return;
for (size_t index = 0; index != numberOfBvhChildNodes; ++index)
{
auto nextJoint = getPoserJointByName(joint.bvhChildren()[index]);
if (!nextJoint)
continue;
undoOrRedoJointOrFirstLockedChild(*nextJoint, posingMotion, redo);
}
}
void FSPoserAnimator::undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo)
{
FSJointPose* jointPose = posingMotion->getJointPoseByJointName(joint.jointName());
if (!jointPose)
return;
if (jointPose->getWorldRotationLockState())
{
redo ? jointPose->redoLastChange() : jointPose->undoLastChange();
return;
}
size_t numberOfBvhChildNodes = joint.bvhChildren().size();
if (numberOfBvhChildNodes < 1)
return;
for (size_t index = 0; index != numberOfBvhChildNodes; ++index)
{
auto nextJoint = getPoserJointByName(joint.bvhChildren()[index]);
if (!nextJoint)
continue;
undoOrRedoJointOrFirstLockedChild(*nextJoint, posingMotion, redo);
}
}

View File

@ -608,6 +608,14 @@ public:
/// <returns>True if the joint is maintaining a fixed-rotation in world, otherwise false.</returns>
bool getRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint) const;
/// <summary>
/// Sets the world-rotation-lock status for supplied joint for the supplied avatar.
/// </summary>
/// <param name="avatar">The avatar owning the supplied joint.</param>
/// <param name="joint">The joint to query.</param>
/// <param name="newState">The lock state to apply.</param>
void setRotationIsWorldLocked(LLVOAvatar* avatar, const FSPoserJoint& joint, bool newState);
/// <summary>
/// Determines if the kind of save to perform should be a 'delta' save, or a complete save.
/// </summary>
@ -732,6 +740,45 @@ public:
/// <returns>The number of generations of descendents the joint has, if none, then zero.</returns>
int getChildJointDepth(const FSPoserJoint* joint, int depth) const;
/// <summary>
/// Derotates the first world-locked child joint to the supplied joint.
/// </summary>
/// <param name="joint">The edited joint, whose children may be world-locked.</param>
/// <param name="posingMotion">The posing motion.</param>
/// <param name="rotation">The rotation the supplied joint was/is being changed by.</param>
/// <remarks>
/// There are two ways to resolve this problem: before the rotation is applied in the PosingMotion (the animation) or after.
/// If performed after, a feedback loop is created, because you're noting the world-rotation in one frame, then correcting it back to that in another.
/// This implementation works by applying an opposing-rotation to the locked child joint which is corrected for the relative world-rotations of parent and child.
/// </remarks>
void deRotateWorldLockedDescendants(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion rotation);
/// <summary>
/// Recursively tests the supplied joint and all its children for their world-locked status, and applies a de-rotation if it is world-locked.
/// </summary>
/// <param name="joint">The edited joint, whose children may be world-locked.</param>
/// <param name="posingMotion">The posing motion.</param>
/// <param name="parentWorldRot">The world-rotation of the joint that was edited.</param>
/// <param name="rotation">The rotation the joint was edit is being changed by.</param>
void deRotateJointOrFirstLockedChild(const FSPoserJoint* joint, FSPosingMotion* posingMotion, LLQuaternion parentWorldRot,
LLQuaternion rotation);
/// <summary>
/// Performs an undo or redo of an edit to the supplied joints world-locked descendants.
/// </summary>
/// <param name="joint">The edited joint, whose children may be world-locked.</param>
/// <param name="posingMotion">The posing motion.</param>
/// <param name="redo">Whether to redo the edit, otherwise the edit is undone.</param>
void undoOrRedoWorldLockedDescendants(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo);
/// <summary>
/// Recursively tests the supplied joint and all its children for their world-locked status, and applies an undo or redo if it is world-locked.
/// </summary>
/// <param name="joint">The joint which will have the undo or redo performed, if it is world locked.</param>
/// <param name="posingMotion">The posing motion.</param>
/// <param name="redo">Whether to redo the edit, otherwise the edit is undone.</param>
void undoOrRedoJointOrFirstLockedChild(const FSPoserJoint& joint, FSPosingMotion* posingMotion, bool redo);
/// <summary>
/// Maps the avatar's ID to the animation registered to them.
/// Thus we start/stop the same animation, and get/set the same rotations etc.

View File

@ -1751,6 +1751,20 @@ width="430">
<button.commit_callback
function="Poser.TogglePosingSelectedBones"/>
</button>
<button
follows="left|top"
height="21"
layout="topleft"
image_overlay="Locked_Icon"
image_hover_unselected="Toolbar_Middle_Over"
image_selected="Toolbar_Middle_Selected"
image_unselected="Toolbar_Middle_Off"
name="toggle_LockWorldRotation"
left_pad="1"
top_delta="0"
tool_tip="Toggle rotation world-lock for the selected limb(s). Limbs that are world-locked keep the same rotation in-world when their parent-limbs move. Eg: lock your eyes and turn your head, your eyes keep looking in the same direction."
width="21" >
</button>
<panel
follows="left|top|bottom"
height="22"
@ -1760,7 +1774,7 @@ width="430">
left_pad="0"
top_delta="0"
name="button_spacer_panel"
width="80"/>
width="59"/>
<button
follows="left|top"
height="21"