Handle physics corner cases (literally)

This commit is contained in:
Drew DeVault 2016-07-04 20:12:48 -04:00
parent a2f7f62a67
commit e51d22cb52
10 changed files with 296 additions and 14 deletions

View File

@ -0,0 +1,62 @@
using System;
namespace TrueCraft.API
{
public struct BoundingCylinder : IEquatable<BoundingCylinder>
{
public Vector3 Min;
public Vector3 Max;
public double Radius;
public BoundingCylinder(Vector3 min, Vector3 max, double radius)
{
Min = min;
Max = max;
Radius = radius;
}
public bool Intersects(Vector3 q)
{
return DistancePointLine(q, Min, Max) < Radius;
}
public bool Intersects(BoundingBox q)
{
var corners = q.GetCorners();
for (int i = 0; i < corners.Length; i++)
{
if (Intersects(corners[i]))
return true;
}
return false;
}
// http://answers.unity3d.com/questions/62644/distance-between-a-ray-and-a-point.html
public static double DistancePointLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
return (ProjectPointLine(point, lineStart, lineEnd) - point).Distance;
}
public static Vector3 ProjectPointLine(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
var rhs = point - lineStart;
var vector2 = lineEnd - lineStart;
var magnitude = vector2.Distance;
var lhs = vector2;
if (magnitude > 1E-06f)
lhs = lhs / magnitude;
var num2 = Vector3.Dot(lhs, rhs);
if (num2 < 0) num2 = 0;
if (num2 > magnitude) num2 = magnitude;
return lineStart + (lhs * num2);
}
public bool Equals(BoundingCylinder other)
{
return other.Max == this.Max
&& other.Min == this.Min
&& other.Radius == this.Radius;
}
}
}

View File

@ -127,6 +127,7 @@
<Compile Include="Logic\IBurnableItem.cs" />
<Compile Include="Logic\SoundEffectClass.cs" />
<Compile Include="Matrix.cs" />
<Compile Include="BoundingCylinder.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup />

View File

@ -167,6 +167,42 @@ namespace TrueCraft.API
Math.Max(value1.Z, value2.Z)
);
}
/// <summary>
/// Calculates the dot product between two vectors.
/// </summary>
public static double Dot(Vector3 value1, Vector3 value2)
{
return value1.X * value2.X + value1.Y * value2.Y + value1.Z * value2.Z;
}
/// <summary>
/// Computes the cross product of two vectors.
/// </summary>
/// <param name="vector1">The first vector.</param>
/// <param name="vector2">The second vector.</param>
/// <returns>The cross product of two vectors.</returns>
public static Vector3 Cross(Vector3 vector1, Vector3 vector2)
{
Cross(ref vector1, ref vector2, out vector1);
return vector1;
}
/// <summary>
/// Computes the cross product of two vectors.
/// </summary>
/// <param name="vector1">The first vector.</param>
/// <param name="vector2">The second vector.</param>
/// <param name="result">The cross product of two vectors as an output parameter.</param>
public static void Cross(ref Vector3 vector1, ref Vector3 vector2, out Vector3 result)
{
var x = vector1.Y * vector2.Z - vector2.Y * vector1.Z;
var y = -(vector1.X * vector2.Z - vector2.X * vector1.Z);
var z = vector1.X * vector2.Y - vector2.X * vector1.Y;
result.X = x;
result.Y = y;
result.Z = z;
}
#endregion

View File

@ -216,7 +216,6 @@ namespace TrueCraft.Core.Test.Physics
}
[Test]
[Ignore("Fails until I rewrite the physics engine AGAIN")]
public void TestCornerCollision()
{
var repository = GetBlockRepository();
@ -233,8 +232,8 @@ namespace TrueCraft.Core.Test.Physics
// Test
physics.Update(TimeSpan.FromSeconds(1));
Assert.AreEqual(0, entity.Position.X);
Assert.AreEqual(0, entity.Position.Z);
Assert.AreEqual(-1, entity.Position.X);
Assert.AreEqual(-1, entity.Position.Z);
Assert.AreEqual(0, entity.Velocity.X);
Assert.AreEqual(0, entity.Velocity.Z);
}

View File

