Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -509,8 +509,9 @@ public void GetBoneRotAndPos(quaternion RootRotation, Animator anim, HumanBodyBo
/// <returns>Offset vector applied to the base position.</returns>
public float3 CalculateFallbackOffset(HumanBodyBones bone, float fallbackHeight, float3 heightPercentage)
{
Vector3 playerUp = BasisLocalPlayer.localToWorldMatrix.MultiplyVector(Vector3.up).normalized;
Vector3 height = fallbackHeight * heightPercentage;
return bone == HumanBodyBones.Hips ? math.mul(height, -Vector3.up) : math.mul(height, Vector3.up);
return bone == HumanBodyBones.Hips ? math.mul(height, -playerUp) : math.mul(height, playerUp);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class BasisLocalCharacterDriver
{
public BasisLocalPlayer LocalPlayer;
[System.NonSerialized] public BasisLocalAnimatorDriver LocalAnimatorDriver;
public CharacterController characterController;
public BasisKinematicCharacterController characterController;
public Vector3 bottomPointLocalSpace;
public Vector3 LastBottomPoint;
public bool groundedPlayer;
Expand Down Expand Up @@ -103,6 +103,23 @@ public void SetMode(Mode mode)
public Quaternion CurrentRotation;
public CollisionFlags Flags;
public float radius;
/// <summary>
/// The direction gravity pulls the character. Defaults to Vector3.down.
/// Changing this allows the character to walk on walls/ceilings.
/// </summary>
public Vector3 GravityDirection
{
get => characterController != null ? characterController.GravityDirection : Vector3.down;
set
{
if (characterController != null)
characterController.GravityDirection = value;
}
}
/// <summary>
/// The up direction for the character, opposite of GravityDirection.
/// </summary>
public Vector3 UpDirection => characterController != null ? characterController.UpDirection : Vector3.up;
public Vector2 MovementVector { get; private set; }
/// <summary>
/// A value between 0 and 1 representing the relative speed of player movement.
Expand Down Expand Up @@ -162,8 +179,10 @@ public void Initialize(BasisLocalPlayer localPlayer)
LocalPlayer = localPlayer;
BasisLocalPlayerTransform = localPlayer.transform;
LocalAnimatorDriver = localPlayer.LocalAnimatorDriver;
characterController.PlayerInitialize();
characterController.minMoveDistance = 0;
characterController.skinWidth = 0.01f;
characterController.OnKCCColliderHit = OnKCCHit;
if (!HasEvents)
{
HasEvents = true;
Expand All @@ -175,22 +194,20 @@ public void Initialize(BasisLocalPlayer localPlayer)
SetMode(Mode.Walk);
}

public void OnControllerColliderHit(ControllerColliderHit hit)
private void OnKCCHit(KCCHitInfo hit)
{
if (CanPushRigidbodys)
{
// Check if the hit object has a Rigidbody and if it is not kinematic
Rigidbody body = hit.collider.attachedRigidbody;

if (body == null || body.isKinematic)
{
return;
}

// Ensure we're only pushing objects in the horizontal plane
Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z);
// Project push direction onto the plane perpendicular to gravity (horizontal)
Vector3 pushDir = hit.moveDirection - UpDirection * Vector3.Dot(hit.moveDirection, UpDirection);

// Apply the force to the object
body.AddForce(pushDir * pushPower, ForceMode.Impulse);
}
}
Expand Down Expand Up @@ -263,7 +280,7 @@ public void SimulateMovement(float DeltaTime)

// Get the current rotation and position of the player
Vector3 pivot = BasisLocalBoneDriver.EyeControl.OutgoingWorldData.position;
Vector3 upAxis = Vector3.up;
Vector3 upAxis = UpDirection;

// Calculate direction from the pivot to the current position
Vector3 directionToPivot = CurrentPosition - pivot;
Expand All @@ -279,7 +296,7 @@ public void SimulateMovement(float DeltaTime)
BasisLocalPlayerTransform.SetPositionAndRotation(FinalRotation, rotation * CurrentRotation);

float HeightOffset = (characterController.height / 2) - characterController.radius;
bottomPointLocalSpace = FinalRotation + (characterController.center - new Vector3(0, HeightOffset, 0));
bottomPointLocalSpace = FinalRotation + (characterController.center - upAxis * HeightOffset);

Quaternion newRot = rotation * CurrentRotation;
Vector3 newPos = FinalRotation;
Expand Down Expand Up @@ -387,16 +404,20 @@ public void SetMovementVector(Vector2 movement)
}
public void HandleMovement(float DeltaTime)
{
// Cache current rotation and zero out x and z components
// Cache current rotation and flatten to the plane perpendicular to gravity
currentRotation = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation;
Vector3 rotationEulerAngles = currentRotation.eulerAngles;
rotationEulerAngles.x = 0;
rotationEulerAngles.z = 0;

Quaternion flattenedRotation = Quaternion.Euler(rotationEulerAngles);
Vector3 up = UpDirection;
Vector3 flatForward = currentRotation * Vector3.forward;
flatForward -= up * Vector3.Dot(flatForward, up);
if (flatForward.sqrMagnitude < 0.0001f)
{
flatForward = -(currentRotation * Vector3.up);
flatForward -= up * Vector3.Dot(flatForward, up);
}
Quaternion flattenedRotation = Quaternion.LookRotation(flatForward.normalized, up);

if (CrouchBlendDelta != 0) UpdateCrouchBlend(CrouchBlendDelta);
// Calculate horizontal movement direction
// Calculate horizontal movement direction (in the character's gravity-relative plane)
Vector3 horizontalMoveDirection = new Vector3(MovementVector.x, 0, MovementVector.y).normalized;

CurrentSpeed = math.lerp(MinimumMovementSpeed, MaximumMovementSpeed, MovementSpeedScale) + MinimumMovementSpeed * MovementSpeedBoost;
Expand Down Expand Up @@ -426,7 +447,8 @@ public void HandleMovement(float DeltaTime)


HasJumpAction = false;
totalMoveDirection.y = currentVerticalSpeed * DeltaTime;
// Apply vertical speed along the gravity up axis instead of world Y
totalMoveDirection += up * (currentVerticalSpeed * DeltaTime);

// Move character
Flags = characterController.Move(totalMoveDirection);
Expand Down Expand Up @@ -482,7 +504,7 @@ public void CalculateCharacterSize()
}

// Clamp stepOffset to something sane relative to height
float maxStep = (finalHeight + 2f * characterController.radius) - 0.001f;
float maxStep = (finalHeight + 2f * radius) - 0.001f;
maxStep = Mathf.Max(0f, maxStep);
maxStep = Mathf.Min(maxStep, finalHeight * 0.25f);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ public void SimulateIKDestinations(float deltaTime)
}

