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,132 @@
|
|
|
1
|
+
# Temporary camera effects (shake, kick, scripted moves) without taking control
|
|
2
|
+
|
|
3
|
+
For layering a TEMPORARY effect on top of the user's live manipulator (e.g.
|
|
4
|
+
`TrackballManipulator`) without replacing it and without the user losing
|
|
5
|
+
control afterward. Different from [`05-camera-manipulator.md`](05-camera-manipulator.md)'s
|
|
6
|
+
fully-replacing self-driving manipulators — this is about *layering*
|
|
7
|
+
something temporary on top of whatever manipulator is already there.
|
|
8
|
+
|
|
9
|
+
## Why an update callback on `viewer.camera` cannot do this
|
|
10
|
+
|
|
11
|
+
`osgViewer::Viewer::updateTraversal()` (`src/osgViewer/Viewer.cpp`) always
|
|
12
|
+
runs, in this order, every frame:
|
|
13
|
+
|
|
14
|
+
```cpp
|
|
15
|
+
_scene->updateSceneGraph(*_updateVisitor); // invokes a camera's update callback
|
|
16
|
+
...
|
|
17
|
+
_cameraManipulator->updateCamera(*_camera); // ALWAYS runs after, unconditionally
|
|
18
|
+
// overwriting whatever the update callback wrote
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`CameraManipulator::updateCamera()`'s C++ default is
|
|
22
|
+
`camera.setViewMatrix(getInverseMatrix())`, and it runs unconditionally after
|
|
23
|
+
the scene graph's own update traversal. Anything an update callback writes to
|
|
24
|
+
`camera.viewMatrix` is clobbered the same frame.
|
|
25
|
+
|
|
26
|
+
There is also no `cullCallback` exposed on `osg.Camera` in these bindings
|
|
27
|
+
(only `updateCallback`/`preDrawCallback`/`postDrawCallback`/`DrawCallback`/
|
|
28
|
+
`initialDrawCallback`/`finalDrawCallback`), and a pre/post-draw callback would
|
|
29
|
+
be too late anyway — cull has already baked that frame's matrices into the
|
|
30
|
+
render bins by draw time.
|
|
31
|
+
|
|
32
|
+
## The mechanism: `updateCamera()` is virtual, and runs LAST on purpose
|
|
33
|
+
|
|
34
|
+
`updateCamera(osg::Camera&)` is `virtual` specifically so a manipulator
|
|
35
|
+
subclass can override what gets written into the camera each frame — OSG's
|
|
36
|
+
own extension point for exactly this. The pattern is a **decorator
|
|
37
|
+
manipulator**: wrap the user's real manipulator, forward every normal
|
|
38
|
+
interaction to it untouched, and override only `updateCamera()` to compose
|
|
39
|
+
something extra on top of the inner manipulator's own output.
|
|
40
|
+
|
|
41
|
+
`updateCamera()` is bound on `osgGA::CameraManipulator`'s trampoline
|
|
42
|
+
(`pyosg/pyosgGA.hpp`/`.cpp`) and exposed as a normal method, so a decorator
|
|
43
|
+
can call `self.inner.updateCamera(camera)` directly. Uses the same
|
|
44
|
+
`call_override` (not `PYBIND11_OVERRIDE`) pattern `home(ea, aa)` uses: `osg::Camera`
|
|
45
|
+
derives from `osg::Referenced` and isn't copyable, and `PYBIND11_OVERRIDE`'s
|
|
46
|
+
implicit copy-for-marshaling would crash the instant a Python subclass
|
|
47
|
+
overrides it.
|
|
48
|
+
|
|
49
|
+
## The pattern (`examples/pyosg-fire.py`'s `EffectManipulator`)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
class EffectManipulator(osgGA.CameraManipulator):
|
|
53
|
+
def __init__(self, inner):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.inner = inner
|
|
56
|
+
self.effects = [] # zero-arg callables -> None or an osg.Matrixd to compose
|
|
57
|
+
self._node = None
|
|
58
|
+
|
|
59
|
+
# Forward everything else untouched.
|
|
60
|
+
def setNode(self, node):
|
|
61
|
+
self._node = node
|
|
62
|
+
self.inner.node = node
|
|
63
|
+
def getNode(self): return self._node
|
|
64
|
+
def home(self, *args): self.inner.home(0.0) # NOT self.inner.home(*args) — see below
|
|
65
|
+
def handle(self, ea, aa): return self.inner.handle(ea, aa) # untested live, see below
|
|
66
|
+
def getMatrix(self): return self.inner.matrix
|
|
67
|
+
def getInverseMatrix(self): return self.inner.inverseMatrix
|
|
68
|
+
def setByMatrix(self, m): self.inner.matrix = m
|
|
69
|
+
def setByInverseMatrix(self, m): self.inner.inverseMatrix = m
|
|
70
|
+
|
|
71
|
+
# The one method that matters: called LAST, once per frame. Nothing runs after this.
|
|
72
|
+
def updateCamera(self, camera):
|
|
73
|
+
self.inner.updateCamera(camera)
|
|
74
|
+
|
|
75
|
+
m = camera.viewMatrix
|
|
76
|
+
|
|
77
|
+
for effect in self.effects:
|
|
78
|
+
delta = effect()
|
|
79
|
+
if delta is not None:
|
|
80
|
+
m = m * delta # eye-space compose — see 01-core.md rule 9 for OSG's multiply order
|
|
81
|
+
|
|
82
|
+
camera.viewMatrix = m
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
trackball = osgGA.TrackballManipulator()
|
|
89
|
+
effect_manip = EffectManipulator(trackball)
|
|
90
|
+
viewer.cameraManipulator = effect_manip # installed ONCE, stays for the session
|
|
91
|
+
|
|
92
|
+
shake_effect, shake_trigger = make_shake_effect(viewer, duration=0.3, magnitude=0.1)
|
|
93
|
+
effect_manip.effects.append(shake_effect)
|
|
94
|
+
|
|
95
|
+
shake_trigger() # fire it any time — orbiting/panning underneath keeps working
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
An "effect" is any zero-arg callable returning `None` (inactive) or a matrix
|
|
99
|
+
to compose — a punch-zoom, a forced-look-at, a scripted flourish, all
|
|
100
|
+
installed once and left there for the rest of the session.
|
|
101
|
+
|
|
102
|
+
## The `home(ea, aa)` forwarding trap
|
|
103
|
+
|
|
104
|
+
`View.setCameraManipulator(resetPosition=True)` calls `manip->home(ea, *this)`
|
|
105
|
+
internally — `*this` being the live `View`/`Viewer`, passed where C++ expects
|
|
106
|
+
`GUIActionAdapter&`. Python resolves that argument to a full
|
|
107
|
+
`osgViewer.Viewer` object; forwarding it into `self.inner.home(ea, aa)`
|
|
108
|
+
(typed `GUIActionAdapter&` too) fails, because pybind11 doesn't know
|
|
109
|
+
`View`/`Viewer` "is a" `GUIActionAdapter`.
|
|
110
|
+
|
|
111
|
+
**Not fixable by declaring `GUIActionAdapter` as a base of `View`**:
|
|
112
|
+
`osgGA::GUIActionAdapter` is not `osg::Referenced`-derived (no `ref()`/
|
|
113
|
+
`unref()`), so it can never take an `osg::ref_ptr` holder to match `View`'s —
|
|
114
|
+
pybind11 requires every base in a `py::class_`'s template list to agree on
|
|
115
|
+
holder type. Fails at import time (`ImportError: generic_type: type "View"
|
|
116
|
+
has a non-default holder type while its base "osgGA::GUIActionAdapter" does
|
|
117
|
+
not`). This is a hard wall in the binding model — don't re-attempt it.
|
|
118
|
+
|
|
119
|
+
**The actual fix**: `EffectManipulator.home()` never forwards `(ea, aa)` — it
|
|
120
|
+
always calls `self.inner.home(0.0)`, the overload that doesn't need a
|
|
121
|
+
`GUIActionAdapter`.
|
|
122
|
+
|
|
123
|
+
`handle()` (mouse drag/orbit input) has the identical exposure and no
|
|
124
|
+
equivalent no-`aa` overload — `GUIEventHandler::handle()`'s real C++
|
|
125
|
+
implementation resolves a real `GUIActionAdapter&` before calling the 2-arg
|
|
126
|
+
`handle(ea, aa)` this binding exposes to Python, likely the same forwarding
|
|
127
|
+
trap. **Not yet tested live** — verify by actually dragging the mouse through
|
|
128
|
+
an installed `EffectManipulator` before trusting it. If it crashes the same
|
|
129
|
+
way, the likely fix is making `EffectManipulator` *inherit* from the real
|
|
130
|
+
manipulator class instead of holding it as `self.inner` and forwarding —
|
|
131
|
+
inherited (non-overridden) C++ methods resolve through the trampoline via
|
|
132
|
+
real virtual dispatch, no Python-argument-marshaling boundary to cross.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Fixed camera, no `osgGA.CameraManipulator` at all
|
|
2
|
+
|
|
3
|
+
Sibling to [`05-camera-manipulator.md`](05-camera-manipulator.md): that file
|
|
4
|
+
covers driving/customizing a manipulator; this one is a camera that should
|
|
5
|
+
never move (an orthographic front-on layout, a locked cinematic angle),
|
|
6
|
+
driven by directly assigning `viewer.camera.viewMatrix`/`projectionMatrix`
|
|
7
|
+
with no manipulator attached at all.
|
|
8
|
+
|
|
9
|
+
## Rule 1: `viewer.realize()` BEFORE setting view/projection, not after
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
viewer = osgViewer.Viewer()
|
|
13
|
+
viewer.sceneData = root
|
|
14
|
+
viewer.realize() # FIRST
|
|
15
|
+
|
|
16
|
+
viewer.camera.viewMatrix = osg.Matrix.lookAt(eye, center, up)
|
|
17
|
+
viewer.camera.projectionMatrix = osg.Matrix.ortho(l, r, b, t, near, far)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Setting the projection matrix before `realize()`/the first `frame()` gets its
|
|
21
|
+
horizontal extent silently zeroed on the next frame:
|
|
22
|
+
`osg::Camera::ProjectionResizePolicy` (default `HORIZONTAL`) rescales the
|
|
23
|
+
projection against a "reference" viewport size that is wrong (effectively
|
|
24
|
+
unset) if a custom projection was assigned before the window had a real size.
|
|
25
|
+
An `ortho(-3, 3, -2, 2, 0.1, 100)` matrix's `element[0]` (`0.333`) goes to
|
|
26
|
+
`0.0` after one `frame()` call; vertical extent (`element[5]`) is untouched,
|
|
27
|
+
matching `HORIZONTAL`'s asymmetry. Once corrupted, nothing from that camera
|
|
28
|
+
renders — no diagnostic, just the clear color, indistinguishable from an
|
|
29
|
+
unrelated bug.
|
|
30
|
+
|
|
31
|
+
The reorder above is the real fix. No per-frame reassertion is needed once
|
|
32
|
+
ordered correctly — a correctly-ordered fixed camera should never need its
|
|
33
|
+
matrices touched again after setup; if a "reassert every frame" band-aid
|
|
34
|
+
feels necessary, that's a signal the ordering is still wrong, not a reason to
|
|
35
|
+
ship the band-aid.
|
|
36
|
+
|
|
37
|
+
`viewer.realize()` alone is enough, no full `frame()` needed first.
|
|
38
|
+
|
|
39
|
+
## Rule 2: disable automatic near/far unless you actually want it
|
|
40
|
+
|
|
41
|
+
`osg::Camera`'s default `computeNearFarMode` is
|
|
42
|
+
`COMPUTE_NEAR_FAR_USING_BOUNDING_VOLUMES` — it recomputes near/far from the
|
|
43
|
+
cull-visible scene bounds every frame, independent of whether a manipulator
|
|
44
|
+
is attached. This is a needless per-frame cost for a camera that never moves,
|
|
45
|
+
and a sharp edge for genuinely flat (zero-depth) geometry where the
|
|
46
|
+
recomputed range can be degenerate. Fix:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
viewer.camera.computeNearFarMode = osg.Camera.DO_NOT_COMPUTE_NEAR_FAR
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Rule 3: confining the camera to part of the window is not automatically viewport-aware everywhere
|
|
53
|
+
|
|
54
|
+
`viewer.camera.viewport = osg.Viewport(x, y, w, h)` works exactly as expected
|
|
55
|
+
for geometry — MVP-transformed vertex positions map into that sub-rectangle,
|
|
56
|
+
no extra setup needed. Two things are NOT automatically viewport-aware,
|
|
57
|
+
though:
|
|
58
|
+
|
|
59
|
+
- **`gl_FragCoord` in a fragment shader is WINDOW-absolute, not
|
|
60
|
+
viewport-relative.** A shader that derives spatial patterns from
|
|
61
|
+
`gl_FragCoord`/a resolution uniform samples a shifted, wrong region once the
|
|
62
|
+
viewport's origin isn't `(0, 0)`. Don't patch this with a manual
|
|
63
|
+
origin-correction uniform — don't depend on `gl_FragCoord` at all: derive
|
|
64
|
+
the spatial domain from a vertex-shader `out` varying (object/world-space
|
|
65
|
+
position) instead, which is genuinely view-independent. See
|
|
66
|
+
`pyosg-noise.py`'s `VERTEX_SHADER`/`FRAGMENT_SHADER_NOISE` (`vPos`) for the
|
|
67
|
+
pattern.
|
|
68
|
+
- **`osgx` needs the main camera's viewport origin subtracted from
|
|
69
|
+
window-absolute mouse coordinates.** `PickReadback::setWindowOrigin(x, y)`
|
|
70
|
+
(alongside `setWindowSize(w, h)`) is refreshed every frame by
|
|
71
|
+
`PickCameraSync` from the live viewport; both the pick1x1 sub-frustum math
|
|
72
|
+
and full-image CLICK-mode pixel mapping subtract it. Not Python-bound
|
|
73
|
+
(driven automatically). If picking/hover looks offset by a consistent pixel
|
|
74
|
+
amount after confining a camera's viewport, check whether the running osgx
|
|
75
|
+
build has this fix (see the repo's `CLAUDE.md` on `PYOSG_OSGX_SOURCE_DIR`)
|
|
76
|
+
before re-debugging from scratch. If instead picking triggers UNDER an
|
|
77
|
+
ImGui panel, or hover sticks/flickers near one, that's a different bug —
|
|
78
|
+
see [`09-picking.md`](09-picking.md).
|
|
79
|
+
|
|
80
|
+
## What a manipulator gives you that a fixed camera does not
|
|
81
|
+
|
|
82
|
+
Just initial framing, nothing else. `viewer.cameraManipulator = ...` after
|
|
83
|
+
`sceneData` is set auto-computes a reasonable home position from the scene
|
|
84
|
+
bounds (see [`05-camera-manipulator.md`](05-camera-manipulator.md)'s "There
|
|
85
|
+
is no `viewer.home()`"); a fixed camera has no equivalent, the view is
|
|
86
|
+
hand-computed. Rules 1–2 apply identically whether or not a manipulator is
|
|
87
|
+
attached; a manipulator doesn't make viewport-offset picking correct either —
|
|
88
|
+
that fix lives in `osgx` regardless of what drives the camera.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Composing osgx::gltf materials with generic osgx lighting
|
|
2
|
+
|
|
3
|
+
`osgx::gltf` (formerly a separate `osgGLTF` module/repo, merged into `osgx.gltf` — there is
|
|
4
|
+
no `osgGLTF` Python module) is the glTF loader plus its optional PBR/IBL adapter. The ownership
|
|
5
|
+
boundary is:
|
|
6
|
+
|
|
7
|
+
- `osgx.gltf.shader` defines the state populated by the loader (attribute locations, sampler
|
|
8
|
+
units, `configureProgram()`/`configureStateSet()`).
|
|
9
|
+
- `osgx.gltf.pbribl` provides glTF-specific material/shading GLSL snippets and the optional
|
|
10
|
+
one-call renderer (`PBRIBLEnvironment`/`PBRIBLScene`).
|
|
11
|
+
- `osgx` and `osgx` provide generic rendering and environment-processing facilities.
|
|
12
|
+
- `osgx.resolveShaderLibs()` expands generic catalogs (`osgx::pbr`, `osgx::ibl`, `osgx::shadow`).
|
|
13
|
+
- `osgx.gltf.pbribl.resolveShaderLibs()` registers and expands the `osgx::gltf` catalog together
|
|
14
|
+
with those generic osgx catalogs, in one call.
|
|
15
|
+
|
|
16
|
+
Import `osgx` before resolving a hand-assembled glTF shader — it registers every catalog
|
|
17
|
+
(`osgx.gltf.pbribl.resolveShaderLibs()` calls the PBR/IBL/shadow/gltf registration functions
|
|
18
|
+
itself, so nothing else needs to be imported separately):
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import osgx
|
|
22
|
+
|
|
23
|
+
fragment_source = """
|
|
24
|
+
#version 460 core
|
|
25
|
+
|
|
26
|
+
const float PI = 3.14159265359;
|
|
27
|
+
|
|
28
|
+
#pragma osgx::pbr MATERIAL_STRUCT, D_GGX, G_SCHLICK, G_SMITH, F_SCHLICK, DIRECT_SPECULAR, TONEMAP_PBR_NEUTRAL
|
|
29
|
+
#pragma osgx::gltf MATERIAL_INPUTS, GET_MATERIAL, SHADING_NORMAL, EMISSIVE, ALPHA_COVERAGE
|
|
30
|
+
#pragma osgx::ibl HEMISPHERE_AMBIENT
|
|
31
|
+
|
|
32
|
+
// application-specific lighting and main()
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
fragment_shader = osgx.gltf.pbribl.resolveShaderLibs(fragment_source)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Catalog and library names are case-insensitive. Unknown entries in a registered namespace fail
|
|
39
|
+
immediately; unrelated pragmas remain intact for OSG or other tooling.
|
|
40
|
+
|
|
41
|
+
## Required program and StateSet setup
|
|
42
|
+
|
|
43
|
+
Custom renderers should use `osgx.gltf.shader`'s public contract rather than repeat attribute
|
|
44
|
+
locations or sampler units:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
program = osg.Program(shaders=(
|
|
48
|
+
osg.Shader(osg.Shader.VERTEX, vertex_source),
|
|
49
|
+
osg.Shader(osg.Shader.FRAGMENT, fragment_shader),
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
state_set = model.stateSet
|
|
53
|
+
osgx.gltf.shader.configureProgram(program)
|
|
54
|
+
osgx.gltf.shader.configureStateSet(state_set)
|
|
55
|
+
state_set.setAttributeAndModes(
|
|
56
|
+
program,
|
|
57
|
+
osg.StateAttribute.ON | osg.StateAttribute.OVERRIDE,
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`configureProgram()` binds tangent and skin attributes to the locations populated by the loader.
|
|
62
|
+
`configureStateSet()` maps base-color, normal, ORM, and emissive samplers to the loader's texture
|
|
63
|
+
units. The loader owns the material data; the application still owns its renderer and Program.
|
|
64
|
+
|
|
65
|
+
`examples/pyosg-voxelize2d.py` is a complete hand-assembled fallback example (its
|
|
66
|
+
`PBR_FALLBACK_FRAGMENT_SHADER`).
|
|
67
|
+
|
|
68
|
+
## GLSL dependency gotcha
|
|
69
|
+
|
|
70
|
+
Some generic PBR snippets reference a caller-owned `PI`. Because pragma expansion is literal and
|
|
71
|
+
ordered, declare it before the pragma:
|
|
72
|
+
|
|
73
|
+
```glsl
|
|
74
|
+
const float PI = 3.14159265359;
|
|
75
|
+
#pragma osgx::pbr D_GGX, G_SCHLICK, G_SMITH
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Direct-light gotcha
|
|
79
|
+
|
|
80
|
+
`osgx_DirectSpecular()` already includes its `NdotL` factor. Apply `NdotL` to the separate Lambert
|
|
81
|
+
diffuse term, not to the combined diffuse-plus-specular result:
|
|
82
|
+
|
|
83
|
+
```glsl
|
|
84
|
+
float NdotL = max(dot(N, L), 0.0);
|
|
85
|
+
vec3 diffuse = kD * mat.albedo / PI * NdotL;
|
|
86
|
+
vec3 specular = osgx_DirectSpecular(N, V, L, NdotV, mat.roughness, mat.F0);
|
|
87
|
+
Lo += (diffuse + specular) * lightColor[i] * attenuation;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## One-call PBR/IBL renderer
|
|
91
|
+
|
|
92
|
+
For a pre-baked environment, load its `osgx_pbribl` manifest and use osgx's optional renderer —
|
|
93
|
+
`PBRIBLEnvironment`/`PBRIBLScene` are classes with static factory methods, not free functions (see
|
|
94
|
+
[`30-pbribl.md`](30-pbribl.md) for the full API, including the shadow/skinning/tonemap `hooks`
|
|
95
|
+
options and the deferred G-buffer variant for many-light scenes):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
model = osgDB.readNodeFile("scene.gltf")
|
|
99
|
+
environment = osgx.gltf.pbribl.PBRIBLEnvironment.load("papermill.gltf")
|
|
100
|
+
scene = osgx.gltf.pbribl.PBRIBLScene.create(model, environment)
|
|
101
|
+
|
|
102
|
+
if not environment.valid() or not scene.valid():
|
|
103
|
+
raise RuntimeError("PBR/IBL setup failed")
|
|
104
|
+
|
|
105
|
+
root = osg.Group()
|
|
106
|
+
|
|
107
|
+
if environment.root is not None:
|
|
108
|
+
root.children.append(environment.root)
|
|
109
|
+
|
|
110
|
+
root.children.append(scene.node)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The environment root must participate in the rendered scene graph when the manifest uses a built-in
|
|
114
|
+
LUT. For a fully dynamic setup, use `PBRIBLEnvironment.prepare("environment.hdr")`; it bakes
|
|
115
|
+
specular, diffuse, and the BRDF LUT from that one source. The helper is IBL-only and does not
|
|
116
|
+
invent authored/direct lights. Generic light rigs remain in `osgx` (see
|
|
117
|
+
[`40-typed-lights-gizmos.md`](40-typed-lights-gizmos.md)); glTF-authored camera and
|
|
118
|
+
`KHR_lights_punctual` support are separate loader work.
|
|
119
|
+
|
|
120
|
+
Use `examples/pyosg-khronos-viewer.py` (`osgx.gltf.pbribl.PBRIBLScene.create()`'s thin viewer
|
|
121
|
+
consumer) and `/home/cubicool/tmp/khronos/CODEX.md` for authoritative Khronos parity work.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `osgx` + `osgx.imgui`: the mouse-capture race
|
|
2
|
+
|
|
3
|
+
Sibling to [`07-camera-manual.md`](07-camera-manual.md)'s Rule 3, which
|
|
4
|
+
covers a different, earlier picking fix (window-origin subtraction). This
|
|
5
|
+
file is about `osgx` coexisting with an `osgx.imgui.Widget` panel in
|
|
6
|
+
the same window: three independent guards, all in `~/dev/osgx`
|
|
7
|
+
(`Picking.cpp`/`Picking.hpp`) — needs a rebuild to take effect; check
|
|
8
|
+
`PYOSG_OSGX_SOURCE_DIR` in your build dir's `CMakeCache.txt` first (see the
|
|
9
|
+
repo's `CLAUDE.md`).
|
|
10
|
+
|
|
11
|
+
Ship all three together. Each closes a different failure mode; removing any
|
|
12
|
+
one reopens a real, reproducible bug.
|
|
13
|
+
|
|
14
|
+
## Guard 1: `PickHandler` must check `ea.getHandled()` itself
|
|
15
|
+
|
|
16
|
+
`osgViewer::Viewer::eventTraversal()` calls **every** handler in the list for
|
|
17
|
+
**every** event — it never stops early just because an earlier handler
|
|
18
|
+
returned `true`. Every stock OSG handler
|
|
19
|
+
(`StatsHandler`/`HelpHandler`/the manipulators) opens `handle()` with
|
|
20
|
+
`if (ea.getHandled()) return false;` for exactly this reason. Without the
|
|
21
|
+
same check, `PickHandler` keeps recording mouse position from events an
|
|
22
|
+
`osgx.imgui.Widget` already claimed (`Widget` registers at the front of the
|
|
23
|
+
handler list via `push_front`, so it always runs first for a given event —
|
|
24
|
+
ordering is never the problem here).
|
|
25
|
+
|
|
26
|
+
## Guard 2: a one-shot `invalidate()` isn't enough — suspend continuously
|
|
27
|
+
|
|
28
|
+
`PickCameraSync`'s continuous per-frame readback re-samples whatever
|
|
29
|
+
`mouseX()`/`mouseY()` currently is, every frame, regardless of events. Guard
|
|
30
|
+
1 stops `PickHandler` from updating that position while ImGui has capture,
|
|
31
|
+
but the frozen position is still real, valid geometry — so the next
|
|
32
|
+
continuous-readback frame re-detects it as hovered, undoing a single
|
|
33
|
+
`invalidate()` call (visible as flickering tint, not a clean off).
|
|
34
|
+
|
|
35
|
+
Fix: `PickReadback::setSuspended(bool)`/`isSuspended()`, set from
|
|
36
|
+
`PickHandler::handle()` on every dispatched event, and
|
|
37
|
+
`PickCameraSync::operator()` calls `invalidate()` every frame for as long as
|
|
38
|
+
`isSuspended()` stays true — the same pattern already used for "cursor left
|
|
39
|
+
the OS window" (`platform::isCursorInWindow()`).
|
|
40
|
+
|
|
41
|
+
## Guard 3: `WantCaptureMouse` itself lags one frame behind the event that set it
|
|
42
|
+
|
|
43
|
+
The subtlest guard, and the one that survives Guards 1+2. `osgx::imgui::Widget`'s
|
|
44
|
+
MOVE/DRAG handler does, in one call:
|
|
45
|
+
|
|
46
|
+
```cpp
|
|
47
|
+
io.AddMousePosEvent(ea.getX(), io.DisplaySize.y - ea.getY());
|
|
48
|
+
return io.WantCaptureMouse;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Dear ImGui only *recomputes* `WantCaptureMouse` inside its own `NewFrame()`
|
|
52
|
+
(run later, from `Widget`'s `PreDraw` callback), so this read reflects the
|
|
53
|
+
**previous** frame's hover result. The first MOVE event whose coordinates
|
|
54
|
+
land on an ImGui panel is still judged against the old (still-`false`)
|
|
55
|
+
capture state, so it comes through with `ea.getHandled() == false`.
|
|
56
|
+
`isSuspended()` never gets set for that event, and `PickHandler` stores that
|
|
57
|
+
real but genuinely out-of-viewport coordinate as `mouseX()`/`mouseY()`. If
|
|
58
|
+
the cursor stops moving right there, picking stays pinned to that bad sample
|
|
59
|
+
indefinitely — an inherent one-frame lag in immediate-mode GUI input, not an
|
|
60
|
+
event-ordering bug, so don't re-chase Guard 1/2 if this specific symptom
|
|
61
|
+
recurs.
|
|
62
|
+
|
|
63
|
+
Fix: a third, purely geometric guard in `PickCameraSync::operator()`, immune
|
|
64
|
+
to the lag because it depends only on this frame's own numbers:
|
|
65
|
+
|
|
66
|
+
```cpp
|
|
67
|
+
int localX = _rb->mouseX() - originX;
|
|
68
|
+
int localY = _rb->mouseY() - originY;
|
|
69
|
+
bool cursorInViewport = localX >= 0 && localX < width && localY >= 0 && localY < height;
|
|
70
|
+
|
|
71
|
+
if (!cursorInViewport) _rb->invalidate();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The pick1x1 sub-frustum block only runs when `cursorInViewport` is true;
|
|
75
|
+
otherwise the pick camera gets the plain (un-aimed) projection matrix, since
|
|
76
|
+
nothing downstream observes it.
|
|
77
|
+
|
|
78
|
+
## Why not JUST the geometric check?
|
|
79
|
+
|
|
80
|
+
It's tempting to drop Guards 1/2 and rely on Guard 3 alone — simpler, no
|
|
81
|
+
lag, no `ea.getHandled()`/event-chain reasoning. It would even fix a
|
|
82
|
+
fixed-dock ImGui layout, where the dock exactly coincides with "outside
|
|
83
|
+
`camera.viewport`". But a floating/undocked ImGui window, a popup, a
|
|
84
|
+
tooltip, or a context menu drawn ON TOP of the 3D viewport's own rectangle
|
|
85
|
+
puts the cursor geometrically *inside* the viewport while ImGui still
|
|
86
|
+
legitimately has capture — Guard 3 alone waves that through (click-fallthrough
|
|
87
|
+
again), while Guard 1/2's `ea.getHandled()`-driven `isSuspended()` still
|
|
88
|
+
correctly reflects "some handler already claimed this," regardless of screen
|
|
89
|
+
position. Keep both: the event-based guard generalizes across UI layouts, the
|
|
90
|
+
geometric guard closes the timing hole the event-based one cannot.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Render-to-texture and multi-camera scene graphs, built live
|
|
2
|
+
|
|
3
|
+
## `osg.Camera()` accepts the same kwargs constructor as `osg.Group`
|
|
4
|
+
|
|
5
|
+
`osg.Camera(children=(a, b), clearColor=..., renderOrder=..., viewport=...)`
|
|
6
|
+
works — `Camera`'s `kwargs_base` chain (`pyosg/pyosg.hpp`) runs through
|
|
7
|
+
`Transform` → `Group`, so `Group`'s `kwargs_init_own` (which handles
|
|
8
|
+
`children=`) applies to `Camera` too, on top of `Camera`'s own kwargs
|
|
9
|
+
(`viewport`, `clearColor`, `clearMask`, `projectionMatrix`, `viewMatrix`,
|
|
10
|
+
`renderOrder`, `graphicsContext`, `renderTargetImplementation`,
|
|
11
|
+
`allowEventFocus`, `computeNearFarMode`, `nearFarRatio`, draw callbacks —
|
|
12
|
+
see `pyosg/osg/Camera.cpp`). Building without kwargs is still fine:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
cam = osg.Camera()
|
|
16
|
+
cam.children.append(some_node)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## `Group.children.remove()`/`del`/`.pop()` work
|
|
20
|
+
|
|
21
|
+
`pyx::SequenceTraits<osg::Group>::del()` (`pyosg/osg/Group.hpp` →
|
|
22
|
+
`removeChild()`) backs `SequenceProxy`'s conditional deletable methods
|
|
23
|
+
(`remove`/`pop`/`del`/`clear`):
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
rtt_cam.children.remove(model) # works
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If some OTHER `SequenceProxy`-backed container raises `AttributeError` on
|
|
30
|
+
`.remove()`/`.pop()`/`del container[i]`, that type's `SequenceTraits`
|
|
31
|
+
specialization likely doesn't implement `del()` yet — don't assume the whole
|
|
32
|
+
proxy mechanism is broken, check that one type's traits file first.
|
|
33
|
+
|
|
34
|
+
## PRE_RENDER RTT cameras need `referenceFrame = osg.Transform.ABSOLUTE_RF`
|
|
35
|
+
|
|
36
|
+
If an RTT camera has its own explicit `viewMatrix`/`projectionMatrix` (not
|
|
37
|
+
meant to inherit the parent's transform — the normal case for an offscreen
|
|
38
|
+
"render this from a fixed angle" pass), set:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
rtt_cam.referenceFrame = osg.Transform.ABSOLUTE_RF
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Without this, the RTT camera's own `viewMatrix` (typically a `lookAt` matrix
|
|
45
|
+
with the eye far from the origin) gets composed into the *parent scene's
|
|
46
|
+
bound computation* — `osg::Camera` IS-A `osg::Transform`, and by default
|
|
47
|
+
(`RELATIVE_RF`) a Transform's matrix is applied to its children's bounds when
|
|
48
|
+
computing the parent's overall bound, exactly like `osg::MatrixTransform`.
|
|
49
|
+
This silently produces a wildly wrong combined scene bound, and
|
|
50
|
+
`TrackballManipulator`'s "home" framing ends up looking at nothing — the
|
|
51
|
+
symptom is a screenshot/window that's just solid black or shows nothing
|
|
52
|
+
recognizable, with nothing else actually broken. Confirm by checking
|
|
53
|
+
`root.bound` before/after setting `ABSOLUTE_RF`: it should snap to a sane
|
|
54
|
+
bound matching your *visible* geometry, not something enormous or offset.
|
|
55
|
+
|
|
56
|
+
Any RTT/shadow-style camera with its own independent view should be
|
|
57
|
+
`ABSOLUTE_RF` (matches `osgx::shadow::ShadowMap::create()`, `~/dev/osgx/src/Shadow.cpp`,
|
|
58
|
+
which sets it on its own `camera` — the Lighting Series' `08-shadows.py` builds
|
|
59
|
+
its shadow pass via `osgx.ShadowMap.create()` rather than a hand-rolled
|
|
60
|
+
`osg.Camera()`).
|
|
61
|
+
|
|
62
|
+
## A fullscreen-quad pass re-targeted to an FBO renders a single flat color
|
|
63
|
+
|
|
64
|
+
A fullscreen-quad camera (`ABSOLUTE_RF`, identity view/projection: composite,
|
|
65
|
+
SSAO, bloom, deferred lighting) must own its own depth state:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
cam.stateSet.modes[GL_DEPTH_TEST] = osg.StateAttribute.OFF
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Without it, such a pass works only by accident while it draws to the
|
|
72
|
+
backbuffer — the main camera clears depth to 1.0 every frame, so the quad
|
|
73
|
+
happens to pass `GL_LESS`. Re-target that same camera to an FBO
|
|
74
|
+
(`renderOrder = PRE_RENDER` + `attach()`) and OSG attaches an implicit depth
|
|
75
|
+
renderbuffer that a color-only `clearMask` never clears. Undefined depth,
|
|
76
|
+
every fragment discarded, and the attachment keeps only its clear color.
|
|
77
|
+
|
|
78
|
+
The failure is silent: the FBO is valid, the viewport is right, the draw
|
|
79
|
+
call is issued, no GL errors — reads exactly like a broken shader.
|
|
80
|
+
|
|
81
|
+
> "It renders fine today" is not evidence a pass owns its depth state — only
|
|
82
|
+
> that something else cleared depth for it. Every backbuffer-only fullscreen
|
|
83
|
+
> pass is a latent instance of this bug; re-targeting is what exposes it.
|
|
84
|
+
|
|
85
|
+
### Diagnosing a pass that outputs one flat color
|
|
86
|
+
|
|
87
|
+
1. **Dump the texture and count distinct colors** rather than judging by
|
|
88
|
+
eye — a "flat gray" that's exactly the *master camera's* clear color
|
|
89
|
+
repeated proves you're looking at another framebuffer's contents:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from PIL import Image
|
|
93
|
+
from collections import Counter
|
|
94
|
+
px = list(Image.open("dump.png").convert("RGB").getdata())
|
|
95
|
+
print(len(set(px)), Counter(px).most_common(5))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
2. **Set a deliberately absurd clear color** (magenta) on the suspect camera
|
|
99
|
+
— separates "FBO live but nothing drew," "never rendered," and "shaded to
|
|
100
|
+
black," which are indistinguishable at the default black.
|
|
101
|
+
|
|
102
|
+
3. **Probe from a draw callback on the pass's own geometry** — proves
|
|
103
|
+
whether the draw happens at all, and whether an FBO is really bound:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# GL_DRAW_FRAMEBUFFER_BINDING == 0x8CA6; 0 means the DEFAULT framebuffer
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
4. **Add a temporary flag/toggle to a known-good example** instead of
|
|
110
|
+
writing a fresh repro, so the A/B differs by exactly one variable in one
|
|
111
|
+
binary.
|
|
112
|
+
|
|
113
|
+
### A debug blit must not disable the camera that writes the texture
|
|
114
|
+
|
|
115
|
+
A "visualize mode" that `nodeMask`s off a pass and then blits that pass's own
|
|
116
|
+
RTT output samples an attachment nothing rendered into this frame —
|
|
117
|
+
undefined, spatially uniform, unaffected by camera movement. That looks
|
|
118
|
+
exactly like a broken render pass and is purely a broken instrument. Toggle
|
|
119
|
+
only cameras that draw to the **backbuffer**; leave every `PRE_RENDER`/FBO
|
|
120
|
+
stage running in all modes.
|
|
121
|
+
|
|
122
|
+
## Z-up convention: a quad grid built in the XY plane is invisible
|
|
123
|
+
|
|
124
|
+
This project's examples default to Z-up (`up = (0, 0, 1)` in `lookAt` calls,
|
|
125
|
+
matching `TrackballManipulator`'s default framing along `-Y`). A
|
|
126
|
+
fullscreen/instanced quad grid whose vertex shader writes `vec4(x, y, z, 1.0)`
|
|
127
|
+
with the 2D grid spread across `x`/`y` and depth along `z` lies in the **XY
|
|
128
|
+
plane** — edge-on to the default camera view, reading as a thin,
|
|
129
|
+
nearly-invisible line.
|
|
130
|
+
|
|
131
|
+
Build the grid in **X/Z**, with the explode-depth axis on **Y**:
|
|
132
|
+
|
|
133
|
+
```glsl
|
|
134
|
+
// WRONG for this project's Z-up default framing:
|
|
135
|
+
gl_Position = osg_ModelViewProjectionMatrix * vec4(gridX, gridY, depth, 1.0);
|
|
136
|
+
|
|
137
|
+
// RIGHT:
|
|
138
|
+
gl_Position = osg_ModelViewProjectionMatrix * vec4(gridX, -depth, gridZ, 1.0);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Verified-working RTT setup shape (`pyosg-rtt.py`)
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
cb = osg.Texture2D()
|
|
145
|
+
cb.size = (w, h)
|
|
146
|
+
cb.internalFormat = GL_RGBA
|
|
147
|
+
cb.filter = (osg.Texture.LINEAR, osg.Texture.LINEAR)
|
|
148
|
+
|
|
149
|
+
db = osg.Texture2D()
|
|
150
|
+
db.size = (w, h)
|
|
151
|
+
db.internalFormat = GL_DEPTH_COMPONENT24
|
|
152
|
+
db.sourceFormat = GL_DEPTH_COMPONENT
|
|
153
|
+
db.sourceType = GL_FLOAT
|
|
154
|
+
db.filter = (osg.Texture.NEAREST, osg.Texture.NEAREST)
|
|
155
|
+
|
|
156
|
+
cam = osg.Camera()
|
|
157
|
+
cam.renderOrder = osg.Camera.PRE_RENDER
|
|
158
|
+
cam.renderTargetImplementation = osg.Camera.FRAME_BUFFER_OBJECT
|
|
159
|
+
cam.clearMask = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT
|
|
160
|
+
cam.clearColor = osg.Vec4(0.1, 0.1, 0.1, 1.0)
|
|
161
|
+
cam.viewport = osg.Viewport(0, 0, w, h)
|
|
162
|
+
cam.attach(osg.Camera.COLOR_BUFFER, cb)
|
|
163
|
+
cam.attach(osg.Camera.DEPTH_BUFFER, db)
|
|
164
|
+
# If this camera has its own explicit view — see ABSOLUTE_RF note above:
|
|
165
|
+
cam.referenceFrame = osg.Transform.ABSOLUTE_RF
|
|
166
|
+
cam.viewMatrix = osg.Matrix.lookAt(eye, center, osg.Vec3(0, 0, 1))
|
|
167
|
+
cam.projectionMatrix = osg.Matrix.perspective(40.0, aspect, near, far)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A downstream drawable samples `cb` as an ordinary texture:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
stateSet.textureAttributes[0] = cb
|
|
174
|
+
stateSet.uniforms["someTexUniform"] = 0 # texture UNIT index, not the texture object
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Reparenting a node without disturbing its own transform
|
|
178
|
+
|
|
179
|
+
If a loaded model (e.g. from glTF) is already an `osg.MatrixTransform`
|
|
180
|
+
holding a real, load-bearing matrix (commonly a Y-up-to-Z-up axis
|
|
181
|
+
conversion), do not overwrite `.matrix` directly to animate it — that
|
|
182
|
+
destroys the conversion. Check first:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
print(model.matrix) # if this isn't identity, it's doing real work
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Wrap it in a new transform dedicated to the animation instead:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
spin_xform = osg.MatrixTransform()
|
|
192
|
+
spin_xform.children.append(model)
|
|
193
|
+
# parent.children.append(spin_xform), not model directly
|
|
194
|
+
spin_xform.updateCallback = lambda node, nv, osg=osg: setattr(
|
|
195
|
+
node, "matrix", osg.Matrix.rotate(nv.frameStamp.simulationTime * 0.4, osg.Vec3(0, 0, 1))
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
(See [`01-core.md`](01-core.md) rule 2 for why `osg=osg` is required here.)
|