Skip to content

Commit

Permalink
Merge pull request #1399 from ergoxiv/hotfix/expression-pos-reset-on-…
Browse files Browse the repository at this point in the history
…mismatched-import

Simplified import process; Resolved issue causing facial expression to reset due to bone mismatch
  • Loading branch information
StoiaCode authored Nov 12, 2024
2 parents d65794f + c6ff147 commit 6f5311d
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 88 deletions.
62 changes: 30 additions & 32 deletions Anamnesis/Actor/Pages/PosePage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ private async Task ImportPose(PoseImportOptions importOption, PoseFile.Mode mode
if (result.File is not PoseFile poseFile)
return;

Dictionary<string, Vector3> bonePositions = new();
Dictionary<BoneVisual3d, Vector3> bodyPositions = new();
Dictionary<BoneVisual3d, Vector3> facePositions = new();
bool mismatchedFaceBones = false;

// Disable auto-commit at the beginning
Expand Down Expand Up @@ -411,22 +412,29 @@ private async Task ImportPose(PoseImportOptions importOption, PoseFile.Mode mode
PoseService.Instance.SetEnabled(true);
PoseService.Instance.FreezeScale |= mode.HasFlag(PoseFile.Mode.Scale);
PoseService.Instance.FreezeRotation |= mode.HasFlag(PoseFile.Mode.Rotation);
PoseService.Instance.FreezePositions |= mode.HasFlag(PoseFile.Mode.Position);

if (importOption == PoseImportOptions.SelectedBones)
{
PoseService.Instance.FreezePositions = mode.HasFlag(PoseFile.Mode.Position);

// Don't unselected bones after import. Let the user decide what to do with the selection.
var selectedBones = this.Skeleton.SelectedBones.Select(bone => bone.BoneName).ToHashSet();
await poseFile.Apply(this.Actor, this.Skeleton, selectedBones, mode, false);
poseFile.Apply(this.Actor, this.Skeleton, selectedBones, mode, false);
return;
}

// Backup face bone positions before importing the body pose.
// "Freeze Position" toggle resets them, so restore after import. Relevant only when pose service is enabled.
this.Skeleton.SelectHead();
facePositions = this.Skeleton.SelectedBones.ToDictionary(bone => bone, bone => bone.Position);
this.Skeleton.ClearSelection();

// Step 1: Import body part of the pose
if (importOption is PoseImportOptions.Character or PoseImportOptions.FullTransform or PoseImportOptions.BodyOnly)
{
this.Skeleton.SelectBody();
var selectedBoneNames = this.Skeleton.SelectedBones.Select(bone => bone.BoneName).ToHashSet();
bodyPositions = this.Skeleton.SelectedBones.ToDictionary(bone => bone, bone => bone.Position);
this.Skeleton.ClearSelection();

// Don't import body with positions unless it's "Full Transform".
// Otherwise, the body will be deformed if the pose file was created for another race.
Expand All @@ -436,28 +444,22 @@ private async Task ImportPose(PoseImportOptions importOption, PoseFile.Mode mode
mode &= ~PoseFile.Mode.Position;
}

PoseService.Instance.FreezePositions = mode.HasFlag(PoseFile.Mode.Position);

// Don't apply the facial expression hack for the body import step.
// Otherwise, the head won't pose as intended and will return to its original position.
await poseFile.Apply(this.Actor, this.Skeleton, selectedBoneNames, mode, false);
poseFile.Apply(this.Actor, this.Skeleton, selectedBoneNames, mode, false);

// Re-enable positions if they were disabled.
if (doLegacyImport)
{
mode |= PoseFile.Mode.Position;
PoseService.Instance.FreezePositions |= mode.HasFlag(PoseFile.Mode.Position);
foreach ((BoneVisual3d bone, Vector3 position) in bodyPositions)
{
bone.Position = position;
bone.WriteTransform(this.Skeleton, false);
}
}

// Store updated bone positions as they will get reset by the expression import
bonePositions = this.Skeleton.SelectedBones.Select(bone => (bone.BoneName, bone.Position)).ToDictionary(t => t.BoneName, t => t.Position);
this.Skeleton.ClearSelection();
}

// Body pose is loaded. Exit early if we're only importing the body.
if (importOption == PoseImportOptions.BodyOnly)
return;

// Step 2: Import the facial expression
if (!mismatchedFaceBones && (importOption is PoseImportOptions.Character or PoseImportOptions.FullTransform or PoseImportOptions.ExpressionOnly))
{
Expand All @@ -473,33 +475,26 @@ private async Task ImportPose(PoseImportOptions importOption, PoseFile.Mode mode
mode &= ~PoseFile.Mode.Position;
}

PoseService.Instance.FreezePositions = mode.HasFlag(PoseFile.Mode.Position);

// Apply facial expression hack for the expression import
await poseFile.Apply(this.Actor, this.Skeleton, selectedBones, mode, true);
poseFile.Apply(this.Actor, this.Skeleton, selectedBones, mode, true);

// Re-enable positions if they were disabled.
if (doLegacyImport)
{
mode |= PoseFile.Mode.Position;
PoseService.Instance.FreezePositions |= mode.HasFlag(PoseFile.Mode.Position);
foreach ((BoneVisual3d bone, Vector3 position) in facePositions)
{
bone.Position = position;
bone.WriteTransform(this.Skeleton, false);
}
}
}

// Expression is loaded. Exit early if we're only importing expressions.
if (importOption == PoseImportOptions.ExpressionOnly)
return;

// Step 3: Restore body bone positions
if (importOption == PoseImportOptions.FullTransform && mode.HasFlag(PoseFile.Mode.Position))
// Step 3: Restore face bone positions if face bones were mismatched
if (mismatchedFaceBones)
{
foreach ((string name, Vector3 position) in bonePositions)
foreach ((BoneVisual3d bone, Vector3 position) in facePositions)
{
BoneVisual3d? bone = this.Skeleton.GetBone(name);

if (bone == null)
continue;

bone.Position = position;
bone.WriteTransform(this.Skeleton, false);
}
Expand All @@ -511,6 +506,9 @@ private async Task ImportPose(PoseImportOptions importOption, PoseFile.Mode mode
}
finally
{
// Update the skeleton after applying the pose
this.Skeleton.ReadTransforms();

// Re-enable auto-commit and commit changes
this.Actor.History.Commit();
this.Actor.History.AutoCommitEnabled = originalAutoCommitEnabled;
Expand Down
44 changes: 18 additions & 26 deletions Anamnesis/Actor/Posing/Visuals/BoneVisual3d.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,33 +290,25 @@ public virtual void ReadTransform(bool readChildren = false, Dictionary<string,
newTransform.Rotation = CmQuaternion.Normalize(CmQuaternion.Multiply(parentRot, newTransform.Rotation));
}

// Check if there are actual changes before updating
bool positionChanged = !this.Position.IsApproximately(newTransform.Position);
bool rotationChanged = !this.Rotation.IsApproximately(newTransform.Rotation);
bool scaleChanged = !this.Scale.IsApproximately(newTransform.Scale);

if (positionChanged || rotationChanged || scaleChanged)
// Apply the updates in a single step
this.Position = newTransform.Position;
this.Rotation = newTransform.Rotation;
this.Scale = newTransform.Scale;

// Set the Media3D hierarchy transforms
this.rotation.Quaternion = newTransform.Rotation.ToMedia3DQuaternion();
this.position.OffsetX = newTransform.Position.X;
this.position.OffsetY = newTransform.Position.Y;
this.position.OffsetZ = newTransform.Position.Z;

// Draw a line for visualization
if (this.Parent != null && this.lineToParent != null)
{
// Apply the updates in a single step
this.Position = newTransform.Position;
this.Rotation = newTransform.Rotation;
this.Scale = newTransform.Scale;

// Set the Media3D hierarchy transforms
this.rotation.Quaternion = newTransform.Rotation.ToMedia3DQuaternion();
this.position.OffsetX = newTransform.Position.X;
this.position.OffsetY = newTransform.Position.Y;
this.position.OffsetZ = newTransform.Position.Z;

// Draw a line for visualization
if (this.Parent != null && this.lineToParent != null)
{
var endPoint = this.lineToParent.Points[1];
endPoint.X = newTransform.Position.X;
endPoint.Y = newTransform.Position.Y;
endPoint.Z = newTransform.Position.Z;
this.lineToParent.Points[1] = endPoint;
}
var endPoint = this.lineToParent.Points[1];
endPoint.X = newTransform.Position.X;
endPoint.Y = newTransform.Position.Y;
endPoint.Z = newTransform.Position.Z;
this.lineToParent.Points[1] = endPoint;
}

if (readChildren)
Expand Down
8 changes: 4 additions & 4 deletions Anamnesis/Actor/Refresh/BrioActorRefresher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

namespace Anamnesis.Actor.Refresh;

using System.Threading.Tasks;
using Anamnesis.Brio;
using Anamnesis.Files;
using Anamnesis.Memory;
using Anamnesis.Services;
using System.Threading.Tasks;
using XivToolsWpf;

public class BrioActorRefresher : IActorRefresher
Expand All @@ -28,7 +28,7 @@ public async Task RefreshActor(ActorMemory actor)

RedrawType redrawType = RedrawType.AllowFull | RedrawType.PreservePosition | RedrawType.AllowOptimized | RedrawType.ForceAllowNPCAppearance;

if(actor.IsWeaponDirty)
if (actor.IsWeaponDirty)
{
redrawType |= RedrawType.ForceRedrawWeaponsOnOptimized;
}
Expand All @@ -48,7 +48,7 @@ public async Task RefreshActor(ActorMemory actor)

// Redraw
var result = await Brio.Redraw(actor.ObjectIndex, redrawType);
if(result == "\"Full\"")
if (result == "\"Full\"")
{
// TODO: It's probably best to find some way to detect when it's safe
// this is a good first attempt though.
Expand All @@ -60,7 +60,7 @@ public async Task RefreshActor(ActorMemory actor)
// Restore current pose
skeletonVisual3D = new SkeletonVisual3d();
await skeletonVisual3D.SetActor(actor);
await poseFile.Apply(actor, skeletonVisual3D, null, PoseFile.Mode.All, true);
poseFile.Apply(actor, skeletonVisual3D, null, PoseFile.Mode.All, true);
}).Start();
}
}
Expand Down
27 changes: 3 additions & 24 deletions Anamnesis/Files/PoseFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,7 @@ public enum BoneProcessingModes

public static BoneProcessingModes GetBoneMode(ActorMemory? actor, SkeletonVisual3d? skeleton, string boneName)
{
if (boneName == "n_root")
return BoneProcessingModes.Ignore;

// Special case for elezen ears as they cannot use other races ear values.
if (actor?.Customize?.Race == ActorCustomizeMemory.Races.Elezen)
{
// append '_elezen' to both ear bones.
if (boneName == "j_mimi_l" || boneName == "j_mimi_r")
{
return BoneProcessingModes.KeepRelative;
}
}

return BoneProcessingModes.FullLoad;
return boneName != "n_root" ? BoneProcessingModes.FullLoad : BoneProcessingModes.Ignore;
}

public static async Task<DirectoryInfo?> Save(DirectoryInfo? dir, ActorMemory? actor, SkeletonVisual3d? skeleton, HashSet<string>? bones = null, bool editMeta = false)
Expand Down Expand Up @@ -107,7 +94,7 @@ public void WriteToFile(ActorMemory actor, SkeletonVisual3d skeleton, HashSet<st
}
}

