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
Show all changes
21 commits
Select commit Hold shift + click to select a range
258da38
InputState にマウスボタン入力とカーソル位置を追加
HMasataka Mar 7, 2026
2f55438
Grabbable / GrabState コンポーネントを追加
HMasataka Mar 7, 2026
40f5c0a
デスクトップ掴みシステムを追加
HMasataka Mar 7, 2026
564e7df
VR 手の速度追跡を追加
HMasataka Mar 7, 2026
b46e215
VR ハンドグラブとスプリングジョイントを追加
HMasataka Mar 7, 2026
c67ce96
VR 距離グラブシステムを追加
HMasataka Mar 7, 2026
8f66f52
ハプティクスフィードバックを追加
HMasataka Mar 7, 2026
fe7b262
デモアプリに掴みインタラクションを統合
HMasataka Mar 7, 2026
c4e4f5b
動的オブジェクトの WorldBounds を毎フレーム再計算して描画消失を修正
HMasataka Mar 7, 2026
d97e2fb
マウス Pitch の上下反転を修正
HMasataka Mar 7, 2026
739b592
グラブレイの原点を目線位置に修正し Pitch 符号を統一
HMasataka Mar 7, 2026
24a1723
デバッグ用クロスヘアをカメラ前方に表示
HMasataka Mar 7, 2026
fc9cc2d
掴んだオブジェクトをカメラ前方 2.5m に保持するよう修正
HMasataka Mar 7, 2026
3cfd2d8
掴み中のオブジェクトの Velocity をゼロにして物理干渉を防止
HMasataka Mar 7, 2026
90dbc59
掴んだオブジェクトの向きをカメラに追従させる
HMasataka Mar 7, 2026
07f484b
RigidBody に InverseInertia フィールドを追加
HMasataka Mar 7, 2026
2272e8b
PGS ソルバーに角慣性サポートと線形のみ法線インパルスを導入
HMasataka Mar 7, 2026
b1f1236
永続接触マニフォールドとウォームスタートを追加
HMasataka Mar 7, 2026
fe91b39
角速度の安定化(MaxRotation クランプ、スリープ閾値、面整列)
HMasataka Mar 7, 2026
d515532
キューブの質量を体積比で設定しダンピングを調整
HMasataka Mar 7, 2026
780c7b5
roadmap.md v0.12 を完了に更新
HMasataka Mar 7, 2026
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
14 changes: 7 additions & 7 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@

ゴール: 両プラットフォームで物理インタラクションが動く

- [ ] デスクトップ: マウスレイキャストでオブジェクト選択
- [ ] デスクトップ: クリック長押し → ドラッグ → リリースで掴む・投げる
- [ ] VR: ハンドグラブ (手の形状に基づく掴みポーズ)
- [ ] VR: 距離グラブ (ポイントして引き寄せ)
- [ ] VR: 手の速度をオブジェクトに伝達して投げる
- [ ] VR: スプリングジョイント接続
- [ ] VR: ハプティクスフィードバック
- [x] デスクトップ: マウスレイキャストでオブジェクト選択
- [x] デスクトップ: クリック長押し → ドラッグ → リリースで掴む・投げる
- [x] VR: ハンドグラブ (手の形状に基づく掴みポーズ)
- [x] VR: 距離グラブ (ポイントして引き寄せ)
- [x] VR: 手の速度をオブジェクトに伝達して投げる
- [x] VR: スプリングジョイント接続
- [x] VR: ハプティクスフィードバック

完動品としての価値: デスクトップでもVRでもオブジェクトを掴んで投げられる。VRでは手の動きが自然に反映される。

Expand Down
123 changes: 114 additions & 9 deletions src/Seed.Engine.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -554,12 +554,29 @@ private static void Main()

scheduler.AddSystem(new MovementSystem());
scheduler.AddSystem(new CharacterPhysicsSystem());

if (renderMode == RenderMode.Stereo && xrSessionForInit != null)
{
scheduler.AddSystem(new VrGrabSystem());
scheduler.AddSystem(new VrDistanceGrabSystem());
scheduler.AddSystem(new HapticFeedbackSystem(xrSessionForInit));
}
else
{
scheduler.AddSystem(new DesktopGrabSystem());
}

