Compare commits

...
58 Commits
Author SHA1 Message Date
Hyblocker 8100d776b8 Merge pull request #14 from PiMaker/pi-adds-require-trigger-press-mode
adds require trigger press mode
2024-07-14 17:33:06 +02:00
Hyblocker 137d8916af Merge pull request #13 from PiMaker/pi-does-dynamic-calibration-speed-in-continuous
dynamic calibration speed in continuous, merge arcticfox's relative pose lock
2024-07-14 17:32:56 +02:00
Hyblocker ceeab0e6e2 Merge pull request #12 from PiMaker/pi-falls-back-to-exclude-serial-for-hmd
fall back to serial-excluded matching for HMD finding
2024-07-14 17:32:45 +02:00
Hyblocker 797f65ad0d Merge pull request #11 from PiMaker/pi-pause-on-tracking-lost
detect when headset does not send valid tracking data and pause
2024-07-14 17:32:32 +02:00
pi 3460437924 fall back to serial-excluded matching for HMD finding
this allows keeping settings working between different tracking
methods (e.g. switching from Virtual Desktop to Oculus Link will
not break detecting the same HMD)

for some reason serial number changes between link methods...
2024-07-06 18:51:14 +02:00
pi 67f7c820f5 move trigger detection earlier 2024-07-06 16:56:05 +02:00
pi 3ac6dfc208 add "require trigger press to apply calibration" mode 2024-07-06 16:55:28 +02:00
pi 8b86898417 allow calibration speed setting to be used for continuous 2024-07-06 16:52:52 +02:00
pi d9fb08560b dynamically calculate dropping frames in continuous 2024-07-06 16:51:03 +02:00
pi 0ac3ab3ad8 Merge remote-tracking branch 'arcticfox/Branch_v1.4-bd_-r2' into pi-merges-relative-pose-lock 2024-07-06 16:50:05 +02:00
pi b0fe076845 detect when headset does not send valid tracking data and pause
this happens when a Quest enters out-of-bounds passthrough mode or
standby for example, in which case it's almost always cleaner to keep
the current calibration alive instead of calibrating towards a zero
location
2024-07-06 16:47:27 +02:00
Hyblocker 5fba766d4d Merge pull request #10 from Tropingenie/Branch_v1.4-bd_-r2
Update README.md
2024-06-18 01:36:29 +02:00
Tropingenie 9da1c41927 Update README.md
Updates "latest release" hyperlink. Now correctly opens latest release, and points to hyblocker repo instead of pushrax.
2024-06-16 20:26:16 -07:00
Hyblocker 31fb1d15f9 Add preliminary support for playspace scale 2024-05-22 20:17:32 +02:00
Hyblocker 16625138e1 Return more verbose error on startup fail 2024-05-22 20:16:30 +02:00
Evgeny Zuev d0159852e1 Add relative pose lock, use slow preset again 2024-05-21 20:36:15 +03:00
Hyblocker d22b2d1c69 Make settings UI more coherent 2024-02-18 19:04:23 +01:00
Hyblocker 308641ebf2 Add tracker offsets 2024-02-18 19:01:21 +01:00
Hyblocker 19ed75c378 Add tracker offset UI to settings 2024-02-18 05:23:09 +01:00
Hyblocker 4dd9ccfc79 Group settings into groups 2024-02-18 05:22:53 +01:00
Hyblocker f578999f0e Rename alignment speeds to settings 2024-02-18 05:20:57 +01:00
Hyblocker 7a4dc9007d Save cc tracker offset to config 2024-02-18 05:20:21 +01:00
Hyblocker 02f4aa22dc Add fields for cc tracker offset 2024-02-18 05:20:03 +01:00
Hyblocker 89d231472a Fix crash with keyboard input 2024-02-18 05:19:40 +01:00
Hyblocker 1515312e76 Add Extension to ImGui for box style containers 2024-02-18 05:19:22 +01:00
Hyblocker 7349f510ef Add instructions for continuous calibration 2024-02-17 17:44:12 +01:00
Hyblocker 71b82d0641 Cleanup installer dir 2024-02-17 17:36:57 +01:00
Hyblocker 68476caafc Bump version 2024-02-17 17:35:33 +01:00
Hyblocker 011158cd2c Add special handling for Pimax Crystal
Pimax please don't touch other drivers and please don't lie about what you are 😭
2024-02-17 17:34:17 +01:00
Hyblocker a8c48136c0 Make IPC errors work on non latin machines 2024-02-17 17:33:32 +01:00
Hyblocker dc9ada1a60 Remove missing file from project 2024-02-17 17:32:50 +01:00
Hyblocker 3acdbe5654 Bump version 2023-12-06 12:06:13 +01:00
Hyblocker de0d3425fa Merge pull request #1 from TCL987/limit-minimized-frame-rate
Limit the frame rate when minimized
2023-12-06 11:51:48 +01:00
ArcticFox8515 3b8ea60c8a Revert back to fast continuous calibration 2023-10-23 17:53:08 +03:00
ArcticFox8515 56531e0432 Optimize resource usage when running in background
Draw GUI only when window or overlay is visible.
Set minimal update time to 1/90 seconds,
so application won't "busy loop".
2023-10-22 18:00:47 +03:00
TCL987 1c18cd5445 Limit the frame rate when minimized
This avoids high CPU usage when minimized.
2023-10-10 23:42:13 -07:00
ArcticFox8515 e371b1d13f Use only yaw rotation for calibration
Pitch and roll offsets usually should be 0
because "up" vector in all VR tracking systems
is the same.
2023-09-23 19:29:18 +03:00
ArcticFox8515 cc4ffa774d Use slow preset; save relative transform
1. Use SLOW preset for continuous calibration. This should
   increase its accuracy with expense of increased CPU usage
   before fully calibrated.

2. Save/load relative transform, so new calibration doesn't have
  to start from scratch.
2023-08-20 19:10:25 +03:00
ArcticFox8515 9a0204466a Continuous calibration improvements
Use best effort calibration while nothing better is available,
this makes calibration converge within seconds.

Use relative pose for quick re-calibration until it provides
sensible quality result. This saves a lot of CPU load after
pose is calibrated.
2023-05-30 20:47:02 +03:00
bd_ 71e9683dc4 r2 2022-09-25 18:08:19 -07:00
bd_ f21de937f0 Enable calibration loading for continuous calibration mode
Fixes: https://github.com/bdunderscore/OpenVR-SpaceCalibrator/issues/2
2022-09-25 18:08:19 -07:00
bd_ aecc9612e2 Import minhook as a submodule
libMinHook needs to be built using the same VC++ version as OVRAS.
To help keep things consistent without having to check in binaries,
include libMinHook as a submodule so we can build it as part of the
solution.
2022-09-25 18:08:19 -07:00
bd_ 0e8ab54400 Save alignment config 2022-09-24 18:50:57 -07:00
bd_ c5c63da491 Adjustments to calibration config UI 2022-09-24 18:48:19 -07:00
bd_ f779d68dff Adjusting default static calibration configuration 2022-09-24 18:13:28 -07:00
bd_ 20a1054111 [v1.1] Add manual mark logs button 2022-08-23 18:09:23 -07:00
bd_ 3e3d7750f1 Bump version 2022-08-21 18:56:16 -07:00
bd_ 37a951a4cc Fixing logging bugs, adding switch for logging 2022-08-21 18:56:08 -07:00
bd_ 492743e4d0 Update protocol version 2022-08-21 18:46:19 -07:00
bd_ d9af36f290 Add calibration log 2022-08-21 18:46:10 -07:00
bd_ 98760540e6 UI improvements 2022-08-21 18:01:10 -07:00
bd_ be51eeeda1 Gradual correction 2022-08-20 19:53:36 -07:00
bd_ 1e8191bcf1 Static calibration mode works 2022-08-16 17:23:58 -07:00
bd_ 889cdf9828 Infrastructure for more internal metrics 2022-08-14 16:50:11 -07:00
bd_ 650d86dcc3 More extensible debug UI 2022-08-14 14:57:52 -07:00
bd_ 996373e07d Separate out continuous calibration UI 2022-08-14 14:09:32 -07:00
bd_ 50ecc1af1c Calibration debug UI 2022-08-14 13:46:34 -07:00
bd_ 50991f17a8 Update version string 2022-08-13 15:03:28 -07:00
44 changed files with 16461 additions and 618 deletions

No files matched your search

