diff --git a/src/Box2D.NET.Samples/SampleApp.cs b/src/Box2D.NET.Samples/SampleApp.cs index a1a6248f..b6b7d9cc 100644 --- a/src/Box2D.NET.Samples/SampleApp.cs +++ b/src/Box2D.NET.Samples/SampleApp.cs @@ -27,6 +27,7 @@ using static Box2D.NET.B2Timers; using static Box2D.NET.Samples.Graphics.Draws; using static Box2D.NET.Samples.Graphics.Cameras; +using static Box2D.NET.Samples.Samples.Sample; using ErrorCode = Silk.NET.GLFW.ErrorCode; using Monitor = Silk.NET.GLFW.Monitor; using MouseButton = Silk.NET.GLFW.MouseButton; @@ -41,8 +42,6 @@ public class SampleApp private IWindow _window; private IInputContext _input; private ImGuiController _imgui; - private int s_selection = 0; - private Sample s_sample = null; private SampleContext _context; private bool s_rightMouseDown = false; private B2Vec2 s_clickPointWS = b2Vec2_zero; @@ -167,8 +166,7 @@ private void OnWindowClosingSafe() private void OnWindowClosing() { - s_sample?.Dispose(); - s_sample = null; + _context.sample?.Dispose(); DestroyDraw(_context.draw); DestroyUI(); } @@ -246,7 +244,6 @@ private void OnWindowLoad() _context.draw = CreateDraw(_context); _context.sampleIndex = b2ClampInt(_context.sampleIndex, 0, SampleFactory.Shared.SampleCount - 1); - s_selection = _context.sampleIndex; // todo put this in _context.settings CreateUI(glslVersion); @@ -303,27 +300,13 @@ private void OnWindowUpdate(double dt) // For the Tracy profiler //FrameMark; - if (s_selection != _context.sampleIndex) - { - ResetView(_context.camera); - _context.sampleIndex = s_selection; - - // #todo restore all drawing settings that may have been overridden by a sample - _context.subStepCount = 4; - _context.debugDraw.drawJoints = true; - - s_sample?.Dispose(); - s_sample = null; - s_sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); - } - - if (s_sample == null) + if (_context.sample == null) { // delayed creation because imgui doesn't create fonts until NewFrame() is called - s_sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); + _context.sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); } - s_sample.Step(); + _context.sample.Step(); _context.glfw.PollEvents(); @@ -374,19 +357,19 @@ private void OnWindowRender(double dt) ImGui.Begin("Overlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar); ImGui.End(); - s_sample.ResetText(); + _context.sample.ResetText(); var title = SampleFactory.Shared.GetTitle(_context.sampleIndex); - s_sample.DrawColoredTextLine(B2HexColor.b2_colorYellow, title); + _context.sample.DrawColoredTextLine(B2HexColor.b2_colorYellow, title); - string buffer = $"{1000.0f * _frameTime:0.0} ms - step {s_sample.m_stepCount} - " + + string buffer = $"{1000.0f * _frameTime:0.0} ms - step {_context.sample.m_stepCount} - " + $"camera ({_context.camera.center.X:G}, {_context.camera.center.Y:G}, {_context.camera.zoom:G})"; DrawScreenString(_context.draw, 5.0f, _context.camera.height - 18.0f, B2HexColor.b2_colorSeaGreen, buffer); - s_sample.Draw(); + _context.sample.Draw(); FlushDraw(_context.draw, _context.camera); - UpdateUI(); + UpdateSampleUI(_context); //ImGui.ShowDemoWindow(); @@ -443,16 +426,6 @@ private void glfwErrorCallback(ErrorCode error, string description) Logger.Information($"GLFW error occurred. Code: {error}. Description: {description}"); } - private void RestartSample() - { - s_sample?.Dispose(); - s_sample = null; - _context.restart = true; - - s_sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); - _context.restart = false; - } - private void CreateUI(string glslVersion) { //IMGUI_CHECKVERSION(); @@ -541,7 +514,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(2.0f, 0.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -555,7 +528,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(-2.0f, 0.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -569,7 +542,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(0.0f, 2.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -583,7 +556,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(0.0f, -2.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -597,7 +570,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In break; case Keys.R: - RestartSample(); + SelectSample(_context, _context.sampleIndex, true); break; case Keys.O: @@ -610,20 +583,28 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In case Keys.LeftBracket: // Switch to previous test - --s_selection; - if (s_selection < 0) { - s_selection = SampleFactory.Shared.SampleCount - 1; + int selection = _context.sampleIndex - 1; + if (selection < 0) + { + selection = SampleFactory.Shared.SampleCount - 1; + } + + SelectSample(_context, selection, false); } break; case Keys.RightBracket: // Switch to next test - ++s_selection; - if (s_selection == SampleFactory.Shared.SampleCount) { - s_selection = 0; + int selection = _context.sampleIndex + 1; + if (selection == SampleFactory.Shared.SampleCount) + { + selection = 0; + } + + SelectSample(_context, selection, false); } break; @@ -633,9 +614,9 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In break; default: - if (null != s_sample) + if (_context.sample != null) { - s_sample.Keyboard(key); + _context.sample.Keyboard(key); } break; @@ -681,12 +662,12 @@ private unsafe void MouseButtonCallback(WindowHandle* window, MouseButton button B2Vec2 pw = ConvertScreenToWorld(_context.camera, ps); if (action == InputAction.Press) { - s_sample.MouseDown(pw, button, modifiers); + _context.sample.MouseDown(pw, button, modifiers); } if (action == InputAction.Release) { - s_sample.MouseUp(pw, button); + _context.sample.MouseUp(pw, button); } } else if (button == MouseButton.Right) @@ -722,7 +703,7 @@ private unsafe void MouseMotionCallback(WindowHandle* window, double xd, double //ImGui_ImplGlfw_CursorPosCallback(window, ps.x, ps.y); B2Vec2 pw = ConvertScreenToWorld(_context.camera, ps); - s_sample?.MouseMove(pw); + _context.sample.MouseMove(pw); if (s_rightMouseDown) { @@ -747,6 +728,10 @@ private unsafe void ScrollCallback(WindowHandle* window, double dx, double dy) return; } + _context.glfw.GetCursorPos(_context.window, out double xd, out double yd); + B2Vec2 ps = new B2Vec2((float)xd, (float)yd); + B2Vec2 pw1 = ConvertScreenToWorld(_context.camera, ps); + if (dy > 0) { _context.camera.zoom /= 1.1f; @@ -755,182 +740,9 @@ private unsafe void ScrollCallback(WindowHandle* window, double dx, double dy) { _context.camera.zoom *= 1.1f; } - } - - private void UpdateUI() - { - int maxWorkers = B2_MAX_WORKERS; - - float fontSize = ImGui.GetFontSize(); - float menuWidth = 13.0f * fontSize; - if (_context.showUI) - { - ImGui.SetNextWindowPos(new Vector2(_context.camera.width - menuWidth - 0.5f * fontSize, 0.5f * fontSize)); - ImGui.SetNextWindowSize(new Vector2(menuWidth, _context.camera.height - fontSize)); - - ImGui.Begin("Tools", ref _context.showUI, ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse); - - if (ImGui.BeginTabBar("ControlTabs", ImGuiTabBarFlags.None)) - { - if (ImGui.BeginTabItem("Controls")) - { - ImGui.PushItemWidth(100.0f); - ImGui.SliderInt("Sub-steps", ref _context.subStepCount, 1, 32); - ImGui.SliderFloat("Hertz", ref _context.hertz, 5.0f, 240.0f, "%.0f hz"); - - if (ImGui.SliderInt("Workers", ref _context.workerCount, 1, maxWorkers)) - { - _context.workerCount = b2ClampInt(_context.workerCount, 1, maxWorkers); - RestartSample(); - } - - ImGui.PopItemWidth(); - - ImGui.Separator(); - - ImGui.Checkbox("Sleep", ref _context.enableSleep); - ImGui.Checkbox("Warm Starting", ref _context.enableWarmStarting); - ImGui.Checkbox("Continuous", ref _context.enableContinuous); - ImGui.Checkbox("Contact Recycling", ref _context.enableRecycling); - - ImGui.Separator(); - - ImGui.Checkbox("Shapes", ref _context.debugDraw.drawShapes); - ImGui.Checkbox("Joints", ref _context.debugDraw.drawJoints); - ImGui.Checkbox("Joint Extras", ref _context.debugDraw.drawJointExtras); - ImGui.Checkbox("Bounds", ref _context.debugDraw.drawBounds); - ImGui.Checkbox("Mass", ref _context.debugDraw.drawMass); - ImGui.Checkbox("Body Names", ref _context.debugDraw.drawBodyNames); - ImGui.Checkbox("Graph Colors", ref _context.debugDraw.drawGraphColors); - ImGui.Checkbox("Islands", ref _context.debugDraw.drawIslands); - ImGui.Checkbox("Counters", ref _context.drawCounters); - ImGui.Checkbox("Profile", ref _context.drawProfile); - ImGui.Checkbox("Frame Time", ref _context.frameTime); - - ImGui.Separator(); - - { - bool changed = false; - string[] drawTypes = - [ - "None", "Clip", "AnchorA", "AnchorB", "Average" - ]; - int drawType = (int)_context.debugDraw.contactDrawType; - changed = changed || ImGui.Combo("Contact", ref drawType, drawTypes, drawTypes.Length); - _context.debugDraw.contactDrawType = (B2ContactDrawType)drawType; - } - - ImGui.Checkbox("Contact Normals", ref _context.debugDraw.drawContactNormals); - ImGui.Checkbox("Contact Features", ref _context.debugDraw.drawContactFeatures); - ImGui.Checkbox("Contact Forces", ref _context.debugDraw.drawContactForces); - ImGui.Checkbox("Friction Forces", ref _context.debugDraw.drawFrictionForces); - - ImGui.Separator(); - - - ImGui.PushItemWidth(80.0f); - ImGui.InputFloat("Joint Scale", ref _context.debugDraw.jointScale); - ImGui.InputFloat("Force Scale", ref _context.debugDraw.forceScale); - ImGui.PopItemWidth(); - - Vector2 button_sz = new Vector2(-1, 0); - if (ImGui.Button("Pause (P)", button_sz)) - { - _context.pause = !_context.pause; - } - - if (ImGui.Button("Single Step (O)", button_sz)) - { - _context.singleStep = !_context.singleStep; - } - - if (ImGui.Button("Dump Mem Stats", button_sz)) - { - b2World_DumpMemoryStats(s_sample.m_worldId); - } - - if (ImGui.Button("Reset Profile", button_sz)) - { - s_sample.ResetProfile(); - } - - if (ImGui.Button("Restart (R)", button_sz)) - { - RestartSample(); - } - - if (ImGui.Button("Quit", button_sz)) - { - unsafe - { - _context.glfw.SetWindowShouldClose(_context.window, true); - } - } - - ImGui.EndTabItem(); - } - ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; - leafNodeFlags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; - - ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; - - if (ImGui.BeginTabItem("Samples")) - { - int categoryIndex = 0; - string category = SampleFactory.Shared.GetCategory(categoryIndex); - int i = 0; - while (i < SampleFactory.Shared.SampleCount) - { - bool categorySelected = category == SampleFactory.Shared.GetCategory(_context.sampleIndex); - ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags.Selected : 0; - bool nodeOpen = ImGui.TreeNodeEx(category, nodeFlags | nodeSelectionFlags); - - if (nodeOpen) - { - while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) - { - ImGuiTreeNodeFlags selectionFlags = 0; - if (_context.sampleIndex == i) - { - selectionFlags = ImGuiTreeNodeFlags.Selected; - } - - ImGui.TreeNodeEx(SampleFactory.Shared.GetName(i), leafNodeFlags | selectionFlags); - if (ImGui.IsItemClicked()) - { - s_selection = i; - } - - ++i; - } - - ImGui.TreePop(); - } - else - { - while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) - { - ++i; - } - } - - if (i < SampleFactory.Shared.SampleCount) - { - category = SampleFactory.Shared.GetCategory(i); - categoryIndex = i; - } - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - } - - ImGui.End(); - - s_sample.UpdateGui(); - } + B2Vec2 pw2 = ConvertScreenToWorld(_context.camera, ps); + _context.camera.center -= pw2 - pw1; } + } diff --git a/src/Box2D.NET.Samples/SampleContext.cs b/src/Box2D.NET.Samples/SampleContext.cs index a10ad239..a55a0e31 100644 --- a/src/Box2D.NET.Samples/SampleContext.cs +++ b/src/Box2D.NET.Samples/SampleContext.cs @@ -6,10 +6,12 @@ using System.IO; using System.Runtime.CompilerServices; using Box2D.NET.Samples.Graphics; +using Box2D.NET.Samples.Samples; using ImGuiNET; using Silk.NET.GLFW; using Silk.NET.OpenGL; using static Box2D.NET.B2Types; +using static Box2D.NET.B2Constants; using static Box2D.NET.Samples.Graphics.Backgrounds; using static Box2D.NET.Samples.Graphics.Draws; @@ -20,11 +22,20 @@ public class SampleContext // public readonly string Signature; public readonly Glfw glfw; + public GL gl; + public unsafe WindowHandle* window; public readonly Camera camera; public Draw draw; + public Sample sample = null; + public B2Capacity capacity; + public B2DebugDraw debugDraw; + public ImFontPtr regularFont; + public ImFontPtr mediumFont; + public ImFontPtr largeFont; public float uiScale = 1.0f; public float hertz = 60.0f; + public float recycleDistance = 0.05f; public int subStepCount = 4; public int workerCount = 1; public bool restart = false; @@ -34,25 +45,12 @@ public class SampleContext public bool drawProfile = false; public bool enableWarmStarting = true; public bool enableContinuous = true; - public bool enableRecycling = true; public bool enableSleep = true; public bool showUI = true; public bool frameTime = false; // These are persisted public int sampleIndex = 0; - public B2Capacity capacity; - - - public B2DebugDraw debugDraw; - - public ImFontPtr regularFont; - public ImFontPtr mediumFont; - public ImFontPtr largeFont; - - // - public GL gl; - public unsafe WindowHandle* window; private static string CreateSignature(string member, string file, int line) { @@ -92,6 +90,8 @@ private SampleContext(string signature, Glfw glfw) public void Load() { + recycleDistance = B2_CONTACT_RECYCLE_DISTANCE; + var settings = Settings.Load(); // @@ -110,7 +110,6 @@ public void Load() debugDraw.drawJoints = settings.drawJoints; // - debugDraw.contactDrawType = settings.contactDrawType; debugDraw.drawShapes = settings.drawShapes; debugDraw.drawJoints = settings.drawJoints; debugDraw.drawJointExtras = settings.drawJointExtras; diff --git a/src/Box2D.NET.Samples/Samples/Sample.cs b/src/Box2D.NET.Samples/Samples/Sample.cs index 7db11327..f375e59c 100644 --- a/src/Box2D.NET.Samples/Samples/Sample.cs +++ b/src/Box2D.NET.Samples/Samples/Sample.cs @@ -59,13 +59,13 @@ public class Sample : IDisposable private ulong m_profileWriteIndex; // - private B2Profile m_totalProfile; private static bool s_showProfilePlots; private static readonly bool[] s_profileRowOpen = new bool[22]; // private bool m_didStep; + // private readonly float[] m_frameTimes; public Sample(SampleContext context) @@ -94,8 +94,6 @@ public Sample(SampleContext context) m_profileReadIndex = 0; m_profileWriteIndex = 0; - m_totalProfile = new B2Profile(); - g_randomSeed = RAND_SEED; CreateWorld(); @@ -124,6 +122,7 @@ public void CreateWorld() worldDef.capacity = m_context.capacity; m_worldId = b2CreateWorld(worldDef); + b2World_SetContactRecycleDistance(m_worldId, m_context.recycleDistance); } public void TestMathCpp() @@ -166,34 +165,86 @@ public virtual void UpdateGui() int count = (int)(m_profileWriteIndex - m_profileReadIndex); // Unroll ring buffer into per-field histories. - const int rowCount = 22; - float[][] histories = new float[rowCount][]; - for (int row = 0; row < rowCount; ++row) + const int kRowCount = 22; + float[][] histories = new float[kRowCount][]; + float[] totals = new float[kRowCount]; + for (int i = 0; i < kRowCount; ++i) { - histories[row] = new float[m_profileCapacity]; + histories[i] = new float[m_profileCapacity]; } for (int i = 0; i < count; ++i) { - int index = (int)((m_profileReadIndex + (ulong)i) & (m_profileCapacity - 1)); - B2Profile profile = m_profiles[index]; - for (int row = 0; row < rowCount; ++row) - { - histories[row][i] = GetProfileValue(profile, row); - } + int idx = (int)((m_profileReadIndex + (ulong)i) & (m_profileCapacity - 1)); + ref readonly B2Profile p = ref m_profiles[idx]; + histories[0][i] = p.step; + histories[1][i] = p.pairs; + histories[2][i] = p.collide; + histories[3][i] = p.solve; + histories[4][i] = p.solverSetup; + histories[5][i] = p.constraints; + histories[6][i] = p.prepareConstraints; + histories[7][i] = p.integrateVelocities; + histories[8][i] = p.warmStart; + histories[9][i] = p.solveImpulses; + histories[10][i] = p.integratePositions; + histories[11][i] = p.relaxImpulses; + histories[12][i] = p.applyRestitution; + histories[13][i] = p.storeImpulses; + histories[14][i] = p.splitIslands; + histories[15][i] = p.transforms; + histories[16][i] = p.jointEvents; + histories[17][i] = p.hitEvents; + histories[18][i] = p.refit; + histories[19][i] = p.sleepIslands; + histories[20][i] = p.bullets; + histories[21][i] = p.sensors; + + totals[0] += p.step; + totals[1] += p.pairs; + totals[2] += p.collide; + totals[3] += p.solve; + totals[4] += p.solverSetup; + totals[5] += p.constraints; + totals[6] += p.prepareConstraints; + totals[7] += p.integrateVelocities; + totals[8] += p.warmStart; + totals[9] += p.solveImpulses; + totals[10] += p.integratePositions; + totals[11] += p.relaxImpulses; + totals[12] += p.applyRestitution; + totals[13] += p.storeImpulses; + totals[14] += p.splitIslands; + totals[15] += p.transforms; + totals[16] += p.jointEvents; + totals[17] += p.hitEvents; + totals[18] += p.refit; + totals[19] += p.sleepIslands; + totals[20] += p.bullets; + totals[21] += p.sensors; } - float[] avg = new float[rowCount]; - if (m_stepCount > 0) + ref readonly B2Profile cur = ref m_profiles[m_currentProfileIndex]; + float[] now = + [ + cur.step, cur.pairs, cur.collide, cur.solve, cur.solverSetup, + cur.constraints, cur.prepareConstraints, cur.integrateVelocities, cur.warmStart, + cur.solveImpulses, cur.integratePositions, cur.relaxImpulses, cur.applyRestitution, + cur.storeImpulses, cur.splitIslands, cur.transforms, cur.jointEvents, + cur.hitEvents, cur.refit, cur.sleepIslands, cur.bullets, cur.sensors + ]; + + // Rolling average + float[] avg = new float[kRowCount]; + if (count > 0) { - float scale = 1.0f / m_stepCount; - for (int row = 0; row < rowCount; ++row) + float scale = 1.0f / count; + for (int i = 0; i < kRowCount; ++i) { - avg[row] = scale * GetProfileValue(m_totalProfile, row); + avg[i] = scale * totals[i]; } } - ref readonly B2Profile current = ref m_profiles[m_currentProfileIndex]; string[] names = [ "step", "pairs", "collide", "solve", "setup", "constraints", "prepare", @@ -203,11 +254,11 @@ public virtual void UpdateGui() ]; int[] indents = [0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0]; // Derive parent/child links from the indent levels so we can collapse subtrees. - int[] parents = new int[rowCount]; - bool[] hasChildren = new bool[rowCount]; + int[] parents = new int[kRowCount]; + bool[] hasChildren = new bool[kRowCount]; int[] stack = new int[8]; int stackSize = 0; - for (int i = 0; i < rowCount; ++i) + for (int i = 0; i < kRowCount; ++i) { while (stackSize > 0 && indents[stack[stackSize - 1]] >= indents[i]) { @@ -237,15 +288,6 @@ public virtual void UpdateGui() colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault ]; - float[] now = - [ - current.step, current.pairs, current.collide, current.solve, current.solverSetup, - current.constraints, current.prepareConstraints, current.integrateVelocities, current.warmStart, - current.solveImpulses, current.integratePositions, current.relaxImpulses, current.applyRestitution, - current.storeImpulses, current.splitIslands, current.transforms, current.jointEvents, - current.hitEvents, current.refit, current.sleepIslands, current.bullets, current.sensors - ]; - if (ImGui.Button("Reset")) { ResetProfile(); @@ -272,9 +314,9 @@ public virtual void UpdateGui() float rowHeight = 1.5f * fontSize; // Bars are drawn relative to the step row so the proportions are visually consistent. - float stepNow = b2MaxFloat(current.step, 0.001f); + float stepNow = b2MaxFloat(cur.step, 0.001f); - for (int row = 0; row < rowCount; ++row) + for (int row = 0; row < kRowCount; ++row) { bool visible = true; for (int parent = parents[row]; parent >= 0; parent = parents[parent]) @@ -455,36 +497,6 @@ public virtual void UpdateGui() } } - private static float GetProfileValue(in B2Profile profile, int row) - { - return row switch - { - 0 => profile.step, - 1 => profile.pairs, - 2 => profile.collide, - 3 => profile.solve, - 4 => profile.solverSetup, - 5 => profile.constraints, - 6 => profile.prepareConstraints, - 7 => profile.integrateVelocities, - 8 => profile.warmStart, - 9 => profile.solveImpulses, - 10 => profile.integratePositions, - 11 => profile.relaxImpulses, - 12 => profile.applyRestitution, - 13 => profile.storeImpulses, - 14 => profile.splitIslands, - 15 => profile.transforms, - 16 => profile.jointEvents, - 17 => profile.hitEvents, - 18 => profile.refit, - 19 => profile.sleepIslands, - 20 => profile.bullets, - 21 => profile.sensors, - _ => 0.0f, - }; - } - private static Vector4 HexToColor(B2HexColor color) { uint hex = (uint)color; @@ -722,11 +734,7 @@ public void DrawTextLine(string text) public void ResetProfile() { - m_totalProfile = new B2Profile(); m_stepCount = 0; - m_currentProfileIndex = 0; - m_profileReadIndex = 0; - m_profileWriteIndex = 0; } public virtual void Step() @@ -769,19 +777,9 @@ public virtual void Step() b2World_EnableWarmStarting(m_worldId, m_context.enableWarmStarting); b2World_EnableContinuous(m_worldId, m_context.enableContinuous); - if (m_context.enableRecycling) - { - b2World_SetContactRecycleDistance(m_worldId, B2_CONTACT_RECYCLE_DISTANCE); - } - else - { - b2World_SetContactRecycleDistance(m_worldId, 0.0f); - } - for (int i = 0; i < 1; ++i) { b2World_Step(m_worldId, timeStep, m_context.subStepCount); - // m_taskCount = 0; } if (timeStep > 0.0f) @@ -799,33 +797,6 @@ public virtual void Step() m_profileWriteIndex += 1; } - // Accumulate profile averages - if (m_didStep) - { - B2Profile p = m_profiles[m_currentProfileIndex]; - m_totalProfile.step += p.step; - m_totalProfile.pairs += p.pairs; - m_totalProfile.collide += p.collide; - m_totalProfile.solve += p.solve; - m_totalProfile.solverSetup += p.solverSetup; - m_totalProfile.constraints += p.constraints; - m_totalProfile.prepareConstraints += p.prepareConstraints; - m_totalProfile.integrateVelocities += p.integrateVelocities; - m_totalProfile.warmStart += p.warmStart; - m_totalProfile.solveImpulses += p.solveImpulses; - m_totalProfile.integratePositions += p.integratePositions; - m_totalProfile.relaxImpulses += p.relaxImpulses; - m_totalProfile.applyRestitution += p.applyRestitution; - m_totalProfile.storeImpulses += p.storeImpulses; - m_totalProfile.transforms += p.transforms; - m_totalProfile.splitIslands += p.splitIslands; - m_totalProfile.jointEvents += p.jointEvents; - m_totalProfile.hitEvents += p.hitEvents; - m_totalProfile.refit += p.refit; - m_totalProfile.bullets += p.bullets; - m_totalProfile.sleepIslands += p.sleepIslands; - m_totalProfile.sensors += p.sensors; - } } public virtual void Draw() @@ -854,4 +825,201 @@ protected InputAction GetKey(Keys key) { return GlfwHelpers.GetKey(m_context, key); } + + public static void SelectSample(SampleContext context, int selection, bool restart) + { + if (restart == false) + { + ResetView(context.camera); + context.sampleIndex = selection; + context.subStepCount = 4; + context.debugDraw.drawJoints = true; + } + + context.sample?.Dispose(); + context.sample = null; + context.restart = restart; + context.sample = SampleFactory.Shared.Create(context.sampleIndex, context); + context.restart = false; + } + + public static void UpdateSampleUI(SampleContext context) + { + int maxWorkers = B2_MAX_WORKERS; + B2WorldId worldId = context.sample.m_worldId; + + float fontSize = ImGui.GetFontSize(); + float menuWidth = 13.0f * fontSize; + if (context.showUI) + { + ImGui.SetNextWindowPos(new Vector2(context.camera.width - menuWidth - 0.5f * fontSize, 0.5f * fontSize)); + ImGui.SetNextWindowSize(new Vector2(menuWidth, context.camera.height - fontSize)); + + ImGui.Begin("Tools", ref context.showUI, ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse); + + if (ImGui.BeginTabBar("ControlTabs", ImGuiTabBarFlags.None)) + { + if (ImGui.BeginTabItem("Controls")) + { + ImGui.PushItemWidth(100.0f); + ImGui.SliderInt("Sub-steps", ref context.subStepCount, 1, 32); + ImGui.SliderFloat("Hertz", ref context.hertz, 5.0f, 240.0f, "%.0f hz"); + + if (ImGui.SliderInt("Workers", ref context.workerCount, 1, maxWorkers)) + { + context.workerCount = b2ClampInt(context.workerCount, 1, maxWorkers); + SelectSample(context, context.sampleIndex, true); + } + ImGui.PopItemWidth(); + + ImGui.Separator(); + + ImGui.Checkbox("Sleep", ref context.enableSleep); + ImGui.Checkbox("Warm Starting", ref context.enableWarmStarting); + ImGui.Checkbox("Continuous", ref context.enableContinuous); + + ImGui.PushItemWidth(100.0f); + float recyclingCentimeters = 100.0f * context.recycleDistance; + if (ImGui.SliderFloat("Recycle", ref recyclingCentimeters, 0.0f, 10.0f, "%.1f cm")) + { + context.recycleDistance = 0.01f * recyclingCentimeters; + b2World_SetContactRecycleDistance(worldId, context.recycleDistance); + } + ImGui.PopItemWidth(); + + ImGui.Separator(); + + ImGui.Checkbox("Shapes", ref context.debugDraw.drawShapes); + ImGui.Checkbox("Joints", ref context.debugDraw.drawJoints); + ImGui.Checkbox("Joint Extras", ref context.debugDraw.drawJointExtras); + ImGui.Checkbox("Bounds", ref context.debugDraw.drawBounds); + ImGui.Checkbox("Mass", ref context.debugDraw.drawMass); + ImGui.Checkbox("Body Names", ref context.debugDraw.drawBodyNames); + ImGui.Checkbox("Graph Colors", ref context.debugDraw.drawGraphColors); + ImGui.Checkbox("Islands", ref context.debugDraw.drawIslands); + ImGui.Checkbox("Counters", ref context.drawCounters); + ImGui.Checkbox("Profile", ref context.drawProfile); + ImGui.Checkbox("Frame Time", ref context.frameTime); + + ImGui.Separator(); + + ImGui.Checkbox("Contact Points", ref context.debugDraw.drawContacts); + + if (ImGui.RadioButton("Anchor A", context.debugDraw.drawAnchorA == true)) + { + context.debugDraw.drawAnchorA = true; + } + ImGui.SameLine(); + if (ImGui.RadioButton("Anchor B", context.debugDraw.drawAnchorA == false)) + { + context.debugDraw.drawAnchorA = false; + } + ImGui.Checkbox("Contact Normals", ref context.debugDraw.drawContactNormals); + ImGui.Checkbox("Contact Features", ref context.debugDraw.drawContactFeatures); + ImGui.Checkbox("Contact Forces", ref context.debugDraw.drawContactForces); + ImGui.Checkbox("Friction Forces", ref context.debugDraw.drawFrictionForces); + + ImGui.Separator(); + + ImGui.PushItemWidth(80.0f); + ImGui.InputFloat("Joint Scale", ref context.debugDraw.jointScale); + ImGui.InputFloat("Force Scale", ref context.debugDraw.forceScale); + ImGui.PopItemWidth(); + + Vector2 button_sz = new Vector2(-1, 0); + if (ImGui.Button("Pause (P)", button_sz)) + { + context.pause = !context.pause; + } + + if (ImGui.Button("Single Step (O)", button_sz)) + { + context.singleStep = !context.singleStep; + } + + if (ImGui.Button("Dump Mem Stats", button_sz)) + { + b2World_DumpMemoryStats(context.sample.m_worldId); + } + + if (ImGui.Button("Reset Profile", button_sz)) + { + context.sample.ResetProfile(); + } + + if (ImGui.Button("Restart (R)", button_sz)) + { + SelectSample(context, context.sampleIndex, true); + } + + if (ImGui.Button("Quit", button_sz)) + { + unsafe + { + context.glfw.SetWindowShouldClose(context.window, true); + } + } + + ImGui.EndTabItem(); + } + + ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; + leafNodeFlags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; + + ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; + + if (ImGui.BeginTabItem("Samples")) + { + int categoryIndex = 0; + string category = SampleFactory.Shared.GetCategory(categoryIndex); + int i = 0; + while (i < SampleFactory.Shared.SampleCount) + { + bool categorySelected = category == SampleFactory.Shared.GetCategory(context.sampleIndex); + ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags.Selected : 0; + bool nodeOpen = ImGui.TreeNodeEx(category, nodeFlags | nodeSelectionFlags); + + if (nodeOpen) + { + while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) + { + ImGuiTreeNodeFlags selectionFlags = 0; + if (context.sampleIndex == i) + { + selectionFlags = ImGuiTreeNodeFlags.Selected; + } + + ImGui.TreeNodeEx(SampleFactory.Shared.GetName(i), leafNodeFlags | selectionFlags); + if (ImGui.IsItemClicked()) + { + SelectSample(context, i, false); + } + ++i; + } + ImGui.TreePop(); + } + else + { + while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) + { + ++i; + } + } + + if (i < SampleFactory.Shared.SampleCount) + { + category = SampleFactory.Shared.GetCategory(i); + categoryIndex = i; + } + } + ImGui.EndTabItem(); + } + ImGui.EndTabBar(); + } + + ImGui.End(); + + context.sample.UpdateGui(); + } + } } diff --git a/src/Box2D.NET.Samples/Settings.cs b/src/Box2D.NET.Samples/Settings.cs index 39f87907..86360e2f 100644 --- a/src/Box2D.NET.Samples/Settings.cs +++ b/src/Box2D.NET.Samples/Settings.cs @@ -31,7 +31,6 @@ public class Settings public bool drawBounds = false; public bool drawMass = false; public bool drawBodyNames = false; - public B2ContactDrawType contactDrawType; public bool drawContactNormals = false; public bool drawContactFeatures = false; public bool drawContactForces = false; @@ -93,7 +92,6 @@ public static Settings CopyFrom(SampleContext context) setting.singleStep = context.singleStep; // - setting.contactDrawType = context.debugDraw.contactDrawType; setting.drawShapes = context.debugDraw.drawShapes; setting.drawJoints = context.debugDraw.drawJoints; setting.drawJointExtras = context.debugDraw.drawJointExtras; diff --git a/src/Box2D.NET/B2BodyDef.cs b/src/Box2D.NET/B2BodyDef.cs index b8d5626f..ce542c8b 100644 --- a/src/Box2D.NET/B2BodyDef.cs +++ b/src/Box2D.NET/B2BodyDef.cs @@ -75,7 +75,7 @@ public struct B2BodyDef /// If you want a fast moving projectile to collide with a fast moving target, you need to consider the relative /// movement in your ray or shape cast. This is out of the scope of Box2D. /// So what are good use cases for bullets? Pinball games or games with dynamic containers that hold other objects. - /// It should be a use case where it doesn't break the game if there is a collision missed, but the having them + /// It should be a use case where it doesn't break the game if there is a collision missed, but having them /// captured improves the quality of the game. public bool isBullet; diff --git a/src/Box2D.NET/B2ContactDrawType.cs b/src/Box2D.NET/B2ContactDrawType.cs deleted file mode 100644 index 2c85d870..00000000 --- a/src/Box2D.NET/B2ContactDrawType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Box2D.NET -{ - /// The type of contact point drawing - public enum B2ContactDrawType - { - b2_drawContacts_None = 0, - b2_drawContacts_Clip = 1, - b2_drawContacts_AnchorA = 2, - b2_drawContacts_AnchorB = 3, - b2_drawContacts_Average = 4, - } -} \ No newline at end of file diff --git a/src/Box2D.NET/B2DebugDraw.cs b/src/Box2D.NET/B2DebugDraw.cs index 956b92a9..94dcdf37 100644 --- a/src/Box2D.NET/B2DebugDraw.cs +++ b/src/Box2D.NET/B2DebugDraw.cs @@ -29,7 +29,10 @@ public struct B2DebugDraw public float jointScale; /// Option to draw contact points - public B2ContactDrawType contactDrawType; + public bool drawContacts; + + /// Draw anchor A for contact points (instead of anchorB) + public bool drawAnchorA; /// Option to draw shapes public bool drawShapes; diff --git a/src/Box2D.NET/B2Worlds.cs b/src/Box2D.NET/B2Worlds.cs index b392e44a..fe3d64cd 100644 --- a/src/Box2D.NET/B2Worlds.cs +++ b/src/Box2D.NET/B2Worlds.cs @@ -1252,7 +1252,7 @@ public static void b2World_Draw(B2WorldId worldId, in B2DebugDraw draw) } float linearSlop = B2_LINEAR_SLOP; - if (draw.contactDrawType != B2ContactDrawType.b2_drawContacts_None && body.type == B2BodyType.b2_dynamicBody) + if (draw.drawContacts && body.type == B2BodyType.b2_dynamicBody) { int contactKey = body.headContactKey; while (contactKey != B2_NULL_INDEX) @@ -1279,21 +1279,15 @@ public static void b2World_Draw(B2WorldId worldId, in B2DebugDraw draw) { ref B2ManifoldPoint mp = ref contactSim.manifold.points[j]; - B2Vec2 p = mp.clipPoint; - if (draw.contactDrawType == B2ContactDrawType.b2_drawContacts_AnchorA) + B2Vec2 p; + if (draw.drawAnchorA) { p = b2Add(bodySimA.center, mp.anchorA); } - else if (draw.contactDrawType == B2ContactDrawType.b2_drawContacts_AnchorB) + else { p = b2Add(bodySimB.center, mp.anchorB); } - else if (draw.contactDrawType == B2ContactDrawType.b2_drawContacts_Average) - { - B2Vec2 pA = b2Add(bodySimA.center, mp.anchorA); - B2Vec2 pB = b2Add(bodySimB.center, mp.anchorB); - p = b2Lerp(pA, pB, 0.5f); - } if (draw.drawGraphColors && contact.colorIndex != B2_NULL_INDEX) { diff --git a/test/Box2D.NET.Test/B2DebugDrawTests.cs b/test/Box2D.NET.Test/B2DebugDrawTests.cs new file mode 100644 index 00000000..4f075857 --- /dev/null +++ b/test/Box2D.NET.Test/B2DebugDrawTests.cs @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2026 Erin Catto +// SPDX-FileCopyrightText: 2026 Ikpil Choi(ikpil@naver.com) +// SPDX-License-Identifier: MIT + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using static Box2D.NET.B2Bodies; +using static Box2D.NET.B2Geometries; +using static Box2D.NET.B2Shapes; +using static Box2D.NET.B2Types; +using static Box2D.NET.B2Worlds; + +namespace Box2D.NET.Test; + +public class B2DebugDrawTests +{ + private sealed class PointCapture + { + public readonly List Points = new List(); + } + + [Test] + public void ContactDrawingUsesSelectedBodyAnchor() + { + B2WorldDef worldDef = b2DefaultWorldDef(); + worldDef.gravity = new B2Vec2(0.0f, -10.0f); + B2WorldId worldId = b2CreateWorld(worldDef); + + try + { + B2BodyDef groundDef = b2DefaultBodyDef(); + B2BodyId groundId = b2CreateBody(worldId, groundDef); + B2ShapeDef groundShapeDef = b2DefaultShapeDef(); + b2CreatePolygonShape(groundId, groundShapeDef, b2MakeBox(2.0f, 0.5f)); + + B2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = B2BodyType.b2_dynamicBody; + bodyDef.position = new B2Vec2(0.0f, 0.9f); + B2BodyId bodyId = b2CreateBody(worldId, bodyDef); + B2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.density = 1.0f; + b2CreatePolygonShape(bodyId, shapeDef, b2MakeBox(0.5f, 0.5f)); + + b2World_Step(worldId, 1.0f / 60.0f, 4); + + B2DebugDraw draw = b2DefaultDebugDraw(); + draw.drawShapes = false; + draw.DrawPointFcn = CapturePoint; + var capture = new PointCapture(); + draw.context = capture; + + b2World_Draw(worldId, draw); + Assert.That(capture.Points, Is.Empty, "drawContacts=false must suppress contact points."); + + draw.drawContacts = true; + draw.drawAnchorA = false; + b2World_Draw(worldId, draw); + B2Vec2[] anchorBPoints = capture.Points.ToArray(); + Assert.That(anchorBPoints, Is.Not.Empty); + + capture.Points.Clear(); + draw.drawAnchorA = true; + b2World_Draw(worldId, draw); + B2Vec2[] anchorAPoints = capture.Points.ToArray(); + + Assert.That(anchorAPoints, Has.Length.EqualTo(anchorBPoints.Length)); + Assert.That(AnyPointDiffers(anchorAPoints, anchorBPoints), Is.True, + "Anchor A and anchor B should be transformed from their respective body centers."); + } + finally + { + b2DestroyWorld(worldId); + } + } + + [Test] + public void ContactDrawEnumIsRemovedFromThePublicApi() + { + B2DebugDraw draw = b2DefaultDebugDraw(); + + Assert.Multiple((Action)(() => + { + Assert.That(draw.drawContacts, Is.False); + Assert.That(draw.drawAnchorA, Is.False); + Assert.That(typeof(B2DebugDraw).Assembly.GetType("Box2D.NET.B2ContactDrawType"), Is.Null); + })); + } + + private static void CapturePoint(in B2Vec2 point, float size, B2HexColor color, object context) + { + ((PointCapture)context).Points.Add(point); + } + + private static bool AnyPointDiffers(B2Vec2[] pointsA, B2Vec2[] pointsB) + { + for (int i = 0; i < pointsA.Length; ++i) + { + if (pointsA[i] != pointsB[i]) + { + return true; + } + } + + return false; + } +}