Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
df94822
updated protoocol
daniele-dcl Nov 26, 2025
130a441
head angles syncing
daniele-dcl Dec 2, 2025
8f0847f
working head sync
daniele-dcl Dec 3, 2025
abc630e
fixed syncing edge case
daniele-dcl Dec 3, 2025
cc28013
fixed usage if is enabled flag
daniele-dcl Dec 4, 2025
551079b
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 4, 2025
18ee906
general clean-up
daniele-dcl Dec 4, 2025
f562bc7
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 9, 2025
fe0b448
remote players head ik interpolation
daniele-dcl Dec 9, 2025
c83e6a3
optimized head ik math
daniele-dcl Dec 9, 2025
b343f73
max remote players distance to compute head ik
daniele-dcl Dec 9, 2025
08985bd
head sync setting
daniele-dcl Dec 11, 2025
2263c30
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 11, 2025
a7749a0
updated proto
daniele-dcl Dec 11, 2025
0ae6c1f
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 15, 2025
3dc1524
separated control of head ik yaw and pitch
daniele-dcl Dec 15, 2025
b2760ee
head sync feature flag
daniele-dcl Dec 16, 2025
44572f1
moved head sync setting
daniele-dcl Dec 16, 2025
3022b79
haed sync always enabled in editor
daniele-dcl Dec 16, 2025
5e632a0
fixed 1st person camera rotation issues
daniele-dcl Dec 17, 2025
f575f39
enable head sync with --head-sync command line arg
daniele-dcl Dec 17, 2025
c13e913
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 17, 2025
8c5b4ab
rebuilt protocol
daniele-dcl Dec 17, 2025
63f1b37
updated protocol to exp branch
daniele-dcl Dec 17, 2025
2351285
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 19, 2025
925d294
fixed compilation error
daniele-dcl Dec 19, 2025
d845476
fixed unit tests
daniele-dcl Dec 19, 2025
34db369
Merge branch 'dev' into feat/head-sync
daniele-dcl Dec 22, 2025
5e7ee74
fixed missing head sync toggle in settings
daniele-dcl Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using DCL.AvatarRendering.Wearables.Helpers;
using DCL.Character.Components;
using DCL.Diagnostics;
using DCL.FeatureFlags;
using DCL.Optimization.PerformanceBudgeting;
using DCL.Optimization.Pools;
using DCL.Utilities;
Expand Down Expand Up @@ -124,6 +125,17 @@ protected override void Update(float t)
AvatarCustomSkinningComponent skinningComponent = InstantiateAvatar(ref avatarShapeComponent, in wearablesResult, avatarBase);

World.Add(entity, avatarBase, (IAvatarView)avatarBase, avatarTransformMatrixComponent, skinningComponent);

// Only enable the rig if head-sync is enabled
// The local player will ALWAYS re-enable the rig in InstantiateMainPlayerAvatar, so it's safe
avatarBase.RigBuilder.enabled = FeaturesRegistry.Instance.IsEnabled(FeatureId.HEAD_SYNC);

// Hands / Feet IK components are not added to remote entities
// We still disable the rigs to ensure no cpu time is wasted
// For the local player avatar we re-enable the rigs in InstantiateMainPlayerAvatar
avatarBase.HandsIKRig.enabled = false;
avatarBase.FeetIKRig.enabled = false;

return avatarBase;
}

