From 8e5a217687ec49ffb09326c12ee848b1c5628c1d Mon Sep 17 00:00:00 2001 From: Nigel-Bess Date: Wed, 5 Nov 2025 23:00:08 -0800 Subject: [PATCH 1/6] started on demo setup --- DemoUtilities/Window.cs | 39 ++++++++++++- Demos.GL/Broken/CompoundFallsThroughFloor.cs | 55 +++++++++++++++++++ Demos.GL/Broken/PoseIntegratorCallbacks.cs | 33 +++++++++++ Demos.GL/Broken/SimpleNarrowPhaseCallbacks.cs | 52 ++++++++++++++++++ Demos/DemoSet.cs | 3 +- Demos/GameLoop.cs | 16 ++++-- Demos/Program.cs | 31 ++++++++++- 7 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 Demos.GL/Broken/CompoundFallsThroughFloor.cs create mode 100644 Demos.GL/Broken/PoseIntegratorCallbacks.cs create mode 100644 Demos.GL/Broken/SimpleNarrowPhaseCallbacks.cs diff --git a/DemoUtilities/Window.cs b/DemoUtilities/Window.cs index 1cacc475b..3867e47eb 100644 --- a/DemoUtilities/Window.cs +++ b/DemoUtilities/Window.cs @@ -1,9 +1,9 @@ -using System; -using System.Diagnostics; +using BepuUtilities; using OpenTK; -using BepuUtilities; using OpenTK.Graphics; using OpenTK.Platform; +using System; +using System.Diagnostics; using System.Threading; using Vector2 = System.Numerics.Vector2; @@ -182,6 +182,39 @@ public void Run(Action updateHandler, Action onResize) windowUpdateLoopRunning = false; } + public void SingleFrame(Action updateHandler, Action onResize, float dt) + { + + if (disposed) + return; + if (resized) + { + //Note that minimizing or resizing the window to invalid sizes don't result in actual resize attempts. Zero width rendering surfaces aren't allowed. + if (window.Width > 0 && window.Height > 0) + { + onResize(new Int2(window.Width, window.Height)); + } + resized = false; + } + window.ProcessEvents(); + if (tryToClose) + { + window.Close(); + return; + } + long time = Stopwatch.GetTimestamp(); + + if (window.WindowState != WindowState.Minimized) + { + updateHandler(dt); + } + else + { + //If the window is minimized, take a breather. + Thread.Sleep(1); + } + } + private bool disposed; public void Dispose() { diff --git a/Demos.GL/Broken/CompoundFallsThroughFloor.cs b/Demos.GL/Broken/CompoundFallsThroughFloor.cs new file mode 100644 index 000000000..8414f2bd0 --- /dev/null +++ b/Demos.GL/Broken/CompoundFallsThroughFloor.cs @@ -0,0 +1,55 @@ + +using BepuPhysics; +using BepuPhysics.Collidables; +using BepuUtilities; +using DemoContentLoader; +using DemoRenderer; +using DemoUtilities; +using System.Numerics; + +namespace Demos.Broken; + +internal class CompoundFallsThroughFloor : Demo +{ + private BodyActivityDescription NeverSleep() => new BodyActivityDescription(sleepThreshold: float.MinValue, minimumTimestepCountUnderThreshold: byte.MaxValue); + + public override void Initialize(ContentArchive content, Camera camera) + { + camera.Position = new Vector3(-1000f, 400, -1000f); + camera.Yaw = MathHelper.Pi * 3f / 4; + camera.Pitch = MathHelper.Pi * 0.1f; + + var narrowPhaseCallbacks = new SimpleNarrowPhaseCallbacks( + frictionCoefficient: 0.3f, + maximumRecoveryVelocity: 50, + impactFrequency: 20, + impactDampingRatio: 0.5f + ); + + var gravity = new Vector3(0, -9807f, 0); // mm/s + var timeStepSeconds = .12f; + + Simulation = Simulation.Create(BufferPool, narrowPhaseCallbacks, new PoseIntegratorCallbacks(gravity: gravity, timeStepSeconds: timeStepSeconds), new SolveDescription(8, 1)); + var floor = new Box(1000, 9, 1000); + + Simulation.Statics.Add(new StaticDescription(RigidPose.Identity, Simulation.Shapes.Add(floor))); + + // Item as a standalone (not compound) + { + var rawCollidableBox = new Box(100, 130, 100); + var boxPose = new RigidPose(new Vector3(-500, rawCollidableBox.HalfHeight + 18, -500), Quaternion.Identity); + var boxInertia = rawCollidableBox.ComputeInertia(1); + var boxIndex = Simulation.Shapes.Add(rawCollidableBox); + var collisionDetection = new ContinuousDetection(); + var boxCollidable = new CollidableDescription(boxIndex, collisionDetection); + var boxDescription = BodyDescription.CreateDynamic(boxPose, boxInertia, boxCollidable, NeverSleep()); + Simulation.Bodies.Add(boxDescription); + } + + } + + public override void Update(Window window, Camera camera, Input input, float dt) + { + base.Update(window, camera, input, dt); + } +} diff --git a/Demos.GL/Broken/PoseIntegratorCallbacks.cs b/Demos.GL/Broken/PoseIntegratorCallbacks.cs new file mode 100644 index 000000000..d14fcf7eb --- /dev/null +++ b/Demos.GL/Broken/PoseIntegratorCallbacks.cs @@ -0,0 +1,33 @@ +using BepuPhysics; +using BepuUtilities; +using System.Numerics; + +namespace Demos.Broken; + +/// +/// An implementation of INarrowPhaseCallbacks (a BePu interface) needed to get simulation running with BePu. +/// +/// Basically a verbatim copy from the example given here: https://github.com/bepu/bepuphysics2/blob/master/Demos/Demos/SimpleSelfContainedDemo.cs +public struct PoseIntegratorCallbacks : IPoseIntegratorCallbacks +{ + public void Initialize(Simulation simulation) { } + + public readonly AngularIntegrationMode AngularIntegrationMode => AngularIntegrationMode.Nonconserving; + + public readonly bool AllowSubstepsForUnconstrainedBodies => false; + + public readonly bool IntegrateVelocityForKinematics => false; + + private readonly Vector3Wide _gravityWideDt; + public PoseIntegratorCallbacks(Vector3 gravity, float timeStepSeconds) : this() + { + _gravityWideDt = Vector3Wide.Broadcast(gravity * timeStepSeconds); + } + + public void IntegrateVelocity(Vector bodyIndices, Vector3Wide position, QuaternionWide orientation, BodyInertiaWide localInertia, Vector integrationMask, int workerIndex, Vector dt, ref BodyVelocityWide velocity) + { + velocity.Linear += _gravityWideDt; + } + + public void PrepareForIntegration(float dt) { } +} diff --git a/Demos.GL/Broken/SimpleNarrowPhaseCallbacks.cs b/Demos.GL/Broken/SimpleNarrowPhaseCallbacks.cs new file mode 100644 index 000000000..0d129de3e --- /dev/null +++ b/Demos.GL/Broken/SimpleNarrowPhaseCallbacks.cs @@ -0,0 +1,52 @@ +using BepuPhysics; +using BepuPhysics.Collidables; +using BepuPhysics.CollisionDetection; +using BepuPhysics.Constraints; + +namespace Demos.Broken; + +/// +/// An implementation of INarrowPhaseCallbacks (a BePu interface) needed to get simulation running with BePu. +/// +/// Roughly a verbatim copy of the example given here: https://github.com/bepu/bepuphysics2/blob/master/Demos/Demos/SimpleSelfContainedDemo.cs +public struct SimpleNarrowPhaseCallbacks : INarrowPhaseCallbacks +{ + private readonly float _frictionCoefficient; + private readonly float _maximumRecoveryVelocity; + private readonly float _impactFrequency; + private readonly float _impactDampingRatio; + + //TODO (Nigel): verify that these populate in the online docs without the summary tag + + /// Coefficient of friction to apply for the constraint. Maximum friction force will be equal to the normal force times the friction coefficient. + /// Maximum relative velocity along the contact normal at which the collision constraint will recover from penetration. Clamps the velocity goal created from the spring settings. + /// Target number of undamped oscillations per unit of time. + /// Ratio of the spring's actual damping to its critical damping. 0 is undamped, 1 is critically damped, and higher values are overdamped. + public SimpleNarrowPhaseCallbacks(float frictionCoefficient, float maximumRecoveryVelocity, float impactFrequency, float impactDampingRatio) + { + _frictionCoefficient = frictionCoefficient; + _maximumRecoveryVelocity = maximumRecoveryVelocity; + _impactFrequency = impactFrequency; + _impactDampingRatio = impactDampingRatio; + } + public void Initialize(Simulation simulation) { } + + public void Dispose() { } + + public bool AllowContactGeneration(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB) => true; + public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, ref TManifold manifold, out PairMaterialProperties pairMaterial) where TManifold : unmanaged, IContactManifold + { + pairMaterial.FrictionCoefficient = _frictionCoefficient; + pairMaterial.MaximumRecoveryVelocity = _maximumRecoveryVelocity; + pairMaterial.SpringSettings = new SpringSettings(_impactFrequency, _impactDampingRatio); + //For the purposes of the demo, contact constraints are always generated. + return true; + } + public bool ConfigureContactManifold(int workerIndex, CollidablePair pair, int childIndexA, int childIndexB, ref ConvexContactManifold manifold) => true; + + public bool AllowContactGeneration(int workerIndex, CollidableReference a, CollidableReference b, ref float speculativeMargin) + { + + return a.Mobility == CollidableMobility.Dynamic || b.Mobility == CollidableMobility.Dynamic; + } +} diff --git a/Demos/DemoSet.cs b/Demos/DemoSet.cs index e2b78ac67..5bc51752b 100644 --- a/Demos/DemoSet.cs +++ b/Demos/DemoSet.cs @@ -1,12 +1,12 @@ using DemoContentLoader; using DemoRenderer; +using Demos.Broken; using Demos.Demos; using Demos.Demos.Cars; using Demos.Demos.Characters; using Demos.Demos.Dancers; using Demos.Demos.Sponsors; using Demos.Demos.Tanks; -using Demos.SpecializedTests; using System; using System.Collections.Generic; @@ -44,6 +44,7 @@ struct Option public DemoSet() { + AddOption(); AddOption(); AddOption(); AddOption(); diff --git a/Demos/GameLoop.cs b/Demos/GameLoop.cs index ddedab484..8e0435fab 100644 --- a/Demos/GameLoop.cs +++ b/Demos/GameLoop.cs @@ -1,8 +1,8 @@ -using DemoRenderer; +using BepuUtilities; +using BepuUtilities.Memory; +using DemoRenderer; using DemoUtilities; using System; -using BepuUtilities; -using BepuUtilities.Memory; namespace Demos; @@ -29,10 +29,10 @@ public GameLoop(Window window) window.Resolution, enableDeviceDebugLayer: false ); Renderer = new Renderer(Surface); - Camera = new Camera(window.Resolution.X / (float)window.Resolution.Y, (float)Math.PI / 3, 0.01f, 100000); + Camera = new Camera(window.Resolution.X / (float)window.Resolution.Y, (float)Math.PI / 3, 0.01f, 100000); } - void Update(float dt) + public void Update(float dt) { Input.Start(); if (DemoHarness != null) @@ -53,6 +53,12 @@ public void Run(DemoHarness harness) Window.Run(Update, OnResize); } + public void SingleFrame(DemoHarness harness, float dt) + { + DemoHarness = harness; + Window.SingleFrame(Update, OnResize, dt); + } + private void OnResize(Int2 resolution) { //We just don't support true fullscreen in the demos. Would be pretty pointless. diff --git a/Demos/Program.cs b/Demos/Program.cs index aaa4f3ca5..84e7a23de 100644 --- a/Demos/Program.cs +++ b/Demos/Program.cs @@ -2,12 +2,41 @@ using DemoContentLoader; using DemoUtilities; using OpenTK; +using System.Threading; +using System.Threading.Tasks; namespace Demos; class Program { - static void Main() + + + static async Task Main() + { + var window = new Window("pretty cool multicolored window", + new Int2((int)(DisplayDevice.Default.Width * 0.75f), (int)(DisplayDevice.Default.Height * 0.75f)), WindowMode.Windowed); + var loop = new GameLoop(window); + ContentArchive content; + using (var stream = typeof(Program).Assembly.GetManifestResourceStream("Demos.Demos.contentarchive")) + { + content = ContentArchive.Load(stream); + } + //HeadlessTest.Test(content, 4, 32, 512); + var demo = new DemoHarness(loop, content); + var dt = 0.12f; + loop.Update(dt); + + while (true) + { + loop.SingleFrame(demo, dt); + Thread.Sleep(1000); + } + + loop.Dispose(); + window.Dispose(); + } + + static void OldMain() { var window = new Window("pretty cool multicolored window", new Int2((int)(DisplayDevice.Default.Width * 0.75f), (int)(DisplayDevice.Default.Height * 0.75f)), WindowMode.Windowed); From a09b2175d31704e09eec090c4f39462b8655f6c0 Mon Sep 17 00:00:00 2001 From: Nigel-Bess Date: Wed, 5 Nov 2025 23:08:21 -0800 Subject: [PATCH 2/6] got demo working --- Demos/Program.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Demos/Program.cs b/Demos/Program.cs index 84e7a23de..957d32c9e 100644 --- a/Demos/Program.cs +++ b/Demos/Program.cs @@ -2,16 +2,27 @@ using DemoContentLoader; using DemoUtilities; using OpenTK; +using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; + + namespace Demos; + + class Program { + [DllImport("user32.dll")] + static extern short GetAsyncKeyState(int key); + + static bool IsKeyPressed(int key) => (GetAsyncKeyState(key) & 0x8000) != 0; + + + - static async Task Main() + static void Main() { var window = new Window("pretty cool multicolored window", new Int2((int)(DisplayDevice.Default.Width * 0.75f), (int)(DisplayDevice.Default.Height * 0.75f)), WindowMode.Windowed); @@ -28,8 +39,13 @@ static async Task Main() while (true) { - loop.SingleFrame(demo, dt); - Thread.Sleep(1000); + if (IsKeyPressed(0x1B)) break; // escape + if (IsKeyPressed(0x27)) // right arrow + { + loop.SingleFrame(demo, dt); + Thread.Sleep((int)(dt * 1000)); + } + Thread.Sleep(100); } loop.Dispose(); From fb79f455c8259ffc15baf610d727c00f67752032 Mon Sep 17 00:00:00 2001 From: Nigel-Bess Date: Wed, 5 Nov 2025 23:24:10 -0800 Subject: [PATCH 3/6] item doesn't fall through anymore --- Demos.GL/Broken/CompoundFallsThroughFloor.cs | 6 +++--- Demos/Program.cs | 15 +++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Demos.GL/Broken/CompoundFallsThroughFloor.cs b/Demos.GL/Broken/CompoundFallsThroughFloor.cs index 8414f2bd0..090ddeda6 100644 --- a/Demos.GL/Broken/CompoundFallsThroughFloor.cs +++ b/Demos.GL/Broken/CompoundFallsThroughFloor.cs @@ -21,7 +21,7 @@ public override void Initialize(ContentArchive content, Camera camera) var narrowPhaseCallbacks = new SimpleNarrowPhaseCallbacks( frictionCoefficient: 0.3f, - maximumRecoveryVelocity: 50, + maximumRecoveryVelocity: 500, impactFrequency: 20, impactDampingRatio: 0.5f ); @@ -30,14 +30,14 @@ public override void Initialize(ContentArchive content, Camera camera) var timeStepSeconds = .12f; Simulation = Simulation.Create(BufferPool, narrowPhaseCallbacks, new PoseIntegratorCallbacks(gravity: gravity, timeStepSeconds: timeStepSeconds), new SolveDescription(8, 1)); - var floor = new Box(1000, 9, 1000); + var floor = new Box(1000, 30, 1000); Simulation.Statics.Add(new StaticDescription(RigidPose.Identity, Simulation.Shapes.Add(floor))); // Item as a standalone (not compound) { var rawCollidableBox = new Box(100, 130, 100); - var boxPose = new RigidPose(new Vector3(-500, rawCollidableBox.HalfHeight + 18, -500), Quaternion.Identity); + var boxPose = new RigidPose(new Vector3(-300, rawCollidableBox.HalfHeight + floor.HalfHeight + 18, 300), Quaternion.Identity); var boxInertia = rawCollidableBox.ComputeInertia(1); var boxIndex = Simulation.Shapes.Add(rawCollidableBox); var collisionDetection = new ContinuousDetection(); diff --git a/Demos/Program.cs b/Demos/Program.cs index 957d32c9e..e0551d406 100644 --- a/Demos/Program.cs +++ b/Demos/Program.cs @@ -2,6 +2,7 @@ using DemoContentLoader; using DemoUtilities; using OpenTK; +using System; using System.Runtime.InteropServices; using System.Threading; @@ -36,16 +37,22 @@ static void Main() var demo = new DemoHarness(loop, content); var dt = 0.12f; loop.Update(dt); - + DateTime? arrowDownTime = null; + var holdDownTimeMs = 500; while (true) { if (IsKeyPressed(0x1B)) break; // escape - if (IsKeyPressed(0x27)) // right arrow + if (!IsKeyPressed(0x27)) + { + arrowDownTime = null; + Thread.Sleep(10); + continue; + } + if (arrowDownTime is null || (DateTime.Now - arrowDownTime.Value).TotalMilliseconds > holdDownTimeMs) { loop.SingleFrame(demo, dt); - Thread.Sleep((int)(dt * 1000)); + Thread.Sleep(50); } - Thread.Sleep(100); } loop.Dispose(); From 99c3bf63259c5055fae35b8b1ae469e9de399817 Mon Sep 17 00:00:00 2001 From: Nigel-Bess Date: Wed, 5 Nov 2025 23:32:46 -0800 Subject: [PATCH 4/6] built complete demo --- Demos.GL/Broken/CompoundFallsThroughFloor.cs | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Demos.GL/Broken/CompoundFallsThroughFloor.cs b/Demos.GL/Broken/CompoundFallsThroughFloor.cs index 090ddeda6..2d14ed2f2 100644 --- a/Demos.GL/Broken/CompoundFallsThroughFloor.cs +++ b/Demos.GL/Broken/CompoundFallsThroughFloor.cs @@ -30,15 +30,20 @@ public override void Initialize(ContentArchive content, Camera camera) var timeStepSeconds = .12f; Simulation = Simulation.Create(BufferPool, narrowPhaseCallbacks, new PoseIntegratorCallbacks(gravity: gravity, timeStepSeconds: timeStepSeconds), new SolveDescription(8, 1)); - var floor = new Box(1000, 30, 1000); + var floor = new Box(1000, 9, 1000); Simulation.Statics.Add(new StaticDescription(RigidPose.Identity, Simulation.Shapes.Add(floor))); + + // shared constants + const float itemMass = 1; + const float startingHeight = 18; + // Item as a standalone (not compound) { var rawCollidableBox = new Box(100, 130, 100); - var boxPose = new RigidPose(new Vector3(-300, rawCollidableBox.HalfHeight + floor.HalfHeight + 18, 300), Quaternion.Identity); - var boxInertia = rawCollidableBox.ComputeInertia(1); + var boxPose = new RigidPose(new Vector3(-300, rawCollidableBox.HalfHeight + floor.HalfHeight + startingHeight, 300), Quaternion.Identity); + var boxInertia = rawCollidableBox.ComputeInertia(itemMass); var boxIndex = Simulation.Shapes.Add(rawCollidableBox); var collisionDetection = new ContinuousDetection(); var boxCollidable = new CollidableDescription(boxIndex, collisionDetection); @@ -46,6 +51,17 @@ public override void Initialize(ContentArchive content, Camera camera) Simulation.Bodies.Add(boxDescription); } + // Item as compound + { + var childShape = new Box(100, 130, 100); + var compoundPose = new RigidPose(new Vector3(300, childShape.HalfHeight + floor.HalfHeight + startingHeight, -300), Quaternion.Identity); + var builder = new CompoundBuilder(BufferPool, Simulation.Shapes, 1); + + builder.Add(childShape, RigidPose.Identity, itemMass); + builder.BuildDynamicCompound(out var children, out var inertia); + Simulation.Bodies.Add(BodyDescription.CreateDynamic(compoundPose, inertia, Simulation.Shapes.Add(new Compound(children)), NeverSleep())); + } + } public override void Update(Window window, Camera camera, Input input, float dt) From eaead869c884d547e32bcd9f6889b308cb0b223c Mon Sep 17 00:00:00 2001 From: Nigel-Bess Date: Thu, 6 Nov 2025 00:05:24 -0800 Subject: [PATCH 5/6] fix timestep --- Demos/Demo.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Demos/Demo.cs b/Demos/Demo.cs index 329db2071..be2d6506e 100644 --- a/Demos/Demo.cs +++ b/Demos/Demo.cs @@ -1,11 +1,11 @@ -using BepuUtilities.Memory; +using BepuPhysics; +using BepuUtilities; +using BepuUtilities.Memory; +using DemoContentLoader; using DemoRenderer; +using DemoRenderer.UI; using DemoUtilities; -using BepuPhysics; using System; -using DemoRenderer.UI; -using DemoContentLoader; -using BepuUtilities; namespace Demos; @@ -62,7 +62,7 @@ public virtual void Update(Window window, Camera camera, Input input, float dt) //fully decouple simulation and rendering rates across different threads. //(In either case, you'd also want to interpolate or extrapolate simulation results during rendering for smoothness.) //Note that taking steps of variable length can reduce stability. Gradual or one-off changes can work reasonably well. - Simulation.Timestep(TimestepDuration, ThreadDispatcher); + Simulation.Timestep(dt, ThreadDispatcher); ////Here's an example of how it would look to use more frequent updates, but still with a fixed amount of time simulated per update call: //const float timeToSimulate = 1 / 60f; From 208f94797651ecaa4b17bfbcd3ca7276a352af41 Mon Sep 17 00:00:00 2001 From: Nigel-Bess Date: Thu, 6 Nov 2025 00:12:04 -0800 Subject: [PATCH 6/6] show first frame --- Demos/Demo.cs | 1 + Demos/Program.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Demos/Demo.cs b/Demos/Demo.cs index be2d6506e..8594fa69e 100644 --- a/Demos/Demo.cs +++ b/Demos/Demo.cs @@ -56,6 +56,7 @@ public virtual void LoadGraphicalContent(ContentArchive content, RenderSurface s public const float TimestepDuration = 1 / 60f; public virtual void Update(Window window, Camera camera, Input input, float dt) { + if (dt == 0) return; //In the demos, we use one time step per frame. We don't bother modifying the physics time step duration for different monitors so different refresh rates //change the rate of simulation. This doesn't actually change the result of the simulation, though, and the simplicity is a good fit for the demos. //In the context of a 'real' application, you could instead use a time accumulator to take time steps of fixed length as needed, or diff --git a/Demos/Program.cs b/Demos/Program.cs index e0551d406..ec29cce7f 100644 --- a/Demos/Program.cs +++ b/Demos/Program.cs @@ -36,9 +36,9 @@ static void Main() //HeadlessTest.Test(content, 4, 32, 512); var demo = new DemoHarness(loop, content); var dt = 0.12f; - loop.Update(dt); DateTime? arrowDownTime = null; var holdDownTimeMs = 500; + loop.SingleFrame(demo, 0); while (true) { if (IsKeyPressed(0x1B)) break; // escape