Skip to content
This repository was archived by the owner on Mar 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@

ゴール: オブジェクト同士の衝突を正確に検出できる

- [ ] SpatialHashGrid
- [ ] BVH広域フェーズ
- [ ] GJK+EPA狭域フェーズ
- [ ] Raycast, RaycastAll, SphereCast, OverlapSphere
- [x] SpatialHashGrid
- [x] BVH広域フェーズ
- [x] GJK+EPA狭域フェーズ
- [x] Raycast, RaycastAll, SphereCast, OverlapSphere

完動品としての価値: オブジェクト同士の衝突が検出される。レイキャストで任意のオブジェクトを特定できる。

Expand Down
173 changes: 173 additions & 0 deletions src/Seed.Engine.Tests/Physics/BroadPhase/BvhTreeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using System;
using Xunit;

using Seed.Engine.Ecs;
using Seed.Engine.Foundation.Collections;
using Seed.Engine.Foundation.Mathematics;
using Seed.Engine.Physics.BroadPhase;
using Seed.Engine.Physics.Collision;

namespace Seed.Engine.Tests.Physics.BroadPhase;

public class BvhTreeTests
{
[Fact]
public void Build_SingleElement_CreatesLeafNode()
{
// Given
var bvh = new BvhTree();
Span<AABB> bounds = [new AABB(Vector3.Zero, Vector3.One)];
Span<int> indices = [0];

// When
bvh.Build(bounds, indices);

// Then
Assert.Equal(1, bvh.NodeCount);

bvh.Dispose();
}

[Fact]
public void Build_MultipleElements_CreatesTree()
{
// Given
var bvh = new BvhTree();
Span<AABB> bounds =
[
new AABB(new Vector3(0f, 0f, 0f), new Vector3(1f, 1f, 1f)),
new AABB(new Vector3(5f, 0f, 0f), new Vector3(6f, 1f, 1f)),
new AABB(new Vector3(10f, 0f, 0f), new Vector3(11f, 1f, 1f)),
];
Span<int> indices = [0, 1, 2];

// When
bvh.Build(bounds, indices);

// Then: 3 leaves + internal nodes
Assert.True(bvh.NodeCount >= 3);

bvh.Dispose();
}

[Fact]
public void QueryAABB_Intersecting_ReturnsMatches()
{
// Given
var bvh = new BvhTree();
Span<AABB> bounds =
[
new AABB(new Vector3(0f, 0f, 0f), new Vector3(1f, 1f, 1f)),
new AABB(new Vector3(5f, 0f, 0f), new Vector3(6f, 1f, 1f)),
new AABB(new Vector3(10f, 0f, 0f), new Vector3(11f, 1f, 1f)),
];
Span<int> indices = [0, 1, 2];
bvh.Build(bounds, indices);

var queryBounds = new AABB(new Vector3(4f, 0f, 0f), new Vector3(7f, 1f, 1f));
var results = new NativeList<int>(8);

// When
bvh.QueryAABB(queryBounds, ref results);

// Then: only index 1 should match
Assert.True(results.Length >= 1);

results.Dispose();
bvh.Dispose();
}

[Fact]
public void QueryAABB_NonIntersecting_ReturnsEmpty()
{
// Given
var bvh = new BvhTree();
Span<AABB> bounds =
[
new AABB(new Vector3(0f, 0f, 0f), new Vector3(1f, 1f, 1f)),
];
Span<int> indices = [0];
bvh.Build(bounds, indices);

var queryBounds = new AABB(new Vector3(50f, 50f, 50f), new Vector3(51f, 51f, 51f));
var results = new NativeList<int>(8);

// When
bvh.QueryAABB(queryBounds, ref results);

// Then
Assert.Equal(0, results.Length);

results.Dispose();
bvh.Dispose();
}

[Fact]
public void QueryRay_HitsLeaf_ReturnsMatch()
{
// Given
var bvh = new BvhTree();
Span<AABB> bounds =
[
new AABB(new Vector3(4f, -1f, -1f), new Vector3(6f, 1f, 1f)),
];
Span<int> indices = [0];
bvh.Build(bounds, indices);

var ray = new Ray(Vector3.Zero, Vector3.UnitX);
var results = new NativeList<int>(8);

// When
bvh.QueryRay(ray, 100f, ref results);

// Then
Assert.Equal(1, results.Length);
Assert.Equal(0, results[0]);

results.Dispose();
bvh.Dispose();
}

[Fact]
public void FindOverlappingPairs_OverlappingLeaves_ReturnsPair()
{
// Given
var bvh = new BvhTree();
Span<AABB> bounds =
[
new AABB(new Vector3(0f, 0f, 0f), new Vector3(2f, 2f, 2f)),
new AABB(new Vector3(1f, 0f, 0f), new Vector3(3f, 2f, 2f)),
];
Span<int> indices = [0, 1];
bvh.Build(bounds, indices);

Entity e0 = new Entity(0, 1);
Entity e1 = new Entity(1, 1);
ReadOnlySpan<Entity> entities = [e0, e1];
var pairs = new NativeList<CollisionPair>(8);

// When
bvh.FindOverlappingPairs(ref pairs, entities);

// Then
Assert.Equal(1, pairs.Length);

pairs.Dispose();
bvh.Dispose();
}

[Fact]
public void Build_Empty_HandlesGracefully()
{
// Given
var bvh = new BvhTree();

// When
bvh.Build(ReadOnlySpan<AABB>.Empty, ReadOnlySpan<int>.Empty);

// Then
Assert.Equal(0, bvh.NodeCount);

bvh.Dispose();
}
}
89 changes: 89 additions & 0 deletions src/Seed.Engine.Tests/Physics/BroadPhase/SpatialHashGridTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Xunit;

