omi-physics 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. omi_physics-0.1.0/LICENSE +21 -0
  2. omi_physics-0.1.0/MANIFEST.in +7 -0
  3. omi_physics-0.1.0/PKG-INFO +208 -0
  4. omi_physics-0.1.0/README.md +164 -0
  5. omi_physics-0.1.0/docs/ACCELERATORS.md +85 -0
  6. omi_physics-0.1.0/docs/ARCHITECTURE.md +99 -0
  7. omi_physics-0.1.0/docs/DATA-MODEL.md +132 -0
  8. omi_physics-0.1.0/docs/PIPELINE.md +113 -0
  9. omi_physics-0.1.0/docs/README.md +16 -0
  10. omi_physics-0.1.0/pyproject.toml +89 -0
  11. omi_physics-0.1.0/setup.cfg +4 -0
  12. omi_physics-0.1.0/setup.py +64 -0
  13. omi_physics-0.1.0/src/omi_physics/__init__.py +35 -0
  14. omi_physics-0.1.0/src/omi_physics/_accel.py +16 -0
  15. omi_physics-0.1.0/src/omi_physics/_collide_native.c +30753 -0
  16. omi_physics-0.1.0/src/omi_physics/_collide_native.pyx +344 -0
  17. omi_physics-0.1.0/src/omi_physics/_solver_native.c +31845 -0
  18. omi_physics-0.1.0/src/omi_physics/_solver_native.pyx +312 -0
  19. omi_physics-0.1.0/src/omi_physics/backend.py +120 -0
  20. omi_physics-0.1.0/src/omi_physics/body.py +272 -0
  21. omi_physics-0.1.0/src/omi_physics/broadphase.py +314 -0
  22. omi_physics-0.1.0/src/omi_physics/character.py +399 -0
  23. omi_physics-0.1.0/src/omi_physics/collide.py +378 -0
  24. omi_physics-0.1.0/src/omi_physics/cookery.py +93 -0
  25. omi_physics-0.1.0/src/omi_physics/gjk.py +264 -0
  26. omi_physics-0.1.0/src/omi_physics/glcompute.py +390 -0
  27. omi_physics-0.1.0/src/omi_physics/gravity.py +102 -0
  28. omi_physics-0.1.0/src/omi_physics/hull.py +153 -0
  29. omi_physics-0.1.0/src/omi_physics/joints.py +212 -0
  30. omi_physics-0.1.0/src/omi_physics/kinematic.py +80 -0
  31. omi_physics-0.1.0/src/omi_physics/mathutil.py +121 -0
  32. omi_physics-0.1.0/src/omi_physics/model.py +243 -0
  33. omi_physics-0.1.0/src/omi_physics/narrowphase.py +101 -0
  34. omi_physics-0.1.0/src/omi_physics/omi_gltf.py +334 -0
  35. omi_physics-0.1.0/src/omi_physics/py.typed +0 -0
  36. omi_physics-0.1.0/src/omi_physics/solver.py +445 -0
  37. omi_physics-0.1.0/src/omi_physics/threaded.py +110 -0
  38. omi_physics-0.1.0/src/omi_physics/triggers.py +70 -0
  39. omi_physics-0.1.0/src/omi_physics/world.py +676 -0
  40. omi_physics-0.1.0/src/omi_physics.egg-info/PKG-INFO +208 -0
  41. omi_physics-0.1.0/src/omi_physics.egg-info/SOURCES.txt +60 -0
  42. omi_physics-0.1.0/src/omi_physics.egg-info/dependency_links.txt +1 -0
  43. omi_physics-0.1.0/src/omi_physics.egg-info/requires.txt +19 -0
  44. omi_physics-0.1.0/src/omi_physics.egg-info/top_level.txt +1 -0
  45. omi_physics-0.1.0/tests/test_backend_parity.py +73 -0
  46. omi_physics-0.1.0/tests/test_backend_parity_gpu.py +181 -0
  47. omi_physics-0.1.0/tests/test_broadphase.py +92 -0
  48. omi_physics-0.1.0/tests/test_character.py +120 -0
  49. omi_physics-0.1.0/tests/test_character_void.py +78 -0
  50. omi_physics-0.1.0/tests/test_collide_primitives.py +87 -0
  51. omi_physics-0.1.0/tests/test_cookery.py +91 -0
  52. omi_physics-0.1.0/tests/test_gjk_epa.py +67 -0
  53. omi_physics-0.1.0/tests/test_gravity.py +62 -0
  54. omi_physics-0.1.0/tests/test_joints.py +93 -0
  55. omi_physics-0.1.0/tests/test_mesh_collision.py +62 -0
  56. omi_physics-0.1.0/tests/test_omi_physics_roundtrip.py +113 -0
  57. omi_physics-0.1.0/tests/test_physics_benchmark.py +78 -0
  58. omi_physics-0.1.0/tests/test_physics_impulse.py +135 -0
  59. omi_physics-0.1.0/tests/test_physics_integrate.py +100 -0
  60. omi_physics-0.1.0/tests/test_physics_kinematic_animator.py +91 -0
  61. omi_physics-0.1.0/tests/test_solver.py +115 -0
  62. omi_physics-0.1.0/tests/test_triggers.py +67 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike C. Fletcher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ include setup.py