timeAccumulator += Mathf.Max(deltaTime, 1e-6f);
Vector3 cachedPlayerUp = localPlayer.LocalCharacterDriver.UpDirection;

BasisFullBodyData data = BasisFullIKConstraint.data;

Expand Down Expand Up @@ -375,7 +376,7 @@ public void SimulateIKDestinations(float deltaTime)
hipsRot = EuroRot[S_Hips] ? fRotHips.Filter(hipsRot, timeAccumulator) : FallbackRot(ref sRotHips, hipsRot, deltaTime);
}

hipsPos.y -= localPlayer.LocalCharacterDriver.landingCrouchEffect;
hipsPos -= cachedPlayerUp * localPlayer.LocalCharacterDriver.landingCrouchEffect;
data.PositionHips = hipsPos;
data.RotationHips = hipsRot;

Expand Down Expand Up @@ -525,9 +526,7 @@ public void SimulateIKDestinations(float deltaTime)
bool hipsHaveTracker = BasisLocalBoneDriver.HipsControl.HasTracked == BasisHasTracked.HasTracker;
if (!hipsHaveTracker)
{
data.PositionHips = new Vector3(data.PositionHips.x,
data.PositionHips.y + footDriver.ComputeHipBob() * footIKBlendWeight,
data.PositionHips.z);
data.PositionHips += cachedPlayerUp * (footDriver.ComputeHipBob() * footIKBlendWeight);
}
}