using Seed.Engine.Foundation.Collections;
using Seed.Engine.Foundation.Mathematics;
using Seed.Engine.Physics.BroadPhase;

namespace Seed.Engine.Tests.Physics.BroadPhase;

public class SpatialHashGridTests
{
[Fact]
public void Insert_And_QuerySphere_FindsEntity()
{
// Given
var grid = new SpatialHashGrid(2.0f, 16);
var bounds = new AABB(new Vector3(0f, 0f, 0f), new Vector3(1f, 1f, 1f));
grid.Insert(0, bounds);

var results = new NativeList<int>(8);

// When
grid.QuerySphere(new Vector3(0.5f, 0.5f, 0.5f), 1.0f, ref results);

// Then
Assert.True(results.Length >= 1);

results.Dispose();
grid.Dispose();
}

[Fact]
public void QuerySphere_FarAway_ReturnsEmpty()
{
// Given
var grid = new SpatialHashGrid(2.0f, 16);
var bounds = new AABB(new Vector3(0f, 0f, 0f), new Vector3(1f, 1f, 1f));
grid.Insert(0, bounds);

var results = new NativeList<int>(8);

// When
grid.QuerySphere(new Vector3(100f, 100f, 100f), 1.0f, ref results);

// Then
Assert.Equal(0, results.Length);

results.Dispose();
grid.Dispose();
}

[Fact]
public void Clear_RemovesAllEntries()
{
// Given
var grid = new SpatialHashGrid(2.0f, 16);
grid.Insert(0, new AABB(Vector3.Zero, Vector3.One));

// When
grid.Clear();
var results = new NativeList<int>(8);
grid.QuerySphere(new Vector3(0.5f, 0.5f, 0.5f), 1.0f, ref results);

// Then
Assert.Equal(0, results.Length);

results.Dispose();
grid.Dispose();
}

[Fact]
public void Insert_MultipleEntities_AllFound()
{
// Given
var grid = new SpatialHashGrid(2.0f, 16);
grid.Insert(0, new AABB(new Vector3(0f, 0f, 0f), new Vector3(1f, 1f, 1f)));
grid.Insert(1, new AABB(new Vector3(0.5f, 0f, 0f), new Vector3(1.5f, 1f, 1f)));

var results = new NativeList<int>(8);

// When
grid.QuerySphere(new Vector3(0.5f, 0.5f, 0.5f), 2.0f, ref results);

// Then
Assert.True(results.Length >= 2);

results.Dispose();
grid.Dispose();
}
}
Loading