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..2d14ed2f2 --- /dev/null +++ b/Demos.GL/Broken/CompoundFallsThroughFloor.cs @@ -0,0 +1,71 @@ + +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: 500, + 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))); + + + // 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 + 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); + var boxDescription = BodyDescription.CreateDynamic(boxPose, boxInertia, boxCollidable, NeverSleep()); + 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) + { + 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/Demo.cs b/Demos/Demo.cs index 329db2071..8594fa69e 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; @@ -56,13 +56,14 @@ 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 //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; 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..ec29cce7f 100644 --- a/Demos/Program.cs +++ b/Demos/Program.cs @@ -2,12 +2,64 @@ using DemoContentLoader; using DemoUtilities; using OpenTK; +using System; +using System.Runtime.InteropServices; +using System.Threading; + + namespace Demos; + + class Program { + + [DllImport("user32.dll")] + static extern short GetAsyncKeyState(int key); + + static bool IsKeyPressed(int key) => (GetAsyncKeyState(key) & 0x8000) != 0; + + + + 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); + 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; + DateTime? arrowDownTime = null; + var holdDownTimeMs = 500; + loop.SingleFrame(demo, 0); + while (true) + { + if (IsKeyPressed(0x1B)) break; // escape + 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(50); + } + } + + 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);