Expand Down Expand Up @@ -568,7 +567,7 @@ public void SimulateIKDestinations(float deltaTime)
}
else if (footIKBlendWeightLeft > 0.001f && footDriverReady)
{
Quaternion targetRotL = ComputeKneeHintRotation(data.PositionHips, data.LeftFootPosition, footDriver.LeftKneeHint);
Quaternion targetRotL = ComputeKneeHintRotation(data.PositionHips, data.LeftFootPosition, footDriver.LeftKneeHint, cachedPlayerUp);
float kneeRotAlpha = 1f - Mathf.Exp(-8f * deltaTime);
smoothedLeftKneeRot = Quaternion.Slerp(smoothedLeftKneeRot, targetRotL, kneeRotAlpha);
data.PositionLeftLowerLeg = footDriver.LeftKneeHint;
Expand Down Expand Up @@ -602,7 +601,7 @@ public void SimulateIKDestinations(float deltaTime)
}
else if (footIKBlendWeightRight > 0.001f && footDriverReady)
{
Quaternion targetRotR = ComputeKneeHintRotation(data.PositionHips, data.RightFootPosition, footDriver.RightKneeHint);
Quaternion targetRotR = ComputeKneeHintRotation(data.PositionHips, data.RightFootPosition, footDriver.RightKneeHint, cachedPlayerUp);
float kneeRotAlpha = 1f - Mathf.Exp(-8f * deltaTime);
smoothedRightKneeRot = Quaternion.Slerp(smoothedRightKneeRot, targetRotR, kneeRotAlpha);

Expand Down Expand Up @@ -743,6 +742,7 @@ public void SimulateIKDestinations(float deltaTime)
data.KneeBendPrefRight = (hipsRot * Vector3.right);

data.SpineBendNormal = (fwd * spineBendNormalWeights.x + outR * spineBendNormalWeights.y + up * spineBendNormalWeights.z).normalized;
data.PlayerUp = cachedPlayerUp;
// Commit & evaluate
BasisFullIKConstraint.data = data;

Expand Down Expand Up @@ -1094,7 +1094,7 @@ private static Vector3 ComputeKneeBendNormal(Vector3 hip, Vector3 foot, Vector3
/// Forward = knee→foot direction, Up = derived from the bend plane.
/// This prevents snapping that occurs with Quaternion.identity.
/// </summary>
private static Quaternion ComputeKneeHintRotation(Vector3 hip, Vector3 foot, Vector3 kneeHint)
private static Quaternion ComputeKneeHintRotation(Vector3 hip, Vector3 foot, Vector3 kneeHint, Vector3 playerUp)
{
Vector3 kneeToFoot = foot - kneeHint;
Vector3 kneeToHip = hip - kneeHint;
Expand All @@ -1110,7 +1110,7 @@ private static Quaternion ComputeKneeHintRotation(Vector3 hip, Vector3 foot, Vec
Vector3 up = Vector3.Cross(fwd, bendNormal);

if (up.sqrMagnitude < 1e-8f)
up = Vector3.up;
up = playerUp;
else
up.Normalize();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ public void OnSimulate()
float dt = Time.deltaTime;
Matrix4x4 parentMatrix = BasisLocalPlayer.localToWorldMatrix;

// OutGoingData is in local space where Y is always the character's up axis,
// regardless of world orientation. Use Vector3.up for all local-space operations.
// (World-space player up is handled by the rig driver / FBIK animation job.)
Vector3 worldUp = Vector3.up;

// =========================
// 1) HEAD & NECK (top cues)
// =========================
Expand All @@ -146,27 +151,19 @@ public void OnSimulate()
neck.OutGoingData.rotation = SmoothSlerp(neck.OutGoingData.rotation, head.OutGoingData.rotation, NeckRotationSpeed, dt);

// Positions for head/neck come from their tracker-driven targets + offsets
ApplyPositionControl(head, parentMatrix, torsoLock: false);
ApplyPositionControl(neck, parentMatrix, torsoLock: false);
ApplyPositionControl(head, parentMatrix, torsoLock: false, worldUp);
ApplyPositionControl(neck, parentMatrix, torsoLock: false, worldUp);

Vector3 neckPosWorld = neck.OutGoingData.position;

// ===========================================
// 2) HIPS: build from neck and preserved span
// ===========================================
// Determine a stable world up
Vector3 worldUp = parentMatrix.MultiplyVector(Vector3.up).normalized;
if (worldUp.sqrMagnitude < 1e-6f) worldUp = Vector3.up;

// Preserve total length neck→hips, except when overridden.
Vector3 idealHips = HipsFreezeToTpose ? hips.TposeLocalScaled.position : neckPosWorld - worldUp * _lenTotal;

// Add small forward bias using head yaw, which also applies to the hips, except when overridden.
Quaternion headYaw = HipsFreezeToTpose ? Quaternion.identity : ExtractYawRotation(head.OutGoingData.rotation);
Quaternion headYaw = HipsFreezeToTpose ? Quaternion.identity : ExtractYawRotation(head.OutGoingData.rotation, worldUp);
idealHips += (headYaw * Vector3.forward) * (HipsForwardBias * BasisHeightDriver.AvatarToDefaultRatioScaledWithAvatarScale);


// Blend XZ with tracked hips for authority retention
// Blend horizontal position with tracked hips for authority retention
Vector3 trackedHips = hips.Target.OutGoingData.position;
Vector3 blendedHips = idealHips;
if (HipsXZFollowBlend > 0f)
Expand All @@ -177,14 +174,14 @@ public void OnSimulate()

// Hips rotation follows head yaw, damped.
Quaternion hipsYawTarget = headYaw;
hips.OutGoingData.rotation = ExtractYawRotation(SmoothSlerp(hips.OutGoingData.rotation, hipsYawTarget, HipsRotationSpeed, dt));
hips.OutGoingData.rotation = ExtractYawRotation(SmoothSlerp(hips.OutGoingData.rotation, hipsYawTarget, HipsRotationSpeed, dt), worldUp);
hips.OutGoingData.position = blendedHips;
hips.ApplyWorldAndLast(parentMatrix);

// =======================================================
// 3) Fill the middle: chest & spine positions and yaws
// =======================================================
Quaternion neckYaw = ExtractYawRotation(neck.OutGoingData.rotation);
Quaternion neckYaw = ExtractYawRotation(neck.OutGoingData.rotation, worldUp);
Quaternion hipsYaw = hips.OutGoingData.rotation; // already yaw-only

Vector3 neckToHips = hips.OutGoingData.position - neck.OutGoingData.position;
Expand All @@ -193,8 +190,8 @@ public void OnSimulate()
if (distNeckToHips < 1e-5f)
{
// Guard: fall back to tracker-driven positions
ApplyPositionControl(chest, parentMatrix, torsoLock: true);
ApplyPositionControl(spine, parentMatrix, torsoLock: true);
ApplyPositionControl(chest, parentMatrix, torsoLock: true, worldUp);
ApplyPositionControl(spine, parentMatrix, torsoLock: true, worldUp);
}
else
{
Expand All @@ -211,15 +208,15 @@ public void OnSimulate()

// Smooth rotations
chest.OutGoingData.rotation = ExtractYawRotation(
SmoothSlerp(chest.OutGoingData.rotation, chestYawTarget, ChestRotationSpeed, dt)
SmoothSlerp(chest.OutGoingData.rotation, chestYawTarget, ChestRotationSpeed, dt), worldUp
);
spine.OutGoingData.rotation = ExtractYawRotation(
SmoothSlerp(spine.OutGoingData.rotation, spineYawTarget, SpineRotationSpeed, dt)
SmoothSlerp(spine.OutGoingData.rotation, spineYawTarget, SpineRotationSpeed, dt), worldUp
);

// Apply positions with offsets (torsoLock removes vertical offset)
ApplyPositionWithGivenBase(chest, parentMatrix, chestPos, torsoLock: true);
ApplyPositionWithGivenBase(spine, parentMatrix, spinePos, torsoLock: true);
// Apply positions with offsets (torsoLock removes up-axis offset)
ApplyPositionWithGivenBase(chest, parentMatrix, chestPos, torsoLock: true, worldUp);
ApplyPositionWithGivenBase(spine, parentMatrix, spinePos, torsoLock: true, worldUp);
}

// Finalize head/neck
Expand All @@ -229,18 +226,21 @@ public void OnSimulate()

/// <summary>
/// Applies tracker-driven position plus offset for a bone control,
/// optionally locking vertical to TPose baseline and yaw-only rotation.
/// optionally locking the up-axis component to TPose baseline and yaw-only rotation.
/// </summary>
private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, bool torsoLock)
private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, bool torsoLock, Vector3 up)
{
Quaternion rot = boneControl.Target.OutGoingData.rotation;
if (torsoLock) rot = ExtractYawRotation(rot);
if (torsoLock) rot = ExtractYawRotation(rot, up);

Vector3 localOffset = boneControl.ScaledOffset;
if (torsoLock) localOffset.y = 0f;

Vector3 desired = boneControl.Target.OutGoingData.position + (rot * localOffset);
if (torsoLock) desired.y = boneControl.TposeLocalScaled.position.y;
if (torsoLock)
{
desired.y = boneControl.TposeLocalScaled.position.y;
}

boneControl.OutGoingData.position = desired;
boneControl.ApplyWorldAndLast(parentMatrix);
Expand All @@ -249,16 +249,19 @@ private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 p
/// <summary>
/// Applies position using a provided world base position and the control's yaw/offset rules.
/// </summary>
private void ApplyPositionWithGivenBase(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, Vector3 basePositionWorld, bool torsoLock)
private void ApplyPositionWithGivenBase(BasisLocalBoneControl boneControl, Matrix4x4 parentMatrix, Vector3 basePositionWorld, bool torsoLock, Vector3 up)
{
Quaternion rot = boneControl.OutGoingData.rotation;
if (torsoLock) rot = ExtractYawRotation(rot);
if (torsoLock) rot = ExtractYawRotation(rot, up);

Vector3 localOffset = boneControl.ScaledOffset;
if (torsoLock) localOffset.y = 0f;

Vector3 desired = basePositionWorld + (rot * localOffset);
if (torsoLock) desired.y = boneControl.TposeLocalScaled.position.y;
if (torsoLock)
{
desired.y = boneControl.TposeLocalScaled.position.y;
}

boneControl.OutGoingData.position = desired;
boneControl.ApplyWorldAndLast(parentMatrix);
Expand All @@ -274,14 +277,14 @@ private static Quaternion SmoothSlerp(Quaternion current, Quaternion target, flo
}

/// <summary>
/// Extracts yaw-only rotation (around global up) from a full quaternion.
/// Extracts yaw-only rotation (around the given up axis) from a full quaternion.
/// </summary>
private static Quaternion ExtractYawRotation(Quaternion rotation)
private static Quaternion ExtractYawRotation(Quaternion rotation, Vector3 up)
{
Vector3 f = rotation * Vector3.forward;
f.y = 0f;
f -= up * Vector3.Dot(f, up); // project onto plane perpendicular to up
if (f.sqrMagnitude < 1e-6f) f = Vector3.forward;
f.Normalize();
return Quaternion.LookRotation(f, Vector3.up);
return Quaternion.LookRotation(f, up);
}
}
Loading
Loading