Fill out cases for the shape switch.

Circle-Box collisions!

Circle Collisions!

PhysShapeCircle!
This commit is contained in:
Acruid
2020-07-23 22:34:36 -07:00
parent 2b692d596e
commit 3fd1a2f3fb
7 changed files with 734 additions and 1 deletions

View File

@@ -155,10 +155,17 @@ namespace Robust.Client.Debugging
_handle.DrawRect(box, color);
}
public override void DrawCircle(Vector2 origin, float radius, in Color color)
{
_handle.DrawCircle(origin, radius, color);
}
public override void SetTransform(in Matrix3 transform)
{
_handle.SetTransform(transform);
}
}
}

View File

@@ -326,7 +326,30 @@ namespace Robust.Client.Graphics.Clyde
public override void DrawCircle(Vector2 position, float radius, Color color, bool filled = true)
{
// TODO: Implement this.
//TODO: Scale number of sides based on radius
const int Divisions = 8;
const float ArcLength = MathF.PI * 2 / Divisions;
var filledTriangle = new Vector2[3];
// Draws a "circle", but its just a polygon with a bunch of sides
// this is the GL_LINES version, not GL_LINE_STRIP
for (int i = 0; i < Divisions; i++)
{
var startPos = new Vector2(MathF.Cos(ArcLength * i) * radius, MathF.Sin(ArcLength * i) * radius);
var endPos = new Vector2(MathF.Cos(ArcLength * (i+1)) * radius, MathF.Sin(ArcLength * (i + 1)) * radius);
if(!filled)
_renderHandle.DrawLine(startPos, endPos, color);
else
{
filledTriangle[0] = startPos;
filledTriangle[1] = endPos;
filledTriangle[2] = Vector2.Zero;
_renderHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, filledTriangle, color);
}
}
}
public override void DrawLine(Vector2 from, Vector2 to, Color color)

View File

@@ -321,5 +321,17 @@ namespace Robust.Shared.Maths
MathF.Max(x, Right),
MathF.Max(y, Top));
}
/// <summary>
/// Given a point, returns the closest point to it inside the box.
/// </summary>
public Vector2 ClosestPoint(in Vector2 position)
{
// clamp the point to the border of the box
var cx = MathF.Clamp(position.X, Left, Right);
var cy = MathF.Clamp(position.Y, Bottom, Top);
return new Vector2(cx, cy);
}
}
}

View File