scheduler.AddSystem(new DesktopGrabPhysicsSystem());
scheduler.AddSystem(new SpringJointSystem());
scheduler.AddSystem(new ForceIntegrationSystem(1f / 60f));
scheduler.AddSystem(new BroadPhaseSystem());
scheduler.AddSystem(new NarrowPhaseSystem());
scheduler.AddSystem(new ConstraintSolverSystem(1f / 60f));
scheduler.AddSystem(new IntegrationSystem(1f / 60f));
scheduler.AddSystem(new RotationSettlingSystem(1f / 60f));
scheduler.AddSystem(new TransformSystem());
scheduler.AddSystem(new WorldBoundsUpdateSystem());
scheduler.AddSystem(new DebugCrosshairSystem());
scheduler.AddSystem(new CameraViewSystem(
(float)window.Width / window.Height));

Expand Down Expand Up @@ -1272,17 +1289,66 @@ private static void CreateDynamicCubeEntity(
{
Value = Collider.CreateBox(new Vector3(0.5f, 0.5f, 0.5f)),
};
world.GetComponent<RigidBody>(entity) = new RigidBody
// Volume = 1.0^3 = 1.0, density = 8.0 → mass = 8.0
const float dynamicCubeMass = 8.0f;
world.GetComponent<RigidBody>(entity) = RigidBody.CreateDynamic(dynamicCubeMass);
world.GetComponent<Velocity>(entity) = new Velocity();
}

private static void CreateGrabbableCubeEntity(
World world, int meshId, int materialId, Vector3 position)
{
var ltType = ComponentRegistry.Get<LocalTransform>();
var ltwType = ComponentRegistry.Get<LocalToWorld>();
var meshType = ComponentRegistry.Get<MeshReference>();
var matType = ComponentRegistry.Get<MaterialReference>();
var boundsType = ComponentRegistry.Get<WorldBounds>();
var ccType = ComponentRegistry.Get<ColliderComponent>();
var rbType = ComponentRegistry.Get<RigidBody>();
var velType = ComponentRegistry.Get<Velocity>();
var grabbType = ComponentRegistry.Get<Grabbable>();
var gsType = ComponentRegistry.Get<GrabState>();
var sjType = ComponentRegistry.Get<SpringJoint>();
ReadOnlySpan<ComponentType> types =
[ltType, ltwType, meshType, matType, boundsType,
ccType, rbType, velType, grabbType, gsType, sjType];

Vector3 he = new Vector3(0.3f, 0.3f, 0.3f);
Entity entity = world.CreateEntity(types);
world.GetComponent<LocalTransform>(entity) =
LocalTransform.FromPositionRotationScale(
position, Quaternion.Identity, he * 2f);
world.GetComponent<LocalToWorld>(entity) = new LocalToWorld
{
Value = Matrix4x4.Identity,
};
world.GetComponent<MeshReference>(entity) = new MeshReference
{
MeshId = meshId,
};
world.GetComponent<MaterialReference>(entity) = new MaterialReference
{
MaterialId = materialId,
};
world.GetComponent<WorldBounds>(entity) = new WorldBounds
{
Mass = 1.0f,
InverseMass = 1.0f,
LinearDamping = 0.01f,
AngularDamping = 0.05f,
GravityScale = 1.0f,
IsKinematic = false,
FreezeRotation = new Vector3(0f, 0f, 0f),
Value = new AABB(position - he, position + he),
};
world.GetComponent<ColliderComponent>(entity) = new ColliderComponent
{
Value = Collider.CreateBox(he),
};
// Volume = 0.6^3 = 0.216, density = 8.0 → mass ≈ 1.73
float grabbableCubeMass = 0.6f * 0.6f * 0.6f * 8.0f;
world.GetComponent<RigidBody>(entity) = RigidBody.CreateDynamic(grabbableCubeMass);
world.GetComponent<Velocity>(entity) = new Velocity();
world.GetComponent<Grabbable>(entity) = new Grabbable
{
GrabRadius = 1.0f,
MaxGrabDistance = 10.0f,
};
world.GetComponent<GrabState>(entity) = new GrabState();
world.GetComponent<SpringJoint>(entity) = new SpringJoint();
}

