Introduction: A Bare-Metal Approach to 2D Physics Simulation
Building a 2D physics engine from scratch in vanilla JavaScript—without leaning on external libraries, bundlers, or build tools—is a technical gauntlet. It’s not just about recreating physics; it’s about exposing the raw mechanics of collision, motion, and force resolution using nothing but the browser’s Canvas 2D API and core JavaScript. This project, hosted on GitHub, demonstrates both the feasibility and fragility of such an approach. It includes SAT collision detection, a sequential-impulse solver with friction, sweep-and-prune broadphase, and fixed-timestep simulation—all implemented manually. Yet, its stability is undermined by unresolved bugs, particularly in stack stability and jitter, which highlight the inherent risks of bypassing abstraction layers.
Mechanisms of Failure: Where the Engine Breaks
The most critical failure mode occurs in stack stability. When rigid bodies (e.g., boxes) are stacked, they either jitter indefinitely or sink into each other. This is caused by two interrelated issues:
- Baumgarte Stabilization Mismatch: The Baumgarte stabilization parameter, intended to correct position errors in constraints, is mis-tuned. If too high, it overcorrects, causing oscillatory jitter. If too low, bodies fail to settle, leading to sinking. The causal chain is: impact → excessive correction → energy accumulation → observable jitter.
- Warm-Starting Inconsistency: The solver’s warm-starting mechanism, which initializes impulses from the previous frame, fails to converge for stacked bodies. Without accurate initial guesses, the solver iterates inefficiently, allowing bodies to violate constraints. The result is: impact → insufficient impulse resolution → constraint violation → sinking.
Edge Cases Expose Systemic Weaknesses
The engine’s Newton’s cradle demo, despite functioning, reveals another edge case: energy loss in elastic collisions. The sequential-impulse solver, while handling friction well, struggles to conserve momentum in rapid, high-frequency collisions. This is due to:
- Fixed Timestep Inaccuracy: The fixed timestep (e.g., 1/60s) fails to capture sub-frame collisions, causing energy to dissipate. The mechanism is: high-velocity impact → missed collision detection → uncorrected momentum → observable energy loss.
- Impulse Accumulation Error: Small rounding errors in impulse calculations compound over iterations, leading to non-physical behavior. The causal chain is: repeated collision → floating-point error → impulse drift → non-elastic outcome.
Why This Matters Now: A Case for Open Collaboration
This project’s value lies in its minimalist methodology, proving that complex systems can be built without scaffolding. However, its bugs—while solvable—require community input. For instance, the stack stability issue could be resolved by:
- Adaptive Baumgarte Tuning: Dynamically adjust the stabilization parameter based on constraint violation magnitude. Optimal if X (violation > threshold) → use Y (higher Baumgarte factor). Fails if violation detection itself is inaccurate.
- Hybrid Solver Approach: Combine the sequential-impulse solver with a position-based method for stacked bodies. Optimal for stacks but introduces complexity. Fails if solvers conflict in mixed scenarios.
Without such refinements, the engine remains a proof-of-concept rather than a usable tool. Its timeliness stems from its role as a learning scaffold—a challenge for developers to debug, optimize, and extend. The risks of inaction are clear: stagnation limits its utility, while active collaboration could transform it into a benchmark for web-based physics simulation.
Technical Overview
The 2D physics engine, built entirely in vanilla JavaScript without external libraries or build tools, leverages core web technologies like Canvas 2D to simulate physical interactions. Below is a detailed breakdown of its architecture, algorithms, and implementation challenges, highlighting both innovations and areas requiring refinement.
Core Components and Algorithms
- Collision Detection: SAT (Separating Axis Theorem)
SAT is used to detect collisions between convex polygons by projecting shapes onto axes and checking for overlap. Mechanism: For two boxes, SAT tests overlap on six axes (four edges and two cross-products of edges). Failure Mode: High-velocity objects may tunnel through each other due to fixed-timestep simulation missing sub-frame collisions. Causal Chain: High velocity → missed SAT check → undetected collision → objects pass through.
- Solver: Sequential-Impulse with Friction
The solver resolves collisions by applying impulses iteratively to satisfy constraints. Mechanism: For each contact, impulses are computed to correct interpenetration and enforce friction. Failure Mode: Warm-starting (using previous impulses as initial guesses) fails for stacks due to inconsistent contact persistence. Causal Chain: Impact → inaccurate initial impulse → insufficient constraint resolution → sinking.
- Broadphase: Sweep-and-Prune
Sweep-and-prune sorts objects along an axis and efficiently identifies potential collisions. Mechanism: Objects are sorted by their bounding intervals, and overlapping intervals are flagged for narrow-phase testing. Risk: Performance degrades with large numbers of objects due to O(n log n) sorting complexity. Mechanism of Risk: Increased object count → more sorting operations → higher computational load.
- Simulation: Fixed-Timestep
The simulation runs at a fixed timestep (e.g., 1/60s) to ensure deterministic behavior. Mechanism: Accumulated real-time delta is processed in fixed chunks. Failure Mode: Energy loss in elastic collisions due to missed sub-frame collisions. Causal Chain: High-velocity impact → collision occurs between fixed steps → momentum uncorrected → energy dissipation.
Critical Failure Modes and Proposed Solutions
| Problem | Mechanism | Proposed Solution | Optimality Condition |
| Stack Stability (Jitter/Sinking) | Baumgarte stabilization mismatch and warm-starting inconsistency. | Adaptive Baumgarte Tuning: Dynamically adjust stabilization factor based on constraint violation magnitude. Hybrid Solver: Combine sequential-impulse and position-based solvers for stacked bodies. | Adaptive Baumgarte is optimal for general cases; hybrid solver is optimal for stacks but risks solver conflict in mixed scenarios. |
| Energy Loss in Elastic Collisions | Fixed-timestep inaccuracy and impulse accumulation error. | Sub-stepping: Interpolate additional steps for high-velocity objects. Double Precision: Use 64-bit floats to reduce impulse drift. | Sub-stepping is optimal for high-velocity scenarios; double precision is optimal for long-running simulations but increases memory usage. |
Technical Challenges and Opportunities
The project’s minimalist approach exposes raw physics mechanics but amplifies risks associated with bypassing abstraction layers. For example, floating-point errors in impulse accumulation lead to non-physical behavior due to the absence of error-correcting libraries. Mechanism: Repeated collisions → small errors compound → impulse drift → non-elastic outcomes.
Community collaboration is critical to resolve bugs and transform the engine into a benchmark for web-based physics simulation. Key opportunities include:
- Debugging and Optimization: Address stack stability and energy loss through targeted fixes.
- Extension: Add features like deformable bodies or fluid dynamics to expand utility.
- Educational Scaffold: Serve as a challenge for developers to deepen understanding of physics simulation and JavaScript performance.
Key Takeaways
The engine demonstrates the feasibility of complex systems without scaffolding but underscores the trade-offs of a bare-metal approach. Rule for Choosing Solutions: If constraint violation > threshold → use adaptive Baumgarte tuning. If high-velocity collisions → implement sub-stepping. Unresolved bugs limit usability, but active collaboration could establish this project as a foundational tool for web development and physics simulation.
Identified Issues and Bugs
The vanilla JavaScript 2D physics engine, while impressive in its minimalist approach, suffers from several unresolved bugs and limitations. Below is a categorized breakdown of the issues, their severity, and their impact on stability and performance.
Critical Issues
- Stack Stability (Jitter/Sinking)
Mechanism: Baumgarte stabilization mismatch and warm-starting inconsistency in the sequential-impulse solver.
Causal Chain: Impact → excessive/insufficient correction → energy accumulation/constraint violation → observable jitter or sinking.
Impact: Stacked objects fail to settle realistically, undermining the engine’s utility for simulations requiring stable structures.
- Energy Loss in Elastic Collisions
Mechanism: Fixed-timestep inaccuracy and impulse accumulation error due to floating-point precision.
Causal Chain: High-velocity impact → missed sub-frame collisions → uncorrected momentum → energy dissipation.
Impact: Elastic collisions fail to conserve energy, leading to non-physical behavior and reduced simulation accuracy.
High-Severity Issues
- Tunneling in High-Velocity Collisions
Mechanism: SAT collision detection fails to detect sub-frame collisions due to fixed-timestep simulation.
Causal Chain: High velocity → missed SAT check → undetected collision → objects "tunnel" through each other.
Impact: Reduces the engine’s ability to handle fast-moving objects, limiting its applicability in dynamic simulations.
- Performance Degradation with Large Object Counts
Mechanism: Sweep-and-prune broadphase has O(n log n) sorting complexity, which scales poorly with increased object counts.
Mechanism of Risk: Increased objects → more sorting operations → higher computational load → frame rate drops.
Impact: Limits scalability, making the engine impractical for simulations with many objects.
Medium-Severity Issues
- Warm-Starting Inconsistency in Solver
Mechanism: Inefficient solver iterations due to inaccurate initial impulse guesses for stacked bodies.
Causal Chain: Impact → insufficient impulse resolution → constraint violation → sinking.
Impact: Reduces solver efficiency and exacerbates stack stability issues.
- Floating-Point Errors in Impulse Accumulation
Mechanism: Repeated collisions cause small floating-point errors to compound, leading to impulse drift.
Causal Chain: Repeated collision → floating-point error → impulse drift → non-elastic outcomes.
Impact: Introduces non-physical behavior in long-running simulations, reducing realism.
Proposed Solutions and Trade-offs
For stack stability, adaptive Baumgarte tuning is optimal for general cases, dynamically adjusting stabilization based on constraint violation magnitude. However, it may fail in mixed scenarios with varying body types. A hybrid solver approach combining sequential-impulse and position-based solvers is optimal for stacks but risks solver conflict in mixed scenarios.
For energy loss in elastic collisions, sub-stepping is optimal for high-velocity impacts but increases computational load. Double precision reduces impulse accumulation errors but increases memory usage, making it suitable only for long simulations.
Rule for Choosing Solutions
- If constraint violation > threshold → use adaptive Baumgarte tuning.
- If high-velocity collisions are frequent → implement sub-stepping.
- If long simulations with repeated collisions → use double precision.
Addressing these issues through community collaboration and targeted debugging will transform this engine into a robust tool for web-based physics simulation, showcasing the potential of minimalist methodologies in complex system development.
Proposed Improvements
Addressing the identified issues in the vanilla JavaScript 2D physics engine requires a combination of algorithmic refinements, code optimizations, and strategic feature additions. Below are evidence-driven solutions, evaluated for effectiveness and trade-offs, to guide future development.
1. Stack Stability (Jitter/Sinking)
Mechanism: Baumgarte stabilization mismatch and warm-starting inconsistency in the sequential-impulse solver lead to energy accumulation or constraint violation, causing jitter or sinking.
Causal Chain: Impact → excessive/insufficient correction → energy accumulation/constraint violation → observable jitter/sinking.
- Adaptive Baumgarte Tuning: Dynamically adjust the stabilization parameter based on constraint violation magnitude. Optimal for general cases but may fail in mixed scenarios (e.g., stacks with dynamic bodies). Rule: If constraint violation > threshold → use adaptive Baumgarte tuning.
- Hybrid Solver Approach: Combine sequential-impulse and position-based solvers for stacked bodies. Optimal for stacks but risks solver conflict in mixed scenarios. Typical error: Over-reliance on position-based solvers leads to energy drift in dynamic systems.
2. Energy Loss in Elastic Collisions
Mechanism: Fixed-timestep inaccuracy and impulse accumulation error due to floating-point precision cause missed sub-frame collisions and momentum dissipation.
Causal Chain: High-velocity impact → missed collision detection → uncorrected momentum → energy loss.
- Sub-stepping: Divide high-velocity frames into smaller sub-steps to detect missed collisions. Optimal for high-velocity impacts but increases computational load. Rule: If high-velocity collisions are frequent → implement sub-stepping.
- Double Precision: Use double-precision floats to reduce impulse accumulation errors. Optimal for long simulations but increases memory usage. Rule: If long simulations with repeated collisions → use double precision.
3. Tunneling in High-Velocity Collisions
Mechanism: SAT collision detection fails to detect sub-frame collisions due to fixed-timestep simulation, causing objects to "tunnel" through each other.
Causal Chain: High velocity → missed SAT check → undetected collision → tunneling.
- Continuous Collision Detection (CCD): Interpolate object positions between frames to detect tunneling. Optimal for high-velocity scenarios but adds computational overhead. Rule: If tunneling is frequent → implement CCD.
4. Performance Degradation with Large Object Counts
Mechanism: Sweep-and-prune broadphase has O(n log n) sorting complexity, scaling poorly with increased object counts.
Mechanism of Risk: Increased objects → more sorting operations → higher computational load → frame rate drops.
- Spatial Partitioning (e.g., Grid or Quadtree): Reduce broadphase complexity by dividing space into regions. Optimal for large object counts but increases memory usage. Rule: If object count > threshold → use spatial partitioning.
5. Warm-Starting Inconsistency in Solver
Mechanism: Inefficient solver iterations due to inaccurate initial impulse guesses for stacked bodies exacerbate stack stability issues.
Causal Chain: Impact → insufficient impulse resolution → constraint violation → sinking.
- Persistent Contact Manifold: Store and reuse contact data between frames to improve initial impulse guesses. Optimal for stacks but requires additional memory. Rule: If stack stability is critical → implement persistent contact manifold.
6. Floating-Point Errors in Impulse Accumulation
Mechanism: Repeated collisions cause small floating-point errors to compound, leading to impulse drift and non-elastic outcomes.
Causal Chain: Repeated collision → floating-point error → impulse drift → non-elastic outcomes.
- Impulse Reset: Periodically reset accumulated impulses to mitigate drift. Optimal for long simulations but may introduce transient instability. Rule: If simulation duration > threshold → implement impulse reset.
Trade-offs and Decision Rules
| Issue | Optimal Solution | Trade-offs | Rule |
| Stack Stability | Adaptive Baumgarte Tuning | May fail in mixed scenarios | If constraint violation > threshold → use adaptive Baumgarte tuning |
| Energy Loss | Sub-stepping | Increased computational load | If high-velocity collisions are frequent → implement sub-stepping |
| Tunneling | Continuous Collision Detection | Added computational overhead | If tunneling is frequent → implement CCD |
| Performance Degradation | Spatial Partitioning | Increased memory usage | If object count > threshold → use spatial partitioning |
By systematically applying these solutions, the engine can achieve greater stability, accuracy, and scalability. Community collaboration will be critical to refine these implementations and address edge cases, transforming the project into a robust tool for web-based physics simulation.
Testing and Validation: Ensuring Robustness in the Vanilla JS Physics Engine
The current testing strategy for this 2D physics engine relies heavily on demo scenes and gating tests, such as the stack stability test. While these methods have uncovered numerous bugs, they are insufficient for systematic validation. The engine’s complexity—handling collision detection, impulse resolution, and fixed-timestep simulation—requires a more rigorous approach to ensure reliability across diverse scenarios.
Current Testing Strategy: Strengths and Limitations
The developer’s approach of using interactive demo scenes (e.g., Newton’s cradle, stack stability) has been effective in identifying observable issues like jitter and sinking. However, these tests are scenario-specific and fail to address edge cases or systematic failures in the engine’s core mechanics. For instance, the stack stability test caught bugs related to Baumgarte stabilization but did not account for high-velocity collisions or large object counts, where the engine’s performance degrades.
Proposed Testing Enhancements
To validate the engine’s functionality comprehensively, the following methods should be implemented:
- Unit Testing for Core Components:
Break the engine into modular components (e.g., SAT collision detection, sequential-impulse solver) and test each in isolation. For example, validate the impulse resolver by simulating isolated collisions and verifying momentum conservation. This approach exposes hidden bugs in individual modules before they cascade into system-wide failures.
- Stress Testing for Scalability:
Simulate scenarios with increasing object counts to measure performance degradation. The sweep-and-prune broadphase, with its O(n log n) complexity, is a bottleneck. Stress tests will quantify frame rate drops and identify the threshold beyond which the engine becomes impractical. This data informs the need for spatial partitioning solutions like quadtrees.
- Edge-Case Simulations:
Design tests for extreme conditions, such as high-velocity tunneling or long-running simulations with repeated collisions. For instance, simulate two objects colliding at velocities exceeding the fixed timestep’s resolution to verify if continuous collision detection (CCD) is necessary. These tests reveal failures in the engine’s assumptions about floating-point precision and timestep accuracy.
- Energy Conservation Validation:
Implement tests to verify energy conservation in elastic collisions. Measure the total kinetic energy before and after collisions, ensuring it remains constant within a tolerance threshold. This exposes issues like impulse accumulation errors and guides the adoption of solutions like sub-stepping or double precision.
- Automated Regression Testing:
As bugs are fixed, create automated tests to prevent their recurrence. For example, after addressing stack stability, automate the stack-settling test to ensure future changes do not reintroduce jitter or sinking. This builds a safety net for iterative development.
Decision Rules for Testing Prioritization
To maximize efficiency, prioritize tests based on the following rules:
- If a component handles critical physics (e.g., impulse resolution) → implement unit tests first.
- If performance degradation is suspected → conduct stress tests to quantify thresholds.
- If edge cases are undocumented (e.g., high-velocity tunneling) → design targeted simulations.
- If energy loss is observed → validate conservation in elastic collisions.
Conclusion: A Path to Reliability
The current testing strategy, while effective for initial debugging, lacks the rigor needed to validate the engine’s robustness. By adopting unit testing, stress testing, and edge-case simulations, the developer can systematically address unresolved bugs and ensure the engine’s reliability. These methods not only expose hidden failures but also provide data-driven insights for optimizing performance and scalability. With community collaboration, this engine can evolve into a foundational tool for web-based physics simulation, demonstrating the power of minimalist methodologies in complex system development.
Conclusion and Future Directions
Building a 2D physics engine in vanilla JavaScript without external libraries has been a revealing journey, exposing both the potential of minimalist methodologies and the challenges inherent in complex system development. While the engine demonstrates core physics principles—SAT collision detection, sequential-impulse solving, and fixed-timestep simulation—unresolved bugs underscore the need for refinement. The project’s value lies not just in its current functionality but in its role as a learning tool and a foundation for community collaboration.
Key Findings and Potential
The engine’s ability to handle stack stability, Newton’s cradle, and other demo scenes highlights its potential for educational and practical applications. However, issues like stack jitter, tunneling in high-velocity collisions, and performance degradation with large object counts limit its scalability and reliability. These problems stem from:
- Stack Jitter/Sinking: Baumgarte stabilization mismatch and warm-starting inconsistencies cause energy accumulation or constraint violation, leading to observable jitter or sinking. Impact → excessive/insufficient correction → energy accumulation/constraint violation → observable effect.
- Tunneling: SAT collision detection fails to detect sub-frame collisions in high-velocity scenarios due to fixed-timestep simulation. High velocity → missed SAT check → undetected collision → tunneling.
- Performance Degradation: Sweep-and-prune broadphase’s O(n log n) complexity scales poorly with increased object counts, causing frame rate drops. Increased objects → more sorting operations → higher computational load → frame rate drops.
Next Steps and Community Involvement
To address these issues, the following steps are critical:
- Systematic Testing: Implement unit tests for core components (e.g., SAT collision detection, impulse resolver) and stress tests for scalability bottlenecks. Mechanism: Isolated testing exposes hidden bugs and prevents system-wide failures.
- Edge-Case Simulations: Design targeted tests for high-velocity collisions, long-running simulations, and large object counts to validate assumptions about precision and timestep accuracy. Mechanism: Extreme conditions reveal failures in core mechanics.
-
Solution Implementation: Apply targeted solutions based on decision rules:
- Stack Stability: If constraint violation > threshold → use adaptive Baumgarte tuning. Optimal for general cases but may fail in mixed scenarios.
- Tunneling: If tunneling is frequent → implement Continuous Collision Detection (CCD). Optimal for high-velocity scenarios but adds computational overhead.
- Performance Degradation: If object count > threshold → use spatial partitioning (e.g., Quadtree). Optimal for large object counts but increases memory usage.
Encouraging Collaboration
The project’s open-source nature invites community contributions to refine implementations, address edge cases, and enhance stability. If you have expertise in physics simulation, performance optimization, or testing strategies, consider:
- Opening a PR to fix identified bugs (e.g., stack jitter, tunneling).
- Suggesting improvements to the testing framework or proposing new demo scenes.
- Sharing insights on alternative algorithms or optimizations.
By collaborating, we can transform this engine into a robust tool for web-based physics simulation, demonstrating the power of minimalist methodologies in tackling complex problems. The journey is far from over—join in to shape the future of this project.
Thanks for reading, and let’s build something remarkable together!
Top comments (0)