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,493 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # MRT (Multiple Render Targets) proof-of-concept.
4
+ #
5
+ # pyosg-rtt.py proves COLOR_BUFFER + DEPTH_BUFFER -- two different attachment
6
+ # *types*, one color slot. pyosg-blur.py proves chained multi-pass (several
7
+ # separate single-output passes feeding each other). Neither proves the thing
8
+ # a deferred G-buffer actually needs: ONE geometry pass writing to MULTIPLE
9
+ # color attachments SIMULTANEOUSLY (COLOR_BUFFER0 + COLOR_BUFFER1) from a
10
+ # single shader invocation per pixel, via GLSL `layout(location = n) out`.
11
+ #
12
+ # This example proves that, then goes one step further and actually USES the
13
+ # extra normal buffer: the toon lighting from pyosg-rtt.py's scene shader is
14
+ # moved OUT of the geometry pass entirely (deferred) into the composite pass,
15
+ # computed from the color+normal+depth G-buffer instead -- the same shape
16
+ # Step 11 of the lighting-class series (see ai/context-todo-lighting-class.md)
17
+ # will need for its SSAO/SSR post-processing stack. The composite outline is
18
+ # also upgraded to combine depth-edge AND normal-edge detection (silhouette
19
+ # vs. crease/grazing-angle edges), which is measurably more robust than
20
+ # pyosg-rtt.py's depth-only Sobel-ish outline.
21
+ #
22
+ # Press 1/2/3 to visualize the raw color/depth/normal G-buffer channels;
23
+ # press 0 to return to the deferred-lit composite (the default).
24
+
25
+ import os
26
+ import sys
27
+
28
+ os.environ.update({
29
+ "OSG_WINDOW": "50 50 800 600",
30
+ "OSG_THREADING": "SingleThreaded",
31
+ "OSG_GL_CONTEXT_PROFILE_MASK": "1",
32
+ "OSG_GL_VERSION": "4.6",
33
+ "OSG_GL_CONTEXT_VERSION": "4.6"
34
+ })
35
+
36
+ from pyosg_example import label
37
+
38
+ from OpenSceneGraph import *
39
+ from OpenSceneGraph.GL import *
40
+
41
+ from pyosg_visitor import GatherVisitor
42
+
43
+ W, H = 800, 600
44
+
45
+ # --------------------------------------------------------------------------- #
46
+ # Shaders
47
+ # --------------------------------------------------------------------------- #
48
+
49
+ SCENE_VERTEX_SHADER = """
50
+ #version 330 core
51
+
52
+ in vec4 osg_Vertex;
53
+ in vec4 osg_Color;
54
+ in vec3 osg_Normal;
55
+
56
+ uniform mat4 osg_ModelViewProjectionMatrix;
57
+ uniform mat3 osg_NormalMatrix;
58
+
59
+ out vec4 vColor;
60
+ out vec3 vNormal;
61
+
62
+ void main() {
63
+ vNormal = normalize(osg_NormalMatrix * osg_Normal);
64
+ vColor = osg_Color;
65
+
66
+ gl_Position = osg_ModelViewProjectionMatrix * osg_Vertex;
67
+ }
68
+ """
69
+
70
+ # G-buffer write pass: NO lighting happens here at all -- just raw albedo and
71
+ # view-space normal, written to two color attachments SIMULTANEOUSLY (true
72
+ # MRT, via the two `layout(location = n) out` declarations below). Depth
73
+ # comes "for free" from this camera's own DEPTH_BUFFER attachment/z-test.
74
+ GBUFFER_FRAGMENT_SHADER = """
75
+ #version 330 core
76
+
77
+ in vec4 vColor;
78
+ in vec3 vNormal;
79
+
80
+ layout(location = 0) out vec4 outColor; // COLOR_BUFFER0: unlit albedo
81
+ layout(location = 1) out vec4 outNormal; // COLOR_BUFFER1: view-space normal
82
+
83
+ void main() {
84
+ outColor = vColor;
85
+ outNormal = vec4(normalize(vNormal), 1.0);
86
+ }
87
+ """
88
+
89
+ FULLSCREEN_VERTEX_SHADER = """
90
+ #version 330 core
91
+
92
+ in vec4 osg_Vertex;
93
+ in vec2 osg_MultiTexCoord0;
94
+
95
+ out vec2 uv;
96
+
97
+ void main() {
98
+ uv = osg_MultiTexCoord0;
99
+ gl_Position = osg_Vertex;
100
+ }
101
+ """
102
+
103
+ # Deferred composite: reads the G-buffer (color/normal/depth) back and does
104
+ # ALL of the actual lighting HERE instead of in the geometry pass -- the same
105
+ # cel-shaded diffuse + rim light as pyosg-rtt.py, just computed from the
106
+ # G-buffer instead of inline per-vertex/per-fragment during the geometry
107
+ # pass. View-space position is reconstructed from depth + the inverse
108
+ # projection matrix (the same reconstruction technique a real SSAO/SSR pass
109
+ # needs the depth buffer for).
110
+ COMPOSITE_FRAGMENT_SHADER = """
111
+ #version 330 core
112
+
113
+ uniform sampler2D colorTex;
114
+ uniform sampler2D normalTex;
115
+ uniform sampler2D depthTex;
116
+ uniform mat4 invProjectionMatrix;
117
+ uniform float znear;
118
+ uniform float zfar;
119
+ uniform int visualizeMode; // 0=lit composite, 1=color, 2=depth, 3=normal
120
+
121
+ in vec2 uv;
122
+
123
+ out vec4 fragColor;
124
+
125
+ // Convert depth buffer value -> camera-space Z distance.
126
+ float linearizeDepth(float d, float near, float far) {
127
+ float z = d * 2.0 - 1.0;
128
+
129
+ return (2.0 * near * far) / (far + near - z * (far - near));
130
+ }
131
+
132
+ // Reconstruct view-space position from a screen UV + depth-buffer sample.
133
+ vec3 reconstructViewPos(vec2 uv, float d) {
134
+ vec4 clip = vec4(vec3(uv, d) * 2.0 - 1.0, 1.0);
135
+ vec4 viewPos = invProjectionMatrix * clip;
136
+
137
+ return viewPos.xyz / viewPos.w;
138
+ }
139
+
140
+ void main() {
141
+ vec4 albedo = texture(colorTex, uv);
142
+ vec3 rawNormal = texture(normalTex, uv).rgb;
143
+ float d = texture(depthTex, uv).r;
144
+
145
+ // --- Raw G-buffer visualize modes (bypass lighting entirely) -------- //
146
+ if (visualizeMode == 1) {
147
+ fragColor = albedo;
148
+
149
+ return;
150
+ }
151
+
152
+ if (visualizeMode == 2) {
153
+ float lin = linearizeDepth(d, znear, zfar);
154
+ float t = clamp((lin - znear) / (zfar - znear), 0.0, 1.0);
155
+
156
+ fragColor = vec4(vec3(t), 1.0);
157
+
158
+ return;
159
+ }
160
+
161
+ if (visualizeMode == 3) {
162
+ // Raw (unnormalized) sample -- background pixels the G-buffer pass
163
+ // never touched read back as (0,0,0) -> mid-gray here, which is a
164
+ // harmless "no data" tell rather than a NaN from normalizing a zero
165
+ // vector.
166
+ fragColor = vec4(rawNormal * 0.5 + 0.5, 1.0);
167
+
168
+ return;
169
+ }
170
+
171
+ // --- Deferred lit composite (default) -------------------------------- //
172
+ // A cleared-but-never-written background pixel has a zero-length normal
173
+ // (real written normals are always unit length) -- use that as a sentinel
174
+ // for "no geometry here" instead of trusting the color clear value, which
175
+ // would otherwise get relit as if it were a real (very dark) surface.
176
+ if (dot(rawNormal, rawNormal) < 0.0001) {
177
+ fragColor = vec4(0.1, 0.5, 0.2, 1.0); // same green as pyosg-rtt.py's clear color
178
+
179
+ return;
180
+ }
181
+
182
+ vec3 N = normalize(rawNormal);
183
+
184
+ const vec3 L = vec3(0.268, 0.358, 0.894);
185
+ const vec3 rimColor = vec3(0.9, 0.6, 0.0);
186
+ const float rimPower = 5.0;
187
+ const float rimBase = 0.3;
188
+ const float rimTint = 0.4;
189
+
190
+ float diffuse = max(dot(N, L), 0.0);
191
+ diffuse = floor(diffuse * 3.0) / 3.0;
192
+
193
+ float ambient = 0.25;
194
+ float light = ambient + diffuse;
195
+
196
+ vec3 viewPos = reconstructViewPos(uv, d);
197
+ vec3 viewDir = normalize(-viewPos);
198
+ float rim = pow(1.0 - clamp(dot(N, viewDir), 0.0, 1.0), rimPower);
199
+ vec3 rimLight = rim * (vec3(rimBase) + rimColor * rimTint);
200
+
201
+ vec3 color = albedo.rgb * light + rimLight;
202
+
203
+ // --- Combined depth-edge + normal-edge outline ----------------------- //
204
+ // Depth-edge alone (pyosg-rtt.py's original technique) catches silhouette
205
+ // edges where geometry meets background/other geometry, but misses edges
206
+ // where depth barely changes yet the surface is turning sharply (grazing
207
+ // angles, or two coplanar-ish faces meeting at a crease) -- normal-edge
208
+ // catches exactly those. Neighbor normal taps are deliberately left
209
+ // UNnormalized: a background neighbor's zero vector then contributes a
210
+ // clean "maximum edge" via dot(N, vec3(0)) == 0, with no normalize(0) NaN
211
+ // risk, which conveniently reinforces the silhouette outline for free.
212
+ vec2 texel = 1.0 / vec2(textureSize(depthTex, 0));
213
+
214
+ float dL = texture(depthTex, uv + vec2(-texel.x, 0.0)).r;
215
+ float dR = texture(depthTex, uv + vec2( texel.x, 0.0)).r;
216
+ float dU = texture(depthTex, uv + vec2(0.0, texel.y)).r;
217
+ float dD = texture(depthTex, uv + vec2(0.0, -texel.y)).r;
218
+
219
+ float z = linearizeDepth(d, znear, zfar);
220
+ float zL = linearizeDepth(dL, znear, zfar);
221
+ float zR = linearizeDepth(dR, znear, zfar);
222
+ float zU = linearizeDepth(dU, znear, zfar);
223
+ float zD = linearizeDepth(dD, znear, zfar);
224
+
225
+ float edgeH = abs(zL - zR);
226
+ float edgeV = abs(zU - zD);
227
+ float depthEdge = sqrt(edgeH * edgeH + edgeV * edgeV);
228
+ float depthThreshold = z * 0.03;
229
+ float depthOutline = smoothstep(depthThreshold * 0.5, depthThreshold * 1.5, depthEdge);
230
+
231
+ vec3 nL = texture(normalTex, uv + vec2(-texel.x, 0.0)).rgb;
232
+ vec3 nR = texture(normalTex, uv + vec2( texel.x, 0.0)).rgb;
233
+ vec3 nU = texture(normalTex, uv + vec2(0.0, texel.y)).rgb;
234
+ vec3 nD = texture(normalTex, uv + vec2(0.0, -texel.y)).rgb;
235
+
236
+ float normalEdge = (1.0 - dot(N, nL)) + (1.0 - dot(N, nR)) + (1.0 - dot(N, nU)) + (1.0 - dot(N, nD));
237
+ float normalOutline = smoothstep(0.05, 0.3, normalEdge);
238
+
239
+ float outline = max(depthOutline, normalOutline);
240
+
241
+ color = mix(color, vec3(0.0), outline);
242
+
243
+ fragColor = vec4(color, 1.0);
244
+ }
245
+ """
246
+
247
+ # --------------------------------------------------------------------------- #
248
+ # Scene / camera setup
249
+ # --------------------------------------------------------------------------- #
250
+
251
+ # Same four spheres as pyosg-rtt.py -- this example is about proving/using
252
+ # MRT, not about the scene content, so keep that constant for a fair
253
+ # comparison against the existing example.
254
+ def create_scene():
255
+ g = None
256
+
257
+ if len(sys.argv) >= 2:
258
+ g = osgDB.readNodeFile(sys.argv[1])
259
+
260
+ else:
261
+ g = osg.Geode(drawables=(
262
+ osg.ShapeDrawable(osg.Sphere(osg.Vec3(0, 2.0, 0), 1.0)),
263
+ osg.ShapeDrawable(osg.Sphere(osg.Vec3(0, 5.0, 0), 1.5)),
264
+ osg.ShapeDrawable(osg.Sphere(osg.Vec3(0, 8.0, 0), 2.0)),
265
+ osg.ShapeDrawable(osg.Sphere(osg.Vec3(0, 12.0, 0), 3.0))
266
+ ))
267
+
268
+ p = osg.Program(name="gbufferProgram", shaders=(
269
+ osg.Shader(osg.Shader.VERTEX, SCENE_VERTEX_SHADER),
270
+ osg.Shader(osg.Shader.FRAGMENT, GBUFFER_FRAGMENT_SHADER)
271
+ ))
272
+
273
+ # g.stateSet.setAttributeAndModes(p)
274
+ g.stateSet.attributes.append(p)
275
+
276
+ return g
277
+
278
+ # Creates the G-buffer RTT camera: THREE simultaneous attachments (color,
279
+ # normal, depth) from one geometry pass. Returns the camera plus all three
280
+ # `Texture` instances, which the composite HUD camera samples from directly.
281
+ def create_gbuffer_camera(w=W, h=H):
282
+ color_tex = osg.Texture2D(
283
+ size=(w, h),
284
+ internalFormat=GL_RGBA,
285
+ filter=(osg.Texture.LINEAR, osg.Texture.LINEAR),
286
+ )
287
+
288
+ # Float format so signed [-1, 1] normal components need no encode/decode
289
+ # remap -- same convention as the lighting-class series (env_tex/cube_tex/
290
+ # prefilter_tex all use GL_RGB16F for the same reason).
291
+ normal_tex = osg.Texture2D(
292
+ size=(w, h),
293
+ internalFormat=GL_RGB16F,
294
+ filter=(osg.Texture.NEAREST, osg.Texture.NEAREST),
295
+ )
296
+
297
+ depth_tex = osg.Texture2D(
298
+ size=(w, h),
299
+ internalFormat=GL_DEPTH_COMPONENT24,
300
+ sourceFormat=GL_DEPTH_COMPONENT,
301
+ sourceType=GL_FLOAT,
302
+ filter=(osg.Texture.NEAREST, osg.Texture.NEAREST),
303
+ )
304
+
305
+ cam = osg.Camera(
306
+ renderOrder=osg.Camera.PRE_RENDER,
307
+ renderTargetImplementation=osg.Camera.FRAME_BUFFER_OBJECT,
308
+ clearMask=GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT,
309
+ clearColor=osg.Vec4(0.0, 0.0, 0.0, 0.0),
310
+ viewport=osg.Viewport(0, 0, w, h),
311
+ name="G-Buffer Camera",
312
+ )
313
+
314
+ # True MRT: two SIMULTANEOUS color attachments from a single geometry
315
+ # pass, plus depth -- the thing neither pyosg-rtt.py (COLOR+DEPTH, one
316
+ # color slot) nor pyosg-blur.py (chained single-output passes) proves.
317
+ cam.attach(osg.Camera.COLOR_BUFFER0, color_tex)
318
+ cam.attach(osg.Camera.COLOR_BUFFER1, normal_tex)
319
+ cam.attach(osg.Camera.DEPTH_BUFFER, depth_tex)
320
+
321
+ return cam, color_tex, normal_tex, depth_tex
322
+
323
+ # Creates the composite/HUD camera: samples all three G-buffer textures and
324
+ # either runs the deferred toon-lighting pass or dumps one raw buffer to the
325
+ # screen, depending on `visualizeMode` (see VisualizeModeHandler below).
326
+ def create_hud_camera(color_tex, normal_tex, depth_tex):
327
+ cam = osg.Camera(
328
+ referenceFrame=osg.Transform.ABSOLUTE_RF,
329
+ renderOrder=osg.Camera.POST_RENDER,
330
+ clearMask=0,
331
+ allowEventFocus=False,
332
+ projectionMatrix=osg.Matrix.identity(),
333
+ viewMatrix=osg.Matrix.identity(),
334
+ name="Composite HUD",
335
+ )
336
+
337
+ g = osg.Geode()
338
+ g.drawables.append(osg.createTexturedQuadGeometry(
339
+ osg.Vec3(-1.0, -1.0, -1.0),
340
+ osg.Vec3(2.0, 0.0, 0.0),
341
+ osg.Vec3(0.0, 2.0, 0.0)
342
+ ))
343
+
344
+ cam.children.append(g)
345
+
346
+ cam.stateSet.textureAttributes[0] = color_tex
347
+ cam.stateSet.textureAttributes[1] = normal_tex
348
+ cam.stateSet.textureAttributes[2] = depth_tex
349
+
350
+ cam.stateSet.uniforms["colorTex"] = 0
351
+ cam.stateSet.uniforms["normalTex"] = 1
352
+ cam.stateSet.uniforms["depthTex"] = 2
353
+
354
+ p = osg.Program(name="compositeProgram", shaders=(
355
+ osg.Shader(osg.Shader.VERTEX, FULLSCREEN_VERTEX_SHADER),
356
+ osg.Shader(osg.Shader.FRAGMENT, COMPOSITE_FRAGMENT_SHADER)
357
+ ))
358
+
359
+ # g.stateSet.setAttributeAndModes(p)
360
+ g.stateSet.attributes.append(p)
361
+
362
+ return cam
363
+
364
+ # --------------------------------------------------------------------------- #
365
+ # Input
366
+ # --------------------------------------------------------------------------- #
367
+ class VisualizeModeHandler(osgGA.GUIEventHandler):
368
+ """Press 1/2/3 to visualize the raw color/depth/normal G-buffer channels; 0 for the lit composite."""
369
+
370
+ def __init__(self, mode_uniform):
371
+ super().__init__()
372
+
373
+ self.mode_uniform = mode_uniform
374
+
375
+ def handle(self, ea, aa):
376
+ if ea.handled or ea.type != osgGA.GUIEventAdapter.KEYUP:
377
+ return False
378
+
379
+ if ea.key == ord("0"):
380
+ self.mode_uniform.value = 0
381
+
382
+ elif ea.key == ord("1"):
383
+ self.mode_uniform.value = 1
384
+
385
+ elif ea.key == ord("2"):
386
+ self.mode_uniform.value = 2
387
+
388
+ elif ea.key == ord("3"):
389
+ self.mode_uniform.value = 3
390
+
391
+ else:
392
+ return False
393
+
394
+ return True
395
+
396
+ # The real pipeline-assembly entrypoint -- returns the root Node, no viewer/window side
397
+ # effects. This is what external tooling (e.g. etc/pyside6-glsl.py's shader-editor scaffold,
398
+ # ../pyosg-cli) imports and calls directly, so it MUST stay side-effect-free w.r.t. any
399
+ # global viewer state. Takes (w, h) explicitly rather than reading module-level W/H.
400
+ def build_scene(w, h):
401
+ gbuffer_cam, color_tex, normal_tex, depth_tex = create_gbuffer_camera(w, h)
402
+ hud_cam = create_hud_camera(color_tex, normal_tex, depth_tex)
403
+
404
+ gbuffer_cam.children.append(create_scene())
405
+
406
+ znear_u = osg.Uniform("znear", 0.0)
407
+ zfar_u = osg.Uniform("zfar", 0.0)
408
+ inv_proj_u = osg.Uniform("invProjectionMatrix", osg.Matrixf.identity())
409
+ visualize_mode_u = osg.Uniform("visualizeMode", 0)
410
+
411
+ hud_cam.stateSet.uniforms.extend((znear_u, zfar_u, inv_proj_u, visualize_mode_u))
412
+
413
+ # Same idea as pyosg-rtt.py's update_uniforms: OSG recomputes znear/zfar every frame
414
+ # based on the CameraManipulator, so depth linearization and view-space reconstruction
415
+ # both need fresh values every frame, not just at startup.
416
+ #
417
+ # ri.state.projectionMatrix is a SHARED per-context value, not scoped to whichever
418
+ # camera's callback reads it -- it reflects whatever was last applied to the GL state,
419
+ # not necessarily this camera's own matrix. gbuffer_cam is PRE_RENDER (draws FIRST each
420
+ # frame), so its preDrawCallback fires before anything THIS frame has applied a fresh
421
+ # projection -- it read a STALE leftover from the END of the PREVIOUS frame instead:
422
+ # hud_cam's own identity projectionMatrix (hud_cam draws last). That silently broke
423
+ # znear/zfar (decomposing an identity matrix as a perspective gives garbage near/far,
424
+ # killing the depth/normal-edge outline) and invProjectionMatrix (inverse(identity) =
425
+ # identity, breaking reconstructViewPos() and blowing out the rim-light term instead).
426
+ # Confirmed visually 2026-08-19: outline vanished, image washed out toward rimColor's
427
+ # orange/yellow.
428
+ #
429
+ # hud_cam is POST_RENDER (draws LAST), so by the time ITS preDrawCallback fires, the
430
+ # real viewer camera has already drawn in between (PRE_RENDER -> viewer's own NESTED_RENDER
431
+ # -> POST_RENDER) and applied its real, this-frame-fresh projection -- the same timing
432
+ # guarantee the original code got directly from attaching to the caller's own
433
+ # v.camera.preDrawCallback, without build_scene() needing a viewer reference at all.
434
+ def update_uniforms(ri):
435
+ pm = ri.state.projectionMatrix
436
+ fovy, aspect, near, far = pm.getPerspective()
437
+
438
+ znear_u.value = float(near)
439
+ zfar_u.value = float(far)
440
+ inv_proj_u.value = osg.Matrixf(osg.Matrix.inverse(pm))
441
+
442
+ hud_cam.preDrawCallback = update_uniforms
443
+
444
+ root = osg.Group()
445
+ root.children.extend((gbuffer_cam, hud_cam, label("1 color | 2 depth | 3 normal | 0 default", w, h)))
446
+
447
+ return root
448
+
449
+ # Optional second hook, beyond build_scene(): a Node factory has no Viewer to attach
450
+ # osgGA.GUIEventHandlers to, so any example wanting real interactivity (custom key bindings,
451
+ # debug visitors, ...) beyond the default TrackballManipulator defines this too. Purely
452
+ # optional -- an example with nothing interactive just doesn't define it. Called with the
453
+ # SAME (viewer, root) shape by both __main__ below and ../pyosg-cli, so standalone and
454
+ # runner-driven runs behave identically instead of the runner silently dropping this example's
455
+ # interactivity (confirmed missing 2026-08-19, before this hook existed).
456
+ def configure_viewer(viewer, root):
457
+ gbuffer_cam, hud_cam = root.children[:2]
458
+
459
+ # build_scene() doesn't return the visualizeMode Uniform directly (its contract is just
460
+ # "return a Node") -- pull it back out of the HUD camera's StateSet, the same place
461
+ # build_scene() put it.
462
+ visualize_mode_u = hud_cam.stateSet.uniforms["visualizeMode"]
463
+
464
+ viewer.eventHandlers.append(VisualizeModeHandler(visualize_mode_u))
465
+
466
+ # GatherVisitor bound as a default arg -- this file doesn't drive its loop
467
+ # through aipython/pyosg_repl today, but a callback's free variables resolve
468
+ # unpredictably the moment it IS invoked from that bridge (confirmed live in
469
+ # 11-sketchfab.py's --repl session). See aipython/01-core.md rule 2.
470
+ def gather(ea, aa, GatherVisitor=GatherVisitor, v=viewer):
471
+ if ea.handled or ea.type != osgGA.GUIEventAdapter.KEYUP:
472
+ return False
473
+
474
+ if ea.key == ord("g"):
475
+ v.sceneData.accept(GatherVisitor(namespace=globals()))
476
+
477
+ viewer.eventHandlers.append(gather)
478
+
479
+ print("Press 1=color 2=depth 3=normal 0=lit composite (default)", flush=True)
480
+
481
+ if __name__ == "__main__":
482
+ osg.setNotifyLevel(osg.NotifySeverity.NOTICE)
483
+
484
+ v = osgViewer.Viewer()
485
+ root = build_scene(W, H)
486
+
487
+ v.sceneData = root
488
+ v.cameraManipulator = osgGA.TrackballManipulator()
489
+
490
+ configure_viewer(v, root)
491
+
492
+ while not v.done:
493
+ v.frame()