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.
- omi_physics-0.1.0/LICENSE +21 -0
- omi_physics-0.1.0/MANIFEST.in +7 -0
- omi_physics-0.1.0/PKG-INFO +208 -0
- omi_physics-0.1.0/README.md +164 -0
- omi_physics-0.1.0/docs/ACCELERATORS.md +85 -0
- omi_physics-0.1.0/docs/ARCHITECTURE.md +99 -0
- omi_physics-0.1.0/docs/DATA-MODEL.md +132 -0
- omi_physics-0.1.0/docs/PIPELINE.md +113 -0
- omi_physics-0.1.0/docs/README.md +16 -0
- omi_physics-0.1.0/pyproject.toml +89 -0
- omi_physics-0.1.0/setup.cfg +4 -0
- omi_physics-0.1.0/setup.py +64 -0
- omi_physics-0.1.0/src/omi_physics/__init__.py +35 -0
- omi_physics-0.1.0/src/omi_physics/_accel.py +16 -0
- omi_physics-0.1.0/src/omi_physics/_collide_native.c +30753 -0
- omi_physics-0.1.0/src/omi_physics/_collide_native.pyx +344 -0
- omi_physics-0.1.0/src/omi_physics/_solver_native.c +31845 -0
- omi_physics-0.1.0/src/omi_physics/_solver_native.pyx +312 -0
- omi_physics-0.1.0/src/omi_physics/backend.py +120 -0
- omi_physics-0.1.0/src/omi_physics/body.py +272 -0
- omi_physics-0.1.0/src/omi_physics/broadphase.py +314 -0
- omi_physics-0.1.0/src/omi_physics/character.py +399 -0
- omi_physics-0.1.0/src/omi_physics/collide.py +378 -0
- omi_physics-0.1.0/src/omi_physics/cookery.py +93 -0
- omi_physics-0.1.0/src/omi_physics/gjk.py +264 -0
- omi_physics-0.1.0/src/omi_physics/glcompute.py +390 -0
- omi_physics-0.1.0/src/omi_physics/gravity.py +102 -0
- omi_physics-0.1.0/src/omi_physics/hull.py +153 -0
- omi_physics-0.1.0/src/omi_physics/joints.py +212 -0
- omi_physics-0.1.0/src/omi_physics/kinematic.py +80 -0
- omi_physics-0.1.0/src/omi_physics/mathutil.py +121 -0
- omi_physics-0.1.0/src/omi_physics/model.py +243 -0
- omi_physics-0.1.0/src/omi_physics/narrowphase.py +101 -0
- omi_physics-0.1.0/src/omi_physics/omi_gltf.py +334 -0
- omi_physics-0.1.0/src/omi_physics/py.typed +0 -0
- omi_physics-0.1.0/src/omi_physics/solver.py +445 -0
- omi_physics-0.1.0/src/omi_physics/threaded.py +110 -0
- omi_physics-0.1.0/src/omi_physics/triggers.py +70 -0
- omi_physics-0.1.0/src/omi_physics/world.py +676 -0
- omi_physics-0.1.0/src/omi_physics.egg-info/PKG-INFO +208 -0
- omi_physics-0.1.0/src/omi_physics.egg-info/SOURCES.txt +60 -0
- omi_physics-0.1.0/src/omi_physics.egg-info/dependency_links.txt +1 -0
- omi_physics-0.1.0/src/omi_physics.egg-info/requires.txt +19 -0
- omi_physics-0.1.0/src/omi_physics.egg-info/top_level.txt +1 -0
- omi_physics-0.1.0/tests/test_backend_parity.py +73 -0
- omi_physics-0.1.0/tests/test_backend_parity_gpu.py +181 -0
- omi_physics-0.1.0/tests/test_broadphase.py +92 -0
- omi_physics-0.1.0/tests/test_character.py +120 -0
- omi_physics-0.1.0/tests/test_character_void.py +78 -0
- omi_physics-0.1.0/tests/test_collide_primitives.py +87 -0
- omi_physics-0.1.0/tests/test_cookery.py +91 -0
- omi_physics-0.1.0/tests/test_gjk_epa.py +67 -0
- omi_physics-0.1.0/tests/test_gravity.py +62 -0
- omi_physics-0.1.0/tests/test_joints.py +93 -0
- omi_physics-0.1.0/tests/test_mesh_collision.py +62 -0
- omi_physics-0.1.0/tests/test_omi_physics_roundtrip.py +113 -0
- omi_physics-0.1.0/tests/test_physics_benchmark.py +78 -0
- omi_physics-0.1.0/tests/test_physics_impulse.py +135 -0
- omi_physics-0.1.0/tests/test_physics_integrate.py +100 -0
- omi_physics-0.1.0/tests/test_physics_kinematic_animator.py +91 -0
- omi_physics-0.1.0/tests/test_solver.py +115 -0
- 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,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.
|