OpenSceneGraph 0.1.2__cp313-cp313-win_amd64.whl
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.
- OpenSceneGraph/OpenThreads.lib +0 -0
- OpenSceneGraph/_OpenSceneGraph.cp313-win_amd64.pyd +0 -0
- OpenSceneGraph/__init__.py +52 -0
- OpenSceneGraph/aipython/00-index.md +44 -0
- OpenSceneGraph/aipython/01-core.md +255 -0
- OpenSceneGraph/aipython/02-inspect.md +81 -0
- OpenSceneGraph/aipython/03-headless-frames.md +149 -0
- OpenSceneGraph/aipython/05-camera-manipulator.md +68 -0
- OpenSceneGraph/aipython/06-camera-effects.md +132 -0
- OpenSceneGraph/aipython/07-camera-manual.md +88 -0
- OpenSceneGraph/aipython/08-lighting.md +121 -0
- OpenSceneGraph/aipython/09-picking.md +90 -0
- OpenSceneGraph/aipython/10-rtt.md +199 -0
- OpenSceneGraph/aipython/11-mrt.md +104 -0
- OpenSceneGraph/aipython/12-gbuffer.md +48 -0
- OpenSceneGraph/aipython/15-shader-hotswap.md +72 -0
- OpenSceneGraph/aipython/17-particles.md +189 -0
- OpenSceneGraph/aipython/18-deterministic-captures.md +157 -0
- OpenSceneGraph/aipython/20-object-lifetime.md +77 -0
- OpenSceneGraph/aipython/25-async-osgpy.md +228 -0
- OpenSceneGraph/aipython/29-material.md +109 -0
- OpenSceneGraph/aipython/30-pbribl.md +162 -0
- OpenSceneGraph/aipython/40-typed-lights-gizmos.md +218 -0
- OpenSceneGraph/examples/__init__.py +24 -0
- OpenSceneGraph/examples/__main__.py +143 -0
- OpenSceneGraph/examples/blur.py +370 -0
- OpenSceneGraph/examples/info.py +126 -0
- OpenSceneGraph/examples/mrt.py +493 -0
- OpenSceneGraph/examples/pyosg_async.py +467 -0
- OpenSceneGraph/examples/pyosg_example.py +120 -0
- OpenSceneGraph/examples/pyosg_repl.py +842 -0
- OpenSceneGraph/examples/pyosg_visitor.py +138 -0
- OpenSceneGraph/ktx.dll +0 -0
- OpenSceneGraph/ktx.lib +0 -0
- OpenSceneGraph/osg.lib +0 -0
- OpenSceneGraph/osg161-osg.dll +0 -0
- OpenSceneGraph/osg161-osgAnimation.dll +0 -0
- OpenSceneGraph/osg161-osgDB.dll +0 -0
- OpenSceneGraph/osg161-osgFX.dll +0 -0
- OpenSceneGraph/osg161-osgGA.dll +0 -0
- OpenSceneGraph/osg161-osgText.dll +0 -0
- OpenSceneGraph/osg161-osgUtil.dll +0 -0
- OpenSceneGraph/osg161-osgViewer.dll +0 -0
- OpenSceneGraph/osg161-osgWidget.dll +0 -0
- OpenSceneGraph/osgAnimation.lib +0 -0
- OpenSceneGraph/osgDB.lib +0 -0
- OpenSceneGraph/osgFX.lib +0 -0
- OpenSceneGraph/osgGA.lib +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/jpeg62-ebb2f26be87097e77bafd1e9095b4820.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/ktx.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/liblzma-b9cff3753c4848b9ff1350ba5a053c6e.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/libpng16-36b7e1e8d185cb21280e5688f99660c0.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/msvcp140.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_bmp.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_dds.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_gltf.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_hdr.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_jpeg.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_ktx2.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_obj.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_osg.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_png.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_pnm.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_rgb.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_serializers_osg.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_stl.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_tga.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/osgdb_tiff.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/tiff-1defa8059e5ba115a3ab4b120de6f4bb.dll +0 -0
- OpenSceneGraph/osgPlugins-3.6.5/z.dll +0 -0
- OpenSceneGraph/osgText.lib +0 -0
- OpenSceneGraph/osgUtil.lib +0 -0
- OpenSceneGraph/osgViewer.lib +0 -0
- OpenSceneGraph/osgWidget.lib +0 -0
- OpenSceneGraph/osgx.cp313-win_amd64.pyd +0 -0
- OpenSceneGraph/osgx_static.lib +0 -0
- OpenSceneGraph/ot21-OpenThreads.dll +0 -0
- openscenegraph-0.1.2.dist-info/DELVEWHEEL +2 -0
- openscenegraph-0.1.2.dist-info/METADATA +563 -0
- openscenegraph-0.1.2.dist-info/RECORD +90 -0
- openscenegraph-0.1.2.dist-info/WHEEL +5 -0
- openscenegraph-0.1.2.dist-info/entry_points.txt +6 -0
- openscenegraph-0.1.2.dist-info/licenses/LICENSE +21 -0
- openscenegraph.libs/jpeg62-ebb2f26be87097e77bafd1e9095b4820.dll +0 -0
- openscenegraph.libs/liblzma-b9cff3753c4848b9ff1350ba5a053c6e.dll +0 -0
- openscenegraph.libs/libpng16-36b7e1e8d185cb21280e5688f99660c0.dll +0 -0
- openscenegraph.libs/msvcp140.dll +0 -0
- openscenegraph.libs/tiff-1defa8059e5ba115a3ab4b120de6f4bb.dll +0 -0
- openscenegraph.libs/z.dll +0 -0
- osgx.py +7 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# osgx typed direct lights (`osgx.LightSet`) + light gizmos
|
|
2
|
+
|
|
3
|
+
Check `git log -1 -- src/osgx/PBR.hpp` in the configured osgx source dir (see
|
|
4
|
+
the repo's `CLAUDE.md` on `PYOSG_OSGX_SOURCE_DIR`) if anything here looks
|
|
5
|
+
stale — this describes an SSBO-backed contract that may still be uncommitted
|
|
6
|
+
in some checkouts.
|
|
7
|
+
|
|
8
|
+
## What this is
|
|
9
|
+
|
|
10
|
+
`osgx::pbr::LightSet` (C++: `src/osgx/PBR.hpp`/`src/PBR.cpp`, Python:
|
|
11
|
+
`osgx.LightSet`) is a typed direct-light rig. Per-light data
|
|
12
|
+
(`osgx.MAX_LIGHTS` == 6 slots) lives in a single `std430` Shader Storage
|
|
13
|
+
Buffer Object (`osgx_LightBuffer`/`osgx_lights[]`, binding 3 — C++
|
|
14
|
+
`osgx::LIGHT_BINDING`, not currently exposed to Python) plus one
|
|
15
|
+
`osgx_lightCount` uniform — not parallel flat uniform arrays. Three real
|
|
16
|
+
types — Point, Directional, Spot — plus a "Sphere" light that is NOT a
|
|
17
|
+
fourth type: it's `setPoint(..., sourceRadius>0)`, see below.
|
|
18
|
+
|
|
19
|
+
Every consumer shader reaches the light array through one hook call,
|
|
20
|
+
**`osgx_DirectLighting(N, V, worldPos, mat)`** — declared by
|
|
21
|
+
`osgx.DIRECT_LIGHTING_DECL` (splice into your fragment shader's
|
|
22
|
+
`#pragma osgx::pbr ...` line, alongside `MATERIAL_STRUCT`) and defined by
|
|
23
|
+
`osgx.DIRECT_LIGHTING_HOOK_DEFAULT`, a second self-contained FRAGMENT
|
|
24
|
+
shader object added to the same `Program`.
|
|
25
|
+
`osgx.makeDirectLightingHookShader()` returns that shader object ready
|
|
26
|
+
to append:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
program = osg.Program(name="my-shader", shaders=(
|
|
30
|
+
osg.Shader(osg.Shader.VERTEX, vertex_source),
|
|
31
|
+
osg.Shader(osg.Shader.FRAGMENT, osgx.resolveShaderLibs(fragment_source)),
|
|
32
|
+
osgx.makeDirectLightingHookShader(),
|
|
33
|
+
))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This is the same "hook" pattern `osgSlug` uses: a base shader owns `main()`
|
|
37
|
+
and only *declares* a fixed entry point; each consumer supplies — or accepts
|
|
38
|
+
the library default for — that entry point's definition as a separate
|
|
39
|
+
compiled shader object. It exists so the per-light dispatch loop lives in
|
|
40
|
+
exactly one place instead of being hand-copied into every consumer's
|
|
41
|
+
`main()`.
|
|
42
|
+
|
|
43
|
+
`osgx::LightGizmos`/`osgx::LightMarkers` (C++: `src/osgx/Gizmos.hpp`/
|
|
44
|
+
`src/Gizmos.cpp`, Python: `osgx.LightGizmos`/`osgx.LightMarkers` — flat under
|
|
45
|
+
`osgx`, not a `gizmo` submodule) are the matching debug visualization: real
|
|
46
|
+
depth-tested marker geometry for point/sphere/spot, plus a non-depth-tested
|
|
47
|
+
overlay for directional (which has no position to place a marker at). Both
|
|
48
|
+
take a live `osgx.LightSet` directly and read it through its typed
|
|
49
|
+
accessors (`getCount`/`getType`/`getPosIntensity`/`getColor`/`getDirection`/
|
|
50
|
+
`getSpotAngles`/`getSourceRadius`).
|
|
51
|
+
|
|
52
|
+
## Minimal live REPL setup
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
lights = osgx.LightSet.create(root.stateSet) # allocates the SSBO buffer (size MAX_LIGHTS, zero-initialized) + osgx_lightCount on root.stateSet
|
|
56
|
+
|
|
57
|
+
lights.setCount(1) # how many of the 6 slots osgx_DirectLighting()'s loop actually reads this frame
|
|
58
|
+
|
|
59
|
+
# Point -- inverse-square falloff, ideal (zero-size) specular highlight
|
|
60
|
+
lights.setPoint(0, osg.Vec3(2.5, -2.5, 6.0), osg.Vec3(0.85, 0.55, 0.30), 12.0)
|
|
61
|
+
|
|
62
|
+
# Directional -- "travel direction" (KHR_lights_punctual convention), no position, no falloff
|
|
63
|
+
lights.setDirectional(0, osg.Vec3(0.2, 0.4, -1.0), osg.Vec3(0.75, 0.70, 0.60), 3.0)
|
|
64
|
+
|
|
65
|
+
# Spot -- cone-attenuated point light; angles in RADIANS (use math.radians(), no osg.DegreesToRadians)
|
|
66
|
+
import math
|
|
67
|
+
lights.setSpot(
|
|
68
|
+
0, osg.Vec3(0.0, -4.0, 8.0), osg.Vec3(0.0, 1.0, -0.35), osg.Vec3(0.95, 0.85, 0.65),
|
|
69
|
+
40.0, math.radians(15.0), math.radians(32.0),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Sphere -- NOT a separate call: setPoint(..., sourceRadius>0). Widens the SPECULAR highlight only
|
|
73
|
+
# (Karis/UE4 representative-point trick) -- diffuse falloff is deliberately IDENTICAL to a plain
|
|
74
|
+
# point light at the same position/intensity. Has visibly ZERO effect on a fully rough/matte
|
|
75
|
+
# surface -- that's expected, not a bug.
|
|
76
|
+
lights.setPoint(0, osg.Vec3(2.5, -2.5, 6.0), osg.Vec3(0.85, 0.55, 0.30), 12.0, sourceRadius=0.7)
|
|
77
|
+
|
|
78
|
+
# Read back any field live:
|
|
79
|
+
lights.getType(0), lights.getPosIntensity(0), lights.getColor(0), lights.getSourceRadius(0)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`LightSet.create()` REPLACES the SSBO buffer + `osgx_lightCount` on that
|
|
83
|
+
StateSet — don't call it twice on the same live StateSet if you've already
|
|
84
|
+
populated lights there, or you'll zero it out. To WRAP an already-populated
|
|
85
|
+
StateSet's LightSet instead, construct `osgx.LightSet()` and set `.ss`
|
|
86
|
+
directly — `LightSet` has exactly one field (`ss`); the setters/getters
|
|
87
|
+
mutate the SSBO/uniforms that `ss` already carries, there is no separate
|
|
88
|
+
`.lights` handle to assign:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
lights = osgx.LightSet()
|
|
92
|
+
lights.ss = already_populated_stateset
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A rig-building helper (`pyosg-match4-dice.py`'s `add_torch_rig()`):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
def add_torch_rig(stateset, torches):
|
|
99
|
+
"""Build an osgx.LightSet on `stateset` from (position, intensity, color) tuples."""
|
|
100
|
+
lights = osgx.LightSet.create(stateset)
|
|
101
|
+
|
|
102
|
+
for i, (pos, intensity, color) in enumerate(torches):
|
|
103
|
+
lights.setPoint(i, osg.Vec3(*pos), osg.Vec3(*color), intensity)
|
|
104
|
+
|
|
105
|
+
lights.setCount(len(torches))
|
|
106
|
+
|
|
107
|
+
return lights
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Gizmos
|
|
111
|
+
|
|
112
|
+
`osgx.LightGizmos` is itself an `osg.Group` — construct it and add the one
|
|
113
|
+
instance to the scene graph; its constructor already parents both the marker
|
|
114
|
+
geometry and the directional overlay as its own children, so don't also
|
|
115
|
+
add `.markers`/`.overlay` separately:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
gizmos = osgx.LightGizmos(lights, root, 0.4, 10.0) # (LightSet, scene node for directional's bound-sizing, minMarkerRadius, spotConeLength)
|
|
119
|
+
|
|
120
|
+
root.children.append(gizmos)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`.markers` (an `osgx.LightMarkers`) and `.overlay` (an `osg.Camera`) are
|
|
124
|
+
read-only properties for reaching the two pieces individually — e.g. to
|
|
125
|
+
toggle visibility:
|
|
126
|
+
|
|
127
|
+
- `gizmos.markers`' point/spot markers are real scene geometry and DO get
|
|
128
|
+
depth-occluded by actual room geometry (no shadow-mapping in this system,
|
|
129
|
+
so occlusion only affects the debug marker's visibility, never the shading
|
|
130
|
+
math). Keep test-light Z below the real ceiling if you want to see the
|
|
131
|
+
marker.
|
|
132
|
+
- `gizmos.overlay` is unaffected (`GL_DEPTH_TEST` off, `POST_RENDER`).
|
|
133
|
+
- Toggle `gizmos.markers.nodeMask = 0` / `gizmos.overlay.nodeMask = 0` to
|
|
134
|
+
hide them without removing them.
|
|
135
|
+
- `LightGizmos::computeBound()` is overridden to always return an empty
|
|
136
|
+
bound — gizmo geometry deliberately never affects `TrackballManipulator`
|
|
137
|
+
auto-fit/`home()` or cull's near/far, even though the overlay's own
|
|
138
|
+
plane/arrow are sized larger than the scene it annotates.
|
|
139
|
+
|
|
140
|
+
## Live ImGui control panel
|
|
141
|
+
|
|
142
|
+
`osgx.imgui`'s Python bindings are function-style wrappers (`slider_float`,
|
|
143
|
+
`color_edit3`, `checkbox`, `radio_group`, `button`, `text`, `separator`,
|
|
144
|
+
`input_text` — all return `(changed, *new_values)` tuples), not the raw C++
|
|
145
|
+
`ImGui::` namespace. There is no `SliderFloat3`, `PushID`/`PopID`, or
|
|
146
|
+
`SameLine`. Build a vec3 control from three `slider_float` calls, and give
|
|
147
|
+
every widget label a `##<section-scope>` suffix — `Panel::draw()` only
|
|
148
|
+
`PushID`s a section when `options.expand=True`, so plain sections share one
|
|
149
|
+
global Dear ImGui id namespace and identical labels across sections (every
|
|
150
|
+
light type has a "Position"/"Color"/"Intensity") fight over the same drag
|
|
151
|
+
state without the suffix.
|
|
152
|
+
|
|
153
|
+
Working shape — one collapsible section per light type, an
|
|
154
|
+
Activate/Deactivate button driving a single shared "live" `LightSet` slot,
|
|
155
|
+
sliders scoped per section:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import math
|
|
159
|
+
|
|
160
|
+
active_slot = 0 # the ONLY light -- lightCount stays 0 until a section is Activated
|
|
161
|
+
|
|
162
|
+
class LightsState:
|
|
163
|
+
def __init__(self):
|
|
164
|
+
self.active_demo = None
|
|
165
|
+
self.point_position = [2.5, -2.5, 6.0]
|
|
166
|
+
self.point_color = [0.85, 0.55, 0.30]
|
|
167
|
+
self.point_intensity = 12.0
|
|
168
|
+
# ... directional_*, sphere_* (+radius), spot_* (+direction, inner/outer deg, source_radius)
|
|
169
|
+
|
|
170
|
+
state = LightsState()
|
|
171
|
+
|
|
172
|
+
def apply_state():
|
|
173
|
+
if state.active_demo is None:
|
|
174
|
+
lights.setCount(0)
|
|
175
|
+
return
|
|
176
|
+
lights.setCount(1)
|
|
177
|
+
if state.active_demo == "Point":
|
|
178
|
+
lights.setPoint(active_slot, osg.Vec3(*state.point_position), osg.Vec3(*state.point_color), state.point_intensity)
|
|
179
|
+
# ... Directional -> setDirectional, Sphere -> setPoint(..., sourceRadius=...), Spot -> setSpot(...)
|
|
180
|
+
|
|
181
|
+
def point_section(ri): # addSection()'s callback always takes one osg.RenderInfo arg, even if unused
|
|
182
|
+
# activate_button(name): text("ACTIVE"/"(inactive)") + Activate/Deactivate osgx.imgui.button(),
|
|
183
|
+
# calling apply_state() on either. Every widget label carries a "##point"-style scope suffix.
|
|
184
|
+
# vec3_sliders(prefix, scope, values, lo, hi): three osgx.imgui.slider_float() calls, one per axis.
|
|
185
|
+
activate_button("Point")
|
|
186
|
+
changed = vec3_sliders("Position", "ptpos", state.point_position, -10.0, 10.0)
|
|
187
|
+
c, r, g, b = osgx.imgui.color_edit3("Color##pt", *state.point_color)
|
|
188
|
+
state.point_color[:] = [r, g, b]
|
|
189
|
+
changed = changed or c
|
|
190
|
+
c, v = osgx.imgui.slider_float("Intensity##pt", state.point_intensity, 0.0, 30.0)
|
|
191
|
+
state.point_intensity = v
|
|
192
|
+
changed = changed or c
|
|
193
|
+
if changed and state.active_demo == "Point": apply_state()
|
|
194
|
+
|
|
195
|
+
# directional_section / sphere_section / spot_section follow the same shape
|
|
196
|
+
# (Sphere adds a "Source Radius" slider 0..3; Spot adds Direction, Inner/Outer
|
|
197
|
+
# Cone (deg), Source Radius, and clamps inner < outer so GLSL's
|
|
198
|
+
# smoothstep(cos(outer), cos(inner), ...) stays well-formed).
|
|
199
|
+
|
|
200
|
+
lights = osgx.LightSet.create(root.stateSet)
|
|
201
|
+
gizmos = osgx.LightGizmos(lights, root, 0.4, 10.0)
|
|
202
|
+
root.children.append(gizmos)
|
|
203
|
+
|
|
204
|
+
gui = osgx.imgui.Widget(viewer)
|
|
205
|
+
gui.addSection("Directional", directional_section)
|
|
206
|
+
gui.addSection("Point", point_section)
|
|
207
|
+
gui.addSection("Sphere", sphere_section)
|
|
208
|
+
gui.addSection("Spot", spot_section)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Known gaps
|
|
212
|
+
|
|
213
|
+
No `HookList`-style helper (mirroring osgSlug's `HookList`) yet for composing
|
|
214
|
+
multiple fragment-stage hooks on one Program — `osgx_DirectLighting()` is the
|
|
215
|
+
first hook, added by hand since there's only one so far. `PBRIBL.cpp`'s
|
|
216
|
+
`evaluateIBL()` and the final tonemap/composite step are candidates for the
|
|
217
|
+
same treatment once a real second consumer needs to override one
|
|
218
|
+
specifically.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Curated, runnable examples installed with the OpenSceneGraph wheel.
|
|
2
|
+
|
|
3
|
+
Each module here exposes ``build_scene(width, height) -> osg.Node`` and, optionally,
|
|
4
|
+
``configure_viewer(viewer, root)``; see ``OpenSceneGraph.examples.__main__`` (the
|
|
5
|
+
implementation behind the ``pyosg`` console script) for the shared runner that drives both
|
|
6
|
+
hooks. Every module is also directly runnable on its own, e.g.
|
|
7
|
+
``python -m OpenSceneGraph.examples.mrt``.
|
|
8
|
+
|
|
9
|
+
This is a small, curated subset of the full example sandbox in the project's `examples/`
|
|
10
|
+
source directory -- not everything there implements the build_scene()/configure_viewer()
|
|
11
|
+
contract (yet), and most of that directory (the Lighting Series, GLSL experiments, data
|
|
12
|
+
files, etc.) isn't meant to ship in the wheel at all.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys as _sys
|
|
16
|
+
from pathlib import Path as _Path
|
|
17
|
+
|
|
18
|
+
# A couple of these examples do a bare `from pyosg_visitor import GatherVisitor` rather than
|
|
19
|
+
# a package-relative import, matching how they're written to also run standalone straight out
|
|
20
|
+
# of the project's examples/ directory (see pyosg-cli). Put this package's own directory on
|
|
21
|
+
# sys.path so that resolves the same way here.
|
|
22
|
+
_sys.path.insert(0, str(_Path(__file__).parent))
|
|
23
|
+
|
|
24
|
+
del _sys, _Path
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""CLI backing the `pyosg` console script and `python -m OpenSceneGraph.examples <name>`.
|
|
2
|
+
|
|
3
|
+
Runs one of this package's example modules standalone (no Qt, no aipython) by creating a
|
|
4
|
+
bare osgViewer.Viewer, calling the example's build_scene(w, h), and its
|
|
5
|
+
configure_viewer(viewer, root) if it defines one. Adapted from the pyosg-cli
|
|
6
|
+
proof-of-concept in the project's repository root, which loads examples by file path out of
|
|
7
|
+
examples/ instead -- useful for the full example sandbox, most of which doesn't implement
|
|
8
|
+
this module's build_scene()/configure_viewer() contract (yet).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import importlib
|
|
13
|
+
import pkgutil
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from OpenSceneGraph import *
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_module(name):
|
|
20
|
+
try:
|
|
21
|
+
return importlib.import_module(f"OpenSceneGraph.examples.{name}")
|
|
22
|
+
|
|
23
|
+
except ModuleNotFoundError as error:
|
|
24
|
+
# Only turn failure to locate the requested example itself into the
|
|
25
|
+
# friendly availability list. A ModuleNotFoundError raised by one of the
|
|
26
|
+
# example's own imports is a real diagnostic and must not be disguised as
|
|
27
|
+
# an unknown runner name.
|
|
28
|
+
if error.name != f"OpenSceneGraph.examples.{name}":
|
|
29
|
+
raise
|
|
30
|
+
|
|
31
|
+
import OpenSceneGraph.examples as _examples
|
|
32
|
+
|
|
33
|
+
# pyosg_* are shared helper modules (pyosg_example, pyosg_visitor, pyosg_repl,
|
|
34
|
+
# pyosg_dice), not runnable examples -- they have no build_scene() and were never meant
|
|
35
|
+
# to appear here, same reason a leading "_" is already excluded.
|
|
36
|
+
available = sorted(
|
|
37
|
+
mod.name for mod in pkgutil.iter_modules(_examples.__path__)
|
|
38
|
+
if not mod.name.startswith("_") and not mod.name.startswith("pyosg_")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
sys.exit(f"error: no such example: {name!r} (available: {', '.join(available)})")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_module(module, width=800, height=600, extra_argv=(), name=None):
|
|
45
|
+
"""Shared runner core: viewer construction, window setup, build_scene()/configure_viewer(),
|
|
46
|
+
and the frame loop. Deliberately takes an already-loaded module object rather than a name --
|
|
47
|
+
`run()` below loads it from the installed OpenSceneGraph.examples package, while pyosg-cli
|
|
48
|
+
(repo root) loads an arbitrary examples/*.py file by path and calls this directly, so the
|
|
49
|
+
actual viewer-setup/frame-loop logic exists in exactly one place instead of two copies that
|
|
50
|
+
can silently drift (this bit pyosg-cli once already -- see PYOSG_BUILD_PACKAGE_OVERLAY/
|
|
51
|
+
setUpViewInWindow ordering history in project_pyosg_contract_conversion for the pyosg-
|
|
52
|
+
polyhaven.py --hdr bug that ordering mismatch caused)."""
|
|
53
|
+
|
|
54
|
+
if not hasattr(module, "build_scene"):
|
|
55
|
+
sys.exit(f"error: {name or module.__name__} has no build_scene(w, h)")
|
|
56
|
+
|
|
57
|
+
viewer = osgViewer.Viewer()
|
|
58
|
+
|
|
59
|
+
# Explicit window setup, driven by the same width/height build_scene() receives -- a single
|
|
60
|
+
# source of truth instead of every example hardcoding its own OSG_WINDOW env var string that
|
|
61
|
+
# has to be kept in sync by hand (and silently isn't, the moment --width/--height differs from
|
|
62
|
+
# an example's own hardcoded default). x/y match the "50 50 ..." every example's old OSG_WINDOW
|
|
63
|
+
# used.
|
|
64
|
+
viewer.setUpViewInWindow(50, 50, width, height)
|
|
65
|
+
|
|
66
|
+
viewer.cameraManipulator = osgGA.TrackballManipulator()
|
|
67
|
+
|
|
68
|
+
# Sandbox sys.argv for the duration of both hook calls below: some examples (e.g.
|
|
69
|
+
# blur's create_scene()) read sys.argv[1] lazily, from inside build_scene() itself, not
|
|
70
|
+
# just at module-import time. extra_argv forwards e.g. `pyosg blur -- foo.gltf` through
|
|
71
|
+
# to the example's own sys.argv[1:], matching `python -m OpenSceneGraph.examples.blur
|
|
72
|
+
# foo.gltf`.
|
|
73
|
+
saved_argv = sys.argv
|
|
74
|
+
|
|
75
|
+
sys.argv = [name or module.__name__, *extra_argv]
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
root = module.build_scene(width, height)
|
|
79
|
+
|
|
80
|
+
viewer.sceneData = root
|
|
81
|
+
|
|
82
|
+
if hasattr(module, "configure_viewer"):
|
|
83
|
+
module.configure_viewer(viewer, root)
|
|
84
|
+
|
|
85
|
+
finally:
|
|
86
|
+
sys.argv = saved_argv
|
|
87
|
+
|
|
88
|
+
viewer.TODO()
|
|
89
|
+
|
|
90
|
+
# KNOWN ISSUE, deliberately not "fixed" with a sleep() here -- see feedback_runner_unthrottled_loop
|
|
91
|
+
# for the full writeup. Short version: a sleep() here treats the symptom (starves anything else
|
|
92
|
+
# on the process needing the GIL on a regular cadence, confirmed via audible clicking in
|
|
93
|
+
# pyosg-animusic-grid.py's sounddevice callback thread); the real fix under consideration is
|
|
94
|
+
# releasing the GIL inside viewer.frame()'s own pybind11 binding instead.
|
|
95
|
+
while not viewer.done:
|
|
96
|
+
viewer.frame()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run(name, width=800, height=600, extra_argv=()):
|
|
100
|
+
module = _load_module(name)
|
|
101
|
+
|
|
102
|
+
run_module(module, width, height, extra_argv, name=name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main():
|
|
106
|
+
parser = argparse.ArgumentParser(
|
|
107
|
+
prog="pyosg",
|
|
108
|
+
description="Run an OpenSceneGraph.examples module standalone, no Qt required"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
parser.add_argument("name", help="Example module name, e.g. 'mrt' for OpenSceneGraph.examples.mrt")
|
|
112
|
+
parser.add_argument("--width", type=int, default=800)
|
|
113
|
+
parser.add_argument("--height", type=int, default=600)
|
|
114
|
+
|
|
115
|
+
# Split on the FIRST literal "--" ourselves, before argparse ever sees it, and hand it
|
|
116
|
+
# only the part before that. argparse.add_argument("extra", nargs="*") looks like the
|
|
117
|
+
# obvious way to do this instead, but has a real, confirmed bug: "mrt -- foo.osgt" parses
|
|
118
|
+
# fine, but "mrt --width 640 -- foo.osgt" fails with "unrecognized arguments: -- foo.osgt"
|
|
119
|
+
# -- a known CPython argparse limitation around "--" interacting with a nargs="*"
|
|
120
|
+
# positional when other flags are interspersed, not something fixable by reordering our
|
|
121
|
+
# own argument definitions.
|
|
122
|
+
#
|
|
123
|
+
# KNOWN FRICTION (deferred, not fixed, same as pyosg-cli): every one of the target example's
|
|
124
|
+
# own arguments -- including its OWN required positionals/flags -- has to go after this "--",
|
|
125
|
+
# since this parser only knows about name/--width/--height. A `parse_known_args()`-based
|
|
126
|
+
# two-pass parse could remove the need for "--" in the common case; not attempted, needs real
|
|
127
|
+
# testing against the SAME argparse quirk documented above first.
|
|
128
|
+
argv = sys.argv[1:]
|
|
129
|
+
|
|
130
|
+
if "--" in argv:
|
|
131
|
+
i = argv.index("--")
|
|
132
|
+
own_argv, extra_argv = argv[:i], argv[i + 1:]
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
own_argv, extra_argv = argv, []
|
|
136
|
+
|
|
137
|
+
args = parser.parse_args(own_argv)
|
|
138
|
+
|
|
139
|
+
run(args.name, args.width, args.height, extra_argv)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
main()
|