+1
View File
@@ -9,3 +9,4 @@ ipch
/x64/
/lib/boost*
/install/OpenVR-SpaceCalibrator.exe
lib/Release/
+3
View File
@@ -0,0 +1,3 @@
[submodule "lib/minhook"]
path = lib/minhook
url = https://github.com/bdunderscore/minhook.git
+568 -1
View File
@@ -1,6 +1,8 @@
== OpenVR-SpaceCalibrator ==
MIT License
Copyright (c) 2020 Justin Li
Copyright (c) 2020-2022 Justin Li and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +21,568 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This software also includes components licensed as follows:
== MinHook ==
MinHook - The Minimalistic API Hooking Library for x64/x86
Copyright (C) 2009-2017 Tsuda Kageyu.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
Portions of this software are Copyright (c) 2008-2009, Vyacheslav Patkov.
================================================================================
Hacker Disassembler Engine 32 C
Copyright (c) 2008-2009, Vyacheslav Patkov.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Hacker Disassembler Engine 64 C
Copyright (c) 2008-2009, Vyacheslav Patkov.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
== ImGui ==
The MIT License (MIT)
Copyright (c) 2014-2022 Omar Cornut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
== ImPlot ==
MIT License
Copyright (c) 2020 Evan Pezent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
== glfw ==
Copyright (c) 2002-2006 Marcus Geelnard
Copyright (c) 2006-2016 Camilla Berglund <elmindreda@glfw.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would
be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not
be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
== gl3w ==
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
== Eigen ==
Portions copyrighted by many parties; see source code for names.
(The upstream distribution does not provide any single listing)
This application uses only the MPL-2.0 licensed components of Eigen.
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+28 -2
View File
@@ -1,28 +1,54 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
# Visual Studio Version 17
VisualStudioVersion = 17.3.32819.101
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OpenVR-SpaceCalibrator", "OpenVR-SpaceCalibrator\OpenVR-SpaceCalibrator.vcxproj", "{63BAE169-D595-4464-8E7E-50D6FD379348}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OpenVR-SpaceCalibratorDriver", "OpenVR-SpaceCalibratorDriver\OpenVR-SpaceCalibratorDriver.vcxproj", "{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}"
ProjectSection(ProjectDependencies) = postProject
{F142A341-5EE0-442D-A15F-98AE9B48DBAE} = {F142A341-5EE0-442D-A15F-98AE9B48DBAE}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libMinHook", "lib\minhook\build\VC16\libMinHook.vcxproj", "{F142A341-5EE0-442D-A15F-98AE9B48DBAE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{63BAE169-D595-4464-8E7E-50D6FD379348}.Debug|x64.ActiveCfg = Debug|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Debug|x64.Build.0 = Debug|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Debug|x86.ActiveCfg = Debug|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Debug|x86.Build.0 = Debug|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Release|x64.ActiveCfg = Release|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Release|x64.Build.0 = Release|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Release|x86.ActiveCfg = Release|x64
{63BAE169-D595-4464-8E7E-50D6FD379348}.Release|x86.Build.0 = Release|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Debug|x64.ActiveCfg = Debug|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Debug|x64.Build.0 = Debug|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Debug|x86.ActiveCfg = Debug|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Debug|x86.Build.0 = Debug|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Release|x64.ActiveCfg = Release|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Release|x64.Build.0 = Release|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Release|x86.ActiveCfg = Release|x64
{A61324AD-CE32-46D1-A95E-7E28A6D8CCA7}.Release|x86.Build.0 = Release|x64
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.ActiveCfg = Debug|x64
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x64.Build.0 = Debug|x64
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x86.ActiveCfg = Debug|Win32
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Debug|x86.Build.0 = Debug|Win32
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.ActiveCfg = Release|x64
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x64.Build.0 = Release|x64
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x86.ActiveCfg = Release|Win32
{F142A341-5EE0-442D-A15F-98AE9B48DBAE}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCE727E8-4604-4D4F-B5FF-321A7D7868D6}
EndGlobalSection
EndGlobal
+160 -36
View File
@@ -1,8 +1,10 @@
#include "stdafx.h"
#include "Calibration.h"
#include "CalibrationMetrics.h"
#include "Configuration.h"
#include "IPCClient.h"
#include "CalibrationCalc.h"
#include "VRState.h"
#include <string>
#include <vector>
@@ -19,6 +21,10 @@ inline vr::HmdQuaternion_t operator*(const vr::HmdQuaternion_t& lhs, const vr::H
};
}
CalibrationContext CalCtx;
IPCClient Driver;
static protocol::DriverPoseShmem shmem;
namespace {
CalibrationCalc calibration;
@@ -178,6 +184,13 @@ namespace {
return false;
}
// Apply tracker offsets
if (CalCtx.state == CalibrationState::Continuous || CalCtx.state == CalibrationState::ContinuousStandby) {
reference.vecPosition[0] += ctx.continuousCalibrationOffset.x();
reference.vecPosition[1] += ctx.continuousCalibrationOffset.y();
reference.vecPosition[2] += ctx.continuousCalibrationOffset.z();
}
calibration.PushSample(Sample(
ConvertPose(reference),
ConvertPose(target)
@@ -185,16 +198,34 @@ namespace {
return true;
}
bool AssignTargets() {
auto state = VRState::Load();
if (CalCtx.referenceID < 0) {
CalCtx.referenceID = state.FindDevice(CalCtx.referenceStandby.trackingSystem, CalCtx.referenceStandby.model, CalCtx.referenceStandby.serial);
}
if (CalCtx.targetID < 0) {
CalCtx.targetID = state.FindDevice(CalCtx.targetStandby.trackingSystem, CalCtx.targetStandby.model, CalCtx.targetStandby.serial);
}
for (int i = 0; i < CalCtx.MAX_CONTROLLERS; i++) {
if (i < state.devices.size()
&& state.devices[i].trackingSystem == CalCtx.targetTrackingSystem
&& state.devices[i].deviceClass == vr::TrackedDeviceClass_Controller
&& (state.devices[i].controllerRole == vr::TrackedControllerRole_LeftHand || state.devices[i].controllerRole == vr::TrackedControllerRole_RightHand))
{
CalCtx.controllerIDs[i] = state.devices[i].id;
} else {
CalCtx.controllerIDs[i] = -1;
}
}
return CalCtx.referenceID >= 0 && CalCtx.targetID >= 0;
}
}
IPCClient m_Driver;
protocol::DriverPoseShmem m_shmem;
CalibrationContext CalCtx;
IPCClient Driver;
static protocol::DriverPoseShmem shmem;
void InitCalibrator()
{
Driver.Connect();
@@ -218,11 +249,14 @@ static_assert(vr::k_unTrackedDeviceIndex_Hmd == 0, "HMD index expected to be 0")
void ScanAndApplyProfile(CalibrationContext &ctx)
{
std::unique_ptr<char[]> buffer_array(new char
[vr::k_unMaxPropertyStringSize]);
std::unique_ptr<char[]> buffer_array(new char [vr::k_unMaxPropertyStringSize]);
char* buffer = buffer_array.get();
ctx.enabled = ctx.validProfile;
protocol::Request setParamsReq(protocol::RequestSetAlignmentSpeedParams);
setParamsReq.setAlignmentSpeedParams = ctx.alignmentSpeedParams;
Driver.SendBlocking(setParamsReq);
for (uint32_t id = 0; id < vr::k_unMaxTrackedDeviceCount; ++id)
{
auto deviceClass = vr::VRSystem()->GetTrackedDeviceClass(id);
@@ -260,6 +294,22 @@ void ScanAndApplyProfile(CalibrationContext &ctx)
//auto p = ctx.devicePoses[id].mDeviceToAbsoluteTracking.m;
//printf("HMD %d: %f %f %f\n", id, p[0][3], p[1][3], p[2][3]);
// Check if the current HMD is a Pimax crystal
if (trackingSystem == "aapvr") {
// HMD is a Pimax HMD
vr::HmdMatrix34_t eyeToHeadLeft = vr::VRSystem()->GetEyeToHeadTransform(vr::Eye_Left);
// Crystal's projection matrix is constant 0s or 1s except for [0][3], which stores the IPD offset from the nose
bool isCrystalHmd =
eyeToHeadLeft.m[0][0] == 1 && eyeToHeadLeft.m[0][1] == 0 && eyeToHeadLeft.m[0][2] == 0 && // IPD
eyeToHeadLeft.m[1][0] == 0 && eyeToHeadLeft.m[1][1] == 1 && eyeToHeadLeft.m[1][2] == 0 && eyeToHeadLeft.m[1][3] == 0 &&
eyeToHeadLeft.m[2][0] == 0 && eyeToHeadLeft.m[2][1] == 0 && eyeToHeadLeft.m[2][2] == 1 && eyeToHeadLeft.m[2][3] == 0;
if (isCrystalHmd) {
// Move it outside the aapvr system ; we treat aapvr as if it were lighthouse
trackingSystem = "Pimax Crystal HMD";
}
}
if (trackingSystem != ctx.referenceTrackingSystem)
{
// Currently using an HMD with a different tracking system than the calibration.
@@ -270,6 +320,23 @@ void ScanAndApplyProfile(CalibrationContext &ctx)
continue;
}
// Detect Pimax crystal controllers and separate them too
if (deviceClass == vr::TrackedDeviceClass_Controller) {
if (trackingSystem == "oculus") {
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_RenderModelName_String, buffer, vr::k_unMaxPropertyStringSize, &err);
std::string renderModel(buffer);
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_ConnectedWirelessDongle_String, buffer, vr::k_unMaxPropertyStringSize, &err);
std::string connectedWirelessDongle(buffer);
// Check if the controller claims its an oculus controller but also pimax
if (renderModel.find("{aapvr}") != std::string::npos &&
renderModel.find("crystal") != std::string::npos &&
connectedWirelessDongle.find("lighthouse") != std::string::npos) {
trackingSystem = "Pimax Crystal Controllers";
}
}
}
if (trackingSystem != ctx.targetTrackingSystem)
{
ResetAndDisableOffsets(id);
@@ -304,23 +371,35 @@ void ScanAndApplyProfile(CalibrationContext &ctx)
}
}
void StartCalibration()
{
void StartCalibration() {
AssignTargets();
CalCtx.state = CalibrationState::Begin;
CalCtx.wantedUpdateInterval = 0.0;
CalCtx.messages.clear();
calibration.Clear();
Metrics::WriteLogAnnotation("StartCalibration");
}
void StartContinuousCalibration() {
AssignTargets();
StartCalibration();
CalCtx.state = CalibrationState::Continuous;
CalCtx.Log("Collecting initial samples...");
calibration.setRelativeTransformation(CalCtx.refToTargetPose, CalCtx.relativePosCalibrated);
calibration.lockRelativePosition = CalCtx.lockRelativePosition;
if (CalCtx.lockRelativePosition) {
CalCtx.Log("Relative position locked");
}
else {
CalCtx.Log("Collecting initial samples...");
}
Metrics::WriteLogAnnotation("StartContinuousCalibration");
}
void EndContinuousCalibration() {
CalCtx.state = CalibrationState::None;
CalCtx.relativePosCalibrated = false;
SaveProfile(CalCtx);
Metrics::WriteLogAnnotation("EndContinuousCalibration");
}
void CalibrationTick(double time)
@@ -343,8 +422,28 @@ void CalibrationTick(double time)
}
});
// check for non-updating headset tracking space (caused by quest out of bounds or taken off head for example) and abort everything for this tick
auto p = ctx.devicePoses[vr::k_unTrackedDeviceIndex_Hmd].vecPosition;
if ((p[0] == 0.0 && p[1] == 0.0 && p[2] == 0.0) || (ctx.xprev == p[0] && ctx.yprev == p[1] && ctx.zprev == p[2])) {
// std::cerr << "HMD tracking didn't update, skipping update" << std::endl;
return;
}
ctx.xprev = p[0];
ctx.yprev = p[1];
ctx.zprev = p[2];
if (ctx.state == CalibrationState::None || ctx.state == CalibrationState::ContinuousStandby
|| (ctx.state == CalibrationState::Continuous && !calibration.isValid()))
{
if ((time - ctx.timeLastScan) >= 1.0)
{
ScanAndApplyProfile(ctx);
ctx.timeLastScan = time;
}
}
if (ctx.state == CalibrationState::ContinuousStandby) {
if (ctx.targetID >= 0 && ctx.referenceID >= 0) {
if (AssignTargets()) {
StartContinuousCalibration();
}
else {
@@ -354,15 +453,8 @@ void CalibrationTick(double time)
}
}
if (ctx.state == CalibrationState::None)
{
if (ctx.state == CalibrationState::None) {
ctx.wantedUpdateInterval = 1.0;
if ((time - ctx.timeLastScan) >= 1.0)
{
ScanAndApplyProfile(ctx);
ctx.timeLastScan = time;
}
return;
}
@@ -404,17 +496,18 @@ void CalibrationTick(double time)
snprintf(buf, sizeof buf, "Target device ID: %d, serial %s\n", ctx.targetID, targetSerial);
CalCtx.Log(buf);
if (!ctx.devicePoses[ctx.referenceID].poseIsValid)
ScanAndApplyProfile(ctx);
if (!CalCtx.ReferencePoseIsValid())
{
CalCtx.Log("Reference device is not tracking\n"); ok = false;
}
if (!ctx.devicePoses[ctx.targetID].poseIsValid)
if (!CalCtx.TargetPoseIsValid())
{
CalCtx.Log("Target device is not tracking\n"); ok = false;
}
if (ok) {
//ResetAndDisableOffsets(ctx.targetID);
ctx.state = CalibrationState::Rotation;
@@ -435,12 +528,33 @@ void CalibrationTick(double time)
return;
}
if (CalCtx.state == CalibrationState::Continuous && CalCtx.requireTriggerPressToApply) {
bool triggerPressed = true;
vr::VRControllerState_t state;
for (int i = 0; i < CalCtx.MAX_CONTROLLERS; i++) {
if (CalCtx.controllerIDs[i] >= 0) {
vr::VRSystem()->GetControllerState(CalCtx.controllerIDs[i], &state, sizeof(state));
triggerPressed &= state.rAxis[vr::k_eControllerAxis_TrackPad /* matches trigger on Index controllers?? */].x > 0.75f
|| state.rAxis[vr::k_eControllerAxis_Trigger].x > 0.75f;
//printf("Controller %d tracpad: %f\n", i, state.rAxis[vr::k_eControllerAxis_TrackPad].x);
//printf("Controller %d trigger: %f\n", i, state.rAxis[vr::k_eControllerAxis_Trigger].x);
if (!triggerPressed) break;
}
}
if (!triggerPressed) {
ctx.wantedUpdateInterval = 0.5;
CalCtx.Log("Waiting for trigger press...\n");
return;
}
}
if (!CollectSample(ctx))
{
return;
}
CalCtx.Progress(calibration.SampleCount(), CalCtx.SampleCount());
CalCtx.Progress(calibration.SampleCount(), (int)CalCtx.SampleCount());
while (calibration.SampleCount() > CalCtx.SampleCount()) calibration.ShiftSample();
@@ -453,15 +567,20 @@ void CalibrationTick(double time)
if (CalCtx.state == CalibrationState::Continuous) {
CalCtx.messages.clear();
ok = calibration.ComputeIncremental(lerp);
calibration.enableStaticRecalibration = CalCtx.enableStaticRecalibration;
calibration.lockRelativePosition = CalCtx.lockRelativePosition;
ok = calibration.ComputeIncremental(lerp, CalCtx.continuousCalibrationThreshold);
}
else {
calibration.enableStaticRecalibration = false;
ok = calibration.ComputeOneshot();
}
if (calibration.isValid()) {
ctx.calibratedRotation = calibration.EulerRotation();
ctx.calibratedTranslation = calibration.Transformation().translation() * 100.0; // convert to cm units for profile storage
ctx.refToTargetPose = calibration.RelativeTransformation();
ctx.relativePosCalibrated = calibration.isRelativeTransformationCalibrated();
auto vrTrans = VRTranslationVec(ctx.calibratedTranslation);
auto vrRot = VRRotationQuat(Eigen::Quaterniond(calibration.Transformation().rotation()));
@@ -472,9 +591,7 @@ void CalibrationTick(double time)
ScanAndApplyProfile(ctx);
CalCtx.Log("Finished calibration, profile saved\n");
}
else
{
} else {
CalCtx.Log("Calibration failed.\n");
}
@@ -483,17 +600,19 @@ void CalibrationTick(double time)
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
double duration = (end_time.QuadPart - start_time.QuadPart) / (double)freq.QuadPart;
char buf[256];
snprintf(buf, sizeof buf, "Elapsed time for computation: %0.2f\n", duration * 1000.0);
CalCtx.Log(buf);
Metrics::computationTime.Push(duration * 1000.0);
Metrics::WriteLogEntry();
if (CalCtx.state != CalibrationState::Continuous) {
ctx.state = CalibrationState::None;
calibration.Clear();
}
else {
for (int i = 0; i < 10; i++) calibration.ShiftSample();
size_t drop_samples = CalCtx.SampleCount() / 10;
for (int i = 0; i < drop_samples; i++) {
calibration.ShiftSample();
}
}
}
}
@@ -515,8 +634,13 @@ void LoadChaperoneBounds()
void ApplyChaperoneBounds()
{
vr::VRChaperoneSetup()->RevertWorkingCopy();
vr::VRChaperoneSetup()->SetWorkingCollisionBoundsInfo(&CalCtx.chaperone.geometry[0], CalCtx.chaperone.geometry.size());
vr::VRChaperoneSetup()->SetWorkingCollisionBoundsInfo(&CalCtx.chaperone.geometry[0], (uint32_t)CalCtx.chaperone.geometry.size());
vr::VRChaperoneSetup()->SetWorkingStandingZeroPoseToRawTrackingPose(&CalCtx.chaperone.standingCenter);
vr::VRChaperoneSetup()->SetWorkingPlayAreaSize(CalCtx.chaperone.playSpaceSize.v[0], CalCtx.chaperone.playSpaceSize.v[1]);
vr::VRChaperoneSetup()->CommitWorkingCopy(vr::EChaperoneConfigFile_Live);
}
void DebugApplyRandomOffset() {
protocol::Request req(protocol::RequestDebugOffset);
Driver.SendBlocking(req);
}
+60 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <Eigen/Core>
#include <Eigen/Geometry>
#include <Windows.h>
#include <openvr.h>
#include <vector>
@@ -29,6 +30,9 @@ struct CalibrationContext
CalibrationState state = CalibrationState::None;
int32_t referenceID = -1, targetID = -1;
static const size_t MAX_CONTROLLERS = 8;
int32_t controllerIDs[MAX_CONTROLLERS];
StandbyDevice targetStandby, referenceStandby;
Eigen::Vector3d calibratedRotation;
@@ -42,9 +46,22 @@ struct CalibrationContext
bool validProfile = false;
bool clearOnLog = false;
bool quashTargetInContinuous = false;
bool requireTriggerPressToApply = false;
double timeLastTick = 0, timeLastScan = 0;
double wantedUpdateInterval = 1.0;
float xprev, yprev, zprev;
float continuousCalibrationThreshold;
Eigen::Vector3d continuousCalibrationOffset;
protocol::AlignmentSpeedParams alignmentSpeedParams;
bool enableStaticRecalibration;
bool lockRelativePosition = false;
Eigen::AffineCompact3d refToTargetPose = Eigen::AffineCompact3d::Identity();
bool relativePosCalibrated = false;
enum Speed
{
FAST = 0,
@@ -55,6 +72,32 @@ struct CalibrationContext
vr::DriverPose_t devicePoses[vr::k_unMaxTrackedDeviceCount];
CalibrationContext() {
calibratedScale = 1.0;
memset(devicePoses, 0, sizeof(devicePoses));
ResetConfig();
}
void ResetConfig() {
alignmentSpeedParams.thr_rot_tiny = 0.49f * (EIGEN_PI / 180.0f);
alignmentSpeedParams.thr_rot_small = 0.5f * (EIGEN_PI / 180.0f);
alignmentSpeedParams.thr_rot_large = 5.0f * (EIGEN_PI / 180.0f);
alignmentSpeedParams.thr_trans_tiny = 0.98f / 1000.0; // mm
alignmentSpeedParams.thr_trans_small = 1.0f / 1000.0; // mm
alignmentSpeedParams.thr_trans_large = 20.0f / 1000.0; // mm
alignmentSpeedParams.align_speed_tiny = 1.0f;
alignmentSpeedParams.align_speed_small = 1.0f;
alignmentSpeedParams.align_speed_large = 2.0f;
continuousCalibrationThreshold = 1.5f;
continuousCalibrationOffset = Eigen::Vector3d::Zero();
enableStaticRecalibration = true;
}
struct Chaperone
{
bool valid = false;
@@ -82,6 +125,8 @@ struct CalibrationContext
targetTrackingSystem = "";
enabled = false;
validProfile = false;
refToTargetPose = Eigen::AffineCompact3d::Identity();
relativePosCalibrated = true;
}
size_t SampleCount()
@@ -140,6 +185,16 @@ struct CalibrationContext
messages.back().progress = current;
messages.back().target = target;
}
bool TargetPoseIsValid() const {
return targetID >= 0 && targetID <= vr::k_unMaxTrackedDeviceCount
&& devicePoses[targetID].poseIsValid;
}
bool ReferencePoseIsValid() const {
return referenceID >= 0 && referenceID <= vr::k_unMaxTrackedDeviceCount
&& devicePoses[referenceID].poseIsValid;
}
};
extern CalibrationContext CalCtx;
@@ -150,4 +205,8 @@ void StartCalibration();
void StartContinuousCalibration();
void EndContinuousCalibration();
void LoadChaperoneBounds();
void ApplyChaperoneBounds();
void ApplyChaperoneBounds();
void PushCalibrationApplyTime();
void ShowCalibrationDebug(int r, int c);
void DebugApplyRandomOffset();
+292 -75
View File
@@ -1,5 +1,6 @@
#include "CalibrationCalc.h"
#include "Calibration.h"
#include "CalibrationMetrics.h"
#include "..\Protocol.h"
inline vr::HmdQuaternion_t operator*(const vr::HmdQuaternion_t& lhs, const vr::HmdQuaternion_t& rhs) {
@@ -106,6 +107,7 @@ namespace {
}
}
const double CalibrationCalc::AxisVarianceThreshold = 0.001;
void CalibrationCalc::PushSample(const Sample& sample) {
m_samples.push_back(sample);
}
@@ -114,6 +116,9 @@ void CalibrationCalc::Clear() {
m_estimatedTransformation.setIdentity();
m_isValid = false;
m_samples.clear();
m_axisVariance = 0.0;
m_refToTargetPose = Eigen::AffineCompact3d::Identity();
m_relativePosCalibrated = false;
}
Eigen::Vector3d CalibrationCalc::CalibrateRotation() const {
@@ -128,51 +133,52 @@ Eigen::Vector3d CalibrationCalc::CalibrateRotation() const {
deltas.push_back(delta);
}
}
char buf[256];
snprintf(buf, sizeof buf, "Got %zd samples with %zd delta samples\n", m_samples.size(), deltas.size());
CalCtx.Log(buf);
//char buf[256];
//snprintf(buf, sizeof buf, "Got %zd samples with %zd delta samples\n", m_samples.size(), deltas.size());
//CalCtx.Log(buf);
// Kabsch algorithm
Eigen::MatrixXd refPoints(deltas.size(), 3), targetPoints(deltas.size(), 3);
Eigen::Vector3d refCentroid(0, 0, 0), targetCentroid(0, 0, 0);
// Initialize 2D points and centroids
Eigen::MatrixXd refPoints(deltas.size(), 2), targetPoints(deltas.size(), 2);
Eigen::Vector2d refCentroid(0, 0), targetCentroid(0, 0);
for (size_t i = 0; i < deltas.size(); i++)
{
refPoints.row(i) = deltas[i].ref;
refCentroid += deltas[i].ref;
// Fill matrices and calculate centroids
for (size_t i = 0; i < deltas.size(); i++) {
refPoints.row(i) << deltas[i].ref[0], deltas[i].ref[2]; // Take only the x and z components
refCentroid += refPoints.row(i);
targetPoints.row(i) = deltas[i].target;
targetCentroid += deltas[i].target;
}
targetPoints.row(i) << deltas[i].target[0], deltas[i].target[2]; // Take only the x and z components
targetCentroid += targetPoints.row(i);
}
refCentroid /= (double)deltas.size();
targetCentroid /= (double)deltas.size();
refCentroid /= (double)deltas.size();
targetCentroid /= (double)deltas.size();
for (size_t i = 0; i < deltas.size(); i++)
{
refPoints.row(i) -= refCentroid;
targetPoints.row(i) -= targetCentroid;
}
// Center the points
for (size_t i = 0; i < deltas.size(); i++) {
refPoints.row(i) -= refCentroid;
targetPoints.row(i) -= targetCentroid;
}
auto crossCV = refPoints.transpose() * targetPoints;
// Calculate cross-covariance matrix
auto crossCV = refPoints.transpose() * targetPoints;
Eigen::BDCSVD<Eigen::MatrixXd> bdcsvd;
auto svd = bdcsvd.compute(crossCV, Eigen::ComputeThinU | Eigen::ComputeThinV);
// Singular Value Decomposition (SVD)
Eigen::JacobiSVD<Eigen::MatrixXd> svd(crossCV, Eigen::ComputeThinU | Eigen::ComputeThinV);
Eigen::Matrix3d i = Eigen::Matrix3d::Identity();
if ((svd.matrixU() * svd.matrixV().transpose()).determinant() < 0)
{
i(2, 2) = -1;
}
// Calculate 2D rotation matrix
Eigen::Matrix2d i = Eigen::Matrix2d::Identity();
Eigen::Matrix2d rot = svd.matrixV() * i * svd.matrixU().transpose();
Eigen::Matrix3d rot = svd.matrixV() * i * svd.matrixU().transpose();
rot.transposeInPlace();
// Calculate yaw angle in radians
double yaw = std::atan2(rot(1, 0), rot(0, 0));
Eigen::Vector3d euler = rot.eulerAngles(2, 1, 0) * 180.0 / EIGEN_PI;
// Convert to degrees
Eigen::Vector3d euler(0.0, yaw * 180.0 / EIGEN_PI, 0.0);
snprintf(buf, sizeof buf, "Calibrated rotation: yaw=%.2f pitch=%.2f roll=%.2f\n", euler[1], euler[2], euler[0]);
CalCtx.Log(buf);
//snprintf(buf, sizeof buf, "Calibrated rotation: yaw=%.2f pitch=%.2f roll=%.2f\n", euler[1], euler[2], euler[0]);
//CalCtx.Log(buf);
return euler;
}
@@ -221,12 +227,16 @@ Eigen::Vector3d CalibrationCalc::CalibrateTranslation(const Eigen::Matrix3d &rot
Eigen::Vector3d trans = coefficients.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(constants);
auto transcm = trans * 100.0;
char buf[256];
snprintf(buf, sizeof buf, "Calibrated translation x=%.2f y=%.2f z=%.2f\n", transcm[0], transcm[1], transcm[2]);
CalCtx.Log(buf);
//char buf[256];
//snprintf(buf, sizeof buf, "Calibrated translation x=%.2f y=%.2f z=%.2f\n", transcm[0], transcm[1], transcm[2]);
//CalCtx.Log(buf);
return trans;
}
void CalibrationCalc::CalibrateScaleOffset(const Eigen::Matrix3d& rotation, Eigen::Vector3d* out_scaleOffset, float* out_scaleFactor) const {
// @TODO: figure out where the target and ref
}
namespace {
Pose ApplyTransform(const Pose& originalPose, const Eigen::AffineCompact3d& transform) {
@@ -333,7 +343,7 @@ Eigen::Vector4d CalibrationCalc::ComputeAxisVariance(
points.push_back(point);
}
mean /= points.size();
mean /= (double) points.size();
// Compute covariance matrix
Eigen::Matrix4d covMatrix = Eigen::Matrix4d::Zero();
@@ -345,7 +355,7 @@ Eigen::Vector4d CalibrationCalc::ComputeAxisVariance(
}
}
}
covMatrix /= points.size();
covMatrix /= (double) points.size();
Eigen::SelfAdjointEigenSolver<Eigen::Matrix4d> solver;
solver.compute(covMatrix);
@@ -353,29 +363,20 @@ Eigen::Vector4d CalibrationCalc::ComputeAxisVariance(
return solver.eigenvalues();
}
bool CalibrationCalc::ValidateCalibration(const Eigen::AffineCompact3d &calibration, double *error) {
bool CalibrationCalc::ValidateCalibration(const Eigen::AffineCompact3d &calibration, double *error, Eigen::Vector3d *posOffsetV) {
bool ok = true;
auto variance = ComputeAxisVariance(calibration);
std::ostringstream oss;
oss << "Axis variance: " << variance(0) << " " << variance(1) << " " << variance(2) << " " << variance(3) << "\n";
CalCtx.Log(oss.str());
oss.clear();
if (variance(1) < 0.0005) {
CalCtx.Log("Calibration points are nearly coplanar. Try moving around more?\n");
ok = false;
}
const auto posOffset = ComputeRefToTargetOffset(calibration);
char buf[256];
snprintf(buf, sizeof buf, "HMD to target offset: (%.2f, %.2f, %.2f)\n", posOffset(0), posOffset(1), posOffset(2));
CalCtx.Log(buf);
if (posOffsetV) *posOffsetV = posOffset;
// char buf[256];
//snprintf(buf, sizeof buf, "HMD to target offset: (%.2f, %.2f, %.2f)\n", posOffset(0), posOffset(1), posOffset(2));
//CalCtx.Log(buf);
double rmsError = RetargetingErrorRMS(posOffset, calibration);
snprintf(buf, sizeof buf, "Position error (RMS): %.3f\n", rmsError);
CalCtx.Log(buf);
//snprintf(buf, sizeof buf, "Position error (RMS): %.3f\n", rmsError);
//CalCtx.Log(buf);
if (rmsError > 0.1) ok = false;
if (error) *error = rmsError;
@@ -383,13 +384,123 @@ bool CalibrationCalc::ValidateCalibration(const Eigen::AffineCompact3d &calibrat
return ok;
}
// Given:
// R - the reference pose (in reference world space)
// T - the target pose (in target world space)
// C - the true calibration (target world -> reference world)
// We assume that there is some "static target pose" S s.t.:
// R * S = C * T (we'll call this the static target pose)
// To compute S:
// S = R^-1 * C * T
// To compute C:
// R * S * T^-1 = C
namespace {
class PoseAverager {
private:
Eigen::Matrix<double, 4, Eigen::Dynamic> quatAvg;
Eigen::Vector3d accum = Eigen::Vector3d::Zero();
int i = 0;
public:
PoseAverager(size_t n_samples) {
quatAvg.resize(4, n_samples);
}
template<typename P>
void Push(const P &pose) {
const Eigen::Quaterniond rot(pose.rotation());
quatAvg.col(i++) = Eigen::Vector4d(rot.w(), rot.x(), rot.y(), rot.z());
accum += pose.translation();
}
Eigen::AffineCompact3d Average() {
// https://stackoverflow.com/a/27410865/36723
auto quatT = quatAvg.transpose();
Eigen::Matrix4d quatMul = quatAvg * quatT;
Eigen::SelfAdjointEigenSolver<Eigen::Matrix4d> solver;
solver.compute(quatMul);
Eigen::Vector4d quatAvgV = solver.eigenvectors().col(3).real().normalized();
Eigen::Quaterniond avgQ(quatAvgV(0), quatAvgV(1), quatAvgV(2), quatAvgV(3));
avgQ.normalize();
Eigen::AffineCompact3d pose(avgQ);
pose.pretranslate(accum * (1.0 / i));
return pose;
}
template<typename XS, typename F>
static Eigen::AffineCompact3d AverageFor(const XS& samples, const F& poseProvider) {
int sampleCount = 0;
for (auto& sample : samples) {
if (!sample.valid) continue;
sampleCount++;
}
PoseAverager accum(sampleCount);
for (auto& sample : samples) {
if (!sample.valid) continue;
auto pose = poseProvider(sample);
accum.Push(pose);
}
return accum.Average();
}
};
}
// S = R^-1 * C * T
Eigen::AffineCompact3d CalibrationCalc::EstimateRefToTargetPose(const Eigen::AffineCompact3d &calibration) const {
auto avg = PoseAverager::AverageFor(m_samples, [&](const auto& sample) {
return Eigen::Affine3d(sample.ref.ToAffine().inverse() * calibration * sample.target.ToAffine());
});
#if 0
Eigen::Vector3d eulerAvgQ = avg.rotation().eulerAngles(2, 1, 0) * 180.0 / EIGEN_PI;
Eigen::Vector3d trans = Eigen::Vector3d(avg.translation());
std::ostringstream oss;
oss << "==========================================================================================\n";
oss << "Avg rot: " << eulerAvgQ.x() << ", " << eulerAvgQ.y() << ", " << eulerAvgQ.z() << "\n";
oss << "Avg trans:\n" << trans.x() << ", " << trans.y() << ", " << trans.z() << "\n";
OutputDebugStringA(oss.str().c_str());
#endif
return avg;
}
// S = R^-1 * C * T
// R * S * T^-1 = C
// R * (R^-1 * C * T) * T^-1 = C
/*
* This calibration routine attempts to use the estimated refToTargetPose to derive the
* playspace calibration based on the relative position of reference and target device.
* This computation can be performed even when the devices are not moving.
*/
bool CalibrationCalc::CalibrateByRelPose(Eigen::AffineCompact3d &out) const {
// R * S * T^-1 = C
out = PoseAverager::AverageFor(m_samples, [&](const auto& sample) {
return Eigen::AffineCompact3d(sample.ref.ToAffine() * m_refToTargetPose * sample.target.ToAffine().inverse());
});
return true;
}
bool CalibrationCalc::ComputeOneshot() {
auto calibration = ComputeCalibration();
bool valid = ValidateCalibration(calibration);
if (valid) {
m_estimatedTransformation = calibration;
m_estimatedTransformation = calibration; // @NOTE: Normal calibration
m_isValid = true;
return true;
}
@@ -399,39 +510,145 @@ bool CalibrationCalc::ComputeOneshot() {
}
}
bool CalibrationCalc::ComputeIncremental(bool &lerp) {
auto calibration = ComputeCalibration();
void CalibrationCalc::ComputeInstantOffset() {
const auto &latestSample = m_samples.back();
// Apply transformation
const auto updatedPose = ApplyTransform(latestSample.target, m_estimatedTransformation);
// Now move the transform from world to HMD space
const auto hmdOriginPos = updatedPose.trans - latestSample.ref.trans;
const auto hmdSpace = latestSample.ref.rot.inverse() * hmdOriginPos;
double newError, priorCalibrationError;
bool valid = ValidateCalibration(calibration, &newError);
Metrics::posOffset_lastSample.Push(hmdSpace * 1000);
}
// Use stricter thresholds for continuous calibration to limit jitter
valid = valid && newError < 0.005;
bool CalibrationCalc::ComputeIncremental(bool &lerp, double threshold) {
priorCalibrationError = INFINITY;
if (m_isValid) {
ValidateCalibration(m_estimatedTransformation, &priorCalibrationError);
bool oldCalibrationBetter = m_isValid && priorCalibrationError < newError * 4;
char tmp[256];
snprintf(tmp, sizeof tmp, "Prior calibration error: %.3f (valid: %s); new error %.3f; new better? %s\n",
priorCalibrationError, m_isValid ? "yes" : "no", newError, !oldCalibrationBetter ? "yes" : "no");
CalCtx.Log(tmp);
// If we have a more noisy calibration than before, avoid updating.
if (oldCalibrationBetter) return false;
Metrics::RecordTimestamp();
if (lockRelativePosition) {
Eigen::AffineCompact3d byRelPose;
double relPoseError = INFINITY;
Eigen::Vector3d relPosOffset;
CalibrateByRelPose(byRelPose);
ValidateCalibration(byRelPose, &relPoseError, &relPosOffset);
Metrics::posOffset_byRelPose.Push(relPosOffset * 1000);
Metrics::error_byRelPose.Push(relPoseError * 1000);
m_isValid = true;
m_estimatedTransformation = byRelPose;
return true;
}
double priorCalibrationError = INFINITY;
Eigen::Vector3d priorPosOffset;
if (m_isValid) {
ValidateCalibration(m_estimatedTransformation, &priorCalibrationError, &priorPosOffset);
Metrics::posOffset_currentCal.Push(priorPosOffset * 1000);
Metrics::error_currentCal.Push(priorCalibrationError * 1000);
if (priorCalibrationError < 0.005) {
return false;
}
}
double newError = INFINITY;
bool newCalibrationValid = false;
Eigen::AffineCompact3d byRelPose;
Eigen::AffineCompact3d calibration;
bool usingRelPose = false;
double relPoseError = INFINITY;
if (enableStaticRecalibration && CalibrateByRelPose(byRelPose)) {
Eigen::Vector3d relPosOffset;
ValidateCalibration(byRelPose, &relPoseError, &relPosOffset);
Metrics::posOffset_byRelPose.Push(relPosOffset * 1000);
Metrics::error_byRelPose.Push(relPoseError * 1000);
if (relPoseError < 0.010 || m_relativePosCalibrated && relPoseError < 0.025) {
newCalibrationValid = true;
usingRelPose = true;
newError = relPoseError;
calibration = byRelPose;
if (relPoseError * threshold >= priorCalibrationError) {
return false;
}
}
}
double newVariance = 0;
if (!newCalibrationValid) {
calibration = ComputeCalibration();
newVariance = ComputeAxisVariance(calibration)(1);
Metrics::axisIndependence.Push(newVariance);
if (newVariance < AxisVarianceThreshold && newVariance < m_axisVariance) {
newCalibrationValid = false;
} else {
newCalibrationValid = ValidateCalibration(calibration, &newError, &m_posOffset);
Metrics::posOffset_rawComputed.Push(m_posOffset * 1000);
}
if (m_isValid) {
if (priorCalibrationError < newError * threshold) {
// If we have a more noisy calibration than before, avoid updating.
newCalibrationValid = false;
}
}
Metrics::error_rawComputed.Push(newError * 1000);
ComputeInstantOffset();
}
if (valid) {
#if 0
char tmp[256];
snprintf(tmp, sizeof tmp, "Prior calibration error: %.3f (valid: %s) sct %d; new error %.3f; new better? %s\n",
priorCalibrationError, m_isValid ? "yes" : "no", stableCt, newError, !oldCalibrationBetter ? "yes" : "no");
CalCtx.Log(tmp);
#endif
// Now, can we use the relative pose to perform a rapid correction?
if (!newCalibrationValid) {
double existingPoseErrorUsingRelPosition = RetargetingErrorRMS(m_refToTargetPose.translation(), m_estimatedTransformation);
Metrics::error_currentCalRelPose.Push(existingPoseErrorUsingRelPosition * 1000);
if (relPoseError * threshold < existingPoseErrorUsingRelPosition || newCalibrationValid && relPoseError < newError) {
newCalibrationValid = true;
usingRelPose = true;
newError = relPoseError;
calibration = byRelPose;
}
}
if (newCalibrationValid) {
lerp = m_isValid;
m_relativePosCalibrated = m_relativePosCalibrated || newError < 0.005;
if (!m_isValid) {
CalCtx.Log("Applying initial transformation...");
}
else {
else if (m_relativePosCalibrated) {
CalCtx.Log("Applying updated transformation...");
} else {
CalCtx.Log("Applying temporary transformation...");
}
m_isValid = true;
m_estimatedTransformation = calibration;
m_estimatedTransformation = calibration; // @NOTE: Continuous calibration
m_axisVariance = newVariance;
if (!usingRelPose) {
m_refToTargetPose = EstimateRefToTargetPose(m_estimatedTransformation);
}
Metrics::calibrationApplied.Push(!usingRelPose);
return true;
}
+41 -3
View File
@@ -56,6 +56,11 @@ struct Sample
class CalibrationCalc {
public:
static const double AxisVarianceThreshold;
bool enableStaticRecalibration;
bool lockRelativePosition = false;
const Eigen::AffineCompact3d Transformation() const
{
return m_estimatedTransformation;
@@ -69,12 +74,28 @@ public:
bool isValid() const {
return m_isValid;
}
const Eigen::AffineCompact3d RelativeTransformation() const
{
return m_refToTargetPose;
}
bool isRelativeTransformationCalibrated() const
{
return m_relativePosCalibrated;
}
void setRelativeTransformation(const Eigen::AffineCompact3d transform, bool calibrated)
{
m_refToTargetPose = transform;
m_relativePosCalibrated = calibrated;
}
void PushSample(const Sample& sample);
void Clear();
bool ComputeOneshot();
bool ComputeIncremental(bool &lerp);
bool ComputeIncremental(bool &lerp, double threshold);
size_t SampleCount() const {
return m_samples.size();
@@ -84,15 +105,29 @@ public:
if (!m_samples.empty()) m_samples.pop_front();
}
CalibrationCalc() : m_isValid(false) {}
CalibrationCalc() : m_isValid(false), m_calcCycle(0), enableStaticRecalibration(true) {}
// Debug fields
Eigen::Vector3d m_posOffset;
double m_axisVariance = 0.0;
long m_calcCycle;
private:
bool m_isValid;
Eigen::AffineCompact3d m_estimatedTransformation;
bool m_relativePosCalibrated = false;
/*
* This affine transform estimates the pose of the target within the reference device's local pose space.
* That is to say, it's given by transforming the target world pose by the inverse reference pose.
*/
Eigen::AffineCompact3d m_refToTargetPose = Eigen::AffineCompact3d::Identity();
std::deque<Sample> m_samples;
Eigen::Vector3d CalibrateRotation() const;
Eigen::Vector3d CalibrateTranslation(const Eigen::Matrix3d &rotation) const;
void CalibrateScaleOffset(const Eigen::Matrix3d &rotation, Eigen::Vector3d* out_scaleOffset, float* out_scaleFactor) const;
Eigen::AffineCompact3d ComputeCalibration() const;
@@ -101,6 +136,9 @@ private:
Eigen::Vector4d ComputeAxisVariance(const Eigen::AffineCompact3d& calibration) const;
bool ValidateCalibration(const Eigen::AffineCompact3d& calibration, double *errorOut = nullptr);
bool ValidateCalibration(const Eigen::AffineCompact3d& calibration, double *errorOut = nullptr, Eigen::Vector3d* posOffsetV = nullptr);
void ComputeInstantOffset();
Eigen::AffineCompact3d EstimateRefToTargetPose(const Eigen::AffineCompact3d& calibration) const;
bool CalibrateByRelPose(Eigen::AffineCompact3d &out) const;
};
+379
View File
@@ -0,0 +1,379 @@
#include "stdafx.h"
#include <vector>
#include <implot/implot.h>
#include "CalibrationCalc.h"
#include "CalibrationMetrics.h"
#include "UserInterface.h"
// ImPlotPoint (*ImPlotGetter)(void* user_data, int idx);
namespace {
double refTime;
template<typename F>
ImPlotPoint VPIndexer(void* ptr, int idx) {
auto point = (*reinterpret_cast<const F*>(ptr))(idx);
point.x -= refTime;
return point;
}
template<typename F>
void PlotLineG(const char* name, const F& f, int points) {
const void* vp_f = &f;
if (points > 0) {
ImPlot::PlotLineG(name, VPIndexer<F>, const_cast<void*>(vp_f), points);
}
else {
double x = -INFINITY;
double y = 0;
ImPlot::PlotLine(name, &x, &y, 1);
}
}
template<typename F, typename G>
void PlotShadedG(const char* name, const F& data, const G& reference, int count) {
const void* vp_data = &data;
const void* vp_reference = &reference;
if (count > 0) {
ImPlot::PlotShadedG(name,
VPIndexer<F>, const_cast<void*>(vp_data),
VPIndexer<G>, const_cast<void*>(vp_reference),
count
);
}
else {
double x = -INFINITY;
double y = 0;
ImPlot::PlotShaded(name, &x, &y, &y, 1);
}
}
void PlotLineG(const char* name, const Metrics::TimeSeries<double>& ts) {
PlotLineG(name, [&](int index) {
const auto& p = ts[index];
return ImPlotPoint(p.first, p.second);
},
ts.size()
);
}
void PlotVector(const char* namePrefix, const Metrics::TimeSeries<Eigen::Vector3d>& ts) {
std::string name(namePrefix);
name += "X";
PlotLineG(name.c_str(), [&](int index) {
const auto& p = ts[index];
return ImPlotPoint(p.first, p.second(0));
}, ts.size());
name.pop_back();
name += "Y";
PlotLineG(name.c_str(), [&](int index) {
const auto& p = ts[index];
return ImPlotPoint(p.first, p.second(1));
}, ts.size());
name.pop_back();
name += "Z";
PlotLineG(name.c_str(), [&](int index) {
const auto& p = ts[index];
return ImPlotPoint(p.first, p.second(2));
}, ts.size());
}
double lastMouseX = -INFINITY;
bool wasHovered;
std::vector<double> calAppliedTimeBuffer, calByRelPoseTimeBuffer;
void PrepApplyTicks() {
calAppliedTimeBuffer.clear();
calByRelPoseTimeBuffer.clear();
for (auto t : Metrics::calibrationApplied.data()) {
if (t.second) {
calAppliedTimeBuffer.push_back(t.first - refTime);
}
else {
calByRelPoseTimeBuffer.push_back(t.first - refTime);
}
}
}
void AddApplyTicks() {
if (calAppliedTimeBuffer.empty()) {
double x = -INFINITY;
ImPlot::PlotVLines("##CalibrationAppliedTime", &x, 1);
} else {
ImPlot::PlotVLines("##CalibrationAppliedTime", &calAppliedTimeBuffer[0], (int)calAppliedTimeBuffer.size());
}
if (calByRelPoseTimeBuffer.empty()) {
double x = -INFINITY;
ImPlot::PlotVLines("##CalibrationAppliedTimeByRelPose", &x, 1);
}
else {
ImPlot::PlotVLines("##CalibrationAppliedTimeByRelPose", &calByRelPoseTimeBuffer[0], (int)calByRelPoseTimeBuffer.size());
}
ImPlot::SetNextLineStyle(ImVec4(0.5, 0.5, 1, 1));
ImPlot::PlotVLines("##TagLine", &lastMouseX, 1);
if (ImPlot::IsPlotHovered()) {
auto mousePos = ImPlot::GetPlotMousePos();
lastMouseX = mousePos.x;
wasHovered = true;
}
}
struct GraphInfo {
const char* name;
void (*callback)();
};
void SetupXAxis() {
ImPlot::SetupAxisLimits(ImAxis_X1, -Metrics::TimeSpan, 0, ImGuiCond_Always);
}
void G_PosOffset_RawComputed() {
if (ImPlot::BeginPlot("##posOffsetRawComputed")) {
ImPlot::SetupAxes(NULL, "mm", 0, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_RangeFit);
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, -200, 200, ImGuiCond_Appearing);
AddApplyTicks();
PlotVector("", Metrics::posOffset_rawComputed);
ImPlot::EndPlot();
}
}
void G_PosOffset_CurrentCal() {
if (ImPlot::BeginPlot("##posOffsetCurrentCal")) {
ImPlot::SetupAxes(NULL, "mm", 0, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_RangeFit);
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, -200, 200, ImGuiCond_Appearing);
AddApplyTicks();
PlotVector("", Metrics::posOffset_currentCal);
ImPlot::EndPlot();
}
}
void G_PosOffset_LastSample() {
if (ImPlot::BeginPlot("##posOffsetLastSample")) {
ImPlot::SetupAxes(NULL, "mm", 0, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_RangeFit);
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, -200, 200, ImGuiCond_Appearing);
AddApplyTicks();
PlotVector("", Metrics::posOffset_lastSample);
ImPlot::EndPlot();
}
}
void G_PosOffset_ByRelPose() {
if (ImPlot::BeginPlot("##posOffsetByRelPose")) {
ImPlot::SetupAxes(NULL, "mm", 0, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_RangeFit);
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, -200, 200, ImGuiCond_Appearing);
AddApplyTicks();
PlotVector("", Metrics::posOffset_byRelPose);
ImPlot::EndPlot();
}
}
void G_PosOffset_PosError() {
if (ImPlot::BeginPlot("##Position error")) {
ImPlot::SetupAxes(NULL, "mm (RMS)");
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 25, ImGuiCond_Appearing);
AddApplyTicks();
PlotLineG("Candidate", Metrics::error_rawComputed);
PlotLineG("Active", Metrics::error_currentCal);
PlotLineG("By Rel Pose", Metrics::error_byRelPose);
PlotLineG("CC Rel Pose", Metrics::error_currentCalRelPose);
ImPlot::EndPlot();
}
}
void G_ComputationTime() {
if (ImPlot::BeginPlot("##Computation Time", ImVec2(-1, 0), ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(NULL, "ms", 0, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_RangeFit);
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 200, ImGuiCond_Appearing);
AddApplyTicks();
PlotLineG("Time", Metrics::computationTime);
ImPlot::EndPlot();
}
}
void G_AxisVariance() {
static bool firstrun = true;
static ImPlotColormap axisVarianceColormap;
if (firstrun) {
firstrun = false;
auto defaultFirst = ImPlot::GetColormapColor(0);
ImVec4 colors[] = {
ImPlot::GetColormapColor(0),
ImPlot::GetColormapColor(1),
{ 1, 0, 0, 1 },
{ 0, 1, 0, 1 },
{ 0.5, 0.5, 0.5, 1 },
};
axisVarianceColormap = ImPlot::AddColormap("AxisVarianceColormap", colors, sizeof(colors) / sizeof(colors[0]));
}
if (ImPlot::BeginPlot("##Axis variance", ImVec2(-1, 0), ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(NULL, NULL, 0, 0);
SetupXAxis();
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 0.003, ImGuiCond_Always);
AddApplyTicks();
ImPlot::PushColormap(axisVarianceColormap);
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.5f);
ImPlot::SetNextLineStyle(ImVec4(1, 0, 0, 1));
PlotShadedG("##VarianceLow",
[&](int index) {
auto p = Metrics::axisIndependence[index];
p.second = min(p.second, CalibrationCalc::AxisVarianceThreshold);
return ImPlotPoint(p.first, p.second);
},
[&](int index) {
auto p = Metrics::axisIndependence[index];
return ImPlotPoint(p.first, 0);
},
Metrics::axisIndependence.size()
);
ImPlot::SetNextLineStyle(ImVec4(0, 1, 0, 1));
PlotShadedG("##VarianceHigh",
[&](int index) {
auto p = Metrics::axisIndependence[index];
p.second = max(p.second, CalibrationCalc::AxisVarianceThreshold);
return ImPlotPoint(p.first, p.second);
},
[&](int index) {
auto p = Metrics::axisIndependence[index];
return ImPlotPoint(p.first, CalibrationCalc::AxisVarianceThreshold);
},
Metrics::axisIndependence.size()
);
PlotLineG("Datapoint", Metrics::axisIndependence);
ImPlot::PopStyleVar(1);
ImPlot::PopColormap(1);
ImPlot::EndPlot();
}
}
const struct GraphInfo graphs[] = {
{ "Position Error", G_PosOffset_PosError },
{ "Axis Variance", G_AxisVariance },
{ "Offset: Raw Computed", G_PosOffset_RawComputed },
{ "Offset: Current Calibration", G_PosOffset_CurrentCal },
{ "Offset: Last Sample", G_PosOffset_LastSample },
{ "Offset: By Rel Pose", G_PosOffset_ByRelPose },
{ "Processing time", G_ComputationTime }
};
const int N_GRAPHS = sizeof(graphs) / sizeof(graphs[0]);
}
void PushCalibrationApplyTime() {
Metrics::calibrationApplied.Push(true);
}
void ShowCalibrationDebug(int rows, int cols) {
static std::vector<int> curIndexes;
//ImGui::ShowDemoWindow();
//ImPlot::ShowDemoWindow();
double initMouseX = lastMouseX;
wasHovered = false;
for (int i = (int)curIndexes.size(); i < rows * cols; i++) {
curIndexes.push_back(i % N_GRAPHS);
}
auto avail = ImGui::GetContentRegionAvail();
auto bgCol = ImGui::GetStyleColorVec4(ImGuiCol_FrameBg);
ImGui::PushStyleColor(ImGuiCol_TableRowBg, bgCol);
ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, bgCol);
ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0,0,0,0));
ImGui::SetNextWindowBgAlpha(1);
if (!ImGui::BeginChild("##CalibrationDebug", avail, false,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoTitleBar)) {
ImGui::EndChild();
return;
}
if (!ImGui::BeginTable("##CalibrationDebug", cols, ImGuiTableFlags_RowBg)) {
return;
}
double t = refTime = Metrics::timestamp();
PrepApplyTicks();
for (int r = 0; r < rows; r++) {
ImGui::TableNextRow();
for (int c = 0; c < cols; c++) {
int i = r * cols + c;
ImGui::TableSetColumnIndex(c);
ImGui::PushID(i);
ImGui::SetNextItemWidth(ImGui::GetColumnWidth());
if (ImGui::BeginCombo("", graphs[curIndexes[i]].name, 0)) {
for (int j = 0; j < N_GRAPHS; j++) {
bool isSelected = j == curIndexes[i];
if (ImGui::Selectable(graphs[j].name, isSelected)) {
curIndexes[i] = j;
}
if (isSelected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
graphs[curIndexes[i]].callback();
ImGui::PopID();
}
}
ImGui::EndTable();
ImGui::EndChild();
ImPlot::PopStyleColor(1);
ImGui::PopStyleColor(2);
if (!wasHovered) {
lastMouseX = -INFINITY;
}
}
@@ -0,0 +1,215 @@
#include "stdafx.h"
#include "CalibrationMetrics.h"
#include <shlobj_core.h>
#include <fstream>
#include <vector>
namespace Metrics {
double TimeSpan = 30, CurrentTime;
TimeSeries<Eigen::Vector3d> posOffset_rawComputed; // , rotOffset_rawComputed;
TimeSeries<Eigen::Vector3d> posOffset_currentCal; // , rotOffset_currentCal;
TimeSeries<Eigen::Vector3d> posOffset_lastSample; // , rotOffset_lastSample;
TimeSeries<Eigen::Vector3d> posOffset_byRelPose;
TimeSeries<double> error_rawComputed, error_currentCal, error_byRelPose, error_currentCalRelPose;
TimeSeries<double> axisIndependence;
TimeSeries<double> computationTime;
// true - full calibration, false - static calibration
TimeSeries<bool> calibrationApplied;
double timestamp() {
static long long ts_start = ~0LL;
LARGE_INTEGER ts, freq;
QueryPerformanceCounter(&ts);
QueryPerformanceFrequency(&freq);
if (ts_start == ~0LL) ts_start = ts.QuadPart;
ts.QuadPart -= ts_start;
return ts.QuadPart / (double)freq.QuadPart;
}
void RecordTimestamp() {
CurrentTime = timestamp();
}
bool enableLogs = false;
static std::ofstream logFile;
static bool logFileIsOpen = false;
static bool failedToOpenLogFile = false;
struct CsvField {
const char* name;
void (*writer)(std::ofstream& s);
};
#define TS_FIELD(n) \
{ #n, [](auto &s) { s << n.last(); } }
#define TS_VECTOR_FIELD(n) \
{ #n ".x", [](auto &s) { s << n.last()(0); } }, \
{ #n ".y", [](auto &s) { s << n.last()(1); } }, \
{ #n ".z", [](auto &s) { s << n.last()(2); } }
static const CsvField fields[] = {
{
"Timestamp",
[](auto& s) { s << CurrentTime; }
},
TS_VECTOR_FIELD(posOffset_rawComputed),
TS_VECTOR_FIELD(posOffset_currentCal),
TS_VECTOR_FIELD(posOffset_lastSample),
TS_VECTOR_FIELD(posOffset_byRelPose),
TS_FIELD(error_rawComputed),
TS_FIELD(error_currentCal),
TS_FIELD(error_byRelPose),
TS_FIELD(error_currentCalRelPose),
TS_FIELD(axisIndependence),
TS_FIELD(computationTime),
{
"calibrationApplied",
[](auto& s) {
if (calibrationApplied.lastTs() == CurrentTime) {
if (calibrationApplied.last()) {
s << "FULL";
}
else {
s << "STATIC";
}
}
}
}
};
static void ClearOldLogs(const std::wstring& path) {
std::wstring search_path = path + L"\\spacecal_log.*.txt";
WIN32_FIND_DATA find_data;
SYSTEMTIME st_now;
FILETIME ft_now;
GetSystemTime(&st_now);
SystemTimeToFileTime(&st_now, &ft_now);
ULARGE_INTEGER ft_tmp;
ft_tmp.HighPart = ft_now.dwHighDateTime;
ft_tmp.LowPart = ft_now.dwLowDateTime;
// one day ago
uint64_t limit = ft_tmp.QuadPart - (uint64_t)(24LL * 3600LL * 10LL * 1000LL * 1000LL);
HANDLE find_handle = FindFirstFile(search_path.c_str(), &find_data);
if (find_handle != INVALID_HANDLE_VALUE) {
do {
ft_tmp.HighPart = find_data.ftLastWriteTime.dwHighDateTime;
ft_tmp.LowPart = find_data.ftLastWriteTime.dwLowDateTime;
if (ft_tmp.QuadPart < limit) {
std::wstring file_path = path + L"\\" + find_data.cFileName;
DeleteFile(file_path.c_str());
}
} while (FindNextFile(find_handle, &find_data));
FindClose(find_handle);
}
}
static bool OpenLogFile() {
PWSTR RootPath = NULL;
if (S_OK != SHGetKnownFolderPath(FOLDERID_LocalAppDataLow, 0, NULL, &RootPath)) {
CoTaskMemFree(RootPath);
return false;
}
std::wstring path(RootPath);
CoTaskMemFree(RootPath);
path += LR"(\OpenVR-SpaceCalibrator)";
if (CreateDirectoryW(path.c_str(), 0) == 0 && GetLastError() != ERROR_ALREADY_EXISTS) {
return false;
}
path += LR"(\Logs)";
if (CreateDirectoryW(path.c_str(), 0) == 0 && GetLastError() != ERROR_ALREADY_EXISTS) {
return false;
}
ClearOldLogs(path);
SYSTEMTIME now;
GetSystemTime(&now);
size_t dateBufLen = GetDateFormatW(LOCALE_USER_DEFAULT, 0, &now, L"yyyy-MM-dd", NULL, 0);
std::vector<WCHAR> dateBuf(dateBufLen);
if (!GetDateFormatEx(LOCALE_NAME_INVARIANT, 0, &now, L"yyyy-MM-dd", &dateBuf[0], dateBufLen, NULL)) return false;
size_t timeBufLen = GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &now, L"HH-mm-ss", NULL, 0);
std::vector<WCHAR> timeBuf(timeBufLen);
if (!GetTimeFormatEx(LOCALE_NAME_INVARIANT, 0, &now, L"HH-mm-ss", &timeBuf[0], timeBufLen)) return false;
path += LR"(\spacecal_log.)";
path += &dateBuf[0];
path += L"T";
path += &timeBuf[0];
path += L".txt";
logFile.open(path);
if (logFile.fail()) {
return false;
}
for (int i = 0; i < sizeof fields / sizeof fields[0]; i++) {
if (i > 0) logFile << ",";
logFile << fields[i].name;
}
logFile << "\n";
logFileIsOpen = true;
return true;
}
static bool CheckLogOpen() {
if (!enableLogs) {
if (logFileIsOpen) {
logFile.close();
}
logFileIsOpen = false;
failedToOpenLogFile = false;
return false;
}
if (failedToOpenLogFile) return false;
if (!logFileIsOpen && !OpenLogFile()) {
failedToOpenLogFile = true;
return false;
}
return true;
}
void WriteLogAnnotation(const char *s) {
if (!CheckLogOpen()) return;
logFile << "# [" << timestamp() << "] " << s << "\n";
logFile.flush();
}
void WriteLogEntry() {
if (!CheckLogOpen()) return;
if (logFileIsOpen) {
for (int i = 0; i < sizeof fields / sizeof fields[0]; i++) {
if (i > 0) logFile << ",";
fields[i].writer(logFile);
}
logFile << "\n";
}
logFile.flush();
}
}
@@ -0,0 +1,58 @@
#pragma once
#include <deque>
#include <utility>
#include <Eigen/Dense>
namespace Metrics {
extern double TimeSpan, CurrentTime;
double timestamp();
void RecordTimestamp();
template<typename T>
class TimeSeries {
std::deque<std::pair<double, T>> Data;
public:
const std::deque<std::pair<double, T>> &data() const { return Data; }
void Push(const T& data) {
Data.push_back(std::make_pair(CurrentTime, data));
double cutoff = CurrentTime - TimeSpan;
while (!Data.empty() && (Data.front().first < cutoff || Data.size() > INT_MAX)) {
Data.pop_front();
}
}
int size() const { return (int)Data.size(); }
const std::pair<double, T>& operator[](int index) const { return Data[index]; }
const T& last() const {
static const T fallback;
return Data.size() > 0 ? Data.back().second : fallback;
}
const double lastTs() const {
return Data.size() > 0 ? Data.back().first : 0;
}
};
extern TimeSeries<Eigen::Vector3d> posOffset_rawComputed; // , rotOffset_rawComputed;
extern TimeSeries<Eigen::Vector3d> posOffset_currentCal; // , rotOffset_currentCal;
extern TimeSeries<Eigen::Vector3d> posOffset_lastSample; // , rotOffset_lastSample;
extern TimeSeries<Eigen::Vector3d> posOffset_byRelPose;
extern TimeSeries<double> error_rawComputed, error_currentCal, error_byRelPose, error_currentCalRelPose;
extern TimeSeries<double> axisIndependence;
extern TimeSeries<double> computationTime;
extern TimeSeries<bool> calibrationApplied;
extern bool enableLogs;
void WriteLogAnnotation(const char* s);
void WriteLogEntry();
}
+99
View File
@@ -46,6 +46,48 @@ static void LoadStandby(StandbyDevice& device, picojson::value& value) {
if (serial.is<std::string>()) device.serial = serial.get<std::string>();
}
static void VisitAlignmentParams(CalibrationContext& ctx, std::function<void(const char *, double&)> MapParam) {
#define P(s) MapParam(#s, ctx.alignmentSpeedParams.s)
P(align_speed_tiny);
P(align_speed_small);
P(align_speed_large);
P(thr_trans_tiny);
P(thr_trans_small);
P(thr_trans_large);
P(thr_rot_tiny);
P(thr_rot_small);
P(thr_rot_large);
// Convert to double and back
double tmp = ctx.continuousCalibrationThreshold;
MapParam("continuousCalibrationThreshold", tmp);
ctx.continuousCalibrationThreshold = (float)tmp;
}
static void LoadAlignmentParams(CalibrationContext& ctx, picojson::value& value) {
ctx.ResetConfig();
if (!value.is<picojson::object>()) return;
auto& obj = value.get<picojson::object>();
VisitAlignmentParams(ctx, [&](auto name, auto& param) {
const picojson::value& node = obj[name];
if (node.is<double>()) {
param = (float)node.get<double>();
}
});
}
static picojson::object SaveAlignmentParams(CalibrationContext& ctx) {
picojson::object obj;
VisitAlignmentParams(ctx, [&](auto name, auto& param) {
obj[name].set<double>(param);
});
return obj;
}
static void ParseProfile(CalibrationContext &ctx, std::istream &stream)
{
picojson::value v;
@@ -59,6 +101,7 @@ static void ParseProfile(CalibrationContext &ctx, std::istream &stream)
auto obj = arr[0].get<picojson::object>();
LoadAlignmentParams(ctx, obj["alignment_params"]);
ctx.referenceTrackingSystem = obj["reference_tracking_system"].get<std::string>();
ctx.targetTrackingSystem = obj["target_tracking_system"].get<std::string>();
ctx.calibratedRotation(0) = obj["roll"].get<double>();
@@ -73,6 +116,10 @@ static void ParseProfile(CalibrationContext &ctx, std::istream &stream)
ctx.state = CalibrationState::ContinuousStandby;
}
ctx.quashTargetInContinuous = obj["quash_target_in_continuous"].evaluate_as_boolean();
ctx.requireTriggerPressToApply = obj["require_trigger_press_to_apply"].evaluate_as_boolean();
ctx.continuousCalibrationOffset(0) = obj["continuous_calibration_target_offset_x"].get<double>();
ctx.continuousCalibrationOffset(1) = obj["continuous_calibration_target_offset_y"].get<double>();
ctx.continuousCalibrationOffset(2) = obj["continuous_calibration_target_offset_z"].get<double>();
if (obj["scale"].is<double>())
ctx.calibratedScale = obj["scale"].get<double>();
@@ -108,6 +155,34 @@ static void ParseProfile(CalibrationContext &ctx, std::istream &stream)
ctx.chaperone.valid = true;
}
}
if (obj["relative_pos_calibrated"].is<bool>()) {
ctx.relativePosCalibrated = obj["relative_pos_calibrated"].get<bool>();
}
if (obj["lock_relative_position"].is<bool>()) {
ctx.lockRelativePosition = obj["lock_relative_position"].get<bool>();
}
if (obj["relative_transform"].is<picojson::object>()) {
auto relTransform = obj["relative_transform"].get<picojson::object>();
Eigen::Vector3d refToTragetRoation;
Eigen::Vector3d refToTargetTranslation;
refToTragetRoation(0) = relTransform["roll"].get<double>();
refToTragetRoation(1) = relTransform["yaw"].get<double>();
refToTragetRoation(2) = relTransform["pitch"].get<double>();
refToTargetTranslation(0) = relTransform["x"].get<double>();
refToTargetTranslation(1) = relTransform["y"].get<double>();
refToTargetTranslation(2) = relTransform["z"].get<double>();
Eigen::Matrix3d rotationMatrix;
rotationMatrix =
Eigen::AngleAxisd(refToTragetRoation[0], Eigen::Vector3d::UnitX()) *
Eigen::AngleAxisd(refToTragetRoation[1], Eigen::Vector3d::UnitY()) *
Eigen::AngleAxisd(refToTragetRoation[2], Eigen::Vector3d::UnitZ());
ctx.refToTargetPose = Eigen::AffineCompact3d::Identity();
ctx.refToTargetPose.linear() = rotationMatrix;
ctx.refToTargetPose.translation() = refToTargetTranslation;
}
ctx.validProfile = true;
}
@@ -130,6 +205,8 @@ static void WriteProfile(CalibrationContext &ctx, std::ostream &out)
return;
picojson::object profile;
profile["alignment_params"].set<picojson::object>(SaveAlignmentParams(ctx));
profile["reference_tracking_system"].set<std::string>(ctx.referenceTrackingSystem);
profile["target_tracking_system"].set<std::string>(ctx.targetTrackingSystem);
profile["roll"].set<double>(ctx.calibratedRotation(0));
@@ -144,6 +221,10 @@ static void WriteProfile(CalibrationContext &ctx, std::ostream &out)
bool isInContinuousCalibrationMode = ctx.state == CalibrationState::Continuous || ctx.state == CalibrationState::ContinuousStandby;
profile["autostart_continuous_calibration"].set<bool>(isInContinuousCalibrationMode);
profile["quash_target_in_continuous"].set<bool>(ctx.quashTargetInContinuous);
profile["require_trigger_press_to_apply"].set<bool>(ctx.requireTriggerPressToApply);
profile["continuous_calibration_target_offset_x"].set<double>(ctx.continuousCalibrationOffset(0));
profile["continuous_calibration_target_offset_y"].set<double>(ctx.continuousCalibrationOffset(1));
profile["continuous_calibration_target_offset_z"].set<double>(ctx.continuousCalibrationOffset(2));
double speed = (int) ctx.calibrationSpeed;
profile["calibration_speed"].set<double>(speed);
@@ -167,6 +248,19 @@ static void WriteProfile(CalibrationContext &ctx, std::ostream &out)
profile["chaperone"].set<picojson::object>(chaperone);
}
Eigen::Vector3d refToTragetRoation = ctx.refToTargetPose.rotation().eulerAngles(0, 1, 2);
Eigen::Vector3d refToTargetTranslation = ctx.refToTargetPose.translation();
picojson::object refToTarget;
refToTarget["x"].set<double>(refToTargetTranslation(0));
refToTarget["y"].set<double>(refToTargetTranslation(1));
refToTarget["z"].set<double>(refToTargetTranslation(2));
refToTarget["roll"].set<double>(refToTragetRoation(0));
refToTarget["yaw"].set<double>(refToTragetRoation(1));
refToTarget["pitch"].set<double>(refToTragetRoation(2));
profile["relative_pos_calibrated"].set<bool>(ctx.relativePosCalibrated);
profile["lock_relative_position"].set<bool>(ctx.lockRelativePosition);
profile["relative_transform"].set<picojson::object>(refToTarget);
picojson::value profileV;
profileV.set<picojson::object>(profile);
@@ -233,6 +327,11 @@ static void WriteRegistryKey(std::string str)
void LoadProfile(CalibrationContext &ctx)
{
// @TODO: Rewrite this to migrate configs from the registry to the spacecal directory
// I don't know why whoever wrote this thought writing to the registry in the 2020s was a good idea...
// NOTE: HKEY_CURRENT_USER_LOCAL_SETTINGS evaluates to HKCU\Software\Classes\Local Settings
// Settings are currently stored at HKCU\Software\Classes\Local Settings\Software\OpenVR-SpaceCalibrator
ctx.validProfile = false;
auto str = ReadRegistryKey();
+19 -10
View File
@@ -3,17 +3,24 @@
#include <string>
std::string WStringToString(const std::wstring& wstr)
{
int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL);
std::string str_to(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &str_to[0], size_needed, NULL, NULL);
return str_to;
}
static std::string LastErrorString(DWORD lastError)
{
LPSTR buffer = nullptr;
size_t size = FormatMessageA(
LPWSTR buffer = nullptr;
size_t size = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&buffer, 0, NULL
NULL, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&buffer, 0, NULL
);
std::string message(buffer, size);
std::wstring message(buffer, size);
LocalFree(buffer);
return message;
return WStringToString(message);
}
IPCClient::~IPCClient()
@@ -37,7 +44,8 @@ void IPCClient::Connect()
DWORD mode = PIPE_READMODE_MESSAGE;
if (!SetNamedPipeHandleState(pipe, &mode, 0, 0))
{
throw std::runtime_error("Couldn't set pipe mode. Error: " + LastErrorString(GetLastError()));
DWORD lastError = GetLastError();
throw std::runtime_error("Couldn't set pipe mode. Error " + std::to_string(lastError) + ": " + LastErrorString(lastError));
}
auto response = SendBlocking(protocol::Request(protocol::RequestHandshake));
@@ -65,7 +73,8 @@ void IPCClient::Send(const protocol::Request &request)
BOOL success = WriteFile(pipe, &request, sizeof request, &bytesWritten, 0);
if (!success)
{
throw std::runtime_error("Error writing IPC request. Error: " + LastErrorString(GetLastError()));
DWORD lastError = GetLastError();
throw std::runtime_error("Error writing IPC request. Error " + std::to_string(lastError) + ": " + LastErrorString(lastError));
}
}
@@ -80,13 +89,13 @@ protocol::Response IPCClient::Receive()
DWORD lastError = GetLastError();
if (lastError != ERROR_MORE_DATA)
{
throw std::runtime_error("Error reading IPC response. Error: " + LastErrorString(lastError));
throw std::runtime_error("Error reading IPC response. Error " + std::to_string(lastError) + ": " + LastErrorString(lastError));
}
}
if (bytesRead != sizeof response)
{
throw std::runtime_error("Invalid IPC response with size " + std::to_string(bytesRead));
throw std::runtime_error("Invalid IPC response. Error SIZE_MISMATCH, got size " + std::to_string(bytesRead));
}
return response;
@@ -8,10 +8,14 @@
#include <imgui/imgui_internal.h>
#include <imgui/imgui_impl_glfw.h>
#include <imgui/imgui_impl_opengl3.h>
#include <implot/implot.h>
#include <GL/gl3w.h>
#include <GLFW/glfw3.h>
#include <openvr.h>
#include <direct.h>
#include <chrono>
#include <thread>
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
@@ -56,6 +60,7 @@ static GLuint fboHandle = 0, fboTextureHandle = 0;
static int fboTextureWidth = 0, fboTextureHeight = 0;
static char cwd[MAX_PATH];
const float MINIMIZED_MAX_FPS = 60.0f;
void CreateGLFWWindow()
{
@@ -87,6 +92,7 @@ void CreateGLFWWindow()
#endif
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO &io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
@@ -205,6 +211,12 @@ void InitVR()
static char textBuf[0x400];
static bool immediateRedraw;
void RequestImmediateRedraw() {
immediateRedraw = true;
}
double lastFrameStartTime = glfwGetTime();
void RunLoop()
{
while (!glfwWindowShouldClose(glfwWindow))
@@ -217,6 +229,7 @@ void RunLoop()
bool dashboardVisible = false;
int width, height;
glfwGetFramebufferSize(glfwWindow, &width, &height);
const bool windowVisible = (width > 0 && height > 0);
if (overlayMainHandle && vr::VROverlay())
{
@@ -247,17 +260,19 @@ void RunLoop()
int id = ImGui::GetActiveID();
auto textInfo = ImGui::GetInputTextState(id);
textBuf[0] = 0;
int len = WideCharToMultiByte(CP_ACP, 0, (LPCWCH)textInfo->TextW.Data, textInfo->TextW.Size, textBuf, sizeof(textBuf), NULL, NULL);
textBuf[min(len, sizeof(textBuf) - 1)] = 0;
uint32_t unFlags = 0; // EKeyboardFlags
if (textInfo != nullptr) {
textBuf[0] = 0;
int len = WideCharToMultiByte(CP_ACP, 0, (LPCWCH)textInfo->TextW.Data, textInfo->TextW.Size, textBuf, sizeof(textBuf), NULL, NULL);
textBuf[min(len, sizeof(textBuf) - 1)] = 0;
vr::VROverlay()->ShowKeyboardForOverlay(
overlayMainHandle, vr::k_EGamepadTextInputModeNormal, vr::k_EGamepadTextInputLineModeSingleLine,
unFlags, "Space Calibrator Overlay", sizeof textBuf, textBuf, 0
);
keyboardOpen = true;
uint32_t unFlags = 0; // EKeyboardFlags
vr::VROverlay()->ShowKeyboardForOverlay(
overlayMainHandle, vr::k_EGamepadTextInputModeNormal, vr::k_EGamepadTextInputLineModeSingleLine,
unFlags, "Space Calibrator Overlay", sizeof textBuf, textBuf, 0
);
keyboardOpen = true;
}
}
vr::VREvent_t vrEvent;
@@ -275,8 +290,8 @@ void RunLoop()
break;
case vr::VREvent_ScrollDiscrete:
{
double x = vrEvent.data.scroll.xdelta * 360.0f * 8.0f;
double y = vrEvent.data.scroll.ydelta * 360.0f * 8.0f;
float x = vrEvent.data.scroll.xdelta * 360.0f * 8.0f;
float y = vrEvent.data.scroll.ydelta * 360.0f * 8.0f;
io.AddMouseWheelEvent(x, y);
break;
}
@@ -299,64 +314,85 @@ void RunLoop()
}
}
}
auto &io = ImGui::GetIO();
io.DisplaySize = ImVec2((float) fboTextureWidth, (float) fboTextureHeight);
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
io.ConfigFlags = io.ConfigFlags & ~ImGuiConfigFlags_NoMouseCursorChange;
if (dashboardVisible) {
io.ConfigFlags = io.ConfigFlags | ImGuiConfigFlags_NoMouseCursorChange;
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
BuildMainWindow(dashboardVisible);
ImGui::Render();
glBindFramebuffer(GL_FRAMEBUFFER, fboHandle);
glViewport(0, 0, fboTextureWidth, fboTextureHeight);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (width && height)
if (windowVisible || dashboardVisible)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, fboHandle);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glfwSwapBuffers(glfwWindow);
}
auto &io = ImGui::GetIO();
io.DisplaySize = ImVec2((float) fboTextureWidth, (float) fboTextureHeight);
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
if (dashboardVisible)
{
vr::Texture_t vrTex;
vrTex.eType = vr::TextureType_OpenGL;
vrTex.eColorSpace = vr::ColorSpace_Auto;
io.ConfigFlags = io.ConfigFlags & ~ImGuiConfigFlags_NoMouseCursorChange;
if (dashboardVisible) {
io.ConfigFlags = io.ConfigFlags | ImGuiConfigFlags_NoMouseCursorChange;
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
vrTex.handle = (void *)
BuildMainWindow(dashboardVisible);
ImGui::Render();
glBindFramebuffer(GL_FRAMEBUFFER, fboHandle);
glViewport(0, 0, fboTextureWidth, fboTextureHeight);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (width && height)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, fboHandle);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glfwSwapBuffers(glfwWindow);
}
if (dashboardVisible)
{
vr::Texture_t vrTex;
vrTex.eType = vr::TextureType_OpenGL;
vrTex.eColorSpace = vr::ColorSpace_Auto;
vrTex.handle = (void *)
#if defined _WIN64 || defined _LP64
(uint64_t)
(uint64_t)
#endif
fboTextureHandle;
fboTextureHandle;
vr::HmdVector2_t mouseScale = { (float) fboTextureWidth, (float) fboTextureHeight };
vr::HmdVector2_t mouseScale = { (float) fboTextureWidth, (float) fboTextureHeight };
vr::VROverlay()->SetOverlayTexture(overlayMainHandle, &vrTex);
vr::VROverlay()->SetOverlayMouseScale(overlayMainHandle, &mouseScale);
vr::VROverlay()->SetOverlayTexture(overlayMainHandle, &vrTex);
vr::VROverlay()->SetOverlayMouseScale(overlayMainHandle, &mouseScale);
}
}
const double dashboardInterval = 1.0 / 90.0; // fps
double waitEventsTimeout = CalCtx.wantedUpdateInterval;
double waitEventsTimeout = max(CalCtx.wantedUpdateInterval, dashboardInterval);
if (dashboardVisible && waitEventsTimeout > dashboardInterval)
waitEventsTimeout = dashboardInterval;
if (immediateRedraw) {
waitEventsTimeout = 0;
immediateRedraw = false;
}
glfwWaitEventsTimeout(waitEventsTimeout);
// If we're minimized rendering won't limit our frame rate so we need to do it ourselves.
if (glfwGetWindowAttrib(glfwWindow, GLFW_ICONIFIED))
{
double targetFrameTime = 1 / MINIMIZED_MAX_FPS;
double waitTime = targetFrameTime - (glfwGetTime() - lastFrameStartTime);
if (waitTime > 0)
{
std::this_thread::sleep_for(std::chrono::duration<double>(waitTime));
}
lastFrameStartTime += targetFrameTime;
}
}
}
@@ -394,6 +430,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
}
catch (std::runtime_error &e)
@@ -55,7 +55,7 @@
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_SCL_SECURE_NO_WARNINGS;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>..\lib\gl3w\include;..\lib\glfw\include;..\lib\openvr;..\lib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\lib\imgui;..\lib\gl3w\include;..\lib\glfw\include;..\lib\openvr;..\lib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/bigobj %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
@@ -74,7 +74,7 @@
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>_SCL_SECURE_NO_WARNINGS;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>..\lib\gl3w\include;..\lib\glfw\include;..\lib\openvr;..\lib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>..\lib\imgui;..\lib\gl3w\include;..\lib\glfw\include;..\lib\openvr;..\lib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@@ -86,15 +86,20 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\lib\implot\implot.h" />
<ClInclude Include="..\lib\implot\implot_internal.h" />
<ClInclude Include="..\Version.h" />
<ClInclude Include="Calibration.h" />
<ClInclude Include="CalibrationCalc.h" />
<ClInclude Include="CalibrationMetrics.h" />
<ClInclude Include="Configuration.h" />
<ClInclude Include="EmbeddedFiles.h" />
<ClInclude Include="imgui_extensions.h" />
<ClInclude Include="IPCClient.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="UserInterface.h" />
<ClInclude Include="VRState.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\lib\gl3w\src\gl3w.c">
@@ -103,6 +108,7 @@
<ClCompile Include="..\lib\imgui\imgui.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="..\lib\imgui\imgui_demo.cpp" />
<ClCompile Include="..\lib\imgui\imgui_draw.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
@@ -114,12 +120,18 @@
</ClCompile>
<ClCompile Include="..\lib\imgui\imgui_tables.cpp" />
<ClCompile Include="..\lib\imgui\imgui_widgets.cpp" />
<ClCompile Include="..\lib\implot\implot.cpp" />
<ClCompile Include="..\lib\implot\implot_demo.cpp" />
<ClCompile Include="..\lib\implot\implot_items.cpp" />
<ClCompile Include="Calibration.cpp" />
<ClCompile Include="CalibrationCalc.cpp" />
<ClCompile Include="CalibrationDebug.cpp" />
<ClCompile Include="CalibrationMetrics.cpp" />
<ClCompile Include="Configuration.cpp" />
<ClCompile Include="EmbeddedFiles.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="imgui_extensions.cpp" />
<ClCompile Include="IPCClient.cpp" />
<ClCompile Include="OpenVR-SpaceCalibrator.cpp" />
<ClCompile Include="stdafx.cpp">
@@ -127,6 +139,7 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="UserInterface.cpp" />
<ClCompile Include="VRState.cpp" />
</ItemGroup>
<ItemGroup>
<Image Include="OpenVR-SpaceCalibrator.ico" />
@@ -19,6 +19,9 @@
<Filter Include="Source Files\GL">
<UniqueIdentifier>{d156ae5c-9fe6-41f0-a34e-333dc7a6b2f8}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\ImGui\ImPlot">
<UniqueIdentifier>{7334a0fc-18cd-446d-b621-6dadb942fe6f}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="stdafx.h">
@@ -48,6 +51,21 @@
<ClInclude Include="CalibrationCalc.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\implot\implot.h">
<Filter>Source Files\ImGui\ImPlot</Filter>
</ClInclude>
<ClInclude Include="..\lib\implot\implot_internal.h">
<Filter>Source Files\ImGui\ImPlot</Filter>
</ClInclude>
<ClInclude Include="VRState.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CalibrationMetrics.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="imgui_extensions.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="stdafx.cpp">
@@ -95,6 +113,30 @@
<ClCompile Include="..\lib\imgui\imgui_widgets.cpp">
<Filter>Source Files\ImGui</Filter>
</ClCompile>
<ClCompile Include="..\lib\implot\implot.cpp">
<Filter>Source Files\ImGui\ImPlot</Filter>
</ClCompile>
<ClCompile Include="..\lib\implot\implot_demo.cpp">
<Filter>Source Files\ImGui\ImPlot</Filter>
</ClCompile>
<ClCompile Include="..\lib\implot\implot_items.cpp">
<Filter>Source Files\ImGui\ImPlot</Filter>
</ClCompile>
<ClCompile Include="..\lib\imgui\imgui_demo.cpp">
<Filter>Source Files\ImGui</Filter>
</ClCompile>
<ClCompile Include="CalibrationDebug.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="VRState.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CalibrationMetrics.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="imgui_extensions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Image Include="small.ico">
+409 -114
View File
@@ -2,6 +2,8 @@
#include "UserInterface.h"
#include "Calibration.h"
#include "Configuration.h"
#include "VRState.h"
#include "CalibrationMetrics.h"
#include "../Version.h"
#include <thread>
@@ -9,22 +11,7 @@
#include <vector>
#include <algorithm>
#include <imgui/imgui.h>
struct VRDevice
{
int id = -1;
vr::TrackedDeviceClass deviceClass;
std::string model = "";
std::string serial = "";
std::string trackingSystem = "";
vr::ETrackedControllerRole controllerRole = vr::TrackedControllerRole_Invalid;
};
struct VRState
{
std::vector<std::string> trackingSystems;
std::vector<VRDevice> devices;
};
#include "imgui_extensions.h"
void TextWithWidth(const char *label, const char *text, float width);
@@ -37,16 +24,27 @@ void BuildMenu(bool runningInOverlay);
static const ImGuiWindowFlags bareWindowFlags =
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove;
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse |
ImGuiWindowFlags_NoCollapse;
void BuildMainWindow(bool runningInOverlay)
void BuildContinuousCalDisplay();
void ShowVersionLine();
static bool runningInOverlay;
void BuildMainWindow(bool runningInOverlay_)
{
runningInOverlay = runningInOverlay_;
bool continuousCalibration = CalCtx.state == CalibrationState::Continuous || CalCtx.state == CalibrationState::ContinuousStandby;
auto &io = ImGui::GetIO();
ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(io.DisplaySize, ImGuiCond_Always);
if (!ImGui::Begin("MainWindow", nullptr, bareWindowFlags))
if (!ImGui::Begin("OpenVRSpaceCalibrator", nullptr, bareWindowFlags))
{
ImGui::End();
return;
@@ -54,46 +52,402 @@ void BuildMainWindow(bool runningInOverlay)
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImGui::GetStyleColorVec4(ImGuiCol_Button));
auto state = LoadVRState();
if (continuousCalibration) {
BuildContinuousCalDisplay();
}
else {
auto state = LoadVRState();
ImGui::BeginDisabled(CalCtx.state == CalibrationState::Continuous);
BuildSystemSelection(state);
BuildDeviceSelections(state);
ImGui::EndDisabled();
BuildMenu(runningInOverlay);
ImGui::BeginDisabled(CalCtx.state == CalibrationState::Continuous);
BuildSystemSelection(state);
BuildDeviceSelections(state);
ImGui::EndDisabled();
BuildMenu(runningInOverlay);
}
ShowVersionLine();
ImGui::PopStyleColor();
ImGui::End();
}
void ShowVersionLine() {
ImGui::SetNextWindowPos(ImVec2(10.0f, ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing()));
if (!ImGui::BeginChild("bottom line", ImVec2(ImGui::GetWindowWidth() - 20.0f, ImGui::GetFrameHeightWithSpacing() * 2), false)) {
ImGui::EndChild();
return;
}
ImGui::Text("OpenVR Space Calibrator v" SPACECAL_VERSION_STRING);
if (runningInOverlay)
{
ImGui::SameLine();
ImGui::Text("- close VR overlay to use mouse");
}
ImGui::EndChild();
}
void CCal_BasicInfo();
void CCal_DrawSettings();
void BuildContinuousCalDisplay() {
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(ImGui::GetWindowSize());
ImGui::SetNextWindowBgAlpha(1);
if (!ImGui::Begin("Continuous Calibration", nullptr,
bareWindowFlags & ~ImGuiWindowFlags_NoTitleBar
)) {
ImGui::End();
return;
}
ImVec2 contentRegion;
contentRegion.x = ImGui::GetWindowContentRegionWidth();
contentRegion.y = ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing() * 2.1;
if (!ImGui::BeginChild("CCalDisplayFrame", contentRegion, false)) {
ImGui::EndChild();
return;
}
if (ImGui::BeginTabBar("CCalTabs", 0)) {
if (ImGui::BeginTabItem("Status")) {
CCal_BasicInfo();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("More Graphs")) {
ShowCalibrationDebug(2, 3);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Settings")) {
CCal_DrawSettings();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::EndChild();
ShowVersionLine();
ImGui::End();
}
static void ScaledDragFloat(const char* label, double& f, double scale, double min, double max, int flags = ImGuiSliderFlags_AlwaysClamp) {
float v = (float) (f * scale);
std::string labelStr = std::string(label);
// If starts with ##, just do a normal SliderFloat
if (labelStr.size() > 2 && labelStr[0] == '#' && labelStr[1] == '#') {
ImGui::SliderFloat(label, &v, (float)min, (float)max, "%1.2f", flags);
} else {
// Otherwise do funny
ImGui::Text(label);
ImGui::SameLine();
ImGui::PushID((std::string(label) + "_id").c_str());
// Line up to a column, multiples of 100
constexpr uint32_t LABEL_CURSOR = 100;
uint32_t cursorPosX = ImGui::GetCursorPosX();
uint32_t roundedPosition = ((cursorPosX + LABEL_CURSOR / 2) / LABEL_CURSOR) * LABEL_CURSOR;
ImGui::SetCursorPosX(roundedPosition);
ImGui::SliderFloat((std::string("##") + label).c_str(), &v, (float)min, (float)max, "%1.2f", flags);
ImGui::PopID();
}
f = v / scale;
}
void DrawVectorElement(std::string id, const char* text, double* value);
void CCal_DrawSettings() {
// panel size for boxes
ImVec2 panel_size { ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x, 0 };
ImGui::BeginGroupPanel("Tip", panel_size);
ImGui::Text("Hover over settings to learn more about them!");
ImGui::EndGroupPanel();
// @TODO: Group in UI
// Section: Alignment speeds
{
ImGui::BeginGroupPanel("Calibration speeds", panel_size);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped(
"SpaceCalibrator uses up to three different speeds at which it drags the calibration back into "
"position when drift occurs. These settings control how far off the calibration should be before going back to low speed (for "
"Decel) or going to higher speeds (for Slow and Fast)."
);
ImGui::PopStyleColor();
if (ImGui::BeginTable("SpeedThresholds", 3, 0)) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(1);
ImGui::Text("Translation (mm)");
ImGui::TableSetColumnIndex(2);
ImGui::Text("Rotation (degrees)");
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Decel");
ImGui::TableSetColumnIndex(1);
ScaledDragFloat("##TransDecel", CalCtx.alignmentSpeedParams.thr_trans_tiny, 1000.0, 0, 20.0);
ImGui::TableSetColumnIndex(2);
ScaledDragFloat("##RotDecel", CalCtx.alignmentSpeedParams.thr_rot_tiny, 180.0 / EIGEN_PI, 0, 5.0);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Slow");
ImGui::TableSetColumnIndex(1);
ScaledDragFloat("##TransSlow", CalCtx.alignmentSpeedParams.thr_trans_small, 1000.0,
CalCtx.alignmentSpeedParams.thr_trans_tiny * 1000.0, 20.0);
ImGui::TableSetColumnIndex(2);
ScaledDragFloat("##RotSlow", CalCtx.alignmentSpeedParams.thr_rot_small, 180.0 / EIGEN_PI,
CalCtx.alignmentSpeedParams.thr_rot_tiny * (180.0 / EIGEN_PI), 10.0);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Fast");
ImGui::TableSetColumnIndex(1);
ScaledDragFloat("##TransFast", CalCtx.alignmentSpeedParams.thr_trans_large, 1000.0,
CalCtx.alignmentSpeedParams.thr_trans_small * 1000.0, 50.0);
ImGui::TableSetColumnIndex(2);
ScaledDragFloat("##RotFast", CalCtx.alignmentSpeedParams.thr_rot_large, 180.0 / EIGEN_PI,
CalCtx.alignmentSpeedParams.thr_rot_small * (180.0 / EIGEN_PI), 20.0);
ImGui::EndTable();
}
ImGui::EndGroupPanel();
}
// Section: Alignment speeds
{
ImGui::BeginGroupPanel("Alignment speeds", panel_size);
// ImGui::Separator();
// ImGui::Text("Alignment speeds");
ScaledDragFloat("Decel", CalCtx.alignmentSpeedParams.align_speed_tiny, 1.0, 0, 2.0, 0);
ScaledDragFloat("Slow", CalCtx.alignmentSpeedParams.align_speed_small, 1.0, 0, 2.0, 0);
ScaledDragFloat("Fast", CalCtx.alignmentSpeedParams.align_speed_large, 1.0, 0, 2.0, 0);
ImGui::EndGroupPanel();
}
// Section: Continuous Calibration settings
{
ImGui::BeginGroupPanel("Continuous calibration", panel_size);
{
// @TODO: Reduce code duplication (tooltips)
// Recalibration threshold
ImGui::Text("Recalibration threshold");
ImGui::SameLine();
ImGui::PushID("recalibration_threshold");
ImGui::SliderFloat("##recalibration_threshold_slider", &CalCtx.continuousCalibrationThreshold, 1.01f, 10.0f, "%1.1f", 0);
if (ImGui::IsItemHovered(0)) {
ImGui::SetTooltip("Controls how good the calibration must be before realigning the trackers.\n"
"Higher values cause calibration to happen less often, and may be useful for system with lots of tracking drift.");
}
ImGui::PopID();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped("Controls how often SpaceCalibrator synchronises playspaces.");
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(0)) {
ImGui::SetTooltip("Controls how good the calibration must be before realigning the trackers.\n"
"Higher values cause calibration to happen less often, and may be useful for system with lots of tracking drift.");
}
}
{
// Tracker offset
// ImVec2 panel_size_inner { ImGui::GetCurrentWindow()->DC.ItemWidth, 0};
ImVec2 panel_size_inner { panel_size.x - 11 * 2, 0};
ImGui::BeginGroupPanel("Tracker offset", panel_size_inner);
DrawVectorElement("cc_tracker_offset", "X", &CalCtx.continuousCalibrationOffset.x());
DrawVectorElement("cc_tracker_offset", "Y", &CalCtx.continuousCalibrationOffset.y());
DrawVectorElement("cc_tracker_offset", "Z", &CalCtx.continuousCalibrationOffset.z());
ImGui::EndGroupPanel();
}
{
// Playspace offset
ImVec2 panel_size_inner{ panel_size.x - 11 * 2, 0 };
ImGui::BeginGroupPanel("Playspace scale", panel_size_inner);
DrawVectorElement("cc_playspace_scale", "PLayspace Scale", &CalCtx.calibratedScale);
ImGui::EndGroupPanel();
}
ImGui::EndGroupPanel();
}
ImGui::NewLine();
ImGui::Indent();
if (ImGui::Button("Reset settings")) {
CalCtx.ResetConfig();
}
ImGui::Unindent();
ImGui::NewLine();
// Section: Contributors credits
{
ImGui::BeginGroupPanel("Credits", panel_size);
ImGui::TextDisabled("tach");
ImGui::TextDisabled("pushrax");
ImGui::TextDisabled("bd_");
ImGui::TextDisabled("ArcticFox");
ImGui::TextDisabled("hekky");
ImGui::EndGroupPanel();
}
}
void DrawVectorElement(std::string id, const char* text, double* value) {
constexpr float CONTINUOUS_CALIBRATION_TRACKER_OFFSET_DELTA = 0.01f;
ImGui::Text(text);
ImGui::SameLine();
ImGui::PushID((id + text + "_btn_reset").c_str());
if (ImGui::Button(" 0 ")) {
*value *= 0;
}
ImGui::PopID();
ImGui::SameLine();
if (ImGui::ArrowButton((id + text + "_decrease").c_str(), ImGuiDir_Down)) {
*value -= CONTINUOUS_CALIBRATION_TRACKER_OFFSET_DELTA;
}
ImGui::SameLine();
ImGui::PushItemWidth(100);
ImGui::PushID((id + text + "_text_field").c_str());
ImGui::InputDouble("##label", value, 0, 0, "%.2f");
ImGui::PopID();
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::ArrowButton((id + text + "_increase").c_str(), ImGuiDir_Up)) {
*value += CONTINUOUS_CALIBRATION_TRACKER_OFFSET_DELTA;
}
}
void CCal_BasicInfo() {
if (ImGui::BeginTable("DeviceInfo", 2, 0)) {
ImGui::TableSetupColumn("Reference device");
ImGui::TableSetupColumn("Target device");
ImGui::TableHeadersRow();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::BeginGroup();
ImGui::Text("%s / %s / %s",
CalCtx.referenceStandby.trackingSystem.c_str(),
CalCtx.referenceStandby.model.c_str(),
CalCtx.referenceStandby.serial.c_str()
);
const char* status;
if (CalCtx.referenceID < 0) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, 0xFF000080);
status = "NOT FOUND";
}
else if (!CalCtx.ReferencePoseIsValid()) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, 0xFFFF0080);
status = "NOT TRACKING";
}
else {
status = "OK";
}
ImGui::Text("Status: %s", status);
ImGui::EndGroup();
ImGui::TableSetColumnIndex(1);
ImGui::BeginGroup();
ImGui::Text("%s / %s / %s",
CalCtx.targetStandby.trackingSystem.c_str(),
CalCtx.targetStandby.model.c_str(),
CalCtx.targetStandby.serial.c_str()
);
if (CalCtx.targetID < 0) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, 0xFF000080);
status = "NOT FOUND";
}
else if (!CalCtx.TargetPoseIsValid()) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, 0xFFFF0080);
status = "NOT TRACKING";
}
else {
status = "OK";
}
ImGui::Text("Status: %s", status);
ImGui::EndGroup();
ImGui::EndTable();
}
float width = ImGui::GetWindowContentRegionWidth(), scale = 1.0f;
if (ImGui::BeginTable("##CCal_Cancel", Metrics::enableLogs ? 3 : 2, 0, ImVec2(width * scale, ImGui::GetTextLineHeight() * 2))) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::Button("Cancel Continuous Calibration", ImVec2(-FLT_MIN, 0.0f))) {
EndContinuousCalibration();
}
ImGui::TableSetColumnIndex(1);
if (ImGui::Button("Debug: Force break calibration", ImVec2(-FLT_MIN, 0.0f))) {
DebugApplyRandomOffset();
}
if (Metrics::enableLogs) {
ImGui::TableSetColumnIndex(2);
if (ImGui::Button("Debug: Mark logs", ImVec2(-FLT_MIN, 0.0f))) {
Metrics::WriteLogAnnotation("MARK LOGS");
}
}
ImGui::EndTable();
}
ImGui::Checkbox("Hide tracker", &CalCtx.quashTargetInContinuous);
ImGui::SameLine();
ImGui::Checkbox("Static recalibration", &CalCtx.enableStaticRecalibration);
ImGui::SameLine();
ImGui::Checkbox("Enable debug logs", &Metrics::enableLogs);
ImGui::SameLine();
ImGui::Checkbox("Lock relative transform", &CalCtx.lockRelativePosition);
ImGui::SameLine();
ImGui::Checkbox("Require triggers", &CalCtx.requireTriggerPressToApply);
// Status field...
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 1));
for (const auto& msg : CalCtx.messages) {
if (msg.type == CalibrationContext::Message::String) {
ImGui::TextWrapped("> %s", msg.str.c_str());
}
}
ImGui::PopStyleColor();
ShowCalibrationDebug(1, 3);
}
void BuildMenu(bool runningInOverlay)
{
auto &io = ImGui::GetIO();
ImGuiStyle &style = ImGui::GetStyle();
ImGui::Text("");
if (CalCtx.state == CalibrationState::Continuous || CalCtx.state == CalibrationState::ContinuousStandby) {
float width = ImGui::GetWindowContentRegionWidth(), scale = 1.0f;
if (ImGui::Button("Cancel Continuous Calibration", ImVec2(width * scale, ImGui::GetTextLineHeight() * 2))) {
EndContinuousCalibration();
}
ImGui::Checkbox("Hide target device from application", &CalCtx.quashTargetInContinuous);
// Status field...
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 1));
for (const auto& msg : CalCtx.messages) {
if (msg.type == CalibrationContext::Message::String) {
ImGui::TextWrapped("> %s", msg.str.c_str());
}
}
ImGui::PopStyleColor();
}
else if (CalCtx.state == CalibrationState::None)
if (CalCtx.state == CalibrationState::None)
{
if (CalCtx.validProfile && !CalCtx.enabled)
{
@@ -199,16 +553,6 @@ void BuildMenu(bool runningInOverlay)
ImGui::Button("Calibration in progress...", ImVec2(ImGui::GetWindowContentRegionWidth(), ImGui::GetTextLineHeight() * 2));
}
ImGui::SetNextWindowPos(ImVec2(10.0f, ImGui::GetWindowHeight() - ImGui::GetFrameHeightWithSpacing()));
ImGui::BeginChild("bottom line", ImVec2(ImGui::GetWindowWidth() - 20.0f, ImGui::GetFrameHeightWithSpacing() * 2), false);
ImGui::Text("OpenVR Space Calibrator v" SPACECAL_VERSION_STRING " - by tach/pushrax");
if (runningInOverlay)
{
ImGui::SameLine();
ImGui::Text("- close VR overlay to use mouse");
}
ImGui::EndChild();
ImGui::SetNextWindowPos(ImVec2(20.0f, 20.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x - 40.0f, io.DisplaySize.y - 40.0f), ImGuiCond_Always);
if (ImGui::BeginPopupModal("Calibration Progress", nullptr, bareWindowFlags))
@@ -426,6 +770,7 @@ void BuildDeviceSelection(const VRState &state, int &initialSelected, const std:
}
if (selected == -1 && standby) {
bool present = false;
for (auto& device : state.devices)
{
if (device.trackingSystem != system)
@@ -434,11 +779,11 @@ void BuildDeviceSelection(const VRState &state, int &initialSelected, const std:
if (standbyDevice.model != device.model) continue;
if (standbyDevice.serial != device.serial) continue;
selected = device.id;
present = true;
break;
}
if (selected == -1) {
if (!present) {
auto label = LabelString(standbyDevice);
ImGui::Selectable(label.c_str(), true);
}
@@ -491,62 +836,11 @@ void BuildDeviceSelections(const VRState &state)
}
}
VRState LoadVRState()
{
VRState state;
auto &trackingSystems = state.trackingSystems;
VRState LoadVRState() {
VRState state = VRState::Load();
auto& trackingSystems = state.trackingSystems;
char buffer[vr::k_unMaxPropertyStringSize];
for (uint32_t id = 0; id < vr::k_unMaxTrackedDeviceCount; ++id)
{
vr::ETrackedPropertyError err = vr::TrackedProp_Success;
auto deviceClass = vr::VRSystem()->GetTrackedDeviceClass(id);
if (deviceClass == vr::TrackedDeviceClass_Invalid)
continue;
if (deviceClass != vr::TrackedDeviceClass_TrackingReference)
{
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_TrackingSystemName_String, buffer, vr::k_unMaxPropertyStringSize, &err);
if (err == vr::TrackedProp_Success)
{
std::string system(buffer);
auto existing = std::find(trackingSystems.begin(), trackingSystems.end(), system);
if (existing != trackingSystems.end())
{
if (deviceClass == vr::TrackedDeviceClass_HMD)
{
trackingSystems.erase(existing);
trackingSystems.insert(trackingSystems.begin(), system);
}
}
else
{
trackingSystems.push_back(system);
}
VRDevice device;
device.id = id;
device.deviceClass = deviceClass;
device.trackingSystem = system;
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_ModelNumber_String, buffer, vr::k_unMaxPropertyStringSize, &err);
device.model = std::string(buffer);
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_SerialNumber_String, buffer, vr::k_unMaxPropertyStringSize, &err);
device.serial = std::string(buffer);
device.controllerRole = (vr::ETrackedControllerRole) vr::VRSystem()->GetInt32TrackedDeviceProperty(id, vr::Prop_ControllerRoleHint_Int32, &err);
state.devices.push_back(device);
}
else
{
printf("failed to get tracking system name for id %d\n", id);
}
}
}
// Inject entries for continuous calibration targets which have yet to load
if (CalCtx.state == CalibrationState::ContinuousStandby) {
auto existing = std::find(trackingSystems.begin(), trackingSystems.end(), CalCtx.referenceTrackingSystem);
@@ -606,3 +900,4 @@ void TextWithWidth(const char *label, const char *text, float width)
ImGui::Text(text);
ImGui::EndChild();
}
+2 -1
View File
@@ -1,3 +1,4 @@
#pragma once
void BuildMainWindow(bool runningInOverlay);
void BuildMainWindow(bool runningInOverlay);
void RequestImmediateRedraw();
+108
View File
@@ -0,0 +1,108 @@
#include "stdafx.h"
#include "VRState.h"
VRState VRState::Load()
{
VRState state;
auto& trackingSystems = state.trackingSystems;
char buffer[vr::k_unMaxPropertyStringSize];
for (uint32_t id = 0; id < vr::k_unMaxTrackedDeviceCount; ++id)
{
vr::ETrackedPropertyError err = vr::TrackedProp_Success;
auto deviceClass = vr::VRSystem()->GetTrackedDeviceClass(id);
if (deviceClass == vr::TrackedDeviceClass_Invalid)
continue;
if (deviceClass != vr::TrackedDeviceClass_TrackingReference)
{
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_TrackingSystemName_String, buffer, vr::k_unMaxPropertyStringSize, &err);
if (err == vr::TrackedProp_Success)
{
std::string system(buffer);
// Check if the current HMD is a Pimax crystal
if (deviceClass == vr::TrackedDeviceClass_HMD && system == "aapvr") {
// HMD is a Pimax HMD
vr::HmdMatrix34_t eyeToHeadLeft = vr::VRSystem()->GetEyeToHeadTransform(vr::Eye_Left);
// Crystal's projection matrix is constant 0s or 1s except for [0][3], which stores the IPD offset from the nose
bool isCrystalHmd =
eyeToHeadLeft.m[0][0] == 1 && eyeToHeadLeft.m[0][1] == 0 && eyeToHeadLeft.m[0][2] == 0 && // IPD
eyeToHeadLeft.m[1][0] == 0 && eyeToHeadLeft.m[1][1] == 1 && eyeToHeadLeft.m[1][2] == 0 && eyeToHeadLeft.m[1][3] == 0 &&
eyeToHeadLeft.m[2][0] == 0 && eyeToHeadLeft.m[2][1] == 0 && eyeToHeadLeft.m[2][2] == 1 && eyeToHeadLeft.m[2][3] == 0;
if (isCrystalHmd) {
// Move it outside the aapvr system ; we treat aapvr as if it were lighthouse
system = "Pimax Crystal HMD";
}
} else if (deviceClass == vr::TrackedDeviceClass_Controller && system == "oculus") {
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_RenderModelName_String, buffer, vr::k_unMaxPropertyStringSize, &err);
std::string renderModel(buffer);
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_ConnectedWirelessDongle_String, buffer, vr::k_unMaxPropertyStringSize, &err);
std::string connectedWirelessDongle(buffer);
// Check if the controller claims its an oculus controller but also pimax
if (renderModel.find("{aapvr}") != std::string::npos &&
renderModel.find("crystal") != std::string::npos &&
connectedWirelessDongle.find("lighthouse") != std::string::npos) {
system = "Pimax Crystal Controllers";
}
}
auto existing = std::find(trackingSystems.begin(), trackingSystems.end(), system);
if (existing != trackingSystems.end())
{
if (deviceClass == vr::TrackedDeviceClass_HMD)
{
trackingSystems.erase(existing);
trackingSystems.insert(trackingSystems.begin(), system);
}
}
else
{
trackingSystems.push_back(system);
}
VRDevice device;
device.id = id;
device.deviceClass = deviceClass;
device.trackingSystem = system;
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_ModelNumber_String, buffer, vr::k_unMaxPropertyStringSize, &err);
device.model = std::string(buffer);
vr::VRSystem()->GetStringTrackedDeviceProperty(id, vr::Prop_SerialNumber_String, buffer, vr::k_unMaxPropertyStringSize, &err);
device.serial = std::string(buffer);
device.controllerRole = (vr::ETrackedControllerRole)vr::VRSystem()->GetInt32TrackedDeviceProperty(id, vr::Prop_ControllerRoleHint_Int32, &err);
state.devices.push_back(device);
}
else
{
printf("failed to get tracking system name for id %d\n", id);
}
}
}
return state;
}
int VRState::FindDevice(const std::string& trackingSystem, const std::string& model, const std::string& serial) const {
for (int i = 0; i < devices.size(); i++) {
const auto& device = devices[i];
if (device.trackingSystem == trackingSystem && device.model == model && device.serial == serial) return device.id;
}
// allow fallback excluding serial for HMD specifically
for (int i = 0; i < devices.size(); i++) {
const auto& device = devices[i];
if (device.trackingSystem == trackingSystem && device.model == model && device.deviceClass == vr::TrackedDeviceClass::TrackedDeviceClass_HMD) return device.id;
}
return -1;
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include <vector>
#include <openvr.h>
struct VRDevice
{
int id = -1;
vr::TrackedDeviceClass deviceClass;
std::string model = "";
std::string serial = "";
std::string trackingSystem = "";
vr::ETrackedControllerRole controllerRole = vr::TrackedControllerRole_Invalid;
};
struct VRState
{
std::vector<std::string> trackingSystems;
std::vector<VRDevice> devices;
int FindDevice(const std::string& trackingSystem, const std::string& model, const std::string& serial) const;
static VRState Load();
};
+128
View File
@@ -0,0 +1,128 @@
#include "imgui_extensions.h"
#define IMGUI_DEFINE_MATH_OPERATORS
#include "imgui_internal.h"
static ImVector<ImRect> s_GroupPanelLabelStack;
void ImGui::BeginGroupPanel(const char* name, const ImVec2& size)
{
ImGui::BeginGroup();
auto cursorPos = ImGui::GetCursorScreenPos();
auto itemSpacing = ImGui::GetStyle().ItemSpacing;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
auto frameHeight = ImGui::GetFrameHeight();
ImGui::BeginGroup();
ImVec2 effectiveSize = size;
if (size.x < 0.0f)
effectiveSize.x = ImGui::GetContentRegionAvail().x;
else
effectiveSize.x = size.x;
ImGui::Dummy(ImVec2(effectiveSize.x, 0.0f));
ImGui::Dummy(ImVec2(frameHeight * 0.5f, 0.0f));
ImGui::SameLine(0.0f, 0.0f);
ImGui::BeginGroup();
ImGui::Dummy(ImVec2(frameHeight * 0.5f, 0.0f));
ImGui::SameLine(0.0f, 0.0f);
ImGui::TextUnformatted(name);
auto labelMin = ImGui::GetItemRectMin();
auto labelMax = ImGui::GetItemRectMax();
ImGui::SameLine(0.0f, 0.0f);
ImGui::Dummy(ImVec2(0.0, frameHeight + itemSpacing.y));
ImGui::BeginGroup();
//ImGui::GetWindowDrawList()->AddRect(labelMin, labelMax, IM_COL32(255, 0, 255, 255));
ImGui::PopStyleVar(2);
#if IMGUI_VERSION_NUM >= 17301
ImGui::GetCurrentWindow()->ContentRegionRect.Max.x -= frameHeight * 0.5f;
ImGui::GetCurrentWindow()->WorkRect.Max.x -= frameHeight * 0.5f;
ImGui::GetCurrentWindow()->InnerRect.Max.x -= frameHeight * 0.5f;
#else
ImGui::GetCurrentWindow()->ContentsRegionRect.Max.x -= frameHeight * 0.5f;
#endif
ImGui::GetCurrentWindow()->Size.x -= frameHeight;
auto itemWidth = ImGui::CalcItemWidth();
ImGui::PushItemWidth(ImMax(0.0f, itemWidth - frameHeight));
s_GroupPanelLabelStack.push_back(ImRect(labelMin, labelMax));
}
void ImGui::EndGroupPanel()
{
ImGui::PopItemWidth();
auto itemSpacing = ImGui::GetStyle().ItemSpacing;
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
auto frameHeight = ImGui::GetFrameHeight();
ImGui::EndGroup();
//ImGui::GetWindowDrawList()->AddRectFilled(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(0, 255, 0, 64), 4.0f);
ImGui::EndGroup();
ImGui::SameLine(0.0f, 0.0f);
ImGui::Dummy(ImVec2(frameHeight * 0.5f, 0.0f));
ImGui::Dummy(ImVec2(0.0, frameHeight - frameHeight * 0.5f - itemSpacing.y));
ImGui::EndGroup();
auto itemMin = ImGui::GetItemRectMin();
auto itemMax = ImGui::GetItemRectMax();
//ImGui::GetWindowDrawList()->AddRectFilled(itemMin, itemMax, IM_COL32(255, 0, 0, 64), 4.0f);
auto labelRect = s_GroupPanelLabelStack.back();
s_GroupPanelLabelStack.pop_back();
ImVec2 halfFrame = ImVec2(frameHeight * 0.25f, frameHeight) * 0.5f;
ImRect frameRect = ImRect(itemMin + halfFrame, itemMax - ImVec2(halfFrame.x, 0.0f));
labelRect.Min.x -= itemSpacing.x;
labelRect.Max.x += itemSpacing.x;
for (int i = 0; i < 4; ++i)
{
switch (i)
{
// left half-plane
case 0: ImGui::PushClipRect(ImVec2(-FLT_MAX, -FLT_MAX), ImVec2(labelRect.Min.x, FLT_MAX), true); break;
// right half-plane
case 1: ImGui::PushClipRect(ImVec2(labelRect.Max.x, -FLT_MAX), ImVec2(FLT_MAX, FLT_MAX), true); break;
// top
case 2: ImGui::PushClipRect(ImVec2(labelRect.Min.x, -FLT_MAX), ImVec2(labelRect.Max.x, labelRect.Min.y), true); break;
// bottom
case 3: ImGui::PushClipRect(ImVec2(labelRect.Min.x, labelRect.Max.y), ImVec2(labelRect.Max.x, FLT_MAX), true); break;
}
ImGui::GetWindowDrawList()->AddRect(
frameRect.Min, frameRect.Max,
ImColor(ImGui::GetStyleColorVec4(ImGuiCol_Border)),
halfFrame.x);
ImGui::PopClipRect();
}
ImGui::PopStyleVar(2);
#if IMGUI_VERSION_NUM >= 17301
ImGui::GetCurrentWindow()->ContentRegionRect.Max.x += frameHeight * 0.5f;
ImGui::GetCurrentWindow()->WorkRect.Max.x += frameHeight * 0.5f;
ImGui::GetCurrentWindow()->InnerRect.Max.x += frameHeight * 0.5f;
#else
ImGui::GetCurrentWindow()->ContentsRegionRect.Max.x += frameHeight * 0.5f;
#endif
ImGui::GetCurrentWindow()->Size.x += frameHeight;
ImGui::Dummy(ImVec2(0.0f, 0.0f));
ImGui::EndGroup();
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <imgui.h>
namespace ImGui
{
// Group panels taken from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
// Licensed under CC0 license as per https://github.com/ocornut/imgui/issues/1496#issuecomment-1287772456
void BeginGroupPanel(const char* name, const ImVec2& size = ImVec2(0.0f, 0.0f));
void EndGroupPanel();
}
+2
View File
@@ -1,5 +1,7 @@
#pragma once
#define EIGEN_MPL2_ONLY
#include "targetver.h"
#define WIN32_LEAN_AND_MEAN
@@ -16,6 +16,16 @@ void IPCServer::HandleRequest(const protocol::Request &request, protocol::Respon
response.type = protocol::ResponseSuccess;
break;
case protocol::RequestDebugOffset:
driver->HandleApplyRandomOffset();
response.type = protocol::ResponseSuccess;
break;
case protocol::RequestSetAlignmentSpeedParams:
driver->HandleSetAlignmentSpeedParams(request.setAlignmentSpeedParams);
response.type = protocol::ResponseSuccess;
break;
default:
LOG("Invalid IPC request: %d", request.type);
break;
@@ -0,0 +1,50 @@
#pragma once
#define EIGEN_MPL2_ONLY
#include <Eigen/Dense>
/**
* Contains an isometric transformation, represented as the pair of a rotation quaternion and translation vector.
* The translation is applied to the left of the quaternion.
*/
struct IsoTransform {
Eigen::Quaterniond rotation;
Eigen::Vector3d translation;
IsoTransform() : rotation(Eigen::Quaterniond::Identity()), translation(Eigen::Vector3d::Zero()) {}
IsoTransform(const Eigen::Quaterniond &rot) : rotation(rot), translation(Eigen::Vector3d::Zero()) {}
IsoTransform(const Eigen::Vector3d &trans) : rotation(Eigen::Quaterniond::Identity()) {}
IsoTransform(const Eigen::Quaterniond& rot, const Eigen::Vector3d& trans) : rotation(rot), translation(trans) {}
void pretranslate(const Eigen::Vector3d& t) {
translation += t;
}
/**
* Interpolates between this transform and target. The position of localPoint after transformation will smoothly
* lerp between (this * localPoint) and (target * localPoint), despite rotation occurring around it.
*/
IsoTransform interpolateAround(double lerp, const IsoTransform& target, const Eigen::Vector3d& localPoint) const;
};
inline IsoTransform operator*(const IsoTransform& a, const IsoTransform& b) {
// tA * rA * tB * rB = tA * (trans(rA * tB)) * rA * rB
auto rot = a.rotation * b.rotation;
Eigen::Vector3d trans = a.translation + Eigen::Isometry3d(a.rotation) * b.translation;
return IsoTransform(rot, trans);
}
inline Eigen::Vector3d operator*(const IsoTransform& a, const Eigen::Vector3d& p) {
return a.translation + Eigen::Isometry3d(a.rotation) * p;
}
inline IsoTransform IsoTransform::interpolateAround(double lerp, const IsoTransform& target, const Eigen::Vector3d& localPoint) const {
auto initialPos = (*this) * localPoint;
Eigen::Vector3d finalPos = initialPos * (1 - lerp) + (target * localPoint) * lerp;
auto newRotation = rotation.slerp(lerp, target.rotation);
Eigen::Vector3d newTranslation = finalPos - Eigen::Isometry3d(newRotation) * localPoint;
return IsoTransform(newRotation, newTranslation);
}
@@ -64,7 +64,7 @@
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>..\lib\MinHook\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>libMinHook.x64.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -85,7 +85,7 @@
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>..\lib\MinHook\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>libMinHook.x64.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
@@ -93,6 +93,7 @@
<ClInclude Include="Hooking.h" />
<ClInclude Include="InterfaceHookInjector.h" />
<ClInclude Include="IPCServer.h" />
<ClInclude Include="IsometryTransform.h" />
<ClInclude Include="Logging.h" />
<ClInclude Include="OpenVR-SpaceCalibratorDriver.h" />
<ClInclude Include="ServerTrackedDeviceProvider.h" />
@@ -114,6 +115,11 @@
<ClCompile Include="OpenVR-SpaceCalibratorDriver.cpp" />
<ClCompile Include="ServerTrackedDeviceProvider.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\lib\minhook\build\VC16\libMinHook.vcxproj">
<Project>{f142a341-5ee0-442d-a15f-98ae9b48dbae}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
@@ -39,6 +39,9 @@
<ClInclude Include="InterfaceHookInjector.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="IsometryTransform.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="OpenVR-SpaceCalibratorDriver.cpp">
@@ -1,6 +1,9 @@
#include "ServerTrackedDeviceProvider.h"
#include "Logging.h"
#include "InterfaceHookInjector.h"
#include "IsometryTransform.h"
#include <random>
vr::EVRInitError ServerTrackedDeviceProvider::Init(vr::IVRDriverContext *pDriverContext)
{
@@ -8,11 +11,27 @@ vr::EVRInitError ServerTrackedDeviceProvider::Init(vr::IVRDriverContext *pDriver
VR_INIT_SERVER_DRIVER_CONTEXT(pDriverContext);
memset(transforms, 0, vr::k_unMaxTrackedDeviceCount * sizeof DeviceTransform);
memset(&alignmentSpeedParams, 0, sizeof alignmentSpeedParams);
alignmentSpeedParams.thr_rot_tiny = 0.1f * (EIGEN_PI / 180.0f);
alignmentSpeedParams.thr_rot_small = 1.0f * (EIGEN_PI / 180.0f);
alignmentSpeedParams.thr_rot_large = 5.0f * (EIGEN_PI / 180.0f);
alignmentSpeedParams.thr_trans_tiny = 0.1f / 1000.0; // mm
alignmentSpeedParams.thr_trans_small = 1.0f / 1000.0; // mm
alignmentSpeedParams.thr_trans_large = 20.0f / 1000.0; // mm
alignmentSpeedParams.align_speed_tiny = 0.05f;
alignmentSpeedParams.align_speed_small = 0.2f;
alignmentSpeedParams.align_speed_large = 2.0f;
InjectHooks(this, pDriverContext);
server.Run();
shmem.Create(OPENVR_SPACECALIBRATOR_SHMEM_NAME);
debugTransform = Eigen::Vector3d::Zero();
debugRotation = Eigen::Quaterniond::Identity();
return vr::VRInitError_None;
}
@@ -26,6 +45,8 @@ void ServerTrackedDeviceProvider::Cleanup()
}
namespace {
vr::HmdQuaternion_t convert(const Eigen::Quaterniond& q) {
vr::HmdQuaternion_t result;
result.w = q.w();
@@ -37,9 +58,9 @@ namespace {
vr::HmdVector3_t convert(const Eigen::Vector3d& v) {
vr::HmdVector3_t result;
result.v[0] = v.x();
result.v[1] = v.y();
result.v[2] = v.z();
result.v[0] = (float) v.x();
result.v[1] = (float) v.y();
result.v[2] = (float) v.z();
return result;
}
@@ -54,8 +75,93 @@ namespace {
Eigen::Vector3d convert(const double* arr) {
return Eigen::Vector3d(arr[0], arr[1], arr[2]);
}
IsoTransform toIsoWorldTransform(const vr::DriverPose_t& pose) {
Eigen::Quaterniond rot(pose.qWorldFromDriverRotation.w, pose.qWorldFromDriverRotation.x, pose.qWorldFromDriverRotation.y, pose.qWorldFromDriverRotation.z);
Eigen::Vector3d trans(pose.vecWorldFromDriverTranslation[0], pose.vecWorldFromDriverTranslation[1], pose.vecWorldFromDriverTranslation[2]);
return IsoTransform(rot, trans);
}
IsoTransform toIsoPose(const vr::DriverPose_t& pose) {
auto worldXform = toIsoWorldTransform(pose);
Eigen::Quaterniond rot(pose.qRotation.w, pose.qRotation.x, pose.qRotation.y, pose.qRotation.z);
Eigen::Vector3d trans(pose.vecPosition[0], pose.vecPosition[1], pose.vecPosition[2]);
return worldXform * IsoTransform(rot, trans);
}
}
/**
* This function heuristically evaluates the amount of drift between the src and target playspace transforms,
* evaluated centered on the `pose` device transform. This is then used to control the speed of realignment.
*/
ServerTrackedDeviceProvider::DeltaSize ServerTrackedDeviceProvider::GetTransformDeltaSize(
DeltaSize prior_delta,
const IsoTransform& deviceWorldPose,
const IsoTransform& src,
const IsoTransform& target
) const {
const auto src_pose = src * deviceWorldPose;
const auto target_pose = target * deviceWorldPose;
const auto trans_delta = (src_pose.translation - target_pose.translation).squaredNorm();
const auto rot_delta = src_pose.rotation.angularDistance(target_pose.rotation);
DeltaSize trans_level, rot_level;
if (trans_delta > alignmentSpeedParams.thr_trans_large) trans_level = DeltaSize::LARGE;
else if (trans_delta > alignmentSpeedParams.thr_trans_small) trans_level = DeltaSize::SMALL;
else trans_level = DeltaSize::TINY;
if (rot_delta > alignmentSpeedParams.thr_rot_large) rot_level = DeltaSize::LARGE;
else if (rot_delta > alignmentSpeedParams.thr_rot_small) rot_level = DeltaSize::SMALL;
else rot_level = DeltaSize::TINY;
if (trans_level == DeltaSize::TINY && rot_level == DeltaSize::TINY) return DeltaSize::TINY;
else return max(prior_delta, max(trans_level, rot_level));
}
double ServerTrackedDeviceProvider::GetTransformRate(DeltaSize delta) const {
switch (delta) {
case DeltaSize::TINY: return alignmentSpeedParams.align_speed_tiny;
case DeltaSize::SMALL: return alignmentSpeedParams.align_speed_small;
default: return alignmentSpeedParams.align_speed_large;
}
}
/**
* Smoothly interpolates the device active transform towards the target transform.
*/
void ServerTrackedDeviceProvider::BlendTransform(DeviceTransform& device, const IsoTransform &deviceWorldPose) const {
LARGE_INTEGER timestamp, freq;
QueryPerformanceCounter(&timestamp);
QueryPerformanceFrequency(&freq);
double lerp = (timestamp.QuadPart - device.lastPoll.QuadPart) / (double)freq.QuadPart;
device.lastPoll = timestamp;
lerp *= GetTransformRate(device.currentRate);
if (lerp > 1.0)
lerp = 1.0;
if (lerp < 0 || isnan(lerp))
lerp = 0;
device.transform = device.transform.interpolateAround(lerp, device.targetTransform, deviceWorldPose.translation);
}
void ServerTrackedDeviceProvider::ApplyTransform(DeviceTransform& device, vr::DriverPose_t& devicePose) const {
auto deviceWorldTransform = toIsoWorldTransform(devicePose);
deviceWorldTransform = device.transform * deviceWorldTransform;
devicePose.vecWorldFromDriverTranslation[0] = deviceWorldTransform.translation(0);
devicePose.vecWorldFromDriverTranslation[1] = deviceWorldTransform.translation(1);
devicePose.vecWorldFromDriverTranslation[2] = deviceWorldTransform.translation(2);
devicePose.qWorldFromDriverRotation = convert(deviceWorldTransform.rotation);
}
inline vr::HmdQuaternion_t operator*(const vr::HmdQuaternion_t &lhs, const vr::HmdQuaternion_t &rhs) {
return {
(lhs.w * rhs.w) - (lhs.x * rhs.x) - (lhs.y * rhs.y) - (lhs.z * rhs.z),
@@ -78,17 +184,17 @@ void ServerTrackedDeviceProvider::SetDeviceTransform(const protocol::SetDeviceTr
tf.enabled = newTransform.enabled;
if (newTransform.updateTranslation) {
tf.targetTranslation = convert(newTransform.translation);
tf.targetTransform.translation = convert(newTransform.translation);
if (!newTransform.lerp) {
tf.translation = tf.targetTranslation;
tf.transform.translation = tf.targetTransform.translation;
}
}
if (newTransform.updateRotation) {
tf.targetRotation = convert(newTransform.rotation);
tf.targetTransform.rotation = convert(newTransform.rotation);
if (!newTransform.lerp) {
tf.rotation = tf.targetRotation;
tf.transform.rotation = tf.targetTransform.rotation;
}
}
@@ -100,6 +206,16 @@ void ServerTrackedDeviceProvider::SetDeviceTransform(const protocol::SetDeviceTr
bool ServerTrackedDeviceProvider::HandleDevicePoseUpdated(uint32_t openVRID, vr::DriverPose_t &pose)
{
// Apply debug pose before anything else
if (openVRID > 0) {
auto dbgPos = convert(pose.vecPosition) + debugTransform;
auto dbgRot = convert(pose.qRotation) * debugRotation;
pose.qRotation = convert(dbgRot);
pose.vecPosition[0] = dbgPos(0);
pose.vecPosition[1] = dbgPos(1);
pose.vecPosition[2] = dbgPos(2);
}
shmem.SetPose(openVRID, pose);
auto& tf = transforms[openVRID];
@@ -110,39 +226,32 @@ bool ServerTrackedDeviceProvider::HandleDevicePoseUpdated(uint32_t openVRID, vr:
pose.vecPosition[2] = -pose.vecWorldFromDriverTranslation[2];
} else if (tf.enabled)
{
LARGE_INTEGER timestamp, freq;
QueryPerformanceCounter(&timestamp);
QueryPerformanceFrequency(&freq);
double lerp = (timestamp.QuadPart - tf.lastPoll.QuadPart) / (double)freq.QuadPart;
lerp *= 2;
if (lerp > 1.0)
lerp = 1.0;
if (lerp < 0 || isnan(lerp))
lerp = 0;
// Cancel out any translation induced by this rotation slerp. This helps avoid your
// controllers/trackers flying away when far from the origin and a rotation correction
// is performed.
auto priorPos = tf.rotation * convert(pose.vecPosition);
tf.rotation = tf.rotation.slerp(lerp, tf.targetRotation);
auto newPos = tf.rotation * convert(pose.vecPosition);
tf.translation -= newPos - priorPos;
tf.translation = tf.translation * (1 - lerp) + tf.targetTranslation * lerp;
tf.lastPoll = timestamp;
pose.qWorldFromDriverRotation = convert(tf.rotation * convert(pose.qWorldFromDriverRotation));
// @TODO: Offset, scale, and re-offset
pose.vecPosition[0] *= tf.scale;
pose.vecPosition[1] *= tf.scale;
pose.vecPosition[2] *= tf.scale;
auto newTranslation = tf.translation + tf.rotation * convert(pose.vecWorldFromDriverTranslation);
pose.vecWorldFromDriverTranslation[0] = newTranslation.x();
pose.vecWorldFromDriverTranslation[1] = newTranslation.y();
pose.vecWorldFromDriverTranslation[2] = newTranslation.z();
auto deviceWorldPose = toIsoPose(pose);
tf.currentRate = GetTransformDeltaSize(tf.currentRate, deviceWorldPose, tf.transform, tf.targetTransform);
double lerp = GetTransformRate(tf.currentRate);
BlendTransform(tf, deviceWorldPose);
ApplyTransform(tf, pose);
}
return true;
}
void ServerTrackedDeviceProvider::HandleApplyRandomOffset() {
std::random_device gen;
std::uniform_real_distribution<double> d(-1, 1);
auto init = Eigen::Vector3d(d(gen), d(gen), d(gen));
auto posOffset = init * 0.25f;
debugTransform = posOffset;
debugRotation = Eigen::Quaterniond::Identity();
std::ostringstream oss;
oss << "Applied random offset: " << posOffset << " from init " << init << std::endl;
LOG("%s", oss.str().c_str());
}
@@ -1,12 +1,16 @@
#pragma once
#define EIGEN_MPL2_ONLY
#include "IPCServer.h"
#include "../Protocol.h"
#include "IsometryTransform.h"
#include <Eigen/Dense>
#include <openvr_driver.h>
class ServerTrackedDeviceProvider : public vr::IServerTrackedDeviceProvider
{
public:
@@ -40,20 +44,48 @@ public:
ServerTrackedDeviceProvider() : server(this) { }
void SetDeviceTransform(const protocol::SetDeviceTransform &newTransform);
bool HandleDevicePoseUpdated(uint32_t openVRID, vr::DriverPose_t &pose);
void HandleApplyRandomOffset();
void HandleSetAlignmentSpeedParams(const protocol::AlignmentSpeedParams params) {
alignmentSpeedParams = params;
}
private:
IPCServer server;
protocol::DriverPoseShmem shmem;
enum DeltaSize {
TINY,
SMALL,
LARGE
};
struct DeviceTransform
{
bool enabled = false;
bool quash = false;
Eigen::Vector3d translation, targetTranslation;
Eigen::Quaterniond rotation, targetRotation;
IsoTransform transform, targetTransform;
double scale;
LARGE_INTEGER lastPoll;
DeltaSize currentRate = DeltaSize::TINY;
};
DeviceTransform transforms[vr::k_unMaxTrackedDeviceCount];
Eigen::Vector3d debugTransform;
Eigen::Quaterniond debugRotation;
DeltaSize currentDeltaSpeed[vr::k_unMaxTrackedDeviceCount];
protocol::AlignmentSpeedParams alignmentSpeedParams;
DeltaSize GetTransformDeltaSize(
DeltaSize prior_delta,
const IsoTransform& deviceWorldPose,
const IsoTransform& src,
const IsoTransform& target
) const;
double GetTransformRate(DeltaSize delta) const;
void BlendTransform(DeviceTransform& device, const IsoTransform& deviceWorldPose) const;
void ApplyTransform(DeviceTransform& device, vr::DriverPose_t& devicePose) const;
};
+28 -1
View File
@@ -92,13 +92,15 @@ namespace vr {
namespace protocol
{
const uint32_t Version = 3;
const uint32_t Version = 4;
enum RequestType
{
RequestInvalid,
RequestHandshake,
RequestSetDeviceTransform,
RequestSetAlignmentSpeedParams,
RequestDebugOffset
};
enum ResponseType
@@ -113,6 +115,29 @@ namespace protocol
uint32_t version = Version;
};
struct AlignmentSpeedParams
{
/**
* The threshold at which we adjust the alignment speed based on the position offset
* between current and target calibrations. Generally, we increase the speed if we go
* above small/large, and decrease it only once it's under tiny.
*
* These values are expressed as distance squared
*/
double thr_trans_tiny, thr_trans_small, thr_trans_large;
/**
* Similar thresholds for rotation offsets, in radians
*/
double thr_rot_tiny, thr_rot_small, thr_rot_large;
/**
* The speed of alignment, expressed as a lerp/slerp factor. 1 will blend most of the way in <1 second.
* (We actually do a lerp(s * delta_t) where s is the speed factor here)
*/
double align_speed_tiny, align_speed_small, align_speed_large;
};
struct SetDeviceTransform
{
uint32_t openVRID;
@@ -151,10 +176,12 @@ namespace protocol
union {
SetDeviceTransform setDeviceTransform;
AlignmentSpeedParams setAlignmentSpeedParams;
};
Request() : type(RequestInvalid) { }
Request(RequestType type) : type(type) { }
Request(AlignmentSpeedParams params) : type(RequestType::RequestSetAlignmentSpeedParams), setAlignmentSpeedParams(params) {}
};
struct Response
+36 -12
View File
@@ -13,7 +13,7 @@ A quick video of how this works using an old version (~v0.3) is available at htt
### Install
Before following the directions below, download and run the installer for the [latest release](https://github.com/pushrax/OpenVR-SpaceCalibrator/releases). This will automatically set up SteamVR for use with multiple tracking systems (`activateMultipleDrivers: true`). There are many guides that say you need to edit the SteamVR config manually. You do not.
Before following the directions below, download and run the installer for the [latest release](https://github.com/hyblocker/OpenVR-SpaceCalibrator/releases/latest). This will automatically set up SteamVR for use with multiple tracking systems (`activateMultipleDrivers: true`). There are many guides that say you need to edit the SteamVR config manually. You do not.
### Usage
@@ -21,22 +21,46 @@ Once Space Calibrator has a calibration, it works in the background to keep your
### Calibration
This fork supports continuous calibration. This means that there are two ways to calibrate:
#### 1. Normal Calibration
> Use this if you do not have a tracker on your VR headset.
As part of first time setup, or when you make a change to your space (e.g. move a sensor), and occasionally as the calibration drifts over time (consumer VR tracking isn't perfectly stable), you'll need to run a calibration:
1. Copy the chaperone/guardian bounds from your HMD's play space. This doesn't need to be run if your HMD's play space hasn't changed since last time you copied it. __Example:__ if you're using the Rift with Vive trackers and you bump a Vive lighthouse, or if the calibration has just drifted a little, you likely don't need to run this step, but if you bump an Oculus sensor you will (after running Oculus guardian setup again).
1. Run SteamVR, with only devices from your HMD's tracking system powered on. __Example:__ for Rift with Vive trackers, don't turn on the trackers yet.
2. Confirm your chaperone/guardian is set up with the walls in the right place. If you change it later, you need to run step again.
3. Open SPACE CAL in the SteamVR dashboard overlay.
4. Click `Copy Chaperone Bounds to profile`
1. Run SteamVR, with only devices from your HMD's tracking system powered on. __Example:__ for Rift with Vive trackers, don't turn on the trackers yet.
2. Confirm your chaperone/guardian is set up with the walls in the right place. If you change it later, you need to run step again.
3. Open SPACE CAL in the SteamVR dashboard overlay.
4. Click `Copy Chaperone Bounds to profile`
2. Calibrate devices.
1. Open SteamVR if you haven't already. Turn on some or all your devices.
2. Open SPACE CAL in the SteamVR dashboard overlay.
3. Select one device from the reference space on the left and one device from the target space on the right. If you turned on multiple devices from one space and can't tell which one is selected, click "Identify selected devices" to blink an LED or vibrate it. __Example:__ for Rift with Vive trackers, you'll see the Touch controllers on the left, and Vive trackers on the right. __Pro tip:__ if you turn on just one Vive tracker, you don't have to figure out which one is selected.
4. Hold these two devices in one hand, like they're glued together. If they slip, calibration won't work as well.
5. Click `Start Calibration`
6. Move and rotate your hand around slowly a few times, like you're calibrating the compass on your phone. You want to sample as many orientations as possible.
7. Done! A profile will be saved automatically. If you haven't already, turn on all your devices. Space Calibrator will automatically apply the calibration to devices as they turn on.
1. Open SteamVR if you haven't already. Turn on some or all your devices.
2. Open SPACE CAL in the SteamVR dashboard overlay.
3. Select one device from the reference space on the left and one device from the target space on the right. If you turned on multiple devices from one space and can't tell which one is selected, click "Identify selected devices" to blink an LED or vibrate it. __Example:__ for Rift with Vive trackers, you'll see the Touch controllers on the left, and Vive trackers on the right. __Pro tip:__ if you turn on just one Vive tracker, you don't have to figure out which one is selected.
4. Hold these two devices in one hand, like they're glued together. If they slip, calibration won't work as well.
5. Click `Start Calibration`
6. Move and rotate your hand around slowly a few times, like you're calibrating the compass on your phone. You want to sample as many orientations as possible.
7. Done! A profile will be saved automatically. If you haven't already, turn on all your devices. Space Calibrator will automatically apply the calibration to devices as they turn on.
#### 2. Continuous Calibration
> Use this only if you have a tracker on your VR headset.
Continuous calibration allows you to calibrate playspaces continuously, eliminating the need to do any of the work above.
1. Start SteamVR with the VR headset you wish to use.
2. Turn on **ONLY** the tracker which is attached on the VR headset.
3. Select the VR headset and tracker and calibrate.
4. Turn on your other devices.
5. You should see them line up with you as you after moving around your playspace for a bit for an initial calibration.
### Calibration outside VR
+1 -1
View File
@@ -1,3 +1,3 @@
#pragma once
#define SPACECAL_VERSION_STRING "1.4"
#define SPACECAL_VERSION_STRING "1.4-contcal-r8"
-81
View File
@@ -1,81 +0,0 @@
MinHook - The Minimalistic API Hooking Library for x64/x86
Copyright (C) 2009-2017 Tsuda Kageyu.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
Portions of this software are Copyright (c) 2008-2009, Vyacheslav Patkov.
================================================================================
Hacker Disassembler Engine 32 C
Copyright (c) 2008-2009, Vyacheslav Patkov.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Hacker Disassembler Engine 64 C
Copyright (c) 2008-2009, Vyacheslav Patkov.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-185
View File
@@ -1,185 +0,0 @@
/*
* MinHook - The Minimalistic API Hooking Library for x64/x86
* Copyright (C) 2009-2017 Tsuda Kageyu.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#if !(defined _M_IX86) && !(defined _M_X64) && !(defined __i386__) && !(defined __x86_64__)
#error MinHook supports only x86 and x64 systems.
#endif
#include <windows.h>
// MinHook Error Codes.
typedef enum MH_STATUS
{
// Unknown error. Should not be returned.
MH_UNKNOWN = -1,
// Successful.
MH_OK = 0,
// MinHook is already initialized.
MH_ERROR_ALREADY_INITIALIZED,
// MinHook is not initialized yet, or already uninitialized.
MH_ERROR_NOT_INITIALIZED,
// The hook for the specified target function is already created.
MH_ERROR_ALREADY_CREATED,
// The hook for the specified target function is not created yet.
MH_ERROR_NOT_CREATED,
// The hook for the specified target function is already enabled.
MH_ERROR_ENABLED,
// The hook for the specified target function is not enabled yet, or already
// disabled.
MH_ERROR_DISABLED,
// The specified pointer is invalid. It points the address of non-allocated
// and/or non-executable region.
MH_ERROR_NOT_EXECUTABLE,
// The specified target function cannot be hooked.
MH_ERROR_UNSUPPORTED_FUNCTION,
// Failed to allocate memory.
MH_ERROR_MEMORY_ALLOC,
// Failed to change the memory protection.
MH_ERROR_MEMORY_PROTECT,
// The specified module is not loaded.
MH_ERROR_MODULE_NOT_FOUND,
// The specified function is not found.
MH_ERROR_FUNCTION_NOT_FOUND
}
MH_STATUS;
// Can be passed as a parameter to MH_EnableHook, MH_DisableHook,
// MH_QueueEnableHook or MH_QueueDisableHook.
#define MH_ALL_HOOKS NULL
#ifdef __cplusplus
extern "C" {
#endif
// Initialize the MinHook library. You must call this function EXACTLY ONCE
// at the beginning of your program.
MH_STATUS WINAPI MH_Initialize(VOID);
// Uninitialize the MinHook library. You must call this function EXACTLY
// ONCE at the end of your program.
MH_STATUS WINAPI MH_Uninitialize(VOID);
// Creates a hook for the specified target function, in disabled state.
// Parameters:
// pTarget [in] A pointer to the target function, which will be
// overridden by the detour function.
// pDetour [in] A pointer to the detour function, which will override
// the target function.
// ppOriginal [out] A pointer to the trampoline function, which will be
// used to call the original target function.
// This parameter can be NULL.
MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal);
// Creates a hook for the specified API function, in disabled state.
// Parameters:
// pszModule [in] A pointer to the loaded module name which contains the
// target function.
// pszProcName [in] A pointer to the target function name, which will be
// overridden by the detour function.
// pDetour [in] A pointer to the detour function, which will override
// the target function.
// ppOriginal [out] A pointer to the trampoline function, which will be
// used to call the original target function.
// This parameter can be NULL.
MH_STATUS WINAPI MH_CreateHookApi(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal);
// Creates a hook for the specified API function, in disabled state.
// Parameters:
// pszModule [in] A pointer to the loaded module name which contains the
// target function.
// pszProcName [in] A pointer to the target function name, which will be
// overridden by the detour function.
// pDetour [in] A pointer to the detour function, which will override
// the target function.
// ppOriginal [out] A pointer to the trampoline function, which will be
// used to call the original target function.
// This parameter can be NULL.
// ppTarget [out] A pointer to the target function, which will be used
// with other functions.
// This parameter can be NULL.
MH_STATUS WINAPI MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal, LPVOID *ppTarget);
// Removes an already created hook.
// Parameters:
// pTarget [in] A pointer to the target function.
MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget);
// Enables an already created hook.
// Parameters:
// pTarget [in] A pointer to the target function.
// If this parameter is MH_ALL_HOOKS, all created hooks are
// enabled in one go.
MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget);
// Disables an already created hook.
// Parameters:
// pTarget [in] A pointer to the target function.
// If this parameter is MH_ALL_HOOKS, all created hooks are
// disabled in one go.
MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget);
// Queues to enable an already created hook.
// Parameters:
// pTarget [in] A pointer to the target function.
// If this parameter is MH_ALL_HOOKS, all created hooks are
// queued to be enabled.
MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget);
// Queues to disable an already created hook.
// Parameters:
// pTarget [in] A pointer to the target function.
// If this parameter is MH_ALL_HOOKS, all created hooks are
// queued to be disabled.
MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget);
// Applies all queued changes in one go.
MH_STATUS WINAPI MH_ApplyQueued(VOID);
// Translates the MH_STATUS to its name as a string.
const char * WINAPI MH_StatusToString(MH_STATUS status);
#ifdef __cplusplus
}
#endif
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Evan Pezent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+166
View File
@@ -0,0 +1,166 @@
# ImPlot
ImPlot is an immediate mode, GPU accelerated plotting library for [Dear ImGui](https://github.com/ocornut/imgui). It aims to provide a first-class API that ImGui fans will love. ImPlot is well suited for visualizing program data in real-time or creating interactive plots, and requires minimal code to integrate. Just like ImGui, it does not burden the end user with GUI state management, avoids STL containers and C++ headers, and has no external dependencies except for ImGui itself.
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/controls.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/dnd.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/pie.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/query.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/bars.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/rt.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/stem.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/markers.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/shaded.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/candle.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/heat.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/tables.gif" width="270">
## Features
- GPU accelerated rendering
- multiple plot types:
- line plots
- shaded plots
- scatter plots
- vertical/horizontal/stacked bars graphs
- vertical/horizontal error bars
- stem plots
- stair plots
- pie charts
- heatmap charts
- 1D/2D histograms
- images
- and more likely to come
- mix/match multiple plot items on a single plot
- configurable axes ranges and scaling (linear/log)
- subplots
- time formatted x-axes (US formatted or ISO 8601)
- reversible and lockable axes
- multiple x-axes and y-axes
- controls for zooming, panning, box selection, and auto-fitting data
- controls for creating persistent query ranges (see demo)
- several plot styling options: 10 marker types, adjustable marker sizes, line weights, outline colors, fill colors, etc.
- 16 built-in colormaps and support for and user-added colormaps
- optional plot titles, axis labels, and grid labels
- optional and configurable legends with toggle buttons to quickly show/hide plot items
- default styling based on current ImGui theme, or completely custom plot styles
- customizable data getters and data striding (just like ImGui:PlotLine)
- accepts data as float, double, and 8, 16, 32, and 64-bit signed/unsigned integral types
- and more! (see Announcements [2020](https://github.com/epezent/implot/issues/48)/[2021](https://github.com/epezent/implot/issues/168))
## Usage
The API is used just like any other ImGui `BeginX`/`EndX` pair. First, start a new plot with `ImPlot::BeginPlot()`. Next, plot as many items as you want with the provided `PlotX` functions (e.g. `PlotLine()`, `PlotBars()`, `PlotScatter()`, etc). Finally, wrap things up with a call to `ImPlot::EndPlot()`. That's it!
```cpp
int bar_data[11] = ...;
float x_data[1000] = ...;
float y_data[1000] = ...;
ImGui::Begin("My Window");
if (ImPlot::BeginPlot("My Plot")) {
ImPlot::PlotBars("My Bar Plot", bar_data, 11);
ImPlot::PlotLine("My Line Plot", x_data, y_data, 1000);
...
ImPlot::EndPlot();
}
ImGui::End();
```
![Usage](https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/example.PNG)
Of course, there's much more you can do with ImPlot...
## Demos
A comprehensive example of ImPlot's features can be found in `implot_demo.h`. Add this file to your sources and call `ImPlot::ShowDemoWindow()` somewhere in your update loop. You are encouraged to use this file as a reference when needing to implement various plot types. The demo is always updated to show new plot types and features as they are added, so check back with each release!
An online version of the demo is hosted [here](https://traineq.org/implot_demo/src/implot_demo.html). You can view the plots and the source code that generated them. Note that this demo may not always be up to date and is not as performant as a desktop implementation, but it should give you a general taste of what's possible with ImPlot. Special thanks to [pthom](https://github.com/pthom) for creating and hosting this!
More sophisticated demos requiring lengthier code and/or third-party libraries can be found in a separate repository: [implot_demos](https://github.com/epezent/implot_demos). Here, you will find advanced signal processing and ImPlot usage in action. Please read the `Contributing` section of that repository if you have an idea for a new demo!
## Integration
0) Set up an [ImGui](https://github.com/ocornut/imgui) environment if you don't already have one.
1) Add `implot.h`, `implot_internal.h`, `implot.cpp`, `implot_items.cpp` and optionally `implot_demo.cpp` to your sources. Alternatively, you can get ImPlot using [vcpkg](https://github.com/microsoft/vcpkg/tree/master/ports/implot).
2) Create and destroy an `ImPlotContext` wherever you do so for your `ImGuiContext`:
```cpp
ImGui::CreateContext();
ImPlot::CreateContext();
...
ImPlot::DestroyContext();
ImGui::DestroyContext();
```
You should be good to go!
If you want to test ImPlot quickly, consider trying [mahi-gui](https://github.com/mahilab/mahi-gui), which bundles ImGui, ImPlot, and several other packages for you.
## Extremely Important Note
Dear ImGui uses **16-bit indexing by default**, so high-density ImPlot widgets like `ImPlot::PlotHeatmap()` may produce too many vertices into `ImDrawList`, which causes an assertion failure and will result in data truncation and/or visual glitches. Therefore, it is **HIGHLY** recommended that you EITHER:
- **Option 1:** Enable 32-bit indices by uncommenting `#define ImDrawIdx unsigned int` in your ImGui [`imconfig.h`](https://github.com/ocornut/imgui/blob/master/imconfig.h#L89) file.
- **Option 2:** Handle the `ImGuiBackendFlags_RendererHasVtxOffset` flag in your renderer if you must use 16-bit indices. Many of the default ImGui rendering backends already support `ImGuiBackendFlags_RendererHasVtxOffset`. Refer to [this issue](https://github.com/ocornut/imgui/issues/2591) for more information.
## FAQ
**Q: Why?**
A: ImGui is an incredibly powerful tool for rapid prototyping and development, but provides only limited mechanisms for data visualization. Two dimensional plots are ubiquitous and useful to almost any application. Being able to visualize your data in real-time will give you insight and better understanding of your application.
**Q: Is ImPlot the right plotting library for me?**
A: If you're looking to generate publication quality plots and/or export plots to a file, ImPlot is NOT the library for you! ImPlot is geared toward plotting application data at realtime speeds with high levels of interactivity. ImPlot does its best to create pretty plots (indeed, there are quite a few styling options available), but it will always favor function over form.
**Q: Where is the documentation?**
A: The API is thoroughly commented in `implot.h`, and the demo in `implot_demo.cpp` should be more than enough to get you started. Also take a look at the [implot_demos](https://github.com/epezent/implot_demos) repository.
**Q: Is ImPlot suitable for plotting large datasets?**
A: Yes, within reason. You can plot tens to hundreds of thousands of points without issue, but don't expect millions to be a buttery smooth experience. That said, you can always downsample extremely large datasets by telling ImPlot to stride your data at larger intervals if needed. Also try the experimental `backends` branch which aims to provide GPU acceleration support.
**Q: What data types can I plot?**
A: ImPlot plotting functions accept most scalar types:
`float`, `double`, `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64`. Arrays of custom structs or classes (e.g. `Vector2f` or similar) are easily passed to ImPlot functions using the built in striding features (see `implot.h` for documentation), and many plotters provide a "getter" overload which accepts data generating callbacks.
**Q: Can plot styles be modified?**
A: Yes. Data colormaps and various styling colors and variables can be pushed/popped or modified permanently on startup. Three default styles are available, as well as an automatic style that attempts to match you ImGui style.
**Q: Does ImPlot support logarithmic scaling or time formatting?**
A: Yep! Both logscale and timescale are supported.
**Q: Does ImPlot support multiple y-axes? x-axes?**
A: Yes. Up to three x-axes and three y-axes can be enabled.
**Q: Does ImPlot support [insert plot type]?**
A: Maybe. Check the demo, gallery, or Announcements ([2020](https://github.com/epezent/implot/issues/48)/[2021](https://github.com/epezent/implot/issues/168))to see if your desired plot type is shown. If not, consider submitting an issue or better yet, a PR!
**Q: Does ImPlot support 3D plots?**
A: No, and likely never will since ImGui only deals in 2D rendering.
**Q: My plot lines look like crap!**
A: By default, no anti-aliasing is done on line plots for performance gains. If you use at least 4x MSAA, then you likely won't even notice. However, you can enable software AA per-plot with the `ImPlotFlags_AntiAliased` flag, or globally with `ImPlot::GetStyle().AntiAliasedLines = true;`.
**Q: Does ImPlot provide analytic tools?**
A: Not exactly, but it does give you the ability to query plot sub-ranges, with which you can process your data however you like.
**Q: Can plots be exported/saved to image?**
A: Not currently. Use your OS's screen capturing mechanisms if you need to capture a plot. ImPlot is not suitable for rendering publication quality plots; it is only intended to be used as a visualization tool. Post-process your data with MATLAB or matplotlib for these purposes.
**Q: Can I compile ImPlot as a dynamic library?**
A: Like ImGui, it is recommended that you compile and link ImPlot as a *static* library or directly as a part of your sources. However, if you must and are compiling ImPlot and ImGui as separate DLLs, make sure you set the current *ImGui* context with `ImPlot::SetImGuiContext(ImGuiContext* ctx)`. This ensures that global ImGui variables are correctly shared across the DLL boundary.
**Q: Can ImPlot be used with other languages/bindings?**
A: Yes, you can use the generated C binding, [cimplot](https://github.com/cimgui/cimplot) with most high level languages. [DearPyGui](https://github.com/hoffstadt/DearPyGui) provides a Python wrapper, among other things. [imgui-java](https://github.com/SpaiR/imgui-java) provides bindings for Java. A Rust binding, [implot-rs](https://github.com/4bb4/implot-rs), is currently in the works. An example using Emscripten can be found [here](https://github.com/pthom/implot_demo).
+69
View File
@@ -0,0 +1,69 @@
The list below represents a combination of high-priority work, nice-to-have features, and random ideas. We make no guarantees that all of this work will be completed or even started. If you see something that you need or would like to have, let us know, or better yet consider submitting a PR for the feature.
## API
- add shortcut/legacy overloads for BeginPlot
## Axes
- add flag to remove weekends on Time axis
- pixel space scale (`ImPlotTransform_Display`), normalized space scale (`ImPlotTransform_Axes`), data space scale (`ImPloTransform_Data`)
- make ImPlotFlags_Equal not a flag -> `SetupEqual(ImPlotAxis x, ImPlotAxis y)`
- allow inverted arguments `SetAxes` to transpose data?
- `SetupAxisColors()`
- `SetupAxisConstraints()`
- `SetupAxisHome()`
## Plot Items
- add `ImPlotLineFlags`, `ImPlotBarsFlags`, etc. for each plot type
- add `PlotBarGroups` wrapper that makes rendering groups of bars easier, with stacked bar support
- add `PlotBubbles` (see MATLAB bubble chart)
- add non-zero references for `PlotBars` etc.
- add exploding to `PlotPieChart` (on hover-highlight?)
## Styling
- support gradient and/or colormap sampled fills (e.g. ImPlotFillStyle_)
- add hover/active color for plot axes
- API for setting different fonts for plot elements
## Colormaps
- gradient editing tool
- `RemoveColormap`
## Legend
- `ImPlotLegendFlags_SortItems`
- `ImPlotLegendFlags_Scroll`
- improve legend icons (e.g. adopt markers, gradients, etc)
- make legend frame use ButtonBehavior (maybe impossible)
## Tools / Misc.
- add `IsPlotChanging` to detect change in limits
- add ability to extend plot/axis context menus
- add LTTB downsampling for lines
- add box selection to axes
- first frame render delay might fix "fit pop" effect
- move some code to new `implot_tools.cpp`
## Optimizations
- find faster way to buffer data into ImDrawList (very slow)
- reduce number of calls to `PushClipRect`
- explore SIMD operations for high density plot items
## Completed
- make BeginPlot take fewer args:
- make query a tool -> `DragRect`
- rework DragLine/Point to use ButtonBehavior
- add support for multiple x-axes and don't limit count to 3
- make axis side configurable (top/left, right/bottom) via new flag `ImPlotAxisFlags_Opposite`
- add support for setting tick label strings via callback
- give each axis an ID, remove ad-hoc DND solution
- allow axis to be drag to opposite side (ala ImGui Table headers)
- legend items can be hovered even if plot is not
- fix frame delay on DragX tools
- remove tag from drag line/point -> add `Tag` tool
File diff suppressed because it is too large. Load diff
+1143
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Submodule
+1
Submodule lib/minhook added at e33ec5abc0.