| | | 1 | | //======================================================================= |
| | | 2 | | // ContactManifold.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2026–present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using FixedMathSharp; |
| | | 9 | | using Gravitas.Materials; |
| | | 10 | | using System.Collections; |
| | | 11 | | using System.Collections.Generic; |
| | | 12 | | using System.Runtime.CompilerServices; |
| | | 13 | | |
| | | 14 | | namespace Gravitas.CollisionHandling; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Fixed-capacity deterministic contact manifold owned by one collision pair. |
| | | 18 | | /// </summary> |
| | | 19 | | public sealed class ContactManifold : IEnumerable<ManifoldContact> |
| | | 20 | | { |
| | | 21 | | /// <summary>Maximum number of contacts retained by a 3D manifold.</summary> |
| | | 22 | | public const int MaxContactCount = 4; |
| | | 23 | | |
| | | 24 | | private ManifoldContact _contact0; |
| | | 25 | | private ManifoldContact _contact1; |
| | | 26 | | private ManifoldContact _contact2; |
| | | 27 | | private ManifoldContact _contact3; |
| | | 28 | | private int _count; |
| | 10866 | 29 | | private int _lastUpdatedFrame = -1; |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Number of active contacts in this manifold. |
| | | 33 | | /// </summary> |
| | | 34 | | public int Count |
| | | 35 | | { |
| | | 36 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 20781 | 37 | | get => _count; |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | /// <summary> |
| | | 41 | | /// Gets whether this manifold currently contains narrow-phase contact data. |
| | | 42 | | /// </summary> |
| | | 43 | | public bool HasContact |
| | | 44 | | { |
| | | 45 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 17026 | 46 | | get => _count > 0; |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Simulation frame in which the active contacts were last rebuilt. |
| | | 51 | | /// </summary> |
| | | 52 | | public int LastUpdatedFrame |
| | | 53 | | { |
| | | 54 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 44 | 55 | | get => _lastUpdatedFrame; |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | /// <summary> |
| | | 59 | | /// Deepest contact in the manifold. Ties use the lowest contact identity. |
| | | 60 | | /// </summary> |
| | | 61 | | public ManifoldContact PrimaryContact |
| | | 62 | | { |
| | | 63 | | get |
| | | 64 | | { |
| | 1566 | 65 | | SwiftThrowHelper.ThrowIfListIndexInvalid(0, _count); |
| | | 66 | | |
| | 1566 | 67 | | int bestIndex = 0; |
| | 1566 | 68 | | ManifoldContact best = _contact0; |
| | 3574 | 69 | | for (int i = 1; i < _count; i++) |
| | | 70 | | { |
| | 221 | 71 | | ManifoldContact candidate = this[i]; |
| | 221 | 72 | | if (candidate.Depth > best.Depth |
| | 221 | 73 | | || candidate.Depth == best.Depth && candidate.ContactId < best.ContactId) |
| | | 74 | | { |
| | 8 | 75 | | best = candidate; |
| | 8 | 76 | | bestIndex = i; |
| | | 77 | | } |
| | | 78 | | } |
| | | 79 | | |
| | 1566 | 80 | | return this[bestIndex]; |
| | | 81 | | } |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | /// <summary>Gets the contact at the specified deterministic order index.</summary> |
| | | 85 | | public ManifoldContact this[int index] |
| | | 86 | | { |
| | | 87 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 88 | | get |
| | | 89 | | { |
| | 13013 | 90 | | SwiftThrowHelper.ThrowIfListIndexInvalid(index, _count); |
| | 13013 | 91 | | return GetContactUnchecked(index); |
| | | 92 | | } |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Clears contacts and records the frame for a new narrow-phase pass. |
| | | 97 | | /// </summary> |
| | | 98 | | public void BeginUpdate(int frame) |
| | | 99 | | { |
| | 10052 | 100 | | _count = 0; |
| | 10052 | 101 | | _lastUpdatedFrame = frame; |
| | 10052 | 102 | | } |
| | | 103 | | |
| | | 104 | | /// <summary> |
| | | 105 | | /// Clears all contact data. |
| | | 106 | | /// </summary> |
| | | 107 | | public void Reset() |
| | | 108 | | { |
| | 2534 | 109 | | _count = 0; |
| | 2534 | 110 | | _lastUpdatedFrame = -1; |
| | 2534 | 111 | | _contact0 = default; |
| | 2534 | 112 | | _contact1 = default; |
| | 2534 | 113 | | _contact2 = default; |
| | 2534 | 114 | | _contact3 = default; |
| | 2534 | 115 | | } |
| | | 116 | | |
| | | 117 | | /// <summary> |
| | | 118 | | /// Replaces the manifold with one contact. |
| | | 119 | | /// </summary> |
| | | 120 | | public void SetContact(Vector3d pointA, Vector3d pointB, Fixed64 depth, Vector3d normal) |
| | | 121 | | { |
| | 64 | 122 | | _count = 0; |
| | 64 | 123 | | AddContact(pointA, pointB, depth, normal); |
| | 64 | 124 | | } |
| | | 125 | | |
| | | 126 | | /// <summary> |
| | | 127 | | /// Replaces the manifold with one rigid-frame contact. |
| | | 128 | | /// </summary> |
| | | 129 | | public void SetContact( |
| | | 130 | | ContactAnchor anchorA, |
| | | 131 | | ContactAnchor anchorB, |
| | | 132 | | Fixed64 depth, |
| | | 133 | | Vector3d normal, |
| | | 134 | | bool depthIsClamped = false) |
| | | 135 | | { |
| | 6189 | 136 | | _count = 0; |
| | 6189 | 137 | | AddContact(anchorA, anchorB, depth, normal, depthIsClamped); |
| | 6189 | 138 | | } |
| | | 139 | | |
| | | 140 | | /// <summary> |
| | | 141 | | /// Adds a contact, keeping the deepest four contacts and exposing them by stable contact identity. |
| | | 142 | | /// </summary> |
| | | 143 | | public void AddContact(Vector3d pointA, Vector3d pointB, Fixed64 depth, Vector3d normal) |
| | | 144 | | { |
| | 1091 | 145 | | AddContact( |
| | 1091 | 146 | | ContactAnchor.FromWorldPoint(pointA), |
| | 1091 | 147 | | ContactAnchor.FromWorldPoint(pointB), |
| | 1091 | 148 | | depth, |
| | 1091 | 149 | | normal); |
| | 1091 | 150 | | } |
| | | 151 | | |
| | | 152 | | /// <summary> |
| | | 153 | | /// Adds a rigid-frame contact, keeping the deepest four contacts and |
| | | 154 | | /// exposing them by stable anchor identity. |
| | | 155 | | /// </summary> |
| | | 156 | | public void AddContact( |
| | | 157 | | ContactAnchor anchorA, |
| | | 158 | | ContactAnchor anchorB, |
| | | 159 | | Fixed64 depth, |
| | | 160 | | Vector3d normal, |
| | | 161 | | bool depthIsClamped = false) |
| | | 162 | | { |
| | 30360 | 163 | | AddContactCore( |
| | 30360 | 164 | | anchorA, |
| | 30360 | 165 | | anchorB, |
| | 30360 | 166 | | depth, |
| | 30360 | 167 | | normal, |
| | 30360 | 168 | | hasMaterialOverride: false, |
| | 30360 | 169 | | default, |
| | 30360 | 170 | | default, |
| | 30360 | 171 | | depthIsClamped, |
| | 30360 | 172 | | featureNamespaceA: 0, |
| | 30360 | 173 | | featureNamespaceB: 0); |
| | 30360 | 174 | | } |
| | | 175 | | |
| | | 176 | | internal void AddContact( |
| | | 177 | | Vector3d pointA, |
| | | 178 | | Vector3d pointB, |
| | | 179 | | Fixed64 depth, |
| | | 180 | | Vector3d normal, |
| | | 181 | | PhysicsMaterial materialA, |
| | | 182 | | PhysicsMaterial materialB, |
| | | 183 | | bool depthIsClamped = false) |
| | | 184 | | { |
| | 1 | 185 | | AddContactCore( |
| | 1 | 186 | | ContactAnchor.FromWorldPoint(pointA), |
| | 1 | 187 | | ContactAnchor.FromWorldPoint(pointB), |
| | 1 | 188 | | depth, |
| | 1 | 189 | | normal, |
| | 1 | 190 | | hasMaterialOverride: true, |
| | 1 | 191 | | materialA, |
| | 1 | 192 | | materialB, |
| | 1 | 193 | | depthIsClamped, |
| | 1 | 194 | | featureNamespaceA: 0, |
| | 1 | 195 | | featureNamespaceB: 0); |
| | 1 | 196 | | } |
| | | 197 | | |
| | | 198 | | internal void AddContact( |
| | | 199 | | ContactAnchor anchorA, |
| | | 200 | | ContactAnchor anchorB, |
| | | 201 | | Fixed64 depth, |
| | | 202 | | Vector3d normal, |
| | | 203 | | PhysicsMaterial materialA, |
| | | 204 | | PhysicsMaterial materialB, |
| | | 205 | | bool depthIsClamped = false, |
| | | 206 | | int featureNamespaceA = 0, |
| | | 207 | | int featureNamespaceB = 0) |
| | | 208 | | { |
| | 19 | 209 | | AddContactCore( |
| | 19 | 210 | | anchorA, |
| | 19 | 211 | | anchorB, |
| | 19 | 212 | | depth, |
| | 19 | 213 | | normal, |
| | 19 | 214 | | hasMaterialOverride: true, |
| | 19 | 215 | | materialA, |
| | 19 | 216 | | materialB, |
| | 19 | 217 | | depthIsClamped, |
| | 19 | 218 | | featureNamespaceA, |
| | 19 | 219 | | featureNamespaceB); |
| | 19 | 220 | | } |
| | | 221 | | |
| | | 222 | | private void AddContactCore( |
| | | 223 | | ContactAnchor anchorA, |
| | | 224 | | ContactAnchor anchorB, |
| | | 225 | | Fixed64 depth, |
| | | 226 | | Vector3d normal, |
| | | 227 | | bool hasMaterialOverride, |
| | | 228 | | PhysicsMaterial materialA, |
| | | 229 | | PhysicsMaterial materialB, |
| | | 230 | | bool depthIsClamped, |
| | | 231 | | int featureNamespaceA, |
| | | 232 | | int featureNamespaceB) |
| | | 233 | | { |
| | 30380 | 234 | | ulong contactId = CreateContactId( |
| | 30380 | 235 | | anchorA, |
| | 30380 | 236 | | featureNamespaceA, |
| | 30380 | 237 | | anchorB, |
| | 30380 | 238 | | featureNamespaceB); |
| | 30380 | 239 | | var contact = new ManifoldContact( |
| | 30380 | 240 | | contactId, |
| | 30380 | 241 | | anchorA, |
| | 30380 | 242 | | anchorB, |
| | 30380 | 243 | | depth, |
| | 30380 | 244 | | normal, |
| | 30380 | 245 | | hasMaterialOverride, |
| | 30380 | 246 | | materialA, |
| | 30380 | 247 | | materialB, |
| | 30380 | 248 | | depthIsClamped, |
| | 30380 | 249 | | featureNamespaceA, |
| | 30380 | 250 | | featureNamespaceB); |
| | | 251 | | |
| | 212098 | 252 | | for (int i = 0; i < _count; i++) |
| | | 253 | | { |
| | 76959 | 254 | | ManifoldContact existing = GetContactUnchecked(i); |
| | 76959 | 255 | | if (existing.ContactId != contactId) |
| | | 256 | | continue; |
| | | 257 | | |
| | 1290 | 258 | | if (IsDeeper(contact, existing)) |
| | 3 | 259 | | SetContactUnchecked(i, contact); |
| | 1290 | 260 | | SortContactsById(); |
| | 1290 | 261 | | return; |
| | | 262 | | } |
| | | 263 | | |
| | 29090 | 264 | | if (_count < MaxContactCount) |
| | | 265 | | { |
| | 12682 | 266 | | SetContactUnchecked(_count, contact); |
| | 12682 | 267 | | _count++; |
| | 12682 | 268 | | SortContactsById(); |
| | 12682 | 269 | | return; |
| | | 270 | | } |
| | | 271 | | |
| | 16408 | 272 | | int replaceIndex = FindShallowestReplacementIndex(contact); |
| | 16408 | 273 | | if (replaceIndex < 0) |
| | 12382 | 274 | | return; |
| | | 275 | | |
| | 4026 | 276 | | SetContactUnchecked(replaceIndex, contact); |
| | 4026 | 277 | | SortContactsById(); |
| | 4026 | 278 | | } |
| | | 279 | | |
| | | 280 | | /// <summary>Returns an allocation-free enumerator over the active contacts.</summary> |
| | 51 | 281 | | public Enumerator GetEnumerator() => new(this); |
| | | 282 | | |
| | 50 | 283 | | IEnumerator<ManifoldContact> IEnumerable<ManifoldContact>.GetEnumerator() => GetEnumerator(); |
| | | 284 | | |
| | 1 | 285 | | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
| | | 286 | | |
| | | 287 | | private int FindShallowestReplacementIndex(ManifoldContact candidate) |
| | | 288 | | { |
| | 16408 | 289 | | int replaceIndex = 0; |
| | 16408 | 290 | | ManifoldContact shallowest = _contact0; |
| | | 291 | | |
| | 131264 | 292 | | for (int i = 1; i < _count; i++) |
| | | 293 | | { |
| | 49224 | 294 | | ManifoldContact contact = GetContactUnchecked(i); |
| | 49224 | 295 | | if (contact.Depth <= shallowest.Depth) |
| | | 296 | | { |
| | 49211 | 297 | | shallowest = contact; |
| | 49211 | 298 | | replaceIndex = i; |
| | | 299 | | } |
| | | 300 | | } |
| | | 301 | | |
| | 16408 | 302 | | if (IsDeeper(candidate, shallowest)) |
| | 6 | 303 | | return replaceIndex; |
| | | 304 | | |
| | 16402 | 305 | | if (HasEqualDepth(candidate, shallowest) && candidate.ContactId < shallowest.ContactId) |
| | 4020 | 306 | | return replaceIndex; |
| | | 307 | | |
| | 12382 | 308 | | return -1; |
| | | 309 | | } |
| | | 310 | | |
| | | 311 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 312 | | private static bool IsDeeper(ManifoldContact candidate, ManifoldContact existing) => |
| | 17698 | 313 | | candidate.Depth > existing.Depth |
| | 17698 | 314 | | || candidate.Depth == existing.Depth |
| | 17698 | 315 | | && candidate.DepthIsClamped |
| | 17698 | 316 | | && !existing.DepthIsClamped; |
| | | 317 | | |
| | | 318 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 319 | | private static bool HasEqualDepth(ManifoldContact left, ManifoldContact right) => |
| | 16402 | 320 | | left.Depth == right.Depth |
| | 16402 | 321 | | && left.DepthIsClamped == right.DepthIsClamped; |
| | | 322 | | |
| | | 323 | | private void SortContactsById() |
| | | 324 | | { |
| | 84484 | 325 | | for (int i = 1; i < _count; i++) |
| | | 326 | | { |
| | 24244 | 327 | | ManifoldContact contact = GetContactUnchecked(i); |
| | 24244 | 328 | | int j = i - 1; |
| | 34363 | 329 | | while (j >= 0 && GetContactUnchecked(j).ContactId > contact.ContactId) |
| | | 330 | | { |
| | 10119 | 331 | | SetContactUnchecked(j + 1, GetContactUnchecked(j)); |
| | 10119 | 332 | | j--; |
| | | 333 | | } |
| | | 334 | | |
| | 24244 | 335 | | SetContactUnchecked(j + 1, contact); |
| | | 336 | | } |
| | 17998 | 337 | | } |
| | | 338 | | |
| | | 339 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 340 | | private ManifoldContact GetContactUnchecked(int index) => |
| | 205530 | 341 | | index switch |
| | 205530 | 342 | | { |
| | 49972 | 343 | | 0 => _contact0, |
| | 62320 | 344 | | 1 => _contact1, |
| | 53808 | 345 | | 2 => _contact2, |
| | 39430 | 346 | | _ => _contact3 |
| | 205530 | 347 | | }; |
| | | 348 | | |
| | | 349 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 350 | | private void SetContactUnchecked(int index, ManifoldContact contact) |
| | | 351 | | { |
| | | 352 | | switch (index) |
| | | 353 | | { |
| | | 354 | | case 0: |
| | 10482 | 355 | | _contact0 = contact; |
| | 10482 | 356 | | break; |
| | | 357 | | case 1: |
| | 14478 | 358 | | _contact1 = contact; |
| | 14478 | 359 | | break; |
| | | 360 | | case 2: |
| | 14163 | 361 | | _contact2 = contact; |
| | 14163 | 362 | | break; |
| | | 363 | | default: |
| | 11951 | 364 | | _contact3 = contact; |
| | | 365 | | break; |
| | | 366 | | } |
| | 11951 | 367 | | } |
| | | 368 | | |
| | | 369 | | private static ulong CreateContactId( |
| | | 370 | | ContactAnchor anchorA, |
| | | 371 | | int featureNamespaceA, |
| | | 372 | | ContactAnchor anchorB, |
| | | 373 | | int featureNamespaceB) |
| | | 374 | | { |
| | 30380 | 375 | | if (CompareLocalFeature( |
| | 30380 | 376 | | featureNamespaceB, |
| | 30380 | 377 | | anchorB, |
| | 30380 | 378 | | featureNamespaceA, |
| | 30380 | 379 | | anchorA) < 0) |
| | | 380 | | { |
| | 27632 | 381 | | (anchorA, anchorB) = (anchorB, anchorA); |
| | 27632 | 382 | | (featureNamespaceA, featureNamespaceB) = |
| | 27632 | 383 | | (featureNamespaceB, featureNamespaceA); |
| | | 384 | | } |
| | | 385 | | |
| | 30380 | 386 | | ulong hash = 14695981039346656037UL; |
| | 30380 | 387 | | Mix(ref hash, featureNamespaceA); |
| | 30380 | 388 | | MixLocalFeature(ref hash, anchorA); |
| | 30380 | 389 | | Mix(ref hash, featureNamespaceB); |
| | 30380 | 390 | | MixLocalFeature(ref hash, anchorB); |
| | 30380 | 391 | | return hash; |
| | | 392 | | } |
| | | 393 | | |
| | | 394 | | private static int CompareLocalFeature( |
| | | 395 | | int leftNamespace, |
| | | 396 | | ContactAnchor left, |
| | | 397 | | int rightNamespace, |
| | | 398 | | ContactAnchor right) |
| | | 399 | | { |
| | 30380 | 400 | | int comparison = leftNamespace.CompareTo(rightNamespace); |
| | 30380 | 401 | | return comparison != 0 |
| | 30380 | 402 | | ? comparison |
| | 30380 | 403 | | : left.CompareLocalFeature(right); |
| | | 404 | | } |
| | | 405 | | |
| | | 406 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 407 | | private static void Mix(ref ulong hash, long value) |
| | | 408 | | { |
| | | 409 | | unchecked |
| | | 410 | | { |
| | 121520 | 411 | | hash ^= (ulong)value; |
| | 121520 | 412 | | hash *= 1099511628211UL; |
| | | 413 | | } |
| | 121520 | 414 | | } |
| | | 415 | | |
| | | 416 | | private static void MixLocalFeature( |
| | | 417 | | ref ulong hash, |
| | | 418 | | ContactAnchor anchor) |
| | | 419 | | { |
| | 60760 | 420 | | Mix( |
| | 60760 | 421 | | ref hash, |
| | 60760 | 422 | | unchecked((long)anchor.GetLocalFeatureHash64())); |
| | 60760 | 423 | | } |
| | | 424 | | |
| | | 425 | | /// <summary>Enumerates the active contacts in deterministic order.</summary> |
| | | 426 | | public struct Enumerator : IEnumerator<ManifoldContact> |
| | | 427 | | { |
| | | 428 | | private readonly ContactManifold _manifold; |
| | | 429 | | private int _index; |
| | | 430 | | |
| | | 431 | | internal Enumerator(ContactManifold manifold) |
| | | 432 | | { |
| | 51 | 433 | | _manifold = manifold; |
| | 51 | 434 | | _index = -1; |
| | 51 | 435 | | } |
| | | 436 | | |
| | | 437 | | /// <summary>Gets the current contact.</summary> |
| | 177 | 438 | | public ManifoldContact Current => _manifold[_index]; |
| | | 439 | | |
| | 3 | 440 | | object IEnumerator.Current => Current; |
| | | 441 | | |
| | | 442 | | /// <summary>Advances to the next active contact.</summary> |
| | | 443 | | public bool MoveNext() |
| | | 444 | | { |
| | 227 | 445 | | int next = _index + 1; |
| | 227 | 446 | | if (next >= _manifold._count) |
| | 50 | 447 | | return false; |
| | | 448 | | |
| | 177 | 449 | | _index = next; |
| | 177 | 450 | | return true; |
| | | 451 | | } |
| | | 452 | | |
| | | 453 | | /// <summary>Resets the enumerator to its initial position.</summary> |
| | 1 | 454 | | public void Reset() => _index = -1; |
| | | 455 | | |
| | | 456 | | /// <summary>Releases enumerator resources.</summary> |
| | 50 | 457 | | public void Dispose() { } |
| | | 458 | | } |
| | | 459 | | } |