@ -0,0 +1,37 @@
using NUnit.Framework;
using System;
namespace TrueCraft.API.Test
{
[TestFixture]
public class TestBoundingCylinder
{
[Test]
public void TestIntersectsPoint()
{
// x
// /
// x
var cylinder = new BoundingCylinder(Vector3.Zero, Vector3.One, 1);
Assert.IsTrue(cylinder.Intersects(cylinder.Min));
Assert.IsTrue(cylinder.Intersects(cylinder.Max));
Assert.IsTrue(cylinder.Intersects(cylinder.Min + (Vector3.One / 2)));
Assert.IsTrue(cylinder.Intersects(cylinder.Max - (Vector3.One / 2)));
Assert.IsTrue(cylinder.Intersects(new Vector3(0.25, 0, 0)));
Assert.IsFalse(cylinder.Intersects(new Vector3(5, 5, 5)));
}
[Test]
public void TestIntersectsBox()
{
// x
// /
// x
var cylinder = new BoundingCylinder(Vector3.Zero, Vector3.One * 10, 3);
var doesNotIntersect = new BoundingBox(Vector3.One * 10 + 5, Vector3.One * 10 + 5);
Assert.IsFalse(cylinder.Intersects(doesNotIntersect));
var intersects = new BoundingBox(Vector3.Zero, Vector3.One);
Assert.IsTrue(cylinder.Intersects(intersects));
}
}
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{90712322-5904-4BCE-8606-C662706B8EAE}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>TrueCraft.API.Test</RootNamespace>
<AssemblyName>TrueCraft.API.Test</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="nunit.framework">
<HintPath>..\..\packages\NUnit.3.4.1\lib\net45\nunit.framework.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="TestBoundingCylinder.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\TrueCraft.API\TrueCraft.API.csproj">
<Project>{FEE55B54-91B0-4325-A2C3-D576C0B7A81F}</Project>
<Name>TrueCraft.API</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NUnit" version="3.4.1" targetFramework="net45" />
</packages>

View File

@ -70,15 +70,91 @@ namespace TrueCraft.Core.Physics
aabbEntity.TerrainCollision(collision, before.X < 0 ? Vector3.Left : Vector3.Right);
if (TestTerrainCollisionZ(aabbEntity, out collision))
aabbEntity.TerrainCollision(collision, before.Z < 0 ? Vector3.Backwards : Vector3.Forwards);
if (TestTerrainCollisionCylinder(aabbEntity, out collision))
aabbEntity.TerrainCollision(collision, before);
}
entity.EndUpdate(entity.Position + entity.Velocity);
TestTerrainCollisionY(aabbEntity, out collision);
}
}
}
}
private BoundingBox GetAABBVelocityBox(IAABBEntity entity)
{
var min = new Vector3(
Math.Min(entity.BoundingBox.Min.X, entity.BoundingBox.Min.X + entity.Velocity.X),
Math.Min(entity.BoundingBox.Min.Y, entity.BoundingBox.Min.Y + entity.Velocity.Y),
Math.Min(entity.BoundingBox.Min.Z, entity.BoundingBox.Min.Z + entity.Velocity.Z)
);
var max = new Vector3(
Math.Max(entity.BoundingBox.Max.X, entity.BoundingBox.Max.X + entity.Velocity.X),
Math.Max(entity.BoundingBox.Max.Y, entity.BoundingBox.Max.Y + entity.Velocity.Y),
Math.Max(entity.BoundingBox.Max.Z, entity.BoundingBox.Max.Z + entity.Velocity.Z)
);
return new BoundingBox(min, max);
}
private void AdjustVelocityForCollision(IAABBEntity entity, BoundingBox problem)
{
var velocity = entity.Velocity;
if (entity.Velocity.X < 0)
velocity.X = entity.BoundingBox.Min.X - problem.Max.X;
if (entity.Velocity.X > 0)
velocity.X = entity.BoundingBox.Max.X - problem.Min.X;
if (entity.Velocity.Y < 0)
velocity.Y = entity.BoundingBox.Min.Y - problem.Max.Y;
if (entity.Velocity.Y > 0)
velocity.Y = entity.BoundingBox.Max.Y - problem.Min.Y;
if (entity.Velocity.Z < 0)
velocity.Z = entity.BoundingBox.Min.Z - problem.Max.Z;
if (entity.Velocity.Z > 0)
velocity.Z = entity.BoundingBox.Max.Z - problem.Min.Z;
entity.Velocity = velocity;
}
public bool TestTerrainCollisionCylinder(IAABBEntity entity, out Vector3 collisionPoint)
{
collisionPoint = Vector3.Zero;
var testBox = GetAABBVelocityBox(entity);
var testCylinder = new BoundingCylinder(testBox.Min, testBox.Max,
entity.BoundingBox.Min.DistanceTo(entity.BoundingBox.Max));
bool collision = false;
for (int x = (int)(Math.Floor(testBox.Min.X)); x <= (int)(Math.Ceiling(testBox.Max.X)); x++)
{
for (int z = (int)(Math.Floor(testBox.Min.Z)); z <= (int)(Math.Ceiling(testBox.Max.Z)); z++)
{
for (int y = (int)(Math.Floor(testBox.Min.Y)); y <= (int)(Math.Ceiling(testBox.Max.Y)); y++)
{
var coords = new Coordinates3D(x, y, z);
if (!World.IsValidPosition(coords))
continue;
var _box = BlockPhysicsProvider.GetBoundingBox(World, coords);
if (_box == null)
continue;
var box = _box.Value.OffsetBy(coords);
if (testCylinder.Intersects(box))
{
if (testBox.Intersects(box))
{
collision = true;
AdjustVelocityForCollision(entity, box);
testBox = GetAABBVelocityBox(entity);
testCylinder = new BoundingCylinder(testBox.Min, testBox.Max,
entity.BoundingBox.Min.DistanceTo(entity.BoundingBox.Max));
collisionPoint = coords;
}
}
}
}
}
return collision;
}
public bool TestTerrainCollisionY(IAABBEntity entity, out Vector3 collisionPoint)
{
// Things we need to do:

View File

@ -371,16 +371,6 @@
<Name>TrueCraft.Profiling</Name>
</ProjectReference>
</ItemGroup>
<ProjectExtensions>
<MonoDevelop>
<Properties>
<Policies>
<TextStylePolicy inheritsSet="VisualStudio" inheritsScope="text/plain" scope="text/x-csharp" />
<CSharpFormattingPolicy inheritsSet="Mono" inheritsScope="text/x-csharp" scope="text/x-csharp" />
</Policies>
</Properties>
</MonoDevelop>
</ProjectExtensions>
<ItemGroup>
<Folder Include="Lighting\" />
<Folder Include="AI\" />

View File

@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCraft.Core.Test", "True
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCraft.Profiling", "TrueCraft.Profiling\TrueCraft.Profiling.csproj", "{BCA0E139-CF47-43B3-9DC9-D4611C0A2AAD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrueCraft.API.Test", "TrueCraft.Core.Test\TrueCraft.API.Test\TrueCraft.API.Test.csproj", "{90712322-5904-4BCE-8606-C662706B8EAE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -76,9 +78,16 @@ Global
{FEE55B54-91B0-4325-A2C3-D576C0B7A81F}.Optimized Debug|Any CPU.Build.0 = Optimized Debug|Any CPU
{FEE55B54-91B0-4325-A2C3-D576C0B7A81F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEE55B54-91B0-4325-A2C3-D576C0B7A81F}.Release|Any CPU.Build.0 = Release|Any CPU
{90712322-5904-4BCE-8606-C662706B8EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90712322-5904-4BCE-8606-C662706B8EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90712322-5904-4BCE-8606-C662706B8EAE}.Optimized Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90712322-5904-4BCE-8606-C662706B8EAE}.Optimized Debug|Any CPU.Build.0 = Debug|Any CPU
{90712322-5904-4BCE-8606-C662706B8EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90712322-5904-4BCE-8606-C662706B8EAE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BCFDCD93-C23E-49E6-9767-A887B3C2A709} = {6756B61E-5856-4CA7-90B5-6053763FE7BA}
{90712322-5904-4BCE-8606-C662706B8EAE} = {6756B61E-5856-4CA7-90B5-6053763FE7BA}
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
Policies = $0
@ -114,6 +123,26 @@ Global
$2.inheritsSet = Mono
$2.inheritsScope = text/x-csharp
$2.scope = text/x-csharp
$2.IndentSwitchSection = True
$2.NewLinesForBracesInProperties = True
$2.NewLinesForBracesInAccessors = True
$2.NewLinesForBracesInAnonymousMethods = True
$2.NewLinesForBracesInControlBlocks = True
$2.NewLinesForBracesInAnonymousTypes = True
$2.NewLinesForBracesInObjectCollectionArrayInitializers = True
$2.NewLinesForBracesInLambdaExpressionBody = True
$2.NewLineForElse = True
$2.NewLineForCatch = True
$2.NewLineForFinally = True
$2.NewLineForMembersInObjectInit = True
$2.NewLineForMembersInAnonymousTypes = True
$2.NewLineForClausesInQuery = True
$2.SpacingAfterMethodDeclarationName = False
$2.SpaceAfterMethodCallName = False
$2.SpaceBeforeOpenSquareBracket = False
$0.DotNetNamingPolicy = $3
$3.DirectoryNamespaceAssociation = PrefixedHierarchical
$3.ResourceNamePolicy = FileFormatDefault
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE