Skip to content

Commit 96c1a37

Browse files
committed
Move root with pelvis tracker if it's active
1 parent eee1d7b commit 96c1a37

File tree

5 files changed

+200
-57
lines changed

5 files changed

+200
-57
lines changed

Source/CustomAvatar/Avatar/AvatarIK.cs

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public bool isLocomotionEnabled
5959
private Pose _defaultRootPose;
6060
private Pose _previousParentPose;
6161

62+
private bool _hasPelvisTarget;
63+
private bool _hasBothLegTargets;
64+
6265
#region Behaviour Lifecycle
6366

6467
protected void Awake()
@@ -139,7 +142,6 @@ protected void Start()
139142

140143
_input.inputChanged += OnInputChanged;
141144

142-
UpdateLocomotion();
143145
UpdateSolverTargets();
144146

145147
foreach (BeatSaberDynamicBone::DynamicBone dynamicBone in _dynamicBones)
@@ -179,6 +181,8 @@ protected void OnDestroy()
179181

180182
private void OnPreUpdate()
181183
{
184+
ApplyRootPose();
185+
182186
foreach (BeatSaberDynamicBone::DynamicBone dynamicBone in _dynamicBones)
183187
{
184188
dynamicBone.Update();
@@ -195,6 +199,39 @@ private void OnPostUpdate()
195199
}
196200
}
197201

202+
// adapted from VRIKRootController to work when IK is rotated by parent
203+
private void ApplyRootPose()
204+
{
205+
// don't move the root if locomotion is disabled and both feet aren't being tracked
206+
// (i.e. keep previous behaviour of sticking to the origin when locomotion is disabled and we're missing one or more FBT trackers)
207+
if (!_isLocomotionEnabled && !_hasBothLegTargets)
208+
{
209+
return;
210+
}
211+
212+
if (!_hasPelvisTarget)
213+
{
214+
return;
215+
}
216+
217+
Transform pelvisTarget = _vrik.solver.spine.pelvisTarget;
218+
Transform root = _vrik.references.root;
219+
Transform parent = root.parent;
220+
bool hasParent = parent != null;
221+
222+
Vector3 up = hasParent ? parent.up : Vector3.up;
223+
root.rotation = Quaternion.LookRotation(Vector3.ProjectOnPlane(pelvisTarget.rotation * _avatar.prefab.pelvisRootForward, up), up);
224+
225+
var position = Vector3.ProjectOnPlane(pelvisTarget.position - root.TransformVector(_avatar.prefab.pelvisRootOffset), up);
226+
227+
if (hasParent)
228+
{
229+
position = parent.InverseTransformPoint(position);
230+
}
231+
232+
root.localPosition = new Vector3(position.x, root.localPosition.y, position.z);
233+
}
234+
198235
private void ApplyPlatformMotion()
199236
{
200237
Transform parent = _vrik.references.root.parent;
@@ -221,10 +258,16 @@ private void UpdateLocomotion()
221258
return;
222259
}
223260

224-
_vrik.solver.locomotion.weight = _isLocomotionEnabled ? vrikManager.solver_locomotion_weight : 0;
261+
// don't enable locomotion if FBT is applied
262+
bool shouldEnableLocomotion = _isLocomotionEnabled && !(_hasPelvisTarget && _hasBothLegTargets);
225263

226-
if (!_isLocomotionEnabled && _vrik.references.root != null)
264+
if (shouldEnableLocomotion)
265+
{
266+
_vrik.solver.locomotion.weight = vrikManager.solver_locomotion_weight;
267+
}
268+
else
227269
{
270+
_vrik.solver.locomotion.weight = 0;
228271
_vrik.references.root.SetLocalPose(_defaultRootPose);
229272
}
230273
}
@@ -236,24 +279,31 @@ private void OnInputChanged()
236279

237280
private void UpdateSolverTargets()
238281
{
239-
if (_vrik == null || vrikManager == null) return;
282+
if (_vrik == null || vrikManager == null)
283+
{
284+
return;
285+
}
240286

241287
_logger.LogTrace("Updating solver targets");
242288

243289
UpdateSolverTarget(DeviceUse.Head, ref _vrik.solver.spine.headTarget, ref _vrik.solver.spine.positionWeight, ref _vrik.solver.spine.rotationWeight);
244290
UpdateSolverTarget(DeviceUse.LeftHand, ref _vrik.solver.leftArm.target, ref _vrik.solver.leftArm.positionWeight, ref _vrik.solver.leftArm.rotationWeight);
245291
UpdateSolverTarget(DeviceUse.RightHand, ref _vrik.solver.rightArm.target, ref _vrik.solver.rightArm.positionWeight, ref _vrik.solver.rightArm.rotationWeight);
246-
UpdateSolverTarget(DeviceUse.LeftFoot, ref _vrik.solver.leftLeg.target, ref _vrik.solver.leftLeg.positionWeight, ref _vrik.solver.leftLeg.rotationWeight);
247-
UpdateSolverTarget(DeviceUse.RightFoot, ref _vrik.solver.rightLeg.target, ref _vrik.solver.rightLeg.positionWeight, ref _vrik.solver.rightLeg.rotationWeight);
248292

249-
if (UpdateSolverTarget(DeviceUse.Waist, ref _vrik.solver.spine.pelvisTarget, ref _vrik.solver.spine.pelvisPositionWeight, ref _vrik.solver.spine.pelvisRotationWeight))
293+
_hasBothLegTargets = true;
294+
_hasBothLegTargets &= UpdateSolverTarget(DeviceUse.LeftFoot, ref _vrik.solver.leftLeg.target, ref _vrik.solver.leftLeg.positionWeight, ref _vrik.solver.leftLeg.rotationWeight);
295+
_hasBothLegTargets &= UpdateSolverTarget(DeviceUse.RightFoot, ref _vrik.solver.rightLeg.target, ref _vrik.solver.rightLeg.positionWeight, ref _vrik.solver.rightLeg.rotationWeight);
296+
297+
if (_hasPelvisTarget = UpdateSolverTarget(DeviceUse.Waist, ref _vrik.solver.spine.pelvisTarget, ref _vrik.solver.spine.pelvisPositionWeight, ref _vrik.solver.spine.pelvisRotationWeight))
250298
{
251299
_vrik.solver.plantFeet = false;
252300
}
253301
else
254302
{
255303
_vrik.solver.plantFeet = vrikManager.solver_plantFeet;
256304
}
305+
306+
UpdateLocomotion();
257307
}
258308

259309
private bool UpdateSolverTarget(DeviceUse deviceUse, ref Transform target, ref float positionWeight, ref float rotationWeight)

Source/CustomAvatar/Avatar/AvatarPrefab.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public class AvatarPrefab : MonoBehaviour
8282

8383
internal Pose rightFootCalibrationOffset { get; private set; }
8484

85+
internal Vector3 pelvisRootForward { get; private set; }
86+
87+
internal Vector3 pelvisRootOffset { get; private set; }
88+
8589
private ILogger<AvatarPrefab> _logger;
8690

8791
[Inject]
@@ -195,6 +199,9 @@ internal void Construct(ILoggerFactory loggerFactory, DiContainer container)
195199

196200
Destroy(targetObj);
197201
}
202+
203+
pelvisRootForward = Quaternion.Inverse(vrikManager.references_pelvis.rotation) * vrikManager.references_root.forward;
204+
pelvisRootOffset = vrikManager.references_root.InverseTransformPoint(vrikManager.references_pelvis.position);
198205
}
199206
}
200207

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Beat Saber Custom Avatars - Custom player models for body presence in Beat Saber.
2+
// Copyright © 2018-2024 Nicolas Gnyra and Beat Saber Custom Avatars Contributors
3+
//
4+
// This library is free software: you can redistribute it and/or
5+
// modify it under the terms of the GNU Lesser General Public
6+
// License as published by the Free Software Foundation, either
7+
// version 3 of the License, or (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
extern alias BeatSaberFinalIK;
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Reflection;
22+
using System.Reflection.Emit;
23+
using BeatSaberFinalIK::RootMotion.FinalIK;
24+
using CustomAvatar.Scripts;
25+
using CustomAvatar.Utilities;
26+
using HarmonyLib;
27+
28+
namespace CustomAvatar.Patches
29+
{
30+
/// <summary>
31+
/// This patch enables the use of <see cref="IKSolverVR_Arm.shoulderPitchOffset"/>.
32+
/// </summary>
33+
34+
[HarmonyPatch(typeof(IKSolverVR.Arm), nameof(IKSolverVR.Arm.Solve))]
35+
internal static class IKSolverVR_Arm_PitchAngleOffset
36+
{
37+
private static readonly FieldInfo kPitchOffsetAngleField = AccessTools.DeclaredField(typeof(IKSolverVR_Arm), nameof(IKSolverVR_Arm.shoulderPitchOffset));
38+
39+
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
40+
{
41+
return new CodeMatcher(instructions)
42+
/* Quaternion.AngleAxis(isLeft ? pitchOffsetAngle : -pitchOffsetAngle, chestForward) */
43+
.MatchForward(false,
44+
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, 30f)),
45+
new CodeMatch(i => i.Branches(out Label? _)),
46+
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, -30f)))
47+
.SetAndAdvance(OpCodes.Ldarg_0, null)
48+
.InsertAndAdvance(
49+
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField),
50+
new CodeInstruction(OpCodes.Neg))
51+
.Advance(1)
52+
.SetAndAdvance(OpCodes.Ldarg_0, null)
53+
.InsertAndAdvance(
54+
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField))
55+
56+
/* pitch -= pitchOffsetAngle */
57+
.MatchForward(false,
58+
new CodeMatch(i => i.LoadsLocal(11)),
59+
new CodeMatch(i => i.opcode == OpCodes.Ldc_R4 && (float)i.operand == -30f),
60+
new CodeMatch(OpCodes.Sub),
61+
new CodeMatch(i => i.SetsLocal(11)))
62+
.ThrowIfInvalid("`pitch -= pitchOffsetAngle` not found")
63+
.Advance(1)
64+
.SetAndAdvance(OpCodes.Ldarg_0, null)
65+
.InsertAndAdvance(
66+
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField))
67+
68+
/* DamperValue(pitch, -45f - pitchOffsetAngle, 45f - pitchOffsetAngle) */
69+
.MatchForward(false,
70+
new CodeMatch(i => i.LoadsLocal(11)),
71+
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, -15f)),
72+
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, 75f)))
73+
.Advance(1)
74+
.SetOperandAndAdvance(-45f)
75+
.InsertAndAdvance(
76+
new CodeInstruction(OpCodes.Ldarg_0, null),
77+
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField),
78+
new CodeInstruction(OpCodes.Sub))
79+
.SetOperandAndAdvance(45f)
80+
.InsertAndAdvance(
81+
new CodeInstruction(OpCodes.Ldarg_0, null),
82+
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField),
83+
new CodeInstruction(OpCodes.Sub))
84+
.InstructionEnumeration();
85+
}
86+
}
87+
}