public async Task Apply(ActorMemory actor, SkeletonVisual3d skeleton, HashSet<string>? bones, Mode mode, bool doFacialExpressionHack)
public void Apply(ActorMemory actor, SkeletonVisual3d skeleton, HashSet<string>? bones, Mode mode, bool doFacialExpressionHack)
{
if (actor == null)
throw new ArgumentNullException(nameof(actor));
Expand Down Expand Up @@ -298,8 +285,6 @@ public async Task Apply(ActorMemory actor, SkeletonVisual3d skeleton, HashSet<st
bone.ReadTransform();
bone.WriteTransform(skeleton, false);
}

await Task.Delay(10);
}

// Restore the head bone rotation if we were only loading an expression
Expand All @@ -323,9 +308,6 @@ public async Task Apply(ActorMemory actor, SkeletonVisual3d skeleton, HashSet<st
bone.WriteTransform(skeleton, false);
}

// Give enough time for the game to process the bone transform updates.
await Task.Delay(100);

skeletonMem.PauseSynchronization = false;
skeletonMem.WriteDelayedBinds();

Expand All @@ -349,13 +331,10 @@ public bool IsPreDTPoseFile()
}
}

if (!hasFaceBones)
return false;

// Looking for the tongue-A bone, a new bone common to all races and genders added in DT.
// If we dont have it, we are assumed to be a pre-DT pose file.
// This doesn't account for users manually editing the JSON.
if (!this.Bones.ContainsKey("j_f_bero_01"))
if (!hasFaceBones || !this.Bones.ContainsKey("j_f_bero_01"))
return true;

return false;
Expand Down
2 changes: 1 addition & 1 deletion Anamnesis/Files/SceneFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public async Task Apply(Mode mode)
if (mode.HasFlag(Mode.Pose))
{
// TODO: This should follow the same approach as the pose page imports
await entry.Pose!.Apply(actor, skeleton, null, PoseFile.Mode.Rotation, true);
entry.Pose!.Apply(actor, skeleton, null, PoseFile.Mode.Rotation, true);
}
}

Expand Down
2 changes: 1 addition & 1 deletion Anamnesis/Views/TargetSelectorView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ private async void OnCreateActorClicked(object sender, RoutedEventArgs e)
fullActor.SetAddress(newActor.Address);
fullActor.Synchronize();
await skeletonVisual3D.SetActor(fullActor);
await poseFile.Apply(fullActor, skeletonVisual3D, null, PoseFile.Mode.Rotation, true);
poseFile.Apply(fullActor, skeletonVisual3D, null, PoseFile.Mode.Rotation, true);
}
}

Expand Down

0 comments on commit 6f5311d

Please sign in to comment.