@@ -0,0 +1,494 @@
using System;
using Robust.Shared.Interfaces.Physics;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Math = CannyFastMath.Math;
using MathF = CannyFastMath.MathF;
namespace Robust.Shared.Physics
{
internal static class CollisionSolver
{
public static void CalculateFeatures(Manifold manifold, IPhysShape a, IPhysShape b, out CollisionFeatures features)
{
// 2D table of all possible PhysShape combinations
switch (a)
{
case PhysShapeCircle aCircle:
switch (b)
{
case PhysShapeCircle bCircle:
CircleCircle(manifold, aCircle, bCircle, false, out features);
return;
case PhysShapeAabb bAabb:
CircleBBox(manifold, aCircle, bAabb, false, out features);
return;
case PhysShapeRect bRect:
RectCircle(manifold, bRect, aCircle, true, out features);
return;
case PhysShapeGrid bGrid:
features = default;
return;
}
break;
case PhysShapeAabb aAabb:
switch (b)
{
case PhysShapeCircle bCircle:
features = default;
return;
case PhysShapeAabb bAabb:
features = default;
return;
case PhysShapeRect bRect:
features = default;
return;
case PhysShapeGrid bGrid:
features = default;
return;
}
break;
case PhysShapeRect aRect:
switch (b)
{
case PhysShapeCircle bCircle:
features = default;
return;
case PhysShapeAabb bAabb:
features = default;
return;
case PhysShapeRect bRect:
features = default;
return;
case PhysShapeGrid bGrid:
features = default;
return;
}
break;
case PhysShapeGrid aGrid:
switch (b)
{
case PhysShapeCircle bCircle:
features = default;
return;
case PhysShapeAabb bAabb:
features = default;
return;
case PhysShapeRect bRect:
features = default;
return;
case PhysShapeGrid bGrid:
features = default;
return;
}
break;
}
features = default;
}
private static void CircleCircle(Manifold manifold, PhysShapeCircle a, PhysShapeCircle b, bool flip, out CollisionFeatures features)
{
var aRad = a.Radius;
var bRad = b.Radius;
var aPos = manifold.A.Entity.Transform.WorldPosition;
var bPos = manifold.B.Entity.Transform.WorldPosition;
CalculateCollisionFeatures(new Circle(aPos, aRad), new Circle(bPos, bRad), false, out features);
}
private static void CircleBBox(Manifold manifold, PhysShapeCircle a, PhysShapeAabb b, bool flip, out CollisionFeatures features)
{
var aRad = a.Radius;
var aPos = manifold.A.Entity.Transform.WorldPosition;
var bBox = b.LocalBounds.Translated(manifold.B.Entity.Transform.WorldPosition);
CalculateCollisionFeatures(in bBox, new Circle(aPos, aRad), flip, out features);
}
private static void RectCircle(Manifold manifold, PhysShapeRect a, PhysShapeCircle b, bool flip, out CollisionFeatures features)
{
var aPos = manifold.A.Entity.Transform.WorldPosition;
var bPos = manifold.B.Entity.Transform.WorldPosition;
var bRot = (float)manifold.B.Entity.Transform.WorldRotation.Theta;
CalculateCollisionFeatures(new OrientedRectangle(aPos, a.Rectangle, bRot), new Circle(bPos, b.Radius), flip, out features);
}
public static void CalculateCollisionFeatures(in Circle A, in Circle B, bool flip, out CollisionFeatures features)
{
var aRad = A.Radius;
var bRad = B.Radius;
var aPos = A.Position;
var bPos = B.Position;
// combined radius
var radiiSum = aRad + bRad;
// distance between circles
var dist = bPos - aPos;
// if the distance between two circles is larger than their combined radii,
// they are not colliding, otherwise they are
if (dist.LengthSquared > radiiSum * radiiSum)
{
features = default;
return;
}
// if dist between circles is zero, the circles are concentric, this collision cannot be resolved
if (dist.LengthSquared.Equals(0f))
{
features = default;
return;
}
// generate collision normal
var normal = dist.Normalized;
// half of the total
var penetraction = (radiiSum - dist.Length) * 0.5f;
var contacts = new Vector2[1];
// dtp - Distance to intersection point
var dtp = aRad - penetraction;
var contact = aPos + normal * dtp;
contacts[0] = contact;
features = new CollisionFeatures(true, normal, penetraction, contacts);
}
public static void CalculateCollisionFeatures(in Box2 A, in Circle B, bool flip, out CollisionFeatures features)
{
// closest point inside the rectangle to the center of the sphere.
var closestPoint = A.ClosestPoint(in B.Position);
// If the point is outside the sphere, the sphere and OBB do not intersect.
var distanceSq = (closestPoint - B.Position).LengthSquared;
if (distanceSq > B.Radius * B.Radius)
{
features = default;
return;
}
Vector2 normal;
if (distanceSq.Equals(0.0f))
{
var mSq = (closestPoint - A.Center).LengthSquared;
if (mSq.Equals(0.0f))
{
features = default;
return;
}
// Closest point is at the center of the sphere
normal = (closestPoint - A.Center).Normalized;
}
else
normal = (B.Position - closestPoint).Normalized;
var outsidePoint = B.Position - normal * B.Radius;
var distance = (closestPoint - outsidePoint).Length;
var contacts = new Vector2[1];
contacts[0] = closestPoint + (outsidePoint - closestPoint) * 0.5f;
var depth = distance * 0.5f;
features = new CollisionFeatures(true, normal, depth, contacts);
}
public static void CalculateCollisionFeatures(in OrientedRectangle A, in Circle B, bool flip, out CollisionFeatures features)
{
// closest point inside the rectangle to the center of the sphere.
var closestPoint = A.ClosestPointWorld(B.Position);
// If the point is outside the sphere, the sphere and OBB do not intersect.
var distanceSq = (closestPoint - B.Position).LengthSquared;
if (distanceSq > B.Radius * B.Radius)
{
features = default;
return;
}
Vector2 normal;
if (distanceSq.Equals(0.0f))
{
var mSq = (closestPoint - A.Center).LengthSquared;
if (mSq.Equals(0.0f))
{
features = default;
return;
}
// Closest point is at the center of the sphere
normal = (closestPoint - A.Center).Normalized;
}
else
normal = (B.Position - closestPoint).Normalized;
var outsidePoint = B.Position - normal * B.Radius;
var distance = (closestPoint - outsidePoint).Length;
var contacts = new Vector2[1];
contacts[0] = closestPoint + (outsidePoint - closestPoint) * 0.5f;
var depth = distance * 0.5f;
features = new CollisionFeatures(true, normal, depth, contacts);
}
}
/// <summary>
/// Features of the collision.
/// </summary>
internal readonly struct CollisionFeatures
{
/// <summary>
/// Are the two shapes *actually* colliding? If this is false, the rest of the
/// values in this struct are default.
/// </summary>
public readonly bool Collided;
/// <summary>
/// Collision normal. If A moves in the negative direction of the normal and
/// B moves in the positive direction, the objects will no longer intersect.
/// </summary>
public readonly Vector2 Normal;
/// <summary>
/// Half of the total length of penetration. Each object needs to move
/// by the penetration distance along the normal to resolve the collision.
/// </summary>
public readonly float Penetration;
/// <summary>
/// all the points at which the two objects collide, projected onto a plane.The plane
/// these points are projected onto has the normal of the collision normal and is
/// located halfway between the colliding objects.
/// </summary>
/// <remarks>
/// Circle-Circle collision only generates one contact.
/// </remarks>
public readonly Vector2[] Contacts;
/// <summary>
/// Constructs a new instance of <see cref="CollisionFeatures"/>.
/// </summary>
public CollisionFeatures(bool collided, Vector2 normal, float penetration, Vector2[] contacts)
{
Collided = collided;
Normal = normal;
Penetration = penetration;
Contacts = contacts;
}
}
/// <summary>
/// A rectangle that can be rotated.
/// </summary>
[Serializable]
internal readonly struct OrientedRectangle : IEquatable<OrientedRectangle>
{
/// <summary>
/// Center point of the rectangle in world space.
/// </summary>
public readonly Vector2 Center;
/// <summary>
/// Half of the total width and height of the rectangle.
/// </summary>
public readonly Vector2 HalfExtents;
/// <summary>
/// World rotation of the rectangle in radians.
/// </summary>
public readonly float Rotation;
/// <summary>
/// A 1x1 unit box with the origin centered and identity rotation.
/// </summary>
public static readonly OrientedRectangle UnitCentered = new OrientedRectangle(Vector2.Zero, Vector2.One, 0);
public OrientedRectangle(Box2 worldBox)
{
Center = worldBox.Center;
var hWidth = MathF.Abs(worldBox.Right - worldBox.Left) * 0.5f;
var hHeight = MathF.Abs(worldBox.Bottom - worldBox.Top) * 0.5f;
HalfExtents = new Vector2(hWidth, hHeight);
Rotation = 0;
}
public OrientedRectangle(Vector2 halfExtents)
{
Center = default;
HalfExtents = halfExtents;
Rotation = default;
}
public OrientedRectangle(Vector2 center, Vector2 halfExtents)
{
Center = center;
HalfExtents = halfExtents;
Rotation = default;
}
public OrientedRectangle(Vector2 center, Vector2 halfExtents, float rotation)
{
Center = center;
HalfExtents = halfExtents;
Rotation = rotation;
}
public OrientedRectangle(in Vector2 center, in Box2 localBox, float rotation)
{
Center = center;
HalfExtents = new Vector2(localBox.Width / 2, localBox.Height / 2);
Rotation = rotation;
}
/// <summary>
/// calculates the smallest AABB that will encompass this rectangle. The AABB is in local space.
/// </summary>
public Box2 CalcBoundingBox()
{
var Fi = Rotation;
var CX = Center.X;
var CY = Center.Y;
var WX = HalfExtents.X;
var WY = HalfExtents.Y;
var SF = MathF.Sin(Fi);
var CF = MathF.Cos(Fi);
var NH = MathF.Abs(WX * SF) + MathF.Abs(WY * CF); //boundrect half-height
var NW = MathF.Abs(WX * CF) + MathF.Abs(WY * SF); //boundrect half-width
return new Box2((float)(CX - NW), (float)(CY - NH), (float)(CX + NW), (float)(CY + NH)); //draw bound rectangle
}
/// <summary>
/// Tests if a point is contained inside this rectangle.
/// </summary>
/// <param name="point">Point to test.</param>
/// <returns>True if the point is contained inside this rectangle.</returns>
public bool Contains(Vector2 point)
{
// rotate around rectangle center by -rectAngle
var s = MathF.Sin(-Rotation);
var c = MathF.Cos(-Rotation);
// set origin to rect center
var newPoint = point - Center;
// rotate
newPoint = new Vector2(newPoint.X * c - newPoint.Y * s, newPoint.X * s + newPoint.Y * c);
// put origin back
newPoint += Center;
// check if our transformed point is in the rectangle, which is no longer
// rotated relative to the point
var xMin = -HalfExtents.X;
var xMax = HalfExtents.X;
var yMin = -HalfExtents.Y;
var yMax = HalfExtents.Y;
return newPoint.X >= xMin && newPoint.X <= xMax && newPoint.Y >= yMin && newPoint.Y <= yMax;
}
/// <summary>
/// Returns the closest point inside the rectangle to the given point in world space.
/// </summary>
public Vector2 ClosestPointWorld(Vector2 worldPoint)
{
// inverse-transform the sphere's center into the box's local space.
var localPoint = InverseTransformPoint(worldPoint);
var xMin = -HalfExtents.X;
var xMax = HalfExtents.X;
var yMin = -HalfExtents.Y;
var yMax = HalfExtents.Y;
// clamp the point to the border of the box
var cx = MathF.Clamp(localPoint.X, xMin, xMax);
var cy = MathF.Clamp(localPoint.Y, yMin, yMax);
return TransformPoint(new Vector2(cx, cy));
}
/// <summary>
/// Transforms a point from the rectangle's local space to world space.
/// </summary>
public Vector2 TransformPoint(Vector2 localPoint)
{
var theta = Rotation;
var (x, y) = localPoint;
var dx = MathF.Cos(theta) * x - MathF.Sin(theta) * y;
var dy = MathF.Sin(theta) * x + MathF.Cos(theta) * y;
return new Vector2(dx, dy) + Center;
}
/// <summary>
/// Transforms a point from world space to the rectangle's local space.
/// </summary>
public Vector2 InverseTransformPoint(Vector2 worldPoint)
{
var theta = -Rotation;
var (x, y) = worldPoint + -Center;
var dx = MathF.Cos(theta) * x - MathF.Sin(theta) * y;
var dy = MathF.Sin(theta) * x + MathF.Cos(theta) * y;
return new Vector2(dx, dy);
}
#region Equality
/// <inheritdoc />
public bool Equals(OrientedRectangle other)
{
return Center.Equals(other.Center) && HalfExtents.Equals(other.HalfExtents) && Rotation.Equals(other.Rotation);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is OrientedRectangle other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Center, HalfExtents, Rotation);
}
/// <summary>
/// Check for equality by value between two <see cref="OrientedRectangle"/>.
/// </summary>
public static bool operator ==(OrientedRectangle left, OrientedRectangle right) {
return left.Equals(right);
}
/// <summary>
/// Check for inequality by value between two <see cref="OrientedRectangle"/>.
/// </summary>
public static bool operator !=(OrientedRectangle left, OrientedRectangle right) {
return !left.Equals(right);
}
#endregion
/// <summary>
/// Returns the string representation of this object.
/// </summary>
public override string ToString()
{
var box = new Box2(-HalfExtents.X, -HalfExtents.Y, HalfExtents.X, HalfExtents.Y).Translated(Center);
return $"{box}, {Rotation}";
}
}
}