Source/CustomAvatar/Patches/IKSolverVR.cs

Lines changed: 49 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
extern alias BeatSaberFinalIK;
1818

19-
using System;
2019
using System.Collections.Generic;
20+
using System.Linq;
2121
using System.Reflection;
2222
using System.Reflection.Emit;
2323
using BeatSaberFinalIK::RootMotion.FinalIK;
@@ -27,66 +27,65 @@
2727

2828
namespace CustomAvatar.Patches
2929
{
30+
/// <summary>
31+
/// This patch makes the constructor of <see cref="IKSolverVR"/> instantiate our <see cref="IKSolverVR_Arm"/> in the <see cref="IKSolverVR.leftArm"/> and <see cref="IKSolverVR.rightArm"/> fields.
32+
/// </summary>
3033
[HarmonyPatch(typeof(IKSolverVR), MethodType.Constructor)]
3134
internal static class IKSolverVR_Constructor
3235
{
33-
public static void Postfix(IKSolverVR __instance)
36+
private static readonly ConstructorInfo kIKSolverVRArmConstructor = typeof(IKSolverVR.Arm).GetConstructors().Single();
37+
private static readonly ConstructorInfo kNewIKSolverVRArmConstructor = typeof(IKSolverVR_Arm).GetConstructors().Single();
38+
39+
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
3440
{
35-
__instance.leftArm = new IKSolverVR_Arm();
36-
__instance.rightArm = new IKSolverVR_Arm();
41+
foreach (CodeInstruction instruction in instructions)
42+
{
43+
if (instruction.opcode == OpCodes.Newobj && (ConstructorInfo)instruction.operand == kIKSolverVRArmConstructor)
44+
{
45+
instruction.operand = kNewIKSolverVRArmConstructor;
46+
}
47+
48+
yield return instruction;
49+
}
3750
}
3851
}
3952

40-
[HarmonyPatch(typeof(IKSolverVR.Arm), nameof(IKSolverVR.Arm.Solve))]
41-
internal static class IKSolverVR_Arm_PitchAngleOffset
53+
/// <summary>
54+
/// This patch prevents locomotion from fighting against the position we set in <see cref="Avatar.AvatarIK"/> when the pelvis target exists.
55+
/// </summary>
56+
[HarmonyPatch(typeof(IKSolverVR), nameof(IKSolverVR.Solve))]
57+
internal static class IKSolverVR_Solve
4258
{
43-
private static readonly FieldInfo kPitchOffsetAngleField = AccessTools.DeclaredField(typeof(IKSolverVR_Arm), nameof(IKSolverVR_Arm.shoulderPitchOffset));
59+
private static readonly MethodInfo kRootBonePropertyGetter = AccessTools.DeclaredPropertyGetter(typeof(IKSolverVR), nameof(IKSolverVR.rootBone));
60+
private static readonly FieldInfo kVirtualBoneSolverPositionField = AccessTools.DeclaredField(typeof(IKSolverVR.VirtualBone), nameof(IKSolverVR.VirtualBone.solverPosition));
61+
private static readonly FieldInfo kSpineField = AccessTools.DeclaredField(typeof(IKSolverVR), nameof(IKSolverVR.spine));
62+
private static readonly FieldInfo kSpinePelvisTargetField = AccessTools.DeclaredField(typeof(IKSolverVR.Spine), nameof(IKSolverVR.Spine.pelvisTarget));
63+
private static readonly MethodInfo kUnityObjectEqualsMethod = AccessTools.DeclaredMethod(typeof(UnityEngine.Object), "op_Equality");
4464

45-
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
65+
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator ilGenerator)
4666
{
47-
return new CodeMatcher(instructions)
48-
/* Quaternion.AngleAxis(isLeft ? pitchOffsetAngle : -pitchOffsetAngle, chestForward) */
49-
.MatchForward(false,
50-
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, 30f)),
51-
new CodeMatch(i => i.Branches(out Label? _)),
52-
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, -30f)))
53-
.SetAndAdvance(OpCodes.Ldarg_0, null)
54-
.InsertAndAdvance(
55-
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField),
56-
new CodeInstruction(OpCodes.Neg))
57-
.Advance(1)
58-
.SetAndAdvance(OpCodes.Ldarg_0, null)
59-
.InsertAndAdvance(
60-
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField))
61-
62-
/* pitch -= pitchOffsetAngle */
63-
.MatchForward(false,
64-
new CodeMatch(i => i.LoadsLocal(11)),
65-
new CodeMatch(i => i.opcode == OpCodes.Ldc_R4 && (float)i.operand == -30f),
66-
new CodeMatch(OpCodes.Sub),
67-
new CodeMatch(i => i.SetsLocal(11)))
68-
.ThrowIfInvalid("`pitch -= pitchOffsetAngle` not found")
69-
.Advance(1)
70-
.SetAndAdvance(OpCodes.Ldarg_0, null)
71-
.InsertAndAdvance(
72-
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField))
73-
74-
/* DamperValue(pitch, -45f - pitchOffsetAngle, 45f - pitchOffsetAngle) */
75-
.MatchForward(false,
76-
new CodeMatch(i => i.LoadsLocal(11)),
77-
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, -15f)),
78-
new CodeMatch(i => i.Equals(OpCodes.Ldc_R4, 75f)))
79-
.Advance(1)
80-
.SetOperandAndAdvance(-45f)
81-
.InsertAndAdvance(
82-
new CodeInstruction(OpCodes.Ldarg_0, null),
83-
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField),
84-
new CodeInstruction(OpCodes.Sub))
85-
.SetOperandAndAdvance(45f)
67+
return new CodeMatcher(instructions, ilGenerator)
68+
.MatchForward(
69+
false,
70+
new CodeMatch(OpCodes.Ldarg_0),
71+
new CodeMatch(i => i.Calls(kRootBonePropertyGetter)),
72+
new CodeMatch(i => i.LoadsLocal(14)),
73+
new CodeMatch(i => i.StoresField(kVirtualBoneSolverPositionField)))
74+
.CreateLabelWithOffsets(4, out Label label)
8675
.InsertAndAdvance(
87-
new CodeInstruction(OpCodes.Ldarg_0, null),
88-
new CodeInstruction(OpCodes.Ldfld, kPitchOffsetAngleField),
89-
new CodeInstruction(OpCodes.Sub))
76+
new CodeInstruction(OpCodes.Ldarg_0),
77+
new CodeInstruction(OpCodes.Ldfld, kSpineField),
78+
new CodeInstruction(OpCodes.Ldfld, kSpinePelvisTargetField),
79+
new CodeInstruction(OpCodes.Ldnull),
80+
new CodeInstruction(OpCodes.Call, kUnityObjectEqualsMethod),
81+
new CodeInstruction(OpCodes.Brfalse_S, label))
82+
.MatchForward(
83+
false,
84+
new CodeMatch(OpCodes.Ldarg_0),
85+
new CodeMatch(i => i.Calls(kRootBonePropertyGetter)),
86+
new CodeMatch(i => i.LoadsField(kVirtualBoneSolverPositionField)))
87+
.RemoveInstructions(3)
88+
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_S, 14))
9089
.InstructionEnumeration();
9190
}
9291
}

0 commit comments

Comments
 (0)