├── .gitignore
├── LICENSE
├── README.md
├── setup.cfg
└── xr_examples
├── __init__.py
├── api_dump_layer.py
├── color_cube.py
├── debug_all_the_things.py
├── extension_properties.py
├── gl_example.py
├── green_blue.py
├── headless.py
├── hello_xr.py
├── hello_xr
├── __init__.py
├── geometry.py
├── graphics_plugin.py
├── graphics_plugin_opengl.py
├── linear.py
├── main.py
├── openxr_program.py
├── options.py
├── platform_plugin.py
├── platform_plugin_win32.py
└── platform_plugin_xlib.py
├── openxr_version.py
├── pink_world.py
├── runtime_name.py
├── track_controller.py
├── track_hmd.py
├── track_hmd2.py
├── vive_tracker.py
└── where_does_it_hang.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Pycharm
132 | /.idea
133 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pyopenxr_examples
2 |
3 | Sample programs using the pyopenxr python AR/VR bindings from https://github.com/cmbruns/pyopenxr
4 |
5 | 1. `hello_xr.py`: This is a faithful translation into python of the hello_xr example from https://github.com/KhronosGroup/OpenXR-SDK-Source/tree/main/src/tests/hello_xr . Watch the colorful cubes attached to various spatial frames, and squeeze the controller triggers to experience an effect. This is a multi-source-file example, because the original C source also spans multiple files.
6 | 2. `color_cube.py`: This is a one-file example showing a large cube on the floor in the center of your VR space. This example uses a high-level `xr.ContextObject` instance to help keep the example compact.
7 | 3. `debug_all_the_things.py`: Combines seven different ways to add logging messages to help debug `pyopenxr` programs.
8 | 4. `pink_world.py`: The simplest opengl rendering example. The whole universe is a uniform pink.
9 | 5. `track_hmd2.py` : Prints the location of the headset to the console.
10 | 6. `track_controller.py`: Prints the location of the motion controllers to the console.
11 | 7. `vive_tracker.py` : Print the locations of Vive Tracker devices. But not if they are assigned "handheld_object" role for some reason.
12 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = pyopenxr_examples
3 | version = 0.5
4 | author = Christopher M. Bruns and others
5 | author_email = cmbruns@rotatingpenguin.com
6 | description = Example programs using pyopenxr VR/AR API
7 | url = https://github.com/cmbruns/pyopenxr_examples
8 | project_urls =
9 | Bug Tracker = https://github.com/cmbruns/pyopenxr_examples/issues
10 | classifiers =
11 | Intended Audience :: Developers
12 | Intended Audience :: Science/Research
13 | License :: OSI Approved :: Apache Software License
14 | Operating System :: Microsoft :: Windows
15 | Operating System :: POSIX :: Linux
16 | Programming Language :: Python :: 3 :: Only
17 | Programming Language :: Python :: Implementation :: CPython
18 | Topic :: Multimedia :: Graphics :: 3D Rendering
19 | Topic :: Scientific/Engineering :: Visualization
20 | Development Status :: 4 - Beta
21 |
22 | [options.entry_points]
23 | console_scripts =
24 | hello_xr = xr_examples:hello_xr:main
25 |
26 | [options]
27 | install_requires =
28 | pyopenxr
29 | numpy
30 | PyOpenGL
31 | glfw
32 | package_dir =
33 | =.
34 | packages = xr_examples
35 | ; 3.5 does not support f-strings
36 | python_requires = >=3.6
37 |
--------------------------------------------------------------------------------
/xr_examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmbruns/pyopenxr_examples/9e3817c8bfd0bcd66282161e014f5256cf123979/xr_examples/__init__.py
--------------------------------------------------------------------------------
/xr_examples/api_dump_layer.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import xr
4 |
5 | # Turn on extra debugging messages in the OpenXR Loader
6 | # os.environ["XR_LOADER_DEBUG"] = "all"
7 | # os.environ["LD_BIND_NOW"] = "1"
8 |
9 | print(os.environ["XR_API_LAYER_PATH"])
10 | print(len(xr.enumerate_api_layer_properties()))
11 | print(*xr.enumerate_api_layer_properties())
12 |
13 | # 1) Setting/clearing the environment from python does affect the layers
14 | os.environ.pop("XR_API_LAYER_PATH", None)
15 | print(len(xr.enumerate_api_layer_properties()))
16 | print(*xr.enumerate_api_layer_properties())
17 |
18 | xr.expose_packaged_api_layers()
19 | print(len(xr.enumerate_api_layer_properties()))
20 | print(*xr.enumerate_api_layer_properties())
21 |
22 | foo_layer = xr.SteamVrLinuxDestroyInstanceLayer()
23 | print(foo_layer.name)
24 |
25 | # 2) The choice of layers can be specified in the instance constructor
26 | instance_handle = xr.create_instance(create_info=xr.InstanceCreateInfo(
27 | enabled_api_layer_names=[
28 | xr.LUNARG_api_dump_APILAYER_NAME,
29 | foo_layer.name,
30 | # xr.LUNARG_core_validation_APILAYER_NAME,
31 | # xr.LUNARG_api_dump_APILAYER_NAME,
32 | ],))
33 |
34 | xr.destroy_instance(instance_handle)
35 |
--------------------------------------------------------------------------------
/xr_examples/color_cube.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr example program color_cube.py
3 | This example renders one big cube.
4 | """
5 |
6 | import inspect
7 | from OpenGL import GL
8 | from OpenGL.GL.shaders import compileShader, compileProgram
9 | import xr
10 |
11 |
12 | # ContextObject is a high level pythonic class meant to keep simple cases simple.
13 | with xr.ContextObject(
14 | instance_create_info=xr.InstanceCreateInfo(
15 | enabled_extension_names=[
16 | # A graphics extension is mandatory (without a headless extension)
17 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME,
18 | ],
19 | ),
20 | ) as context:
21 | vertex_shader = compileShader(
22 | inspect.cleandoc("""
23 | #version 430
24 |
25 | // Adapted from @jherico's RiftDemo.py in pyovr
26 |
27 | /* Draws a cube:
28 |
29 | 2________ 3
30 | /| /|
31 | 6/_|____7/ |
32 | | |_____|_|
33 | | /0 | /1
34 | |/______|/
35 | 4 5
36 |
37 | */
38 |
39 | layout(location = 0) uniform mat4 Projection = mat4(1);
40 | layout(location = 4) uniform mat4 ModelView = mat4(1);
41 | layout(location = 8) uniform float Size = 0.3;
42 |
43 | // Minimum Y value is zero, so cube sits on the floor in room scale
44 | const vec3 UNIT_CUBE[8] = vec3[8](
45 | vec3(-1.0, -0.0, -1.0), // 0: lower left rear
46 | vec3(+1.0, -0.0, -1.0), // 1: lower right rear
47 | vec3(-1.0, +2.0, -1.0), // 2: upper left rear
48 | vec3(+1.0, +2.0, -1.0), // 3: upper right rear
49 | vec3(-1.0, -0.0, +1.0), // 4: lower left front
50 | vec3(+1.0, -0.0, +1.0), // 5: lower right front
51 | vec3(-1.0, +2.0, +1.0), // 6: upper left front
52 | vec3(+1.0, +2.0, +1.0) // 7: upper right front
53 | );
54 |
55 | const vec3 UNIT_CUBE_NORMALS[6] = vec3[6](
56 | vec3(0.0, 0.0, -1.0),
57 | vec3(0.0, 0.0, 1.0),
58 | vec3(1.0, 0.0, 0.0),
59 | vec3(-1.0, 0.0, 0.0),
60 | vec3(0.0, 1.0, 0.0),
61 | vec3(0.0, -1.0, 0.0)
62 | );
63 |
64 | const int CUBE_INDICES[36] = int[36](
65 | 0, 1, 2, 2, 1, 3, // rear
66 | 4, 6, 5, 6, 7, 5, // front
67 | 0, 2, 4, 4, 2, 6, // left
68 | 1, 3, 5, 5, 3, 7, // right
69 | 2, 6, 3, 6, 3, 7, // top
70 | 0, 1, 4, 4, 1, 5 // bottom
71 | );
72 |
73 | out vec3 _color;
74 |
75 | void main() {
76 | _color = vec3(1.0, 0.0, 0.0);
77 | int vertexIndex = CUBE_INDICES[gl_VertexID];
78 | int normalIndex = gl_VertexID / 6;
79 |
80 | _color = UNIT_CUBE_NORMALS[normalIndex];
81 | if (any(lessThan(_color, vec3(0.0)))) {
82 | _color = vec3(1.0) + _color;
83 | }
84 |
85 | gl_Position = Projection * ModelView * vec4(UNIT_CUBE[vertexIndex] * Size, 1.0);
86 | }
87 | """), GL.GL_VERTEX_SHADER)
88 | fragment_shader = compileShader(
89 | inspect.cleandoc("""
90 | #version 430
91 |
92 | in vec3 _color;
93 | out vec4 FragColor;
94 |
95 | void main() {
96 | FragColor = vec4(_color, 1.0);
97 | }
98 | """), GL.GL_FRAGMENT_SHADER)
99 | shader = compileProgram(vertex_shader, fragment_shader)
100 | vao = GL.glGenVertexArrays(1)
101 | GL.glBindVertexArray(vao)
102 | GL.glEnable(GL.GL_DEPTH_TEST)
103 | GL.glClearColor(0.2, 0.2, 0.2, 1)
104 | GL.glClearDepth(1.0)
105 | for frame_index, frame_state in enumerate(context.frame_loop()):
106 | for view_index, view in enumerate(context.view_loop(frame_state)):
107 | if view_index == 1:
108 | # continue
109 | pass
110 | # print(view_index, view.pose.position) # Are both eyes the same?
111 | projection = xr.Matrix4x4f.create_projection_fov(
112 | graphics_api=xr.GraphicsAPI.OPENGL,
113 | fov=view.fov,
114 | near_z=0.05,
115 | far_z=100.0, # tip: use negative far_z for infinity projection...
116 | )
117 | to_view = xr.Matrix4x4f.create_translation_rotation_scale(
118 | translation=view.pose.position,
119 | rotation=view.pose.orientation,
120 | scale=(1, 1, 1),
121 | )
122 | view = xr.Matrix4x4f.invert_rigid_body(to_view)
123 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
124 | GL.glUseProgram(shader)
125 | GL.glUniformMatrix4fv(0, 1, False, projection.as_numpy())
126 | GL.glUniformMatrix4fv(4, 1, False, view.as_numpy())
127 | GL.glBindVertexArray(vao)
128 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, 36)
129 | # if frame_index > 3: break
130 |
--------------------------------------------------------------------------------
/xr_examples/debug_all_the_things.py:
--------------------------------------------------------------------------------
1 | """
2 | debug_all_the_things.py
3 |
4 | This example program demonstrates several logging aids for debugging pyopenxr programs.
5 | Ordinarily you would not do all these things at once.
6 | """
7 |
8 | import ctypes
9 | import logging # 1) Use the python logging system
10 | import os
11 |
12 | import glfw
13 | from OpenGL import GL # OpenGL can report debug messages too
14 | import xr
15 |
16 | # 2) Hook into the pyopenxr logger hierarchy.
17 | logging.basicConfig() # You might want also to study the parameters to basicConfig()...
18 | pyopenxr_logger = logging.getLogger("pyopenxr")
19 | # show me ALL the messages
20 | pyopenxr_logger.setLevel(logging.DEBUG) # Modify argument according to your current needs
21 |
22 | # 3) Create a child logger for this particular program
23 | logger = logging.getLogger("pyopenxr.debug_all_the_things")
24 | logger.info("Hey, this logging thing works!") # Test it out
25 |
26 | # Use API layers for debugging
27 | enabled_api_layers = []
28 |
29 | # 4) Core validation adds additional messages about correct use of the OpenXR api
30 | if xr.LUNARG_core_validation_APILAYER_NAME in xr.enumerate_api_layer_properties():
31 | enabled_api_layers.append(xr.LUNARG_core_validation_APILAYER_NAME)
32 |
33 | # 5) API dump shows the details of every OpenXR call. Use this for deep debugging only.
34 | if xr.LUNARG_api_dump_APILAYER_NAME in xr.enumerate_api_layer_properties():
35 | enabled_api_layers.append(xr.LUNARG_api_dump_APILAYER_NAME)
36 | os.environ["XR_API_DUMP_EXPORT_TYPE"] = "text" # or "html"
37 | # os.environ["XR_API_DUMP_FILE_NAME"] = "/some/file/name"
38 |
39 | # Use extensions for debugging
40 | enabled_extensions = []
41 | # 6) XR_EXT_debug_utils can be used to redirect pyopenxr debugging messages to our logger,
42 | # among other things.
43 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties():
44 | enabled_extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME)
45 |
46 |
47 | # Define helper function for our logging callback
48 | def openxr_log_level(severity_flags: xr.DebugUtilsMessageSeverityFlagsEXT) -> int:
49 | """Convert OpenXR message severities to python logging severities."""
50 | if severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
51 | return logging.ERROR
52 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
53 | return logging.WARNING
54 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
55 | return logging.INFO
56 | else:
57 | return logging.DEBUG
58 |
59 |
60 | def xr_debug_callback(
61 | severity: xr.DebugUtilsMessageSeverityFlagsEXT,
62 | _type: xr.DebugUtilsMessageTypeFlagsEXT,
63 | data: ctypes.POINTER(xr.DebugUtilsMessengerCallbackDataEXT),
64 | _user_data: ctypes.c_void_p) -> bool:
65 | """Redirect OpenXR messages to our python logger."""
66 | d = data.contents
67 | pyopenxr_logger.log(
68 | level=openxr_log_level(severity),
69 | msg=f"OpenXR: {d.function_name.decode()}: {d.message.decode()}")
70 | return True
71 |
72 |
73 | # Prepare to create a debug utils messenger
74 | pfn_xr_debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(xr_debug_callback)
75 | debug_utils_messenger_create_info = xr.DebugUtilsMessengerCreateInfoEXT(
76 | message_severities=(
77 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
78 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
79 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
80 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
81 | ),
82 | message_types=(
83 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
84 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
85 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
86 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT
87 | ),
88 | user_callback=pfn_xr_debug_callback,
89 | )
90 |
91 | ptr_debug_messenger = None
92 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties():
93 | # Insert our debug_utils_messenger create_info
94 | ptr_debug_messenger = ctypes.cast(
95 | ctypes.pointer(debug_utils_messenger_create_info), ctypes.c_void_p)
96 |
97 | # 7) Turn on extra debugging messages in the OpenXR Loader
98 | os.environ["XR_LOADER_DEBUG"] = "all"
99 | os.environ["LD_BIND_NOW"] = "1"
100 |
101 |
102 | # Create OpenXR instance with attached layers, extensions, and debug messenger.
103 | instance = xr.create_instance(
104 | xr.InstanceCreateInfo(
105 | enabled_api_layer_names=enabled_api_layers,
106 | enabled_extension_names=enabled_extensions,
107 | next=ptr_debug_messenger,
108 | )
109 | )
110 |
111 | # OpenGL can report debug messages too
112 | # Create a sub-logger for messages from OpenGL
113 | gl_logger = logging.getLogger("pyopenxr.opengl")
114 |
115 |
116 | def gl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user):
117 | """Redirect OpenGL debug messages"""
118 | log_level = {
119 | GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR,
120 | GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING,
121 | GL.GL_DEBUG_SEVERITY_LOW: logging.INFO,
122 | GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG,
123 | }[severity]
124 | gl_logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}")
125 |
126 |
127 | gl_debug_message_proc = GL.GLDEBUGPROC(gl_debug_message_callback)
128 | # We need an active OpenGL context before calling glDebugMessageCallback
129 | glfw.init()
130 | window = glfw.create_window(16, 16, "glfw window", None, None)
131 | glfw.make_context_current(window)
132 | GL.glDebugMessageCallback(gl_debug_message_proc, None)
133 |
134 |
135 | # Clean up
136 | glfw.terminate()
137 | xr.destroy_instance(instance)
138 |
--------------------------------------------------------------------------------
/xr_examples/extension_properties.py:
--------------------------------------------------------------------------------
1 | import xr
2 |
3 | # Query the available VR/AR extensions
4 | available = xr.enumerate_instance_extension_properties()
5 | for prop in available:
6 | print(prop)
7 |
8 | # Replace with whatever extensions are required for your
9 | # particular application...
10 | required = [xr.KHR_OPENGL_ENABLE_EXTENSION_NAME, ]
11 | for prop in required:
12 | assert prop in available
13 |
--------------------------------------------------------------------------------
/xr_examples/gl_example.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import logging
3 |
4 | import glfw
5 | import platform
6 | from OpenGL import GL
7 | if platform.system() == "Windows":
8 | from OpenGL import WGL
9 | elif platform.system() == "Linux":
10 | from OpenGL import GLX
11 |
12 | import xr
13 |
14 |
15 | ALL_SEVERITIES = (
16 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
17 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
18 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
19 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
20 | )
21 |
22 | ALL_TYPES = (
23 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
24 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
25 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
26 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT
27 | )
28 |
29 |
30 | def py_log_level(severity_flags: int):
31 | if severity_flags & 0x0001: # VERBOSE
32 | return logging.DEBUG
33 | if severity_flags & 0x0010: # INFO
34 | return logging.INFO
35 | if severity_flags & 0x0100: # WARNING
36 | return logging.WARNING
37 | if severity_flags & 0x1000: # ERROR
38 | return logging.ERROR
39 | return logging.CRITICAL
40 |
41 |
42 | stringForFormat = {
43 | GL.GL_COMPRESSED_R11_EAC: "COMPRESSED_R11_EAC",
44 | GL.GL_COMPRESSED_RED_RGTC1: "COMPRESSED_RED_RGTC1",
45 | GL.GL_COMPRESSED_RG_RGTC2: "COMPRESSED_RG_RGTC2",
46 | GL.GL_COMPRESSED_RG11_EAC: "COMPRESSED_RG11_EAC",
47 | GL.GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: "COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT",
48 | GL.GL_COMPRESSED_RGB8_ETC2: "COMPRESSED_RGB8_ETC2",
49 | GL.GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2: "COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2",
50 | GL.GL_COMPRESSED_RGBA8_ETC2_EAC: "COMPRESSED_RGBA8_ETC2_EAC",
51 | GL.GL_COMPRESSED_SIGNED_R11_EAC: "COMPRESSED_SIGNED_R11_EAC",
52 | GL.GL_COMPRESSED_SIGNED_RG11_EAC: "COMPRESSED_SIGNED_RG11_EAC",
53 | GL.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: "COMPRESSED_SRGB_ALPHA_BPTC_UNORM",
54 | GL.GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC: "COMPRESSED_SRGB8_ALPHA8_ETC2_EAC",
55 | GL.GL_COMPRESSED_SRGB8_ETC2: "COMPRESSED_SRGB8_ETC2",
56 | GL.GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2: "COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2",
57 | GL.GL_DEPTH_COMPONENT16: "DEPTH_COMPONENT16",
58 | GL.GL_DEPTH_COMPONENT24: "DEPTH_COMPONENT24",
59 | GL.GL_DEPTH_COMPONENT32: "DEPTH_COMPONENT32",
60 | GL.GL_DEPTH_COMPONENT32F: "DEPTH_COMPONENT32F",
61 | GL.GL_DEPTH24_STENCIL8: "DEPTH24_STENCIL8",
62 | GL.GL_R11F_G11F_B10F: "R11F_G11F_B10F",
63 | GL.GL_R16_SNORM: "R16_SNORM",
64 | GL.GL_R16: "R16",
65 | GL.GL_R16F: "R16F",
66 | GL.GL_R16I: "R16I",
67 | GL.GL_R16UI: "R16UI",
68 | GL.GL_R32F: "R32F",
69 | GL.GL_R32I: "R32I",
70 | GL.GL_R32UI: "R32UI",
71 | GL.GL_R8_SNORM: "R8_SNORM",
72 | GL.GL_R8: "R8",
73 | GL.GL_R8I: "R8I",
74 | GL.GL_R8UI: "R8UI",
75 | GL.GL_RG16_SNORM: "RG16_SNORM",
76 | GL.GL_RG16: "RG16",
77 | GL.GL_RG16F: "RG16F",
78 | GL.GL_RG16I: "RG16I",
79 | GL.GL_RG16UI: "RG16UI",
80 | GL.GL_RG32F: "RG32F",
81 | GL.GL_RG32I: "RG32I",
82 | GL.GL_RG32UI: "RG32UI",
83 | GL.GL_RG8_SNORM: "RG8_SNORM",
84 | GL.GL_RG8: "RG8",
85 | GL.GL_RG8I: "RG8I",
86 | GL.GL_RG8UI: "RG8UI",
87 | GL.GL_RGB10_A2: "RGB10_A2",
88 | GL.GL_RGB8: "RGB8",
89 | GL.GL_RGB9_E5: "RGB9_E5",
90 | GL.GL_RGBA16_SNORM: "RGBA16_SNORM",
91 | GL.GL_RGBA16: "RGBA16",
92 | GL.GL_RGBA16F: "RGBA16F",
93 | GL.GL_RGBA16I: "RGBA16I",
94 | GL.GL_RGBA16UI: "RGBA16UI",
95 | GL.GL_RGBA2: "RGBA2",
96 | GL.GL_RGBA32F: "RGBA32F",
97 | GL.GL_RGBA32I: "RGBA32I",
98 | GL.GL_RGBA32UI: "RGBA32UI",
99 | GL.GL_RGBA8_SNORM: "RGBA8_SNORM",
100 | GL.GL_RGBA8: "RGBA8",
101 | GL.GL_RGBA8I: "RGBA8I",
102 | GL.GL_RGBA8UI: "RGBA8UI",
103 | GL.GL_SRGB8_ALPHA8: "SRGB8_ALPHA8",
104 | GL.GL_SRGB8: "SRGB8",
105 | GL.GL_RGB16F: "RGB16F",
106 | GL.GL_DEPTH32F_STENCIL8: "DEPTH32F_STENCIL8",
107 | GL.GL_BGR: "BGR (Out of spec)",
108 | GL.GL_BGRA: "BGRA (Out of spec)",
109 | }
110 |
111 |
112 | class OpenXrExample(object):
113 | def __init__(self, log_level=logging.WARNING):
114 | logging.basicConfig()
115 | self.logger = logging.getLogger("gl_example")
116 | self.logger.setLevel(log_level)
117 | self.debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(self.debug_callback_py)
118 | self.mirror_window = False
119 | self.instance = None
120 | self.system_id = None
121 | self.pxrCreateDebugUtilsMessengerEXT = None
122 | self.pxrDestroyDebugUtilsMessengerEXT = None
123 | self.pxrGetOpenGLGraphicsRequirementsKHR = None
124 | self.graphics_requirements = xr.GraphicsRequirementsOpenGLKHR()
125 | if platform.system() == 'Windows':
126 | self.graphics_binding = xr.GraphicsBindingOpenGLWin32KHR()
127 | elif platform.system() == 'Linux':
128 | self.graphics_binding = xr.GraphicsBindingOpenGLXlibKHR()
129 | else:
130 | raise NotImplementedError('Unsupported platform')
131 | self.render_target_size = None
132 | self.window = None
133 | self.session = None
134 | self.projection_layer_views = (xr.CompositionLayerProjectionView * 2)(
135 | *([xr.CompositionLayerProjectionView()] * 2))
136 | self.projection_layer = xr.CompositionLayerProjection(
137 | views=self.projection_layer_views)
138 | self.swapchain_create_info = xr.SwapchainCreateInfo()
139 | self.swapchain = None
140 | self.swapchain_images = None
141 | self.fbo_id = None
142 | self.fbo_depth_buffer = None
143 | self.quit = False
144 | self.session_state = xr.SessionState.IDLE
145 | self.frame_state = xr.FrameState()
146 | self.eye_view_states = None
147 | self.window_size = None
148 | self.enable_debug = True
149 | self.linux_steamvr_broken_destroy_instance = False
150 |
151 | def debug_callback_py(
152 | self,
153 | severity: xr.DebugUtilsMessageSeverityFlagsEXT,
154 | _type: xr.DebugUtilsMessageTypeFlagsEXT,
155 | data: ctypes.POINTER(xr.DebugUtilsMessengerCallbackDataEXT),
156 | _user_data: ctypes.c_void_p,
157 | ) -> bool:
158 | d = data.contents
159 | # TODO structure properties to return unicode strings
160 | self.logger.log(py_log_level(severity), f"{d.function_name.decode()}: {d.message.decode()}")
161 | return True
162 |
163 | def run(self):
164 | while not self.quit:
165 | if glfw.window_should_close(self.window):
166 | self.quit = True
167 | else:
168 | self.frame()
169 |
170 | def __enter__(self):
171 | self.prepare_xr_instance()
172 | self.prepare_xr_system()
173 | self.prepare_window()
174 | self.prepare_xr_session()
175 | self.prepare_xr_swapchain()
176 | self.prepare_xr_composition_layers()
177 | self.prepare_gl_framebuffer()
178 | return self
179 |
180 | def prepare_xr_instance(self):
181 | discovered_extensions = xr.enumerate_instance_extension_properties()
182 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME not in discovered_extensions:
183 | self.enable_debug = False
184 | requested_extensions = [xr.KHR_OPENGL_ENABLE_EXTENSION_NAME]
185 | if self.enable_debug:
186 | requested_extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME)
187 | for extension in requested_extensions:
188 | assert extension in discovered_extensions
189 | app_info = xr.ApplicationInfo(
190 | application_name="gl_example",
191 | application_version=0,
192 | engine_name="pyopenxr",
193 | engine_version=xr.PYOPENXR_CURRENT_API_VERSION,
194 | api_version=xr.Version(1, 0, xr.XR_VERSION_PATCH),
195 | )
196 | ici = xr.InstanceCreateInfo(
197 | application_info=app_info,
198 | enabled_extension_names=requested_extensions,
199 | )
200 | dumci = xr.DebugUtilsMessengerCreateInfoEXT()
201 | if self.enable_debug:
202 | dumci.message_severities = ALL_SEVERITIES
203 | dumci.message_types = ALL_TYPES
204 | dumci.user_data = None # TODO
205 | dumci.user_callback = self.debug_callback
206 | ici.next = ctypes.cast(ctypes.pointer(dumci), ctypes.c_void_p) # TODO: yuck
207 | self.instance = xr.create_instance(ici)
208 | # TODO: pythonic wrapper
209 | self.pxrGetOpenGLGraphicsRequirementsKHR = ctypes.cast(
210 | xr.get_instance_proc_addr(
211 | self.instance,
212 | "xrGetOpenGLGraphicsRequirementsKHR",
213 | ),
214 | xr.PFN_xrGetOpenGLGraphicsRequirementsKHR
215 | )
216 | instance_props = xr.get_instance_properties(self.instance)
217 | if platform.system() == 'Linux' and instance_props.runtime_name == b"SteamVR/OpenXR":
218 | print("SteamVR/OpenXR on Linux detected, enabling workarounds")
219 | # Enabling workaround for https://github.com/ValveSoftware/SteamVR-for-Linux/issues/422,
220 | # and https://github.com/ValveSoftware/SteamVR-for-Linux/issues/479
221 | # destroy_instance() causes SteamVR to hang and never recover
222 | self.linux_steamvr_broken_destroy_instance = True
223 |
224 | def prepare_xr_system(self):
225 | get_info = xr.SystemGetInfo(xr.FormFactor.HEAD_MOUNTED_DISPLAY)
226 | self.system_id = xr.get_system(self.instance, get_info) # TODO: not a pointer
227 | view_configs = xr.enumerate_view_configurations(self.instance, self.system_id)
228 | assert view_configs[0] == xr.ViewConfigurationType.PRIMARY_STEREO.value # TODO: equality...
229 | view_config_views = xr.enumerate_view_configuration_views(
230 | self.instance, self.system_id, xr.ViewConfigurationType.PRIMARY_STEREO)
231 | assert len(view_config_views) == 2
232 | assert view_config_views[0].recommended_image_rect_height == view_config_views[1].recommended_image_rect_height
233 | self.render_target_size = (
234 | view_config_views[0].recommended_image_rect_width * 2,
235 | view_config_views[0].recommended_image_rect_height)
236 | result = self.pxrGetOpenGLGraphicsRequirementsKHR(
237 | self.instance, self.system_id, ctypes.byref(self.graphics_requirements)) # TODO: pythonic wrapper
238 | result = xr.exception.check_result(xr.Result(result))
239 | if result.is_exception():
240 | raise result
241 |
242 | def prepare_window(self):
243 | if not glfw.init():
244 | raise RuntimeError("GLFW initialization failed")
245 | if not self.mirror_window:
246 | glfw.window_hint(glfw.VISIBLE, False)
247 | glfw.window_hint(glfw.DOUBLEBUFFER, False)
248 | glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
249 | glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 5)
250 | glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
251 | self.window_size = [s // 4 for s in self.render_target_size]
252 | self.window = glfw.create_window(*self.window_size, "gl_example", None, None)
253 | if self.window is None:
254 | raise RuntimeError("Failed to create GLFW window")
255 | glfw.make_context_current(self.window)
256 | # Attempt to disable vsync on the desktop window or
257 | # it will interfere with the OpenXR frame loop timing
258 | glfw.swap_interval(0)
259 |
260 | def prepare_xr_session(self):
261 | if platform.system() == 'Windows':
262 | self.graphics_binding.h_dc = WGL.wglGetCurrentDC()
263 | self.graphics_binding.h_glrc = WGL.wglGetCurrentContext()
264 | else:
265 | self.graphics_binding.x_display = GLX.glXGetCurrentDisplay()
266 | self.graphics_binding.glx_context = GLX.glXGetCurrentContext()
267 | self.graphics_binding.glx_drawable = GLX.glXGetCurrentDrawable()
268 | pp = ctypes.cast(ctypes.pointer(self.graphics_binding), ctypes.c_void_p)
269 | sci = xr.SessionCreateInfo(0, self.system_id, next=pp)
270 | self.session = xr.create_session(self.instance, sci)
271 | reference_spaces = xr.enumerate_reference_spaces(self.session)
272 | for rs in reference_spaces:
273 | self.logger.debug(f"Session supports reference space {xr.ReferenceSpaceType(rs)}")
274 | # TODO: default constructors for Quaternion, Vector3f, Posef, ReferenceSpaceCreateInfo
275 | rsci = xr.ReferenceSpaceCreateInfo(
276 | xr.ReferenceSpaceType.STAGE,
277 | xr.Posef(xr.Quaternionf(0, 0, 0, 1), xr.Vector3f(0, 0, 0))
278 | )
279 | self.projection_layer.space = xr.create_reference_space(self.session, rsci)
280 | swapchain_formats = xr.enumerate_swapchain_formats(self.session)
281 | for scf in swapchain_formats:
282 | self.logger.debug(f"Session supports swapchain format {stringForFormat[scf]}")
283 |
284 | def prepare_xr_swapchain(self):
285 | self.swapchain_create_info.usage_flags = xr.SWAPCHAIN_USAGE_TRANSFER_DST_BIT
286 | self.swapchain_create_info.format = GL.GL_SRGB8_ALPHA8
287 | self.swapchain_create_info.sample_count = 1
288 | self.swapchain_create_info.array_size = 1
289 | self.swapchain_create_info.face_count = 1
290 | self.swapchain_create_info.mip_count = 1
291 | self.swapchain_create_info.width = self.render_target_size[0]
292 | self.swapchain_create_info.height = self.render_target_size[1]
293 | self.swapchain = xr.create_swapchain(self.session, self.swapchain_create_info)
294 | self.swapchain_images = xr.enumerate_swapchain_images(self.swapchain, xr.SwapchainImageOpenGLKHR)
295 | for i, si in enumerate(self.swapchain_images):
296 | self.logger.debug(f"Swapchain image {i} type = {xr.StructureType(si.type)}")
297 |
298 | def prepare_xr_composition_layers(self):
299 | self.projection_layer.view_count = 2
300 | self.projection_layer.views = self.projection_layer_views
301 | for eye_index in range(2):
302 | layer_view = self.projection_layer_views[eye_index]
303 | layer_view.sub_image.swapchain = self.swapchain
304 | layer_view.sub_image.image_rect.extent = xr.Extent2Di(
305 | self.render_target_size[0] // 2,
306 | self.render_target_size[1],
307 | )
308 | if eye_index == 1:
309 | layer_view.sub_image.image_rect.offset.x = layer_view.sub_image.image_rect.extent.width
310 |
311 | def prepare_gl_framebuffer(self):
312 | glfw.make_context_current(self.window)
313 | self.fbo_depth_buffer = GL.glGenRenderbuffers(1)
314 | GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, self.fbo_depth_buffer)
315 | if self.swapchain_create_info.sample_count == 1:
316 | GL.glRenderbufferStorage(
317 | GL.GL_RENDERBUFFER,
318 | GL.GL_DEPTH24_STENCIL8,
319 | self.swapchain_create_info.width,
320 | self.swapchain_create_info.height,
321 | )
322 | else:
323 | GL.glRenderbufferStorageMultisample(
324 | GL.GL_RENDERBUFFER,
325 | self.swapchain_create_info.sample_count,
326 | GL.GL_DEPTH24_STENCIL8,
327 | self.swapchain_create_info.width,
328 | self.swapchain_create_info.height,
329 | )
330 | self.fbo_id = GL.glGenFramebuffers(1)
331 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, self.fbo_id)
332 | GL.glFramebufferRenderbuffer(
333 | GL.GL_DRAW_FRAMEBUFFER,
334 | GL.GL_DEPTH_STENCIL_ATTACHMENT,
335 | GL.GL_RENDERBUFFER,
336 | self.fbo_depth_buffer,
337 | )
338 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, 0)
339 |
340 | def frame(self):
341 | glfw.poll_events()
342 | self.poll_xr_events()
343 | if self.quit:
344 | return
345 | if self.start_xr_frame():
346 | self.update_xr_views()
347 | if self.frame_state.should_render:
348 | self.render()
349 | self.end_xr_frame()
350 |
351 | def poll_xr_events(self):
352 | while True:
353 | try:
354 | event_buffer = xr.poll_event(self.instance)
355 | event_type = xr.StructureType(event_buffer.type)
356 | if event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED:
357 | self.on_session_state_changed(event_buffer)
358 | except xr.EventUnavailable:
359 | break
360 |
361 | def on_session_state_changed(self, session_state_changed_event):
362 | # TODO: it would be nice to avoid this horrible cast...
363 | event = ctypes.cast(
364 | ctypes.byref(session_state_changed_event),
365 | ctypes.POINTER(xr.EventDataSessionStateChanged)).contents
366 | # TODO: enum property
367 | self.session_state = xr.SessionState(event.state)
368 | if self.session_state == xr.SessionState.READY:
369 | if not self.quit:
370 | sbi = xr.SessionBeginInfo(xr.ViewConfigurationType.PRIMARY_STEREO)
371 | xr.begin_session(self.session, sbi)
372 | elif self.session_state == xr.SessionState.STOPPING:
373 | xr.end_session(self.session)
374 | self.session = None
375 | self.quit = True
376 |
377 | def start_xr_frame(self) -> bool:
378 | if self.session_state in [
379 | xr.SessionState.READY,
380 | xr.SessionState.FOCUSED,
381 | xr.SessionState.SYNCHRONIZED,
382 | xr.SessionState.VISIBLE,
383 | ]:
384 | frame_wait_info = xr.FrameWaitInfo(None)
385 | try:
386 | self.frame_state = xr.wait_frame(self.session, frame_wait_info)
387 | xr.begin_frame(self.session, None)
388 | return True
389 | except xr.ResultException:
390 | return False
391 | return False
392 |
393 | def end_xr_frame(self):
394 | frame_end_info = xr.FrameEndInfo(
395 | self.frame_state.predicted_display_time,
396 | xr.EnvironmentBlendMode.OPAQUE
397 | )
398 | if self.frame_state.should_render:
399 | for eye_index in range(2):
400 | layer_view = self.projection_layer_views[eye_index]
401 | eye_view = self.eye_view_states[eye_index]
402 | layer_view.fov = eye_view.fov
403 | layer_view.pose = eye_view.pose
404 | frame_end_info.layers = [ctypes.byref(self.projection_layer), ]
405 | xr.end_frame(self.session, frame_end_info)
406 |
407 | def update_xr_views(self):
408 | vi = xr.ViewLocateInfo(
409 | xr.ViewConfigurationType.PRIMARY_STEREO,
410 | self.frame_state.predicted_display_time,
411 | self.projection_layer.space,
412 | )
413 | vs, self.eye_view_states = xr.locate_views(self.session, vi)
414 | for eye_index, view_state in enumerate(self.eye_view_states):
415 | # These aren't actually used in this simple example...
416 | # self.eye_projections[eye_index] = something(view_state.fov) # TODO:
417 | # print(view_state.pose)
418 | pass
419 |
420 | def render(self):
421 | ai = xr.SwapchainImageAcquireInfo(None)
422 | swapchain_index = xr.acquire_swapchain_image(self.swapchain, ai)
423 | wi = xr.SwapchainImageWaitInfo(xr.INFINITE_DURATION)
424 | xr.wait_swapchain_image(self.swapchain, wi)
425 | glfw.make_context_current(self.window)
426 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self.fbo_id)
427 | sw_image = self.swapchain_images[swapchain_index]
428 | GL.glFramebufferTexture(
429 | GL.GL_FRAMEBUFFER,
430 | GL.GL_COLOR_ATTACHMENT0,
431 | sw_image.image,
432 | 0,
433 | )
434 | # "render" to the swapchain image
435 | w, h = self.render_target_size
436 | GL.glEnable(GL.GL_SCISSOR_TEST)
437 | GL.glScissor(0, 0, w // 2, h)
438 | GL.glClearColor(0, 1, 0, 1)
439 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
440 | GL.glScissor(w // 2, 0, w // 2, h)
441 | GL.glClearColor(0, 0, 1, 1)
442 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
443 | GL.glDisable(GL.GL_SCISSOR_TEST)
444 |
445 | if self.mirror_window:
446 | # fast blit from the fbo to the window surface
447 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, 0)
448 | GL.glBlitFramebuffer(
449 | 0, 0, w, h, 0, 0,
450 | *self.window_size,
451 | GL.GL_COLOR_BUFFER_BIT,
452 | GL.GL_NEAREST
453 | )
454 | GL.glFramebufferTexture(GL.GL_READ_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, 0, 0)
455 | GL.glBindFramebuffer(GL.GL_READ_FRAMEBUFFER, 0)
456 | ri = xr.SwapchainImageReleaseInfo()
457 | xr.release_swapchain_image(self.swapchain, ri)
458 | # If we're mirror make sure to do the potentially blocking command
459 | # AFTER we've released the swapchain image
460 | if self.mirror_window:
461 | glfw.swap_buffers(self.window)
462 |
463 | def __exit__(self, exc_type, exc_value, traceback):
464 | if self.window is not None:
465 | glfw.make_context_current(self.window)
466 | if self.fbo_id is not None:
467 | GL.glDeleteFramebuffers(1, [self.fbo_id])
468 | self.fbo_id = None
469 | if self.fbo_depth_buffer is not None:
470 | GL.glDeleteRenderbuffers(1, [self.fbo_depth_buffer])
471 | self.fbo_depth_buffer = None
472 | glfw.terminate()
473 | self.window = None
474 | if self.swapchain is not None:
475 | xr.destroy_swapchain(self.swapchain)
476 | self.swapchain = None
477 | if self.session is not None:
478 | xr.destroy_session(self.session)
479 | self.session = None
480 | self.system_id = None
481 | if self.instance is not None:
482 | # Workaround for https://github.com/ValveSoftware/SteamVR-for-Linux/issues/422
483 | # and https://github.com/ValveSoftware/SteamVR-for-Linux/issues/479
484 | if not self.linux_steamvr_broken_destroy_instance:
485 | xr.destroy_instance(self.instance)
486 | self.instance = None
487 | glfw.terminate()
488 |
489 |
490 | if __name__ == "__main__":
491 | with OpenXrExample(logging.DEBUG) as ex:
492 | ex.run()
493 |
--------------------------------------------------------------------------------
/xr_examples/green_blue.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr example program green_blue.py
3 |
4 | Different way to get similar display to the venerable gl_example.py
5 | """
6 |
7 | import os
8 | from OpenGL import GL
9 | import xr
10 |
11 |
12 | # ContextObject is a high level pythonic class meant to keep simple cases simple.
13 | with xr.ContextObject(
14 | instance_create_info=xr.InstanceCreateInfo(
15 | enabled_extension_names=[
16 | # A graphics extension is mandatory (without a headless extension)
17 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME,
18 | ],
19 | ),
20 | ) as context:
21 | eye_colors = [
22 | (0, 1, 0, 1), # Left eye green
23 | (0, 0, 1, 1), # Right eye blue
24 | (1, 0, 0, 1), # Third eye blind
25 | ]
26 | for frame_index, frame_state in enumerate(context.frame_loop()):
27 | for view_index, view in enumerate(context.view_loop(frame_state)):
28 | # set each eye to a different color (not working yet...)
29 | GL.glClearColor(*eye_colors[view_index])
30 | GL.glClear(GL.GL_COLOR_BUFFER_BIT)
31 | if frame_index > 500: # Don't run forever
32 | break
33 |
--------------------------------------------------------------------------------
/xr_examples/headless.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr headless example
3 | using low level OpenXR API
4 | """
5 |
6 | import ctypes
7 | import platform
8 | import time
9 | import xr
10 |
11 | # Enumerate the required instance extensions
12 | extensions = [xr.MND_HEADLESS_EXTENSION_NAME] # Permits use without a graphics display
13 | # Tracking controllers in headless mode requires a way to get the current XrTime
14 | if platform.system() == "Windows":
15 | extensions.append(xr.KHR_WIN32_CONVERT_PERFORMANCE_COUNTER_TIME_EXTENSION_NAME)
16 | else: # Linux
17 | extensions.append(xr.KHR_CONVERT_TIMESPEC_TIME_EXTENSION_NAME)
18 |
19 | # Create instance for headless use
20 | instance = xr.create_instance(xr.InstanceCreateInfo(
21 | enabled_extension_names=extensions,
22 | ))
23 | system = xr.get_system(
24 | instance,
25 | # Presumably the form factor is irrelevant in headless mode...
26 | xr.SystemGetInfo(form_factor=xr.FormFactor.HEAD_MOUNTED_DISPLAY),
27 | )
28 | session = xr.create_session(
29 | instance,
30 | xr.SessionCreateInfo(
31 | system_id=system,
32 | next=None, # No GraphicsBinding structure is required here in HEADLESS mode
33 | )
34 | )
35 |
36 | if platform.system() == "Windows":
37 | import ctypes.wintypes
38 | pc_time = ctypes.wintypes.LARGE_INTEGER()
39 | kernel32 = ctypes.WinDLL("kernel32")
40 | pxrConvertWin32PerformanceCounterToTimeKHR = ctypes.cast(
41 | xr.get_instance_proc_addr(
42 | instance=instance,
43 | name="xrConvertWin32PerformanceCounterToTimeKHR",
44 | ),
45 | xr.PFN_xrConvertWin32PerformanceCounterToTimeKHR,
46 | )
47 |
48 | def time_from_perf_counter(instance: xr.Instance,
49 | performance_counter: ctypes.wintypes.LARGE_INTEGER) -> xr.Time:
50 | xr_time = xr.Time()
51 | result = pxrConvertWin32PerformanceCounterToTimeKHR(
52 | instance,
53 | ctypes.pointer(performance_counter),
54 | ctypes.byref(xr_time),
55 | )
56 | result = xr.check_result(result)
57 | if result.is_exception():
58 | raise result
59 | return xr_time
60 | else:
61 | timespecTime = xr.timespec()
62 | pxrConvertTimespecTimeToTimeKHR = ctypes.cast(
63 | xr.get_instance_proc_addr(
64 | instance=instance,
65 | name="xrConvertTimespecTimeToTimeKHR",
66 | ),
67 | xr.PFN_xrConvertTimespecTimeToTimeKHR,
68 | )
69 |
70 | def time_from_timespec(instance: xr.Instance, timespec_time: xr.timespec) -> xr.Time:
71 | xr_time = xr.Time()
72 | result = pxrConvertTimespecTimeToTimeKHR(
73 | instance,
74 | ctypes.pointer(timespec_time),
75 | ctypes.byref(xr_time),
76 | )
77 | result = xr.check_result(result)
78 | if result.is_exception():
79 | raise result
80 | return xr_time
81 | # Set up controller tracking, as one possible legitimate headless activity
82 | action_set = xr.create_action_set(
83 | instance=instance,
84 | create_info=xr.ActionSetCreateInfo(
85 | action_set_name="action_set",
86 | localized_action_set_name="Action Set",
87 | priority=0,
88 | ),
89 | )
90 | controller_paths = (xr.Path * 2)(
91 | xr.string_to_path(instance, "/user/hand/left"),
92 | xr.string_to_path(instance, "/user/hand/right"),
93 | )
94 | controller_pose_action = xr.create_action(
95 | action_set=action_set,
96 | create_info=xr.ActionCreateInfo(
97 | action_type=xr.ActionType.POSE_INPUT,
98 | action_name="controller_pose",
99 | localized_action_name="Controller pose",
100 | count_subaction_paths=len(controller_paths),
101 | subaction_paths=controller_paths,
102 | ),
103 | )
104 | suggested_bindings = (xr.ActionSuggestedBinding * 2)(
105 | xr.ActionSuggestedBinding(
106 | action=controller_pose_action,
107 | binding=xr.string_to_path(
108 | instance=instance,
109 | path_string="/user/hand/left/input/grip/pose",
110 | ),
111 | ),
112 | xr.ActionSuggestedBinding(
113 | action=controller_pose_action,
114 | binding=xr.string_to_path(
115 | instance=instance,
116 | path_string="/user/hand/right/input/grip/pose",
117 | ),
118 | ),
119 | )
120 | xr.suggest_interaction_profile_bindings(
121 | instance=instance,
122 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
123 | interaction_profile=xr.string_to_path(
124 | instance,
125 | "/interaction_profiles/khr/simple_controller",
126 | ),
127 | count_suggested_bindings=len(suggested_bindings),
128 | suggested_bindings=suggested_bindings,
129 | ),
130 | )
131 | xr.suggest_interaction_profile_bindings(
132 | instance=instance,
133 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
134 | interaction_profile=xr.string_to_path(
135 | instance,
136 | "/interaction_profiles/htc/vive_controller",
137 | ),
138 | count_suggested_bindings=len(suggested_bindings),
139 | suggested_bindings=suggested_bindings,
140 | ),
141 | )
142 | xr.attach_session_action_sets(
143 | session=session,
144 | attach_info=xr.SessionActionSetsAttachInfo(
145 | action_sets=[action_set],
146 | ),
147 | )
148 | action_spaces = [
149 | xr.create_action_space(
150 | session=session,
151 | create_info=xr.ActionSpaceCreateInfo(
152 | action=controller_pose_action,
153 | subaction_path=controller_paths[0],
154 | ),
155 | ),
156 | xr.create_action_space(
157 | session=session,
158 | create_info=xr.ActionSpaceCreateInfo(
159 | action=controller_pose_action,
160 | subaction_path=controller_paths[1],
161 | ),
162 | ),
163 | ]
164 | reference_space = xr.create_reference_space(
165 | session=session,
166 | create_info=xr.ReferenceSpaceCreateInfo(
167 | reference_space_type=xr.ReferenceSpaceType.STAGE,
168 | ),
169 | )
170 | view_reference_space = xr.create_reference_space(
171 | session=session,
172 | create_info=xr.ReferenceSpaceCreateInfo(
173 | reference_space_type=xr.ReferenceSpaceType.VIEW,
174 | ),
175 | )
176 | session_state = xr.SessionState.UNKNOWN
177 | # Loop over session frames
178 | for frame_index in range(30): # Limit number of frames for demo purposes
179 | # Poll session state changed events
180 | while True:
181 | try:
182 | event_buffer = xr.poll_event(instance)
183 | event_type = xr.StructureType(event_buffer.type)
184 | if event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED:
185 | event = ctypes.cast(
186 | ctypes.byref(event_buffer),
187 | ctypes.POINTER(xr.EventDataSessionStateChanged)).contents
188 | session_state = xr.SessionState(event.state)
189 | print(f"OpenXR session state changed to xr.SessionState.{session_state.name}")
190 | if session_state == xr.SessionState.READY:
191 | xr.begin_session(
192 | session,
193 | xr.SessionBeginInfo(
194 | # TODO: zero should be allowed here...
195 | primary_view_configuration_type=xr.ViewConfigurationType.PRIMARY_MONO,
196 | ),
197 | )
198 | elif session_state == xr.SessionState.STOPPING:
199 | xr.destroy_session(session)
200 | session = None
201 | except xr.EventUnavailable:
202 | break # There is no event in the queue at this moment
203 | if session_state == xr.SessionState.FOCUSED:
204 | # wait_frame()/begin_frame()/end_frame() are not required in headless mode
205 | xr.wait_frame(session=session) # Helps SteamVR show application name better
206 | # Perform per-frame activities here
207 |
208 | if platform.system() == "Windows":
209 | kernel32.QueryPerformanceCounter(ctypes.byref(pc_time))
210 | xr_time_now = time_from_perf_counter(instance, pc_time)
211 | else:
212 | time_float = time.clock_gettime(time.CLOCK_MONOTONIC)
213 | timespecTime.tv_sec = int(time_float)
214 | timespecTime.tv_nsec = int((time_float % 1) * 1e9)
215 | xr_time_now = time_from_timespec(instance, timespecTime)
216 |
217 | active_action_set = xr.ActiveActionSet(
218 | action_set=action_set,
219 | subaction_path=xr.NULL_PATH,
220 | )
221 | xr.sync_actions(
222 | session=session,
223 | sync_info=xr.ActionsSyncInfo(
224 | active_action_sets=[active_action_set],
225 | ),
226 | )
227 | found_count = 0
228 | hmd_location = xr.locate_space(
229 | space=view_reference_space,
230 | base_space=reference_space,
231 | time=xr_time_now,
232 | )
233 | if hmd_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT:
234 | print(f"HMD location: {hmd_location.pose}")
235 | found_count += 1
236 | for index, space in enumerate(action_spaces):
237 | space_location = xr.locate_space(
238 | space=space,
239 | base_space=reference_space,
240 | time=xr_time_now,
241 | )
242 | if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT:
243 | print(f"Controller {index + 1}: {space_location.pose}")
244 | found_count += 1
245 | if found_count == 0:
246 | print("no controllers active")
247 |
248 | # Sleep periodically to avoid consuming all available system resources
249 | time.sleep(0.500)
250 |
251 | # Clean up
252 | system = xr.NULL_SYSTEM_ID
253 | xr.destroy_action_set(action_set)
254 | action_set = None
255 | xr.destroy_instance(instance)
256 | instance = None
257 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr.py:
--------------------------------------------------------------------------------
1 | from xr_examples.hello_xr.main import main
2 |
3 | if __name__ == "__main__":
4 | main()
5 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Based on example from https://github.com/KhronosGroup/OpenXR-SDK-Source/tree/main/src/tests/hello_xr
3 | """
4 |
5 | from xr_examples.hello_xr.main import main
6 |
7 | if __name__ == "__main__":
8 | main()
9 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/geometry.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2017-2022, The Khronos Group Inc.
3 | SPDX-License-Identifier: Apache-2.0
4 | """
5 |
6 | import ctypes
7 | import numpy
8 | import xr
9 |
10 |
11 | class Vertex(ctypes.Structure):
12 | _fields_ = [
13 | ("position", xr.Vector3f),
14 | ("color", xr.Vector3f),
15 | ]
16 |
17 |
18 | Red = numpy.array([1, 0, 0], dtype=numpy.float32)
19 | DarkRed = numpy.array([0.25, 0, 0], dtype=numpy.float32)
20 | Green = numpy.array([0, 1, 0], dtype=numpy.float32)
21 | DarkGreen = numpy.array([0, 0.25, 0], dtype=numpy.float32)
22 | Blue = numpy.array([0, 0, 1], dtype=numpy.float32)
23 | DarkBlue = numpy.array([0, 0, 0.25], dtype=numpy.float32)
24 |
25 | # Vertices for a 1x1x1 meter cube. (Left/Right, Top/Bottom, Front/Back)
26 | LBB = numpy.array([-0.5, -0.5, -0.5], dtype=numpy.float32)
27 | LBF = numpy.array([-0.5, -0.5, 0.5], dtype=numpy.float32)
28 | LTB = numpy.array([-0.5, 0.5, -0.5], dtype=numpy.float32)
29 | LTF = numpy.array([-0.5, 0.5, 0.5], dtype=numpy.float32)
30 | RBB = numpy.array([0.5, -0.5, -0.5], dtype=numpy.float32)
31 | RBF = numpy.array([0.5, -0.5, 0.5], dtype=numpy.float32)
32 | RTB = numpy.array([0.5, 0.5, -0.5], dtype=numpy.float32)
33 | RTF = numpy.array([0.5, 0.5, 0.5], dtype=numpy.float32)
34 |
35 |
36 | def cube_side(v1, v2, v3, v4, v5, v6, color):
37 | return numpy.array([
38 | [v1, color], [v2, color], [v3, color], [v4, color], [v5, color], [v6, color],
39 | ], dtype=numpy.float32)
40 |
41 |
42 | c_cubeVertices = numpy.array([
43 | cube_side(LTB, LBF, LBB, LTB, LTF, LBF, DarkRed), # -X
44 | cube_side(RTB, RBB, RBF, RTB, RBF, RTF, Red), # +X
45 | cube_side(LBB, LBF, RBF, LBB, RBF, RBB, DarkGreen), # -Y
46 | cube_side(LTB, RTB, RTF, LTB, RTF, LTF, Green), # +Y
47 | cube_side(LBB, RBB, RTB, LBB, RTB, LTB, DarkBlue), # -Z
48 | cube_side(LBF, LTF, RTF, LBF, RTF, RBF, Blue), # +Z
49 | ], dtype=numpy.float32)
50 |
51 | # Winding order is clockwise. Each side uses a different color.
52 | c_cubeIndices = numpy.array([
53 | 0, 1, 2, 3, 4, 5, # -X
54 | 6, 7, 8, 9, 10, 11, # +X
55 | 12, 13, 14, 15, 16, 17, # -Y
56 | 18, 19, 20, 21, 22, 23, # +Y
57 | 24, 25, 26, 27, 28, 29, # -Z
58 | 30, 31, 32, 33, 34, 35, # +Z
59 | ], dtype=numpy.uint16)
60 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/graphics_plugin.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import ctypes
3 | from typing import List, Union
4 |
5 | import xr
6 |
7 |
8 | class Cube(ctypes.Structure):
9 | _fields_ = [
10 | ("Pose", xr.Posef),
11 | ("Scale", xr.Vector3f),
12 | ]
13 |
14 |
15 | class IGraphicsPlugin(abc.ABC):
16 | """Wraps a graphics API so the main openxr program can be graphics API-independent."""
17 |
18 | @abc.abstractmethod
19 | def __enter__(self):
20 | pass
21 |
22 | @abc.abstractmethod
23 | def __exit__(self, exc_type, exc_val, exc_tb):
24 | """Clean up graphics resources."""
25 |
26 | """
27 | AllocateSwapchainImageStructs is not implemented in python.
28 | Because unlike C++, we can use the swapchain_image_type property.
29 | """
30 |
31 | @abc.abstractmethod
32 | def get_supported_swapchain_sample_count(self, xr_view_configuration_view: xr.ViewConfigurationView) -> int:
33 | """
34 | Get recommended number of sub-data element samples in view (recommendedSwapchainSampleCount)
35 | if supported by the graphics plugin. A supported value otherwise.
36 | """
37 |
38 | @property
39 | @abc.abstractmethod
40 | def graphics_binding(self) -> ctypes.Structure:
41 | """Get the graphics binding header for session creation."""
42 |
43 | @abc.abstractmethod
44 | def initialize_device(self, instance: xr.Instance, system_id: xr.SystemId) -> None:
45 | """Create an instance of this graphics api for the provided instance and systemId."""
46 |
47 | @property
48 | @abc.abstractmethod
49 | def instance_extensions(self) -> List[str]:
50 | """OpenXR extensions required by this graphics API."""
51 |
52 | @abc.abstractmethod
53 | def poll_events(self) -> bool:
54 | """"""
55 |
56 | @abc.abstractmethod
57 | def render_view(self, layer_view: xr.CompositionLayerProjectionView, swapchain_image: xr.SwapchainImageBaseHeader,
58 | swapchain_format: int, cubes: List[Cube], mirror=False):
59 | """Render to a swapchain image for a projection view."""
60 |
61 | @abc.abstractmethod
62 | def select_color_swapchain_format(self, runtime_formats: Union[List[int], ctypes.Array]) -> int:
63 | """Select the preferred swapchain format from the list of available formats."""
64 |
65 | @property
66 | @abc.abstractmethod
67 | def swapchain_image_type(self):
68 | """The type of xr swapchain image used by this graphics plugin."""
69 |
70 | @abc.abstractmethod
71 | def update_options(self, options) -> None:
72 | pass
73 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/graphics_plugin_opengl.py:
--------------------------------------------------------------------------------
1 | from ctypes import byref, c_void_p, cast, sizeof, POINTER, Structure
2 | import inspect
3 | import logging
4 | import platform
5 | from typing import Dict, List, Optional
6 |
7 | import numpy
8 | from OpenGL import GL
9 |
10 | from .graphics_plugin import Cube, IGraphicsPlugin
11 |
12 | if platform.system() == "Windows":
13 | from OpenGL import WGL
14 | elif platform.system() == "Linux":
15 | from OpenGL import GLX
16 | import glfw
17 |
18 | import xr
19 |
20 | from .geometry import c_cubeVertices, c_cubeIndices, Vertex
21 | from .linear import GraphicsAPI, Matrix4x4f
22 | from .options import Options
23 |
24 | logger = logging.getLogger("hello_xr.graphics_plugin_opengl")
25 |
26 |
27 | dark_slate_gray = numpy.array([0.184313729, 0.309803933, 0.309803933, 1.0], dtype=numpy.float32)
28 |
29 | vertex_shader_glsl = inspect.cleandoc("""
30 | #version 410
31 |
32 | in vec3 VertexPos;
33 | in vec3 VertexColor;
34 |
35 | out vec3 PSVertexColor;
36 |
37 | uniform mat4 ModelViewProjection;
38 |
39 | void main() {
40 | gl_Position = ModelViewProjection * vec4(VertexPos, 1.0);
41 | PSVertexColor = VertexColor;
42 | }
43 | """)
44 |
45 | fragment_shader_glsl = inspect.cleandoc("""
46 | #version 410
47 |
48 | in vec3 PSVertexColor;
49 | out vec4 FragColor;
50 |
51 | void main() {
52 | FragColor = vec4(PSVertexColor, 1);
53 | }
54 | """)
55 |
56 |
57 | class OpenGLGraphicsPlugin(IGraphicsPlugin):
58 | def __init__(self, options: Options):
59 | super().__init__()
60 | self.window = None
61 |
62 | self.background_clear_color = options.background_clear_color
63 | if platform.system() == "Windows":
64 | self._graphics_binding = xr.GraphicsBindingOpenGLWin32KHR()
65 | elif platform.system() == "Linux":
66 | # TODO more nuance on Linux: Xlib, Xcb, Wayland
67 | self._graphics_binding = xr.GraphicsBindingOpenGLXlibKHR()
68 | self.swapchain_image_buffers: List[xr.SwapchainImageOpenGLKHR] = [] # To keep the swapchain images alive
69 | self.swapchain_framebuffer: Optional[int] = None
70 | self.program = None
71 | self.model_view_projection_uniform_location = 0
72 | self.vertex_attrib_coords = 0
73 | self.vertex_attrib_color = 0
74 | self.vao = None
75 | self.cube_vertex_buffer = None
76 | self.cube_index_buffer = None
77 | # Map color buffer to associated depth buffer. This map is populated on demand.
78 | self.color_to_depth_map: Dict[int, int] = {}
79 | self.debug_message_proc = None # To keep the callback alive
80 |
81 | def __enter__(self):
82 | return self
83 |
84 | def __exit__(self, exc_type, exc_val, exc_tb):
85 | if self.window:
86 | glfw.make_context_current(self.window)
87 | if self.swapchain_framebuffer is not None:
88 | GL.glDeleteFramebuffers(1, [self.swapchain_framebuffer])
89 | if self.program is not None:
90 | GL.glDeleteProgram(self.program)
91 | if self.vao is not None:
92 | GL.glDeleteVertexArrays(1, [self.vao])
93 | if self.cube_vertex_buffer is not None:
94 | GL.glDeleteBuffers(1, [self.cube_vertex_buffer])
95 | if self.cube_index_buffer is not None:
96 | GL.glDeleteBuffers(1, [self.cube_index_buffer])
97 | self.swapchain_framebuffer = None
98 | self.program = None
99 | self.vao = None
100 | self.cube_vertex_buffer = None
101 | self.cube_index_buffer = None
102 | for color, depth in self.color_to_depth_map.items():
103 | if depth is not None:
104 | GL.glDeleteTextures(1, [depth])
105 | self.color_to_depth_map = {}
106 | if self.window is not None:
107 | glfw.destroy_window(self.window)
108 | self.window = None
109 | glfw.terminate()
110 |
111 | @staticmethod
112 | def check_shader(shader):
113 | result = GL.glGetShaderiv(shader, GL.GL_COMPILE_STATUS)
114 | if not result:
115 | raise RuntimeError(f"Compile shader failed: {GL.glGetShaderInfoLog(shader)}")
116 |
117 | @staticmethod
118 | def check_program(prog):
119 | result = GL.glGetProgramiv(prog, GL.GL_LINK_STATUS)
120 | if not result:
121 | raise RuntimeError(f"Link program failed: {GL.glGetProgramInfoLog(prog)}")
122 |
123 | @staticmethod
124 | def opengl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user):
125 | """Redirect OpenGL debug messages"""
126 | log_level = {
127 | GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR,
128 | GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING,
129 | GL.GL_DEBUG_SEVERITY_LOW: logging.INFO,
130 | GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG,
131 | }[severity]
132 | logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}")
133 |
134 | def focus_window(self):
135 | glfw.focus_window(self.window)
136 | glfw.make_context_current(self.window)
137 |
138 | def get_depth_texture(self, color_texture) -> int:
139 | # If a depth-stencil view has already been created for this back-buffer, use it.
140 | if color_texture in self.color_to_depth_map:
141 | return self.color_to_depth_map[color_texture]
142 | # This back-buffer has no corresponding depth-stencil texture, so create one with matching dimensions.
143 | GL.glBindTexture(GL.GL_TEXTURE_2D, color_texture)
144 | width = GL.glGetTexLevelParameteriv(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_WIDTH)
145 | height = GL.glGetTexLevelParameteriv(GL.GL_TEXTURE_2D, 0, GL.GL_TEXTURE_HEIGHT)
146 |
147 | depth_texture = GL.glGenTextures(1)
148 | GL.glBindTexture(GL.GL_TEXTURE_2D, depth_texture)
149 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
150 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
151 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE)
152 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE)
153 | GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_DEPTH_COMPONENT32, width, height, 0, GL.GL_DEPTH_COMPONENT, GL.GL_FLOAT, None)
154 | self.color_to_depth_map[color_texture] = depth_texture
155 | return depth_texture
156 |
157 | def get_supported_swapchain_sample_count(self, _xr_view_configuration_view: xr.ViewConfigurationView):
158 | return 1
159 |
160 | @property
161 | def instance_extensions(self) -> List[str]:
162 | return [xr.KHR_OPENGL_ENABLE_EXTENSION_NAME]
163 |
164 | @property
165 | def swapchain_image_type(self):
166 | return xr.SwapchainImageOpenGLKHR
167 |
168 | @property
169 | def graphics_binding(self) -> Structure:
170 | return self._graphics_binding
171 |
172 | def initialize_device(self, instance: xr.Instance, system_id: xr.SystemId):
173 | # extension function must be loaded by name
174 | pfn_get_open_gl_graphics_requirements_khr = cast(
175 | xr.get_instance_proc_addr(
176 | instance,
177 | "xrGetOpenGLGraphicsRequirementsKHR",
178 | ),
179 | xr.PFN_xrGetOpenGLGraphicsRequirementsKHR
180 | )
181 | graphics_requirements = xr.GraphicsRequirementsOpenGLKHR()
182 | result = pfn_get_open_gl_graphics_requirements_khr(instance, system_id, byref(graphics_requirements))
183 | result = xr.check_result(xr.Result(result))
184 | if result.is_exception():
185 | raise result
186 | # Initialize the gl extensions. Note we have to open a window.
187 | if not glfw.init():
188 | raise xr.XrException("GLFW initialization failed")
189 | glfw.window_hint(glfw.DOUBLEBUFFER, False)
190 | glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
191 | glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 5)
192 | glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
193 | self.window = glfw.create_window(640, 480, "GLFW Window", None, None)
194 | if self.window is None:
195 | raise xr.XrException("Failed to create GLFW window")
196 | glfw.make_context_current(self.window)
197 | glfw.show_window(self.window)
198 | glfw.swap_interval(0)
199 | glfw.focus_window(self.window)
200 | major = GL.glGetIntegerv(GL.GL_MAJOR_VERSION)
201 | minor = GL.glGetIntegerv(GL.GL_MINOR_VERSION)
202 | logger.debug(f"OpenGL version {major}.{minor}")
203 | desired_api_version = xr.Version(major, minor, 0)
204 | if graphics_requirements.min_api_version_supported > desired_api_version.number():
205 | ms = xr.Version(graphics_requirements.min_api_version_supported).number()
206 | raise xr.XrException(f"Runtime does not support desired Graphics API and/or version {hex(ms)}")
207 | if platform.system() == "Windows":
208 | self._graphics_binding.h_dc = WGL.wglGetCurrentDC()
209 | self._graphics_binding.h_glrc = WGL.wglGetCurrentContext()
210 | elif platform.system() == "Linux":
211 | # TODO more nuance on Linux: Xlib, Xcb, Wayland
212 | self._graphics_binding.x_display = GLX.glXGetCurrentDisplay()
213 | self._graphics_binding.glx_drawable = GLX.glXGetCurrentDrawable()
214 | self._graphics_binding.glx_context = GLX.glXGetCurrentContext()
215 | GL.glEnable(GL.GL_DEBUG_OUTPUT)
216 | # Store the debug callback function pointer, so it won't get garbage collected;
217 | # ...otherwise mysterious GL crashes will ensue.
218 | self.debug_message_proc = GL.GLDEBUGPROC(self.opengl_debug_message_callback)
219 | GL.glDebugMessageCallback(self.debug_message_proc, None)
220 | self.initialize_resources()
221 |
222 | def initialize_resources(self):
223 | self.swapchain_framebuffer = GL.glGenFramebuffers(1)
224 | vertex_shader = GL.glCreateShader(GL.GL_VERTEX_SHADER)
225 | GL.glShaderSource(vertex_shader, vertex_shader_glsl)
226 | GL.glCompileShader(vertex_shader)
227 | self.check_shader(vertex_shader)
228 | fragment_shader = GL.glCreateShader(GL.GL_FRAGMENT_SHADER)
229 | GL.glShaderSource(fragment_shader, fragment_shader_glsl)
230 | GL.glCompileShader(fragment_shader)
231 | self.check_shader(fragment_shader)
232 | self.program = GL.glCreateProgram()
233 | GL.glAttachShader(self.program, vertex_shader)
234 | GL.glAttachShader(self.program, fragment_shader)
235 | GL.glLinkProgram(self.program)
236 | self.check_program(self.program)
237 | GL.glDeleteShader(vertex_shader)
238 | GL.glDeleteShader(fragment_shader)
239 | self.model_view_projection_uniform_location = GL.glGetUniformLocation(self.program, "ModelViewProjection")
240 | self.vertex_attrib_coords = GL.glGetAttribLocation(self.program, "VertexPos")
241 | self.vertex_attrib_color = GL.glGetAttribLocation(self.program, "VertexColor")
242 | self.cube_vertex_buffer = GL.glGenBuffers(1)
243 | GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.cube_vertex_buffer)
244 | GL.glBufferData(GL.GL_ARRAY_BUFFER, c_cubeVertices, GL.GL_STATIC_DRAW)
245 | self.cube_index_buffer = GL.glGenBuffers(1)
246 | GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.cube_index_buffer)
247 | GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, c_cubeIndices, GL.GL_STATIC_DRAW)
248 | self.vao = GL.glGenVertexArrays(1)
249 | GL.glBindVertexArray(self.vao)
250 | GL.glEnableVertexAttribArray(self.vertex_attrib_coords)
251 | GL.glEnableVertexAttribArray(self.vertex_attrib_color)
252 | GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.cube_vertex_buffer)
253 | GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.cube_index_buffer)
254 | GL.glVertexAttribPointer(self.vertex_attrib_coords, 3, GL.GL_FLOAT, False,
255 | sizeof(Vertex), cast(0, c_void_p))
256 | GL.glVertexAttribPointer(self.vertex_attrib_color, 3, GL.GL_FLOAT, False,
257 | sizeof(Vertex),
258 | cast(sizeof(xr.Vector3f), c_void_p))
259 |
260 | def poll_events(self) -> bool:
261 | glfw.poll_events()
262 | return glfw.window_should_close(self.window)
263 |
264 | def render_view(
265 | self,
266 | layer_view: xr.CompositionLayerProjectionView,
267 | swapchain_image_base_ptr: POINTER(xr.SwapchainImageBaseHeader),
268 | _swapchain_format: int,
269 | cubes: List[Cube],
270 | mirror=False,
271 | ):
272 | assert layer_view.sub_image.image_array_index == 0 # texture arrays not supported.
273 | # UNUSED_PARM(swapchain_format) # not used in this function for now.
274 | glfw.make_context_current(self.window)
275 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self.swapchain_framebuffer)
276 | swapchain_image = cast(swapchain_image_base_ptr, POINTER(xr.SwapchainImageOpenGLKHR)).contents
277 | color_texture = swapchain_image.image
278 | GL.glViewport(layer_view.sub_image.image_rect.offset.x,
279 | layer_view.sub_image.image_rect.offset.y,
280 | layer_view.sub_image.image_rect.extent.width,
281 | layer_view.sub_image.image_rect.extent.height)
282 | GL.glFrontFace(GL.GL_CW)
283 | GL.glCullFace(GL.GL_BACK)
284 | GL.glEnable(GL.GL_CULL_FACE)
285 | GL.glEnable(GL.GL_DEPTH_TEST)
286 | depth_texture = self.get_depth_texture(color_texture)
287 | GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, GL.GL_TEXTURE_2D, color_texture, 0)
288 | GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_DEPTH_ATTACHMENT, GL.GL_TEXTURE_2D, depth_texture, 0)
289 | # Clear swapchain and depth buffer.
290 | GL.glClearColor(*self.background_clear_color)
291 | GL.glClearDepth(1.0)
292 | GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT | GL.GL_STENCIL_BUFFER_BIT)
293 | # Set shaders and uniform variables.
294 | GL.glUseProgram(self.program)
295 | pose = layer_view.pose
296 | proj = Matrix4x4f.create_projection_fov(GraphicsAPI.OPENGL, layer_view.fov, 0.05, 100.0)
297 | scale = xr.Vector3f(1, 1, 1)
298 | to_view = Matrix4x4f.create_translation_rotation_scale(pose.position, pose.orientation, scale)
299 | view = Matrix4x4f.invert_rigid_body(to_view)
300 | vp = proj @ view
301 | # Set cube primitive data.
302 | GL.glBindVertexArray(self.vao)
303 | # Render each cube
304 | for cube in cubes:
305 | # Compute the model-view-projection transform and set it.
306 | model = Matrix4x4f.create_translation_rotation_scale(cube.Pose.position, cube.Pose.orientation, cube.Scale)
307 | mvp = vp @ model
308 | GL.glUniformMatrix4fv(self.model_view_projection_uniform_location, 1, False, mvp.as_numpy())
309 | # Draw the cube.
310 | GL.glDrawElements(GL.GL_TRIANGLES, len(c_cubeIndices), GL.GL_UNSIGNED_SHORT, None)
311 |
312 | if mirror:
313 | # fast blit from the fbo to the window surface
314 | GL.glBindFramebuffer(GL.GL_DRAW_FRAMEBUFFER, 0)
315 | w, h = layer_view.sub_image.image_rect.extent.width, layer_view.sub_image.image_rect.extent.height
316 | GL.glBlitFramebuffer(
317 | 0, 0, w, h, 0, 0,
318 | 640, 480,
319 | GL.GL_COLOR_BUFFER_BIT,
320 | GL.GL_NEAREST
321 | )
322 |
323 | GL.glBindVertexArray(0)
324 | GL.glUseProgram(0)
325 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0)
326 |
327 | def select_color_swapchain_format(self, runtime_formats):
328 | # List of supported color swapchain formats.
329 | supported_color_swapchain_formats = [
330 | GL.GL_RGB10_A2,
331 | GL.GL_RGBA16F,
332 | # The two below should only be used as a fallback, as they are linear color formats without enough bits for color
333 | # depth, thus leading to banding.
334 | GL.GL_RGBA8,
335 | GL.GL_RGBA8_SNORM,
336 | # These two below are the only color formats reported by Steam VR beta 1.24.2
337 | GL.GL_SRGB8,
338 | GL.GL_SRGB8_ALPHA8,
339 | ]
340 | for rf in runtime_formats:
341 | for sf in supported_color_swapchain_formats:
342 | if rf == sf:
343 | return sf
344 | raise RuntimeError("No runtime swapchain format supported for color swapchain")
345 |
346 | def update_options(self, options) -> None:
347 | self.background_clear_color = options.background_clear_color
348 |
349 | def window_should_close(self):
350 | return glfw.window_should_close(self.window)
351 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/linear.py:
--------------------------------------------------------------------------------
1 | """
2 | # Copyright (c) 2017 The Khronos Group Inc.
3 | # Copyright (c) 2016 Oculus VR, LLC.
4 | #
5 | # SPDX-License-Identifier: Apache-2.0
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License")
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http:#www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | #
19 | # Author: J.M.P. van Waveren
20 | #
21 | """
22 |
23 | from ctypes import addressof, c_float, Structure
24 | import enum
25 | import math
26 | import numpy
27 |
28 | import xr
29 |
30 |
31 | class GraphicsAPI(enum.Enum):
32 | VULKAN = 0,
33 | OPENGL = 1,
34 | OPENGL_ES = 2,
35 | D3D = 3,
36 |
37 |
38 | # Column-major, pre-multiplied. This type does not exist in the OpenXR API and is provided for convenience.
39 | class Matrix4x4f(Structure):
40 | _fields_ = [("m", c_float * 16), ]
41 |
42 | def __init__(self):
43 | super().__init__()
44 | self._numpy = None
45 |
46 | def __matmul__(self, other) -> "Matrix4x4f":
47 | return self.multiply(other)
48 |
49 | def as_numpy(self):
50 | if not hasattr(self, "_numpy") or self._numpy is None:
51 | # Just in time construction
52 | buffer = (c_float * 16).from_address(addressof(self))
53 | buffer._wrapper = self # To link lifetime of buffer to self
54 | self._numpy = numpy.ctypeslib.as_array(buffer)
55 | return self._numpy
56 |
57 | @staticmethod
58 | def create_from_quaternion(quat: xr.Quaternionf) -> "Matrix4x4f":
59 | """ Creates a matrix from a quaternion. """
60 | x2 = quat.x + quat.x
61 | y2 = quat.y + quat.y
62 | z2 = quat.z + quat.z
63 |
64 | xx2 = quat.x * x2
65 | yy2 = quat.y * y2
66 | zz2 = quat.z * z2
67 |
68 | yz2 = quat.y * z2
69 | wx2 = quat.w * x2
70 | xy2 = quat.x * y2
71 | wz2 = quat.w * z2
72 | xz2 = quat.x * z2
73 | wy2 = quat.w * y2
74 |
75 | result = Matrix4x4f()
76 | result.m[0] = 1.0 - yy2 - zz2
77 | result.m[1] = xy2 + wz2
78 | result.m[2] = xz2 - wy2
79 | result.m[3] = 0.0
80 |
81 | result.m[4] = xy2 - wz2
82 | result.m[5] = 1.0 - xx2 - zz2
83 | result.m[6] = yz2 + wx2
84 | result.m[7] = 0.0
85 |
86 | result.m[8] = xz2 + wy2
87 | result.m[9] = yz2 - wx2
88 | result.m[10] = 1.0 - xx2 - yy2
89 | result.m[11] = 0.0
90 |
91 | result.m[12] = 0.0
92 | result.m[13] = 0.0
93 | result.m[14] = 0.0
94 | result.m[15] = 1.0
95 | return result
96 |
97 | @staticmethod
98 | def create_projection(graphics_api: GraphicsAPI, tan_angle_left: float, tan_angle_right: float, tan_angle_up: float,
99 | tan_angle_down: float, near_z: float, far_z: float) -> "Matrix4x4f":
100 | """
101 | # Creates a projection matrix based on the specified dimensions.
102 | # The projection matrix transforms -Z=forward, +Y=up, +X=right to the appropriate clip space for the graphics API.
103 | # The far plane is placed at infinity if far_z <= near_z.
104 | # An infinite projection matrix is preferred for rasterization because, except for
105 | # things *right* up against the near plane, it always provides better precision:
106 | # "Tightening the Precision of Perspective Rendering"
107 | # Paul Upchurch, Mathieu Desbrun
108 | # Journal of Graphics Tools, Volume 16, Issue 1, 2012
109 | """
110 | tan_angle_width = tan_angle_right - tan_angle_left
111 | # Set to tan_angle_down - tan_angle_up for a clip space with positive Y down (Vulkan).
112 | # Set to tan_angle_up - tan_angle_down for a clip space with positive Y up (OpenGL / D3D / Metal).
113 | tan_angle_height = (tan_angle_down - tan_angle_up) if graphics_api == GraphicsAPI.VULKAN else (tan_angle_up - tan_angle_down)
114 | # Set to near_z for a [-1,1] Z clip space (OpenGL / OpenGL ES).
115 | # Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal).
116 | offset_z = near_z if graphics_api == GraphicsAPI.OPENGL or graphics_api == GraphicsAPI.OPENGL_ES else 0
117 | result = Matrix4x4f()
118 | if far_z <= near_z:
119 | # place the far plane at infinity
120 | result.m[0] = 2.0 / tan_angle_width
121 | result.m[4] = 0.0
122 | result.m[8] = (tan_angle_right + tan_angle_left) / tan_angle_width
123 | result.m[12] = 0.0
124 |
125 | result.m[1] = 0.0
126 | result.m[5] = 2.0 / tan_angle_height
127 | result.m[9] = (tan_angle_up + tan_angle_down) / tan_angle_height
128 | result.m[13] = 0.0
129 |
130 | result.m[2] = 0.0
131 | result.m[6] = 0.0
132 | result.m[10] = -1.0
133 | result.m[14] = -(near_z + offset_z)
134 |
135 | result.m[3] = 0.0
136 | result.m[7] = 0.0
137 | result.m[11] = -1.0
138 | result.m[15] = 0.0
139 | else:
140 | # normal projection
141 | result.m[0] = 2.0 / tan_angle_width
142 | result.m[4] = 0.0
143 | result.m[8] = (tan_angle_right + tan_angle_left) / tan_angle_width
144 | result.m[12] = 0.0
145 |
146 | result.m[1] = 0.0
147 | result.m[5] = 2.0 / tan_angle_height
148 | result.m[9] = (tan_angle_up + tan_angle_down) / tan_angle_height
149 | result.m[13] = 0.0
150 |
151 | result.m[2] = 0.0
152 | result.m[6] = 0.0
153 | result.m[10] = -(far_z + offset_z) / (far_z - near_z)
154 | result.m[14] = -(far_z * (near_z + offset_z)) / (far_z - near_z)
155 |
156 | result.m[3] = 0.0
157 | result.m[7] = 0.0
158 | result.m[11] = -1.0
159 | result.m[15] = 0.0
160 | return result
161 |
162 | @staticmethod
163 | def create_projection_fov(graphics_api: GraphicsAPI, fov: xr.Fovf, near_z: float, far_z: float) -> "Matrix4x4f":
164 | """ Creates a projection matrix based on the specified FOV. """
165 | tan_left = math.tan(fov.angle_left)
166 | tan_right = math.tan(fov.angle_right)
167 | tan_down = math.tan(fov.angle_down)
168 | tan_up = math.tan(fov.angle_up)
169 | return Matrix4x4f.create_projection(graphics_api, tan_left, tan_right, tan_up, tan_down, near_z, far_z)
170 |
171 | @staticmethod
172 | def create_scale(x: float, y: float, z: float) -> "Matrix4x4f":
173 | """ Creates a scale matrix. """
174 | result = Matrix4x4f()
175 | result.m[0] = x
176 | result.m[1] = 0.0
177 | result.m[2] = 0.0
178 | result.m[3] = 0.0
179 | result.m[4] = 0.0
180 | result.m[5] = y
181 | result.m[6] = 0.0
182 | result.m[7] = 0.0
183 | result.m[8] = 0.0
184 | result.m[9] = 0.0
185 | result.m[10] = z
186 | result.m[11] = 0.0
187 | result.m[12] = 0.0
188 | result.m[13] = 0.0
189 | result.m[14] = 0.0
190 | result.m[15] = 1.0
191 | return result
192 |
193 | @staticmethod
194 | def create_translation(x: float, y: float, z: float) -> "Matrix4x4f":
195 | """ Creates a translation matrix. """
196 | result = Matrix4x4f()
197 | result.m[0] = 1.0
198 | result.m[1] = 0.0
199 | result.m[2] = 0.0
200 | result.m[3] = 0.0
201 | result.m[4] = 0.0
202 | result.m[5] = 1.0
203 | result.m[6] = 0.0
204 | result.m[7] = 0.0
205 | result.m[8] = 0.0
206 | result.m[9] = 0.0
207 | result.m[10] = 1.0
208 | result.m[11] = 0.0
209 | result.m[12] = x
210 | result.m[13] = y
211 | result.m[14] = z
212 | result.m[15] = 1.0
213 | return result
214 |
215 | @staticmethod
216 | def create_translation_rotation_scale(translation: xr.Vector3f, rotation: xr.Quaternionf, scale: xr.Vector3f) -> "Matrix4x4f":
217 | """ Creates a combined translation(rotation(scale(object))) matrix. """
218 | scale_matrix = Matrix4x4f.create_scale(*scale)
219 | rotation_matrix = Matrix4x4f.create_from_quaternion(rotation)
220 | translation_matrix = Matrix4x4f.create_translation(*translation)
221 | combined_matrix = rotation_matrix @ scale_matrix
222 | return translation_matrix @ combined_matrix
223 |
224 | def multiply(self, b: "Matrix4x4f") -> "Matrix4x4f":
225 | """ Use left-multiplication to accumulate transformations. """
226 | result = Matrix4x4f()
227 | result.m[0] = self.m[0] * b.m[0] + self.m[4] * b.m[1] + self.m[8] * b.m[2] + self.m[12] * b.m[3]
228 | result.m[1] = self.m[1] * b.m[0] + self.m[5] * b.m[1] + self.m[9] * b.m[2] + self.m[13] * b.m[3]
229 | result.m[2] = self.m[2] * b.m[0] + self.m[6] * b.m[1] + self.m[10] * b.m[2] + self.m[14] * b.m[3]
230 | result.m[3] = self.m[3] * b.m[0] + self.m[7] * b.m[1] + self.m[11] * b.m[2] + self.m[15] * b.m[3]
231 |
232 | result.m[4] = self.m[0] * b.m[4] + self.m[4] * b.m[5] + self.m[8] * b.m[6] + self.m[12] * b.m[7]
233 | result.m[5] = self.m[1] * b.m[4] + self.m[5] * b.m[5] + self.m[9] * b.m[6] + self.m[13] * b.m[7]
234 | result.m[6] = self.m[2] * b.m[4] + self.m[6] * b.m[5] + self.m[10] * b.m[6] + self.m[14] * b.m[7]
235 | result.m[7] = self.m[3] * b.m[4] + self.m[7] * b.m[5] + self.m[11] * b.m[6] + self.m[15] * b.m[7]
236 |
237 | result.m[8] = self.m[0] * b.m[8] + self.m[4] * b.m[9] + self.m[8] * b.m[10] + self.m[12] * b.m[11]
238 | result.m[9] = self.m[1] * b.m[8] + self.m[5] * b.m[9] + self.m[9] * b.m[10] + self.m[13] * b.m[11]
239 | result.m[10] = self.m[2] * b.m[8] + self.m[6] * b.m[9] + self.m[10] * b.m[10] + self.m[14] * b.m[11]
240 | result.m[11] = self.m[3] * b.m[8] + self.m[7] * b.m[9] + self.m[11] * b.m[10] + self.m[15] * b.m[11]
241 |
242 | result.m[12] = self.m[0] * b.m[12] + self.m[4] * b.m[13] + self.m[8] * b.m[14] + self.m[12] * b.m[15]
243 | result.m[13] = self.m[1] * b.m[12] + self.m[5] * b.m[13] + self.m[9] * b.m[14] + self.m[13] * b.m[15]
244 | result.m[14] = self.m[2] * b.m[12] + self.m[6] * b.m[13] + self.m[10] * b.m[14] + self.m[14] * b.m[15]
245 | result.m[15] = self.m[3] * b.m[12] + self.m[7] * b.m[13] + self.m[11] * b.m[14] + self.m[15] * b.m[15]
246 | return result
247 |
248 | """ Calculates the inverse of a rigid body transform. """
249 | def invert_rigid_body(self) -> "Matrix4x4f":
250 | result = Matrix4x4f()
251 | result.m[0] = self.m[0]
252 | result.m[1] = self.m[4]
253 | result.m[2] = self.m[8]
254 | result.m[3] = 0.0
255 | result.m[4] = self.m[1]
256 | result.m[5] = self.m[5]
257 | result.m[6] = self.m[9]
258 | result.m[7] = 0.0
259 | result.m[8] = self.m[2]
260 | result.m[9] = self.m[6]
261 | result.m[10] = self.m[10]
262 | result.m[11] = 0.0
263 | result.m[12] = -(self.m[0] * self.m[12] + self.m[1] * self.m[13] + self.m[2] * self.m[14])
264 | result.m[13] = -(self.m[4] * self.m[12] + self.m[5] * self.m[13] + self.m[6] * self.m[14])
265 | result.m[14] = -(self.m[8] * self.m[12] + self.m[9] * self.m[13] + self.m[10] * self.m[14])
266 | result.m[15] = 1.0
267 | return result
268 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/main.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | """
4 |
5 | import argparse
6 | import logging
7 | import platform
8 | import sys
9 | import threading
10 | import time
11 |
12 | import xr
13 |
14 | from xr_examples.hello_xr.graphics_plugin import IGraphicsPlugin
15 | from xr_examples.hello_xr.platform_plugin import IPlatformPlugin
16 | from xr_examples.hello_xr.openxr_program import OpenXRProgram
17 | from xr_examples.hello_xr.graphics_plugin_opengl import OpenGLGraphicsPlugin
18 | from xr_examples.hello_xr.platform_plugin_win32 import Win32PlatformPlugin
19 | from xr_examples.hello_xr.platform_plugin_xlib import XlibPlatformPlugin
20 | from .options import Options
21 |
22 | key_press_event = threading.Event()
23 | logger = logging.getLogger("hello_xr.main")
24 |
25 |
26 | def create_graphics_plugin(options: argparse.Namespace) -> IGraphicsPlugin:
27 | """Create a graphics plugin for the graphics API specified in the options."""
28 | graphics_plugin_map = {
29 | "OpenGL": OpenGLGraphicsPlugin,
30 | }
31 | if options.graphics_plugin not in graphics_plugin_map:
32 | raise NotImplementedError
33 | return graphics_plugin_map[options.graphics_plugin](options)
34 |
35 |
36 | def create_platform_plugin(_options: argparse.Namespace) -> IPlatformPlugin:
37 | if platform.system() == "Windows":
38 | return Win32PlatformPlugin()
39 | elif platform.system() == "Linux":
40 | return XlibPlatformPlugin()
41 | raise NotImplementedError
42 |
43 |
44 | def poll_keyboard():
45 | logger.info("Press any key to shutdown...")
46 | try:
47 | sys.stdin.read(1)
48 | key_press_event.set()
49 | logger.debug("A key was pressed")
50 | finally:
51 | pass
52 |
53 |
54 | def main():
55 | logging.basicConfig(
56 | format="%(asctime)s.%(msecs)03d %(module)s %(levelname)s: %(message)s",
57 | datefmt='%m/%d/%y %I:%M:%S',
58 | level=logging.INFO,
59 | )
60 | options = Options()
61 | update_options_from_command_line(options)
62 |
63 | # Install keyboard handler to exit on keypress
64 | threading.Thread(target=poll_keyboard, daemon=True).start()
65 |
66 | request_restart = False
67 | while True:
68 | # Create platform-specific implementation.
69 | platform_plugin = create_platform_plugin(options)
70 | # Create graphics API implementation.
71 | with create_graphics_plugin(options) as graphics_plugin, \
72 | OpenXRProgram(options, platform_plugin, graphics_plugin) as program:
73 | program.create_instance()
74 | program.initialize_system()
75 |
76 | options.set_environment_blend_mode(program.preferred_blend_mode)
77 | update_options_from_command_line(options)
78 | platform_plugin.update_options(options)
79 | graphics_plugin.update_options(options)
80 |
81 | program.initialize_device()
82 | program.initialize_session()
83 | program.create_swapchains()
84 | while not key_press_event.is_set():
85 | # glfw notices when you click the close button
86 | exit_render_loop = graphics_plugin.poll_events()
87 | if exit_render_loop:
88 | break
89 | exit_render_loop, request_restart = program.poll_events()
90 | if exit_render_loop:
91 | break
92 | if program.session_running:
93 | try:
94 | program.poll_actions()
95 | except xr.exception.SessionNotFocused:
96 | # TODO: C++ code does not need this conditional. Why does python?
97 | pass
98 | program.render_frame()
99 | else:
100 | # Throttle loop since xrWaitFrame won't be called.
101 | time.sleep(0.250)
102 | if key_press_event.is_set() or not request_restart:
103 | break
104 |
105 |
106 | def update_options_from_command_line(options: Options) -> bool:
107 | parser = argparse.ArgumentParser()
108 | parser.add_argument(
109 | "--graphics", "-g",
110 | choices=["D3D11", "D3D12", "OpenGLES", "OpenGL", "Vulkan2", "Vulkan"],
111 | )
112 | parser.add_argument(
113 | "--formfactor", "-ff",
114 | choices=["Hmd", "Handheld"],
115 | )
116 | parser.add_argument(
117 | "--viewconfig", "-vc",
118 | choices=["Mono", "Stereo"],
119 | )
120 | parser.add_argument(
121 | "--blendmode", "-bm",
122 | choices=["Opaque", "Additive", "AlphaBlend"],
123 | )
124 | parser.add_argument(
125 | "--space", "-s",
126 | choices=["View", "Local", "Stage"],
127 | )
128 | parser.add_argument(
129 | "--verbose", "-v", action="store_true",
130 | )
131 | parsed = parser.parse_args()
132 | if parsed.verbose:
133 | logging.getLogger("hello_xr").setLevel(logging.DEBUG)
134 | if parsed.graphics is not None:
135 | options.graphics_plugin = parsed.graphics
136 | if parsed.formfactor is not None:
137 | options.form_factor = parsed.formfactor
138 | if parsed.viewconfig is not None:
139 | options.view_configuration = parsed.viewconfig
140 | if parsed.blendmode is not None:
141 | options.environment_blend_mode = parsed.blendmode
142 | if parsed.space is not None:
143 | options.app_space = parsed.space
144 | options.parse_strings()
145 | return True
146 |
147 |
148 | if __name__ == "__main__":
149 | main()
150 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/openxr_program.py:
--------------------------------------------------------------------------------
1 | """
2 |
3 | """
4 |
5 | import argparse
6 | import ctypes
7 | from ctypes import (
8 | addressof,
9 | byref,
10 | c_float,
11 | c_int32,
12 | c_void_p,
13 | cast,
14 | pointer,
15 | POINTER,
16 | Structure,
17 | )
18 | import enum
19 | import logging
20 | import math
21 | import platform
22 | from typing import List, Optional
23 |
24 | import xr.raw_functions
25 |
26 | from .graphics_plugin import Cube, IGraphicsPlugin
27 | from .platform_plugin import IPlatformPlugin
28 | from .options import Options
29 |
30 | logger = logging.getLogger("hello_xr.program")
31 |
32 |
33 | class Math(object):
34 | class Pose(object):
35 | @staticmethod
36 | def identity():
37 | t = xr.Posef()
38 | assert t.orientation.w == 1
39 | return t
40 |
41 | @staticmethod
42 | def translation(translation: List[float]):
43 | t = Math.Pose.identity()
44 | t.position[:] = translation[:]
45 | return t
46 |
47 | @staticmethod
48 | def rotate_ccw_about_y_axis(radians: float, translation: List[float]):
49 | t = Math.Pose.identity()
50 | t.orientation.x = 0
51 | t.orientation.y = math.sin(radians * 0.5)
52 | t.orientation.z = 0
53 | t.orientation.w = math.cos(radians * 0.5)
54 | t.position[:] = translation[:]
55 | return t
56 |
57 |
58 | class Side(enum.IntEnum):
59 | LEFT = 0
60 | RIGHT = 1
61 |
62 |
63 | class Swapchain(Structure):
64 | _fields_ = [
65 | ("handle", xr.Swapchain),
66 | ("width", c_int32),
67 | ("height", c_int32),
68 | ]
69 |
70 |
71 | class OpenXRProgram(object):
72 | def __init__(self, options: argparse.Namespace, platform_plugin: IPlatformPlugin, graphics_plugin: IGraphicsPlugin):
73 | self.options = options
74 | self.platform_plugin = platform_plugin
75 | self.graphics_plugin = graphics_plugin
76 |
77 | self.debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(xr_debug_callback)
78 |
79 | self.instance = None
80 | self.session = None
81 | self.app_space = None
82 | self.form_factor = xr.FormFactor.HEAD_MOUNTED_DISPLAY
83 | self.system = None # Higher level System class, not just ID
84 |
85 | self.config_views = []
86 | self.swapchains = []
87 | self.swapchain_image_buffers = [] # to keep objects alive
88 | self.swapchain_image_ptr_buffers = {} # m_swapchainImages
89 | self.views = (xr.View * 2)(xr.View(), xr.View())
90 | self.color_swapchain_format = -1
91 |
92 | self.visualized_spaces = []
93 |
94 | # Application's current lifecycle state according to the runtime
95 | self.session_state = xr.SessionState.UNKNOWN
96 | self.session_running = False
97 |
98 | self.event_data_buffer = xr.EventDataBuffer()
99 | self.input = OpenXRProgram.InputState()
100 |
101 | self.acceptable_blend_modes = [
102 | xr.EnvironmentBlendMode.OPAQUE,
103 | xr.EnvironmentBlendMode.ADDITIVE,
104 | xr.EnvironmentBlendMode.ALPHA_BLEND,
105 | ]
106 |
107 | def __enter__(self):
108 | return self
109 |
110 | def __exit__(self, exc_type, exc_val, exc_tb):
111 | if self.input.action_set is not None:
112 | for hand in Side:
113 | if self.input.hand_space[hand] is not None:
114 | xr.destroy_space(self.input.hand_space[hand])
115 | self.input.hand_space[hand] = None
116 | xr.destroy_action_set(self.input.action_set)
117 | self.input.action_set = None
118 | for swapchain in self.swapchains:
119 | xr.destroy_swapchain(swapchain.handle)
120 | self.swapchains[:] = []
121 | for visualized_space in self.visualized_spaces:
122 | xr.destroy_space(visualized_space)
123 | self.visualized_spaces[:] = []
124 | if self.app_space is not None:
125 | xr.destroy_space(self.app_space)
126 | self.app_space = None
127 | if self.session is not None:
128 | xr.destroy_session(self.session)
129 | self.session = None
130 | if self.instance is not None:
131 | # Workaround for bug https://github.com/ValveSoftware/SteamVR-for-Linux/issues/422
132 | if platform.system() != "Linux":
133 | self.instance.destroy()
134 | self.instance = None
135 |
136 | def create_instance(self) -> None:
137 | """Create an Instance and other basic instance-level initialization."""
138 | self.log_layers_and_extensions()
139 | self.create_instance_internal()
140 | self.log_instance_info()
141 |
142 | def create_instance_internal(self):
143 | assert self.instance is None
144 | # Create union of extensions required by platform and graphics plugins.
145 | extensions = []
146 | # Enable debug messaging
147 | discovered_extensions = xr.enumerate_instance_extension_properties()
148 | dumci = xr.DebugUtilsMessengerCreateInfoEXT()
149 | next_structure = self.platform_plugin.instance_create_extension
150 | ALL_SEVERITIES = (
151 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
152 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
153 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
154 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
155 | )
156 |
157 | ALL_TYPES = (
158 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
159 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
160 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
161 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT
162 | )
163 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in discovered_extensions:
164 | extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME)
165 | dumci.message_severities = ALL_SEVERITIES
166 | dumci.message_types = ALL_TYPES
167 | dumci.user_data = None
168 | dumci.user_callback = self.debug_callback
169 | if next_structure is None:
170 | next_structure = cast(pointer(dumci), c_void_p)
171 | else:
172 | next_structure.next = cast(pointer(dumci), c_void_p)
173 | #
174 | extensions.extend(self.platform_plugin.instance_extensions)
175 | extensions.extend(self.graphics_plugin.instance_extensions)
176 |
177 | self.instance = xr.InstanceObject(
178 | enabled_extensions=extensions,
179 | application_name="hello_xr.py",
180 | application_version=xr.Version(0, 0, 1),
181 | # Current version is 1.1.x, but hello_xr only requires 1.0.x
182 | api_version=xr.XR_API_VERSION_1_0,
183 | next=next_structure,
184 | )
185 |
186 | def create_swapchains(self) -> None:
187 | """
188 | Create a Swapchain which requires coordinating with the graphics plugin to select the format, getting the system graphics
189 | properties, getting the view configuration and grabbing the resulting swapchain images.
190 | """
191 | assert self.session is not None
192 | assert len(self.swapchains) == 0
193 | assert len(self.config_views) == 0
194 | # Read graphics properties for preferred swapchain length and logging.
195 | system_properties = xr.get_system_properties(self.instance.handle, self.system.id)
196 | # Log system properties
197 | logger.info("System Properties: "
198 | f"Name={system_properties.system_name.decode()} "
199 | f"VendorId={system_properties.vendor_id}")
200 | logger.info("System Graphics Properties: "
201 | f"MaxWidth={system_properties.graphics_properties.max_swapchain_image_width} "
202 | f"MaxHeight={system_properties.graphics_properties.max_swapchain_image_height} "
203 | f"MaxLayers={system_properties.graphics_properties.max_layer_count}")
204 | logger.info("System Tracking Properties: "
205 | f"OrientationTracking={bool(system_properties.tracking_properties.orientation_tracking)} "
206 | f"PositionTracking={bool(system_properties.tracking_properties.position_tracking)}")
207 | # Note: No other view configurations exist at the time this (C++) code was written. If this
208 | # condition is not met, the project will need to be audited to see how support should be
209 | # added.
210 | if self.options.parsed["view_config_type"] != xr.ViewConfigurationType.PRIMARY_STEREO:
211 | raise RuntimeError("Unsupported view configuration type")
212 | # Query and cache view configuration views.
213 | self.config_views = xr.enumerate_view_configuration_views(
214 | instance=self.instance.handle,
215 | system_id=self.system.id,
216 | view_configuration_type=self.options.parsed["view_config_type"],
217 | )
218 | # Create and cache view buffer for xrLocateViews later.
219 | view_count = len(self.config_views)
220 | assert view_count == 2
221 | self.views = (xr.View * view_count)(*([xr.View()] * view_count))
222 | # Create the swapchain and get the images.
223 | if view_count > 0:
224 | # Select a swapchain format.
225 | swapchain_formats = xr.enumerate_swapchain_formats(self.session)
226 | self.color_swapchain_format = self.graphics_plugin.select_color_swapchain_format(swapchain_formats)
227 | # Print swapchain formats and the selected one.
228 | formats_string = ""
229 | for sc_format in swapchain_formats:
230 | selected = sc_format == self.color_swapchain_format
231 | formats_string += " "
232 | if selected:
233 | formats_string += "["
234 | formats_string += f"{str(self.color_swapchain_format)}({sc_format})"
235 | formats_string += "]"
236 | else:
237 | formats_string += str(sc_format)
238 | logger.debug(f"Swapchain Formats: {formats_string}")
239 | # Create a swapchain for each view.
240 | for i, vp in enumerate(self.config_views):
241 | logger.info("Creating swapchain for "
242 | f"view {i} with dimensions "
243 | f"Width={vp.recommended_image_rect_width} "
244 | f"Height={vp.recommended_image_rect_height} "
245 | f"SampleCount={vp.recommended_swapchain_sample_count}")
246 | # Create the swapchain.
247 | swapchain_create_info = xr.SwapchainCreateInfo(
248 | array_size=1,
249 | format=self.color_swapchain_format,
250 | width=vp.recommended_image_rect_width,
251 | height=vp.recommended_image_rect_height,
252 | mip_count=1,
253 | face_count=1,
254 | sample_count=self.graphics_plugin.get_supported_swapchain_sample_count(vp),
255 | usage_flags=xr.SwapchainUsageFlags.SAMPLED_BIT | xr.SwapchainUsageFlags.COLOR_ATTACHMENT_BIT,
256 | )
257 | swapchain = Swapchain(
258 | xr.create_swapchain(
259 | session=self.session,
260 | create_info=swapchain_create_info,
261 | ),
262 | swapchain_create_info.width,
263 | swapchain_create_info.height,
264 | )
265 | self.swapchains.append(swapchain)
266 | swapchain_image_buffer = xr.enumerate_swapchain_images(
267 | swapchain=swapchain.handle,
268 | element_type=self.graphics_plugin.swapchain_image_type,
269 | )
270 | # Keep the buffer alive by moving it into the list of buffers.
271 | self.swapchain_image_buffers.append(swapchain_image_buffer)
272 | capacity = len(swapchain_image_buffer)
273 | swapchain_image_ptr_buffer = (POINTER(xr.SwapchainImageBaseHeader) * capacity)()
274 | for ix in range(capacity):
275 | swapchain_image_ptr_buffer[ix] = cast(
276 | byref(swapchain_image_buffer[ix]),
277 | POINTER(xr.SwapchainImageBaseHeader))
278 | self.swapchain_image_ptr_buffers[handle_key(swapchain.handle)] = swapchain_image_ptr_buffer
279 |
280 | def create_visualized_spaces(self):
281 | assert self.session is not None
282 | visualized_spaces = [
283 | "ViewFront", "Local", "Stage", "StageLeft", "StageRight",
284 | "StageLeftRotated", "StageRightRotated",
285 | ]
286 | for visualized_space in visualized_spaces:
287 | try:
288 | space = xr.create_reference_space(
289 | session=self.session,
290 | create_info=get_xr_reference_space_create_info(visualized_space)
291 | )
292 | self.visualized_spaces.append(space)
293 | except xr.XrException as exc:
294 | logger.warning(f"Failed to create reference space {visualized_space} with error {exc}")
295 |
296 | def handle_session_state_changed_event(
297 | self, state_changed_event, exit_render_loop, request_restart) -> (bool, bool):
298 | # TODO: avoid this ugly cast
299 | event = cast(byref(state_changed_event), POINTER(xr.EventDataSessionStateChanged)).contents
300 | old_state = self.session_state
301 | self.session_state = xr.SessionState(event.state)
302 | key = cast(self.session, c_void_p).value
303 | logger.info(f"XrEventDataSessionStateChanged: "
304 | f"state {str(old_state)}->{str(self.session_state)} "
305 | f"session={hex(key)} time={event.time}")
306 | if event.session is not None and handle_key(event.session) != handle_key(self.session):
307 | logger.error(f"XrEventDataSessionStateChanged for unknown session {event.session} {self.session}")
308 | return exit_render_loop, request_restart
309 |
310 | if self.session_state == xr.SessionState.READY:
311 | assert self.session is not None
312 | xr.begin_session(
313 | session=self.session,
314 | begin_info=xr.SessionBeginInfo(
315 | primary_view_configuration_type=self.options.parsed["view_config_type"],
316 | ),
317 | )
318 | self.session_running = True
319 | elif self.session_state == xr.SessionState.STOPPING:
320 | assert self.session is not None
321 | self.session_running = False
322 | xr.end_session(self.session)
323 | elif self.session_state == xr.SessionState.EXITING:
324 | exit_render_loop = True
325 | # Do not attempt to restart because user closed this session.
326 | request_restart = False
327 | elif self.session_state == xr.SessionState.LOSS_PENDING:
328 | exit_render_loop = True
329 | # Poll for a new instance.
330 | request_restart = True
331 | return exit_render_loop, request_restart
332 |
333 | def initialize_actions(self):
334 | # Create an action set.
335 | action_set_info = xr.ActionSetCreateInfo(
336 | action_set_name="gameplay",
337 | localized_action_set_name="Gameplay",
338 | priority=0,
339 | )
340 | self.input.action_set = xr.create_action_set(self.instance.handle, action_set_info)
341 | # Get the XrPath for the left and right hands - we will use them as subaction paths.
342 | self.input.hand_subaction_path[Side.LEFT] = xr.string_to_path(
343 | self.instance.handle,
344 | "/user/hand/left")
345 | self.input.hand_subaction_path[Side.RIGHT] = xr.string_to_path(
346 | self.instance.handle,
347 | "/user/hand/right")
348 | # Create actions
349 | # Create an input action for grabbing objects with the left and right hands.
350 | self.input.grab_action = xr.create_action(
351 | action_set=self.input.action_set,
352 | create_info=xr.ActionCreateInfo(
353 | action_type=xr.ActionType.FLOAT_INPUT,
354 | action_name="grab_object",
355 | localized_action_name="Grab Object",
356 | count_subaction_paths=len(self.input.hand_subaction_path),
357 | subaction_paths=self.input.hand_subaction_path,
358 | ),
359 | )
360 | # Create an input action getting the left and right hand poses.
361 | self.input.pose_action = xr.create_action(
362 | action_set=self.input.action_set,
363 | create_info=xr.ActionCreateInfo(
364 | action_type=xr.ActionType.POSE_INPUT,
365 | action_name="hand_pose",
366 | localized_action_name="Hand Pose",
367 | count_subaction_paths=len(self.input.hand_subaction_path),
368 | subaction_paths=self.input.hand_subaction_path,
369 | ),
370 | )
371 | # Create output actions for vibrating the left and right controller.
372 | self.input.vibrate_action = xr.create_action(
373 | action_set=self.input.action_set,
374 | create_info=xr.ActionCreateInfo(
375 | action_type=xr.ActionType.VIBRATION_OUTPUT,
376 | action_name="vibrate_hand",
377 | localized_action_name="Vibrate Hand",
378 | count_subaction_paths=len(self.input.hand_subaction_path),
379 | subaction_paths=self.input.hand_subaction_path,
380 | ),
381 | )
382 | # Create input actions for quitting the session using the left and right controller.
383 | # Since it doesn't matter which hand did this, we do not specify subaction paths for it.
384 | # We will just suggest bindings for both hands, where possible.
385 | self.input.quit_action = xr.create_action(
386 | action_set=self.input.action_set,
387 | create_info=xr.ActionCreateInfo(
388 | action_type=xr.ActionType.BOOLEAN_INPUT,
389 | action_name="quit_session",
390 | localized_action_name="Quit Session",
391 | count_subaction_paths=0,
392 | subaction_paths=None,
393 | ),
394 | )
395 | select_path = [
396 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/select/click"),
397 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/select/click")]
398 | _squeeze_value_path = [
399 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/squeeze/value"),
400 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/squeeze/value")]
401 | _squeeze_force_path = [
402 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/squeeze/force"),
403 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/squeeze/force")]
404 | _squeeze_click_path = [
405 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/squeeze/click"),
406 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/squeeze/click")]
407 | pose_path = [
408 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/grip/pose"),
409 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/grip/pose")]
410 | haptic_path = [
411 | xr.string_to_path(self.instance.handle, "/user/hand/left/output/haptic"),
412 | xr.string_to_path(self.instance.handle, "/user/hand/right/output/haptic")]
413 | menu_click_path = [
414 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/menu/click"),
415 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/menu/click")]
416 | _b_click_path = [
417 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/b/click"),
418 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/b/click")]
419 | trigger_value_path = [
420 | xr.string_to_path(self.instance.handle, "/user/hand/left/input/trigger/value"),
421 | xr.string_to_path(self.instance.handle, "/user/hand/right/input/trigger/value")]
422 | # Suggest bindings for KHR Simple.
423 | khr_bindings = [
424 | # Fall back to a click input for the grab action.
425 | xr.ActionSuggestedBinding(self.input.grab_action, select_path[Side.LEFT]),
426 | xr.ActionSuggestedBinding(self.input.grab_action, select_path[Side.RIGHT]),
427 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.LEFT]),
428 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.RIGHT]),
429 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.LEFT]),
430 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.RIGHT]),
431 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.LEFT]),
432 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.RIGHT]),
433 | ]
434 | xr.suggest_interaction_profile_bindings(
435 | instance=self.instance.handle,
436 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
437 | interaction_profile=xr.string_to_path(
438 | self.instance.handle,
439 | "/interaction_profiles/khr/simple_controller",
440 | ),
441 | count_suggested_bindings=len(khr_bindings),
442 | suggested_bindings=(xr.ActionSuggestedBinding * len(khr_bindings))(*khr_bindings),
443 | ),
444 | )
445 | # Suggest bindings for the Vive Controller.
446 | vive_bindings = [
447 | xr.ActionSuggestedBinding(self.input.grab_action, trigger_value_path[Side.LEFT]),
448 | xr.ActionSuggestedBinding(self.input.grab_action, trigger_value_path[Side.RIGHT]),
449 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.LEFT]),
450 | xr.ActionSuggestedBinding(self.input.pose_action, pose_path[Side.RIGHT]),
451 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.LEFT]),
452 | xr.ActionSuggestedBinding(self.input.quit_action, menu_click_path[Side.RIGHT]),
453 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.LEFT]),
454 | xr.ActionSuggestedBinding(self.input.vibrate_action, haptic_path[Side.RIGHT]),
455 | ]
456 | xr.suggest_interaction_profile_bindings(
457 | instance=self.instance.handle,
458 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
459 | interaction_profile=xr.string_to_path(
460 | self.instance.handle,
461 | "/interaction_profiles/htc/vive_controller",
462 | ),
463 | count_suggested_bindings=len(vive_bindings),
464 | suggested_bindings=(xr.ActionSuggestedBinding * len(vive_bindings))(*vive_bindings),
465 | ),
466 | )
467 | # TODO the other controller types in openxr_programs.cpp
468 |
469 | action_space_info = xr.ActionSpaceCreateInfo(
470 | action=self.input.pose_action,
471 | # pose_in_action_space # w already defaults to 1 in python...
472 | subaction_path=self.input.hand_subaction_path[Side.LEFT],
473 | )
474 | assert action_space_info.pose_in_action_space.orientation.w == 1
475 | self.input.hand_space[Side.LEFT] = xr.create_action_space(
476 | session=self.session,
477 | create_info=action_space_info,
478 | )
479 | action_space_info.subaction_path = self.input.hand_subaction_path[Side.RIGHT]
480 | self.input.hand_space[Side.RIGHT] = xr.create_action_space(
481 | session=self.session,
482 | create_info=action_space_info,
483 | )
484 | xr.attach_session_action_sets(
485 | session=self.session,
486 | attach_info=xr.SessionActionSetsAttachInfo(
487 | count_action_sets=1,
488 | action_sets=pointer(self.input.action_set),
489 | ),
490 | )
491 |
492 | def initialize_session(self) -> None:
493 | """Create a Session and other basic session-level initialization."""
494 | assert self.instance is not None
495 | assert self.instance.handle != xr.NULL_HANDLE
496 | assert self.session is None
497 | logger.debug(f"Creating session...")
498 | graphics_binding_pointer = cast(
499 | pointer(self.graphics_plugin.graphics_binding),
500 | c_void_p)
501 | create_info = xr.SessionCreateInfo(
502 | next=graphics_binding_pointer,
503 | system_id=self.system.id,
504 | )
505 | self.session = xr.create_session(
506 | instance=self.instance.handle,
507 | create_info=create_info,
508 | )
509 | self.log_reference_spaces()
510 | self.initialize_actions()
511 | self.create_visualized_spaces()
512 | self.app_space = xr.create_reference_space(
513 | session=self.session,
514 | create_info=get_xr_reference_space_create_info(self.options.app_space),
515 | )
516 |
517 | def initialize_system(self) -> None:
518 | """
519 | Select a System for the view configuration specified in the Options and initialize the graphics device for the selected
520 | system.
521 | """
522 | assert self.instance is not None
523 | assert self.system is None
524 | form_factor = Options.get_xr_form_factor(self.options.form_factor)
525 | self.system = xr.SystemObject(instance=self.instance, form_factor=form_factor)
526 | logger.debug(f"Using system {hex(self.system.id.value)} for form factor {str(form_factor)}")
527 | assert self.instance.handle is not None
528 | assert self.system.id is not None
529 |
530 | def initialize_device(self):
531 | self.log_view_configurations()
532 | # The graphics API can initialize the graphics device now that the systemId and instance
533 | # handle are available.
534 | self.graphics_plugin.initialize_device(self.instance.handle, self.system.id)
535 |
536 | def log_action_source_name(self, action: xr.Action, action_name: str):
537 | paths = xr.enumerate_bound_sources_for_action(
538 | session=self.session,
539 | enumerate_info=xr.BoundSourcesForActionEnumerateInfo(
540 | action=action,
541 | ),
542 | )
543 | source_name = ""
544 | for path in paths:
545 | all_flags = xr.INPUT_SOURCE_LOCALIZED_NAME_USER_PATH_BIT \
546 | | xr.INPUT_SOURCE_LOCALIZED_NAME_INTERACTION_PROFILE_BIT \
547 | | xr.INPUT_SOURCE_LOCALIZED_NAME_COMPONENT_BIT
548 | grab_source = xr.get_input_source_localized_name(
549 | session=self.session,
550 | get_info=xr.InputSourceLocalizedNameGetInfo(
551 | source_path=path,
552 | which_components=all_flags,
553 | ),
554 | )
555 | if len(grab_source) < 1:
556 | continue
557 | if len(source_name) > 0:
558 | source_name += " and "
559 | source_name += f"'{grab_source}'"
560 | logger.info(f"{action_name} is bound to {source_name if len(source_name) > 0 else 'nothing'}")
561 |
562 | def log_environment_blend_mode(self, view_config_type):
563 | assert self.instance.handle is not None
564 | assert self.system.id is not None
565 | blend_modes = xr.enumerate_environment_blend_modes(self.instance.handle, self.system.id, view_config_type)
566 | logger.info(f"Available Environment Blend Mode count : ({len(blend_modes)})")
567 | blend_mode_found = False
568 | for mode_value in blend_modes:
569 | mode = xr.EnvironmentBlendMode(mode_value)
570 | blend_mode_match = mode == self.options.parsed["environment_blend_mode"]
571 | logger.info(f"Environment Blend Mode ({str(mode)}) : "
572 | f"{'(Selected)' if blend_mode_match else ''}")
573 | blend_mode_found |= blend_mode_match
574 | assert blend_mode_found
575 |
576 | def log_instance_info(self):
577 | assert self.instance is not None
578 | assert self.instance.handle is not None
579 | instance_properties = self.instance.get_properties()
580 | logger.info(
581 | f"Instance RuntimeName={instance_properties.runtime_name.decode()} "
582 | f"RuntimeVersion={xr.Version(instance_properties.runtime_version)}")
583 |
584 | def log_layers_and_extensions(self):
585 | # Log non-api_layer extensions
586 | self._log_extensions(layer_name=None)
587 | # Log layers and any of their extensions
588 | layers = xr.enumerate_api_layer_properties()
589 | logger.info(f"Available Layers: ({len(layers)})")
590 | for layer in layers:
591 | logger.debug(
592 | f" Name={layer.layer_name.decode()} "
593 | f"SpecVersion={self.xr_version_string()} "
594 | f"LayerVersion={layer.layer_version} "
595 | f"Description={layer.description.decode()}")
596 | self._log_extensions(layer_name=layer.layer_name.decode(), indent=4)
597 |
598 | def log_reference_spaces(self):
599 | assert self.session is not None
600 | spaces = xr.enumerate_reference_spaces(self.session)
601 | logger.info(f"Available reference spaces: {len(spaces)}")
602 | for space in spaces:
603 | logger.debug(f" Name: {str(xr.ReferenceSpaceType(space))}")
604 |
605 | def log_view_configurations(self):
606 | assert self.instance.handle is not None
607 | assert self.system.id is not None
608 | view_config_types = xr.enumerate_view_configurations(self.instance.handle, self.system.id)
609 | logger.info(f"Available View Configuration Types: ({len(view_config_types)})")
610 | for view_config_type_value in view_config_types:
611 | view_config_type = xr.ViewConfigurationType(view_config_type_value)
612 | logger.debug(
613 | f" View Configuration Type: {str(view_config_type)} "
614 | f"{'(Selected)' if view_config_type == self.options.parsed['view_config_type'] else ''}")
615 | view_config_properties = xr.get_view_configuration_properties(
616 | instance=self.instance.handle,
617 | system_id=self.system.id,
618 | view_configuration_type=view_config_type,
619 | )
620 | logger.debug(f" View configuration FovMutable={bool(view_config_properties.fov_mutable)}")
621 | configuration_views = xr.enumerate_view_configuration_views(self.instance.handle, self.system.id,
622 | view_config_type)
623 | if configuration_views is None or len(configuration_views) < 1:
624 | logger.error(f"Empty view configuration type")
625 | else:
626 | for i, view in enumerate(configuration_views):
627 | logger.debug(
628 | f" View [{i}]: Recommended Width={view.recommended_image_rect_width} "
629 | f"Height={view.recommended_image_rect_height} "
630 | f"SampleCount={view.recommended_swapchain_sample_count}")
631 | logger.debug(
632 | f" View [{i}]: Maximum Width={view.max_image_rect_width} "
633 | f"Height={view.max_image_rect_height} "
634 | f"SampleCount={view.max_swapchain_sample_count}")
635 | self.log_environment_blend_mode(view_config_type)
636 |
637 | @staticmethod
638 | def _log_extensions(layer_name, indent: int = 0):
639 | """Write out extension properties for a given api_layer."""
640 | extension_properties = xr.enumerate_instance_extension_properties(layer_name)
641 | indent_str = " " * indent
642 | logger.debug(f"{indent_str}Available Extensions ({len(extension_properties)})")
643 | for extension in extension_properties:
644 | logger.debug(
645 | f"{indent_str} Name={extension.extension_name.decode()} SpecVersion={extension.extension_version}")
646 |
647 | def poll_actions(self) -> None:
648 | """Sample input actions and generate haptic feedback."""
649 | self.input.hand_active[:] = [xr.FALSE, xr.FALSE]
650 | # Sync actions
651 | active_action_set = xr.ActiveActionSet(self.input.action_set, xr.NULL_PATH)
652 | xr.sync_actions(
653 | self.session,
654 | xr.ActionsSyncInfo(
655 | count_active_action_sets=1,
656 | active_action_sets=pointer(active_action_set)
657 | ),
658 | )
659 | # Get pose and grab action state and start haptic vibrate when hand is 90% squeezed.
660 | for hand in Side:
661 | grab_value = xr.get_action_state_float(
662 | self.session,
663 | xr.ActionStateGetInfo(
664 | action=self.input.grab_action,
665 | subaction_path=self.input.hand_subaction_path[hand],
666 | ),
667 | )
668 | if grab_value.is_active:
669 | # Scale the rendered hand by 1.0f (open) to 0.5f (fully squeezed).
670 | self.input.hand_scale[hand] = 1 - 0.5 * grab_value.current_state
671 | if grab_value.current_state > 0.9:
672 | vibration = xr.HapticVibration(
673 | amplitude=0.5,
674 | duration=xr.MIN_HAPTIC_DURATION,
675 | frequency=xr.FREQUENCY_UNSPECIFIED,
676 | )
677 | xr.apply_haptic_feedback(
678 | session=self.session,
679 | haptic_action_info=xr.HapticActionInfo(
680 | action=self.input.vibrate_action,
681 | subaction_path=self.input.hand_subaction_path[hand],
682 | ),
683 | haptic_feedback=cast(byref(vibration), POINTER(xr.HapticBaseHeader)).contents,
684 | )
685 | pose_state = xr.get_action_state_pose(
686 | session=self.session,
687 | get_info=xr.ActionStateGetInfo(
688 | action=self.input.pose_action,
689 | subaction_path=self.input.hand_subaction_path[hand],
690 | ),
691 | )
692 | self.input.hand_active[hand] = pose_state.is_active
693 | # There were no subaction paths specified for the quit action, because we don't care which hand did it.
694 | quit_value = xr.get_action_state_boolean(
695 | session=self.session,
696 | get_info=xr.ActionStateGetInfo(
697 | action=self.input.quit_action,
698 | subaction_path=xr.NULL_PATH,
699 | ),
700 | )
701 | if quit_value.is_active and quit_value.changed_since_last_sync and quit_value.current_state:
702 | xr.request_exit_session(self.session)
703 |
704 | def poll_events(self) -> (bool, bool):
705 | """Process any events in the event queue."""
706 | exit_render_loop = False
707 | request_restart = False
708 | # Process all pending messages.
709 | while True:
710 | event = self.try_read_next_event()
711 | if event is None:
712 | break
713 | event_type = event.type
714 | if event_type == xr.StructureType.EVENT_DATA_INSTANCE_LOSS_PENDING:
715 | logger.warning(f"XrEventDataInstanceLossPending by {event.loss_time}")
716 | return True, True
717 | elif event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED:
718 | exit_render_loop, request_restart = self.handle_session_state_changed_event(
719 | event, exit_render_loop, request_restart)
720 | elif event_type == xr.StructureType.EVENT_DATA_INTERACTION_PROFILE_CHANGED:
721 | self.log_action_source_name(self.input.grab_action, "Grab")
722 | self.log_action_source_name(self.input.quit_action, "Quit")
723 | self.log_action_source_name(self.input.pose_action, "Pose")
724 | self.log_action_source_name(self.input.vibrate_action, "Vibrate")
725 | elif event_type == xr.StructureType.EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING:
726 | logger.debug(f"Ignoring event type {str(event_type)}")
727 | else:
728 | logger.debug(f"Ignoring event type {str(event_type)}")
729 | return exit_render_loop, request_restart
730 |
731 | @property
732 | def preferred_blend_mode(self):
733 | blend_modes = xr.enumerate_environment_blend_modes(self.instance.handle, self.system.id, self.options.parsed["view_config_type"])
734 | for blend_mode in blend_modes:
735 | if blend_mode in self.acceptable_blend_modes:
736 | return blend_mode
737 | raise RuntimeError("No acceptable blend mode returned from the xrEnumerateEnvironmentBlendModes")
738 |
739 | def render_frame(self) -> None:
740 | """Create and submit a frame."""
741 | assert self.session is not None
742 | frame_state = xr.wait_frame(
743 | session=self.session,
744 | frame_wait_info=xr.FrameWaitInfo(),
745 | )
746 | xr.begin_frame(self.session, xr.FrameBeginInfo())
747 |
748 | layers = []
749 | layer_flags = 0
750 | if self.options.environment_blend_mode == "AlphaBlend":
751 | layer_flags = xr.COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT \
752 | | xr.COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT
753 | layer = xr.CompositionLayerProjection(
754 | space=self.app_space,
755 | layer_flags=layer_flags,
756 | )
757 | projection_layer_views = (xr.CompositionLayerProjectionView * 2)(
758 | xr.CompositionLayerProjectionView(),
759 | xr.CompositionLayerProjectionView())
760 | if frame_state.should_render:
761 | if self.render_layer(frame_state.predicted_display_time, projection_layer_views, layer):
762 | layers.append(byref(layer))
763 |
764 | xr.end_frame(
765 | session=self.session,
766 | frame_end_info=xr.FrameEndInfo(
767 | display_time=frame_state.predicted_display_time,
768 | environment_blend_mode=self.options.parsed["environment_blend_mode"],
769 | layers=layers,
770 | ),
771 | )
772 |
773 | def render_layer(
774 | self,
775 | predicted_display_time: xr.Time,
776 | projection_layer_views: ctypes.Array,
777 | layer: xr.CompositionLayerProjection,
778 | ) -> bool:
779 | view_capacity_input = len(self.views)
780 | view_state, self.views = xr.locate_views(
781 | session=self.session,
782 | view_locate_info=xr.ViewLocateInfo(
783 | view_configuration_type=self.options.parsed["view_config_type"],
784 | display_time=predicted_display_time,
785 | space=self.app_space,
786 | ),
787 | )
788 | view_count_output = len(self.views)
789 | vsf = view_state.view_state_flags
790 | if (vsf & xr.VIEW_STATE_POSITION_VALID_BIT == 0
791 | or vsf & xr.VIEW_STATE_ORIENTATION_VALID_BIT == 0):
792 | return False # There are no valid tracking poses for the views.
793 | assert view_count_output == view_capacity_input
794 | assert view_count_output == len(self.config_views)
795 | assert view_count_output == len(self.swapchains)
796 | assert view_count_output == len(projection_layer_views)
797 |
798 | # For each locatable space that we want to visualize, render a 25cm cube.
799 | cubes = []
800 | for visualized_space in self.visualized_spaces:
801 | # TODO: sometimes xr.locate_space() raises xr.exception.TimeInvalidError
802 | # Maybe try skipping a few frames instead of crashing
803 | space_location = xr.locate_space(
804 | space=visualized_space,
805 | base_space=self.app_space,
806 | time=predicted_display_time,
807 | )
808 | loc_flags = space_location.location_flags
809 | if (loc_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT != 0
810 | and loc_flags & xr.SPACE_LOCATION_ORIENTATION_VALID_BIT != 0):
811 | cubes.append(Cube(space_location.pose, xr.Vector3f(0.25, 0.25, 0.25)))
812 | # Render a 10cm cube scaled by grabAction for each hand. Note renderHand will only be
813 | # true when the application has focus.
814 | for hand in Side:
815 | space_location = xr.locate_space(
816 | space=self.input.hand_space[hand],
817 | base_space=self.app_space,
818 | time=predicted_display_time,
819 | )
820 | loc_flags = space_location.location_flags
821 | if (loc_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT != 0
822 | and loc_flags & xr.SPACE_LOCATION_ORIENTATION_VALID_BIT != 0):
823 | scale = 0.1 * self.input.hand_scale[hand]
824 | cubes.append(Cube(space_location.pose, xr.Vector3f(scale, scale, scale)))
825 | # Render view to the appropriate part of the swapchain image.
826 | for i in range(view_count_output):
827 | view_swapchain = self.swapchains[i]
828 | swapchain_image_index = xr.acquire_swapchain_image(
829 | swapchain=view_swapchain.handle,
830 | acquire_info=xr.SwapchainImageAcquireInfo(),
831 | )
832 | xr.wait_swapchain_image(
833 | swapchain=view_swapchain.handle,
834 | wait_info=xr.SwapchainImageWaitInfo(timeout=xr.INFINITE_DURATION),
835 | )
836 | view = projection_layer_views[i]
837 | assert view.type == xr.StructureType.COMPOSITION_LAYER_PROJECTION_VIEW
838 | view.pose = self.views[i].pose
839 | view.fov = self.views[i].fov
840 | view.sub_image.swapchain = view_swapchain.handle
841 | view.sub_image.image_rect.offset[:] = [0, 0]
842 | view.sub_image.image_rect.extent[:] = [
843 | view_swapchain.width, view_swapchain.height, ]
844 | swapchain_image_ptr = self.swapchain_image_ptr_buffers[handle_key(view_swapchain.handle)][swapchain_image_index]
845 | self.graphics_plugin.render_view(
846 | view,
847 | swapchain_image_ptr,
848 | self.color_swapchain_format,
849 | cubes,
850 | mirror=i == Side.LEFT, # mirror left eye only
851 | # mirror=False,
852 | )
853 | xr.release_swapchain_image(
854 | swapchain=view_swapchain.handle,
855 | release_info=xr.SwapchainImageReleaseInfo()
856 | )
857 | layer.views = projection_layer_views
858 | return True
859 |
860 | def try_read_next_event(self) -> Optional[Structure]:
861 | # It is sufficient to clear the just the XrEventDataBuffer header to
862 | # XR_TYPE_EVENT_DATA_BUFFER
863 | base_header = self.event_data_buffer
864 | base_header.type = xr.StructureType.EVENT_DATA_BUFFER
865 | result = xr.raw_functions.xrPollEvent(self.instance.handle, byref(self.event_data_buffer))
866 | if result == xr.Result.SUCCESS:
867 | if base_header.type == xr.StructureType.EVENT_DATA_EVENTS_LOST:
868 | events_lost = cast(base_header, POINTER(xr.EventDataEventsLost))
869 | logger.warning(f"{events_lost} events lost")
870 | return base_header
871 | if result == xr.Result.EVENT_UNAVAILABLE:
872 | return None
873 | result2 = xr.check_result(result)
874 | raise result2
875 |
876 | @staticmethod
877 | def xr_version_string():
878 | return xr.XR_CURRENT_API_VERSION
879 |
880 | class InputState(Structure):
881 | def __init__(self):
882 | super().__init__()
883 | self.hand_scale[:] = [1, 1]
884 |
885 | _fields_ = [
886 | ("action_set", xr.ActionSet),
887 | ("grab_action", xr.Action),
888 | ("pose_action", xr.Action),
889 | ("vibrate_action", xr.Action),
890 | ("quit_action", xr.Action),
891 | ("hand_subaction_path", xr.Path * len(Side)),
892 | ("hand_space", xr.Space * len(Side)),
893 | ("hand_scale", c_float * len(Side)),
894 | ("hand_active", xr.Bool32 * len(Side)),
895 | ]
896 |
897 |
898 | def get_xr_reference_space_create_info(reference_space_type_string: str) -> xr.ReferenceSpaceCreateInfo:
899 | create_info = xr.ReferenceSpaceCreateInfo(
900 | pose_in_reference_space=Math.Pose.identity(),
901 | )
902 | space_type = reference_space_type_string.lower()
903 | if space_type == "View".lower():
904 | create_info.reference_space_type = xr.ReferenceSpaceType.VIEW
905 | elif space_type == "ViewFront".lower():
906 | # Render head-locked 2m in front of device.
907 | create_info.pose_in_reference_space = Math.Pose.translation(translation=[0, 0, -2])
908 | create_info.reference_space_type = xr.ReferenceSpaceType.VIEW
909 | elif space_type == "Local".lower():
910 | create_info.reference_space_type = xr.ReferenceSpaceType.LOCAL
911 | elif space_type == "Stage".lower():
912 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE
913 | elif space_type == "StageLeft".lower():
914 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE
915 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(0, [-2, 0, -2])
916 | elif space_type == "StageRight".lower():
917 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE
918 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(0, [2, 0, -2])
919 | elif space_type == "StageLeftRotated".lower():
920 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE
921 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(math.pi / 3, [-2, 0.5, -2])
922 | elif space_type == "StageRightRotated".lower():
923 | create_info.reference_space_type = xr.ReferenceSpaceType.STAGE
924 | create_info.pose_in_reference_space = Math.Pose.rotate_ccw_about_y_axis(-math.pi / 3, [2, 0.5, -2])
925 | else:
926 | raise ValueError(f"Unknown reference space type '{reference_space_type_string}'")
927 | return create_info
928 |
929 |
930 | def handle_key(handle):
931 | return hex(cast(handle, c_void_p).value)
932 |
933 |
934 | def openxr_log_level(severity_flags: int) -> int:
935 | if severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
936 | return logging.ERROR
937 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
938 | return logging.WARNING
939 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
940 | return logging.INFO
941 | else:
942 | return logging.DEBUG
943 |
944 |
945 | def xr_debug_callback(
946 | severity: xr.DebugUtilsMessageSeverityFlagsEXT,
947 | _type: xr.DebugUtilsMessageTypeFlagsEXT,
948 | data: POINTER(xr.DebugUtilsMessengerCallbackDataEXT),
949 | _user_data: c_void_p) -> bool:
950 | d = data.contents
951 | # TODO structure properties to return unicode strings
952 | logger.log(openxr_log_level(severity), f"OpenXR: {d.function_name.decode()}: {d.message.decode()}")
953 | return True
954 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/options.py:
--------------------------------------------------------------------------------
1 | import xr
2 |
3 |
4 | class Options(object):
5 | def __init__(self):
6 | self.graphics_plugin = "OpenGL"
7 | self.form_factor = "Hmd"
8 | self.view_configuration = "Stereo"
9 | self.environment_blend_mode = "Opaque"
10 | self.app_space = "Local"
11 | self.parsed = {
12 | "form_factor": xr.FormFactor.HEAD_MOUNTED_DISPLAY,
13 | "view_config_type": xr.ViewConfigurationType.PRIMARY_STEREO,
14 | "environment_blend_mode": xr.EnvironmentBlendMode.OPAQUE,
15 | }
16 |
17 | @property
18 | def background_clear_color(self) -> tuple:
19 | slate_grey = (0.184313729, 0.309803933, 0.309803933, 1.0)
20 | if self.parsed["environment_blend_mode"] == xr.EnvironmentBlendMode.OPAQUE:
21 | return slate_grey # SlateGrey
22 | elif self.parsed["environment_blend_mode"] == xr.EnvironmentBlendMode.ADDITIVE:
23 | return 0, 0, 0, 1 # Black
24 | elif self.parsed["environment_blend_mode"] == xr.EnvironmentBlendMode.ALPHA_BLEND:
25 | return 0, 0, 0, 0 # TransparentBlack
26 | else:
27 | return slate_grey
28 |
29 | @staticmethod
30 | def get_xr_environment_blend_mode(environment_blend_mode_string: str) -> xr.EnvironmentBlendMode:
31 | return {
32 | "Opaque": xr.EnvironmentBlendMode.OPAQUE,
33 | "Additive": xr.EnvironmentBlendMode.ADDITIVE,
34 | "AlphaBlend": xr.EnvironmentBlendMode.ALPHA_BLEND,
35 | }[environment_blend_mode_string]
36 |
37 | @staticmethod
38 | def get_xr_environment_blend_mode_string(environment_blend_mode: xr.EnvironmentBlendMode) -> str:
39 | return {
40 | xr.EnvironmentBlendMode.OPAQUE: "Opaque",
41 | xr.EnvironmentBlendMode.ADDITIVE: "Additive",
42 | xr.EnvironmentBlendMode.ALPHA_BLEND: "AlphaBlend",
43 | }[environment_blend_mode]
44 |
45 | @staticmethod
46 | def get_xr_form_factor(form_factor_string: str) -> xr.FormFactor:
47 | if form_factor_string == "Hmd":
48 | return xr.FormFactor.HEAD_MOUNTED_DISPLAY
49 | elif form_factor_string == "Handheld":
50 | return xr.FormFactor.HANDHELD_DISPLAY
51 | raise ValueError(f"Unknown form factor '{form_factor_string}'")
52 |
53 | @staticmethod
54 | def get_xr_view_configuration_type(view_configuration_string: str) -> xr.ViewConfigurationType:
55 | if view_configuration_string == "Mono":
56 | return xr.ViewConfigurationType.PRIMARY_MONO
57 | elif view_configuration_string == "Stereo":
58 | return xr.ViewConfigurationType.PRIMARY_STEREO
59 | raise ValueError(f"Unknown view configuration '{view_configuration_string}'")
60 |
61 | def parse_strings(self):
62 | self.parsed["form_factor"] = self.get_xr_form_factor(self.form_factor)
63 | self.parsed["view_config_type"] = self.get_xr_view_configuration_type(self.view_configuration)
64 | self.parsed["environment_blend_mode"] = self.get_xr_environment_blend_mode(self.environment_blend_mode)
65 |
66 | def set_environment_blend_mode(self, environment_blend_mode: xr.EnvironmentBlendMode) -> None:
67 | self.environment_blend_mode = self.get_xr_environment_blend_mode_string(environment_blend_mode)
68 | self.parsed["environment_blend_mode"] = environment_blend_mode
69 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/platform_plugin.py:
--------------------------------------------------------------------------------
1 | import abc
2 | from ctypes import Structure
3 | from typing import List, Optional
4 |
5 |
6 | class IPlatformPlugin(abc.ABC):
7 | """Wraps platform-specific implementation so the main openxr program can be platform-independent."""
8 |
9 | @abc.abstractmethod
10 | def __enter__(self):
11 | pass
12 |
13 | @abc.abstractmethod
14 | def __exit__(self, exc_type, exc_val, exc_tb):
15 | """Clean up platform resources."""
16 |
17 | @property
18 | @abc.abstractmethod
19 | def instance_create_extension(self) -> Optional[Structure]:
20 | """OpenXR instance-level extensions required by this platform."""
21 |
22 | @property
23 | @abc.abstractmethod
24 | def instance_extensions(self) -> List[str]:
25 | """OpenXR instance-level extensions required by this platform."""
26 |
27 | @abc.abstractmethod
28 | def update_options(self, options) -> None:
29 | pass
30 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/platform_plugin_win32.py:
--------------------------------------------------------------------------------
1 | from ctypes import Structure
2 | from typing import Optional
3 |
4 | from xr_examples.hello_xr.platform_plugin import IPlatformPlugin
5 |
6 |
7 | class Win32PlatformPlugin(IPlatformPlugin):
8 | def __enter__(self):
9 | return self
10 |
11 | def __exit__(self, exc_type, exc_val, exc_tb):
12 | pass
13 |
14 | @property
15 | def instance_create_extension(self) -> Optional[Structure]:
16 | return None
17 |
18 | @property
19 | def instance_extensions(self):
20 | return []
21 |
22 | def update_options(self, options) -> None:
23 | pass
24 |
--------------------------------------------------------------------------------
/xr_examples/hello_xr/platform_plugin_xlib.py:
--------------------------------------------------------------------------------
1 | from ctypes import Structure
2 | from typing import Optional
3 |
4 | from xr_examples.hello_xr.platform_plugin import IPlatformPlugin
5 |
6 |
7 | class XlibPlatformPlugin(IPlatformPlugin):
8 | def __enter__(self):
9 | return self
10 |
11 | def __exit__(self, exc_type, exc_val, exc_tb):
12 | pass
13 |
14 | @property
15 | def instance_create_extension(self) -> Optional[Structure]:
16 | return None
17 |
18 | @property
19 | def instance_extensions(self):
20 | return []
21 |
22 | def update_options(self, options) -> None:
23 | pass
24 |
--------------------------------------------------------------------------------
/xr_examples/openxr_version.py:
--------------------------------------------------------------------------------
1 | import xr
2 |
3 | print(f"OpenXR version {xr.version.XR_CURRENT_API_VERSION}")
4 |
--------------------------------------------------------------------------------
/xr_examples/pink_world.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr example program pink_world.py
3 | This example renders a solid pink field to each eye.
4 | """
5 |
6 | from OpenGL import GL
7 | import xr
8 |
9 |
10 | # ContextObject is a high level pythonic class meant to keep simple cases simple.
11 | with xr.ContextObject(
12 | instance_create_info=xr.InstanceCreateInfo(
13 | enabled_extension_names=[
14 | # A graphics extension is mandatory (without a headless extension)
15 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME,
16 | ],
17 | ),
18 | ) as context:
19 | for frame_index, frame_state in enumerate(context.frame_loop()):
20 | for view in context.view_loop(frame_state):
21 | GL.glClearColor(1, 0.7, 0.7, 1) # pink
22 | GL.glClear(GL.GL_COLOR_BUFFER_BIT)
23 | if frame_index > 500: # Don't run forever
24 | break
25 |
--------------------------------------------------------------------------------
/xr_examples/runtime_name.py:
--------------------------------------------------------------------------------
1 | import xr
2 |
3 | instance = xr.create_instance(create_info=xr.InstanceCreateInfo())
4 | instance_props = xr.get_instance_properties(instance)
5 | print(f"The current active OpenXR runtime is: {instance_props.runtime_name.decode()}")
6 |
--------------------------------------------------------------------------------
/xr_examples/track_controller.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr example program track_controller.py
3 |
4 | Prints the position of your right-hand controller for 30 frames.
5 | """
6 |
7 | import ctypes
8 | import time
9 | import xr
10 |
11 | # ContextObject is a high level pythonic class meant to keep simple cases simple.
12 | with xr.ContextObject(
13 | instance_create_info=xr.InstanceCreateInfo(
14 | enabled_extension_names=[
15 | # A graphics extension is mandatory (without a headless extension)
16 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME,
17 | ],
18 | ),
19 | ) as context:
20 | # Set up the controller pose action
21 | controller_paths = (xr.Path * 2)(
22 | xr.string_to_path(context.instance, "/user/hand/left"),
23 | xr.string_to_path(context.instance, "/user/hand/right"),
24 | )
25 | controller_pose_action = xr.create_action(
26 | action_set=context.default_action_set,
27 | create_info=xr.ActionCreateInfo(
28 | action_type=xr.ActionType.POSE_INPUT,
29 | action_name="hand_pose",
30 | localized_action_name="Hand Pose",
31 | count_subaction_paths=len(controller_paths),
32 | subaction_paths=controller_paths,
33 | ),
34 | )
35 | suggested_bindings = (xr.ActionSuggestedBinding * 2)(
36 | xr.ActionSuggestedBinding(
37 | action=controller_pose_action,
38 | binding=xr.string_to_path(
39 | instance=context.instance,
40 | path_string="/user/hand/left/input/grip/pose",
41 | ),
42 | ),
43 | xr.ActionSuggestedBinding(
44 | action=controller_pose_action,
45 | binding=xr.string_to_path(
46 | instance=context.instance,
47 | path_string="/user/hand/right/input/grip/pose",
48 | ),
49 | ),
50 | )
51 | xr.suggest_interaction_profile_bindings(
52 | instance=context.instance,
53 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
54 | interaction_profile=xr.string_to_path(
55 | context.instance,
56 | "/interaction_profiles/khr/simple_controller",
57 | ),
58 | count_suggested_bindings=len(suggested_bindings),
59 | suggested_bindings=suggested_bindings,
60 | ),
61 | )
62 | xr.suggest_interaction_profile_bindings(
63 | instance=context.instance,
64 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
65 | interaction_profile=xr.string_to_path(
66 | context.instance,
67 | "/interaction_profiles/htc/vive_controller",
68 | ),
69 | count_suggested_bindings=len(suggested_bindings),
70 | suggested_bindings=suggested_bindings,
71 | ),
72 | )
73 |
74 | action_spaces = [
75 | xr.create_action_space(
76 | session=context.session,
77 | create_info=xr.ActionSpaceCreateInfo(
78 | action=controller_pose_action,
79 | subaction_path=controller_paths[0],
80 | ),
81 | ),
82 | xr.create_action_space(
83 | session=context.session,
84 | create_info=xr.ActionSpaceCreateInfo(
85 | action=controller_pose_action,
86 | subaction_path=controller_paths[1],
87 | ),
88 | ),
89 | ]
90 | # Loop over the render frames
91 | session_was_focused = False # Check for a common problem
92 | for frame_index, frame_state in enumerate(context.frame_loop()):
93 |
94 | if context.session_state == xr.SessionState.FOCUSED:
95 | session_was_focused = True
96 | active_action_set = xr.ActiveActionSet(
97 | action_set=context.default_action_set,
98 | subaction_path=xr.NULL_PATH,
99 | )
100 | xr.sync_actions(
101 | session=context.session,
102 | sync_info=xr.ActionsSyncInfo(
103 | count_active_action_sets=1,
104 | active_action_sets=ctypes.pointer(active_action_set),
105 | ),
106 | )
107 | found_count = 0
108 | for index, space in enumerate(action_spaces):
109 | space_location = xr.locate_space(
110 | space=space,
111 | base_space=context.space,
112 | time=frame_state.predicted_display_time,
113 | )
114 | if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT:
115 | print(index + 1, space_location.pose)
116 | found_count += 1
117 | if found_count == 0:
118 | print("no controllers active")
119 |
120 | # Slow things down, especially since we are not rendering anything
121 | time.sleep(0.5)
122 | # Don't run forever
123 | if frame_index > 30:
124 | break
125 | if not session_was_focused:
126 | print("This OpenXR session never entered the FOCUSED state. Did you wear the headset?")
127 |
--------------------------------------------------------------------------------
/xr_examples/track_hmd.py:
--------------------------------------------------------------------------------
1 | import time
2 | import xr
3 |
4 | # Once XR_KHR_headless extension is ratified and adopted, we
5 | # should be able to avoid the Window and frame stuff here.
6 | with xr.InstanceObject(application_name="track_hmd") as instance, \
7 | xr.SystemObject(instance) as system, \
8 | xr.GlfwWindow(system) as window, \
9 | xr.SessionObject(system, graphics_binding=window.graphics_binding) as session:
10 | for _ in range(50):
11 | session.poll_xr_events()
12 | if session.state in (
13 | xr.SessionState.READY,
14 | xr.SessionState.SYNCHRONIZED,
15 | xr.SessionState.VISIBLE,
16 | xr.SessionState.FOCUSED,
17 | ):
18 | session.wait_frame()
19 | session.begin_frame()
20 | view_state, views = session.locate_views()
21 | print(views[xr.Eye.LEFT.value].pose, flush=True)
22 | time.sleep(0.5)
23 | session.end_frame()
24 |
--------------------------------------------------------------------------------
/xr_examples/track_hmd2.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr example program track_hmd2.py
3 |
4 | Prints the position of your head-mounted display for 30 frames.
5 | """
6 |
7 | import time
8 | import xr
9 |
10 | # ContextObject is a high level pythonic class meant to keep simple cases simple.
11 | with xr.ContextObject(
12 | instance_create_info=xr.InstanceCreateInfo(
13 | enabled_extension_names=[
14 | # A graphics extension is mandatory (without a headless extension)
15 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME,
16 | ],
17 | ),
18 | ) as context:
19 | # Loop over the render frames
20 | for frame_index, frame_state in enumerate(context.frame_loop()):
21 | view_state, views = xr.locate_views(
22 | session=context.session,
23 | view_locate_info=xr.ViewLocateInfo(
24 | view_configuration_type=context.view_configuration_type,
25 | display_time=frame_state.predicted_display_time,
26 | space=context.space,
27 | )
28 | )
29 | flags = xr.ViewStateFlags(view_state.view_state_flags)
30 | if flags & xr.ViewStateFlags.POSITION_VALID_BIT:
31 | view = views[xr.Eye.LEFT]
32 | print(view.pose, flush=True)
33 | else:
34 | print("pose not valid")
35 | # Slow things down, especially since we are not rendering anything
36 | time.sleep(0.5)
37 | # Don't run forever
38 | if frame_index > 30:
39 | break
40 |
--------------------------------------------------------------------------------
/xr_examples/vive_tracker.py:
--------------------------------------------------------------------------------
1 | """
2 | pyopenxr example program vive_tracker.py
3 |
4 | Prints the position and orientation of your vive trackers each frame.
5 |
6 | Helpful instructions for getting trackers working on Linux are at
7 | https://gist.github.com/DanielArnett/c9a56c9c7cc0def20648480bca1f6772
8 | The udev symbolic link trick was crucial in my case.
9 | """
10 |
11 | import ctypes
12 | from ctypes import cast, byref
13 | import time
14 | import xr
15 |
16 | print("Warning: trackers with role 'Handheld object' won't be detected.")
17 |
18 | # ContextObject is a high level pythonic class meant to keep simple cases simple.
19 | with xr.ContextObject(
20 | instance_create_info=xr.InstanceCreateInfo(
21 | enabled_extension_names=[
22 | # A graphics extension is mandatory (without a headless extension)
23 | xr.KHR_OPENGL_ENABLE_EXTENSION_NAME,
24 | xr.extension.HTCX_vive_tracker_interaction.NAME,
25 | ],
26 | ),
27 | ) as context:
28 | instance = context.instance
29 | session = context.session
30 |
31 | # Save the function pointer
32 | enumerateViveTrackerPathsHTCX = cast(
33 | xr.get_instance_proc_addr(
34 | instance,
35 | "xrEnumerateViveTrackerPathsHTCX",
36 | ),
37 | xr.PFN_xrEnumerateViveTrackerPathsHTCX
38 | )
39 |
40 | # Create the action with subaction path
41 | # Role strings from
42 | # https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#XR_HTCX_vive_tracker_interaction
43 | role_strings = [
44 | "handheld_object",
45 | "left_foot",
46 | "right_foot",
47 | "left_shoulder",
48 | "right_shoulder",
49 | "left_elbow",
50 | "right_elbow",
51 | "left_knee",
52 | "right_knee",
53 | "waist",
54 | "chest",
55 | "camera",
56 | "keyboard",
57 | ]
58 | role_path_strings = [f"/user/vive_tracker_htcx/role/{role}"
59 | for role in role_strings]
60 | role_paths = (xr.Path * len(role_path_strings))(
61 | *[xr.string_to_path(instance, role_string) for role_string in role_path_strings],
62 | )
63 | pose_action = xr.create_action(
64 | action_set=context.default_action_set,
65 | create_info=xr.ActionCreateInfo(
66 | action_type=xr.ActionType.POSE_INPUT,
67 | action_name="tracker_pose",
68 | localized_action_name="Tracker Pose",
69 | count_subaction_paths=len(role_paths),
70 | subaction_paths=role_paths,
71 | ),
72 | )
73 | # Describe a suggested binding for that action and subaction path
74 | suggested_binding_paths = (xr.ActionSuggestedBinding * len(role_path_strings))(
75 | *[xr.ActionSuggestedBinding(
76 | pose_action,
77 | xr.string_to_path(instance, f"{role_path_string}/input/grip/pose"))
78 | for role_path_string in role_path_strings],
79 | )
80 | xr.suggest_interaction_profile_bindings(
81 | instance=instance,
82 | suggested_bindings=xr.InteractionProfileSuggestedBinding(
83 | interaction_profile=xr.string_to_path(instance, "/interaction_profiles/htc/vive_tracker_htcx"),
84 | count_suggested_bindings=len(suggested_binding_paths),
85 | suggested_bindings=suggested_binding_paths,
86 | )
87 | )
88 | # Create action spaces for locating trackers in each role
89 | tracker_action_spaces = (xr.Space * len(role_paths))(
90 | *[xr.create_action_space(
91 | session=session,
92 | create_info=xr.ActionSpaceCreateInfo(
93 | action=pose_action,
94 | subaction_path=role_path,
95 | )
96 | ) for role_path in role_paths],
97 | )
98 |
99 | n_paths = ctypes.c_uint32(0)
100 | result = enumerateViveTrackerPathsHTCX(instance, 0, byref(n_paths), None)
101 | if xr.check_result(result).is_exception():
102 | raise result
103 | vive_tracker_paths = (xr.ViveTrackerPathsHTCX * n_paths.value)(*([xr.ViveTrackerPathsHTCX()] * n_paths.value))
104 | # print(xr.Result(result), n_paths.value)
105 | result = enumerateViveTrackerPathsHTCX(instance, n_paths, byref(n_paths), vive_tracker_paths)
106 | if xr.check_result(result).is_exception():
107 | raise result
108 | print(xr.Result(result), n_paths.value)
109 | # print(*vive_tracker_paths)
110 |
111 | # Loop over the render frames
112 | session_was_focused = False # Check for a common problem
113 | for frame_index, frame_state in enumerate(context.frame_loop()):
114 |
115 | if context.session_state == xr.SessionState.FOCUSED:
116 | session_was_focused = True
117 | active_action_set = xr.ActiveActionSet(
118 | action_set=context.default_action_set,
119 | subaction_path=xr.NULL_PATH,
120 | )
121 | xr.sync_actions(
122 | session=session,
123 | sync_info=xr.ActionsSyncInfo(
124 | count_active_action_sets=1,
125 | active_action_sets=ctypes.pointer(active_action_set),
126 | ),
127 | )
128 |
129 | n_paths = ctypes.c_uint32(0)
130 | result = enumerateViveTrackerPathsHTCX(instance, 0, byref(n_paths), None)
131 | if xr.check_result(result).is_exception():
132 | raise result
133 | vive_tracker_paths = (xr.ViveTrackerPathsHTCX * n_paths.value)(*([xr.ViveTrackerPathsHTCX()] * n_paths.value))
134 | # print(xr.Result(result), n_paths.value)
135 | result = enumerateViveTrackerPathsHTCX(instance, n_paths, byref(n_paths), vive_tracker_paths)
136 | if xr.check_result(result).is_exception():
137 | raise result
138 | # print(xr.Result(result), n_paths.value)
139 | # print(*vive_tracker_paths)
140 |
141 | found_tracker_count = 0
142 | for index, space in enumerate(tracker_action_spaces):
143 | space_location = xr.locate_space(
144 | space=space,
145 | base_space=context.space,
146 | time=frame_state.predicted_display_time,
147 | )
148 | if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT:
149 | print(f"{role_strings[index]}: {space_location.pose}")
150 | found_tracker_count += 1
151 | if found_tracker_count == 0:
152 | print("no trackers found")
153 |
154 | # Slow things down, especially since we are not rendering anything
155 | time.sleep(0.5)
156 | # Don't run forever
157 | if frame_index > 30:
158 | break
159 | if not session_was_focused:
160 | print("This OpenXR session never entered the FOCUSED state. Did you wear the headset?")
161 |
--------------------------------------------------------------------------------
/xr_examples/where_does_it_hang.py:
--------------------------------------------------------------------------------
1 | """
2 | This is a work in progress...
3 |
4 | The goal here is to create the simplest case to demonstrate the hang that occurs
5 | in xr.destroy_instance() on Linux with SteamVR.
6 |
7 | The hang does not occur on Windows; it *does* occur on linux.
8 | The hang does not occur when calling xr.destroy_instance() without out any frame loop calls.
9 | it *does* occur when calling xr.destroy_instance after running a frame loop.
10 | """
11 |
12 | import ctypes
13 | from ctypes import byref, c_int32, c_void_p, cast, POINTER, pointer, Structure
14 | import logging # 1) Use the python logging system
15 | import os
16 | import platform
17 | import time
18 |
19 | if platform.system() == "Windows":
20 | from OpenGL import WGL
21 | elif platform.system() == "Linux":
22 | from OpenGL import GLX
23 | from OpenGL import GL
24 | import xr
25 |
26 |
27 | run_frame_loop = True
28 |
29 |
30 | class Swapchain(Structure):
31 | _fields_ = [
32 | ("handle", xr.Swapchain),
33 | ("width", c_int32),
34 | ("height", c_int32),
35 | ]
36 |
37 |
38 | # 2) Hook into the pyopenxr logger hierarchy.
39 | logging.basicConfig() # You might want also to study the parameters to basicConfig()...
40 | pyopenxr_logger = logging.getLogger("pyopenxr")
41 | # show me ALL the messages
42 | pyopenxr_logger.setLevel(logging.INFO) # Modify argument according to your current needs
43 |
44 | # 3) Create a child logger for this particular program
45 | logger = logging.getLogger("pyopenxr.where_does_it_hang")
46 | logger.info("Hey, this logging thing works!") # Test it out
47 |
48 | # Use API layers for debugging
49 | enabled_api_layers = []
50 |
51 | # 4) Core validation adds additional messages about correct use of the OpenXR api
52 | if xr.LUNARG_core_validation_APILAYER_NAME in xr.enumerate_api_layer_properties():
53 | enabled_api_layers.append(xr.LUNARG_core_validation_APILAYER_NAME)
54 |
55 | # 5) API dump shows the details of every OpenXR call. Use this for deep debugging only.
56 | if False and xr.LUNARG_api_dump_APILAYER_NAME in xr.enumerate_api_layer_properties():
57 | enabled_api_layers.append(xr.LUNARG_api_dump_APILAYER_NAME)
58 | os.environ["XR_API_DUMP_EXPORT_TYPE"] = "text" # or "html"
59 | # os.environ["XR_API_DUMP_FILE_NAME"] = "/some/file/name"
60 |
61 | # Use extensions for debugging
62 | enabled_extensions = []
63 | # 6) XR_EXT_debug_utils can be used to redirect pyopenxr debugging messages to our logger,
64 | # among other things.
65 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties():
66 | enabled_extensions.append(xr.EXT_DEBUG_UTILS_EXTENSION_NAME)
67 |
68 |
69 | # Define helper function for our logging callback
70 | def openxr_log_level(severity_flags: xr.DebugUtilsMessageSeverityFlagsEXT) -> int:
71 | """Convert OpenXR message severities to python logging severities."""
72 | if severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
73 | return logging.ERROR
74 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
75 | return logging.WARNING
76 | elif severity_flags & xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
77 | return logging.INFO
78 | else:
79 | return logging.DEBUG
80 |
81 |
82 | def xr_debug_callback(
83 | severity: xr.DebugUtilsMessageSeverityFlagsEXT,
84 | _type: xr.DebugUtilsMessageTypeFlagsEXT,
85 | data: ctypes.POINTER(xr.DebugUtilsMessengerCallbackDataEXT),
86 | _user_data: ctypes.c_void_p) -> bool:
87 | """Redirect OpenXR messages to our python logger."""
88 | d = data.contents
89 | pyopenxr_logger.log(
90 | level=openxr_log_level(severity),
91 | msg=f"OpenXR: {d.function_name.decode()}: {d.message.decode()}")
92 | return True
93 |
94 |
95 | # Prepare to create a debug utils messenger
96 | pfn_xr_debug_callback = xr.PFN_xrDebugUtilsMessengerCallbackEXT(xr_debug_callback)
97 | debug_utils_messenger_create_info = xr.DebugUtilsMessengerCreateInfoEXT(
98 | message_severities=(
99 | xr.DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
100 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
101 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
102 | | xr.DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
103 | ),
104 | message_types=(
105 | xr.DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
106 | | xr.DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
107 | | xr.DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT
108 | | xr.DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT
109 | ),
110 | user_callback=pfn_xr_debug_callback,
111 | )
112 |
113 | ptr_debug_messenger = None
114 | if xr.EXT_DEBUG_UTILS_EXTENSION_NAME in xr.enumerate_instance_extension_properties():
115 | # Insert our debug_utils_messenger create_info
116 | ptr_debug_messenger = ctypes.cast(
117 | ctypes.pointer(debug_utils_messenger_create_info), ctypes.c_void_p)
118 |
119 | # 7) Turn on extra debugging messages in the OpenXR Loader
120 | os.environ["XR_LOADER_DEBUG"] = "all"
121 | os.environ["LD_BIND_NOW"] = "1"
122 |
123 |
124 | # Create OpenXR instance with attached layers, extensions, and debug messenger.
125 | enabled_extensions.append(xr.KHR_OPENGL_ENABLE_EXTENSION_NAME)
126 | instance = xr.create_instance(
127 | xr.InstanceCreateInfo(
128 | enabled_api_layer_names=enabled_api_layers,
129 | enabled_extension_names=enabled_extensions,
130 | next=ptr_debug_messenger,
131 | )
132 | )
133 |
134 | system_id = xr.get_system(
135 | instance=instance,
136 | get_info=xr.SystemGetInfo(
137 | form_factor=xr.FormFactor.HEAD_MOUNTED_DISPLAY,
138 | ),
139 | )
140 |
141 | graphics = xr.OpenGLGraphics(
142 | instance=instance,
143 | system=system_id,
144 | title="Horatio Hornblower",
145 | )
146 |
147 | # OpenGL can report debug messages too
148 | # Create a sub-logger for messages from OpenGL
149 | gl_logger = logging.getLogger("pyopenxr.opengl")
150 |
151 |
152 | def gl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user):
153 | """Redirect OpenGL debug messages"""
154 | log_level = {
155 | GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR,
156 | GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING,
157 | GL.GL_DEBUG_SEVERITY_LOW: logging.INFO,
158 | GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG,
159 | }[severity]
160 | gl_logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}")
161 |
162 |
163 | gl_debug_message_proc = GL.GLDEBUGPROC(gl_debug_message_callback)
164 | # We need an active OpenGL context before calling glDebugMessageCallback
165 | graphics.make_current()
166 | GL.glDebugMessageCallback(gl_debug_message_proc, None)
167 |
168 | graphics_binding_pointer = cast(pointer(graphics.graphics_binding), c_void_p)
169 | session = xr.create_session(
170 | instance=instance,
171 | create_info=xr.SessionCreateInfo(
172 | system_id=system_id,
173 | next=graphics_binding_pointer,
174 | ),
175 | )
176 |
177 | space = xr.create_reference_space(
178 | session=session,
179 | create_info=xr.ReferenceSpaceCreateInfo(),
180 | )
181 |
182 | default_action_set = xr.create_action_set(
183 | instance=instance,
184 | create_info=xr.ActionSetCreateInfo(
185 | action_set_name="default_action_set",
186 | localized_action_set_name="Default Action Set",
187 | priority=0,
188 | ),
189 | )
190 |
191 | # Create swapchains
192 | view_configuration_type = xr.ViewConfigurationType.PRIMARY_STEREO
193 | config_views = xr.enumerate_view_configuration_views(
194 | instance=instance,
195 | system_id=system_id,
196 | view_configuration_type=view_configuration_type,
197 | )
198 | graphics.initialize_resources()
199 | swapchain_formats = xr.enumerate_swapchain_formats(session)
200 | color_swapchain_format = graphics.select_color_swapchain_format(swapchain_formats)
201 | # Create a swapchain for each view.
202 | swapchains = []
203 | swapchain_image_buffers = []
204 | swapchain_image_ptr_buffers = []
205 | for vp in config_views:
206 | # Create the swapchain.
207 | swapchain_create_info = xr.SwapchainCreateInfo(
208 | array_size=1,
209 | format=color_swapchain_format,
210 | width=vp.recommended_image_rect_width,
211 | height=vp.recommended_image_rect_height,
212 | mip_count=1,
213 | face_count=1,
214 | sample_count=vp.recommended_swapchain_sample_count,
215 | usage_flags=xr.SwapchainUsageFlags.SAMPLED_BIT | xr.SwapchainUsageFlags.COLOR_ATTACHMENT_BIT,
216 | )
217 | swapchain = Swapchain(
218 | xr.create_swapchain(
219 | session=session,
220 | create_info=swapchain_create_info,
221 | ),
222 | swapchain_create_info.width,
223 | swapchain_create_info.height,
224 | )
225 | swapchains.append(swapchain)
226 | swapchain_image_buffer = xr.enumerate_swapchain_images(
227 | swapchain=swapchain.handle,
228 | element_type=graphics.swapchain_image_type,
229 | )
230 | # Keep the buffer alive by moving it into the list of buffers.
231 | swapchain_image_buffers.append(swapchain_image_buffer)
232 | capacity = len(swapchain_image_buffer)
233 | swapchain_image_ptr_buffer = (POINTER(xr.SwapchainImageBaseHeader) * capacity)()
234 | for ix in range(capacity):
235 | swapchain_image_ptr_buffer[ix] = cast(
236 | byref(swapchain_image_buffer[ix]),
237 | POINTER(xr.SwapchainImageBaseHeader))
238 | swapchain_image_ptr_buffers.append(swapchain_image_ptr_buffer)
239 | graphics.make_current()
240 | # frame_loop
241 | action_sets = [default_action_set,]
242 | xr.attach_session_action_sets(
243 | session=session,
244 | attach_info=xr.SessionActionSetsAttachInfo(
245 | count_action_sets=len(action_sets),
246 | action_sets=(xr.ActionSet * len(action_sets))(
247 | *action_sets
248 | )
249 | ),
250 | )
251 | session_is_running = False
252 | frame_count = 0
253 |
254 | if run_frame_loop:
255 | while True:
256 | frame_count += 1
257 | window_closed = graphics.poll_events()
258 | if window_closed:
259 | xr.request_exit_session(session)
260 | # poll_xr_events
261 | exit_render_loop = False
262 | while True:
263 | try:
264 | event_buffer = xr.poll_event(instance)
265 | event_type = xr.StructureType(event_buffer.type)
266 | if event_type == xr.StructureType.EVENT_DATA_INSTANCE_LOSS_PENDING:
267 | # still handle rest of the events instead of immediately quitting
268 | pass
269 | elif event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED \
270 | and session is not None:
271 | event = cast(
272 | byref(event_buffer),
273 | POINTER(xr.EventDataSessionStateChanged)).contents
274 | session_state = xr.SessionState(event.state)
275 | logger.info(f"Session state changed to {str(session_state)}")
276 | if session_state == xr.SessionState.IDLE:
277 | pass
278 | elif session_state == xr.SessionState.READY:
279 | xr.begin_session(
280 | session=session,
281 | begin_info=xr.SessionBeginInfo(
282 | view_configuration_type,
283 | ),
284 | )
285 | session_is_running = True
286 | elif session_state == xr.SessionState.STOPPING:
287 | session_is_running = False
288 | xr.end_session(session)
289 | elif session_state == xr.SessionState.EXITING:
290 | exit_render_loop = True
291 | elif session_state == xr.SessionState.LOSS_PENDING:
292 | exit_render_loop = True
293 | elif event_type == xr.StructureType.EVENT_DATA_VIVE_TRACKER_CONNECTED_HTCX:
294 | vive_tracker_connected = cast(byref(event_buffer), POINTER(xr.EventDataViveTrackerConnectedHTCX)).contents
295 | paths = vive_tracker_connected.paths.contents
296 | persistent_path_str = xr.path_to_string(instance, paths.persistent_path)
297 | # print(f"Vive Tracker connected: {persistent_path_str}")
298 | if paths.role_path != xr.NULL_PATH:
299 | role_path_str = xr.path_to_string(instance, paths.role_path)
300 | # print(f" New role is: {role_path_str}")
301 | else:
302 | # print(f" No role path.")
303 | pass
304 | elif event_type == xr.StructureType.EVENT_DATA_INTERACTION_PROFILE_CHANGED:
305 | # print("data interaction profile changed")
306 | # TODO:
307 | pass
308 | except xr.EventUnavailable:
309 | break
310 | # end of poll_xr_events
311 | if exit_render_loop:
312 | break
313 | if session_is_running:
314 | if session_state in (
315 | xr.SessionState.READY,
316 | xr.SessionState.SYNCHRONIZED,
317 | xr.SessionState.VISIBLE,
318 | xr.SessionState.FOCUSED,
319 | ):
320 | # xr.request_exit_session(session) # Request exit here allows clean exit
321 | # Something about begin/wait/end_frame is making it hang at the end.
322 | frame_state = xr.wait_frame(session, xr.FrameWaitInfo())
323 | xr.begin_frame(session, xr.FrameBeginInfo())
324 | render_layers = []
325 | if frame_state.should_render:
326 | graphics.make_current()
327 | GL.glClearColor(1, 0.6, 0.6, 1)
328 | GL.glClear(GL.GL_COLOR_BUFFER_BIT)
329 | time.sleep(0.02)
330 | xr.end_frame(
331 | session,
332 | frame_end_info=xr.FrameEndInfo(
333 | display_time=frame_state.predicted_display_time,
334 | environment_blend_mode=xr.EnvironmentBlendMode.OPAQUE,
335 | layers=render_layers,
336 | )
337 | )
338 | # xr.request_exit_session(session) # Request exit here allows clean exit but steamvr is hosed.
339 | if frame_count > 100:
340 | xr.request_exit_session(session)
341 | else:
342 | time.sleep(0.02)
343 |
344 | # Clean up
345 | graphics.destroy()
346 | logger.info("About to call xr.destroy_instance()...")
347 | xr.destroy_instance(instance)
348 | logger.info("... called xr.destroy_instance().")
349 |
--------------------------------------------------------------------------------