View File

@@ -12,6 +12,7 @@ namespace Robust.Shared.Physics
public abstract Color WakeMixColor { get; }
public abstract void DrawRect(in Box2 box, in Color color);
public abstract void DrawCircle(Vector2 origin, float radius, in Color color);
public abstract void SetTransform(in Matrix3 transform);
public abstract Color CalcWakeColor(Color color, float wakePercent);

View File

@@ -0,0 +1,77 @@
using System;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Physics
{
[Serializable, NetSerializable]
internal class PhysShapeCircle : IPhysShape
{
private const float DefaultRadius = 0.5f;
private int _collisionLayer;
private int _collisionMask;
private float _radius = DefaultRadius;
/// <inheritdoc />
[field: NonSerialized]
public event Action? OnDataChanged;
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public int CollisionLayer
{
get => _collisionLayer;
set
{
_collisionLayer = value;
OnDataChanged?.Invoke();
}
}
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public int CollisionMask
{
get => _collisionMask;
set
{
_collisionMask = value;
OnDataChanged?.Invoke();
}
}
/// <summary>
/// The radius of this circle.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float Radius
{
get => _radius;
set => _radius = value;
}
/// <inheritdoc />
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref _collisionLayer, "layer", 0, WithFormat.Flags<CollisionLayer>());
serializer.DataField(ref _collisionMask, "mask", 0, WithFormat.Flags<CollisionMask>());
serializer.DataField(ref _radius, "radius", DefaultRadius);
}
public Box2 CalculateLocalBounds(Angle rotation)
{
return new Box2(-_radius, -_radius, _radius, _radius);
}
public void ApplyState() { }
public void DebugDraw(DebugDrawingHandle handle, in Matrix3 modelMatrix, in Box2 worldViewport, float sleepPercent)
{
handle.SetTransform(in modelMatrix);
handle.DrawCircle(Vector2.Zero, _radius, handle.CalcWakeColor(handle.RectFillColor, sleepPercent));
handle.SetTransform(in Matrix3.Identity);
}
}
}

