Physics-based pool simulation — from a cue strike to rendered animation. Repository: ekiefl/pooltool · commit 79d1df6
Every ball's kinematic state is a 3×3 NumPy array called rvw. Each row is a 3D vector:
Each ball carries an integer state s that controls which physics equations apply:
Sliding decays to rolling (kinetic friction), rolling decays to spinning or stationary, spinning decays to stationary. Pocketed balls are removed from further simulation.
import pooltool as pt # 1. Build system system = pt.System( cue=pt.Cue.default(), table=(table := pt.Table.default()), balls=pt.get_rack(pt.GameType.NINEBALL, table=table), ) # 2. Aim and strike system.strike(V0=8.0, phi=pt.aim.at_ball(system, "1")) # 3. Simulate physics pt.simulate(system, inplace=True) # 4. Densify for animation (optional) pt.continuize(system, dt=0.01, inplace=True) # 5. View in Panda3D window pt.show(system)
Public API surface. Re-exports System, simulate, continuize, show, Ball, Cue, Table.
Root container class System (line 30). Holds balls, cue, table, elapsed time, and event history.
The main simulation loop. simulate() at line 103 drives _SimulationState until quiescence.
Numba-compiled evolve_ball_motion(). Analytically advances rvw based on the current motion state.
One module per event type. detector.py orchestrates all detectors and picks the earliest event.
Pluggable collision models for every event type. resolver.py dispatches to the chosen strategy.
pt.simulate(system) └── simulate() simulate.py:103 ├── SimulationEngine() engine.py:12 │ ├── .detector EventDetector detector.py:105 │ └── .resolver Resolver resolver.py:98 ├── _SimulationState.init() simulate.py:38 │ └── ball.history.reset() × N └── while not_done: └── _SimulationState.step() simulate.py:42 ├── EventDetector.get_next_event() detector.py:123 │ ├── get_next_stick_ball_event() │ ├── transition_cache.get_next() │ ├── get_next_ball_linear_cushion_event() │ ├── get_next_ball_circular_cushion_event() │ ├── get_next_ball_pocket_event() │ ├── get_next_ball_ball_2d_event() │ └── → earliest Event by _get_event_priority() ├── evolve(shot, dt=event.time) simulate.py:80 │ └── evolve_ball_motion() × N evolve/__init__.py:28 ├── Resolver.resolve(event) resolver.py:116 │ └── strategy.resolve(event, shot) └── System._update_history(event) pt.continuize(system) continuous.py:17 └── interpolate_ball_states() × N balls continuous.py:194 └── populates ball.history_cts pt.show(system) interact.py:8 └── ShotViewer(system) animate.py:327 ├── SystemRender.from_system() └── Panda3D animation loop → ball.history_cts