401 lines
13 KiB
C#
Raw Permalink Normal View History

2025-09-24 11:24:38 +05:00
namespace Fusion.Addons.KCC
{
using System;
using UnityEngine;
// This file contains penetration solver.
public partial class KCC
{
// PUBLIC METHODS
[Obsolete("ResolvePenetration now works directly with KCCData.BasePosition and KCCData.TargetPosition. Please switch to ResolvePenetration(KCCOverlapInfo overlapInfo, KCCData data, int maxSteps, bool probeGrounding, bool resolveTriggers).")]
public Vector3 ResolvePenetration(KCCOverlapInfo overlapInfo, KCCData data, Vector3 basePosition, Vector3 targetPosition, bool probeGrounding, int maxSteps, int resolverIterations, bool resolveTriggers)
{
data.BasePosition = basePosition;
data.TargetPosition = targetPosition;
ResolvePenetration(overlapInfo, data, maxSteps, probeGrounding, resolveTriggers);
return data.TargetPosition;
}
/// <summary>
/// Computes penetration vectors for given OverlapInfo and moves the KCC out of collision.
/// It is safe to pass OverlapInfo with results from inflated capsule overlap. Penetration info is stored in overlap hits.
/// </summary>
/// <param name="overlapInfo">Info with overlap hits.</param>
/// <param name="data">KCCData instance. Results are stored here.</param>
/// <param name="maxSteps">How many times the penetration is calculated.</param>
/// <param name="probeGrounding">Make extra ground check after depenetration.</param>
/// <param name="resolveTriggers">Whether the penetration should be checked against triggers.</param>
public void ResolvePenetration(KCCOverlapInfo overlapInfo, KCCData data, int maxSteps, bool probeGrounding, bool resolveTriggers)
{
if (_settings.SuppressConvexMeshColliders == true)
{
overlapInfo.ToggleConvexMeshColliders(false);
}
if (overlapInfo.ColliderHitCount == 1)
{
DepenetrateSingle(overlapInfo, data, probeGrounding);
}
else if (overlapInfo.ColliderHitCount > 1)
{
DepenetrateMultiple(overlapInfo, data, probeGrounding, maxSteps);
}
RecalculateGroundProperties(data);
if (resolveTriggers == true)
{
for (int i = 0; i < overlapInfo.TriggerHitCount; ++i)
{
KCCOverlapHit hit = overlapInfo.TriggerHits[i];
hit.Transform.GetPositionAndRotation(out hit.CachedPosition, out hit.CachedRotation);
bool hasPenetration = Physics.ComputePenetration(_collider.Collider, data.TargetPosition, Quaternion.identity, hit.Collider, hit.CachedPosition, hit.CachedRotation, out Vector3 direction, out float distance);
hit.IsWithinExtent = hasPenetration;
hit.HasPenetration = hasPenetration;
hit.MaxPenetration = default;
hit.CollisionType = hasPenetration == true ? ECollisionType.Trigger : ECollisionType.None;
if (distance > hit.MaxPenetration)
{
hit.MaxPenetration = distance;
}
}
}
if (_settings.SuppressConvexMeshColliders == true)
{
overlapInfo.ToggleConvexMeshColliders(true);
}
}
// PRIVATE METHODS
private void DepenetrateSingle(KCCOverlapInfo overlapInfo, KCCData data, bool probeGrounding)
{
float minGroundDot = Mathf.Cos(Mathf.Clamp(data.MaxGroundAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
KCCOverlapHit hit = overlapInfo.ColliderHits[0];
hit.IsWithinExtent = default;
hit.HasPenetration = default;
hit.MaxPenetration = default;
hit.UpDirectionDot = default;
hit.Transform.GetPositionAndRotation(out hit.CachedPosition, out hit.CachedRotation);
hit.HasPenetration = Physics.ComputePenetration(_collider.Collider, data.TargetPosition, Quaternion.identity, hit.Collider, hit.CachedPosition, hit.CachedRotation, out Vector3 direction, out float distance);
if (hit.HasPenetration == true)
{
hit.IsWithinExtent = true;
hit.MaxPenetration = distance;
hit.UpDirectionDot = Vector3.Dot(direction, Vector3.up);
float minWallDot = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxWallAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
float minHangDot = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxHangAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
if (hit.UpDirectionDot >= minGroundDot)
{
hit.CollisionType = ECollisionType.Ground;
data.IsGrounded = true;
data.GroundNormal = direction;
data.GroundDistance = default;
data.GroundAngle = Vector3.Angle(direction, Vector3.up);
}
else if (hit.UpDirectionDot > -minWallDot)
{
hit.CollisionType = ECollisionType.Slope;
}
else if (hit.UpDirectionDot >= minWallDot)
{
hit.CollisionType = ECollisionType.Wall;
}
else if (hit.UpDirectionDot >= minHangDot)
{
hit.CollisionType = ECollisionType.Hang;
}
else
{
hit.CollisionType = ECollisionType.Top;
}
if (hit.UpDirectionDot > 0.0f && hit.UpDirectionDot < minGroundDot)
{
if (distance >= 0.000001f && data.DynamicVelocity.y <= 0.0f)
{
Vector3 positionDelta = data.TargetPosition - data.BasePosition;
float movementDot = Vector3.Dot(positionDelta.OnlyXZ(), direction.OnlyXZ());
if (movementDot < 0.0f)
{
KCCPhysicsUtility.ProjectVerticalPenetration(ref direction, ref distance);
}
}
}
data.TargetPosition += direction * distance;
}
if (data.IsGrounded == true)
{
data.GroundPosition = data.TargetPosition + (Vector3.up - data.GroundNormal) * _settings.Radius;
data.GroundAngle = Vector3.Angle(data.GroundNormal, Vector3.up);
}
if (data.IsGrounded == false && probeGrounding == true)
{
bool isGrounded = KCCPhysicsUtility.CheckGround(_collider.Collider, data.TargetPosition, hit.Collider, hit.CachedPosition, hit.CachedRotation, _settings.Radius, _settings.Height, _settings.Extent, minGroundDot, out Vector3 checkGroundPosition, out Vector3 checkGroundNormal, out float checkGroundDistance, out bool isWithinExtent);
if (isGrounded == true)
{
data.IsGrounded = true;
data.GroundNormal = checkGroundNormal;
data.GroundPosition = checkGroundPosition;
data.GroundDistance = checkGroundDistance;
data.GroundAngle = Vector3.Angle(checkGroundNormal, Vector3.up);
hit.IsWithinExtent = true;
hit.CollisionType = ECollisionType.Ground;
}
else if (isWithinExtent == true)
{
hit.IsWithinExtent = true;
if (hit.CollisionType == ECollisionType.None)
{
hit.CollisionType = ECollisionType.Slope;
}
}
}
}
private void DepenetrateMultiple(KCCOverlapInfo overlapInfo, KCCData data, bool probeGrounding, int maxSteps)
{
if (maxSteps <= 0)
{
maxSteps = 1;
}
float minGroundDot = Mathf.Cos(Mathf.Clamp(data.MaxGroundAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
float minWallDot = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxWallAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
float minHangDot = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxHangAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
float maxGroundDot = default;
Vector3 maxGroundNormal = default;
Vector3 averageGroundNormal = default;
Vector3 positionDelta = data.TargetPosition - data.BasePosition;
Vector3 positionDeltaXZ = positionDelta.OnlyXZ();
for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
{
KCCOverlapHit hit = overlapInfo.ColliderHits[i];
hit.IsWithinExtent = default;
hit.HasPenetration = default;
hit.MaxPenetration = default;
hit.UpDirectionDot = float.MinValue;
hit.Transform.GetPositionAndRotation(out hit.CachedPosition, out hit.CachedRotation);
}
if (maxSteps > 1)
{
float minStepDistance = 0.001f;
float targetDistance = Vector3.Magnitude(positionDelta);
if (targetDistance < maxSteps * minStepDistance)
{
maxSteps = Mathf.Max(1, (int)(targetDistance / minStepDistance));
}
}
int remainingSteps = maxSteps;
while (remainingSteps > 0)
{
--remainingSteps;
_resolver.Reset();
for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
{
KCCOverlapHit hit = overlapInfo.ColliderHits[i];
bool hasPenetration = Physics.ComputePenetration(_collider.Collider, data.TargetPosition, Quaternion.identity, hit.Collider, hit.CachedPosition, hit.CachedRotation, out Vector3 direction, out float distance);
if (hasPenetration == false)
continue;
hit.HasPenetration = true;
hit.IsWithinExtent = true;
if (distance > hit.MaxPenetration)
{
hit.MaxPenetration = distance;
}
float upDirectionDot = Vector3.Dot(direction, Vector3.up);
if (upDirectionDot > hit.UpDirectionDot)
{
hit.UpDirectionDot = upDirectionDot;
if (upDirectionDot >= minGroundDot)
{
hit.CollisionType = ECollisionType.Ground;
data.IsGrounded = true;
if (upDirectionDot >= maxGroundDot)
{
maxGroundDot = upDirectionDot;
maxGroundNormal = direction;
}
averageGroundNormal += direction * upDirectionDot;
}
else if (upDirectionDot > -minWallDot)
{
hit.CollisionType = ECollisionType.Slope;
}
else if (upDirectionDot >= minWallDot)
{
hit.CollisionType = ECollisionType.Wall;
}
else if (upDirectionDot >= minHangDot)
{
hit.CollisionType = ECollisionType.Hang;
}
else
{
hit.CollisionType = ECollisionType.Top;
}
}
if (upDirectionDot > 0.0f && upDirectionDot < minGroundDot)
{
if (distance >= 0.000001f && data.DynamicVelocity.y <= 0.0f)
{
float movementDot = Vector3.Dot(positionDeltaXZ, direction.OnlyXZ());
if (movementDot < 0.0f)
{
KCCPhysicsUtility.ProjectVerticalPenetration(ref direction, ref distance);
}
}
}
_resolver.AddCorrection(direction, distance);
}
if (_resolver.Count <= 0)
break;
int resolverInterations = 8;
float resolverMaxError = 0.001f;
if (remainingSteps == 0)
{
resolverInterations = 12;
resolverMaxError = 0.0001f;
}
Vector3 correction = _resolver.CalculateBest(resolverInterations, resolverMaxError);
correction = Vector3.ClampMagnitude(correction, _settings.Radius);
float correctionMultiplier = Mathf.Max(0.25f, 1.0f - remainingSteps * 0.25f);
correction *= correctionMultiplier;
data.TargetPosition += correction;
}
for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
{
KCCOverlapHit hit = overlapInfo.ColliderHits[i];
if (hit.UpDirectionDot == float.MinValue)
{
hit.UpDirectionDot = default;
}
}
if (data.IsGrounded == true)
{
data.GroundNormal = maxGroundNormal;
averageGroundNormal.Normalize();
float averageGroundNormalUpDot = Vector3.Dot(averageGroundNormal, Vector3.up);
if (averageGroundNormalUpDot >= maxGroundDot)
{
data.GroundNormal = averageGroundNormal;
}
data.GroundPosition = data.TargetPosition + (Vector3.up - data.GroundNormal) * _settings.Radius;
data.GroundDistance = default;
data.GroundAngle = Vector3.Angle(data.GroundNormal, Vector3.up);
}
if (data.IsGrounded == false && probeGrounding == true)
{
Vector3 closestGroundNormal = Vector3.up;
Vector3 closestGroundPosition = default;
float closestGroundDistance = 1000.0f;
for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
{
KCCOverlapHit hit = overlapInfo.ColliderHits[i];
bool isGrounded = KCCPhysicsUtility.CheckGround(_collider.Collider, data.TargetPosition, hit.Collider, hit.CachedPosition, hit.CachedRotation, _settings.Radius, _settings.Height, _settings.Extent, minGroundDot, out Vector3 checkGroundPosition, out Vector3 checkGroundNormal, out float checkGroundDistance, out bool isWithinExtent);
if (isGrounded == true)
{
data.IsGrounded = true;
if (checkGroundDistance < closestGroundDistance)
{
closestGroundNormal = checkGroundNormal;
closestGroundPosition = checkGroundPosition;
closestGroundDistance = checkGroundDistance;
}
hit.IsWithinExtent = true;
hit.CollisionType = ECollisionType.Ground;
}
else if (isWithinExtent == true)
{
hit.IsWithinExtent = true;
if (hit.CollisionType == ECollisionType.None)
{
hit.CollisionType = ECollisionType.Slope;
}
}
}
if (data.IsGrounded == true)
{
data.IsGrounded = true;
data.GroundNormal = closestGroundNormal;
data.GroundPosition = closestGroundPosition;
data.GroundDistance = closestGroundDistance;
data.GroundAngle = Vector3.Angle(closestGroundNormal, Vector3.up);
}
}
}
private static void RecalculateGroundProperties(KCCData data)
{
if (data.IsGrounded == false)
return;
if (KCCPhysicsUtility.ProjectOnGround(data.GroundNormal, data.GroundNormal.OnlyXZ(), out Vector3 projectedGroundNormal) == true)
{
data.GroundTangent = projectedGroundNormal.normalized;
return;
}
if (KCCPhysicsUtility.ProjectOnGround(data.GroundNormal, data.DesiredVelocity.OnlyXZ(), out Vector3 projectedDesiredVelocity) == true)
{
data.GroundTangent = projectedDesiredVelocity.normalized;
return;
}
data.GroundTangent = data.TransformDirection;
}
}
}