Expand All @@ -136,7 +148,11 @@ private void InstantiateMainPlayerAvatar(in Entity entity, ref AvatarShapeCompon

if (avatarBase != null)
{
// Re-enable rigs since by default we disable them when instantiating new avatars
avatarBase.RigBuilder.enabled = true;
avatarBase.HandsIKRig.enabled = true;
avatarBase.FeetIKRig.enabled = true;

mainPlayerAvatarBaseProxy.SetObject(avatarBase);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1077,8 +1077,8 @@ MonoBehaviour:
m_InputAxisName:
m_InputAxisValue: 0
m_InvertInput: 1
m_MinValue: -90
m_MaxValue: 90
m_MinValue: -89
m_MaxValue: 89
m_Wrap: 0
m_Recentering:
m_enabled: 0
Expand Down Expand Up @@ -2153,14 +2153,14 @@ MonoBehaviour:
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
--- !u!114 &1924827267997815873
MonoBehaviour:
m_ObjectHideFlags: 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"GUID:8322ea9340a544c59ddc56d4793eac74",
"GUID:d832748739a186646b8656bdbd447ad0",
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:d8b63aba1907145bea998dd612889d6b",
"GUID:2665a8d13d1b3f18800f46e256720795",
"GUID:54d33bbd50a28174e8ba0110106203c2"
],
"includePlatforms": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
namespace DCL.CharacterMotion.Components
using UnityEngine;

namespace DCL.CharacterMotion.Components
{
public struct HeadIKComponent
{
public bool IsEnabled;
public bool YawEnabled;

public bool PitchEnabled;

public Vector3 LookAt;

public bool IsEnabled => YawEnabled || PitchEnabled;

public void SetEnabled(bool yawEnabled, bool pitchEnabled)
{
YawEnabled = yawEnabled;
PitchEnabled = pitchEnabled;
}

public readonly Vector2 GetHeadYawAndPitch()
{
if (LookAt.sqrMagnitude < 0.0001f) return Vector2.zero;
Vector3 angles = Quaternion.LookRotation(LookAt).eulerAngles;
return new Vector2(angles.y, angles.x);
}
}
}
112 changes: 80 additions & 32 deletions Explorer/Assets/DCL/Character/CharacterMotion/IK/ApplyHeadLookAt.cs
Original file line number Diff line number Diff line change
@@ -1,62 +1,110 @@
using DCL.AvatarRendering.AvatarShape.UnityInterface;
using DCL.CharacterMotion.Settings;
using System.Runtime.CompilerServices;
using Unity.Burst;
using Unity.Mathematics;
using UnityEngine;

namespace DCL.CharacterMotion.IK
{
[BurstCompile]
public static class ApplyHeadLookAt
{
private const float TWO_PI = math.PI * 2;
private const float THREE_PI = math.PI * 3;

/// <summary>
/// This method updates the head IK targets (horizontal and vertical) based on a target look-at direction
/// </summary>
/// <param name="useFrontalReset"> If the target horizonal angle is outside of the limits, reset the head location to look frontal </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Execute(Vector3 targetDirection, AvatarBase avatarBase, float dt, ICharacterControllerSettings settings, bool useFrontalReset = true)
public static void Execute(Vector3 targetDirection, bool yawEnabled, bool pitchEnabled, AvatarBase avatarBase, float dt, ICharacterControllerSettings settings, bool useFrontalReset = true)
{
Transform reference = avatarBase.HeadPositionConstraint;
Vector3 referenceAngle = Quaternion.LookRotation(reference.forward).eulerAngles;
Vector3 targetAngle = Quaternion.LookRotation(targetDirection).eulerAngles;
Execute(avatarBase.HeadPositionConstraint.forward,
targetDirection,
yawEnabled,
pitchEnabled,
settings.HeadIKRotationSpeed * dt,
avatarBase.HeadLookAtTargetHorizontal.localRotation,
avatarBase.HeadLookAtTargetVertical.localRotation,
new float2(settings.HeadIKHorizontalAngleLimit, settings.HeadIKVerticalAngleLimit),
useFrontalReset,
settings.HeadIKHorizontalAngleReset,
out var horizontalRotationResult,
out var verticalRotationResult);

float horizontalAngle = Mathf.DeltaAngle(referenceAngle.y, targetAngle.y);
avatarBase.HeadLookAtTargetHorizontal.localRotation = horizontalRotationResult;
avatarBase.HeadLookAtTargetVertical.localRotation = verticalRotationResult;
}

Quaternion horizontalTargetRotation;
Quaternion verticalTargetRotation;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Execute(Vector3 targetDirection, AvatarBase avatarBase, float dt, ICharacterControllerSettings settings, bool useFrontalReset = true)
=> Execute(targetDirection, true, true, avatarBase, dt, settings, useFrontalReset);

float rotationSpeed = settings.HeadIKRotationSpeed;
[BurstCompile]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Execute(in float3 referenceDirection,
in float3 targetDirection,
bool yawEnabled,
bool pitchEnabled,
float maxDeltaDegrees,
in quaternion lookAtTargetHorizontal,
in quaternion lookAtTargetVertical,
in float2 angleLimitsDegrees,
bool useFrontalReset,
float angleResetDegrees,
out quaternion horizontalRotationResult,
out quaternion verticalRotationResult)
{
float3 up = math.up();
float3 referenceAngle = math.Euler(quaternion.LookRotation(referenceDirection, up));
float3 targetAngle = math.Euler(quaternion.LookRotation(targetDirection, up));

//If the target horizonal angle is outside of the limits, reset the head location to look frontal
if (useFrontalReset && Mathf.Abs(horizontalAngle) > settings.HeadIKHorizontalAngleReset)
{
//set horizontal rotation to 0
horizontalTargetRotation = Quaternion.AngleAxis(0, Vector3.up);
float horizontalAngle = DeltaAngle(referenceAngle.y, targetAngle.y);

//set vertical rotation to 0
verticalTargetRotation = horizontalTargetRotation * Quaternion.AngleAxis(0, Vector3.right);
rotationSpeed = rotationSpeed / 3;
}
//otherwise, calculate rotation within constraints
quaternion horizontalTargetRotation = quaternion.identity;
quaternion verticalTargetRotation = quaternion.identity;

if (useFrontalReset && math.abs(horizontalAngle) > math.radians(angleResetDegrees))
maxDeltaDegrees *= 0.333333f;
else
{
//clamp horizontal angle and apply rotation
horizontalAngle = Mathf.Clamp(horizontalAngle, -settings.HeadIKHorizontalAngleLimit, settings.HeadIKHorizontalAngleLimit);
horizontalTargetRotation = Quaternion.AngleAxis(horizontalAngle, Vector3.up);
var angleLimitsRads = math.radians(angleLimitsDegrees);

//calculate vertical angle difference between reference and target, clamped to maximum angle
float verticalAngle = Mathf.DeltaAngle(referenceAngle.x, targetAngle.x);
verticalAngle = Mathf.Clamp(verticalAngle, -settings.HeadIKVerticalAngleLimit, settings.HeadIKVerticalAngleLimit);
if (yawEnabled)
{
horizontalAngle = math.clamp(horizontalAngle, -angleLimitsRads.x, angleLimitsRads.x);
horizontalTargetRotation = quaternion.AxisAngle(up, horizontalAngle);
}

//calculate vertical rotation
verticalTargetRotation = horizontalTargetRotation * Quaternion.AngleAxis(verticalAngle, Vector3.right);
if (pitchEnabled)
{
float verticalAngle = DeltaAngle(referenceAngle.x, targetAngle.x);
verticalAngle = math.clamp(verticalAngle, -angleLimitsRads.y, angleLimitsRads.y);
verticalTargetRotation = math.mul(horizontalTargetRotation, quaternion.AxisAngle(math.right(), verticalAngle));
}
}

//apply horizontal rotation
Quaternion newHorizontalRotation = Quaternion.RotateTowards(avatarBase.HeadLookAtTargetHorizontal.localRotation, horizontalTargetRotation, dt * rotationSpeed);
avatarBase.HeadLookAtTargetHorizontal.localRotation = newHorizontalRotation;
float maxDelta = math.radians(maxDeltaDegrees);
horizontalRotationResult = yawEnabled ? RotateTowards(lookAtTargetHorizontal, horizontalTargetRotation, maxDelta) : quaternion.identity;
verticalRotationResult = pitchEnabled ? RotateTowards(lookAtTargetVertical, verticalTargetRotation, maxDelta) : quaternion.identity;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float DeltaAngle(float referenceAngle, float targetAngle) =>
math.fmod(targetAngle - referenceAngle + THREE_PI, TWO_PI) - math.PI;

//apply vertical rotation
Quaternion newVerticalRotation = Quaternion.RotateTowards(avatarBase.HeadLookAtTargetVertical.localRotation, verticalTargetRotation, dt * rotationSpeed);
avatarBase.HeadLookAtTargetVertical.localRotation = newVerticalRotation;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static quaternion RotateTowards(quaternion current, quaternion target, float maxDelta)
{
float dot = math.dot(current, target);
if (dot < 0)
{
target.value = -target.value;
dot = -dot;
}
float angle = math.acos(math.clamp(dot, -1, 1)) * 2;
return angle < 1e-6f ? target : math.slerp(current, target, math.min(1, maxDelta / angle));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ public class CharacterControllerSettings : ScriptableObject, ICharacterControlle
[field: SerializeField] public float WallSlideDetectionDistance { get; private set; } = 0.5f;
[field: SerializeField] public float WallSlideMaxMoveSpeedMultiplier { get; private set; }
[field: SerializeField] public float StepOffset { get; set; } = 0.35f;
[field: SerializeField] public float HeadIKWeightChangeSpeed { get; private set; } = 2;


[field: SerializeField] [field: Header("Animation")] public float RotationSpeed { get; private set; } = 360f;
[field: SerializeField] public float MoveAnimBlendMaxWalkSpeed { get; private set; } = 1f;
[field: SerializeField] public float MoveAnimBlendMaxJogSpeed { get; private set; } = 3f;
Expand Down Expand Up @@ -90,7 +89,8 @@ public class CharacterControllerSettings : ScriptableObject, ICharacterControlle
[field: SerializeField] public float HeadIKHorizontalAngleLimit { get; set; } = 60;
[field: SerializeField] public float HeadIKHorizontalAngleReset { get; set; } = 70;
[field: SerializeField] public float HeadIKRotationSpeed { get; set; } = 45;

[field: SerializeField] public float HeadIKWeightChangeSpeed { get; set; } = 2;
[field: SerializeField] public float HeadIKRemotePlayersDistance { get; set; } = 10;
[field: SerializeField] [field: Header("Cheat/Debug/Misc")] public float JumpPadForce { get; private set; } = 50f;
[field: SerializeField] public float AnimationSpeed { get; private set; } = 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public interface ICharacterControllerSettings
float HeadIKHorizontalAngleLimit { get; set; }
float HeadIKHorizontalAngleReset { get; set; }
float HeadIKRotationSpeed { get; set; }
float HeadIKWeightChangeSpeed { get; set; }
float HeadIKRemotePlayersDistance { get; set; }
AnimationCurve SlopeVelocityModifier { get; }
float SlideAnimationBlendSpeed { get; }
float MinSlopeAngle { get; }
Expand All @@ -79,6 +81,5 @@ public interface ICharacterControllerSettings
float WallSlideDetectionDistance { get; }
float WallSlideMaxMoveSpeedMultiplier { get; }
float StepOffset { get; set; }
float HeadIKWeightChangeSpeed { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ public float StepOffset
set => impl.StepOffset = value;
}

public float HeadIKWeightChangeSpeed => impl.HeadIKWeightChangeSpeed;
public float HeadIKWeightChangeSpeed
{
get => impl.HeadIKWeightChangeSpeed;
set => impl.HeadIKWeightChangeSpeed = value;
}

public float HeadIKRemotePlayersDistance
{
get => impl.HeadIKRemotePlayersDistance;
set => impl.HeadIKRemotePlayersDistance = value;
}
}
}
Loading
Loading