From cca89040c3725c8184b2b0422449ff084b02ec5c Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Wed, 17 Jun 2026 16:45:43 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20JoltPhysicsSharp=20integration=20?= =?UTF-8?q?=E2=80=94=20physics=20simulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Engine.Physics project with JoltPhysicsSharp 2.21.0 - PhysicsWorld: PhysicsSystem init/update/dispose, body creation/removal, transform sync (Jolt body → ECS Transform) - RigidBody ECS component: MotionType (Static/Kinematic/Dynamic), ShapeType (Box/Sphere), mass, friction, restitution, damping - Factory methods: DynamicBox, DynamicSphere, StaticBox, StaticPlane - Main loop integration: create bodies on first frame, step physics, sync transforms back to ECS - Demo scene: 6 dynamic cubes + 3 dynamic spheres fall from height, static floor plane. Torus knot and textured cube remain static. - Collision layers: NonMoving (floor), Moving (dynamic objects) - JobSystemThreadPool for multi-threaded physics - 66/66 tests passing, 0 warnings, 0 errors --- CORTEX_ENGINE.sln | 119 +++++++++++++ CORTEX_ENGINE_ARCHITECTURE.md | 3 +- src/CortexEngine.App/CortexEngine.App.csproj | 1 + src/CortexEngine.App/Program.cs | 60 ++++--- src/Engine.Core/Components/RigidBody.cs | 72 ++++++++ src/Engine.Physics/Engine.Physics.csproj | 20 +++ src/Engine.Physics/PhysicsWorld.cs | 177 +++++++++++++++++++ 7 files changed, 422 insertions(+), 30 deletions(-) create mode 100644 src/Engine.Core/Components/RigidBody.cs create mode 100644 src/Engine.Physics/Engine.Physics.csproj create mode 100644 src/Engine.Physics/PhysicsWorld.cs diff --git a/CORTEX_ENGINE.sln b/CORTEX_ENGINE.sln index 60959f6..d2e66dd 100644 --- a/CORTEX_ENGINE.sln +++ b/CORTEX_ENGINE.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -16,52 +17,170 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics.Raylib", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Tests", "tests\Engine.Tests\Engine.Tests.csproj", "{55555555-5555-5555-5555-555555555555}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Physics", "src\Engine.Physics\Engine.Physics.csproj", "{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 ReleaseAOT|Any CPU = ReleaseAOT|Any CPU + ReleaseAOT|x64 = ReleaseAOT|x64 + ReleaseAOT|x86 = ReleaseAOT|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {11111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x64.ActiveCfg = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x64.Build.0 = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x86.ActiveCfg = Debug|Any CPU + {11111111-1111-1111-1111-111111111111}.Debug|x86.Build.0 = Debug|Any CPU {11111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU {11111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x64.ActiveCfg = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x64.Build.0 = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x86.ActiveCfg = Release|Any CPU + {11111111-1111-1111-1111-111111111111}.Release|x86.Build.0 = Release|Any CPU {11111111-1111-1111-1111-111111111111}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {11111111-1111-1111-1111-111111111111}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU + {11111111-1111-1111-1111-111111111111}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {11111111-1111-1111-1111-111111111111}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {11111111-1111-1111-1111-111111111111}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {11111111-1111-1111-1111-111111111111}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU {22222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22222222-2222-2222-2222-222222222222}.Debug|x64.ActiveCfg = Debug|Any CPU + {22222222-2222-2222-2222-222222222222}.Debug|x64.Build.0 = Debug|Any CPU + {22222222-2222-2222-2222-222222222222}.Debug|x86.ActiveCfg = Debug|Any CPU + {22222222-2222-2222-2222-222222222222}.Debug|x86.Build.0 = Debug|Any CPU {22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU {22222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU + {22222222-2222-2222-2222-222222222222}.Release|x64.ActiveCfg = Release|Any CPU + {22222222-2222-2222-2222-222222222222}.Release|x64.Build.0 = Release|Any CPU + {22222222-2222-2222-2222-222222222222}.Release|x86.ActiveCfg = Release|Any CPU + {22222222-2222-2222-2222-222222222222}.Release|x86.Build.0 = Release|Any CPU {22222222-2222-2222-2222-222222222222}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {22222222-2222-2222-2222-222222222222}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU + {22222222-2222-2222-2222-222222222222}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {22222222-2222-2222-2222-222222222222}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {22222222-2222-2222-2222-222222222222}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {22222222-2222-2222-2222-222222222222}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU {33333333-3333-3333-3333-333333333333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x64.ActiveCfg = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x64.Build.0 = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x86.ActiveCfg = Debug|Any CPU + {33333333-3333-3333-3333-333333333333}.Debug|x86.Build.0 = Debug|Any CPU {33333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU {33333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x64.ActiveCfg = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x64.Build.0 = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x86.ActiveCfg = Release|Any CPU + {33333333-3333-3333-3333-333333333333}.Release|x86.Build.0 = Release|Any CPU {33333333-3333-3333-3333-333333333333}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {33333333-3333-3333-3333-333333333333}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU + {33333333-3333-3333-3333-333333333333}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {33333333-3333-3333-3333-333333333333}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {33333333-3333-3333-3333-333333333333}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {33333333-3333-3333-3333-333333333333}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU {44444444-4444-4444-4444-444444444444}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44444444-4444-4444-4444-444444444444}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44444444-4444-4444-4444-444444444444}.Debug|x64.ActiveCfg = Debug|Any CPU + {44444444-4444-4444-4444-444444444444}.Debug|x64.Build.0 = Debug|Any CPU + {44444444-4444-4444-4444-444444444444}.Debug|x86.ActiveCfg = Debug|Any CPU + {44444444-4444-4444-4444-444444444444}.Debug|x86.Build.0 = Debug|Any CPU {44444444-4444-4444-4444-444444444444}.Release|Any CPU.ActiveCfg = Release|Any CPU {44444444-4444-4444-4444-444444444444}.Release|Any CPU.Build.0 = Release|Any CPU + {44444444-4444-4444-4444-444444444444}.Release|x64.ActiveCfg = Release|Any CPU + {44444444-4444-4444-4444-444444444444}.Release|x64.Build.0 = Release|Any CPU + {44444444-4444-4444-4444-444444444444}.Release|x86.ActiveCfg = Release|Any CPU + {44444444-4444-4444-4444-444444444444}.Release|x86.Build.0 = Release|Any CPU {44444444-4444-4444-4444-444444444444}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {44444444-4444-4444-4444-444444444444}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU + {44444444-4444-4444-4444-444444444444}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {44444444-4444-4444-4444-444444444444}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {44444444-4444-4444-4444-444444444444}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {44444444-4444-4444-4444-444444444444}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Debug|x64.ActiveCfg = Debug|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Debug|x64.Build.0 = Debug|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Debug|x86.ActiveCfg = Debug|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Debug|x86.Build.0 = Debug|Any CPU {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Release|Any CPU.Build.0 = Release|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Release|x64.ActiveCfg = Release|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Release|x64.Build.0 = Release|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Release|x86.ActiveCfg = Release|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.Release|x86.Build.0 = Release|Any CPU {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x64.Build.0 = Debug|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x86.Build.0 = Debug|Any CPU {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|Any CPU.Build.0 = Release|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x64.ActiveCfg = Release|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x64.Build.0 = Release|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x86.ActiveCfg = Release|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x86.Build.0 = Release|Any CPU {31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU {31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU {55555555-5555-5555-5555-555555555555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {55555555-5555-5555-5555-555555555555}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55555555-5555-5555-5555-555555555555}.Debug|x64.ActiveCfg = Debug|Any CPU + {55555555-5555-5555-5555-555555555555}.Debug|x64.Build.0 = Debug|Any CPU + {55555555-5555-5555-5555-555555555555}.Debug|x86.ActiveCfg = Debug|Any CPU + {55555555-5555-5555-5555-555555555555}.Debug|x86.Build.0 = Debug|Any CPU {55555555-5555-5555-5555-555555555555}.Release|Any CPU.ActiveCfg = Release|Any CPU {55555555-5555-5555-5555-555555555555}.Release|Any CPU.Build.0 = Release|Any CPU + {55555555-5555-5555-5555-555555555555}.Release|x64.ActiveCfg = Release|Any CPU + {55555555-5555-5555-5555-555555555555}.Release|x64.Build.0 = Release|Any CPU + {55555555-5555-5555-5555-555555555555}.Release|x86.ActiveCfg = Release|Any CPU + {55555555-5555-5555-5555-555555555555}.Release|x86.Build.0 = Release|Any CPU + {55555555-5555-5555-5555-555555555555}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU + {55555555-5555-5555-5555-555555555555}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU + {55555555-5555-5555-5555-555555555555}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU + {55555555-5555-5555-5555-555555555555}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Debug|x64.Build.0 = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Debug|x86.Build.0 = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Release|Any CPU.Build.0 = Release|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Release|x64.ActiveCfg = Release|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Release|x64.Build.0 = Release|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Release|x86.ActiveCfg = Release|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.Release|x86.Build.0 = Release|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|Any CPU.ActiveCfg = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|Any CPU.Build.0 = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x64.ActiveCfg = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x64.Build.0 = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x86.ActiveCfg = Debug|Any CPU + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x86.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/CORTEX_ENGINE_ARCHITECTURE.md b/CORTEX_ENGINE_ARCHITECTURE.md index 72fc2cd..b900a9f 100644 --- a/CORTEX_ENGINE_ARCHITECTURE.md +++ b/CORTEX_ENGINE_ARCHITECTURE.md @@ -723,7 +723,8 @@ In Release (NativeAOT), the MCP server and ASP.NET Core are excluded. The AI can - [ ] **Vulkan backend reintegration** — see §3.3 checklist. Compiles but untested. Kept architecturally compatible via `IWindow` / `IRenderContext` / `IRenderer`. Deferred because Raylib covers all current needs with far less complexity. -- [ ] Physics (JoltPhysicsSharp) +- [x] Physics (JoltPhysicsSharp 2.21.0) — Engine.Physics project, PhysicsWorld wrapper, + RigidBody ECS component, dynamic boxes/spheres with gravity, static floor, transform sync - [ ] AI hot-reload of C# scripts (Roslyn — conflicts with NativeAOT) - [ ] Semantic segmentation maps for MMLM vision input diff --git a/src/CortexEngine.App/CortexEngine.App.csproj b/src/CortexEngine.App/CortexEngine.App.csproj index 366ac77..f04bd6b 100644 --- a/src/CortexEngine.App/CortexEngine.App.csproj +++ b/src/CortexEngine.App/CortexEngine.App.csproj @@ -25,6 +25,7 @@ + diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index c6bc714..9367a93 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -13,6 +13,7 @@ using Engine.Graphics; using Engine.Graphics.Loaders; using Engine.Graphics.RaylibBackend; using Engine.Graphics.Vulkan; +using Engine.Physics; using Flecs.NET.Core; namespace CortexEngine.App; @@ -38,6 +39,7 @@ class Program using var world = World.Create(); var timing = new Timing(); + using var physicsWorld = new PhysicsWorld(); RaylibBackendRegistrar.EnsureRegistered(); VulkanBackendRegistrar.EnsureRegistered(); @@ -238,15 +240,21 @@ class Program } } - // Slowly rotate the model so we can see it in 3D, unless the camera tour or test scene is active. - if (!cameraTour && !testScene) + // Physics: create bodies, step, sync transforms + if (!cameraTour) { - var modelEntity = world.Lookup("CubeCenter"); - if (modelEntity.Id != 0) + world.Each((Entity e, ref RigidBody rb, ref Transform t) => { - ref var modelTransform = ref modelEntity.Ensure(); - modelTransform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)timing.TotalTime * 0.5f); - } + if (!rb.IsInitialized) + { + physicsWorld.CreateBody(e, rb, t); + rb.IsInitialized = true; + e.Set(rb); + } + }); + + physicsWorld.Update((float)timing.DeltaTime); + physicsWorld.SyncTransforms(world); } // Capture a demo screenshot after the scene warms up (non-tour mode only). @@ -295,14 +303,12 @@ class Program var cubes = new (string name, Vector3 pos, Vector3 color, float scale, float rough, float metal)[] { - ("CubeCenter", new Vector3(0, 0.5f, 0), new Vector3(0.9f, 0.6f, 0.3f), 0.5f, 0.3f, 0.1f), - ("CubeRed", new Vector3(2, 0.5f, 0), new Vector3(0.85f, 0.15f, 0.15f), 0.5f, 0.4f, 0.2f), - ("CubeGreen", new Vector3(-2, 0.5f, 0), new Vector3(0.2f, 0.8f, 0.3f), 0.5f, 0.5f, 0.0f), - ("CubeBlue", new Vector3(0, 0.5f, 3), new Vector3(0.2f, 0.4f, 0.9f), 0.6f, 0.2f, 0.3f), - ("CubeYellow", new Vector3(0, 0.5f, -3), new Vector3(0.95f, 0.85f, 0.2f), 0.5f, 0.6f, 0.0f), - ("CubeOrange", new Vector3(-3, 0.5f, 3), new Vector3(0.95f, 0.5f, 0.1f), 0.45f, 0.5f, 0.1f), - ("CubeWide", new Vector3(-1.5f, 0.5f, -1.5f), new Vector3(0.5f, 0.5f, 0.6f), 0.8f, 0.8f, 0.0f), - ("CubeSmallGold", new Vector3(5, 0.3f, -2), new Vector3(1.0f, 0.8f, 0.3f), 0.3f, 0.15f, 1.0f), + ("CubeCenter", new Vector3(0, 5f, 0), new Vector3(0.9f, 0.6f, 0.3f), 0.5f, 0.3f, 0.1f), + ("CubeRed", new Vector3(0.3f, 7f, 0.3f), new Vector3(0.85f, 0.15f, 0.15f), 0.5f, 0.4f, 0.2f), + ("CubeGreen", new Vector3(-0.3f, 9f, -0.3f), new Vector3(0.2f, 0.8f, 0.3f), 0.5f, 0.5f, 0.0f), + ("CubeBlue", new Vector3(0.1f, 11f, 0.1f), new Vector3(0.2f, 0.4f, 0.9f), 0.6f, 0.2f, 0.3f), + ("CubeYellow", new Vector3(-0.2f, 13f, 0.2f), new Vector3(0.95f, 0.85f, 0.2f), 0.5f, 0.6f, 0.0f), + ("CubeOrange", new Vector3(0.15f, 15f, -0.1f), new Vector3(0.95f, 0.5f, 0.1f), 0.45f, 0.5f, 0.1f), }; foreach (var (name, pos, color, scale, rough, metal) in cubes) @@ -310,18 +316,15 @@ class Program world.Entity(name) .Set(new Transform(pos, Quaternion.Identity, new Vector3(scale))) .Set(mesh) - .Set(new Material(color, roughness: rough, metallic: metal)); + .Set(new Material(color, roughness: rough, metallic: metal)) + .Set(RigidBody.DynamicBox(new Vector3(scale), mass: scale * 2f)); } var spheres = new (string name, Vector3 pos, Vector3 color, float scale, float rough, float metal)[] { - ("SphereGold", new Vector3(-5, 0.5f, -2), new Vector3(1.0f, 0.85f, 0.4f), 1.0f, 0.1f, 1.0f), - ("SphereChrome", new Vector3(-6, 0.5f, 0), new Vector3(0.9f, 0.9f, 0.95f), 1.0f, 0.05f, 1.0f), - ("SphereRed", new Vector3(-5, 0.5f, 2), new Vector3(0.9f, 0.1f, 0.1f), 1.0f, 0.4f, 0.0f), - ("SphereBlue", new Vector3(5, 0.5f, 2), new Vector3(0.1f, 0.3f, 0.9f), 1.0f, 0.2f, 0.5f), - ("SphereGreen", new Vector3(6, 0.5f, 0), new Vector3(0.1f, 0.8f, 0.3f), 1.0f, 0.7f, 0.0f), - ("SphereWhite", new Vector3(5, 0.5f, -4), new Vector3(0.95f, 0.95f, 0.95f), 1.0f, 0.3f, 0.0f), - ("SphereRough", new Vector3(3, 0.5f, 5), new Vector3(0.6f, 0.4f, 0.2f), 1.0f, 0.9f, 0.0f), + ("SphereGold", new Vector3(3, 6f, -2), new Vector3(1.0f, 0.85f, 0.4f), 1.0f, 0.1f, 1.0f), + ("SphereChrome", new Vector3(-3, 8f, 0), new Vector3(0.9f, 0.9f, 0.95f), 1.0f, 0.05f, 1.0f), + ("SphereRed", new Vector3(3, 10f, 2), new Vector3(0.9f, 0.1f, 0.1f), 1.0f, 0.4f, 0.0f), }; foreach (var (name, pos, color, scale, rough, metal) in spheres) @@ -329,26 +332,25 @@ class Program world.Entity(name) .Set(new Transform(pos, Quaternion.Identity, new Vector3(scale))) .Set(sphere) - .Set(new Material(color, roughness: rough, metallic: metal)); + .Set(new Material(color, roughness: rough, metallic: metal)) + .Set(RigidBody.DynamicSphere(scale * 0.5f, mass: scale)); } - // Torus knot with checker texture world.Entity("TorusKnot") - .Set(new Transform(new Vector3(0, 2.0f, 0), Quaternion.Identity, new Vector3(1.5f))) + .Set(new Transform(new Vector3(0, 0.5f, -6), Quaternion.Identity, new Vector3(1.5f))) .Set(torusKnot) .Set(new Material(new Vector3(0.9f, 0.9f, 0.9f), roughness: 0.25f, metallic: 0.6f, texturePath: "Content/checker.png")); - // Textured cube world.Entity("CubeTextured") .Set(new Transform(new Vector3(-4, 0.5f, -3), Quaternion.Identity, new Vector3(0.7f))) .Set(mesh) .Set(new Material(new Vector3(0.8f, 0.8f, 0.85f), roughness: 0.4f, metallic: 0.0f, texturePath: "Content/checker.png")); - // Floor plane for shadow visibility world.Entity("Floor") .Set(new Transform(new Vector3(0, -0.02f, 0), Quaternion.Identity, new Vector3(20, 1, 20))) .Set(mesh) - .Set(new Material(new Vector3(0.45f, 0.45f, 0.5f), roughness: 0.8f, metallic: 0.0f)); + .Set(new Material(new Vector3(0.45f, 0.45f, 0.5f), roughness: 0.8f, metallic: 0.0f)) + .Set(RigidBody.StaticPlane(20f)); world.Entity("Grid") .Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One)) diff --git a/src/Engine.Core/Components/RigidBody.cs b/src/Engine.Core/Components/RigidBody.cs new file mode 100644 index 0000000..a9441d0 --- /dev/null +++ b/src/Engine.Core/Components/RigidBody.cs @@ -0,0 +1,72 @@ +using System.Numerics; + +namespace Engine.Core.Components; + +/// +/// Physics body motion type. +/// +public enum PhysicsMotionType +{ + Static = 0, + Kinematic = 1, + Dynamic = 2, +} + +/// +/// Collision shape type for the physics body. +/// +public enum PhysicsShapeType +{ + Box = 0, + Sphere = 1, +} + +/// +/// RigidBody component — links an ECS entity to a Jolt physics body. +/// Added to entities that should participate in physics simulation. +/// +public record struct RigidBody +{ + public PhysicsMotionType MotionType; + public PhysicsShapeType ShapeType; + public Vector3 ShapeSize; + public float Mass; + public float Friction; + public float Restitution; + public float LinearDamping; + public float AngularDamping; + public bool IsInitialized; + + public RigidBody( + PhysicsMotionType motionType = PhysicsMotionType.Dynamic, + PhysicsShapeType shapeType = PhysicsShapeType.Box, + Vector3? shapeSize = null, + float mass = 1.0f, + float friction = 0.5f, + float restitution = 0.3f, + float linearDamping = 0.1f, + float angularDamping = 0.1f) + { + MotionType = motionType; + ShapeType = shapeType; + ShapeSize = shapeSize ?? new Vector3(0.5f); + Mass = mass; + Friction = friction; + Restitution = restitution; + LinearDamping = linearDamping; + AngularDamping = angularDamping; + IsInitialized = false; + } + + public static RigidBody DynamicBox(Vector3 halfExtent, float mass = 1.0f) => + new(PhysicsMotionType.Dynamic, PhysicsShapeType.Box, halfExtent, mass); + + public static RigidBody DynamicSphere(float radius, float mass = 1.0f) => + new(PhysicsMotionType.Dynamic, PhysicsShapeType.Sphere, new Vector3(radius), mass); + + public static RigidBody StaticBox(Vector3 halfExtent) => + new(PhysicsMotionType.Static, PhysicsShapeType.Box, halfExtent, friction: 0.8f); + + public static RigidBody StaticPlane(float size = 50f) => + new(PhysicsMotionType.Static, PhysicsShapeType.Box, new Vector3(size, 0.5f, size), friction: 0.8f); +} diff --git a/src/Engine.Physics/Engine.Physics.csproj b/src/Engine.Physics/Engine.Physics.csproj new file mode 100644 index 0000000..075abdb --- /dev/null +++ b/src/Engine.Physics/Engine.Physics.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/src/Engine.Physics/PhysicsWorld.cs b/src/Engine.Physics/PhysicsWorld.cs new file mode 100644 index 0000000..b381de4 --- /dev/null +++ b/src/Engine.Physics/PhysicsWorld.cs @@ -0,0 +1,177 @@ +using System.Numerics; +using Engine.Core.Components; +using Flecs.NET.Core; +using JoltPhysicsSharp; + +namespace Engine.Physics; + +/// +/// Physics world wrapper — manages Jolt PhysicsSystem, body creation, and ECS sync. +/// +public sealed class PhysicsWorld : IDisposable +{ + private const int MaxBodies = 65536; + private const int MaxBodyPairs = 65536; + private const int MaxContactConstraints = 65536; + + private static class Layers + { + public static readonly ObjectLayer NonMoving = 0; + public static readonly ObjectLayer Moving = 1; + } + + private readonly PhysicsSystem _physicsSystem; + private readonly BodyInterface _bodyInterface; + private readonly JobSystem _jobSystem; + private readonly Dictionary _entityToBody = new(); + private readonly Dictionary _bodyToEntity = new(); + private readonly List _bodies = new(); + private bool _disposed; + + public Vector3 Gravity + { + get => _physicsSystem.Gravity; + set => _physicsSystem.Gravity = value; + } + + public PhysicsWorld() + { + Foundation.Init(false); + + var broadPhaseLayerInterface = new BroadPhaseLayerInterfaceTable(2, 2); + broadPhaseLayerInterface.MapObjectToBroadPhaseLayer(Layers.NonMoving, new BroadPhaseLayer(0)); + broadPhaseLayerInterface.MapObjectToBroadPhaseLayer(Layers.Moving, new BroadPhaseLayer(1)); + + var objectLayerPairFilter = new ObjectLayerPairFilterTable(2); + + var objectVsBroadPhaseLayerFilter = new ObjectVsBroadPhaseLayerFilterTable( + broadPhaseLayerInterface, 2, objectLayerPairFilter, 2); + + var settings = new PhysicsSystemSettings + { + MaxBodies = MaxBodies, + MaxBodyPairs = MaxBodyPairs, + MaxContactConstraints = MaxContactConstraints, + BroadPhaseLayerInterface = broadPhaseLayerInterface, + ObjectLayerPairFilter = objectLayerPairFilter, + ObjectVsBroadPhaseLayerFilter = objectVsBroadPhaseLayerFilter, + }; + + _physicsSystem = new PhysicsSystem(settings); + _physicsSystem.Gravity = new Vector3(0, -9.81f, 0); + _bodyInterface = _physicsSystem.BodyInterface; + _jobSystem = new JobSystemThreadPool(); + } + + public void CreateBody(Entity entity, in RigidBody rigidBody, in Transform transform) + { + if (_entityToBody.ContainsKey(entity)) + return; + + var shape = CreateShape(rigidBody); + var motionType = ToMotionType(rigidBody.MotionType); + var layer = rigidBody.MotionType == PhysicsMotionType.Static ? Layers.NonMoving : Layers.Moving; + + var creationSettings = new BodyCreationSettings( + shape, + transform.Position, + transform.Rotation, + motionType, + layer); + + var activation = rigidBody.MotionType == PhysicsMotionType.Static + ? Activation.DontActivate + : Activation.Activate; + + var body = _bodyInterface.CreateBody(creationSettings); + _bodyInterface.AddBody(body.ID, activation); + + if (rigidBody.MotionType != PhysicsMotionType.Static) + { + body.SetFriction(rigidBody.Friction); + body.SetRestitution(rigidBody.Restitution); + } + + _entityToBody[entity] = body.ID; + _bodyToEntity[body.ID] = entity; + _bodies.Add(body); + } + + public void RemoveBody(Entity entity) + { + if (!_entityToBody.TryGetValue(entity, out var bodyId)) + return; + + _bodyInterface.RemoveBody(bodyId); + _bodyInterface.DestroyBody(bodyId); + _entityToBody.Remove(entity); + _bodyToEntity.Remove(bodyId); + } + + public void Update(float deltaTime, int collisionSteps = 1) + { + _physicsSystem.Update(deltaTime, collisionSteps, _jobSystem); + } + + public void SyncTransforms(World world) + { + foreach (var (entity, bodyId) in _entityToBody) + { + if ((ulong)entity.Id == 0) + continue; + + var pos = _bodyInterface.GetPosition(bodyId); + var rot = _bodyInterface.GetRotation(bodyId); + + if (entity.Has()) + { + var t = entity.Get(); + t.Position = pos; + t.Rotation = rot; + entity.Set(t); + } + } + } + + public void SyncToPhysics(Entity entity, in Transform transform) + { + if (!_entityToBody.TryGetValue(entity, out var bodyId)) + return; + + _bodyInterface.SetPositionAndRotation(bodyId, transform.Position, transform.Rotation, Activation.Activate); + } + + private static Shape CreateShape(RigidBody rb) + { + return rb.ShapeType switch + { + PhysicsShapeType.Box => new BoxShape(rb.ShapeSize), + PhysicsShapeType.Sphere => new SphereShape(rb.ShapeSize.X), + _ => new BoxShape(rb.ShapeSize), + }; + } + + private static MotionType ToMotionType(PhysicsMotionType type) => type switch + { + PhysicsMotionType.Static => MotionType.Static, + PhysicsMotionType.Kinematic => MotionType.Kinematic, + PhysicsMotionType.Dynamic => MotionType.Dynamic, + _ => MotionType.Dynamic, + }; + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + foreach (var body in _bodies) + body.Dispose(); + _bodies.Clear(); + _entityToBody.Clear(); + _bodyToEntity.Clear(); + + _jobSystem.Dispose(); + _physicsSystem.Dispose(); + Foundation.Shutdown(); + } +}