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,104 @@
|
|
|
1
|
+
# Multiple render targets, depth, and pass timing
|
|
2
|
+
|
|
3
|
+
`pyosg-mrt.py` proves one geometry pass writing multiple color attachments;
|
|
4
|
+
`pyosg-guided-blur.py` builds on that with normal/depth-guided post-processing.
|
|
5
|
+
Read this before treating a G-buffer depth texture as a simple linear distance
|
|
6
|
+
field.
|
|
7
|
+
|
|
8
|
+
## Raw depth is correct, but it is not linear distance
|
|
9
|
+
|
|
10
|
+
Attach depth normally and let the geometry pass write it through the ordinary
|
|
11
|
+
depth test:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
cam.attach(osg.Camera.COLOR_BUFFER0, paint_tex)
|
|
15
|
+
cam.attach(osg.Camera.COLOR_BUFFER1, normal_tex)
|
|
16
|
+
cam.attach(osg.Camera.DEPTH_BUFFER, depth_tex)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The sampled `depth_tex` value is post-projection depth. It is excellent for
|
|
20
|
+
visibility and has most precision near the camera, but comparing two samples
|
|
21
|
+
as though `abs(a - b)` were a world-space distance gives depth-dependent
|
|
22
|
+
results. A post-process needs the near/far parameters from the *same effective
|
|
23
|
+
projection* that wrote the texture:
|
|
24
|
+
|
|
25
|
+
```glsl
|
|
26
|
+
float linearizeDepth(float d, float znear, float zfar) {
|
|
27
|
+
float z = d * 2.0 - 1.0;
|
|
28
|
+
return (2.0 * znear * zfar) / (zfar + znear - z * (zfar - znear));
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The raw-depth display may be nearly white; that is normal perspective-depth
|
|
33
|
+
distribution, not proof the attachment is empty. Always expose both a raw
|
|
34
|
+
depth debug view and a linear-depth debug view while bringing up a pass.
|
|
35
|
+
|
|
36
|
+
## OSG's nominal camera projection is not necessarily the depth projection
|
|
37
|
+
|
|
38
|
+
`viewer.camera.projectionMatrix` commonly remains the nominal lens (for
|
|
39
|
+
example `0.1 - 1000`), while cull traversal tightens the actual projection
|
|
40
|
+
used for a camera pass to improve depth precision. Do not use the nominal
|
|
41
|
+
matrix to linearize an RTT depth texture when the pass uses OSG's automatic
|
|
42
|
+
near/far computation.
|
|
43
|
+
|
|
44
|
+
The reliable observation point is the depth-writing camera's **post-draw**
|
|
45
|
+
callback: its `RenderInfo::State` still holds the cull-adjusted projection,
|
|
46
|
+
and later render-order passes can consume uniforms updated there. Pre-draw is
|
|
47
|
+
too early; its state can still be the preceding pass's projection.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
def update_depth_parameters(ri):
|
|
51
|
+
_fovy, _aspect, near, far = ri.state.projectionMatrix.getPerspective()
|
|
52
|
+
composite.stateSet.uniforms["znear"] = float(near)
|
|
53
|
+
composite.stateSet.uniforms["zfar"] = float(far)
|
|
54
|
+
|
|
55
|
+
gbuffer_cam.postDrawCallback = update_depth_parameters
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For a later guided blur, use a relative difference so the control has a stable
|
|
59
|
+
meaning across distance:
|
|
60
|
+
|
|
61
|
+
```glsl
|
|
62
|
+
float delta = abs(zCenter - zSample) / max(zCenter, 0.001);
|
|
63
|
+
float depthWeight = exp(-delta * depthRejection);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use the same continuous shape for normal rejection. `pow(dot(N0, N1),
|
|
67
|
+
strength)` has a misleading zero special case: any positive strength rejects a
|
|
68
|
+
perpendicular sample completely, making `0` look like a binary mode switch.
|
|
69
|
+
This bilateral form makes strength zero genuinely disable the guide and raises
|
|
70
|
+
rejection smoothly:
|
|
71
|
+
|
|
72
|
+
```glsl
|
|
73
|
+
float normalDifference = 1.0 - clamp(dot(centerNormal, sampleNormal), 0.0, 1.0);
|
|
74
|
+
float normalWeight = exp(-normalDifference * normalRejection);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Draw callback slots have one owner
|
|
78
|
+
|
|
79
|
+
`osg::Camera` has one pre-draw and one post-draw callback slot. Assigning
|
|
80
|
+
`camera.preDrawCallback = other` replaces the existing callback; it does not
|
|
81
|
+
append or compose it. `osgx.imgui.Widget` installs its own `PreDraw` and
|
|
82
|
+
`PostDraw` callbacks on its configured draw camera during initialization, so
|
|
83
|
+
installing another callback on that same camera before constructing the Widget
|
|
84
|
+
silently loses it (and releases the replaced callback immediately).
|
|
85
|
+
|
|
86
|
+
Use `osgx.CallbackGroup` when multiple systems must share a draw slot, or put
|
|
87
|
+
a pass-specific callback on the pass camera instead. In the guided-blur
|
|
88
|
+
example, the G-buffer camera's post-draw callback is the better location: it
|
|
89
|
+
cannot collide with the Widget's composite-camera callbacks and it sees the
|
|
90
|
+
right projection at the right time.
|
|
91
|
+
|
|
92
|
+
## G-buffer debugging order
|
|
93
|
+
|
|
94
|
+
When a depth-aware pass looks wrong, establish these facts in order:
|
|
95
|
+
|
|
96
|
+
1. The raw depth attachment changes with the scene (`depthTex` debug view).
|
|
97
|
+
2. The live `znear`/`zfar` values are not constructor defaults.
|
|
98
|
+
3. Linearized depth has visible structure before tuning any rejection slider.
|
|
99
|
+
4. With normal rejection at zero, depth rejection alone changes overlap/layer
|
|
100
|
+
behavior; then test normal rejection independently.
|
|
101
|
+
|
|
102
|
+
Do not node-mask the G-buffer camera to inspect a later pass: its texture then
|
|
103
|
+
contains no current-frame data. Keep every producer pass running and switch
|
|
104
|
+
only the final backbuffer composite's display mode.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# G-buffer contracts and deferred composition
|
|
2
|
+
|
|
3
|
+
A G-buffer is a geometry pass that stores reusable per-visible-pixel data for
|
|
4
|
+
later passes. It is not inherently PBR, IBL, glTF, or lighting: normal/depth
|
|
5
|
+
guided blur, outlines, SSAO, watercolor diffusion, and deferred lighting can
|
|
6
|
+
all consume the same kind of attachments.
|
|
7
|
+
|
|
8
|
+
## Keep stored data canonical; derive interpretations in consumers
|
|
9
|
+
|
|
10
|
+
Store the native depth attachment as raw depth. A consumer that needs linear
|
|
11
|
+
camera distance should request the camera depth parameters and linearize it;
|
|
12
|
+
one needing a full view-space position can reconstruct it from depth plus an
|
|
13
|
+
inverse projection, or use a deliberately stored position attachment. This
|
|
14
|
+
keeps `osgx::GBuffer` neutral and lets each pass choose the representation its
|
|
15
|
+
algorithm actually needs.
|
|
16
|
+
|
|
17
|
+
Likewise, normals are a geometric guide rather than an outline instruction.
|
|
18
|
+
A guided blur can compare `dot(centerNormal, sampleNormal)` to preserve a
|
|
19
|
+
crease, while an NPR edge pass can turn that same difference into an ink line.
|
|
20
|
+
|
|
21
|
+
## Minimal useful layout
|
|
22
|
+
|
|
23
|
+
The smallest general deferred/post-process layout is often:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
COLOR_BUFFER0 paint/albedo/working color
|
|
27
|
+
COLOR_BUFFER1 view-space normal
|
|
28
|
+
DEPTH_BUFFER native visibility depth
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add attachments only for a demonstrated consumer: material factors for
|
|
32
|
+
deferred PBR, emissive data for lighting, position for algorithms where
|
|
33
|
+
reconstruction is inconvenient or too imprecise, IDs for picking, and so on.
|
|
34
|
+
`pyosg-mrt.py` proves simultaneous color writes; `pyosg-guided-blur.py` proves
|
|
35
|
+
the first downstream normal/depth-aware pass.
|
|
36
|
+
|
|
37
|
+
## Deferred pipeline shape
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
geometry -> G-buffer attachments
|
|
41
|
+
G-buffer -> one or more RTT post-process passes
|
|
42
|
+
all results -> final POST_RENDER composite/debug display
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every RTT producer must remain enabled while a later pass samples it. Debug
|
|
46
|
+
by changing the final composite output, not by disabling the producer whose
|
|
47
|
+
texture you are trying to inspect. For depth timing and callback ownership,
|
|
48
|
+
read [`11-mrt.md`](11-mrt.md).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Live GLSL hot-swapping for shader-side debugging
|
|
2
|
+
|
|
3
|
+
For debugging shader-side logic in a live REPL session, patch the actual GLSL
|
|
4
|
+
source and hot-swap a new `osg.Program` onto the live node's `StateSet` —
|
|
5
|
+
don't reimplement the shader's math in Python/NumPy and compare numbers.
|
|
6
|
+
Matrix-convention mistakes (row vs. column vector, upload transpose) are easy
|
|
7
|
+
to get subtly wrong reimplementing shader math by hand; a live edit that
|
|
8
|
+
visualizes the GPU's *actual* computation (e.g. color-coding which branch of
|
|
9
|
+
an `if` a fragment took) sidesteps that ambiguity.
|
|
10
|
+
|
|
11
|
+
Find the `Program`'s owning node — often a child `Geode`, not the `Camera`
|
|
12
|
+
itself (the shader is typically attached to the fullscreen-quad `Geode`, not
|
|
13
|
+
the camera's own `StateSet`). Then, live in the REPL:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
src = COMPOSITE_FRAGMENT_SHADER # the original global string
|
|
17
|
+
assert src.count(marker) == 1 # a non-unique match silently patches the wrong occurrence
|
|
18
|
+
src2 = src.replace(marker, patched_substring, 1)
|
|
19
|
+
p = osg.Program(shaders=(
|
|
20
|
+
osg.Shader(osg.Shader.VERTEX, FULLSCREEN_VERTEX),
|
|
21
|
+
osg.Shader(osg.Shader.FRAGMENT, src2)
|
|
22
|
+
))
|
|
23
|
+
node.stateSet.setAttributeAndModes(p, osg.StateAttribute.ON | osg.StateAttribute.OVERRIDE)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
No restart needed, no loss of current camera position/state.
|
|
27
|
+
|
|
28
|
+
To prove the old `Program`/`Shader`s actually got destroyed (not just
|
|
29
|
+
detached), add `debug=True` to each constructor call — see
|
|
30
|
+
[`20-object-lifetime.md`](20-object-lifetime.md). Requires the type to be
|
|
31
|
+
wired into `kwargs_init`; check the manifest if `debug=` raises a constructor
|
|
32
|
+
mismatch instead of trusting the swap-and-drop-the-old-ref pattern on faith.
|
|
33
|
+
|
|
34
|
+
## The deeper trap: a live variable reassignment silently not reaching the running callback
|
|
35
|
+
|
|
36
|
+
Two layers, both real:
|
|
37
|
+
|
|
38
|
+
**Layer 1 — wrong variable.** A per-frame callback may read a
|
|
39
|
+
closure-captured local from setup time (e.g. `light_proj`, captured when the
|
|
40
|
+
callback was defined) rather than the live object property it looks like it
|
|
41
|
+
should read (e.g. `shadow_cam.projectionMatrix`, which may have since
|
|
42
|
+
changed). Reassigning the object property silently no-ops if the callback
|
|
43
|
+
never reads it. Grep the actual callback body for what it reads — don't
|
|
44
|
+
assume based on the object's name alone.
|
|
45
|
+
|
|
46
|
+
**Layer 2 — even the right variable name can fail.** Reassigning the correct
|
|
47
|
+
bare name (`light_proj = new_value`) at the prompt — even via
|
|
48
|
+
`exec(open(path).read())` — can silently fail to reach the namespace a
|
|
49
|
+
running callback actually reads from, **despite `globals() is
|
|
50
|
+
callback.__globals__` printing `True`**. Verify via
|
|
51
|
+
`callback.__globals__['light_proj']` showing a different `id()` than a bare
|
|
52
|
+
`light_proj` lookup in the same command before trusting a "fix had zero
|
|
53
|
+
effect" conclusion.
|
|
54
|
+
|
|
55
|
+
The reliable pattern — write through the callback's own `__globals__`
|
|
56
|
+
explicitly:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
cb = shadow_cam.preDrawCallback
|
|
60
|
+
g = cb.__globals__ # the namespace the callback ACTUALLY reads from
|
|
61
|
+
g['light_proj'] = new_value
|
|
62
|
+
g['shadow_cam'].projectionMatrix = new_value # keep any live object property in sync too
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then re-verify by rebuilding the dependent live GPU uniform from `g[...]`
|
|
66
|
+
and diffing it against the actual uniform value before drawing any
|
|
67
|
+
conclusion from the visual result.
|
|
68
|
+
|
|
69
|
+
See [`01-core.md`](01-core.md) rule 2 for the related but distinct
|
|
70
|
+
free-variable `NameError` trap — that one is about a name never resolving at
|
|
71
|
+
all; this one is about a name resolving to a *stale* value in a namespace
|
|
72
|
+
that looks, but isn't, the same one.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Building GPU-only, one-shot particle/burst effects live
|
|
2
|
+
|
|
3
|
+
Patterns for instanced GPU effects (fire, explosions, shockwaves — a swarm of
|
|
4
|
+
quads driven by a formula) built live via REPL. First built out in
|
|
5
|
+
`examples/pyosg-fire.py`; design/TODO for that specific effect lives in
|
|
6
|
+
`ai/context-todo-particles.md`, not here — this file is the reusable
|
|
7
|
+
technique.
|
|
8
|
+
|
|
9
|
+
## 1. Per-instance "seed" data: hash `gl_InstanceID`, don't reach for an SSBO
|
|
10
|
+
|
|
11
|
+
If per-instance data (emission direction, size variance, phase) is
|
|
12
|
+
read-once and never changes after upload, derive it from `gl_InstanceID`
|
|
13
|
+
with an in-shader hash instead of an SSBO + CPU-side random buffer:
|
|
14
|
+
|
|
15
|
+
```glsl
|
|
16
|
+
float hash11(float p) {
|
|
17
|
+
p = fract(p * 0.1031);
|
|
18
|
+
p *= p + 33.33;
|
|
19
|
+
p *= p + p;
|
|
20
|
+
return fract(p);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
vec4 hash14(float p) {
|
|
24
|
+
return vec4(hash11(p+0.13), hash11(p+7.71), hash11(p+23.9), hash11(p+91.7));
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
No buffer object, no numpy RNG array, no upload step. An SSBO earns its keep
|
|
29
|
+
only once something actually *writes* per-instance state over time (a
|
|
30
|
+
compute-shader velocity sim, or multiple independently-triggered effects each
|
|
31
|
+
needing their own origin) — not just to read constants once.
|
|
32
|
+
|
|
33
|
+
## 2. One-shot triggering: a plain `triggerTime` uniform, not a looping `fract()`
|
|
34
|
+
|
|
35
|
+
For an effect that fires once, drive it from `osg_SimulationTime`
|
|
36
|
+
(auto-provided every frame) and a single `triggerTime` uniform, defaulted far
|
|
37
|
+
in the past so it's invisible until triggered:
|
|
38
|
+
|
|
39
|
+
```glsl
|
|
40
|
+
uniform float osg_SimulationTime;
|
|
41
|
+
uniform float triggerTime = -1000.0;
|
|
42
|
+
uniform float duration = 1.2;
|
|
43
|
+
|
|
44
|
+
float t = (osg_SimulationTime - triggerTime) / duration; // NOT fract()'d
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
An envelope like `smoothstep(0,0.08,t) * (1-smoothstep(0.5,1.0,t))` doubles
|
|
48
|
+
as both the grow/shrink curve and the "invisible before t=0 and after t=1"
|
|
49
|
+
gate — no separate visibility toggle needed.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
def trigger(node, viewer):
|
|
53
|
+
node.stateSet.uniforms["triggerTime"] = float(viewer.frameStamp.simulationTime)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Bind number keys to different nodes/presets (`osgGA.GUIEventHandler`,
|
|
57
|
+
`ea.type == osgGA.GUIEventAdapter.KEYDOWN`, `ea.key == ord("1")`) for
|
|
58
|
+
instant hands-on comparison of variants.
|
|
59
|
+
|
|
60
|
+
## 3. Additive-blend saturation dominates the read of the effect — tune it first
|
|
61
|
+
|
|
62
|
+
With a few hundred instances overlapping in a small screen area under
|
|
63
|
+
`BlendFunc(GL_ONE, GL_ONE)`, even modest per-quad brightness sums to solid
|
|
64
|
+
white fast:
|
|
65
|
+
|
|
66
|
+
- Weight any secondary color-driving term (e.g. a height-based bias) low
|
|
67
|
+
relative to the primary noise term — `n * 1.0 + heightBias * 0.15`, not
|
|
68
|
+
`heightBias * 0.5`. Too high washes out noise detail into a flat blob.
|
|
69
|
+
- Multiply final alpha by a damping factor (~0.5–0.6) so one quad's
|
|
70
|
+
contribution stays below full brightness, leaving room for overlaps to sum
|
|
71
|
+
into mid-ramp colors instead of instantly clipping white.
|
|
72
|
+
|
|
73
|
+
## 4. A Program hot-swap does NOT reset uniforms already bound on the StateSet
|
|
74
|
+
|
|
75
|
+
Extends [`15-shader-hotswap.md`](15-shader-hotswap.md). Swapping just the
|
|
76
|
+
`Program` (`setAttributeAndModes(newProgram, ON|OVERRIDE)`) leaves any
|
|
77
|
+
uniform already explicitly set from Python (`ss.uniforms["duration"] = 2.5`)
|
|
78
|
+
bound on the StateSet — it silently overrides the new shader's own
|
|
79
|
+
`uniform float duration = 1.2;` GLSL default. If a hot-swap changes intended
|
|
80
|
+
defaults, explicitly re-set every uniform that changed.
|
|
81
|
+
|
|
82
|
+
Reading a uniform's current value back out is `.value`, not `.getFloat()` or
|
|
83
|
+
index `[0]`: `fire.stateSet.uniforms["duration"].value`.
|
|
84
|
+
|
|
85
|
+
## 5. Verifying a fast one-shot effect: force a known `t`, don't chase real-time screenshots
|
|
86
|
+
|
|
87
|
+
A `duration=1.2s` burst is easy to miss with `capture_framebuffer()` (see
|
|
88
|
+
[`01-core.md`](01-core.md) rule 6). Set `triggerTime` to a known offset in
|
|
89
|
+
the past instead of triggering "now" and hoping the capture lands mid-animation:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
now = float(viewer.frameStamp.simulationTime)
|
|
93
|
+
node.stateSet.uniforms["triggerTime"] = now - 0.4 # force t ~= 0.4/duration, right now
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For a flat/ground-plane effect (a shockwave ring, anything not billboarded),
|
|
97
|
+
also consider the default trackball angle can leave it nearly edge-on and
|
|
98
|
+
hidden behind a brighter overlapping effect — a temporary top-down
|
|
99
|
+
`viewer.camera.viewMatrix = osg.Matrix.lookAt(...)` (gets overwritten by the
|
|
100
|
+
live `CameraManipulator` next frame, so capture immediately after) rules that
|
|
101
|
+
out before assuming the geometry is broken.
|
|
102
|
+
|
|
103
|
+
## 6. `del viewer.eventHandlers[i]` can corrupt a LATER handler's Python identity
|
|
104
|
+
|
|
105
|
+
With `[DebugHandler, HandlerA, HandlerB]`, `del viewer.eventHandlers[1]`
|
|
106
|
+
leaves a surviving `HandlerB` that comes back from `__getitem__` as a bare
|
|
107
|
+
base-class `osgGA.GUIEventHandler` — Python-side attributes set in its own
|
|
108
|
+
`__init__` are gone, and its `handle()` override silently stops firing
|
|
109
|
+
(falls back to the C++ base no-op). Not yet root-caused.
|
|
110
|
+
|
|
111
|
+
**Workaround:** overwrite in place instead of delete —
|
|
112
|
+
`viewer.eventHandlers[i] = new_handler` preserves identity correctly. Avoid
|
|
113
|
+
`del`-ing a middle element of `eventHandlers` if anything after it needs to
|
|
114
|
+
keep working.
|
|
115
|
+
|
|
116
|
+
## 7. Don't guard a MappingProxy assignment with an existence check first
|
|
117
|
+
|
|
118
|
+
`ss.uniforms[key] = value` already creates-or-updates on its own
|
|
119
|
+
(`UniformsTag::apply()` in `pyosg/osg/State.hpp`). Writing
|
|
120
|
+
`if key in ss.uniforms: ss.uniforms[key] = value` makes the assignment
|
|
121
|
+
*conditional on the uniform already existing* — silently no-ops a node's
|
|
122
|
+
very first trigger, since construction only sets the GLSL default, not a
|
|
123
|
+
real bound uniform, until something actually assigns to it. An unconditional
|
|
124
|
+
assignment through a `MappingProxy`/`ValueMappingProxy` is already the safe,
|
|
125
|
+
correct form; adding an existence check in front of it is closer to
|
|
126
|
+
introducing a bug than preventing one.
|
|
127
|
+
|
|
128
|
+
## 8. Nested one-shot effects need a recursive `trigger()`, not a deeper positional walk
|
|
129
|
+
|
|
130
|
+
When a builder wraps several effect groups in per-position
|
|
131
|
+
`MatrixTransform`s — one level deeper than a flat 2–4 child layout a
|
|
132
|
+
`trigger(node, viewer)` originally assumed — recurse to leaf nodes instead of
|
|
133
|
+
adding a depth parameter:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
def trigger(node, viewer):
|
|
137
|
+
now = float(viewer.frameStamp.simulationTime)
|
|
138
|
+
|
|
139
|
+
def walk(n):
|
|
140
|
+
if isinstance(n, osg.Group):
|
|
141
|
+
for child in n.children:
|
|
142
|
+
walk(child)
|
|
143
|
+
else:
|
|
144
|
+
n.stateSet.uniforms["triggerTime"] = now
|
|
145
|
+
|
|
146
|
+
walk(node)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Strict superset of flat-layout behavior, and handles arbitrarily deeper
|
|
150
|
+
nesting for free.
|
|
151
|
+
|
|
152
|
+
## 9. Multi-instance variety: randomize kwargs per instance, not shared state
|
|
153
|
+
|
|
154
|
+
When every tunable parameter is already "just a kwarg that flows straight to
|
|
155
|
+
a uniform," giving each instance independently `random.uniform()`-jittered
|
|
156
|
+
kwargs at construction time is close to free — prefer N independently
|
|
157
|
+
parameterized instances of an existing single-instance builder over one
|
|
158
|
+
shared preset placed N times, when the builder is already kwargs-shaped.
|
|
159
|
+
|
|
160
|
+
## 10. Extending a deliberately-minimal ImGui "bin": add a knob only when needed
|
|
161
|
+
|
|
162
|
+
`osgx.imgui` is intentionally not a general Dear ImGui wrapper — a small,
|
|
163
|
+
fixed set of "knob" primitives (`slider_float`, `checkbox`, `radio_group`,
|
|
164
|
+
...) each returning a `(changed, value)` tuple, since Python values aren't
|
|
165
|
+
mutable references the way ImGui's C++ `&value` out-params expect. Add a
|
|
166
|
+
primitive only when a concrete control is needed:
|
|
167
|
+
|
|
168
|
+
- `button(label) -> bool`
|
|
169
|
+
- `color_edit3(label, r, g, b) -> (changed, r, g, b)`
|
|
170
|
+
|
|
171
|
+
Both are one-line `ImGui::` wrappers in `~/dev/osgx/src/ImGui.cpp` +
|
|
172
|
+
`ext/python/osgx-imgui.cpp`, no new C++ classes. **C++-first, as always**
|
|
173
|
+
(see [[feedback_cpp_first_design]]): the primitive lives in `osgx::imgui`
|
|
174
|
+
proper, Python just calls it.
|
|
175
|
+
|
|
176
|
+
Two sliders in *different* panel sections sharing the exact same label
|
|
177
|
+
collide on the same ImGui ID — `Panel::draw()` only wraps a section in
|
|
178
|
+
`PushID` when `expand=True`, so a naive per-layer helper reusing the raw
|
|
179
|
+
uniform name as the label (`"duration"` for fire, shockwave, smoke, embers
|
|
180
|
+
alike) silently merges all four sliders' drag state. Fix: prefix every
|
|
181
|
+
control label with its section name ("Fire Duration", "Shockwave Duration", ...).
|
|
182
|
+
|
|
183
|
+
A slider that *reads* a uniform's `.value` needs that uniform to already
|
|
184
|
+
exist on the `StateSet` (unlike writing, reading is not create-or-update).
|
|
185
|
+
Fragment-only tuning knobs that were only ever given a GLSL default (never
|
|
186
|
+
assigned from Python) don't exist as a real uniform the first time a slider
|
|
187
|
+
section tries to read them. Fix: seed every slider's uniform to its known
|
|
188
|
+
default once, unconditionally, right before building the section — not a
|
|
189
|
+
guarded "if missing" check (same antipattern as point 7, on the read side).
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Deterministic captures of time-driven GPU effects
|
|
2
|
+
|
|
3
|
+
`capture_framebuffer()` makes the GL context current; it does **not** choose
|
|
4
|
+
which instant of an animation gets rendered. Triggering an effect and racing
|
|
5
|
+
a capture against the next `viewer.frame()` is inherently unreliable.
|
|
6
|
+
|
|
7
|
+
The fix is an effect-local clock with two modes: normal playback derives
|
|
8
|
+
elapsed time from `osg_SimulationTime - triggerTime`; inspection playback
|
|
9
|
+
supplies an explicit elapsed age in seconds.
|
|
10
|
+
|
|
11
|
+
## Shader contract
|
|
12
|
+
|
|
13
|
+
Add a uniform whose negative value means "use realtime":
|
|
14
|
+
|
|
15
|
+
```glsl
|
|
16
|
+
uniform float osg_SimulationTime;
|
|
17
|
+
uniform float triggerTime = -1000.0;
|
|
18
|
+
uniform float effectAge = -1.0;
|
|
19
|
+
|
|
20
|
+
float elapsed = effectAge >= 0.0 ? effectAge : osg_SimulationTime - triggerTime;
|
|
21
|
+
float t = clamp(elapsed / duration, 0.0, 1.0);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `elapsed`, not `osg_SimulationTime`, for **every** time-varying part of
|
|
25
|
+
the effect that needs to match the captured instant: movement, noise scroll,
|
|
26
|
+
flicker, size envelopes, fragment breakup. Freezing only the normalized `t`
|
|
27
|
+
while a noise lookup still uses realtime leaves the result nondeterministic.
|
|
28
|
+
|
|
29
|
+
Seed the uniform from Python during construction:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
ss.uniforms["effectAge"] = -1.0
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Trigger and scrub helpers
|
|
36
|
+
|
|
37
|
+
For a nested effect, recurse to drawable-owning leaves. Starting realtime
|
|
38
|
+
playback must explicitly clear a previously frozen age:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def trigger(node, viewer):
|
|
42
|
+
now = float(viewer.frameStamp.simulationTime)
|
|
43
|
+
|
|
44
|
+
def walk(n):
|
|
45
|
+
if isinstance(n, osg.Group):
|
|
46
|
+
for child in n.children:
|
|
47
|
+
walk(child)
|
|
48
|
+
else:
|
|
49
|
+
n.stateSet.uniforms["effectAge"] = -1.0
|
|
50
|
+
n.stateSet.uniforms["triggerTime"] = now
|
|
51
|
+
|
|
52
|
+
walk(node)
|
|
53
|
+
|
|
54
|
+
def set_age(node, effect_age):
|
|
55
|
+
"""Freeze at elapsed seconds; pass a negative value to resume realtime."""
|
|
56
|
+
|
|
57
|
+
def walk(n):
|
|
58
|
+
if isinstance(n, osg.Group):
|
|
59
|
+
for child in n.children:
|
|
60
|
+
walk(child)
|
|
61
|
+
else:
|
|
62
|
+
n.stateSet.uniforms["effectAge"] = effect_age
|
|
63
|
+
|
|
64
|
+
walk(node)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Age is measured in seconds rather than normalized phase, so layers with
|
|
68
|
+
different durations stay synchronized to one source event: at age `1.2`,
|
|
69
|
+
every layer evaluates itself 1.2 seconds after the trigger and applies its
|
|
70
|
+
own duration naturally.
|
|
71
|
+
|
|
72
|
+
## Live capture workflow
|
|
73
|
+
|
|
74
|
+
In a tmux-backed `pyosg_repl.py` session, freeze the effect, then use the
|
|
75
|
+
controller's queued capture (not a top-level `readPixels()`):
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
set_age(effect, 1.2)
|
|
79
|
+
await _osg_repl_controller.capture_framebuffer("/tmp/effect-age-1.2.png")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If a person may be at the same keyboard/mouse while this runs, wrap it in
|
|
83
|
+
`_osg_repl_controller.locked_input()` (`examples/pyosg_repl.py`'s
|
|
84
|
+
`AgentInputLock`) — otherwise a keypress that re-triggers or mutates the
|
|
85
|
+
effect can land on the same frame as `set_age()`/the capture, unfreezing the
|
|
86
|
+
very state being inspected:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
with _osg_repl_controller.locked_input():
|
|
90
|
+
set_age(effect, 1.2)
|
|
91
|
+
await _osg_repl_controller.capture_framebuffer("/tmp/effect-age-1.2.png")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To prove determinism rather than just eyeball a screenshot, capture the same
|
|
95
|
+
frozen frame twice after letting realtime frames advance in between, then
|
|
96
|
+
compare the files outside the REPL:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
with _osg_repl_controller.locked_input():
|
|
100
|
+
set_age(effect, 1.2)
|
|
101
|
+
await _osg_repl_controller.capture_framebuffer("/tmp/effect-a.png")
|
|
102
|
+
await asyncio.sleep(0.5)
|
|
103
|
+
await _osg_repl_controller.capture_framebuffer("/tmp/effect-b.png")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
sha256sum /tmp/effect-a.png /tmp/effect-b.png
|
|
108
|
+
cmp -s /tmp/effect-a.png /tmp/effect-b.png
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This proves only the frozen nodes are deterministic. To capture a whole
|
|
112
|
+
composite effect, every visible time-driven layer must implement the same
|
|
113
|
+
clock contract; hide or freeze unrelated animated scenery, UI, camera shake,
|
|
114
|
+
and particle layers before comparing capture bytes.
|
|
115
|
+
|
|
116
|
+
## Short realtime video captures
|
|
117
|
+
|
|
118
|
+
For a short MP4 rather than a one-off PNG, use the model-controls facade's
|
|
119
|
+
callback-driven recorder:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
_osg_repl_controls.capture.video("/tmp/take.mp4", fps=24, duration=5)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
It samples in the final-draw callback on a monotonic wall-clock schedule and
|
|
126
|
+
streams raw RGB frames directly to FFmpeg. Good for recording a user's live
|
|
127
|
+
camera manipulation because the call returns immediately and leaves input
|
|
128
|
+
unlocked by default. Pass `lock_input=True` when a script is posing the
|
|
129
|
+
camera or updating uniforms and a human must not race it:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
_osg_repl_controls.capture.video(
|
|
133
|
+
"/tmp/deterministic-take.mp4",
|
|
134
|
+
fps=24,
|
|
135
|
+
duration=5,
|
|
136
|
+
lock_input=True,
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For a reproducible video, first set every time-driven visible effect to its
|
|
141
|
+
explicit age (and keep unrelated animation frozen) before starting the
|
|
142
|
+
recorder. The video sampler controls *which wall-clock moments are
|
|
143
|
+
captured*; it does not make shader time deterministic by itself.
|
|
144
|
+
|
|
145
|
+
Readback still uses synchronous `osg.Image.readPixels()` in the final-draw
|
|
146
|
+
callback — not yet a PBO/fence-backed asynchronous GPU transfer.
|
|
147
|
+
|
|
148
|
+
## Realtime handoff
|
|
149
|
+
|
|
150
|
+
After inspecting an age, either resume the existing effect with
|
|
151
|
+
`set_age(effect, -1.0)` or restart it with `trigger(effect, viewer)`. Prefer
|
|
152
|
+
the latter when the effect may already have elapsed past its duration.
|
|
153
|
+
|
|
154
|
+
`examples/pyosg-praxis.py` is the current minimal working reference. Promote
|
|
155
|
+
this contract into `pyosg-fire.py` only as a coherent change across its
|
|
156
|
+
fire, shockwave, smoke, and ember shaders — a partial conversion gives a
|
|
157
|
+
misleading "frozen" composite whose remaining layers still race realtime.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Object introspection and lifetime debugging
|
|
2
|
+
|
|
3
|
+
Every `osg::Object` subclass in these bindings exposes a small cluster of
|
|
4
|
+
introspection primitives — for "what is this node actually holding" and "did
|
|
5
|
+
this object actually get destroyed," not just serialization.
|
|
6
|
+
|
|
7
|
+
## `.dumps()` — OSGT text serialization
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
print(node.dumps().decode())
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Verifies what attributes, textures, and state are actually attached to a
|
|
14
|
+
node/geometry without a separate viewer or tool. Works on live objects
|
|
15
|
+
mid-session.
|
|
16
|
+
|
|
17
|
+
## `.addr` — the real C++ memory address
|
|
18
|
+
|
|
19
|
+
A `void*` to the actual object, not a vtable-offset pointer. Use to confirm
|
|
20
|
+
two Python handles refer to the *same* underlying C++ object (`a.addr ==
|
|
21
|
+
b.addr`) rather than two separate wrappers, or to correlate with addresses
|
|
22
|
+
printed by C++-side `osg::notify()` output.
|
|
23
|
+
|
|
24
|
+
## `.referenceCount` — `RefCounts(cpp=N, py=M)`
|
|
25
|
+
|
|
26
|
+
The real OSG-side `ref_ptr` count and the Python-side refcount, separately.
|
|
27
|
+
|
|
28
|
+
**The `py` count is artificially inflated inside an interactive IPython
|
|
29
|
+
session** — `Out[]` history caching, `_`/`__`/`___`, and other REPL
|
|
30
|
+
bookkeeping can hold references you didn't intentionally keep. A high `py`
|
|
31
|
+
count does not by itself mean a leak. Removing an object from a scene graph
|
|
32
|
+
container only drops *one* `cpp` ref; if a Python variable (or `Out[]`
|
|
33
|
+
history) still points at it, the object stays fully alive.
|
|
34
|
+
|
|
35
|
+
## `debug=True` / `debug=<callable>` — ground-truth destruction proof
|
|
36
|
+
|
|
37
|
+
Pass as a kwarg to an `osg.Object`-derived constructor to attach a lifetime
|
|
38
|
+
probe via the object's `UserDataContainer`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
node = osg.Node(debug=True) # notify()'s "Observing"/"Destroying"
|
|
42
|
+
node = osg.Node(debug=lambda addr, type_, name, deletions=deletions: ...) # custom callback
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Only works for types wired into the binding layer's `kwargs_init` chain**
|
|
46
|
+
(see `pyosg/pyosg.hpp`'s manifest — `kwargs_base<T>` + each type's own
|
|
47
|
+
`kwargs_init_own`). Most `osg::Object` subclasses are, but it isn't automatic
|
|
48
|
+
just from deriving from `osg::Object` in C++ — a type hand-written with plain
|
|
49
|
+
`py::init<...>()` overloads silently rejects `debug=` (and `name=`,
|
|
50
|
+
`dataVariance=`) with a constructor-overload-mismatch error rather than a
|
|
51
|
+
clear "unsupported kwarg" message. If `debug=`/`name=` fails with
|
|
52
|
+
`incompatible constructor arguments`, check whether the type is in that
|
|
53
|
+
manifest before assuming the probe itself is broken.
|
|
54
|
+
|
|
55
|
+
Since the probe lives inside the target's own `UserDataContainer`, its
|
|
56
|
+
destructor fires at true C++ destruction — not "removed from the scene
|
|
57
|
+
graph," not "Python variable went out of scope." This is the ground-truth
|
|
58
|
+
tool for verifying an object is actually gone, as opposed to inferring it
|
|
59
|
+
from refcounts alone.
|
|
60
|
+
|
|
61
|
+
**The `debug=<callable>` callback is subject to the exact same
|
|
62
|
+
cross-context free-variable `NameError` as any other C++-invoked Python
|
|
63
|
+
callback** (see [`01-core.md`](01-core.md) rule 2) — bind anything it needs
|
|
64
|
+
as a default argument: `def on_delete(addr, type_, name, deletions=deletions):
|
|
65
|
+
...`. This applies even though the trigger here is an ordinary Python
|
|
66
|
+
refcounting/GC destructor call on the main thread, not the render loop or any
|
|
67
|
+
background thread.
|
|
68
|
+
|
|
69
|
+
## How to combine these when investigating a leak
|
|
70
|
+
|
|
71
|
+
1. `.dumps()` to see what's really attached.
|
|
72
|
+
2. `.addr` + `.referenceCount` together to distinguish "detached from the
|
|
73
|
+
scene graph" (a removed-but-still-Python-referenced object shows `cpp` >
|
|
74
|
+
0) from "actually destroyed."
|
|
75
|
+
3. `debug=` when you need proof, not inference — especially when the bug
|
|
76
|
+
might be in the binding layer itself (proxy/container implementation),
|
|
77
|
+
not application code.
|