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.
Files changed (90) hide show
  1. OpenSceneGraph/OpenThreads.lib +0 -0
  2. OpenSceneGraph/_OpenSceneGraph.cp313-win_amd64.pyd +0 -0
  3. OpenSceneGraph/__init__.py +52 -0
  4. OpenSceneGraph/aipython/00-index.md +44 -0
  5. OpenSceneGraph/aipython/01-core.md +255 -0
  6. OpenSceneGraph/aipython/02-inspect.md +81 -0
  7. OpenSceneGraph/aipython/03-headless-frames.md +149 -0
  8. OpenSceneGraph/aipython/05-camera-manipulator.md +68 -0
  9. OpenSceneGraph/aipython/06-camera-effects.md +132 -0
  10. OpenSceneGraph/aipython/07-camera-manual.md +88 -0
  11. OpenSceneGraph/aipython/08-lighting.md +121 -0
  12. OpenSceneGraph/aipython/09-picking.md +90 -0
  13. OpenSceneGraph/aipython/10-rtt.md +199 -0
  14. OpenSceneGraph/aipython/11-mrt.md +104 -0
  15. OpenSceneGraph/aipython/12-gbuffer.md +48 -0
  16. OpenSceneGraph/aipython/15-shader-hotswap.md +72 -0
  17. OpenSceneGraph/aipython/17-particles.md +189 -0
  18. OpenSceneGraph/aipython/18-deterministic-captures.md +157 -0
  19. OpenSceneGraph/aipython/20-object-lifetime.md +77 -0
  20. OpenSceneGraph/aipython/25-async-osgpy.md +228 -0
  21. OpenSceneGraph/aipython/29-material.md +109 -0
  22. OpenSceneGraph/aipython/30-pbribl.md +162 -0
  23. OpenSceneGraph/aipython/40-typed-lights-gizmos.md +218 -0
  24. OpenSceneGraph/examples/__init__.py +24 -0
  25. OpenSceneGraph/examples/__main__.py +143 -0
  26. OpenSceneGraph/examples/blur.py +370 -0
  27. OpenSceneGraph/examples/info.py +126 -0
  28. OpenSceneGraph/examples/mrt.py +493 -0
  29. OpenSceneGraph/examples/pyosg_async.py +467 -0
  30. OpenSceneGraph/examples/pyosg_example.py +120 -0
  31. OpenSceneGraph/examples/pyosg_repl.py +842 -0
  32. OpenSceneGraph/examples/pyosg_visitor.py +138 -0
  33. OpenSceneGraph/ktx.dll +0 -0
  34. OpenSceneGraph/ktx.lib +0 -0
  35. OpenSceneGraph/osg.lib +0 -0
  36. OpenSceneGraph/osg161-osg.dll +0 -0
  37. OpenSceneGraph/osg161-osgAnimation.dll +0 -0
  38. OpenSceneGraph/osg161-osgDB.dll +0 -0
  39. OpenSceneGraph/osg161-osgFX.dll +0 -0
  40. OpenSceneGraph/osg161-osgGA.dll +0 -0
  41. OpenSceneGraph/osg161-osgText.dll +0 -0
  42. OpenSceneGraph/osg161-osgUtil.dll +0 -0
  43. OpenSceneGraph/osg161-osgViewer.dll +0 -0
  44. OpenSceneGraph/osg161-osgWidget.dll +0 -0
  45. OpenSceneGraph/osgAnimation.lib +0 -0
  46. OpenSceneGraph/osgDB.lib +0 -0
  47. OpenSceneGraph/osgFX.lib +0 -0
  48. OpenSceneGraph/osgGA.lib +0 -0
  49. OpenSceneGraph/osgPlugins-3.6.5/jpeg62-ebb2f26be87097e77bafd1e9095b4820.dll +0 -0
  50. OpenSceneGraph/osgPlugins-3.6.5/ktx.dll +0 -0
  51. OpenSceneGraph/osgPlugins-3.6.5/liblzma-b9cff3753c4848b9ff1350ba5a053c6e.dll +0 -0
  52. OpenSceneGraph/osgPlugins-3.6.5/libpng16-36b7e1e8d185cb21280e5688f99660c0.dll +0 -0
  53. OpenSceneGraph/osgPlugins-3.6.5/msvcp140.dll +0 -0
  54. OpenSceneGraph/osgPlugins-3.6.5/osgdb_bmp.dll +0 -0
  55. OpenSceneGraph/osgPlugins-3.6.5/osgdb_dds.dll +0 -0
  56. OpenSceneGraph/osgPlugins-3.6.5/osgdb_gltf.dll +0 -0
  57. OpenSceneGraph/osgPlugins-3.6.5/osgdb_hdr.dll +0 -0
  58. OpenSceneGraph/osgPlugins-3.6.5/osgdb_jpeg.dll +0 -0
  59. OpenSceneGraph/osgPlugins-3.6.5/osgdb_ktx2.dll +0 -0
  60. OpenSceneGraph/osgPlugins-3.6.5/osgdb_obj.dll +0 -0
  61. OpenSceneGraph/osgPlugins-3.6.5/osgdb_osg.dll +0 -0
  62. OpenSceneGraph/osgPlugins-3.6.5/osgdb_png.dll +0 -0
  63. OpenSceneGraph/osgPlugins-3.6.5/osgdb_pnm.dll +0 -0
  64. OpenSceneGraph/osgPlugins-3.6.5/osgdb_rgb.dll +0 -0
  65. OpenSceneGraph/osgPlugins-3.6.5/osgdb_serializers_osg.dll +0 -0
  66. OpenSceneGraph/osgPlugins-3.6.5/osgdb_stl.dll +0 -0
  67. OpenSceneGraph/osgPlugins-3.6.5/osgdb_tga.dll +0 -0
  68. OpenSceneGraph/osgPlugins-3.6.5/osgdb_tiff.dll +0 -0
  69. OpenSceneGraph/osgPlugins-3.6.5/tiff-1defa8059e5ba115a3ab4b120de6f4bb.dll +0 -0
  70. OpenSceneGraph/osgPlugins-3.6.5/z.dll +0 -0
  71. OpenSceneGraph/osgText.lib +0 -0
  72. OpenSceneGraph/osgUtil.lib +0 -0
  73. OpenSceneGraph/osgViewer.lib +0 -0
  74. OpenSceneGraph/osgWidget.lib +0 -0
  75. OpenSceneGraph/osgx.cp313-win_amd64.pyd +0 -0
  76. OpenSceneGraph/osgx_static.lib +0 -0
  77. OpenSceneGraph/ot21-OpenThreads.dll +0 -0
  78. openscenegraph-0.1.2.dist-info/DELVEWHEEL +2 -0
  79. openscenegraph-0.1.2.dist-info/METADATA +563 -0
  80. openscenegraph-0.1.2.dist-info/RECORD +90 -0
  81. openscenegraph-0.1.2.dist-info/WHEEL +5 -0
  82. openscenegraph-0.1.2.dist-info/entry_points.txt +6 -0
  83. openscenegraph-0.1.2.dist-info/licenses/LICENSE +21 -0
  84. openscenegraph.libs/jpeg62-ebb2f26be87097e77bafd1e9095b4820.dll +0 -0
  85. openscenegraph.libs/liblzma-b9cff3753c4848b9ff1350ba5a053c6e.dll +0 -0
  86. openscenegraph.libs/libpng16-36b7e1e8d185cb21280e5688f99660c0.dll +0 -0
  87. openscenegraph.libs/msvcp140.dll +0 -0
  88. openscenegraph.libs/tiff-1defa8059e5ba115a3ab4b120de6f4bb.dll +0 -0
  89. openscenegraph.libs/z.dll +0 -0
  90. 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.)