OpenGLContext 3.0.0a1__py3-none-any.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.
- OpenGLContext/__init__.py +151 -0
- OpenGLContext/arrays.py +2 -0
- OpenGLContext/atlas.py +300 -0
- OpenGLContext/audio/__init__.py +32 -0
- OpenGLContext/audio/scene.py +130 -0
- OpenGLContext/audio/settings.py +82 -0
- OpenGLContext/bin/__init__.py +0 -0
- OpenGLContext/bin/choosecontext.py +224 -0
- OpenGLContext/bin/choosefonts.py +209 -0
- OpenGLContext/bin/gltest.py +121 -0
- OpenGLContext/bin/gltf_demo.py +492 -0
- OpenGLContext/bin/gltf_regression.py +645 -0
- OpenGLContext/bin/gltf_view.py +30 -0
- OpenGLContext/bin/keyboardevents.py +93 -0
- OpenGLContext/bin/lorentz.py +109 -0
- OpenGLContext/bin/physics_cook.py +81 -0
- OpenGLContext/bin/profile_view.py +38 -0
- OpenGLContext/bin/terrain_view.py +581 -0
- OpenGLContext/bin/tiles_view.py +39 -0
- OpenGLContext/bin/ui_demo.py +228 -0
- OpenGLContext/bin/view.py +341 -0
- OpenGLContext/bin/vrml_view.py +42 -0
- OpenGLContext/capture.py +125 -0
- OpenGLContext/context.py +1253 -0
- OpenGLContext/contextconfig.py +177 -0
- OpenGLContext/contextdefinition.py +275 -0
- OpenGLContext/debug/__init__.py +6 -0
- OpenGLContext/debug/bufferimage.py +80 -0
- OpenGLContext/debug/leaks.py +13 -0
- OpenGLContext/debug/logcontext.py +9 -0
- OpenGLContext/debug/logs.py +15 -0
- OpenGLContext/debug/state.py +329 -0
- OpenGLContext/displaylist.py +69 -0
- OpenGLContext/doinchildmatrix.py +34 -0
- OpenGLContext/drawcube.py +58 -0
- OpenGLContext/events/__init__.py +10 -0
- OpenGLContext/events/default_config.py +29 -0
- OpenGLContext/events/event.py +65 -0
- OpenGLContext/events/eventhandlermixin.py +214 -0
- OpenGLContext/events/eventmanager.py +269 -0
- OpenGLContext/events/fxevents.py +90 -0
- OpenGLContext/events/glfwevents.py +424 -0
- OpenGLContext/events/glutevents.py +184 -0
- OpenGLContext/events/inputstate.py +129 -0
- OpenGLContext/events/internaltime.py +161 -0
- OpenGLContext/events/keyboardevents.py +153 -0
- OpenGLContext/events/mouseevents.py +473 -0
- OpenGLContext/events/pygameevents.py +162 -0
- OpenGLContext/events/systemtime.py +12 -0
- OpenGLContext/events/timeeventgeneratormanager.py +40 -0
- OpenGLContext/events/timeevents.py +143 -0
- OpenGLContext/events/timer.py +170 -0
- OpenGLContext/events/tkevents.py +172 -0
- OpenGLContext/events/wxevents.py +185 -0
- OpenGLContext/extensionmanager.py +158 -0
- OpenGLContext/framecounter.py +74 -0
- OpenGLContext/frustum.py +135 -0
- OpenGLContext/glfwcontext.py +395 -0
- OpenGLContext/glfwinteractivecontext.py +34 -0
- OpenGLContext/glfwvrmlcontext.py +11 -0
- OpenGLContext/glutcontext.py +224 -0
- OpenGLContext/glutinteractivecontext.py +24 -0
- OpenGLContext/gluttestingcontext.py +11 -0
- OpenGLContext/glutvrmlcontext.py +25 -0
- OpenGLContext/glutvrmltestingcontext.py +48 -0
- OpenGLContext/hud.py +404 -0
- OpenGLContext/interactivecontext.py +29 -0
- OpenGLContext/loaders/__init__.py +1 -0
- OpenGLContext/loaders/base.py +84 -0
- OpenGLContext/loaders/cc0.py +137 -0
- OpenGLContext/loaders/gltf/__init__.py +71 -0
- OpenGLContext/loaders/gltf/accessors.py +255 -0
- OpenGLContext/loaders/gltf/animation.py +611 -0
- OpenGLContext/loaders/gltf/draco.py +166 -0
- OpenGLContext/loaders/gltf/environment_sky.py +416 -0
- OpenGLContext/loaders/gltf/loader.py +80 -0
- OpenGLContext/loaders/gltf/materials.py +298 -0
- OpenGLContext/loaders/gltf/meshes.py +292 -0
- OpenGLContext/loaders/gltf/samples.py +162 -0
- OpenGLContext/loaders/gltf/scene.py +673 -0
- OpenGLContext/loaders/gltf/specular_glossiness.py +154 -0
- OpenGLContext/loaders/gltf/textures.py +155 -0
- OpenGLContext/loaders/gltf/transforms.py +261 -0
- OpenGLContext/loaders/gltf_demos.py +511 -0
- OpenGLContext/loaders/hdr.py +228 -0
- OpenGLContext/loaders/hdri.py +83 -0
- OpenGLContext/loaders/loader.py +255 -0
- OpenGLContext/loaders/obj.py +299 -0
- OpenGLContext/loaders/resolver.py +536 -0
- OpenGLContext/loaders/tiles3d/__init__.py +9 -0
- OpenGLContext/loaders/tiles3d/boundingvolume.py +120 -0
- OpenGLContext/loaders/tiles3d/dem.py +71 -0
- OpenGLContext/loaders/tiles3d/fetch.py +102 -0
- OpenGLContext/loaders/tiles3d/foliage.py +786 -0
- OpenGLContext/loaders/tiles3d/frustum.py +70 -0
- OpenGLContext/loaders/tiles3d/geomorph.py +57 -0
- OpenGLContext/loaders/tiles3d/gltf_uploader.py +246 -0
- OpenGLContext/loaders/tiles3d/loadmanager.py +121 -0
- OpenGLContext/loaders/tiles3d/physics_colliders.py +78 -0
- OpenGLContext/loaders/tiles3d/procedural.py +355 -0
- OpenGLContext/loaders/tiles3d/residency.py +102 -0
- OpenGLContext/loaders/tiles3d/runtime.py +168 -0
- OpenGLContext/loaders/tiles3d/sample.py +212 -0
- OpenGLContext/loaders/tiles3d/scatter.py +77 -0
- OpenGLContext/loaders/tiles3d/screenspaceerror.py +31 -0
- OpenGLContext/loaders/tiles3d/tileset.py +390 -0
- OpenGLContext/loaders/tiles3d/traversal.py +108 -0
- OpenGLContext/loaders/tiles3d/vegetation.py +361 -0
- OpenGLContext/loaders/vrml97.py +133 -0
- OpenGLContext/looptrace.py +307 -0
- OpenGLContext/move/__init__.py +0 -0
- OpenGLContext/move/bindingstore.py +232 -0
- OpenGLContext/move/direct.py +156 -0
- OpenGLContext/move/dragwatcher.py +42 -0
- OpenGLContext/move/examinemanager.py +100 -0
- OpenGLContext/move/followcam.py +121 -0
- OpenGLContext/move/modes.py +522 -0
- OpenGLContext/move/movementmanager.py +74 -0
- OpenGLContext/move/navigation.py +137 -0
- OpenGLContext/move/physicsplatform.py +203 -0
- OpenGLContext/move/physicswalk.py +469 -0
- OpenGLContext/move/smooth.py +68 -0
- OpenGLContext/move/terrainwalk.py +451 -0
- OpenGLContext/move/trackball.py +92 -0
- OpenGLContext/move/viewplatform.py +283 -0
- OpenGLContext/move/viewplatformmixin.py +335 -0
- OpenGLContext/nav/__init__.py +13 -0
- OpenGLContext/nav/navmesh.py +476 -0
- OpenGLContext/passes/__init__.py +0 -0
- OpenGLContext/passes/_flat.py +1317 -0
- OpenGLContext/passes/asyncpick.py +216 -0
- OpenGLContext/passes/bloom.py +257 -0
- OpenGLContext/passes/flatcompat.py +246 -0
- OpenGLContext/passes/flatcore.py +198 -0
- OpenGLContext/passes/flateffects.py +372 -0
- OpenGLContext/passes/ibl.py +711 -0
- OpenGLContext/passes/instancing.py +775 -0
- OpenGLContext/passes/pbrpass.py +829 -0
- OpenGLContext/passes/renderpass.py +79 -0
- OpenGLContext/passes/renderstats.py +55 -0
- OpenGLContext/passes/rendervisitor.py +60 -0
- OpenGLContext/passes/selection.py +450 -0
- OpenGLContext/passes/selectionbuffers.py +442 -0
- OpenGLContext/passes/shaderpass.py +1018 -0
- OpenGLContext/passes/shaderpass_shadow.py +197 -0
- OpenGLContext/passes/shadersource.py +121 -0
- OpenGLContext/passes/shadowcaps.py +182 -0
- OpenGLContext/passes/shadowmap.py +330 -0
- OpenGLContext/passes/shadowmath.py +290 -0
- OpenGLContext/passes/shadowmixin.py +906 -0
- OpenGLContext/passes/shadowpool.py +186 -0
- OpenGLContext/passes/transmission.py +124 -0
- OpenGLContext/physics/__init__.py +16 -0
- OpenGLContext/physics/debugdraw.py +370 -0
- OpenGLContext/physics/demo.py +157 -0
- OpenGLContext/physics/gltf_world.py +185 -0
- OpenGLContext/physics/manager.py +84 -0
- OpenGLContext/physics/threaded.py +93 -0
- OpenGLContext/plugins.py +61 -0
- OpenGLContext/pygamecontext.py +178 -0
- OpenGLContext/pygameinteractivecontext.py +36 -0
- OpenGLContext/pygametestingcontext.py +10 -0
- OpenGLContext/pygamevrmlcontext.py +13 -0
- OpenGLContext/quaternion.py +190 -0
- OpenGLContext/renderoptions.py +280 -0
- OpenGLContext/resources/__init__.py +36 -0
- OpenGLContext/resources/available_contexts.txt +4 -0
- OpenGLContext/resources/available_contexts_txt.py +9 -0
- OpenGLContext/resources/context_icon_png.py +46 -0
- OpenGLContext/resources/context_icon_small_png.py +21 -0
- OpenGLContext/resources/disk_icon_hi_png.py +14 -0
- OpenGLContext/resources/disk_icon_png.py +15 -0
- OpenGLContext/resources/environment/pimbackground_BK.jpg +0 -0
- OpenGLContext/resources/environment/pimbackground_DN.jpg +0 -0
- OpenGLContext/resources/environment/pimbackground_FR.jpg +0 -0
- OpenGLContext/resources/environment/pimbackground_LF.jpg +0 -0
- OpenGLContext/resources/environment/pimbackground_RT.jpg +0 -0
- OpenGLContext/resources/environment/pimbackground_UP.jpg +0 -0
- OpenGLContext/resources/environment/studio_BK.jpg +0 -0
- OpenGLContext/resources/environment/studio_DN.jpg +0 -0
- OpenGLContext/resources/environment/studio_FR.jpg +0 -0
- OpenGLContext/resources/environment/studio_LF.jpg +0 -0
- OpenGLContext/resources/environment/studio_RT.jpg +0 -0
- OpenGLContext/resources/environment/studio_UP.jpg +0 -0
- OpenGLContext/resources/environment/studiobright_BK.jpg +0 -0
- OpenGLContext/resources/environment/studiobright_DN.jpg +0 -0
- OpenGLContext/resources/environment/studiobright_FR.jpg +0 -0
- OpenGLContext/resources/environment/studiobright_LF.jpg +0 -0
- OpenGLContext/resources/environment/studiobright_RT.jpg +0 -0
- OpenGLContext/resources/environment/studiobright_UP.jpg +0 -0
- OpenGLContext/resources/green_arrow_right_png.py +47 -0
- OpenGLContext/resources/legacy_lighting.vert.txt +37 -0
- OpenGLContext/resources/legacy_lighting_vert_txt.py +18 -0
- OpenGLContext/resources/lights.vert.txt +107 -0
- OpenGLContext/resources/lights_vert_txt.py +23 -0
- OpenGLContext/resources/phongprecalc_vert.py +30 -0
- OpenGLContext/resources/phongweights_frag.py +47 -0
- OpenGLContext/resources/pygame_icon_png.py +47 -0
- OpenGLContext/resources/red_arrow_right_png.py +50 -0
- OpenGLContext/resources/simpleshader.frag.txt +3 -0
- OpenGLContext/resources/simpleshader.vert.txt +3 -0
- OpenGLContext/resources/simpleshader_frag_txt.py +9 -0
- OpenGLContext/resources/simpleshader_vert_txt.py +10 -0
- OpenGLContext/scenegraph/__init__.py +19 -0
- OpenGLContext/scenegraph/appearance.py +107 -0
- OpenGLContext/scenegraph/arraygeometry.py +238 -0
- OpenGLContext/scenegraph/audio.py +632 -0
- OpenGLContext/scenegraph/background.py +108 -0
- OpenGLContext/scenegraph/basenodes.py +50 -0
- OpenGLContext/scenegraph/billboard.py +14 -0
- OpenGLContext/scenegraph/boundingvolume.py +494 -0
- OpenGLContext/scenegraph/box.py +180 -0
- OpenGLContext/scenegraph/collision.py +14 -0
- OpenGLContext/scenegraph/coordinate.py +20 -0
- OpenGLContext/scenegraph/coordinatebounded.py +18 -0
- OpenGLContext/scenegraph/cubebackground.py +207 -0
- OpenGLContext/scenegraph/extrusions.py +275 -0
- OpenGLContext/scenegraph/fog.py +123 -0
- OpenGLContext/scenegraph/gear.py +323 -0
- OpenGLContext/scenegraph/group.py +20 -0
- OpenGLContext/scenegraph/grouping.py +140 -0
- OpenGLContext/scenegraph/hdrbackground.py +384 -0
- OpenGLContext/scenegraph/ifscompiler.py +633 -0
- OpenGLContext/scenegraph/imagetexture.py +365 -0
- OpenGLContext/scenegraph/indexedfaceset.py +238 -0
- OpenGLContext/scenegraph/indexedlineset.py +418 -0
- OpenGLContext/scenegraph/indexedpolygons.py +422 -0
- OpenGLContext/scenegraph/inline.py +79 -0
- OpenGLContext/scenegraph/instancedgl.py +187 -0
- OpenGLContext/scenegraph/interpolators.py +122 -0
- OpenGLContext/scenegraph/light.py +213 -0
- OpenGLContext/scenegraph/lod.py +22 -0
- OpenGLContext/scenegraph/material.py +96 -0
- OpenGLContext/scenegraph/material_fields.py +50 -0
- OpenGLContext/scenegraph/mouseover.py +43 -0
- OpenGLContext/scenegraph/nodepath.py +52 -0
- OpenGLContext/scenegraph/nurbs.py +637 -0
- OpenGLContext/scenegraph/nurbssampling.py +101 -0
- OpenGLContext/scenegraph/nurbstess.py +316 -0
- OpenGLContext/scenegraph/nurbstrim.py +54 -0
- OpenGLContext/scenegraph/particles.py +786 -0
- OpenGLContext/scenegraph/pbrmaterial.py +229 -0
- OpenGLContext/scenegraph/pbrmesh.py +558 -0
- OpenGLContext/scenegraph/physicsbody.py +72 -0
- OpenGLContext/scenegraph/pointset.py +257 -0
- OpenGLContext/scenegraph/polygon.py +166 -0
- OpenGLContext/scenegraph/polygonsort.py +119 -0
- OpenGLContext/scenegraph/polygontessellator.py +213 -0
- OpenGLContext/scenegraph/quadrics.py +494 -0
- OpenGLContext/scenegraph/scenegraph.py +21 -0
- OpenGLContext/scenegraph/shadergeometry.py +403 -0
- OpenGLContext/scenegraph/shaders.py +812 -0
- OpenGLContext/scenegraph/shape.py +243 -0
- OpenGLContext/scenegraph/simplebackground.py +34 -0
- OpenGLContext/scenegraph/spherebackground.py +462 -0
- OpenGLContext/scenegraph/switch.py +75 -0
- OpenGLContext/scenegraph/teapot.py +439 -0
- OpenGLContext/scenegraph/teapot_nurbs.py +291 -0
- OpenGLContext/scenegraph/teapot_nurbs_data.py +384 -0
- OpenGLContext/scenegraph/terrain/__init__.py +12 -0
- OpenGLContext/scenegraph/terrain/heightfield.py +172 -0
- OpenGLContext/scenegraph/terrain/splat.py +224 -0
- OpenGLContext/scenegraph/tessellationlod.py +97 -0
- OpenGLContext/scenegraph/text/__init__.py +1 -0
- OpenGLContext/scenegraph/text/_toolsfont.py +200 -0
- OpenGLContext/scenegraph/text/font.py +452 -0
- OpenGLContext/scenegraph/text/fontprovider.py +193 -0
- OpenGLContext/scenegraph/text/fonts/__init__.py +75 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_10.py +133 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_12.py +158 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_14.py +176 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_16.py +204 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_18.py +222 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_20.py +244 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_24.py +294 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_28.py +339 -0
- OpenGLContext/scenegraph/text/fonts/font_atlas_32.py +390 -0
- OpenGLContext/scenegraph/text/fontstyle3d.py +62 -0
- OpenGLContext/scenegraph/text/glutfont.py +186 -0
- OpenGLContext/scenegraph/text/pygamefont.py +213 -0
- OpenGLContext/scenegraph/text/shaderfont.py +340 -0
- OpenGLContext/scenegraph/text/shadertext.py +433 -0
- OpenGLContext/scenegraph/text/text.py +75 -0
- OpenGLContext/scenegraph/text/toolsfont.py +819 -0
- OpenGLContext/scenegraph/text/ttfregistry.py +135 -0
- OpenGLContext/scenegraph/text/wglfont.py +228 -0
- OpenGLContext/scenegraph/text/wglfontprovider.py +37 -0
- OpenGLContext/scenegraph/text/wxfont.py +314 -0
- OpenGLContext/scenegraph/texturetransform.py +66 -0
- OpenGLContext/scenegraph/tilesterrain.py +95 -0
- OpenGLContext/scenegraph/timesensor.py +51 -0
- OpenGLContext/scenegraph/transform.py +180 -0
- OpenGLContext/scenegraph/vegetation/__init__.py +20 -0
- OpenGLContext/scenegraph/vegetation/base.py +96 -0
- OpenGLContext/scenegraph/vegetation/billboards.py +145 -0
- OpenGLContext/scenegraph/vegetation/clumps.py +260 -0
- OpenGLContext/scenegraph/vegetation/grid.py +112 -0
- OpenGLContext/scenegraph/vegetation/nearmesh.py +185 -0
- OpenGLContext/scenegraph/vertex.py +54 -0
- OpenGLContext/scenegraph/viewpoint.py +42 -0
- OpenGLContext/scenegraph/winding.py +49 -0
- OpenGLContext/shaders/_brdf_inc.glsl +124 -0
- OpenGLContext/shaders/_common_inc.glsl +24 -0
- OpenGLContext/shaders/_cubemap_inc.glsl +26 -0
- OpenGLContext/shaders/_lights_inc.glsl +32 -0
- OpenGLContext/shaders/_objectid_inc.glsl +14 -0
- OpenGLContext/shaders/_shadow_inc.glsl +254 -0
- OpenGLContext/shaders/_vrml97_lighting_inc.glsl +78 -0
- OpenGLContext/shaders/hdr_background.frag +32 -0
- OpenGLContext/shaders/hdr_background.vert +15 -0
- OpenGLContext/shaders/ibl_brdf.frag +78 -0
- OpenGLContext/shaders/ibl_env.frag +53 -0
- OpenGLContext/shaders/ibl_equirect.frag +19 -0
- OpenGLContext/shaders/ibl_fullscreen.vert +11 -0
- OpenGLContext/shaders/ibl_irradiance.frag +40 -0
- OpenGLContext/shaders/ibl_prefilter.frag +56 -0
- OpenGLContext/shaders/particle.frag +42 -0
- OpenGLContext/shaders/particle.vert +52 -0
- OpenGLContext/shaders/pbr.frag +906 -0
- OpenGLContext/shaders/pbr.vert +70 -0
- OpenGLContext/shaders/shadow_depth.frag +4 -0
- OpenGLContext/shaders/shadow_depth.vert +18 -0
- OpenGLContext/shaders/terrain_splat.frag +80 -0
- OpenGLContext/shaders/terrain_splat.vert +16 -0
- OpenGLContext/shaders/veg_billboard.frag +35 -0
- OpenGLContext/shaders/veg_billboard.vert +17 -0
- OpenGLContext/shaders/veg_clump.frag +27 -0
- OpenGLContext/shaders/veg_mesh.frag +26 -0
- OpenGLContext/shaders/veg_mesh.vert +17 -0
- OpenGLContext/shaders/vrml97_background.frag +17 -0
- OpenGLContext/shaders/vrml97_background.vert +24 -0
- OpenGLContext/shaders/vrml97_lighting.frag +111 -0
- OpenGLContext/shaders/vrml97_lighting.vert +50 -0
- OpenGLContext/shaders/vrml97_line.frag +23 -0
- OpenGLContext/shaders/vrml97_line.vert +24 -0
- OpenGLContext/shaders/vrml97_point.frag +36 -0
- OpenGLContext/shaders/vrml97_point.vert +21 -0
- OpenGLContext/shaders/vrml97_unlit.frag +59 -0
- OpenGLContext/shaders/vrml97_unlit.vert +18 -0
- OpenGLContext/shaders/vrml97_vertex_color.frag +85 -0
- OpenGLContext/shaders/vrml97_vertex_color.vert +34 -0
- OpenGLContext/stalltrace.py +637 -0
- OpenGLContext/testing/__init__.py +67 -0
- OpenGLContext/testing/display.py +30 -0
- OpenGLContext/testing/event_injector.py +564 -0
- OpenGLContext/testing/framebuffer_comparison.py +842 -0
- OpenGLContext/testing/gl_env.py +39 -0
- OpenGLContext/testing/paths.py +23 -0
- OpenGLContext/testing/process_exit.py +30 -0
- OpenGLContext/testing/report_generator.py +594 -0
- OpenGLContext/testing/subprocess_runner.py +411 -0
- OpenGLContext/testingcontext.py +36 -0
- OpenGLContext/tests/test_atlas.py +62 -0
- OpenGLContext/tests/test_configs.py +27 -0
- OpenGLContext/tests/test_polygonsort.py +45 -0
- OpenGLContext/tests/test_utilities.py +84 -0
- OpenGLContext/tests/test_viewplatform.py +39 -0
- OpenGLContext/texture.py +449 -0
- OpenGLContext/texturecache.py +37 -0
- OpenGLContext/triangleutilities.py +89 -0
- OpenGLContext/ui/__init__.py +39 -0
- OpenGLContext/ui/bindings.py +243 -0
- OpenGLContext/ui/console.py +339 -0
- OpenGLContext/ui/debugoverlay.py +519 -0
- OpenGLContext/ui/dialogs.py +124 -0
- OpenGLContext/ui/draw.py +617 -0
- OpenGLContext/ui/gallery.py +425 -0
- OpenGLContext/ui/generate.py +195 -0
- OpenGLContext/ui/geometry.py +88 -0
- OpenGLContext/ui/hudwidgets.py +1053 -0
- OpenGLContext/ui/layout.py +263 -0
- OpenGLContext/ui/metrics.py +218 -0
- OpenGLContext/ui/overlay.py +412 -0
- OpenGLContext/ui/panel.py +459 -0
- OpenGLContext/ui/pictures.py +261 -0
- OpenGLContext/ui/screen.py +177 -0
- OpenGLContext/ui/scroll.py +297 -0
- OpenGLContext/ui/session.py +329 -0
- OpenGLContext/ui/settings.py +273 -0
- OpenGLContext/ui/skin.py +330 -0
- OpenGLContext/ui/widgets.py +1199 -0
- OpenGLContext/userpaths.py +48 -0
- OpenGLContext/utilities.py +94 -0
- OpenGLContext/vectorutilities.py +101 -0
- OpenGLContext/viewer/__init__.py +60 -0
- OpenGLContext/viewer/adapters/__init__.py +150 -0
- OpenGLContext/viewer/adapters/base.py +157 -0
- OpenGLContext/viewer/adapters/gltf.py +31 -0
- OpenGLContext/viewer/adapters/obj.py +20 -0
- OpenGLContext/viewer/adapters/scenegraph.py +71 -0
- OpenGLContext/viewer/adapters/tiles.py +296 -0
- OpenGLContext/viewer/adapters/vrml.py +25 -0
- OpenGLContext/viewer/asyncscene.py +117 -0
- OpenGLContext/viewer/caption.py +126 -0
- OpenGLContext/viewer/capture.py +77 -0
- OpenGLContext/viewer/debug.py +78 -0
- OpenGLContext/viewer/environment.py +131 -0
- OpenGLContext/viewer/framing.py +111 -0
- OpenGLContext/viewer/library.py +235 -0
- OpenGLContext/viewer/menu.py +218 -0
- OpenGLContext/viewer/options.py +150 -0
- OpenGLContext/viewer/overlay.py +63 -0
- OpenGLContext/viewer/sceneviewer.py +1056 -0
- OpenGLContext/viewer/screens.py +172 -0
- OpenGLContext/viewer/source.py +56 -0
- OpenGLContext/visitor.py +68 -0
- OpenGLContext/vrmlcontext.py +72 -0
- OpenGLContext/wxcontext.py +424 -0
- OpenGLContext/wxinteractivecontext.py +25 -0
- OpenGLContext/wxtestingcontext.py +9 -0
- OpenGLContext/wxvrmlcontext.py +13 -0
- openglcontext-3.0.0a1.dist-info/METADATA +650 -0
- openglcontext-3.0.0a1.dist-info/RECORD +417 -0
- openglcontext-3.0.0a1.dist-info/WHEEL +5 -0
- openglcontext-3.0.0a1.dist-info/entry_points.txt +11 -0
- openglcontext-3.0.0a1.dist-info/licenses/license.txt +89 -0
- openglcontext-3.0.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#! /usr/bin/env python
|
|
2
|
+
"""Render every glTF demo and diff it against a verified baseline (``oglc-gltf-regression``).
|
|
3
|
+
|
|
4
|
+
The conformance work was verified by hand: capture a demo, eyeball it against the
|
|
5
|
+
Khronos reference, move on. Nothing stored a baseline, so nothing caught a
|
|
6
|
+
*regression* when the next shader or loader change landed. This command closes
|
|
7
|
+
that loop.
|
|
8
|
+
|
|
9
|
+
For every scene in the shared roster (`OpenGLContext.loaders.gltf_demos`) and each
|
|
10
|
+
of its viewpoints it:
|
|
11
|
+
|
|
12
|
+
1. renders the model through the ``oglc-gltf`` viewer in its own hidden GL context
|
|
13
|
+
(one process per view -- the robust way to render a batch),
|
|
14
|
+
2. compares the new render against **our reference** -- the verified baseline in
|
|
15
|
+
the separate ``reference-images`` sub-repo -- and flags a regression when they
|
|
16
|
+
diverge beyond tolerance,
|
|
17
|
+
3. fetches (and disk-caches) the **upstream Khronos reference** screenshot as a
|
|
18
|
+
tertiary sanity check, and
|
|
19
|
+
4. writes an HTML report with *our-reference | new | diff | upstream* side by
|
|
20
|
+
side, click-to-zoom, and an expand-all control. The report links the on-disk
|
|
21
|
+
renders by path (it is not self-contained), keeping it small.
|
|
22
|
+
|
|
23
|
+
oglc-gltf-regression --bless # establish/refresh baselines (after review)
|
|
24
|
+
oglc-gltf-regression # exit non-zero if anything regressed
|
|
25
|
+
oglc-gltf-regression --only MetalRoughSpheres --only Duck
|
|
26
|
+
|
|
27
|
+
Downloaded ``.glb`` files and reference screenshots are cached under the shared
|
|
28
|
+
glTF cache, so reruns only re-render.
|
|
29
|
+
"""
|
|
30
|
+
import argparse
|
|
31
|
+
import hashlib
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
import urllib.parse
|
|
37
|
+
import urllib.request
|
|
38
|
+
from typing import Any, cast
|
|
39
|
+
|
|
40
|
+
from OpenGLContext import renderoptions
|
|
41
|
+
from OpenGLContext.loaders import gltf
|
|
42
|
+
from OpenGLContext.loaders import resolver
|
|
43
|
+
from OpenGLContext.loaders import gltf_demos
|
|
44
|
+
|
|
45
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
46
|
+
REPO = os.path.dirname(os.path.dirname(HERE)) # this package's parent dir
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _source_root() -> str | None:
|
|
50
|
+
"""Locate the openglcontext source checkout (dir with OpenGLContext/ + tests/).
|
|
51
|
+
|
|
52
|
+
Searched from the current directory upward, then this file's location. When
|
|
53
|
+
the package is pip-installed into a virtualenv, ``__file__`` points at
|
|
54
|
+
``site-packages`` -- deriving output paths from it drops the report inside the
|
|
55
|
+
venv. The working tree is what we want, so prefer it.
|
|
56
|
+
"""
|
|
57
|
+
def markers(d: str) -> bool:
|
|
58
|
+
return (os.path.isdir(os.path.join(d, 'tests')) and
|
|
59
|
+
os.path.isfile(os.path.join(d, 'OpenGLContext', '__init__.py')))
|
|
60
|
+
|
|
61
|
+
for start in (os.getcwd(), REPO):
|
|
62
|
+
d = start
|
|
63
|
+
while True:
|
|
64
|
+
for cand in (d, os.path.join(d, 'openglcontext')):
|
|
65
|
+
if markers(cand):
|
|
66
|
+
return cand
|
|
67
|
+
parent = os.path.dirname(d)
|
|
68
|
+
if parent == d:
|
|
69
|
+
break
|
|
70
|
+
d = parent
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def default_report_path() -> str:
|
|
75
|
+
"""Default report location: a subdirectory of the source tree's tests/."""
|
|
76
|
+
root = _source_root() or REPO
|
|
77
|
+
return os.path.join(root, 'tests', 'gltf_regression', 'gltf_regression_report.html')
|
|
78
|
+
|
|
79
|
+
# Render/capture defaults. Six frames + a short settle is the combination the
|
|
80
|
+
# conformance-capture harness proved reliable for back-to-back hidden captures.
|
|
81
|
+
DEFAULT_SIZE = (900, 640)
|
|
82
|
+
# Canonical capture params -- both the baseline bless and the conformance test
|
|
83
|
+
# render with these, so a re-render matches its baseline (a mismatch in frame count
|
|
84
|
+
# / settle delay shifts settle-sensitive scenes -- Parthenon, heavy/animated models
|
|
85
|
+
# -- past tolerance). Keep bless and test on the SAME values.
|
|
86
|
+
DEFAULT_FRAMES = 8
|
|
87
|
+
DEFAULT_DELAY = 0.5
|
|
88
|
+
# Regression tolerance: percent of pixels allowed to differ by more than
|
|
89
|
+
# DIFF_THRESHOLD per channel (the project's documented visual-regression default).
|
|
90
|
+
DEFAULT_TOLERANCE = 2.0
|
|
91
|
+
DIFF_THRESHOLD = 8
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def default_baseline_root() -> str:
|
|
95
|
+
"""Where verified 'our reference' baselines live.
|
|
96
|
+
|
|
97
|
+
``OPENGLCONTEXT_GLTF_BASELINE`` wins; otherwise the sibling ``reference-images``
|
|
98
|
+
sub-repo's ``gltf_baseline`` folder (kept out of the main code repo).
|
|
99
|
+
"""
|
|
100
|
+
env = os.environ.get('OPENGLCONTEXT_GLTF_BASELINE', '').strip()
|
|
101
|
+
if env:
|
|
102
|
+
return env
|
|
103
|
+
parent = os.path.dirname(REPO)
|
|
104
|
+
return os.path.join(parent, 'reference-images', 'gltf_baseline')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _env_prefix(name: str = 'pimbackground_') -> str | None:
|
|
108
|
+
"""Path prefix of a bundled environment cubemap face set, or None.
|
|
109
|
+
|
|
110
|
+
``pimbackground_`` is the default outdoor set; ``studio_`` is the neutral
|
|
111
|
+
studio set the Khronos material references use (bright grey backdrop +
|
|
112
|
+
softboxes) for metals/glass/anisotropy.
|
|
113
|
+
"""
|
|
114
|
+
import OpenGLContext
|
|
115
|
+
prefix = os.path.join(os.path.dirname(OpenGLContext.__file__),
|
|
116
|
+
'resources', 'environment', name)
|
|
117
|
+
return prefix if os.path.exists(prefix + 'UP.jpg') else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _env_prefix_for(spec: gltf_demos.SceneSpec) -> str | None:
|
|
121
|
+
"""The env-cubemap prefix a scene should reflect, honouring ``spec.environment``."""
|
|
122
|
+
env = getattr(spec, 'environment', None)
|
|
123
|
+
if env == 'studio':
|
|
124
|
+
return _env_prefix('studio_') or _env_prefix()
|
|
125
|
+
if env == 'studiobright': # near-white studio for showcase metals/glass
|
|
126
|
+
return _env_prefix('studiobright_') or _env_prefix('studio_') or _env_prefix()
|
|
127
|
+
return _env_prefix()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# The procedural studio cube is smooth and low-detail, so even a mirror metal
|
|
131
|
+
# reflects it softly -- unlike the Khronos references, which are shot against a
|
|
132
|
+
# real HDR studio. Map the studio keywords to a CC0 Poly Haven HDR panorama the
|
|
133
|
+
# viewer loads as both the IBL probe and the skybox, so metals get sharp, detailed
|
|
134
|
+
# reflections. Non-keyword ``environment`` values are treated as an explicit HDR
|
|
135
|
+
# (a Poly Haven catalogue name or an .hdr URL/path) and passed straight through.
|
|
136
|
+
_STUDIO_HDR = 'brown_photostudio_02'
|
|
137
|
+
_PROCEDURAL_ENVS = (None, 'studio', 'studiobright')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _environment_for(spec: gltf_demos.SceneSpec) -> tuple[Any, str]:
|
|
141
|
+
"""Return ``(--environment value, --ibl-intensity)`` for a cube-background scene.
|
|
142
|
+
|
|
143
|
+
``'studio'``/``'studiobright'`` resolve to the HDR studio panorama; ``None``
|
|
144
|
+
keeps the bundled procedural outdoor cube; any other value is an explicit HDR
|
|
145
|
+
source (catalogue name or URL) reflected and drawn as the skybox.
|
|
146
|
+
"""
|
|
147
|
+
env = getattr(spec, 'environment', None)
|
|
148
|
+
if env in ('studio', 'studiobright'):
|
|
149
|
+
return _STUDIO_HDR, '1.0'
|
|
150
|
+
if env == 'procedural_studio':
|
|
151
|
+
# The bundled neutral-grey procedural studio (dim, even). For a *subtle*
|
|
152
|
+
# effect whose colour a bright HDR would tonemap to white -- dielectric
|
|
153
|
+
# thin-film iridescence -- against which the pale tint actually reads, as
|
|
154
|
+
# in the Khronos neutral-grey reference.
|
|
155
|
+
return (_env_prefix('studio_') or _env_prefix()), '0.7'
|
|
156
|
+
if env is None:
|
|
157
|
+
return _env_prefix_for(spec), '0.9'
|
|
158
|
+
return env, '1.0'
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------------- #
|
|
162
|
+
# Model resolution (cached): local path the viewer can load.
|
|
163
|
+
# --------------------------------------------------------------------------- #
|
|
164
|
+
_MIRROR_DIR = os.path.join(resolver._default_cache_dir(), 'gltf_mirror')
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _cached_url_path(url: str, cache_dir: str) -> str:
|
|
168
|
+
"""Fetch ``url`` into the sha1-keyed cache and return its local path."""
|
|
169
|
+
resolver._fetch_url(url, cache_dir)
|
|
170
|
+
key = hashlib.sha1(url.encode('utf-8')).hexdigest() + os.path.splitext(url)[1]
|
|
171
|
+
return os.path.join(cache_dir, key)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _dl(url: str, dst: str) -> None:
|
|
175
|
+
"""Download ``url`` to ``dst`` (once), preserving the on-disk layout so a
|
|
176
|
+
``.gltf``'s relative buffer/image URIs resolve when the viewer loads it."""
|
|
177
|
+
if os.path.exists(dst) and os.path.getsize(dst) > 0:
|
|
178
|
+
return
|
|
179
|
+
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
180
|
+
with urllib.request.urlopen(resolver.safe_url(url), timeout=60) as r, \
|
|
181
|
+
open(dst, 'wb') as f:
|
|
182
|
+
f.write(r.read())
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resolve_model(spec: gltf_demos.SceneSpec, parthenon: str | None = None) -> str | None:
|
|
186
|
+
"""Return a local, loadable path for a scene's model (or None if unavailable).
|
|
187
|
+
|
|
188
|
+
Khronos samples resolve to the cached ``.glb``; a sample published only as
|
|
189
|
+
``.gltf`` (EnvironmentTest, IridescenceMetallicSpheres, TextureTransformTest)
|
|
190
|
+
is mirrored -- the ``.gltf`` plus its external buffers/images -- into the
|
|
191
|
+
cache so the viewer can load it. A local ``source`` (Parthenon) is used
|
|
192
|
+
as-is, and a scene named by URL is fetched into the cache.
|
|
193
|
+
"""
|
|
194
|
+
source, is_local = gltf_demos.resolve_source(spec, parthenon)
|
|
195
|
+
if is_local:
|
|
196
|
+
return source if source and os.path.exists(source) else None
|
|
197
|
+
cache_dir = resolver._default_cache_dir()
|
|
198
|
+
if resolver.is_url(source):
|
|
199
|
+
# One published file, so a failed fetch is the end of it: the .gltf
|
|
200
|
+
# mirror below only makes sense for the catalogue's directory layout.
|
|
201
|
+
try:
|
|
202
|
+
return _cached_url_path(cast(str, source), cache_dir)
|
|
203
|
+
except Exception as err:
|
|
204
|
+
print(' resolve FAILED %s: %s' % (spec.name, err))
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
name = cast(str, source) # a non-local scene resolves to a sample name
|
|
208
|
+
glb_url = gltf.sample_model_url(name)
|
|
209
|
+
try:
|
|
210
|
+
return _cached_url_path(glb_url, cache_dir)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
# Fall back to the external .gltf: mirror it plus its referenced URIs.
|
|
214
|
+
base = '%s/%s' % (gltf.SAMPLE_MODELS_BASE, name)
|
|
215
|
+
gdir = os.path.join(_MIRROR_DIR, name)
|
|
216
|
+
local_gltf = os.path.join(gdir, '%s.gltf' % name)
|
|
217
|
+
try:
|
|
218
|
+
_dl('%s/glTF/%s.gltf' % (base, name), local_gltf)
|
|
219
|
+
doc = json.load(open(local_gltf))
|
|
220
|
+
for coll in ('buffers', 'images'):
|
|
221
|
+
for item in doc.get(coll, []):
|
|
222
|
+
uri = item.get('uri')
|
|
223
|
+
if not uri or uri.startswith('data:'):
|
|
224
|
+
continue
|
|
225
|
+
rel = urllib.parse.unquote(uri)
|
|
226
|
+
_dl('%s/glTF/%s' % (base, uri), os.path.join(gdir, rel))
|
|
227
|
+
return local_gltf
|
|
228
|
+
except Exception as err:
|
|
229
|
+
print(' resolve FAILED %s: %s' % (name, err))
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# --------------------------------------------------------------------------- #
|
|
234
|
+
# Render one view through the viewer CLI (its own hidden GL context).
|
|
235
|
+
# --------------------------------------------------------------------------- #
|
|
236
|
+
def render_view(spec: gltf_demos.SceneSpec, camera: int | None, model: str, out: str,
|
|
237
|
+
size: tuple[int, int], frames: int, delay: float,
|
|
238
|
+
env_prefix: str | None) -> tuple[bool, dict[str, Any]]:
|
|
239
|
+
"""Render one scene/camera to ``out``. Returns ``(ok, stats)`` where ``stats``
|
|
240
|
+
holds the viewer-reported ``load_seconds``/``fps`` for the run (empty on failure)."""
|
|
241
|
+
w, h = size
|
|
242
|
+
common = ['--no-physics', '--no-shadows', '--capture', out,
|
|
243
|
+
'--frames', str(frames), '--capture-delay', repr(delay),
|
|
244
|
+
'--size', '%dx%d' % (w, h), '--background', spec.capture_background]
|
|
245
|
+
if camera is None:
|
|
246
|
+
args = [model, '--no-cameras', '--no-rotate',
|
|
247
|
+
'--yaw', repr(spec.yaw), '--elevation', repr(spec.elevation),
|
|
248
|
+
'--tilt', repr(spec.tilt), '--margin', repr(spec.margin)] + common
|
|
249
|
+
if spec.eye is not None and spec.look_at is not None:
|
|
250
|
+
# ``=`` form so a negative coordinate isn't parsed as another option.
|
|
251
|
+
args += ['--eye=%s' % ','.join(repr(v) for v in spec.eye),
|
|
252
|
+
'--look-at=%s' % ','.join(repr(v) for v in spec.look_at)]
|
|
253
|
+
else:
|
|
254
|
+
# Baked camera: adopt the authored pose, don't re-frame or spin.
|
|
255
|
+
args = [model, '--camera', str(camera), '--no-rotate'] + common
|
|
256
|
+
if spec.capture_background == 'cube':
|
|
257
|
+
env_arg, ibl = _environment_for(spec)
|
|
258
|
+
if env_arg:
|
|
259
|
+
args += ['--environment', env_arg, '--ibl-intensity', ibl]
|
|
260
|
+
# Pin animated models to a fixed time so the captured pose is reproducible
|
|
261
|
+
# (otherwise the frame lands at whatever animation time the settle reached).
|
|
262
|
+
if spec.anim_time is not None:
|
|
263
|
+
args += ['--anim-time', repr(float(spec.anim_time))]
|
|
264
|
+
|
|
265
|
+
# Started from a *clean* rendering environment rather than from whatever
|
|
266
|
+
# this process happens to be carrying: a baseline comparison whose result
|
|
267
|
+
# depends on which variables were set before it is not a baseline. See
|
|
268
|
+
# `renderoptions.clean_environment`.
|
|
269
|
+
env = renderoptions.clean_environment(
|
|
270
|
+
OPENGLCONTEXT_HIDDEN='1', OPENGLCONTEXT_NO_VSYNC='1',
|
|
271
|
+
OPENGLCONTEXT_PROFILE='core', OPENGLCONTEXT_BACKEND='glfw',
|
|
272
|
+
OPENGLCONTEXT_SHADOW_CASCADES='3',
|
|
273
|
+
OPENGLCONTEXT_BLOOM='1' if getattr(spec, 'bloom', False) else '0',
|
|
274
|
+
OPENGLCONTEXT_DISABLE_FPS_DISPLAY='1')
|
|
275
|
+
try:
|
|
276
|
+
proc = subprocess.run(
|
|
277
|
+
[sys.executable, '-m', 'OpenGLContext.bin.view'] + args,
|
|
278
|
+
timeout=120, capture_output=True, text=True,
|
|
279
|
+
cwd=_source_root() or REPO, env=env)
|
|
280
|
+
except subprocess.TimeoutExpired:
|
|
281
|
+
print(' render TIMEOUT')
|
|
282
|
+
return False, {}
|
|
283
|
+
stats = _parse_capture_stats(proc.stdout)
|
|
284
|
+
return os.path.exists(out), stats
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _parse_capture_stats(stdout: str | None) -> dict[str, Any]:
|
|
288
|
+
"""Pull ``load_seconds``/``fps`` from the viewer's ``CAPTURE_STATS`` line."""
|
|
289
|
+
stats: dict[str, Any] = {}
|
|
290
|
+
for line in (stdout or '').splitlines():
|
|
291
|
+
if line.startswith('CAPTURE_STATS'):
|
|
292
|
+
for tok in line.split()[1:]:
|
|
293
|
+
key, _, val = tok.partition('=')
|
|
294
|
+
try:
|
|
295
|
+
stats[key] = float(val) if val not in ('', 'None') else None
|
|
296
|
+
except ValueError:
|
|
297
|
+
stats[key] = val or None
|
|
298
|
+
return stats
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# --------------------------------------------------------------------------- #
|
|
302
|
+
# Compare new render vs baseline.
|
|
303
|
+
# --------------------------------------------------------------------------- #
|
|
304
|
+
def compare(baseline_path: str, new_path: str, diff_out: str,
|
|
305
|
+
threshold: int) -> tuple[Any, dict[str, Any]]:
|
|
306
|
+
"""Compare two PNGs. Returns (ComparisonResult, stats_dict) or (None, None)."""
|
|
307
|
+
import numpy as np
|
|
308
|
+
from PIL import Image
|
|
309
|
+
from OpenGLContext.testing.framebuffer_comparison import ComparisonResult
|
|
310
|
+
|
|
311
|
+
base = np.asarray(Image.open(baseline_path).convert('RGB'))
|
|
312
|
+
new = np.asarray(Image.open(new_path).convert('RGB'))
|
|
313
|
+
result = ComparisonResult(base, new, threshold)
|
|
314
|
+
if result.diff_image is not None:
|
|
315
|
+
Image.fromarray(result.diff_image, 'RGB').save(diff_out)
|
|
316
|
+
stats = {
|
|
317
|
+
'shapes_match': 'Yes' if result.shapes_match else 'No',
|
|
318
|
+
'max_diff': result.max_diff,
|
|
319
|
+
'mean_diff': result.mean_diff,
|
|
320
|
+
'percent_different': result.percent_different,
|
|
321
|
+
'pixels_different': result.pixels_different,
|
|
322
|
+
'total_pixels': result.total_pixels,
|
|
323
|
+
}
|
|
324
|
+
return result, stats
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def is_regression(result: Any, tolerance: float) -> bool:
|
|
328
|
+
"""A regression is a shape change or more than ``tolerance`` percent of pixels
|
|
329
|
+
differing beyond the per-channel threshold."""
|
|
330
|
+
if not result.shapes_match:
|
|
331
|
+
return True
|
|
332
|
+
return result.percent_different > tolerance
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# --------------------------------------------------------------------------- #
|
|
336
|
+
# Driver.
|
|
337
|
+
# --------------------------------------------------------------------------- #
|
|
338
|
+
def select_scenes(only: list[str] | None) -> list[gltf_demos.SceneSpec]:
|
|
339
|
+
"""The scenes to run: all, or the subset named in ``only`` (case-sensitive)."""
|
|
340
|
+
if not only:
|
|
341
|
+
return list(gltf_demos.iter_scenes())
|
|
342
|
+
wanted = set(only)
|
|
343
|
+
return [s for s in gltf_demos.iter_scenes() if s.name in wanted]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _gl_renderer() -> dict[str, str]:
|
|
347
|
+
"""The GPU's GL_RENDERER/GL_VERSION, queried in a throwaway hidden context."""
|
|
348
|
+
code = ("import glfw;glfw.init();glfw.window_hint(glfw.VISIBLE,glfw.FALSE);"
|
|
349
|
+
"w=glfw.create_window(8,8,'r',None,None);glfw.make_context_current(w);"
|
|
350
|
+
"from OpenGL.GL import glGetString,GL_RENDERER,GL_VERSION;"
|
|
351
|
+
"print((glGetString(GL_RENDERER) or b'').decode());"
|
|
352
|
+
"print((glGetString(GL_VERSION) or b'').decode())")
|
|
353
|
+
try:
|
|
354
|
+
out = subprocess.run(
|
|
355
|
+
[sys.executable, '-c', code], capture_output=True, text=True, timeout=30,
|
|
356
|
+
env=dict(os.environ, OPENGLCONTEXT_HIDDEN='1', OPENGLCONTEXT_BACKEND='glfw'),
|
|
357
|
+
).stdout.splitlines()
|
|
358
|
+
return {'gl_renderer': (out[0].strip() if out else '') or 'unknown',
|
|
359
|
+
'gl_version': out[1].strip() if len(out) > 1 else ''}
|
|
360
|
+
except Exception:
|
|
361
|
+
return {'gl_renderer': 'unknown', 'gl_version': ''}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _provenance() -> dict[str, str]:
|
|
365
|
+
"""Git hash (+``-dirty``), timestamp and GPU identity, stamped into every
|
|
366
|
+
render's metadata so a baseline records exactly how (and on what) it was made."""
|
|
367
|
+
import datetime
|
|
368
|
+
root = _source_root() or REPO
|
|
369
|
+
git = 'unknown'
|
|
370
|
+
try:
|
|
371
|
+
rev = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'], cwd=root,
|
|
372
|
+
capture_output=True, text=True)
|
|
373
|
+
dirty = subprocess.run(['git', 'status', '--porcelain'], cwd=root,
|
|
374
|
+
capture_output=True, text=True)
|
|
375
|
+
if rev.returncode == 0 and rev.stdout.strip():
|
|
376
|
+
git = rev.stdout.strip() + ('-dirty' if dirty.stdout.strip() else '')
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
prov = {'git': git,
|
|
380
|
+
'rendered_at': datetime.datetime.now().isoformat(timespec='seconds')}
|
|
381
|
+
prov.update(_gl_renderer())
|
|
382
|
+
return prov
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _view_metadata(spec: gltf_demos.SceneSpec, camera: int | None, url: str | None,
|
|
386
|
+
args: argparse.Namespace, provenance: dict[str, str],
|
|
387
|
+
stats: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
388
|
+
"""The per-view render metadata recorded to a sidecar JSON and shown in the
|
|
389
|
+
report: what was rendered, how it was framed/lit, and the run provenance.
|
|
390
|
+
|
|
391
|
+
``url`` is the ORIGINAL source the viewer loaded -- the remote http(s) URL for a
|
|
392
|
+
Khronos sample (loaded as an untrusted resource through the resolver), not a
|
|
393
|
+
local cache path -- so the record reflects the real load context. ``stats``
|
|
394
|
+
carries the viewer-reported load time / fps for this render."""
|
|
395
|
+
env_arg = ibl = None
|
|
396
|
+
if spec.capture_background == 'cube':
|
|
397
|
+
env_arg, ibl = _environment_for(spec)
|
|
398
|
+
stats = stats or {}
|
|
399
|
+
return {
|
|
400
|
+
'scene': spec.name,
|
|
401
|
+
'slug': spec.slug(camera),
|
|
402
|
+
'camera': camera,
|
|
403
|
+
'url': url,
|
|
404
|
+
'load_context': 'url' if isinstance(url, str) and url.startswith(('http://', 'https://')) else 'local',
|
|
405
|
+
'load_seconds': stats.get('load_seconds'),
|
|
406
|
+
'fps': stats.get('fps'),
|
|
407
|
+
'background': spec.capture_background,
|
|
408
|
+
'environment': env_arg,
|
|
409
|
+
'ibl_intensity': ibl,
|
|
410
|
+
'framing': ({'eye': list(spec.eye), 'look_at': list(spec.look_at)}
|
|
411
|
+
if spec.eye is not None and spec.look_at is not None
|
|
412
|
+
else {'yaw': spec.yaw, 'elevation': spec.elevation,
|
|
413
|
+
'tilt': spec.tilt, 'margin': spec.margin}),
|
|
414
|
+
'anim_time': spec.anim_time,
|
|
415
|
+
'frames': args.frames,
|
|
416
|
+
'delay': args.delay,
|
|
417
|
+
'size': list(args.size),
|
|
418
|
+
'git': provenance['git'],
|
|
419
|
+
'rendered_at': provenance['rendered_at'],
|
|
420
|
+
'gl_renderer': provenance['gl_renderer'],
|
|
421
|
+
'gl_version': provenance['gl_version'],
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def default_out_dir() -> str:
|
|
426
|
+
"""Persistent directory of the latest per-view renders/metadata/diffs. Kept
|
|
427
|
+
across runs so re-rendering one scene (``--only``) leaves the others intact;
|
|
428
|
+
the report is assembled from whatever it holds."""
|
|
429
|
+
return os.path.join(os.path.dirname(default_report_path()), 'renders')
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def resolve_model_url(spec: gltf_demos.SceneSpec,
|
|
433
|
+
parthenon: str | None = None) -> tuple[str | None, bool]:
|
|
434
|
+
"""The source the viewer should load, and whether it is a URL.
|
|
435
|
+
|
|
436
|
+
A Khronos sample resolves to its **http(s) URL** so the viewer loads it through
|
|
437
|
+
the security-hardened resolver -- an untrusted remote resource, exactly as an
|
|
438
|
+
end user would -- rather than as a trusted local file. The ``.glb`` variant is
|
|
439
|
+
used when it exists (probed via the shared cache), else the multi-file ``.gltf``
|
|
440
|
+
URL, whose external buffers/images the resolver fetches same-origin. A scene
|
|
441
|
+
that names its own URL is handed over as it stands, and the local Parthenon
|
|
442
|
+
build stays a path. Returns ``(source, is_url)`` or ``(None, False)``.
|
|
443
|
+
"""
|
|
444
|
+
source, is_local = gltf_demos.resolve_source(spec, parthenon)
|
|
445
|
+
if is_local:
|
|
446
|
+
return (source if source and os.path.exists(source) else None), False
|
|
447
|
+
if resolver.is_url(source):
|
|
448
|
+
return source, True
|
|
449
|
+
name = cast(str, source) # a non-local scene resolves to a sample name
|
|
450
|
+
glb_url = gltf.sample_model_url(name)
|
|
451
|
+
try:
|
|
452
|
+
_cached_url_path(glb_url, resolver._default_cache_dir()) # confirm it exists
|
|
453
|
+
return glb_url, True
|
|
454
|
+
except Exception:
|
|
455
|
+
return '%s/%s/glTF/%s.gltf' % (gltf.SAMPLE_MODELS_BASE, name, name), True
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def render_scenes(scenes: list[gltf_demos.SceneSpec], out_dir: str, baseline_root: str,
|
|
459
|
+
parthenon: str | None, args: argparse.Namespace,
|
|
460
|
+
provenance: dict[str, str]) -> None:
|
|
461
|
+
"""Render each selected view to ``out_dir`` (``<slug>.png`` + ``<slug>.json``),
|
|
462
|
+
blessing to ``baseline_root`` when asked. Overwrites only the rendered views, so
|
|
463
|
+
prior renders of other scenes survive for the report."""
|
|
464
|
+
import shutil
|
|
465
|
+
bless_names = None if args.bless in (None, []) else set(args.bless)
|
|
466
|
+
bless_all = args.bless == []
|
|
467
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
468
|
+
if args.bless is not None:
|
|
469
|
+
os.makedirs(baseline_root, exist_ok=True)
|
|
470
|
+
for spec in scenes:
|
|
471
|
+
url, _is_url = resolve_model_url(spec, parthenon)
|
|
472
|
+
for camera in spec.camera_ids():
|
|
473
|
+
slug = spec.slug(camera)
|
|
474
|
+
new_path = os.path.join(out_dir, slug + '.png')
|
|
475
|
+
if url is None:
|
|
476
|
+
print(' ERROR %s (model unavailable)' % slug)
|
|
477
|
+
continue
|
|
478
|
+
ok, stats = render_view(spec, camera, url, new_path, args.size,
|
|
479
|
+
args.frames, args.delay, None)
|
|
480
|
+
if not ok:
|
|
481
|
+
print(' FAIL %s (render failed)' % slug)
|
|
482
|
+
continue
|
|
483
|
+
metadata = _view_metadata(spec, camera, url, args, provenance, stats)
|
|
484
|
+
do_bless = bless_all or (bless_names and spec.name in bless_names)
|
|
485
|
+
try:
|
|
486
|
+
with open(os.path.join(out_dir, slug + '.json'), 'w') as fh:
|
|
487
|
+
json.dump(metadata, fh, indent=2)
|
|
488
|
+
if do_bless:
|
|
489
|
+
shutil.copyfile(new_path, os.path.join(baseline_root, slug + '.png'))
|
|
490
|
+
with open(os.path.join(baseline_root, slug + '.json'), 'w') as fh:
|
|
491
|
+
json.dump(metadata, fh, indent=2)
|
|
492
|
+
except OSError:
|
|
493
|
+
pass
|
|
494
|
+
print(' %-5s %s' % ('BLESS' if do_bless else 'OK', slug))
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def build_report(out_dir: str, baseline_root: str, report_path: str, tolerance: float,
|
|
498
|
+
diff_threshold: int) -> int:
|
|
499
|
+
"""Assemble the HTML report from whatever renders are present in ``out_dir``
|
|
500
|
+
(each ``<slug>.json`` + ``<slug>.png``), diffed against the baseline. Decoupled
|
|
501
|
+
from rendering, so re-rendering one scene then rebuilding keeps every other row.
|
|
502
|
+
Returns the regression count."""
|
|
503
|
+
import shutil
|
|
504
|
+
from OpenGLContext.testing.report_generator import TestReportGenerator
|
|
505
|
+
gen = TestReportGenerator(title='glTF Demo Regression -- our reference vs new')
|
|
506
|
+
report_dir = os.path.dirname(os.path.abspath(report_path))
|
|
507
|
+
|
|
508
|
+
def stage(src: str | None, name: str) -> str | None:
|
|
509
|
+
"""Return an image for the report to link, guaranteeing it lives under the
|
|
510
|
+
report directory. The result and diff already do; the baseline and Khronos
|
|
511
|
+
upstream live in other trees, so copy those in under ``name`` -- then the
|
|
512
|
+
report (which links images relative to itself) copies elsewhere as one
|
|
513
|
+
self-contained bundle. Missing sources return None."""
|
|
514
|
+
if not (src and os.path.exists(src)):
|
|
515
|
+
return None
|
|
516
|
+
if os.path.abspath(src).startswith(report_dir + os.sep):
|
|
517
|
+
return src
|
|
518
|
+
dst = os.path.join(out_dir, name)
|
|
519
|
+
try:
|
|
520
|
+
shutil.copyfile(src, dst)
|
|
521
|
+
return dst
|
|
522
|
+
except OSError:
|
|
523
|
+
return src
|
|
524
|
+
|
|
525
|
+
regressions = 0
|
|
526
|
+
slugs = sorted(f[:-5] for f in os.listdir(out_dir) if f.endswith('.json'))
|
|
527
|
+
for slug in slugs:
|
|
528
|
+
try:
|
|
529
|
+
meta = json.load(open(os.path.join(out_dir, slug + '.json')))
|
|
530
|
+
except Exception:
|
|
531
|
+
meta = {}
|
|
532
|
+
scene = meta.get('scene', slug.split('__')[0])
|
|
533
|
+
new_path = os.path.join(out_dir, slug + '.png')
|
|
534
|
+
baseline_path = os.path.join(baseline_root, slug + '.png')
|
|
535
|
+
diff_path = os.path.join(out_dir, slug + '_diff.png')
|
|
536
|
+
upstream_path = None
|
|
537
|
+
try:
|
|
538
|
+
upstream_path = gltf.cache_reference_screenshot(scene)
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
status, stats, stderr = 'pass', None, ''
|
|
542
|
+
if not os.path.exists(new_path):
|
|
543
|
+
status, stderr = 'error', 'no render on disk'
|
|
544
|
+
elif not os.path.exists(baseline_path):
|
|
545
|
+
status, stderr = 'skip', 'no baseline -- bless after reviewing'
|
|
546
|
+
else:
|
|
547
|
+
result, stats = compare(baseline_path, new_path, diff_path, diff_threshold)
|
|
548
|
+
if is_regression(result, tolerance):
|
|
549
|
+
status, regressions = 'fail', regressions + 1
|
|
550
|
+
stderr = 'REGRESSION: %s' % result
|
|
551
|
+
else:
|
|
552
|
+
stderr = str(result)
|
|
553
|
+
gen.add_test({
|
|
554
|
+
'test_name': '%s -- %s' % (slug, meta.get('description', '')),
|
|
555
|
+
'status': status,
|
|
556
|
+
'reference_image': stage(baseline_path, slug + '_baseline.png'),
|
|
557
|
+
'result_image': new_path if os.path.exists(new_path) else None,
|
|
558
|
+
'diff_image': diff_path if (stats and os.path.exists(diff_path)) else None,
|
|
559
|
+
'upstream_image': stage(upstream_path, slug + '_upstream.png'),
|
|
560
|
+
'comparison_stats': stats,
|
|
561
|
+
'metadata': meta,
|
|
562
|
+
'stderr': stderr,
|
|
563
|
+
'duration': 0.0,
|
|
564
|
+
})
|
|
565
|
+
os.makedirs(os.path.dirname(os.path.abspath(report_path)), exist_ok=True)
|
|
566
|
+
# Link the renders rather than base64-embedding them: a run covers dozens of
|
|
567
|
+
# views x four images, and embedding balloons the single HTML to hundreds of
|
|
568
|
+
# MB. save() links each image relative to the report; with every image staged
|
|
569
|
+
# under out_dir, the report plus its render directory copy elsewhere as one
|
|
570
|
+
# self-contained bundle (as tests/report.html links its images).
|
|
571
|
+
gen.save(report_path, embed_images=False)
|
|
572
|
+
return regressions
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def run(args: argparse.Namespace) -> int:
|
|
576
|
+
baseline_root = args.baseline_root or default_baseline_root()
|
|
577
|
+
out_dir = args.out_dir or default_out_dir()
|
|
578
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
579
|
+
parthenon = args.parthenon or gltf_demos.find_parthenon()
|
|
580
|
+
report = args.report or default_report_path()
|
|
581
|
+
|
|
582
|
+
if not getattr(args, 'report_only', False):
|
|
583
|
+
provenance = _provenance()
|
|
584
|
+
print('Baselines: %s' % baseline_root)
|
|
585
|
+
print('Renders: %s' % out_dir)
|
|
586
|
+
print('Provenance: git %s, %s, %s' % (
|
|
587
|
+
provenance['git'], provenance['rendered_at'], provenance['gl_renderer']))
|
|
588
|
+
render_scenes(select_scenes(args.only), out_dir, baseline_root,
|
|
589
|
+
parthenon, args, provenance)
|
|
590
|
+
|
|
591
|
+
regressions = build_report(out_dir, baseline_root, report,
|
|
592
|
+
args.tolerance, args.diff_threshold)
|
|
593
|
+
print('\nReport: %s' % report)
|
|
594
|
+
if regressions:
|
|
595
|
+
print('%d regression(s) detected.' % regressions)
|
|
596
|
+
return 1 if regressions else 0
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _parse_size(text: str) -> tuple[int, int]:
|
|
600
|
+
w, _, h = text.lower().partition('x')
|
|
601
|
+
return (int(w), int(h))
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def build_parser(prog: str = 'oglc-gltf-regression') -> argparse.ArgumentParser:
|
|
605
|
+
p = argparse.ArgumentParser(prog=prog, description=__doc__,
|
|
606
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
607
|
+
p.add_argument('--bless', nargs='*', metavar='NAME', default=None,
|
|
608
|
+
help='promote the new renders to baselines (after review). With '
|
|
609
|
+
'no NAME, blesses every scene run this pass; otherwise only '
|
|
610
|
+
'the named scenes.')
|
|
611
|
+
p.add_argument('--only', action='append', metavar='NAME',
|
|
612
|
+
help='render only this scene (repeatable)')
|
|
613
|
+
p.add_argument('--baseline-root', metavar='DIR',
|
|
614
|
+
help='baseline directory (default: sibling reference-images/gltf_baseline '
|
|
615
|
+
'or $OPENGLCONTEXT_GLTF_BASELINE)')
|
|
616
|
+
p.add_argument('--parthenon', metavar='GLB',
|
|
617
|
+
help='path to a local Parthenon .glb (default: auto-detect sibling)')
|
|
618
|
+
p.add_argument('--report', metavar='HTML', help='report output path')
|
|
619
|
+
p.add_argument('--out-dir', metavar='DIR',
|
|
620
|
+
help='persistent directory of per-view renders/metadata/diffs '
|
|
621
|
+
'(default: tests/gltf_regression/renders)')
|
|
622
|
+
p.add_argument('--report-only', action='store_true',
|
|
623
|
+
help='rebuild the report from the existing renders directory '
|
|
624
|
+
'without rendering anything')
|
|
625
|
+
p.add_argument('--size', type=_parse_size, default=DEFAULT_SIZE, metavar='WxH',
|
|
626
|
+
help='render size (default %dx%d)' % DEFAULT_SIZE)
|
|
627
|
+
p.add_argument('--frames', type=int, default=DEFAULT_FRAMES,
|
|
628
|
+
help='frames to render before capture (default %d)' % DEFAULT_FRAMES)
|
|
629
|
+
p.add_argument('--delay', type=float, default=DEFAULT_DELAY,
|
|
630
|
+
help='settle seconds before capture (default %.2f)' % DEFAULT_DELAY)
|
|
631
|
+
p.add_argument('--tolerance', type=float, default=DEFAULT_TOLERANCE, metavar='PCT',
|
|
632
|
+
help='percent of pixels allowed to differ (default %.1f)' % DEFAULT_TOLERANCE)
|
|
633
|
+
p.add_argument('--diff-threshold', type=int, default=DIFF_THRESHOLD, metavar='N',
|
|
634
|
+
help='per-channel delta a pixel must exceed to count as different '
|
|
635
|
+
'(default %d)' % DIFF_THRESHOLD)
|
|
636
|
+
return p
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def main(argv: list[str] | None = None) -> int:
|
|
640
|
+
args = build_parser().parse_args(argv)
|
|
641
|
+
return run(args)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
if __name__ == '__main__': # pragma: no cover - CLI entry point
|
|
645
|
+
sys.exit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#! /usr/bin/env python
|
|
2
|
+
"""``oglc-gltf`` -- the former glTF-only viewer, now an alias for ``oglc-view``.
|
|
3
|
+
|
|
4
|
+
There is one viewer, and it works out the format from the source, so the command
|
|
5
|
+
that opens a ``.glb`` is the command that opens a ``.wrl``. Every option this
|
|
6
|
+
command took is still accepted, unchanged; see :mod:`OpenGLContext.bin.view`.
|
|
7
|
+
|
|
8
|
+
This name is kept for one release cycle so existing scripts and documentation
|
|
9
|
+
keep working, and prints where to go instead. It will then be removed.
|
|
10
|
+
"""
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from OpenGLContext.bin.view import main as view_main
|
|
15
|
+
|
|
16
|
+
#: What to type now. Named here so the notice and the replacement cannot drift.
|
|
17
|
+
REPLACEMENT = 'oglc-view'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: Optional[list] = None) -> Any:
|
|
21
|
+
"""Run ``oglc-view``, having said that is what this now is."""
|
|
22
|
+
sys.stderr.write(
|
|
23
|
+
"oglc-gltf is deprecated and will be removed; use %s instead "
|
|
24
|
+
"(same options, and it opens every format).\n" % REPLACEMENT)
|
|
25
|
+
sys.stderr.flush()
|
|
26
|
+
return view_main(argv, prog='oglc-gltf')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__": # pragma: no cover - CLI entry point
|
|
30
|
+
main()
|