From 6f78152aa049567ccbbee209ad9186056ce1655b Mon Sep 17 00:00:00 2001 From: Sketch <109103755+SketchFoxsky@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:47:45 -0400 Subject: [PATCH 1/5] Intial Push Replaced Unity Character Controller with a Kinematic Rigidbody Character Controller --- .../Local/BasisLocalCharacterDriver.cs | 97 ++- .../BasisFlyMovementMode.cs | 16 +- .../BasisKinematicCharacterController.cs | 642 ++++++++++++++++++ .../BasisKinematicCharacterController.cs.meta | 2 + .../BasisNoClipMovementMode.cs | 46 +- .../BasisWalkMovementMode.cs | 20 +- .../Character Controller/IMovementMode.cs | 2 +- .../Prefabs/Players/LocalPlayer.prefab | 299 ++++---- 8 files changed, 911 insertions(+), 213 deletions(-) create mode 100644 Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs create mode 100644 Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs index a3cfade514..c92153f96f 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs @@ -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; @@ -104,6 +104,23 @@ public void SetMode(Mode mode) public Quaternion CurrentRotation; public CollisionFlags Flags; public float radius; + /// + /// The direction gravity pulls the character. Defaults to Vector3.down. + /// Changing this allows the character to walk on walls/ceilings. + /// + public Vector3 GravityDirection + { + get => characterController != null ? characterController.GravityDirection : Vector3.down; + set + { + if (characterController != null) + characterController.GravityDirection = value; + } + } + /// + /// The up direction for the character, opposite of GravityDirection. + /// + public Vector3 UpDirection => characterController != null ? characterController.UpDirection : Vector3.up; public Vector2 MovementVector { get; private set; } /// /// A value between 0 and 1 representing the relative speed of player movement. @@ -158,13 +175,56 @@ public void DeInitalize() HasEvents = false; } } + /// + /// Ensures the KCC component exists on the player GameObject. + /// Migrates from Unity's CharacterController if one is still present on the prefab. + /// + private void EnsureKCC() + { + if (characterController != null) return; + + GameObject go = BasisLocalPlayerTransform.gameObject; + + // Check if already has KCC + characterController = go.GetComponent(); + if (characterController != null) return; + + // Migrate from legacy CharacterController if present + CharacterController legacy = go.GetComponent(); + float oldHeight = 2f; + float oldRadius = 0.3f; + Vector3 oldCenter = new Vector3(0f, 1f, 0f); + float oldSkinWidth = 0.01f; + float oldStepOffset = 0.3f; + float oldSlopeLimit = 45f; + if (legacy != null) + { + oldHeight = legacy.height; + oldRadius = legacy.radius; + oldCenter = legacy.center; + oldSkinWidth = legacy.skinWidth; + oldStepOffset = legacy.stepOffset; + oldSlopeLimit = legacy.slopeLimit; + UnityEngine.Object.DestroyImmediate(legacy); + } + + characterController = go.AddComponent(); + characterController.height = oldHeight; + characterController.radius = oldRadius; + characterController.center = oldCenter; + characterController.skinWidth = oldSkinWidth; + characterController.stepOffset = oldStepOffset; + characterController.slopeLimit = oldSlopeLimit; + } public void Initialize(BasisLocalPlayer localPlayer) { LocalPlayer = localPlayer; BasisLocalPlayerTransform = localPlayer.transform; LocalAnimatorDriver = localPlayer.LocalAnimatorDriver; + EnsureKCC(); characterController.minMoveDistance = 0; characterController.skinWidth = 0.01f; + characterController.OnKCCColliderHit = OnKCCHit; if (!HasEvents) { HasEvents = true; @@ -176,11 +236,10 @@ 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) @@ -188,10 +247,9 @@ public void OnControllerColliderHit(ControllerColliderHit hit) 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); } } @@ -264,7 +322,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; @@ -280,7 +338,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; @@ -388,16 +446,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; @@ -427,7 +489,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); @@ -479,7 +542,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); diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs index f760ac0989..1ea7798ccb 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisFlyMovementMode.cs @@ -14,7 +14,7 @@ public void Enter(BasisLocalCharacterDriver ctx) { if (ctx.characterController != null) { - ctx.characterController.detectCollisions = true; // solid, but no gravity + ctx.characterController.detectCollisions = true; ctx.characterController.enabled = true; } ctx.currentVerticalSpeed = 0f; @@ -24,16 +24,18 @@ public void Exit(BasisLocalCharacterDriver ctx) { } public void Tick(BasisLocalCharacterDriver ctx, float dt) { - // Project head forward onto horizontal plane (avoids gimbal lock near ±90° pitch) + Vector3 up = ctx.UpDirection; + + // Project head forward onto the plane perpendicular to gravity Quaternion headRot = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation; Vector3 flatForward = headRot * Vector3.forward; - flatForward.y = 0f; + flatForward -= up * Vector3.Dot(flatForward, up); if (flatForward.sqrMagnitude < 0.0001f) { flatForward = -(headRot * Vector3.up); - flatForward.y = 0f; + flatForward -= up * Vector3.Dot(flatForward, up); } - Quaternion facing = Quaternion.LookRotation(flatForward.normalized, Vector3.up); + Quaternion facing = Quaternion.LookRotation(flatForward.normalized, up); // Planar Vector3 planar = new Vector3(ctx.MovementVector.x, 0, ctx.MovementVector.y).normalized; @@ -44,8 +46,8 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt) Vector3 move = facing * planar * ctx.CurrentSpeed * dt; - // ===== Vertical input (held) ===== - move.y = ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt; + // Vertical input along gravity-relative up axis + move += up * (ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt); // Clear tap ctx.HasJumpAction = false; diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs new file mode 100644 index 0000000000..7b93998063 --- /dev/null +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs @@ -0,0 +1,642 @@ +using UnityEngine; + +namespace Basis.Scripts.BasisCharacterController +{ + /// + /// A kinematic character controller that replaces Unity's built-in CharacterController. + /// Uses a CapsuleCollider + kinematic Rigidbody so that the player can be rotated + /// freely, enabling custom gravity directions (not Y-axis locked). + /// + /// TODO + /// Add Rotate to Gravity + /// Add Flight/Noclip exposure for worlds + /// MAYBE add native swimming! + /// + /// + [RequireComponent(typeof(Rigidbody))] + [RequireComponent(typeof(CapsuleCollider))] + public class BasisKinematicCharacterController : MonoBehaviour + { + // Capsule Collider + [SerializeField] private float _height = 2f; + [SerializeField] private float _radius = 0.3f; + [SerializeField] private Vector3 _center = new Vector3(0f, 1f, 0f); + + // CharacterController Parameters + [SerializeField] private float _skinWidth = 0.01f; + [SerializeField] private float _stepOffset = 0.3f; + [SerializeField] private float _minMoveDistance = 0f; + [SerializeField] private float _slopeLimit = 45f; + [SerializeField] private bool _detectCollisions = true; + + //Gravity + [SerializeField] private Vector3 _gravityDirection = Vector3.down; + + // Runtime Info + private Rigidbody _rigidbody; + private CapsuleCollider _capsule; + private bool _isGrounded; + private Vector3 _groundNormal = Vector3.up; + private CollisionFlags _lastFlags; + + // Collision + private const int MaxHits = 16; + private const int MaxOverlaps = 16; + private const int MaxDepenetrationIterations = 4; + private const int MaxMoveIterations = 4; + private readonly RaycastHit[] _hitBuffer = new RaycastHit[MaxHits]; + private readonly Collider[] _overlapBuffer = new Collider[MaxOverlaps]; + private const float GroundProbeExtra = 0.04f; + public delegate void KCCColliderHit(KCCHitInfo hit); + public KCCColliderHit OnKCCColliderHit; + + //Public Get Set + + public float height + { + get => _height; + set + { + _height = Mathf.Max(value, 0.001f); + SyncCapsule(); + } + } + + public float radius + { + get => _radius; + set + { + _radius = Mathf.Max(value, 0.001f); + SyncCapsule(); + } + } + + public Vector3 center + { + get => _center; + set + { + _center = value; + SyncCapsule(); + } + } + + public float skinWidth + { + get => _skinWidth; + set => _skinWidth = Mathf.Max(value, 0.001f); + } + + public float stepOffset + { + get => _stepOffset; + set => _stepOffset = Mathf.Max(value, 0f); + } + + public float minMoveDistance + { + get => _minMoveDistance; + set => _minMoveDistance = Mathf.Max(value, 0f); + } + + public float slopeLimit + { + get => _slopeLimit; + set => _slopeLimit = Mathf.Clamp(value, 0f, 90f); + } + + public bool detectCollisions + { + get => _detectCollisions; + set + { + _detectCollisions = value; + if (_capsule != null) + _capsule.enabled = value; + } + } + + public bool isGrounded => _isGrounded; + + /// + /// The ground surface normal from the last Move() call. Only valid when isGrounded is true. + /// + public Vector3 groundNormal => _groundNormal; + + /// + /// The direction gravity pulls the character. Defaults to Vector3.down. + /// Setting this allows the character to walk on walls/ceilings. + /// Must be normalized. + /// + public Vector3 GravityDirection + { + get => _gravityDirection; + set => _gravityDirection = value.normalized; + } + + /// + /// The "up" direction for this character, opposite of gravity. + /// Guaranteed to be normalized; falls back to Vector3.up if gravity direction is zero. + /// + public Vector3 UpDirection + { + get + { + Vector3 up = -_gravityDirection; + if (up.sqrMagnitude < 0.0001f) up = Vector3.up; + return up.normalized; + } + } + + #region UNITY LIFECYCLE + + private void Awake() + { + // Ensure gravity direction is valid (may be zero if deserialized from a fresh component) + if (_gravityDirection.sqrMagnitude < 0.0001f) + _gravityDirection = Vector3.down; + + _rigidbody = GetComponent(); + _rigidbody.isKinematic = true; + _rigidbody.useGravity = false; + _rigidbody.interpolation = RigidbodyInterpolation.None; + _rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete; + _rigidbody.constraints = RigidbodyConstraints.FreezeAll; + + _capsule = GetComponent(); + SyncCapsule(); + } + + private void SyncCapsule() + { + if (_capsule == null) return; + _capsule.direction = 1; // Y-axis + _capsule.center = _center; + _capsule.radius = _radius; + _capsule.height = _height; + _capsule.isTrigger = false; + } + + // ── Core Move method ──────────────────────────────────────────── + + /// + /// Moves the character by with full collision + /// resolution. Returns CollisionFlags indicating which sides were hit. + /// Gravity/jump velocity should already be included in motion. + /// + public CollisionFlags Move(Vector3 motion) + { + _lastFlags = CollisionFlags.None; + + if (!enabled || !_detectCollisions) + { + transform.position += motion; + _isGrounded = false; + return _lastFlags; + } + + if (motion.sqrMagnitude < _minMoveDistance * _minMoveDistance) + { + GroundProbe(); + return _lastFlags; + } + + Vector3 up = UpDirection; + float cosSlope = Mathf.Cos(_slopeLimit * Mathf.Deg2Rad); + float verticalComponent = Vector3.Dot(motion, up); + Vector3 verticalMotion = up * verticalComponent; + Vector3 horizontalMotion = motion - verticalMotion; + bool movingDown = verticalComponent < 0f; + + // ── Grounded behaviour: slope projection + ground snap ────── + if (_isGrounded && movingDown) + { + float groundDot = Vector3.Dot(_groundNormal, up); + bool walkableSlope = groundDot >= cosSlope; + + if (walkableSlope) + { + if (horizontalMotion.sqrMagnitude > 0.00001f) + { + horizontalMotion = Vector3.ProjectOnPlane(horizontalMotion, _groundNormal); + float origLen = (motion - verticalMotion).magnitude; + float projLen = horizontalMotion.magnitude; + if (projLen > 0.00001f) + horizontalMotion = horizontalMotion * (origLen / projLen); + } + verticalMotion = Vector3.zero; + } + } + + Vector3 pos = transform.position; + + // ── Horizontal movement with step-up fallback ─────────────── + if (horizontalMotion.sqrMagnitude > 0.00001f) + { + Vector3 beforeSlide = pos; + pos = SimpleMove(pos, horizontalMotion, ref _lastFlags, up, cosSlope, isHorizontal: true); + + // If grounded and slide made little horizontal progress, try stepping up + if (_isGrounded && _stepOffset > 0f && movingDown) + { + Vector3 traveled = pos - beforeSlide; + Vector3 horizontalTraveled = traveled - up * Vector3.Dot(traveled, up); + Vector3 horizontalWanted = horizontalMotion - up * Vector3.Dot(horizontalMotion, up); + float wantedLen = horizontalWanted.magnitude; + + if (wantedLen > 0.001f && horizontalTraveled.magnitude < wantedLen * 0.5f) + { + // Blocked — try step-up from the pre-slide position + Vector3 stepPos = beforeSlide; + if (TryStepUp(ref stepPos, horizontalMotion, up, cosSlope)) + { + pos = stepPos; + } + } + } + } + + // vertical movement + if (verticalMotion.sqrMagnitude > 0.00001f) + { + pos = SimpleMove(pos, verticalMotion, ref _lastFlags, up, cosSlope, isHorizontal: false); + } + + transform.position = pos; + + // snap to ground + if (_isGrounded && verticalComponent <= 0f) + { + pos = GroundSnap(pos, up, cosSlope); + transform.position = pos; + } + + // Depenetration if clipping with a collider + pos = Depenetrate(pos); + transform.position = pos; + + // Ground probe + GroundProbe(); + + return _lastFlags; + } + + private Vector3 SimpleMove(Vector3 position, Vector3 motion, ref CollisionFlags flags, Vector3 up, float cosSlope, bool isHorizontal) + { + if (motion.sqrMagnitude < 0.00001f) return position; + + Vector3 remaining = motion; + for (int i = 0; i < MaxMoveIterations && remaining.sqrMagnitude > 0.00001f; i++) + { + float dist = remaining.magnitude; + Vector3 dir = remaining / dist; + + GetCapsuleEnds(position, out Vector3 p1, out Vector3 p2); + float castRadius = _radius - _skinWidth; + if (castRadius < 0.001f) castRadius = 0.001f; + + int hitCount = Physics.CapsuleCastNonAlloc( + p1, p2, castRadius, + dir, _hitBuffer, + dist + _skinWidth, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + + if (!FindClosestHit(hitCount, out RaycastHit closestHit)) + { + position += remaining; + break; + } + + // Move up to the hit point (minus skin) + float safeDistance = Mathf.Max(closestHit.distance - _skinWidth, 0f); + position += dir * safeDistance; + + // Classify collision + Vector3 hitNormal = closestHit.normal; + float dotUp = Vector3.Dot(hitNormal, up); + + if (dotUp > 0.7f) + flags |= CollisionFlags.Below; + else if (dotUp < -0.7f) + flags |= CollisionFlags.Above; + else + flags |= CollisionFlags.Sides; + + FireHitCallback(closestHit, dir, dist); + + // move along the surface + remaining -= dir * safeDistance; + remaining = Vector3.ProjectOnPlane(remaining, hitNormal); + + // For horizontal movement on steep slopes, prevent climbing + if (isHorizontal && dotUp > 0f && dotUp < cosSlope) + { + float upComponent = Vector3.Dot(remaining, up); + if (upComponent > 0f) + remaining -= up * upComponent; + } + } + + return position; + } + + // Step Offset + + // kill me please this took a while to debug, had to look at old braxy tutorials :) + + private bool TryStepUp(ref Vector3 pos, Vector3 horizontalMotion, Vector3 up, float cosSlope) + { + float castRadius = _radius - _skinWidth; + if (castRadius < 0.001f) castRadius = 0.001f; + + Vector3 hDir = horizontalMotion.normalized; + // Use a generous forward distance so we clear the step edge. + // At minimum cast the frame motion, but ensure we cast at least + // radius + skin so the capsule actually clears the step lip. + float hDist = Mathf.Max(horizontalMotion.magnitude, _radius + _skinWidth); + + // Phase 1: Cast UP to find ceiling clearance + float maxUpDist = _stepOffset; + GetCapsuleEnds(pos, out Vector3 up1, out Vector3 up2); + int hitCount = Physics.CapsuleCastNonAlloc( + up1, up2, castRadius, + up, _hitBuffer, + maxUpDist + _skinWidth, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + if (FindClosestHit(hitCount, out RaycastHit ceilingHit)) + { + maxUpDist = Mathf.Max(ceilingHit.distance - _skinWidth, 0f); + } + if (maxUpDist < 0.01f) + return false; // No room to step up + + Vector3 elevated = pos + up * maxUpDist; + + // Phase 2: Cast FORWARD at elevated height + GetCapsuleEnds(elevated, out Vector3 ep1, out Vector3 ep2); + hitCount = Physics.CapsuleCastNonAlloc( + ep1, ep2, castRadius, + hDir, _hitBuffer, + hDist + _skinWidth, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + + float forwardDist = hDist; + if (FindClosestHit(hitCount, out RaycastHit forwardHit)) + { + forwardDist = Mathf.Max(forwardHit.distance - _skinWidth, 0f); + } + // Need at least a tiny forward movement to land on top of the step + if (forwardDist < _skinWidth) + return false; + + Vector3 forwarded = elevated + hDir * Mathf.Min(forwardDist, hDist); + + // Phase 3: Cast DOWN to find the step surface + GetCapsuleEnds(forwarded, out Vector3 dp1, out Vector3 dp2); + float downDist = maxUpDist + GroundProbeExtra; + hitCount = Physics.CapsuleCastNonAlloc( + dp1, dp2, castRadius, + -up, _hitBuffer, + downDist, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + + if (!FindClosestHit(hitCount, out RaycastHit stepHit)) + return false; // No ground found after stepping + + // Verify the surface is walkable + float dotUp = Vector3.Dot(stepHit.normal, up); + if (dotUp < cosSlope) + return false; + + // Snap down to the step surface + float snapDown = Mathf.Max(stepHit.distance - _skinWidth, 0f); + Vector3 finalPos = forwarded - up * snapDown; + + // Must have actually gained height + float heightGain = Vector3.Dot(finalPos - pos, up); + if (heightGain < 0.001f) + return false; + + pos = finalPos; + _lastFlags |= CollisionFlags.Below; + return true; + } + + //Ground Snapping + + // Add ground Friction, its almost 1am im not doing that tonight + + /// + /// When grounded and not jumping, cast downward to anchor the character + /// to the ground surface. Prevents floating over small bumps and slopes. + /// + private Vector3 GroundSnap(Vector3 position, Vector3 up, float cosSlope) + { + GetCapsuleEnds(position, out Vector3 p1, out Vector3 p2); + float castRadius = _radius - _skinWidth; + if (castRadius < 0.001f) castRadius = 0.001f; + + // Snap distance: enough to cover step offset + skin + small gap + float snapDist = _stepOffset + _skinWidth + GroundProbeExtra; + int hitCount = Physics.CapsuleCastNonAlloc( + p1, p2, castRadius, + -up, _hitBuffer, + snapDist, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + + if (FindClosestHit(hitCount, out RaycastHit snapHit)) + { + float dotUp = Vector3.Dot(snapHit.normal, up); + if (dotUp >= cosSlope) + { + float drop = Mathf.Max(snapHit.distance - _skinWidth, 0f); + if (drop > 0.0001f) + { + position -= up * drop; + _lastFlags |= CollisionFlags.Below; + } + } + } + + return position; + } + + // Depenetration Helper + + private Vector3 Depenetrate(Vector3 position) + { + for (int iter = 0; iter < MaxDepenetrationIterations; iter++) + { + GetCapsuleEnds(position, out Vector3 p1, out Vector3 p2); + + int overlapCount = Physics.OverlapCapsuleNonAlloc( + p1, p2, _radius, + _overlapBuffer, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + + bool resolved = true; + for (int i = 0; i < overlapCount; i++) + { + Collider other = _overlapBuffer[i]; + if (other == _capsule) continue; + + if (Physics.ComputePenetration( + _capsule, position, transform.rotation, + other, other.transform.position, other.transform.rotation, + out Vector3 dir, out float dist)) + { + position += dir * (dist + 0.001f); + resolved = false; + } + } + + if (resolved) break; + } + + return position; + } + + // Ground Detection + + private void GroundProbe() + { + if (!_detectCollisions || !enabled) + { + _isGrounded = false; + _groundNormal = UpDirection; + return; + } + + Vector3 up = UpDirection; + Vector3 pos = transform.position; + + // Cast a small sphere downward from the bottom of the capsule + Vector3 worldCenter = pos + transform.rotation * _center; + float halfHeight = (_height * 0.5f) - _radius; + Vector3 bottom = worldCenter - up * halfHeight; + + float castRadius = _radius - _skinWidth; + if (castRadius < 0.001f) castRadius = 0.001f; + + float probeOffset = _skinWidth + 0.01f; + int hitCount = Physics.SphereCastNonAlloc( + bottom + up * probeOffset, + castRadius, + -up, + _hitBuffer, + probeOffset + GroundProbeExtra, + GetCollisionMask(), + QueryTriggerInteraction.Ignore + ); + + _isGrounded = false; + _groundNormal = up; + float cosSlope = Mathf.Cos(_slopeLimit * Mathf.Deg2Rad); + float closestDist = float.MaxValue; + + for (int i = 0; i < hitCount; i++) + { + if (_hitBuffer[i].collider == _capsule) continue; + float dotUp = Vector3.Dot(_hitBuffer[i].normal, up); + if (dotUp >= cosSlope && _hitBuffer[i].distance < closestDist) + { + closestDist = _hitBuffer[i].distance; + _isGrounded = true; + _groundNormal = _hitBuffer[i].normal; + } + } + } + #endregion + + #region HELPERS + + private void GetCapsuleEnds(Vector3 position, out Vector3 point1, out Vector3 point2) + { + Quaternion rot = transform.rotation; + Vector3 worldCenter = position + rot * _center; + Vector3 capsuleUp = rot * Vector3.up; + float halfHeight = (_height * 0.5f) - _radius; + if (halfHeight < 0f) halfHeight = 0f; + point1 = worldCenter + capsuleUp * halfHeight; + point2 = worldCenter - capsuleUp * halfHeight; + } + + private int GetCollisionMask() + { + int layer = gameObject.layer; + int mask = 0; + for (int i = 0; i < 32; i++) + { + if (!Physics.GetIgnoreLayerCollision(layer, i)) + mask |= (1 << i); + } + return mask; + } + + private bool FindClosestHit(int hitCount, out RaycastHit closest) + { + float closestDist = float.MaxValue; + closest = default; + bool found = false; + for (int i = 0; i < hitCount; i++) + { + if (_hitBuffer[i].collider == _capsule) continue; + if (_hitBuffer[i].distance < closestDist) + { + closestDist = _hitBuffer[i].distance; + closest = _hitBuffer[i]; + found = true; + } + } + return found; + } + + private bool HasValidHit(int hitCount) + { + for (int i = 0; i < hitCount; i++) + { + if (_hitBuffer[i].collider != _capsule) return true; + } + return false; + } + + private void FireHitCallback(RaycastHit hit, Vector3 moveDir, float moveDist) + { + if (OnKCCColliderHit == null) return; + KCCHitInfo info; + info.collider = hit.collider; + info.point = hit.point; + info.normal = hit.normal; + info.moveDirection = moveDir; + info.moveLength = moveDist; + OnKCCColliderHit(info); + } + } + + #endregion + + /// + /// Hit information passed to the KCC collision callback, mirroring ControllerColliderHit. + /// + public struct KCCHitInfo + { + public Collider collider; + public Vector3 point; + public Vector3 normal; + public Vector3 moveDirection; + public float moveLength; + } +} diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta new file mode 100644 index 0000000000..feb57957d0 --- /dev/null +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3f50f4d9a7b7b5c488ef048f8e073021 \ No newline at end of file diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs index 5b42d041a3..0385c9cb25 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisNoClipMovementMode.cs @@ -17,14 +17,14 @@ public class BasisNoClipMovementMode : IMovementMode public void Enter(BasisLocalCharacterDriver ctx) { - // Fully ghost the CharacterController so it won’t push/depenetrate + // Disable the KCC's collision detection so it won't push/depenetrate if (ctx.characterController != null) { ctx.characterController.detectCollisions = false; ctx.characterController.enabled = false; } - // Ensure a trigger-only probe exists and matches the CC size + // Ensure a trigger-only probe exists and matches the KCC size EnsureTriggerProbe(ctx); // enable the trigger probe @@ -38,7 +38,7 @@ public void Enter(BasisLocalCharacterDriver ctx) public void Exit(BasisLocalCharacterDriver ctx) { - // Re-enable CC for other modes + // Re-enable KCC for other modes if (ctx.characterController != null) { ctx.characterController.detectCollisions = true; @@ -52,16 +52,18 @@ public void Exit(BasisLocalCharacterDriver ctx) public void Tick(BasisLocalCharacterDriver ctx, float dt) { - // Project head forward onto horizontal plane (avoids gimbal lock near ±90° pitch) + Vector3 up = ctx.UpDirection; + + // Project head forward onto the plane perpendicular to gravity Quaternion headRot = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation; Vector3 flatForward = headRot * Vector3.forward; - flatForward.y = 0f; + flatForward -= up * Vector3.Dot(flatForward, up); if (flatForward.sqrMagnitude < 0.0001f) { flatForward = -(headRot * Vector3.up); - flatForward.y = 0f; + flatForward -= up * Vector3.Dot(flatForward, up); } - Quaternion facing = Quaternion.LookRotation(flatForward.normalized, Vector3.up); + Quaternion facing = Quaternion.LookRotation(flatForward.normalized, up); // Same speed model you already use ctx.CurrentSpeed = @@ -72,8 +74,8 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt) Vector3 planar = new Vector3(ctx.MovementVector.x, 0f, ctx.MovementVector.y).normalized; Vector3 move = facing * planar * ctx.CurrentSpeed * dt; - // Vertical input - move.y = ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt; + // Vertical input along gravity-relative up axis + move += up * (ctx.GetVerticalMovement() * ctx.CurrentSpeed * dt); ctx.HasJumpAction = false; if (ctx.MovementLock) move = Vector3.zero; @@ -87,13 +89,13 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt) _triggerBody.position = ctx.BasisLocalPlayerTransform.position; _triggerBody.rotation = ctx.BasisLocalPlayerTransform.rotation; } - var cc = ctx.characterController; - if (cc != null && _triggerCapsule != null) + var kcc = ctx.characterController; + if (kcc != null && _triggerCapsule != null) { - _triggerCapsule.center = cc.center; - _triggerCapsule.radius = cc.radius; - _triggerCapsule.height = cc.height; - _triggerCapsule.direction = 1; // Y axis like CharacterController + _triggerCapsule.center = kcc.center; + _triggerCapsule.radius = kcc.radius; + _triggerCapsule.height = kcc.height; + _triggerCapsule.direction = 1; // Y axis } // Sync state ctx.BasisLocalPlayerTransform.GetPositionAndRotation(out ctx.CurrentPosition, out ctx.CurrentRotation); @@ -114,14 +116,14 @@ private void EnsureTriggerProbe(BasisLocalCharacterDriver ctx) _triggerCapsule = BasisHelpers.GetOrAddComponent(go); _triggerCapsule.isTrigger = true; - // Match CC dimensions so overlaps are accurate - var cc = ctx.characterController; - if (cc != null) + // Match KCC dimensions so overlaps are accurate + var kcc = ctx.characterController; + if (kcc != null) { - _triggerCapsule.center = cc.center; - _triggerCapsule.radius = cc.radius; - _triggerCapsule.height = cc.height; - _triggerCapsule.direction = 1; // Y axis like CharacterController + _triggerCapsule.center = kcc.center; + _triggerCapsule.radius = kcc.radius; + _triggerCapsule.height = kcc.height; + _triggerCapsule.direction = 1; // Y axis } // Make sure physics queries will consider triggers (usually true by default) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs index 23a1bffa37..f5cccb644f 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisWalkMovementMode.cs @@ -27,16 +27,18 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt) ctx.UpdateCrouchBlend(ctx.CrouchBlendDelta); } - // Project head forward onto horizontal plane (avoids gimbal lock near ±90° pitch) + Vector3 up = ctx.UpDirection; + + // Project head forward onto the plane perpendicular to gravity (avoids gimbal lock near ±90° pitch) Quaternion headRot = BasisLocalBoneDriver.HeadControl.OutgoingWorldData.rotation; Vector3 flatForward = headRot * Vector3.forward; - flatForward.y = 0f; + flatForward -= up * Vector3.Dot(flatForward, up); if (flatForward.sqrMagnitude < 0.0001f) { flatForward = -(headRot * Vector3.up); - flatForward.y = 0f; + flatForward -= up * Vector3.Dot(flatForward, up); } - Quaternion facing = Quaternion.LookRotation(flatForward.normalized, Vector3.up); + Quaternion facing = Quaternion.LookRotation(flatForward.normalized, up); Vector3 inputDir = new Vector3(ctx.MovementVector.x, 0, ctx.MovementVector.y).normalized; @@ -48,7 +50,6 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt) // Ground & gravity ctx.GroundCheck(dt); - if (ctx.CanJump && ctx.HasJumpAction && !ctx.MovementLock) { ctx.currentVerticalSpeed = Mathf.Sqrt(ctx.jumpHeight * -2f * ctx.gravityValue); @@ -61,14 +62,17 @@ public void Tick(BasisLocalCharacterDriver ctx, float dt) } ctx.currentVerticalSpeed = Mathf.Max(ctx.currentVerticalSpeed, -Mathf.Abs(ctx.gravityValue)); + ctx.HasJumpAction = false; - move.y = ctx.currentVerticalSpeed * dt; + // Apply vertical speed along gravity-relative up axis instead of world Y + move += up * (ctx.currentVerticalSpeed * dt); if (ctx.MovementLock) { - move.x = 0; - move.z = 0; + // Zero out horizontal component but keep vertical (gravity) + Vector3 verticalPart = up * Vector3.Dot(move, up); + move = verticalPart; } ctx.Flags = ctx.characterController.Move(move); diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs index 1f8e3b2f00..f320c2d4ed 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/IMovementMode.cs @@ -18,7 +18,7 @@ public interface IMovementMode void Exit(BasisLocalCharacterDriver ctx); // Per-frame simulation of displacement and vertical speed - // Should call CharacterController.Move when Collision==Solid, or set transform directly when Ghost + // Should call BasisKinematicCharacterController.Move when Collision==Solid, or set transform directly when Ghost void Tick(BasisLocalCharacterDriver ctx, float deltaTime); } } diff --git a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab index 1966def44c..019825774e 100644 --- a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab +++ b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab @@ -327,7 +327,7 @@ MonoBehaviour: SpriteMicrophoneOff: {fileID: 21300000, guid: f184b79ab72cc224e94720390fb038c2, type: 3} VRdesiredNormXY: {x: -0.42, y: -0.52} VRextraViewportPad: 0.022 - iconHalfRU: {x: 0, y: 0} + IconPositionOffset: {x: 0, y: 0} LocalIsTransmitting: 0 UnMutedMutedIconColorActive: {r: 1, g: 1, b: 1, a: 0.67058825} UnMutedMutedIconColorInactive: {r: 0.5, g: 0.5, b: 0.5, a: 0.67058825} @@ -484,7 +484,9 @@ GameObject: m_Component: - component: {fileID: 303201899784742928} - component: {fileID: 4587526044252889482} - - component: {fileID: 5548795088377030369} + - component: {fileID: 8369693708823570861} + - component: {fileID: 3011869999757657} + - component: {fileID: 7825922350359905173} m_Layer: 3 m_Name: LocalPlayer m_TagString: Untagged @@ -520,11 +522,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 19f412872e0df964494751723e9f3838, type: 3} m_Name: m_EditorClassIdentifier: + PlayerPlatform: DisplayName: UUID: SafeDisplayName: BasisAvatar: {fileID: 0} AvatarTransform: {fileID: 0} + AvatarAnimatorTransform: {fileID: 0} PlayerSelf: {fileID: 0} FaceIsVisible: 0 FaceRenderer: {fileID: 0} @@ -541,9 +545,18 @@ MonoBehaviour: BasisBundleDescription: AssetBundleName: AssetBundleDescription: + AssetBundleIcon: {fileID: 0} BasisBundleGenerated: [] ImageBase64: DateOfCreation: + Bounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extents: {x: 0, y: 0, z: 0} + MetaData: + TrianglesCount: 0 + MaterialCount: 0 + BonesCount: 0 + ComponentNames: [] LocalCameraDriver: {fileID: 22857711927985140} LocalBoneDriver: ControlsLength: 0 @@ -566,17 +579,69 @@ MonoBehaviour: m_Active: 0 BasisFullIKConstraint: {fileID: 0} timeAccumulator: 0 - OutputkneeBendPrefLeftWeights: {x: 0, y: 0, z: 0} - OutputkneeBendPrefRightWeights: {x: 0, y: 0, z: 0} - kneeBendPrefLeftWeights: {x: 0, y: 1, z: -0.2} - kneeBendPrefRightWeights: {x: 0, y: 1, z: 0.2} elbowBendPrefLeftWeights: {x: 0, y: 1, z: 0} elbowBendPrefRightWeights: {x: 0, y: 1, z: 0} spineBendNormalWeights: {x: 0, y: 1, z: 0} - ScrossLeggedModifier: {x: 1.25, y: 0.25} + BasisLocalFootDriver: + predictionFactor: 0.9 + velocityBiasFactor: 0.18 + leadOffsetFactor: 0.6 + maxVelocityOffsetFraction: 0.35 + maxPredictionFraction: 0.35 + plantedLerpSpeed: 40 + rotationLerpSpeed: 16 + velocitySmoothAccel: 10 + velocitySmoothDecel: 50 + bodyFwdRateMoving: 6 + bodyFwdRateStationary: 2.5 + kneeHintLerpSpeed: 10 + maxFootTiltDegrees: 35 + maxFootYawDegrees: 18 + raySphereRadiusMul: 0.3 + footHeightOffsetMul: 0.2 + stepTriggerMul: 0.08 + strideScaleMul: 0.06 + stepHeightMul: 0.18 + stepDurSlowMul: 0.3 + stepDurFastMul: 0.18 + fastSpeedMul: 1.2 + stepArcLiftExp: 0.6 + stepArcDropExp: 1.4 + stepHeightMinFraction: 0.4 + idleSpeedThreshold: 0.05 + idleBoostFraction: 0.5 + maxPlantedYawDegrees: 35 + idealSideEnforceFraction: 0.3 + stepTargetSideFraction: 0.15 + footSideEnforceFraction: 0.2 + maxVerticalDriftFraction: 0.25 + kneeForwardPushFraction: 0.4 + kneeMinSideFraction: 0.05 + bodyFwdHipsWeight: 3 + bodyFwdChestWeight: 2 + bodyFwdHeadWeight: 1 + hipBobFraction: 0.02 + stanceWidth: 0 + hipToFoot: 0 + leftLegLen: 0 + rightLegLen: 0 + leftThighLen: 0 + leftShinLen: 0 + rightThighLen: 0 + rightShinLen: 0 + footLength: 0 + ankleHeight: 0 + stepTriggerDist: 0 + strideScale: 0 + stepHeightCalc: 0 + stepDurSlow: 0 + stepDurFast: 0 + raySphereRadius: 0 + footHeightOffset: 0 + fastSpeedRef: 0 LocalCharacterDriver: LocalPlayer: {fileID: 4587526044252889482} - characterController: {fileID: 5548795088377030369} + characterController: {fileID: 0} bottomPointLocalSpace: {x: 0, y: 0, z: 0} LastBottomPoint: {x: 0, y: 0, z: 0} groundedPlayer: 0 @@ -590,9 +655,16 @@ MonoBehaviour: LastWasGrounded: 1 IsFalling: 0 IsJumpHeld: 0 + IsDescendHeld: 0 HasJumpAction: 0 jumpHeight: 1 currentVerticalSpeed: 0 + landingDescentSpeed: 15 + landingRecoverySpeed: 6 + landingImpactScale: 0.06 + maxLandingCrouchEffect: 0.35 + coyoteTimeDuration: 0.15 + fallingGracePeriod: 0.1 Rotation: {x: 0, y: 0} RotationSpeed: 200 HasEvents: 0 @@ -600,6 +672,7 @@ MonoBehaviour: CurrentPosition: {x: 0, y: 0, z: 0} CurrentRotation: {x: 0, y: 0, z: 0, w: 0} Flags: 0 + radius: 0 k__BackingField: 0 k__BackingField: 0 CrouchBlend: 1 @@ -617,6 +690,9 @@ MonoBehaviour: m_Bits: 456 maxDownProbe: 3 maxUpProbe: 1 + DebugDrawGizmos: 1 + DebugPointRadius: 0.03 + DebugAxisLength: 0.12 LocalAnimatorDriver: basisAnimatorVariableApply: Animator: {fileID: 0} @@ -680,7 +756,6 @@ MonoBehaviour: centerBias: 2.5 perEyeVarianceDeg: 0.4 occasionalCenterReturn: 1 - HasEyeSchedule: 0 LocalHandDriver: LeftHand: ThumbPercentage: {x: 0, y: 0} @@ -715,148 +790,7 @@ MonoBehaviour: RightMiddle: [] RightRing: [] RightLittle: [] - LastLeftThumbPercentage: {x: -1.1, y: -1.1} - LastLeftIndexPercentage: {x: -1.1, y: -1.1} - LastLeftMiddlePercentage: {x: -1.1, y: -1.1} - LastLeftRingPercentage: {x: -1.1, y: -1.1} - LastLeftLittlePercentage: {x: -1.1, y: -1.1} - LastRightThumbPercentage: {x: -1.1, y: -1.1} - LastRightIndexPercentage: {x: -1.1, y: -1.1} - LastRightMiddlePercentage: {x: -1.1, y: -1.1} - LastRightRingPercentage: {x: -1.1, y: -1.1} - LastRightLittlePercentage: {x: -1.1, y: -1.1} - LeftThumbAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - LeftIndexAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - LeftMiddleAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - LeftRingAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - LeftLittleAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - RightThumbAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - RightIndexAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - RightMiddleAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - RightRingAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} - RightLittleAdditional: - PoseData: - LeftThumb: [] - LeftIndex: [] - LeftMiddle: [] - LeftRing: [] - LeftLittle: [] - RightThumb: [] - RightIndex: [] - RightMiddle: [] - RightRing: [] - RightLittle: [] - Coord: {x: 0, y: 0} LerpSpeed: 22 - Poses: [] LocalVisemeDriver: smoothAmount: 70 HasViseme: @@ -873,10 +807,13 @@ MonoBehaviour: BlendShapeInfos: [] HasJob: 0 blendShapeCount: 0 + UseOpenLipSync: 0 + EligibleForOpenLipSync: 0 phonemeBlendShapeTable: [] WasSuccessful: 0 HashInstanceID: -1 uLipSyncEnabledState: 1 + InVisemeRange: 1 FacialBlinkDriver: Override: 0 meshRenderer: {fileID: 0} @@ -894,8 +831,35 @@ MonoBehaviour: NextBlinkTime: 0 BlinkStartTime: 0 OpenStartTime: 0 ---- !u!143 &5548795088377030369 -CharacterController: +--- !u!54 &8369693708823570861 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8538648612911191307} + serializedVersion: 5 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_CenterOfMass: {x: 0, y: 0, z: 0} + m_InertiaTensor: {x: 1, y: 1, z: 1} + m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ImplicitCom: 1 + m_ImplicitTensor: 1 + m_UseGravity: 0 + m_IsKinematic: 1 + m_Interpolate: 0 + m_Constraints: 0 + m_CollisionDetection: 0 +--- !u!136 &3011869999757657 +CapsuleCollider: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -909,13 +873,32 @@ CharacterController: serializedVersion: 2 m_Bits: 0 m_LayerOverridePriority: 0 + m_IsTrigger: 0 m_ProvidesContacts: 0 m_Enabled: 1 - serializedVersion: 3 + serializedVersion: 2 + m_Radius: 0.3 m_Height: 2 - m_Radius: 0.15 - m_SlopeLimit: 65 - m_StepOffset: 0.5 - m_SkinWidth: 0.01 - m_MinMoveDistance: 0.001 + m_Direction: 1 m_Center: {x: 0, y: 0, z: 0} +--- !u!114 &7825922350359905173 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8538648612911191307} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3f50f4d9a7b7b5c488ef048f8e073021, type: 3} + m_Name: + m_EditorClassIdentifier: Basis Framework::Basis.Scripts.BasisCharacterController.BasisKinematicCharacterController + _height: 2 + _radius: 0.3 + _center: {x: 0, y: 1, z: 0} + _skinWidth: 0.01 + _stepOffset: 0.3 + _minMoveDistance: 0 + _slopeLimit: 45 + _detectCollisions: 1 + _gravityDirection: {x: 0, y: -1, z: 0} From 2bbfe5a6bd95b9931dea31b425431c6522ce4c5f Mon Sep 17 00:00:00 2001 From: Sketch <109103755+SketchFoxsky@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:29:14 -0400 Subject: [PATCH 2/5] Update BasisLocalCharacterDriver.cs Removed Legacy Character Controller check as it is not needed. --- .../Local/BasisLocalCharacterDriver.cs | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs index c92153f96f..55e9052074 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs @@ -175,53 +175,11 @@ public void DeInitalize() HasEvents = false; } } - /// - /// Ensures the KCC component exists on the player GameObject. - /// Migrates from Unity's CharacterController if one is still present on the prefab. - /// - private void EnsureKCC() - { - if (characterController != null) return; - - GameObject go = BasisLocalPlayerTransform.gameObject; - - // Check if already has KCC - characterController = go.GetComponent(); - if (characterController != null) return; - - // Migrate from legacy CharacterController if present - CharacterController legacy = go.GetComponent(); - float oldHeight = 2f; - float oldRadius = 0.3f; - Vector3 oldCenter = new Vector3(0f, 1f, 0f); - float oldSkinWidth = 0.01f; - float oldStepOffset = 0.3f; - float oldSlopeLimit = 45f; - if (legacy != null) - { - oldHeight = legacy.height; - oldRadius = legacy.radius; - oldCenter = legacy.center; - oldSkinWidth = legacy.skinWidth; - oldStepOffset = legacy.stepOffset; - oldSlopeLimit = legacy.slopeLimit; - UnityEngine.Object.DestroyImmediate(legacy); - } - - characterController = go.AddComponent(); - characterController.height = oldHeight; - characterController.radius = oldRadius; - characterController.center = oldCenter; - characterController.skinWidth = oldSkinWidth; - characterController.stepOffset = oldStepOffset; - characterController.slopeLimit = oldSlopeLimit; - } public void Initialize(BasisLocalPlayer localPlayer) { LocalPlayer = localPlayer; BasisLocalPlayerTransform = localPlayer.transform; LocalAnimatorDriver = localPlayer.LocalAnimatorDriver; - EnsureKCC(); characterController.minMoveDistance = 0; characterController.skinWidth = 0.01f; characterController.OnKCCColliderHit = OnKCCHit; From 98b62f8783b90efa916074d2891cfe448424ca63 Mon Sep 17 00:00:00 2001 From: Sketch <109103755+SketchFoxsky@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:03:37 -0400 Subject: [PATCH 3/5] Changed KCC Initialization, Exposed some fields --- .../Local/BasisLocalCharacterDriver.cs | 1 + .../BasisKinematicCharacterController.cs | 33 +++++++------------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs index f4f18084db..f7eec0675b 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalCharacterDriver.cs @@ -179,6 +179,7 @@ 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; diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs index 7b93998063..99fbeb20da 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs @@ -33,9 +33,9 @@ public class BasisKinematicCharacterController : MonoBehaviour [SerializeField] private Vector3 _gravityDirection = Vector3.down; // Runtime Info - private Rigidbody _rigidbody; - private CapsuleCollider _capsule; - private bool _isGrounded; + public Rigidbody _rigidbody; + public CapsuleCollider _capsule; + public bool isGrounded; private Vector3 _groundNormal = Vector3.up; private CollisionFlags _lastFlags; @@ -117,8 +117,6 @@ public bool detectCollisions } } - public bool isGrounded => _isGrounded; - /// /// The ground surface normal from the last Move() call. Only valid when isGrounded is true. /// @@ -151,20 +149,11 @@ public Vector3 UpDirection #region UNITY LIFECYCLE - private void Awake() + public void PlayerInitialize() { // Ensure gravity direction is valid (may be zero if deserialized from a fresh component) if (_gravityDirection.sqrMagnitude < 0.0001f) _gravityDirection = Vector3.down; - - _rigidbody = GetComponent(); - _rigidbody.isKinematic = true; - _rigidbody.useGravity = false; - _rigidbody.interpolation = RigidbodyInterpolation.None; - _rigidbody.collisionDetectionMode = CollisionDetectionMode.Discrete; - _rigidbody.constraints = RigidbodyConstraints.FreezeAll; - - _capsule = GetComponent(); SyncCapsule(); } @@ -192,7 +181,7 @@ public CollisionFlags Move(Vector3 motion) if (!enabled || !_detectCollisions) { transform.position += motion; - _isGrounded = false; + isGrounded = false; return _lastFlags; } @@ -210,7 +199,7 @@ public CollisionFlags Move(Vector3 motion) bool movingDown = verticalComponent < 0f; // ── Grounded behaviour: slope projection + ground snap ────── - if (_isGrounded && movingDown) + if (isGrounded && movingDown) { float groundDot = Vector3.Dot(_groundNormal, up); bool walkableSlope = groundDot >= cosSlope; @@ -238,7 +227,7 @@ public CollisionFlags Move(Vector3 motion) pos = SimpleMove(pos, horizontalMotion, ref _lastFlags, up, cosSlope, isHorizontal: true); // If grounded and slide made little horizontal progress, try stepping up - if (_isGrounded && _stepOffset > 0f && movingDown) + if (isGrounded && _stepOffset > 0f && movingDown) { Vector3 traveled = pos - beforeSlide; Vector3 horizontalTraveled = traveled - up * Vector3.Dot(traveled, up); @@ -266,7 +255,7 @@ public CollisionFlags Move(Vector3 motion) transform.position = pos; // snap to ground - if (_isGrounded && verticalComponent <= 0f) + if (isGrounded && verticalComponent <= 0f) { pos = GroundSnap(pos, up, cosSlope); transform.position = pos; @@ -515,7 +504,7 @@ private void GroundProbe() { if (!_detectCollisions || !enabled) { - _isGrounded = false; + isGrounded = false; _groundNormal = UpDirection; return; } @@ -542,7 +531,7 @@ private void GroundProbe() QueryTriggerInteraction.Ignore ); - _isGrounded = false; + isGrounded = false; _groundNormal = up; float cosSlope = Mathf.Cos(_slopeLimit * Mathf.Deg2Rad); float closestDist = float.MaxValue; @@ -554,7 +543,7 @@ private void GroundProbe() if (dotUp >= cosSlope && _hitBuffer[i].distance < closestDist) { closestDist = _hitBuffer[i].distance; - _isGrounded = true; + isGrounded = true; _groundNormal = _hitBuffer[i].normal; } } From 01aca13c4f1fd625e4ff62a0cd136969f22d0ec1 Mon Sep 17 00:00:00 2001 From: Sketch <109103755+SketchFoxsky@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:33:00 -0400 Subject: [PATCH 4/5] github please what is actually going on Changes made previously were not pushed or git stroked out IK changes from Developer branch broke need to rewire it! - Exposed more fields - Properly Cache Collision Masks - Cache transform data so its not being called a shit ton --- .../BasisKinematicCharacterController.cs | 109 +++++++++--------- .../Prefabs/Players/LocalPlayer.prefab | 14 ++- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs index 99fbeb20da..20a9180b3f 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/Character Controller/BasisKinematicCharacterController.cs @@ -6,31 +6,25 @@ namespace Basis.Scripts.BasisCharacterController /// A kinematic character controller that replaces Unity's built-in CharacterController. /// Uses a CapsuleCollider + kinematic Rigidbody so that the player can be rotated /// freely, enabling custom gravity directions (not Y-axis locked). - /// - /// TODO - /// Add Rotate to Gravity - /// Add Flight/Noclip exposure for worlds - /// MAYBE add native swimming! - /// /// [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(CapsuleCollider))] public class BasisKinematicCharacterController : MonoBehaviour { // Capsule Collider - [SerializeField] private float _height = 2f; - [SerializeField] private float _radius = 0.3f; - [SerializeField] private Vector3 _center = new Vector3(0f, 1f, 0f); + public float _height = 2f; + public float _radius = 0.3f; + public Vector3 _center = new Vector3(0f, 1f, 0f); // CharacterController Parameters - [SerializeField] private float _skinWidth = 0.01f; - [SerializeField] private float _stepOffset = 0.3f; - [SerializeField] private float _minMoveDistance = 0f; - [SerializeField] private float _slopeLimit = 45f; - [SerializeField] private bool _detectCollisions = true; + public float _skinWidth = 0.01f; + public float _stepOffset = 0.3f; + public float _minMoveDistance = 0f; + public float _slopeLimit = 45f; + public bool _detectCollisions = true; - //Gravity - [SerializeField] private Vector3 _gravityDirection = Vector3.down; + // Gravity + public Vector3 _gravityDirection = Vector3.down; // Runtime Info public Rigidbody _rigidbody; @@ -40,6 +34,9 @@ public class BasisKinematicCharacterController : MonoBehaviour private CollisionFlags _lastFlags; // Collision + // 16 hits/overlaps covers worst-case scenarios for capsule casts in typical + // game environments. NonAlloc silently drops excess results but 16 is generous — + // most casts hit 1-3 colliders. Increase only if levels have extreme collider density. private const int MaxHits = 16; private const int MaxOverlaps = 16; private const int MaxDepenetrationIterations = 4; @@ -50,6 +47,13 @@ public class BasisKinematicCharacterController : MonoBehaviour public delegate void KCCColliderHit(KCCHitInfo hit); public KCCColliderHit OnKCCColliderHit; + // Cached collision mask — rebuilt once per Move() instead of per-cast. + private int _collisionMask; + private bool _collisionMaskDirty = true; + + // Cached per-Move() to avoid repeated Transform property access. + private Quaternion _cachedRotation; + //Public Get Set public float height @@ -112,8 +116,7 @@ public bool detectCollisions set { _detectCollisions = value; - if (_capsule != null) - _capsule.enabled = value; + _capsule.enabled = value; } } @@ -159,7 +162,6 @@ public void PlayerInitialize() private void SyncCapsule() { - if (_capsule == null) return; _capsule.direction = 1; // Y-axis _capsule.center = _center; _capsule.radius = _radius; @@ -177,6 +179,7 @@ private void SyncCapsule() public CollisionFlags Move(Vector3 motion) { _lastFlags = CollisionFlags.None; + _collisionMaskDirty = true; // Refresh mask once per Move() if (!enabled || !_detectCollisions) { @@ -185,9 +188,12 @@ public CollisionFlags Move(Vector3 motion) return _lastFlags; } + // Cache rotation once per Move() — used by GetCapsuleEnds, GroundProbe, Depenetrate. + transform.GetPositionAndRotation(out Vector3 pos, out _cachedRotation); + if (motion.sqrMagnitude < _minMoveDistance * _minMoveDistance) { - GroundProbe(); + GroundProbe(pos); return _lastFlags; } @@ -218,8 +224,6 @@ public CollisionFlags Move(Vector3 motion) } } - Vector3 pos = transform.position; - // ── Horizontal movement with step-up fallback ─────────────── if (horizontalMotion.sqrMagnitude > 0.00001f) { @@ -252,21 +256,20 @@ public CollisionFlags Move(Vector3 motion) pos = SimpleMove(pos, verticalMotion, ref _lastFlags, up, cosSlope, isHorizontal: false); } - transform.position = pos; - // snap to ground if (isGrounded && verticalComponent <= 0f) { pos = GroundSnap(pos, up, cosSlope); - transform.position = pos; } // Depenetration if clipping with a collider pos = Depenetrate(pos); + + // Single transform write per Move() call transform.position = pos; // Ground probe - GroundProbe(); + GroundProbe(pos); return _lastFlags; } @@ -332,10 +335,6 @@ private Vector3 SimpleMove(Vector3 position, Vector3 motion, ref CollisionFlags return position; } - // Step Offset - - // kill me please this took a while to debug, had to look at old braxy tutorials :) - private bool TryStepUp(ref Vector3 pos, Vector3 horizontalMotion, Vector3 up, float cosSlope) { float castRadius = _radius - _skinWidth; @@ -420,10 +419,6 @@ private bool TryStepUp(ref Vector3 pos, Vector3 horizontalMotion, Vector3 up, fl return true; } - //Ground Snapping - - // Add ground Friction, its almost 1am im not doing that tonight - /// /// When grounded and not jumping, cast downward to anchor the character /// to the ground surface. Prevents floating over small bumps and slopes. @@ -461,8 +456,6 @@ private Vector3 GroundSnap(Vector3 position, Vector3 up, float cosSlope) return position; } - // Depenetration Helper - private Vector3 Depenetrate(Vector3 position) { for (int iter = 0; iter < MaxDepenetrationIterations; iter++) @@ -483,7 +476,7 @@ private Vector3 Depenetrate(Vector3 position) if (other == _capsule) continue; if (Physics.ComputePenetration( - _capsule, position, transform.rotation, + _capsule, position, _cachedRotation, other, other.transform.position, other.transform.rotation, out Vector3 dir, out float dist)) { @@ -498,9 +491,7 @@ private Vector3 Depenetrate(Vector3 position) return position; } - // Ground Detection - - private void GroundProbe() + private void GroundProbe(Vector3 pos) { if (!_detectCollisions || !enabled) { @@ -510,10 +501,9 @@ private void GroundProbe() } Vector3 up = UpDirection; - Vector3 pos = transform.position; // Cast a small sphere downward from the bottom of the capsule - Vector3 worldCenter = pos + transform.rotation * _center; + Vector3 worldCenter = pos + _cachedRotation * _center; float halfHeight = (_height * 0.5f) - _radius; Vector3 bottom = worldCenter - up * halfHeight; @@ -554,9 +544,8 @@ private void GroundProbe() private void GetCapsuleEnds(Vector3 position, out Vector3 point1, out Vector3 point2) { - Quaternion rot = transform.rotation; - Vector3 worldCenter = position + rot * _center; - Vector3 capsuleUp = rot * Vector3.up; + Vector3 worldCenter = position + _cachedRotation * _center; + Vector3 capsuleUp = _cachedRotation * Vector3.up; float halfHeight = (_height * 0.5f) - _radius; if (halfHeight < 0f) halfHeight = 0f; point1 = worldCenter + capsuleUp * halfHeight; @@ -565,14 +554,28 @@ private void GetCapsuleEnds(Vector3 position, out Vector3 point1, out Vector3 po private int GetCollisionMask() { - int layer = gameObject.layer; - int mask = 0; - for (int i = 0; i < 32; i++) + if (_collisionMaskDirty) { - if (!Physics.GetIgnoreLayerCollision(layer, i)) - mask |= (1 << i); + int layer = gameObject.layer; + int mask = 0; + for (int i = 0; i < 32; i++) + { + if (!Physics.GetIgnoreLayerCollision(layer, i)) + mask |= (1 << i); + } + _collisionMask = mask; + _collisionMaskDirty = false; } - return mask; + return _collisionMask; + } + + /// + /// Call if the object's layer or physics layer collision matrix changes at runtime. + /// The mask is automatically refreshed at the start of each Move() call. + /// + public void InvalidateCollisionMask() + { + _collisionMaskDirty = true; } private bool FindClosestHit(int hitCount, out RaycastHit closest) @@ -613,9 +616,9 @@ private void FireHitCallback(RaycastHit hit, Vector3 moveDir, float moveDist) info.moveLength = moveDist; OnKCCColliderHit(info); } - } - #endregion + #endregion + } /// /// Hit information passed to the KCC collision callback, mirroring ControllerColliderHit. diff --git a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab index 019825774e..51abb167e3 100644 --- a/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab +++ b/Basis/Packages/com.basis.sdk/Prefabs/Players/LocalPlayer.prefab @@ -332,6 +332,8 @@ MonoBehaviour: UnMutedMutedIconColorActive: {r: 1, g: 1, b: 1, a: 0.67058825} UnMutedMutedIconColorInactive: {r: 0.5, g: 0.5, b: 0.5, a: 0.67058825} MutedColor: {r: 1, g: 0, b: 0, a: 0.67058825} + ShoutColorActive: {r: 1, g: 0.92156863, b: 0.015686275, a: 1} + ShoutColorInactive: {r: 0.6, g: 0.6, b: 0, a: 1} StartingScale: {x: 0, y: 0, z: 0} largerScale: {x: 0, y: 0, z: 0} MuteSound: {fileID: 8300000, guid: 5b9ed5013ea1941499840ac97ad2b8e9, type: 3} @@ -340,6 +342,8 @@ MonoBehaviour: duration: 0.35 halfDuration: 0 CameraDriver: {fileID: 22857711927985140} + avatarPreviewDriver: + CameraFieldOfView: 40 --- !u!114 &3806033592985501336 MonoBehaviour: m_ObjectHideFlags: 0 @@ -631,6 +635,7 @@ MonoBehaviour: rightShinLen: 0 footLength: 0 ankleHeight: 0 + upperLegToFootVertical: 0 stepTriggerDist: 0 strideScale: 0 stepHeightCalc: 0 @@ -641,7 +646,7 @@ MonoBehaviour: fastSpeedRef: 0 LocalCharacterDriver: LocalPlayer: {fileID: 4587526044252889482} - characterController: {fileID: 0} + characterController: {fileID: 7825922350359905173} bottomPointLocalSpace: {x: 0, y: 0, z: 0} LastBottomPoint: {x: 0, y: 0, z: 0} groundedPlayer: 0 @@ -666,7 +671,6 @@ MonoBehaviour: coyoteTimeDuration: 0.15 fallingGracePeriod: 0.1 Rotation: {x: 0, y: 0} - RotationSpeed: 200 HasEvents: 0 pushPower: 1 CurrentPosition: {x: 0, y: 0, z: 0} @@ -678,7 +682,7 @@ MonoBehaviour: CrouchBlend: 1 CrouchBlendDelta: 0 CanPushRigidbodys: 0 - BasisLocalPlayerTransform: {fileID: 0} + BasisLocalPlayerTransform: {fileID: 303201899784742928} CurrentSpeed: 0 LocalSeatDriver: UseDefaultMasking: 1 @@ -809,6 +813,7 @@ MonoBehaviour: blendShapeCount: 0 UseOpenLipSync: 0 EligibleForOpenLipSync: 0 + TrackedAudioSource: {fileID: 0} phonemeBlendShapeTable: [] WasSuccessful: 0 HashInstanceID: -1 @@ -902,3 +907,6 @@ MonoBehaviour: _slopeLimit: 45 _detectCollisions: 1 _gravityDirection: {x: 0, y: -1, z: 0} + _rigidbody: {fileID: 8369693708823570861} + _capsule: {fileID: 3011869999757657} + isGrounded: 0 From 1d19ce86ba3e3ac7102969ae51d478167206275a Mon Sep 17 00:00:00 2001 From: Sketch <109103755+SketchFoxsky@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:10:31 -0400 Subject: [PATCH 5/5] I hate Githubbbbbbb IK changes were regressed during a previous merge, These will also be cherry picked later for an update upstream. --- .../Drivers/Local/BasisLocalAvatarDriver.cs | 3 +- .../Drivers/Local/BasisLocalRigDriver.cs | 16 ++--- .../Local/BasisLocalVirtualSpineDriver.cs | 67 ++++++++++--------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs index 4ad01356d7..679b5b70e8 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAvatarDriver.cs @@ -509,8 +509,9 @@ public void GetBoneRotAndPos(quaternion RootRotation, Animator anim, HumanBodyBo /// Offset vector applied to the base position. 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); } /// diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs index 10787bacf7..3797b8171d 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalRigDriver.cs @@ -309,6 +309,7 @@ public void SimulateIKDestinations(float deltaTime) } timeAccumulator += Mathf.Max(deltaTime, 1e-6f); + Vector3 cachedPlayerUp = localPlayer.LocalCharacterDriver.UpDirection; BasisFullBodyData data = BasisFullIKConstraint.data; @@ -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; @@ -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); } } @@ -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; @@ -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); @@ -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; @@ -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. /// - 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; @@ -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(); diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs index 3a8a202e79..0cb3888ec9 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalVirtualSpineDriver.cs @@ -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) // ========================= @@ -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) @@ -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; @@ -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 { @@ -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 @@ -229,18 +226,21 @@ public void OnSimulate() /// /// 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. /// - 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); @@ -249,16 +249,19 @@ private void ApplyPositionControl(BasisLocalBoneControl boneControl, Matrix4x4 p /// /// Applies position using a provided world base position and the control's yaw/offset rules. /// - 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); @@ -274,14 +277,14 @@ private static Quaternion SmoothSlerp(Quaternion current, Quaternion target, flo } /// - /// Extracts yaw-only rotation (around global up) from a full quaternion. + /// Extracts yaw-only rotation (around the given up axis) from a full quaternion. /// - 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); } }