5
+ recursive-include src/omi_physics *.pyx *.pxd py.typed
6
+ recursive-include docs *.md
7
+ recursive-include tests *.py
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: omi_physics
3
+ Version: 0.1.0
4
+ Summary: Renderer-agnostic real-time rigid-body physics on the OMI glTF physics model
5
+ Author-email: "Mike C. Fletcher" <mcfletch@vrplumber.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/mcfletch/omi_physics
8
+ Project-URL: Repository, https://github.com/mcfletch/omi_physics
9
+ Project-URL: Documentation, https://github.com/mcfletch/omi_physics/tree/main/docs
10
+ Keywords: physics,rigid-body,collision,simulation,OMI,glTF,numpy,game,3D
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
+ Classifier: Programming Language :: C
22
+ Classifier: Topic :: Scientific/Engineering :: Physics
23
+ Classifier: Topic :: Games/Entertainment
24
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=2.0
29
+ Provides-Extra: gl
30
+ Requires-Dist: PyOpenGL>=4.0.0a1; extra == "gl"
31
+ Provides-Extra: gltest
32
+ Requires-Dist: PyOpenGL>=4.0.0a1; extra == "gltest"
33
+ Requires-Dist: glfw>=2.0; extra == "gltest"
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest; extra == "test"
36
+ Requires-Dist: pytest-timeout; extra == "test"
37
+ Provides-Extra: dev
38
+ Requires-Dist: Cython>=3.0; extra == "dev"
39
+ Requires-Dist: mypy; extra == "dev"
40
+ Requires-Dist: omi_physics[gltest,test]; extra == "dev"
41
+ Requires-Dist: ruff; extra == "dev"
42
+ Requires-Dist: tox>=4.0; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # omi_physics
46
+
47
+ A renderer-agnostic, real-time **rigid-body physics engine** for Python, built
48
+ natively on the [OMI glTF physics](https://github.com/omigroup/gltf-extensions)
49
+ data model. State lives in flat NumPy arrays, the whole step pipeline is
50
+ vectorized, and optional compiled Cython accelerators drop in transparently for
51
+ the hot paths (the contact solver and box/sphere collision).
52
+
53
+ - **NumPy is the only hard dependency.** No graphics library is required to
54
+ simulate.
55
+ - **PyOpenGL is optional** — used solely by the experimental GPU compute backend
56
+ (`omi_physics.glcompute`).
57
+ - **Deterministic** on the CPU backend: the same inputs produce the same
58
+ trajectory, run to run.
59
+ - **Fast**: sweep-and-prune broadphase, SAT/GJK narrowphase, an island-parallel
60
+ sequential-impulse solver, sleeping, and a background-threaded simulation loop
61
+ that overlaps with a consumer's render/IO thread.
62
+
63
+ > ⚠️ **This code is largely LLM-written.** It has a test suite and the CPU
64
+ > backend is deterministic, but it comes with **no guarantees** of correctness,
65
+ > accuracy, or fitness for any purpose (see [`LICENSE`](LICENSE), MIT). Review it
66
+ > before relying on it for anything that matters.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install omi_physics # core engine (NumPy only)
72
+ pip install "omi_physics[gl]" # + PyOpenGL for the GPU compute backend
73
+ ```
74
+
75
+ Prebuilt wheels ship the compiled accelerators, so no C compiler is needed. A
76
+ source install without a compiler still works — the engine falls back to the
77
+ identical pure-NumPy code paths.
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ import numpy as np
83
+ from omi_physics import PhysicsWorld, model
84
+
85
+ world = PhysicsWorld(gravity=model.Gravity(gravity=9.81, direction=(0, -1, 0)))
86
+
87
+ # A static ground box and a dynamic box that falls onto it.
88
+ ground = world.add_shape(model.Shape.box((20, 1, 20)))
89
+ box = world.add_shape(model.Shape.box((1, 1, 1)))
90
+ wood = world.add_material(model.Material())
91
+
92
+ world.add_body(model.Motion(type=model.STATIC),
93
+ collider=model.Collider(shape=ground, physicsMaterial=wood),
94
+ position=(0, 0, 0))
95
+ falling = world.add_body(model.Motion(type=model.DYNAMIC, mass=1.0),
96
+ collider=model.Collider(shape=box, physicsMaterial=wood),
97
+ position=(0, 5, 0))
98
+
99
+ for _ in range(120): # 2 seconds at 60 Hz
100
+ world.step(1 / 60)
101
+
102
+ print(world.position[falling]) # resting on the ground
103
+ ```
104
+
105
+ > The exact `add_body`/`add_shape` signatures are the source of truth in
106
+ > [`world.py`](src/omi_physics/world.py); see the tests for worked examples.
107
+
108
+ ### Off-thread simulation
109
+
110
+ `ThreadedSimulation` steps the world on a daemon thread and publishes an
111
+ immutable pose snapshot each tick. A renderer reads the latest snapshot every
112
+ frame without ever blocking on the solver:
113
+
114
+ ```python
115
+ from omi_physics.threaded import ThreadedSimulation
116
+
117
+ sim = ThreadedSimulation(world, sim_hz=120)
118
+ sim.start()
119
+ # ... each render frame:
120
+ snapshot, version = sim.latest() # (position, axis_angle, awake, dynamic)
121
+ # ... on shutdown:
122
+ sim.stop()
123
+ ```
124
+
125
+ ## How it works
126
+
127
+ One `world.step(dt)` runs this fixed-timestep pipeline over the world's
128
+ structure-of-arrays state:
129
+
130
+ ```mermaid
131
+ flowchart LR
132
+ A[integrate forces<br/>gravity · damping · drag] --> B[refit AABBs]
133
+ B --> C[broadphase<br/>sweep & prune]
134
+ C --> D[narrowphase<br/>SAT · GJK/EPA]
135
+ D --> E[solver<br/>sequential impulse]
136
+ E --> F[joints]
137
+ F --> G[integrate positions]
138
+ G --> H[sleeping]
139
+ ```
140
+
141
+ The data model is OMI glTF physics (`model.Shape`, `Motion`, `Collider`,
142
+ `Material`, `Joint`, ...), so scenes round-trip to and from glTF documents
143
+ (`omi_physics.omi_gltf`). See [`docs/`](docs/) for a deep dive:
144
+
145
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — components, layering, and how
146
+ they fit together.
147
+ - [`docs/PIPELINE.md`](docs/PIPELINE.md) — the step pipeline stage by stage, with
148
+ the data that flows between stages.
149
+ - [`docs/DATA-MODEL.md`](docs/DATA-MODEL.md) — the OMI data model and the
150
+ structure-of-arrays world state.
151
+ - [`docs/ACCELERATORS.md`](docs/ACCELERATORS.md) — the Cython accelerators and the
152
+ pure-Python fallback contract.
153
+
154
+ ## Working on omi_physics
155
+
156
+ ```bash
157
+ git clone https://github.com/mcfletch/omi_physics
158
+ cd omi_physics
159
+ python -m venv .venv && source .venv/bin/activate
160
+ pip install -e ".[dev]" # editable install, builds the accelerators
161
+ pytest # run the test suite
162
+ ```
163
+
164
+ Handy commands:
165
+
166
+ ```bash
167
+ # Rebuild the accelerators in place after editing a .pyx
168
+ python setup.py build_ext --inplace
169
+
170
+ # Force the pure-Python fallback (delete the compiled modules)
171
+ rm -f src/omi_physics/*.so
172
+
173
+ # Type-check and lint
174
+ mypy src/omi_physics
175
+ ruff check src tests
176
+ ```
177
+
178
+ The accelerators are a **pure speedup**: every `.pyx` has an identical NumPy/
179
+ Python implementation the engine uses when the compiled module is absent. Tests
180
+ must pass in both configurations (with and without the `.so` files present).
181
+
182
+ ## Layout
183
+
184
+ ```
185
+ src/omi_physics/ the engine (importable as omi_physics)
186
+ model.py OMI glTF physics data model
187
+ world.py PhysicsWorld — structure-of-arrays state + step()
188
+ backend.py NumpyBackend / GPU backend selection
189
+ glcompute.py optional GL 4.3 compute integration backend (needs PyOpenGL)
190
+ broadphase.py sweep-and-prune + dynamic AABB tree
191
+ collide.py SAT box-box, sphere tests (vectorized)
192
+ gjk.py GJK/EPA for convex shapes
193
+ narrowphase.py contact generation, routes to accelerators
194
+ solver.py island-parallel sequential-impulse contact solver
195
+ joints.py point / distance / hinge constraints and motors
196
+ character.py kinematic character controller
197
+ cookery.py / hull.py shape cooking (convex hulls, trimeshes)
198
+ omi_gltf.py read/write OMI physics from glTF documents
199
+ threaded.py ThreadedSimulation — background-thread stepping
200
+ _solver_native.pyx Cython contact solver accelerator
201
+ _collide_native.pyx Cython collision accelerator
202
+ tests/ pytest suite (pure NumPy; GPU-parity tests skip w/o GL)
203
+ docs/ deep-dive documentation (Markdown + Mermaid)
204
+ ```
205
+
206
+ ## License
207
+
208
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,164 @@
1
+ # omi_physics
2
+
3
+ A renderer-agnostic, real-time **rigid-body physics engine** for Python, built
4
+ natively on the [OMI glTF physics](https://github.com/omigroup/gltf-extensions)
5
+ data model. State lives in flat NumPy arrays, the whole step pipeline is
6
+ vectorized, and optional compiled Cython accelerators drop in transparently for
7
+ the hot paths (the contact solver and box/sphere collision).
8
+
9
+ - **NumPy is the only hard dependency.** No graphics library is required to
10
+ simulate.
11
+ - **PyOpenGL is optional** — used solely by the experimental GPU compute backend
12
+ (`omi_physics.glcompute`).
13
+ - **Deterministic** on the CPU backend: the same inputs produce the same
14
+ trajectory, run to run.
15
+ - **Fast**: sweep-and-prune broadphase, SAT/GJK narrowphase, an island-parallel
16
+ sequential-impulse solver, sleeping, and a background-threaded simulation loop
17
+ that overlaps with a consumer's render/IO thread.
18
+
19
+ > ⚠️ **This code is largely LLM-written.** It has a test suite and the CPU
20
+ > backend is deterministic, but it comes with **no guarantees** of correctness,
21
+ > accuracy, or fitness for any purpose (see [`LICENSE`](LICENSE), MIT). Review it
22
+ > before relying on it for anything that matters.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install omi_physics # core engine (NumPy only)
28
+ pip install "omi_physics[gl]" # + PyOpenGL for the GPU compute backend
29
+ ```
30
+
31
+ Prebuilt wheels ship the compiled accelerators, so no C compiler is needed. A
32
+ source install without a compiler still works — the engine falls back to the
33
+ identical pure-NumPy code paths.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ import numpy as np
39
+ from omi_physics import PhysicsWorld, model
40
+
41
+ world = PhysicsWorld(gravity=model.Gravity(gravity=9.81, direction=(0, -1, 0)))
42
+
43
+ # A static ground box and a dynamic box that falls onto it.
44
+ ground = world.add_shape(model.Shape.box((20, 1, 20)))
45
+ box = world.add_shape(model.Shape.box((1, 1, 1)))
46
+ wood = world.add_material(model.Material())
47
+
48
+ world.add_body(model.Motion(type=model.STATIC),
49
+ collider=model.Collider(shape=ground, physicsMaterial=wood),
50
+ position=(0, 0, 0))
51
+ falling = world.add_body(model.Motion(type=model.DYNAMIC, mass=1.0),
52
+ collider=model.Collider(shape=box, physicsMaterial=wood),
53
+ position=(0, 5, 0))
54
+
55
+ for _ in range(120): # 2 seconds at 60 Hz
56
+ world.step(1 / 60)
57
+
58
+ print(world.position[falling]) # resting on the ground
59
+ ```
60
+
61
+ > The exact `add_body`/`add_shape` signatures are the source of truth in
62
+ > [`world.py`](src/omi_physics/world.py); see the tests for worked examples.
63
+
64
+ ### Off-thread simulation
65
+
66
+ `ThreadedSimulation` steps the world on a daemon thread and publishes an
67
+ immutable pose snapshot each tick. A renderer reads the latest snapshot every
68
+ frame without ever blocking on the solver:
69
+
70
+ ```python
71
+ from omi_physics.threaded import ThreadedSimulation
72
+
73
+ sim = ThreadedSimulation(world, sim_hz=120)
74
+ sim.start()
75
+ # ... each render frame:
76
+ snapshot, version = sim.latest() # (position, axis_angle, awake, dynamic)
77
+ # ... on shutdown:
78
+ sim.stop()
79
+ ```
80
+
81
+ ## How it works
82
+
83
+ One `world.step(dt)` runs this fixed-timestep pipeline over the world's
84
+ structure-of-arrays state:
85
+
86
+ ```mermaid
87
+ flowchart LR
88
+ A[integrate forces<br/>gravity · damping · drag] --> B[refit AABBs]
89
+ B --> C[broadphase<br/>sweep & prune]
90
+ C --> D[narrowphase<br/>SAT · GJK/EPA]
91
+ D --> E[solver<br/>sequential impulse]
92
+ E --> F[joints]
93
+ F --> G[integrate positions]
94
+ G --> H[sleeping]
95
+ ```
96
+
97
+ The data model is OMI glTF physics (`model.Shape`, `Motion`, `Collider`,
98
+ `Material`, `Joint`, ...), so scenes round-trip to and from glTF documents
99
+ (`omi_physics.omi_gltf`). See [`docs/`](docs/) for a deep dive:
100
+
101
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — components, layering, and how
102
+ they fit together.
103
+ - [`docs/PIPELINE.md`](docs/PIPELINE.md) — the step pipeline stage by stage, with
104
+ the data that flows between stages.
105
+ - [`docs/DATA-MODEL.md`](docs/DATA-MODEL.md) — the OMI data model and the
106
+ structure-of-arrays world state.
107
+ - [`docs/ACCELERATORS.md`](docs/ACCELERATORS.md) — the Cython accelerators and the
108
+ pure-Python fallback contract.
109
+
110
+ ## Working on omi_physics
111
+
112
+ ```bash
113
+ git clone https://github.com/mcfletch/omi_physics
114
+ cd omi_physics
115
+ python -m venv .venv && source .venv/bin/activate
116
+ pip install -e ".[dev]" # editable install, builds the accelerators
117
+ pytest # run the test suite
118
+ ```
119
+
120
+ Handy commands:
121
+
122
+ ```bash
123
+ # Rebuild the accelerators in place after editing a .pyx
124
+ python setup.py build_ext --inplace
125
+
126
+ # Force the pure-Python fallback (delete the compiled modules)
127
+ rm -f src/omi_physics/*.so
128
+
129
+ # Type-check and lint
130
+ mypy src/omi_physics
131
+ ruff check src tests
132
+ ```
133
+
134
+ The accelerators are a **pure speedup**: every `.pyx` has an identical NumPy/
135
+ Python implementation the engine uses when the compiled module is absent. Tests
136
+ must pass in both configurations (with and without the `.so` files present).
137
+
138
+ ## Layout
139
+
140
+ ```
141
+ src/omi_physics/ the engine (importable as omi_physics)
142
+ model.py OMI glTF physics data model
143
+ world.py PhysicsWorld — structure-of-arrays state + step()
144
+ backend.py NumpyBackend / GPU backend selection
145
+ glcompute.py optional GL 4.3 compute integration backend (needs PyOpenGL)
146
+ broadphase.py sweep-and-prune + dynamic AABB tree
147
+ collide.py SAT box-box, sphere tests (vectorized)
148
+ gjk.py GJK/EPA for convex shapes
149
+ narrowphase.py contact generation, routes to accelerators
150
+ solver.py island-parallel sequential-impulse contact solver
151
+ joints.py point / distance / hinge constraints and motors
152
+ character.py kinematic character controller
153
+ cookery.py / hull.py shape cooking (convex hulls, trimeshes)
154
+ omi_gltf.py read/write OMI physics from glTF documents
155
+ threaded.py ThreadedSimulation — background-thread stepping
156
+ _solver_native.pyx Cython contact solver accelerator
157
+ _collide_native.pyx Cython collision accelerator
158
+ tests/ pytest suite (pure NumPy; GPU-parity tests skip w/o GL)
159
+ docs/ deep-dive documentation (Markdown + Mermaid)
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,85 @@
1
+ # Accelerators
2
+
3
+ The two hot stages — the contact **solver** and box/sphere **collision** — have
4
+ optional Cython implementations that compile to native code. They are a **pure
5
+ speedup**: when the compiled module is present the engine uses it; when it is
6
+ absent the engine runs an identical NumPy/Python implementation and produces the
7
+ same results. Nothing else in the code changes.
8
+
9
+ | Accelerator | Replaces | Used by |
10
+ | --- | --- | --- |
11
+ | `_solver_native.pyx` | sequential-impulse inner loops (constraint prep, warm start, velocity + position iterations) | `solver.py` |
12
+ | `_collide_native.pyx` | SAT box-box (with Sutherland–Hodgman clipping), sphere-sphere, sphere-box | `narrowphase.py` |
13
+
14
+ ## The fallback contract
15
+
16
+ Each consumer imports its accelerator inside a `try` / `except ImportError`, sets
17
+ the module to `None` on failure, and guards every call site:
18
+
19
+ ```python
20
+ # solver.py
21
+ try:
22
+ from . import _solver_native as _native # compiled Cython inner loops
23
+ except ImportError:
24
+ _native = None
25
+ ...
26
+ if _native is not None and hasattr(_native, 'prepare_and_solve'):
27
+ self._solve_native_full(world, contacts) # native path
28
+ else:
29
+ ... # identical NumPy/Python path
30
+ ```
31
+
32
+ ```mermaid
33
+ flowchart TD
34
+ CALL[stage runs] --> Q{compiled module<br/>importable?}
35
+ Q -->|yes| N[native path<br/>_solver_native / _collide_native]
36
+ Q -->|no| P[pure NumPy / Python path]
37
+ N --> R[result]
38
+ P --> R
39
+ R --> SAME([same numbers, either way])
40
+ ```
41
+
42
+ The `hasattr` check is deliberate: it means a partially-built or older accelerator
43
+ missing a given entry point still degrades to the Python path for that call rather
44
+ than crashing.
45
+
46
+ ## Why these two, and how they stay honest
47
+
48
+ Integration and broadphase are already whole-array NumPy operations — they are as
49
+ fast as they are going to get without leaving Python, and there is little to gain
50
+ from native code. The solver and narrow phase are different: they are inherently
51
+ **per-contact, sequential** work (Gauss–Seidel resolves each contact reading the
52
+ velocities the previous contact just wrote), which NumPy cannot vectorize away. So
53
+ those are the loops worth compiling.
54
+
55
+ Because both paths must agree, the accelerators are **differential-tested**: the
56
+ test suite exercises the same scenes with and without the compiled modules and
57
+ asserts the trajectories match. When you change a `.pyx`, keep its Python twin in
58
+ lockstep and run the suite both ways:
59
+
60
+ ```bash
61
+ pytest # with the compiled .so present
62
+ rm -f src/omi_physics/*.so
63
+ pytest # pure-Python fallback
64
+ python setup.py build_ext --inplace # rebuild
65
+ ```
66
+
67
+ ## Building
68
+
69
+ The accelerators build from source via `setup.py`, which cythonizes the `.pyx`
70
+ files (falling back to shipped `.c` if Cython is unavailable) and **never fails
71
+ the install** — a missing compiler just leaves the pure-Python path in place:
72
+
73
+ ```mermaid
74
+ flowchart LR
75
+ PYX[".pyx source"] -->|Cython present| C[".c"]
76
+ C -->|C compiler present| SO[".so compiled module"]
77
+ PYX -.->|no Cython, shipped .c| C
78
+ C -.->|no compiler| FB[skip → pure-Python fallback]
79
+ SO --> WHEEL[wheel ships the .so]
80
+ ```
81
+
82
+ Prebuilt wheels (built in CI with `cibuildwheel`) carry the compiled `.so` for
83
+ each supported Python and platform, plus the `.pyx` sources so a source install
84
+ can still rebuild. End users therefore get native speed with no compiler; source
85
+ installs without a toolchain still work, only slower.
@@ -0,0 +1,99 @@
1
+ # Architecture
2
+
3
+ `omi_physics` is a rigid-body engine whose state lives in flat NumPy arrays and
4
+ whose simulation runs as a fixed-timestep pipeline of vectorized stages. Nothing
5
+ in the core touches a graphics library; a renderer (or a network layer, or a
6
+ head-less test) sits *outside* and reads poses back each frame.
7
+
8
+ ## Layers
9
+
10
+ ```mermaid
11
+ flowchart TD
12
+ subgraph consumer["Consumer (your app) — outside omi_physics"]
13
+ R[renderer / game loop / test]
14
+ end
15
+
16
+ subgraph api["Public API"]
17
+ W[PhysicsWorld<br/>world.py]
18
+ TS[ThreadedSimulation<br/>threaded.py]
19
+ M[model.py<br/>OMI data model]
20
+ G[omi_gltf.py<br/>glTF read/write]
21
+ end
22
+
23
+ subgraph pipeline["Step pipeline"]
24
+ BK[backend.py<br/>Numpy / GPU]
25
+ BP[broadphase.py]
26
+ NP[narrowphase.py]
27
+ CO[collide.py · gjk.py]
28
+ SV[solver.py]
29
+ JO[joints.py]
30
+ TR[triggers.py]
31
+ SL[sleeping]
32
+ end
33
+
34
+ subgraph accel["Optional accelerators"]
35
+ SN[_solver_native.pyx]
36
+ CN[_collide_native.pyx]
37
+ end
38
+
39
+ subgraph opt["Optional GL (PyOpenGL)"]
40
+ GC[glcompute.py]
41
+ end
42
+
43
+ R -->|step / latest| W
44
+ R --> TS
45
+ TS --> W
46
+ W --> M
47
+ G <--> M
48
+ W --> BK
49
+ BK --> BP
50
+ BP -->|solid pairs| NP --> SV --> JO --> SL
51
+ BP -->|trigger pairs| TR
52
+ TR -->|enter/exit events| W
53
+ NP --> CO
54
+ SV -. uses when present .-> SN
55
+ NP -. uses when present .-> CN
56
+ BK -. GPU backend .-> GC
57
+ ```
58
+
59
+ The arrows into the accelerators are dashed because they are **optional**: when
60
+ the compiled modules are absent, the same stages run their pure-NumPy code. GL is
61
+ similarly optional — only `glcompute.py` imports PyOpenGL, and only when a caller
62
+ explicitly selects the GPU backend.
63
+
64
+ ## Modules
65
+
66
+ | Module | Responsibility |
67
+ | --- | --- |
68
+ | `model.py` | The OMI glTF physics data model: `Shape`, `Motion`, `Collider`, `Trigger`, `Material`, `CollisionFilter`, `Gravity`, `Joint`. Immutable dataclasses. |
69
+ | `world.py` | `PhysicsWorld` — the structure-of-arrays state and the `step(dt)` that advances it. Source of truth for the simulation. |
70
+ | `mathutil.py` | Quaternion / matrix helpers, batched (`quat_integrate`, `quat_to_axis_angle`, `quat_to_matrix`, `cross3`). |
71
+ | `backend.py` | The compute-backend surface. `NumpyBackend` is the CPU path; `select_backend()` picks CPU vs GPU. |
72
+ | `glcompute.py` | `GLComputeBackend` — per-body integration as GL 4.3 compute shaders. Requires PyOpenGL; falls back to NumPy. |
73
+ | `broadphase.py` | Sweep-and-prune plus a dynamic AABB tree; produces candidate overlapping pairs. |
74
+ | `collide.py` | Vectorized SAT box-box and sphere tests producing contact manifolds. |
75
+ | `gjk.py` | GJK/EPA for general convex shapes. |
76
+ | `narrowphase.py` | Turns broadphase pairs into contacts; routes box/sphere pairs to `_collide_native` when present. |
77
+ | `solver.py` | Island-parallel sequential-impulse contact solver with warm starting; routes to `_solver_native` when present. |
78
+ | `joints.py` | Point, distance, and hinge constraints and angular motors. |
79
+ | `triggers.py` | Non-solid trigger volumes and enter/exit events. |
80
+ | `character.py` | A kinematic character controller (walking, stepping, slopes). |
81
+ | `cookery.py`, `hull.py` | Shape "cooking": convex hull / trimesh preparation. |
82
+ | `omi_gltf.py` | Read OMI physics bodies out of a glTF document and write them back. |
83
+ | `threaded.py` | `ThreadedSimulation` — runs `step()` on a daemon thread, publishes snapshots. |
84
+
85
+ ## Design principles
86
+
87
+ - **Structure-of-arrays, not array-of-structs.** Every per-body quantity is one
88
+ contiguous NumPy column, so each stage is a whole-array operation over the
89
+ awake bodies rather than a Python loop. This is what makes the CPU path fast
90
+ and what makes a future full-GPU residency plausible.
91
+ - **Renderer-agnostic core.** The world never imports a scene graph or a GL
92
+ binding. Consumers read `world.position` / `world.orientation` (or a
93
+ `ThreadedSimulation` snapshot) and draw however they like.
94
+ - **Accelerators are a pure speedup.** Each `.pyx` mirrors a NumPy/Python
95
+ implementation exactly; the engine imports the compiled module when present and
96
+ is otherwise unchanged. See [ACCELERATORS.md](ACCELERATORS.md).
97
+ - **Deterministic on CPU.** The NumPy backend is float64 and order-stable, so a
98
+ scene replays identically. The GPU backend is float32 and matches only within
99
+ tolerance — a deliberate trade for scale.