View File

@@ -0,0 +1,119 @@
using System;
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Robust.UnitTesting.Shared.Physics
{
[TestFixture, Parallelizable, TestOf(typeof(CollisionSolver))]
class CollisionSolverTests
{
[Test]
public void CircleCircle_NotCollide()
{
var a = new Circle(new Vector2(0,0), 0.5f);
var b = new Circle(new Vector2(1, 1), 0.5f);
CollisionSolver.CalculateCollisionFeatures(in a, in b, false, out var results);
Assert.AreEqual(false, results.Collided);
}
[Test]
public void CircleCircle_Collide()
{
var a = new Circle(new Vector2(0, 0), 1f);
var b = new Circle(new Vector2(1.5f, 0), 1f);
CollisionSolver.CalculateCollisionFeatures(in a, in b, false, out var results);
Assert.AreEqual(true, results.Collided);
Assert.AreEqual(Vector2.UnitX, results.Normal);
Assert.AreEqual(0.5f / 2, results.Penetration);
Assert.IsNotNull(results.Contacts);
Assert.AreEqual(1, results.Contacts.Length);
Assert.AreEqual(new Vector2(0.75f, 0), results.Contacts[0]);
}
[Test]
public void CircleCircle_SamePos()
{
var circle = new Circle(new Vector2(0, 0), 0.5f);
CollisionSolver.CalculateCollisionFeatures(in circle, in circle, false, out var results);
Assert.AreEqual(false, results.Collided);
}
[Test]
public void InverseTransformPoint()
{
var obb = new OrientedRectangle(Vector2.One, Vector2.Zero, MathF.PI / 2);
var worldPoint = new Vector2(1, 3);
var localPoint = obb.InverseTransformPoint(worldPoint);
Assert.That(localPoint, Is.Approximately(new Vector2(2, 0)));
}
[Test]
public void TransformPoint()
{
var obb = new OrientedRectangle(Vector2.One, Vector2.Zero, MathF.PI / 2);
var worldPoint = new Vector2(2, 0);
var localPoint = obb.TransformPoint(worldPoint);
Assert.That(localPoint, Is.Approximately(new Vector2(1, 3)));
}
[Test]
public void TransformPointRoundTrip()
{
var obb = new OrientedRectangle(new Vector2(3, 5), Vector2.Zero, MathF.PI / 4);
var worldPoint = new Vector2(11, 13);
var localPoint = obb.InverseTransformPoint(worldPoint);
var result = obb.TransformPoint(localPoint);
Assert.AreEqual(worldPoint, result);
}
[Test]
public void ClosestPoint()
{
var obb = new OrientedRectangle(Vector2.One, Vector2.One * 0.5f, MathF.PI / 2);
var worldPoint = new Vector2(13, -13);
var closestPoint = obb.ClosestPointWorld(worldPoint);
Assert.That(closestPoint, Is.Approximately(new Vector2(1.5f, 0.5f)));
}
[Test]
public void ORectCircle_NotCollide()
{
var a = new OrientedRectangle(Vector2.Zero, Vector2.One * 0.5f, MathF.PI / 4);
var b = new Circle(new Vector2(1, 1), 0.5f);
CollisionSolver.CalculateCollisionFeatures(in a, in b, false, out var results);
Assert.AreEqual(false, results.Collided);
}
[Test]
public void ORectCircle_Collide()
{
var a = new OrientedRectangle(Vector2.Zero, new Vector2(3, 1), MathF.PI / 2);
var b = new Circle(new Vector2(1.5f, 0), 1f);
CollisionSolver.CalculateCollisionFeatures(in a, in b, false, out var results);
Assert.AreEqual(true, results.Collided);
Assert.That(results.Normal, Is.Approximately(Vector2.UnitX));
Assert.AreEqual(0.5f / 2, results.Penetration);
Assert.IsNotNull(results.Contacts);
Assert.AreEqual(1, results.Contacts.Length);
Assert.That(results.Contacts[0], Is.Approximately(new Vector2(0.75f, 0)));
}
}
}