private static void RunMainLoop(
Expand All @@ -1309,7 +1375,13 @@ private static void RunMainLoop(
CreateDynamicCubeEntity(world, cubeMeshId, materialId,
new Vector3(-2f, 1f, -5f));

CreateGrabbableCubeEntity(world, cubeMeshId, materialId,
new Vector3(0f, 0.5f, -3f));
CreateGrabbableCubeEntity(world, cubeMeshId, materialId,
new Vector3(1.5f, 0.5f, -4f));

CreatePlayerEntity(world);
CreateDebugCrosshairEntity(world, cubeMeshId, materialId);
CreateDirectionalLightEntity(world, lightData);

CreatePointLightEntity(world,
Expand Down Expand Up @@ -1343,7 +1415,10 @@ private static void CreateSingletonEntity(
if (renderMode == RenderMode.Stereo)
{
var vrType = ComponentRegistry.Get<VrControllerState>();
ReadOnlySpan<ComponentType> types = [ftType, isType, vrType];
var hsType = ComponentRegistry.Get<VrHandState>();
var hrType = ComponentRegistry.Get<HapticRequest>();
ReadOnlySpan<ComponentType> types =
[ftType, isType, vrType, hsType, hrType];
world.CreateEntity(types);
}
else
Expand Down Expand Up @@ -1379,6 +1454,36 @@ private static void CreatePlatformTagEntity(
}
}

private static void CreateDebugCrosshairEntity(
World world, int meshId, int materialId)
{
var ltType = ComponentRegistry.Get<LocalTransform>();
var ltwType = ComponentRegistry.Get<LocalToWorld>();
var meshType = ComponentRegistry.Get<MeshReference>();
var matType = ComponentRegistry.Get<MaterialReference>();
var tagType = ComponentRegistry.Get<DebugCrosshair>();
ReadOnlySpan<ComponentType> types =
[ltType, ltwType, meshType, matType, tagType];

Entity entity = world.CreateEntity(types);
world.GetComponent<LocalTransform>(entity) =
LocalTransform.FromPositionRotationScale(
Vector3.Zero, Quaternion.Identity,
new Vector3(0.03f, 0.03f, 0.03f));
world.GetComponent<LocalToWorld>(entity) = new LocalToWorld
{
Value = Matrix4x4.Identity,
};
world.GetComponent<MeshReference>(entity) = new MeshReference
{
MeshId = meshId,
};
world.GetComponent<MaterialReference>(entity) = new MaterialReference
{
MaterialId = materialId,
};
}

private static void CreatePlayerEntity(World world)
{
var ltType = ComponentRegistry.Get<LocalTransform>();
Expand Down
148 changes: 148 additions & 0 deletions src/Seed.Engine.Tests/GameLogic/DesktopGrabPhysicsSystemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System;
using Xunit;

using Seed.Engine.Ecs;
using Seed.Engine.Ecs.Components;
using Seed.Engine.Foundation.Mathematics;
using Seed.Engine.GameLogic;
using Seed.Engine.Tests.Foundation.Mathematics;

namespace Seed.Engine.Tests.GameLogic;

public class DesktopGrabPhysicsSystemTests
{
private static (World world, Entity camera, Entity grabbable) SetupWorld()
{
var world = new World();

// Camera entity
var camType = ComponentRegistry.Get<Camera>();
var ltType = ComponentRegistry.Get<LocalTransform>();
var csType = ComponentRegistry.Get<CharacterState>();
var cmType = ComponentRegistry.Get<CameraMode>();
ReadOnlySpan<ComponentType> cameraTypes = [camType, ltType, csType, cmType];
Entity camera = world.CreateEntity(cameraTypes);
world.GetComponent<LocalTransform>(camera) =
LocalTransform.FromPosition(Vector3.Zero);
world.GetComponent<CharacterState>(camera) = new CharacterState();
world.GetComponent<CameraMode>(camera) = new CameraMode();

// Grabbable entity
var grabbType = ComponentRegistry.Get<Grabbable>();
var gsType = ComponentRegistry.Get<GrabState>();
var velType = ComponentRegistry.Get<Velocity>();
ReadOnlySpan<ComponentType> grabbableTypes = [grabbType, gsType, ltType, velType];
Entity grabbable = world.CreateEntity(grabbableTypes);
world.GetComponent<Velocity>(grabbable) = new Velocity();
world.GetComponent<Grabbable>(grabbable) = new Grabbable
{
GrabRadius = 1.0f,
MaxGrabDistance = 10.0f,
};
world.GetComponent<LocalTransform>(grabbable) =
LocalTransform.FromPosition(new Vector3(0f, 0f, -3f));

return (world, camera, grabbable);
}

[Fact]
public void Phase_IsPhysics()
{
var system = new DesktopGrabPhysicsSystem();
Assert.Equal(FramePhase.Physics, system.Phase);
}

[Fact]
public void Execute_GrabbedObject_FollowsCameraPosition()
{
// Given
var (world, camera, grabbable) = SetupWorld();
using var _ = world;

Vector3 grabOffset = new Vector3(0f, 0f, -3f);
world.GetComponent<GrabState>(grabbable) = new GrabState
{
IsGrabbed = 1,
GrabberEntity = camera,
GrabOffset = grabOffset,
};

// Move camera to new position
world.GetComponent<LocalTransform>(camera) =
LocalTransform.FromPosition(new Vector3(5f, 0f, 0f));

var system = new DesktopGrabPhysicsSystem();

// When
system.Execute(world);

// Then: object should follow camera to new position + offset
ref LocalTransform lt = ref world.GetComponent<LocalTransform>(grabbable);
AssertHelper.ApproximatelyEqual(5f, lt.Position.X, 0.01f);
AssertHelper.ApproximatelyEqual(0f, lt.Position.Y, 0.01f);
AssertHelper.ApproximatelyEqual(-3f, lt.Position.Z, 0.01f);
}

[Fact]
public void Execute_NotGrabbed_DoesNotMoveObject()
{
// Given
var (world, camera, grabbable) = SetupWorld();
using var _ = world;

world.GetComponent<GrabState>(grabbable) = new GrabState
{
IsGrabbed = 0,
};

Vector3 originalPos = new Vector3(0f, 0f, -3f);
world.GetComponent<LocalTransform>(grabbable) =
LocalTransform.FromPosition(originalPos);

// Move camera
world.GetComponent<LocalTransform>(camera) =
LocalTransform.FromPosition(new Vector3(10f, 0f, 0f));

var system = new DesktopGrabPhysicsSystem();

// When
system.Execute(world);

// Then: object should stay at original position
ref LocalTransform lt = ref world.GetComponent<LocalTransform>(grabbable);
AssertHelper.ApproximatelyEqual(originalPos, lt.Position, 0.01f);
}

[Fact]
public void Execute_GrabbedWithRotation_OffsetRotatesWithCamera()
{
// Given
var (world, camera, grabbable) = SetupWorld();
using var _ = world;

Vector3 grabOffset = new Vector3(0f, 0f, -3f);
world.GetComponent<GrabState>(grabbable) = new GrabState
{
IsGrabbed = 1,
GrabberEntity = camera,
GrabOffset = grabOffset,
};

// Rotate camera 90 degrees around Y axis (yaw = pi/2)
world.GetComponent<CharacterState>(camera) = new CharacterState
{
Yaw = MathHelper.Pi / 2f,
};

var system = new DesktopGrabPhysicsSystem();

// When
system.Execute(world);

// Then: offset (0,0,-3) rotated by yaw=-pi/2 around Y should yield (3,0,0)
ref LocalTransform lt = ref world.GetComponent<LocalTransform>(grabbable);
AssertHelper.ApproximatelyEqual(3f, lt.Position.X, 0.1f);
AssertHelper.ApproximatelyEqual(0f, lt.Position.Y, 0.1f);
AssertHelper.ApproximatelyEqual(0f, lt.Position.Z, 0.1f);
}
}
Loading