Compare commits

...

125 Commits

Author SHA1 Message Date
Eiren Rain
6c27186ce9 Make GUI less garbage (still gabage, but less) 2021-08-26 11:57:35 +03:00
Eiren Rain
74c25c2ca3 Don't use source port to id trackers 2021-08-23 16:39:43 +03:00
Eiren Rain
91ee6ff6c0 Merge pull request #35 from adigyran/patch-6
Update README.md
2021-08-22 15:59:17 +03:00
Yury
05ba866bef Update README.md
new build command
2021-08-22 15:55:38 +03:00
Eiren Rain
af3aab86dc Don't crash on pipe errors, just log them 2021-08-22 15:03:11 +03:00
Eiren Rain
4370defb69 Merge pull request #34 from adigyran/main
Gradle shadow plugin for dependency resolving
2021-08-22 14:50:20 +03:00
Eiren Rain
a105879c9a Supress config file not found error to create less confusion 2021-08-22 14:46:55 +03:00
Eiren Rain
9383be678c Don't parse some packets when paired with owoTrack #33 2021-08-22 14:46:35 +03:00
Eiren Rain
7c8a394147 Handle HMD pipe better 2021-08-22 14:43:27 +03:00
Eiren Rain
ffc8a9dae4 Remove new spine model from the main branch 2021-08-22 14:36:35 +03:00
adigyran
bb4a65882d Gradle shadow plugin. Based on Kitlith change of build.gradle. Using now library for Slime Commons dependency resolving. Changed gradle.yml accordingly. It produces slimevr.jar file 2021-08-22 13:44:26 +03:00
Yury
5ebbb907e7 Update gradle.yml
fix resolving
2021-08-22 13:39:20 +03:00
Yury
2ba66d7f91 Update .github/workflows/gradle.yml
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
2021-08-22 13:38:28 +03:00
Yury
7f8fe9e4f4 Update .github/workflows/gradle.yml
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
2021-08-22 13:38:22 +03:00
Yury
12292070ce Update .github/workflows/gradle.yml
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
2021-08-22 13:34:46 +03:00
adigyran
8bc2b72ab0 readme fix for new command 2021-08-22 13:32:20 +03:00
adigyran
208ae6b6d6 reformat code 2021-08-22 13:21:20 +03:00
adigyran
ba8121a8a7 Gradle shadow plugin. Based on Kitlith change of build.gradle. Using now library for Slime Commons dependency resolving. Changed gradle.yml accordingly. It produces slimevr.jar file 2021-08-22 13:15:22 +03:00
Eiren Rain
c8da0427f9 Added simple test extended spine model, SlimeVR/SlimeVR-Server#31 2021-08-20 17:05:15 +03:00
Eiren Rain
fed13e8fda Change name of steamvr trackers to legs instead of feet to make it less confusing 2021-08-20 15:32:53 +03:00
Eiren Rain
e3b977c636 Merge pull request #29 from ButterscotchVanilla/main
Add AutoBone, a skeleton auto-configuration algorithm
2021-08-20 15:30:33 +03:00
ButterscotchVanilla
337912f3d0 AutoBone: Optimize PoseRecorder tick 2021-08-19 22:46:41 -04:00
ButterscotchVanilla
3b61f88343 AutoBone: Add save recording button & enable buttons as needed 2021-08-19 18:14:57 -04:00
Eiren Rain
5f6a6ba1c5 Actually start steamvr input bridge 2021-08-20 00:11:25 +03:00
Eiren Rain
bb29844101 Recieve tracker role from SteamVR for input trackers 2021-08-19 23:03:07 +03:00
Eiren Rain
5600d95684 Add OCCLUDED status to the tracker status for future usage 2021-08-19 15:48:51 +03:00
Eiren Rain
45ba341ccf Added new pipe to read basic tracking data from feeder app 2021-08-19 15:43:49 +03:00
ButterscotchVanilla
7992526d2d AutoBone: Rename PoseRecordIO to PoseFrameIO and separate recording load into a method 2021-08-19 04:29:14 -04:00
ButterscotchVanilla
9a6cb23659 AutoBone: Add support for absolute positions 2021-08-19 04:11:23 -04:00
ButterscotchVanilla
bc132b7757 AutoBone: Thow NullPointerException for missing frames 2021-08-19 02:38:18 -04:00
ButterscotchVanilla
b05d726ad0 AutoBone: Return recording ASAP and check if it's empty 2021-08-19 01:54:28 -04:00
ButterscotchVanilla
a7a612aa9b AutoBone: Add recording cancellation, always check if the recording is done and not submitted 2021-08-19 00:52:09 -04:00
ButterscotchVanilla
32a29c8bc7 AutoBone: Add new dedicated AutoBone window 2021-08-18 23:59:12 -04:00
ButterscotchVanilla
23a3babf33 AutoBone: Fix recording 2021-08-17 16:28:13 -04:00
ButterscotchVanilla
3d90f0b284 AutoBone: Add proportion error 2021-08-17 04:12:21 -04:00
ButterscotchVanilla
1e6448c61f AutoBone: Let's pretend this didn't get committed 2021-08-17 00:10:08 -04:00
ButterscotchVanilla
a1f709ca12 AutoBone: Add unused configs to staticConfigs and split error function 2021-08-16 23:55:20 -04:00
ButterscotchVanilla
a8ca2fd6e6 AutoBone: Use abs dist for foot offset error, use total length again, and remove hips 2021-08-16 19:37:13 -04:00
ButterscotchVanilla
f835eeecdd AutoBone: Use error derivative and add more foot offsets 2021-08-16 18:14:17 -04:00
ButterscotchVanilla
70f5228d1c AutoBone: Remove head offset, remove totalLength 2021-08-16 18:14:17 -04:00
ButterscotchVanilla
89e2ea610a AutoBone: Automatically update node positions 2021-08-16 18:14:16 -04:00
ButterscotchVanilla
6b68a983a5 AutoBone: Restructure processFrames and remove unused code 2021-08-16 18:14:16 -04:00
ButterscotchVanilla
4a2878b92e AutoBone: Separate pose recorder from AutoBone & save multiple recordings 2021-08-16 18:14:16 -04:00
ButterscotchVanilla
4f8165c8e1 Set gradle compiler encoding to UTF-8 2021-08-16 18:14:15 -04:00
ButterscotchVanilla
855d15cec5 AutoBone: Fix configs not updating when AutoBone is run 2021-08-16 18:14:15 -04:00
ButterscotchVanilla
e1d17f61c4 AutoBone: Properly handle ratio output 2021-08-16 18:14:15 -04:00
ButterscotchVanilla
380ae27762 AutoBone: Support no chest tracker 2021-08-16 18:14:15 -04:00
ButterscotchVanilla
4775dcd57a AutoBone: Add more configs, fix recording reading 2021-08-16 18:14:14 -04:00
ButterscotchVanilla
807ccc69ce AutoBone: Print file name before processing frames 2021-08-16 18:14:14 -04:00
ButterscotchVanilla
aaee64ce02 AutoBone: Add stabilization, more fine-tuning as usual 2021-08-16 18:14:13 -04:00
ButterscotchVanilla
294141e223 AutoBone: Oops 2021-08-16 18:14:13 -04:00
ButterscotchVanilla
e3b125f244 AutoBone: Add bulk recording loading, add height diff stat 2021-08-16 18:14:13 -04:00
ButterscotchVanilla
7fd3297fed AutoBone: Fix error function, add error derivative, consider positive and negative equally, etc 2021-08-16 18:14:12 -04:00
ButterscotchVanilla
a2f54f67a3 AutoBone: Update GUI values after adjustment 2021-08-16 18:14:12 -04:00
Butterscotch!
d77724a911 Change CI to build on any branch 2021-08-16 18:14:12 -04:00
ButterscotchVanilla
1dc05ba196 AutoBone: Save configs without needing to have a skeleton 2021-08-16 18:14:11 -04:00
ButterscotchVanilla
cd7d4d102b AutoBone: Add config input for recording and adjustment values 2021-08-16 18:14:11 -04:00
ButterscotchVanilla
0ba2450152 AutoBone: Fine-tune chest-waist and leg-waist ratios 2021-08-16 18:14:10 -04:00
ButterscotchVanilla
eee7d67591 AutoBone: Allow manual target height value 2021-08-16 18:14:10 -04:00
ButterscotchVanilla
760dbfa5b9 AutoBone: Load multiple recordings, fine-tune values and extract ratios, fix restricted values from getting stuck 2021-08-16 18:14:10 -04:00
ButterscotchVanilla
a52384de2e AutoBone: This decreases error magically? Fine-tune leg to body ratio range 2021-08-16 18:14:09 -04:00
ButterscotchVanilla
0dab8f0c94 AutoBone: Fix grammar to be clearer 2021-08-16 18:14:09 -04:00
ButterscotchVanilla
629984c792 AutoBone: Remove feet from skeleton, read from AutoBone configs, and make skeletons local 2021-08-16 18:14:09 -04:00
ButterscotchVanilla
707e4c6dde AutoBone: Auto-detect height, add more restrains, fine-tuning adjustment values 2021-08-16 18:14:08 -04:00
ButterscotchVanilla
efbe409399 AutoBone: Only allow one AutoBone thread 2021-08-16 18:14:08 -04:00
ButterscotchVanilla
faf0be6c53 AutoBone: Fine-tune algorithm and error function, apply results to skeleton 2021-08-16 18:14:08 -04:00
ButterscotchVanilla
1a078993f3 AutoBone: Simplify length adjustment code 2021-08-16 18:14:07 -04:00
ButterscotchVanilla
e0ac3bb853 AutoBone: Make PoseRecordIO static and add height to algorithm error 2021-08-16 18:14:07 -04:00
ButterscotchVanilla
c6cd13d9cd AutoBone: Add data distance controls to control amount of context between poses 2021-08-16 18:14:07 -04:00
ButterscotchVanilla
ef88e2e4a9 AutoBone: Modify error function, add average error logs, and add tuning variables 2021-08-16 18:14:06 -04:00
ButterscotchVanilla
d9bcc39ee6 AutoBone: Disable feet 2021-08-16 18:14:06 -04:00
ButterscotchVanilla
84f4a47df1 AutoBone: Load recordings from "ABRecording_Load.abf" 2021-08-16 18:14:06 -04:00
ButterscotchVanilla
1408a5c357 AutoBone: Use skeleton properly and update on tick & add a timer for sampling 2021-08-16 18:14:05 -04:00
ButterscotchVanilla
202b15e8a8 Specify Java 8 compatibility 2021-08-16 18:14:05 -04:00
ButterscotchVanilla
110554a180 AutoBone: Add recording export to process 2021-08-16 18:14:04 -04:00
ButterscotchVanilla
90e3715426 AutoBone: Add serialization/deserialization of recording 2021-08-16 18:14:04 -04:00
ButterscotchVanilla
644fee2d1f AutoBone: Make auto-adjustment wait for recording to finish 2021-08-16 18:14:04 -04:00
ButterscotchVanilla
c163effe60 AutoBone: Add test button 2021-08-16 18:14:03 -04:00
ButterscotchVanilla
0a39c746a3 AutoBone: Add frame recording interval 2021-08-16 18:14:03 -04:00
ButterscotchVanilla
2f46b3ff58 AutoBone: Move configs to HashMap and finish implementing adjustment 2021-08-16 18:14:02 -04:00
ButterscotchVanilla
d35760d3a2 AutoBone: Add basic PoseFrame recording and start processing loop 2021-08-16 18:14:02 -04:00
ButterscotchVanilla
19a1101b43 AutoBone: Add AutoBone, PoseFrame, and finish implementing SimpleSkeleton 2021-08-16 18:14:01 -04:00
ButterscotchVanilla
8b209eaf27 AutoBone: Add node HashMap 2021-08-16 18:14:01 -04:00
ButterscotchVanilla
fc6f7d3004 AutoBone: Add config setting/saving 2021-08-16 18:14:01 -04:00
ButterscotchVanilla
1abab9f92d AutoBone: Add basic skeleton initialization 2021-08-16 18:14:01 -04:00
Eiren Rain
c3b50983e3 Make GUI updates less frequent and save some CPU usage 2021-08-14 18:48:45 +03:00
Eiren Rain
a0857090a0 Minor changes 2021-08-13 22:05:03 +03:00
Eiren Rain
1ce9be3ed3 Merge pull request #23 from ButterscotchVanilla/slime-ci-patch
Separate CI test and build into jobs
2021-08-12 12:18:58 +03:00
Butterscotch!
11d461380d Separate CI test and build into jobs 2021-08-12 05:14:16 -04:00
Eiren Rain
6c0eb07c0b Merge pull request #21 from ButterscotchVanilla/editorconfig
Create .editorconfig
2021-08-10 17:43:56 +03:00
ButterscotchVanilla
fb9ae3e78c Create .editorconfig 2021-08-10 10:38:58 -04:00
Eiren Rain
52f59fbfb3 Fixed typo 2021-08-10 16:51:42 +03:00
Eiren Rain
4a59017269 Added important comment 2021-08-10 09:20:12 +03:00
Eiren Rain
5c6d02de30 Parse handshake properly 2021-08-10 08:58:57 +03:00
Eiren Rain
83b0e78b9e Recieve correction and tap data, currently not used
Fix some typos
2021-08-10 08:31:03 +03:00
Eiren Rain
ac192e2416 Display magentometer calibration separately 2021-08-10 07:26:32 +03:00
Eiren Rain
52932d63d3 Display data from new protocol and firmware version, including calibration statis and magentometer correction 2021-08-09 17:46:00 +03:00
Eiren Rain
6a45e5d32c Another attempt at new knee model
Waist rotation now takes into account knee model
2021-08-08 08:04:28 +03:00
Eiren Rain
6f09598243 Added toggle for extended pelvis model
Extended aknle model is added but not used
Added 3-tracker rudementary support for people that want to hurt themselves
Fixed typos
2021-08-05 07:50:49 +03:00
Eiren Rain
467e79d1c0 Added Fast Reset button that resets only Yaw of the trackers 2021-08-04 07:48:54 +03:00
Eiren Rain
fa66c94ec3 Implemented new pelvis model as average between legs rotation
SlimeVR/SlimeVR-Server#9
2021-08-04 01:03:58 +03:00
Eiren Rain
2b4ce4b920 Fix version number 2021-08-02 23:08:22 +03:00
Eiren Rain
4e7585b87e Added support for different SteamVR trackers configuration 2021-08-02 23:07:25 +03:00
Eiren Rain
de13db4627 Fix right foot wasn't resetting 2021-07-31 03:05:08 +03:00
Eiren Rain
ca8ceb428b Set version to 0.0.12 2021-07-26 02:01:18 +03:00
Eiren Rain
c18597387a Merge pull request #20 from adigyran/patch-5
Update build.gradle
2021-07-26 02:00:25 +03:00
Yury
962504b788 Update build.gradle
useJUnitPlatform
2021-07-26 01:27:15 +03:00
Eiren Rain
8d1886d045 Display raw tracker data in degrees not in quats 2021-07-26 00:55:01 +03:00
Eiren Rain
1c5167bb7c Another tracker adjustment fix, doesn't pass all tests, but works better 2021-07-26 00:48:34 +03:00
Eiren Rain
e248cca4e7 Adjustmed trackers pass all tests 2021-07-25 23:04:34 +03:00
Eiren Rain
89ee97872d Streams go brrrr in unit tests 2021-07-25 22:45:54 +03:00
Eiren Rain
b22a2368d4 Refactor tests, generate tests for each angle dynamically, separate 3 test types 2021-07-25 22:19:04 +03:00
Eiren Rain
9ecfc57e44 Use JUnit 5 framework for testing 2021-07-25 20:57:11 +03:00
Eiren Rain
cd141258c5 Merge pull request #19 from adigyran/patch-4
Update build.gradle
2021-07-23 17:51:47 +03:00
Yury
5dc027a9e2 Update build.gradle
fix gradle dependencies
2021-07-23 17:46:29 +03:00
Eiren Rain
3e55b0e417 Merge pull request #18 from adigyran/patch-3
Update README.md
2021-07-22 12:36:20 +03:00
Eiren Rain
9ca6b21c61 Merge pull request #17 from ButterscotchVanilla/main
Automatically detect and set the Slime Java Commons subproject location
2021-07-22 12:36:01 +03:00
Yury
8ec528d4a0 Update README.md
formatting, thanks Butterscotch for some changes
2021-07-22 02:56:25 +03:00
Yury
961946bd29 Update README.md
formatting
2021-07-22 02:53:35 +03:00
Yury
da5fc860cf Update README.md
formatting
2021-07-22 02:50:54 +03:00
Yury
fdd39c4010 Update README.md
How to build instructions, this is for ButterscotchVanilla's PR  #17
2021-07-22 02:46:39 +03:00
ButterscotchVanilla
900e96a3a6 Announce subproject location before setting it 2021-07-21 17:39:45 -04:00
ButterscotchVanilla
6a9f42f126 Auto-detect Slime Java Commons subproject location 2021-07-21 17:35:09 -04:00
ButterscotchVanilla
72ea196359 Update gradle.yml 2021-07-21 17:11:01 -04:00
ButterscotchVanilla
90a8abeed2 Add comments to build.gradle and add path to subproject 2021-07-21 17:07:16 -04:00
39 changed files with 3206 additions and 858 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# This file is for unifying the coding style for different editors and IDEs
# See editorconfig.org
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -1,48 +1,67 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Build SlimeVR Server with Gradle
name: SlimeVR Server
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on: [ push, pull_request ]
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- name: Clone Slime Java Commons
uses: actions/checkout@v2.3.4
with:
repository: Eirenliel/slime-java-commons
# Relative path under $GITHUB_WORKSPACE to place the repository
path: Slime Java Commons
- name: Set up JDK 11
uses: actions/setup-java@v2.1.0
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew clean test
- name: Build with Gradle
run: ./gradlew clean serverJar
- name: Upload the Server JAR as a Build Artifact
uses: actions/upload-artifact@v2.2.4
with:
# Artifact name
name: "SlimeVR-Server" # optional, default is artifact
# A file, directory or wildcard pattern that describes what to upload
path: build/libs/*
- uses: actions/checkout@v2.3.4
- name: Clone Slime Java Commons
uses: actions/checkout@v2.3.4
with:
repository: Eirenliel/slime-java-commons
# Relative path under $GITHUB_WORKSPACE to place the repository
path: Slime Java Commons
- name: Set up JDK 8
uses: actions/setup-java@v2.1.0
with:
java-version: '8'
distribution: 'adopt'
cache: 'gradle' # will restore cache of dependencies and wrappers
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew clean test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- name: Clone Slime Java Commons
uses: actions/checkout@v2.3.4
with:
repository: Eirenliel/slime-java-commons
# Relative path under $GITHUB_WORKSPACE to place the repository
path: Slime Java Commons
- name: Set up JDK 8
uses: actions/setup-java@v2.1.0
with:
java-version: '8'
distribution: 'adopt'
cache: 'gradle' # will restore cache of dependencies and wrappers
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean shadowJar
- name: Upload the Server JAR as a Build Artifact
uses: actions/upload-artifact@v2.2.4
with:
# Artifact name
name: "SlimeVR-Server" # optional, default is artifact
# A file, directory or wildcard pattern that describes what to upload
path: build/libs/*

View File

@@ -14,3 +14,21 @@ Integrations:
## How to use
Latest instructions are currently [here](https://gist.github.com/Eirenliel/8c0eefcdbda1076d5c2e1bf634831d20). Will be updated and republished as time goes on.
## How to build
You need to execute these commands in the folder where you want this project.
```bash
# Clone repositories
git clone https://github.com/SlimeVR/SlimeVR-Server.git
git clone https://github.com/Eirenliel/slime-java-commons.git
# Enter the directory and build the runnable server JAR
cd SlimeVR-Server
gradlew shadowJar
```
Open Slime VR Server project in Eclipse or Intellij Idea
run gradle command `shadowJar` to build a runnable server JAR

View File

@@ -7,44 +7,64 @@
*/
plugins {
// Apply the java-library plugin to add support for Java Library
id 'java-library'
id 'application'
id "com.github.johnrengelman.shadow" version "6.1.0"
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
// Set compiler to use UTF-8
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'
javadoc.options.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
tasks.withType(Test) {
systemProperty('file.encoding', 'UTF-8')
}
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
}
repositories {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
jcenter()
mavenCentral()
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
jcenter()
mavenCentral()
}
dependencies {
compile project(':Slime Java Commons')
// This dependency is exported to consumers, that is to say found on their compile classpath.
api 'org.apache.commons:commons-math3:3.6.1'
api 'org.yaml:snakeyaml:1.25'
api 'net.java.dev.jna:jna:5.6.0'
api 'net.java.dev.jna:jna-platform:5.6.0'
api 'com.illposed.osc:javaosc-core:0.8'
api 'com.fazecast:jSerialComm:[2.0.0,3.0.0)'
// This dependency is exported to consumers, that is to say found on their compile classpath.
compile 'org.apache.commons:commons-math3:3.6.1'
compile 'org.yaml:snakeyaml:1.25'
compile 'net.java.dev.jna:jna:5.6.0'
compile 'net.java.dev.jna:jna-platform:5.6.0'
compile 'com.illposed.osc:javaosc-core:0.8'
compile 'com.fazecast:jSerialComm:[2.0.0,3.0.0)'
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
implementation 'com.google.guava:guava:28.2-jre'
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
implementation 'com.google.guava:guava:28.2-jre'
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
// Use JUnit test framework
testImplementation platform('org.junit:junit-bom:5.7.2')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
subprojects.each { subproject -> evaluationDependsOn(subproject.path) }
task serverJar (type: Jar, dependsOn: subprojects.tasks['build']) {
manifest {
attributes 'Main-Class': 'io.eiren.vr.Main'
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
with jar
shadowJar {
archiveBaseName.set('slimevr')
archiveClassifier.set('')
archiveVersion.set('')
}
application {
mainClassName = 'io.eiren.vr.Main'
}

View File

@@ -8,4 +8,19 @@
*/
rootProject.name = 'SlimeVR Server'
include('Slime Java Commons')
include('Slime Java Commons')
def commonsDirs = [
new File(settingsDir, 'Slime Java Commons'),
new File(settingsDir, 'slime-java-commons'),
new File(settingsDir, '../Slime Java Commons'),
new File(settingsDir, '../slime-java-commons')
]
for (commonsDir in commonsDirs) {
if (commonsDir.isDirectory()) {
logger.info('\"Slime Java Commons\" subproject detected at \"{}\"', commonsDir.getCanonicalPath())
project(':Slime Java Commons').projectDir = commonsDir
break
}
}

View File

@@ -1,50 +1,117 @@
package io.eiren.gui;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.event.MouseInputAdapter;
import io.eiren.gui.autobone.AutoBoneWindow;
import io.eiren.util.StringUtils;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.HumanSkeletonWithLegs;
import io.eiren.vr.processor.HumanSkeleton;
public class SkeletonConfig extends EJBag {
private final VRServer server;
private final VRServerGUI gui;
private final AutoBoneWindow autoBone;
private Map<String, SkeletonLabel> labels = new HashMap<>();
public SkeletonConfig(VRServer server, VRServerGUI gui) {
super();
this.server = server;
this.gui = gui;
this.autoBone = new AutoBoneWindow(server, this);
setAlignmentY(TOP_ALIGNMENT);
server.humanPoseProcessor.addSkeletonUpdatedCallback(this::skeletonUpdated);
skeletonUpdated(null);
}
@ThreadSafe
public void skeletonUpdated(HumanSkeleton newSkeleton) {
java.awt.EventQueue.invokeLater(() -> {
removeAll();
int row = 0;
add(new TimedResetButton("Reset All", "All"), s(c(1, row, 1), 3, 1));
add(new JCheckBox("Extended pelvis model") {{
addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED) {//checkbox has been selected
if(newSkeleton != null && newSkeleton instanceof HumanSkeletonWithLegs) {
HumanSkeletonWithLegs hswl = (HumanSkeletonWithLegs) newSkeleton;
hswl.setSkeletonConfigBoolean("Extended pelvis model", true);
}
} else {
if(newSkeleton != null && newSkeleton instanceof HumanSkeletonWithLegs) {
HumanSkeletonWithLegs hswl = (HumanSkeletonWithLegs) newSkeleton;
hswl.setSkeletonConfigBoolean("Extended pelvis model", false);
}
}
}
});
if(newSkeleton != null && newSkeleton instanceof HumanSkeletonWithLegs) {
HumanSkeletonWithLegs hswl = (HumanSkeletonWithLegs) newSkeleton;
setSelected(hswl.getSkeletonConfigBoolean("Extended pelvis model"));
}
}}, s(c(0, row, 1), 3, 1));
row++;
/*
add(new JCheckBox("Extended knee model") {{
addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED) {//checkbox has been selected
if(newSkeleton != null && newSkeleton instanceof HumanSkeletonWithLegs) {
HumanSkeletonWithLegs hswl = (HumanSkeletonWithLegs) newSkeleton;
hswl.setSkeletonConfigBoolean("Extended knee model", true);
}
} else {
if(newSkeleton != null && newSkeleton instanceof HumanSkeletonWithLegs) {
HumanSkeletonWithLegs hswl = (HumanSkeletonWithLegs) newSkeleton;
hswl.setSkeletonConfigBoolean("Extended knee model", false);
}
}
}
});
if(newSkeleton != null && newSkeleton instanceof HumanSkeletonWithLegs) {
HumanSkeletonWithLegs hswl = (HumanSkeletonWithLegs) newSkeleton;
setSelected(hswl.getSkeletonConfigBoolean("Extended knee model"));
}
}}, s(c(0, row, 1), 3, 1));
row++;
//*/
add(new TimedResetButton("Reset All", "All"), s(c(1, row, 1), 3, 1));
add(new JButton("Auto") {{
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
autoBone.setVisible(true);
autoBone.toFront();
}
});
}}, s(c(4, row, 1), 3, 1));
row++;
add(new JLabel("Chest"), c(0, row, 1));
add(new AdjButton("+", "Chest", 0.01f), c(1, row, 1));
add(new SkeletonLabel("Chest"), c(2, row, 1));
add(new AdjButton("-", "Chest", -0.01f), c(3, row, 1));
add(new ResetButton("Reset", "Chest"), c(4, row, 1));
row++;
add(new JLabel("Waist"), c(0, row, 1));
add(new AdjButton("+", "Waist", 0.01f), c(1, row, 1));
add(new SkeletonLabel("Waist"), c(2, row, 1));
@@ -93,25 +160,34 @@ public class SkeletonConfig extends EJBag {
add(new AdjButton("-", "Neck", -0.01f), c(3, row, 1));
add(new ResetButton("Reset", "Neck"), c(4, row, 1));
row++;
add(new JLabel("Virtual waist"), c(0, row, 1));
add(new AdjButton("+", "Virtual waist", 0.01f), c(1, row, 1));
add(new SkeletonLabel("Virtual waist"), c(2, row, 1));
add(new AdjButton("-", "Virtual waist", -0.01f), c(3, row, 1));
add(new ResetButton("Reset", "Virtual waist"), c(4, row, 1));
row++;
gui.refresh();
});
}
@ThreadSafe
public void refreshAll() {
java.awt.EventQueue.invokeLater(() -> {
labels.forEach((joint, label) -> {
label.setText(StringUtils.prettyNumber(server.humanPoseProcessor.getSkeletonConfig(joint) * 100, 0));
});
});
}
private void change(String joint, float diff) {
float current = server.humanPoseProcessor.getSkeletonConfig(joint);
server.humanPoseProcessor.setSkeletonConfig(joint, current + diff);
server.saveConfig();
labels.get(joint).setText(StringUtils.prettyNumber((current + diff) * 100, 0));
}
private void reset(String joint) {
server.humanPoseProcessor.resetSkeletonConfig(joint);
server.saveConfig();
@@ -125,17 +201,17 @@ public class SkeletonConfig extends EJBag {
});
}
}
private class SkeletonLabel extends JLabel {
public SkeletonLabel(String joint) {
super(StringUtils.prettyNumber(server.humanPoseProcessor.getSkeletonConfig(joint) * 100, 0));
labels.put(joint, this);
}
}
private class AdjButton extends JButton {
public AdjButton(String text, String joint, float diff) {
super(text);
addMouseListener(new MouseInputAdapter() {
@@ -146,9 +222,9 @@ public class SkeletonConfig extends EJBag {
});
}
}
private class ResetButton extends JButton {
public ResetButton(String text, String joint) {
super(text);
addMouseListener(new MouseInputAdapter() {
@@ -159,9 +235,9 @@ public class SkeletonConfig extends EJBag {
});
}
}
private class TimedResetButton extends JButton {
public TimedResetButton(String text, String joint) {
super(text);
addMouseListener(new MouseInputAdapter() {

View File

@@ -18,6 +18,8 @@ import io.eiren.vr.processor.HumanSkeleton;
import io.eiren.vr.processor.TransformNode;
public class SkeletonList extends EJBag {
private static final long UPDATE_DELAY = 50;
Quaternion q = new Quaternion();
Vector3f v = new Vector3f();
@@ -26,6 +28,7 @@ public class SkeletonList extends EJBag {
private final VRServer server;
private final VRServerGUI gui;
private final List<NodeStatus> nodes = new FastList<>();
private long lastUpdate = 0;
public SkeletonList(VRServer server, VRServerGUI gui) {
super();
@@ -62,6 +65,9 @@ public class SkeletonList extends EJBag {
@VRServerThread
public void updateBones() {
if(lastUpdate + UPDATE_DELAY > System.currentTimeMillis())
return;
lastUpdate = System.currentTimeMillis();
java.awt.EventQueue.invokeLater(() -> {
for(int i = 0; i < nodes.size(); ++i)
nodes.get(i).update();

View File

@@ -1,6 +1,7 @@
package io.eiren.gui;
import java.awt.Color;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@@ -34,14 +35,17 @@ import io.eiren.vr.trackers.TrackerWithTPS;
public class TrackersList extends EJBox {
private static final long UPDATE_DELAY = 50;
Quaternion q = new Quaternion();
Vector3f v = new Vector3f();
float[] angles = new float[3];
private List<TrackerRow> trackers = new FastList<>();
private List<TrackerPanel> trackers = new FastList<>();
private final VRServer server;
private final VRServerGUI gui;
private long lastUpdate = 0;
public TrackersList(VRServer server, VRServerGUI gui) {
super(BoxLayout.PAGE_AXIS);
@@ -61,16 +65,42 @@ public class TrackersList extends EJBox {
Class<? extends Tracker> currentClass = null;
EJBox line = null;
boolean first = true;
for(int i = 0; i < trackers.size(); ++i) {
add(Box.createVerticalStrut(3));
TrackerRow tr = trackers.get(i);
TrackerPanel tr = trackers.get(i);
Tracker t = tr.t;
if(t instanceof ReferenceAdjustedTracker)
t = ((ReferenceAdjustedTracker<?>) t).getTracker();
if(currentClass != t.getClass()) {
currentClass = t.getClass();
add(new JLabel(currentClass.getSimpleName()));
if(line != null)
line.add(Box.createHorizontalGlue());
line = null;
line = new EJBox(BoxLayout.LINE_AXIS);
line.add(Box.createHorizontalGlue());
JLabel nameLabel;
line.add(nameLabel = new JLabel(currentClass.getSimpleName()));
nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD));
line.add(Box.createHorizontalGlue());
add(line);
line = null;
}
if(line == null) {
line = new EJBox(BoxLayout.LINE_AXIS);
add(Box.createVerticalStrut(3));
add(line);
first = true;
} else {
line.add(Box.createHorizontalStrut(3));
first = false;
}
tr.build();
line.add(tr);
if(!first)
line = null;
}
validate();
gui.refresh();
@@ -78,6 +108,9 @@ public class TrackersList extends EJBox {
@ThreadSafe
public void updateTrackers() {
if(lastUpdate + UPDATE_DELAY > System.currentTimeMillis())
return;
lastUpdate = System.currentTimeMillis();
java.awt.EventQueue.invokeLater(() -> {
for(int i = 0; i < trackers.size(); ++i)
trackers.get(i).update();
@@ -87,12 +120,12 @@ public class TrackersList extends EJBox {
@ThreadSafe
public void newTrackerAdded(Tracker t) {
java.awt.EventQueue.invokeLater(() -> {
trackers.add(new TrackerRow(t));
trackers.add(new TrackerPanel(t));
build();
});
}
private class TrackerRow extends EJBag {
private class TrackerPanel extends EJBag {
final Tracker t;
JLabel position;
@@ -102,24 +135,37 @@ public class TrackersList extends EJBox {
JLabel bat;
JLabel ping;
JLabel raw;
JLabel rawMag;
JLabel calibration;
JLabel magAccuracy;
JLabel adj;
JLabel adjYaw;
JLabel correction;
@AWTThread
public TrackerRow(Tracker t) {
public TrackerPanel(Tracker t) {
super();
this.t = t;
}
@SuppressWarnings("unchecked")
@AWTThread
public TrackerRow build() {
public TrackerPanel build() {
int row = 0;
Tracker realTracker = t;
if(t instanceof ReferenceAdjustedTracker)
realTracker = ((ReferenceAdjustedTracker<? extends Tracker>) t).getTracker();
removeAll();
add(new JLabel(t.getName()), s(c(0, 0, 0, GridBagConstraints.FIRST_LINE_START), 4, 1));
JLabel nameLabel;
add(nameLabel = new JLabel(t.getName()), s(c(0, row, 0, GridBagConstraints.FIRST_LINE_START), 4, 1));
nameLabel.setFont(nameLabel.getFont().deriveFont(Font.BOLD));
row++;
if(t.userEditable()) {
TrackerConfig cfg = server.getTrackerConfig(t);
JComboBox<String> desSelect;
add(desSelect = new JComboBox<>(), s(c(0, 1, 0, GridBagConstraints.FIRST_LINE_START), 2, 1));
add(desSelect = new JComboBox<>(), s(c(0, row, 0, GridBagConstraints.FIRST_LINE_START), 2, 1));
for(TrackerBodyPosition p : TrackerBodyPosition.values) {
desSelect.addItem(p.name());
}
@@ -136,14 +182,11 @@ public class TrackersList extends EJBox {
server.trackerUpdated(t);
}
});
Tracker realTracker = t;
if(t instanceof ReferenceAdjustedTracker<?>)
realTracker = ((ReferenceAdjustedTracker<? extends Tracker>) t).getTracker();
if(realTracker instanceof IMUTracker) {
IMUTracker imu = (IMUTracker) realTracker;
TrackerMountingRotation tr = imu.getMountingRotation();
JComboBox<String> mountSelect;
add(mountSelect = new JComboBox<>(), s(c(2, 1, 0, GridBagConstraints.FIRST_LINE_START), 2, 1));
add(mountSelect = new JComboBox<>(), s(c(2, row, 0, GridBagConstraints.FIRST_LINE_START), 2, 1));
for(TrackerMountingRotation p : TrackerMountingRotation.values) {
mountSelect.addItem(p.name());
}
@@ -161,31 +204,49 @@ public class TrackersList extends EJBox {
}
});
}
row++;
}
add(new JLabel("Rotation"), c(0, 2, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Postion"), c(1, 2, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Ping"), c(2, 2, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("TPS"), c(3, 2, 0, GridBagConstraints.FIRST_LINE_START));
add(rotation = new JLabel("0 0 0"), c(0, 3, 0, GridBagConstraints.FIRST_LINE_START));
add(position = new JLabel("0 0 0"), c(1, 3, 0, GridBagConstraints.FIRST_LINE_START));
add(ping = new JLabel(""), c(2, 3, 0, GridBagConstraints.FIRST_LINE_START));
if(t instanceof TrackerWithTPS) {
add(tps = new JLabel("0"), c(3, 3, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Rotation"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Position"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Ping"), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("TPS"), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
row++;
add(rotation = new JLabel("0 0 0"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(position = new JLabel("0 0 0"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
add(ping = new JLabel(""), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
if(realTracker instanceof TrackerWithTPS) {
add(tps = new JLabel("0"), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
} else {
add(new JLabel(""), c(3, 3, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel(""), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
}
row++;
add(new JLabel("Status:"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(status = new JLabel(t.getStatus().toString().toLowerCase()), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Battery:"), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
add(bat = new JLabel("0"), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
row++;
add(new JLabel("Raw:"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(raw = new JLabel("0 0 0"), s(c(1, row, 0, GridBagConstraints.FIRST_LINE_START), 3, 1));
row++;
if(realTracker instanceof IMUTracker) {
add(new JLabel("Raw mag:"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(rawMag = new JLabel("0 0 0"), s(c(1, row, 0, GridBagConstraints.FIRST_LINE_START), 3, 1));
row++;
add(new JLabel("Cal:"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(calibration = new JLabel("0"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Mag acc:"), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
add(magAccuracy = new JLabel(""), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
row++;
add(new JLabel("Correction:"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(correction = new JLabel("0 0 0"), s(c(1, row, 0, GridBagConstraints.FIRST_LINE_START), 3, 1));
row++;
}
add(new JLabel("Status:"), c(0, 4, 0, GridBagConstraints.FIRST_LINE_START));
add(status = new JLabel(t.getStatus().toString().toLowerCase()), c(1, 4, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Battery:"), c(2, 4, 0, GridBagConstraints.FIRST_LINE_START));
add(bat = new JLabel("0"), c(3, 4, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Raw:"), c(0, 5, 0, GridBagConstraints.FIRST_LINE_START));
add(raw = new JLabel("0 0 0 0"), s(c(1, 5, 0, GridBagConstraints.FIRST_LINE_START), 3, 1));
if(t instanceof ReferenceAdjustedTracker) {
add(new JLabel("Adj:"), c(0, 6, 0, GridBagConstraints.FIRST_LINE_START));
add(adj = new JLabel("0 0 0 0"), c(1, 6, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("AdjY:"), c(2, 6, 0, GridBagConstraints.FIRST_LINE_START));
add(adjYaw = new JLabel("0 0 0 0"), c(3, 6, 0, GridBagConstraints.FIRST_LINE_START));
if(t instanceof ReferenceAdjustedTracker) {
add(new JLabel("Adj:"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(adj = new JLabel("0 0 0 0"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("AdjY:"), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
add(adjYaw = new JLabel("0 0 0 0"), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
}
setBorder(BorderFactory.createLineBorder(new Color(0x663399), 4, true));
@@ -198,6 +259,9 @@ public class TrackersList extends EJBox {
public void update() {
if(position == null)
return;
Tracker realTracker = t;
if(t instanceof ReferenceAdjustedTracker)
realTracker = ((ReferenceAdjustedTracker<? extends Tracker>) t).getTracker();
t.getRotation(q);
t.getPosition(v);
q.toAngles(angles);
@@ -210,42 +274,54 @@ public class TrackersList extends EJBox {
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
status.setText(t.getStatus().toString().toLowerCase());
if(t instanceof TrackerWithTPS) {
tps.setText(StringUtils.prettyNumber(((TrackerWithTPS) t).getTPS(), 1));
if(realTracker instanceof TrackerWithTPS) {
tps.setText(StringUtils.prettyNumber(((TrackerWithTPS) realTracker).getTPS(), 1));
}
if(t instanceof TrackerWithBattery)
bat.setText(StringUtils.prettyNumber(((TrackerWithBattery) t).getBatteryVoltage(), 1));
Tracker t2 = t;
if(realTracker instanceof TrackerWithBattery)
bat.setText(StringUtils.prettyNumber(((TrackerWithBattery) realTracker).getBatteryVoltage(), 1));
if(t instanceof ReferenceAdjustedTracker) {
t2 = ((ReferenceAdjustedTracker<Tracker>) t).getTracker();
((ReferenceAdjustedTracker<Tracker>) t).adjustmentAttachment.toAngles(angles);
((ReferenceAdjustedTracker<Tracker>) t).attachmentFix.toAngles(angles);
adj.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
((ReferenceAdjustedTracker<Tracker>) t).adjustmentYaw.toAngles(angles);
((ReferenceAdjustedTracker<Tracker>) t).yawFix.toAngles(angles);
adjYaw.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
}
if(t2 instanceof IMUTracker)
ping.setText(String.valueOf(((IMUTracker) t2).ping));
t2.getRotation(q);
raw.setText(StringUtils.prettyNumber(q.getX(), 4)
+ " " + StringUtils.prettyNumber(q.getY(), 4)
+ " " + StringUtils.prettyNumber(q.getZ(), 4)
+ " " + StringUtils.prettyNumber(q.getW(), 4));
if(realTracker instanceof IMUTracker) {
ping.setText(String.valueOf(((IMUTracker) realTracker).ping));
}
realTracker.getRotation(q);
q.toAngles(angles);
raw.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
if(realTracker instanceof IMUTracker) {
((IMUTracker) realTracker).rotMagQuaternion.toAngles(angles);
rawMag.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
calibration.setText(((IMUTracker) realTracker).calibrationStatus + " / " + ((IMUTracker) realTracker).magCalibrationStatus);
magAccuracy.setText(StringUtils.prettyNumber(((IMUTracker) realTracker).magnetometerAccuracy * FastMath.RAD_TO_DEG, 1) + "°");
((IMUTracker) realTracker).getCorrection(q);
q.toAngles(angles);
correction.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
}
}
}
private static int getTrackerSort(Tracker t) {
if(t instanceof HMDTracker)
return 0;
if(t instanceof ComputedTracker)
return 1;
if(t instanceof IMUTracker)
return 2;
if(t instanceof ReferenceAdjustedTracker)
return 5;
t = ((ReferenceAdjustedTracker<?>) t).getTracker();
if(t instanceof IMUTracker)
return 0;
if(t instanceof HMDTracker)
return 100;
if(t instanceof ComputedTracker)
return 200;
return 1000;
}
}

View File

@@ -8,11 +8,13 @@ import io.eiren.util.StringUtils;
import io.eiren.util.ann.AWTThread;
import io.eiren.vr.Main;
import io.eiren.vr.VRServer;
import io.eiren.vr.bridge.NamedPipeVRBridge;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import static javax.swing.BoxLayout.PAGE_AXIS;
@@ -33,6 +35,11 @@ public class VRServerGUI extends JFrame {
@AWTThread
public VRServerGUI(VRServer server) {
super("SlimeVR Server (" + Main.VERSION + ")");
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch(Exception e) {
e.printStackTrace();
}
//increaseFontSize();
this.server = server;
@@ -50,6 +57,8 @@ public class VRServerGUI extends JFrame {
add(scroll = new JScrollPane(pane = new EJBox(PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
setMinimumSize(new Dimension(1280, 1080));
build();
}
@@ -59,7 +68,7 @@ public class VRServerGUI extends JFrame {
public void refresh() {
// Pack and display
pack();
//pack();
setVisible(true);
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
@@ -73,8 +82,6 @@ public class VRServerGUI extends JFrame {
private void build() {
pane.removeAll();
NamedPipeVRBridge npvb = server.getVRBridge(NamedPipeVRBridge.class);
pane.add(new EJBox(LINE_AXIS) {{
setBorder(new EmptyBorder(i(5)));
@@ -87,19 +94,16 @@ public class VRServerGUI extends JFrame {
}
});
}});
add(Box.createHorizontalStrut(10));
add(new JButton("Fast Reset") {{
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
resetFast();
}
});
}});
add(Box.createHorizontalGlue());
if(npvb != null) {
add(new JButton(npvb.isOneTrackerMode() ? "Trackers: 1" : "Trackers: 3") {{
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
npvb.setSpawnOneTracker(!npvb.isOneTrackerMode());
setText(npvb.isOneTrackerMode() ? "Trackers: 1" : "Trackers: 3");
}
});
}});
add(Box.createHorizontalStrut(10));
}
add(new JButton("GUI Zoom (x" + StringUtils.prettyNumber(zoom, 2) + ")") {{
addMouseListener(new MouseInputAdapter() {
@Override
@@ -126,8 +130,57 @@ public class VRServerGUI extends JFrame {
setBorder(new EmptyBorder(i(5)));
add(new EJBox(PAGE_AXIS) {{
setAlignmentY(TOP_ALIGNMENT);
add(new JLabel("SteamVR Trackers:"));
JComboBox<String> trackersSelect;
add(trackersSelect = new JComboBox<>());
trackersSelect.addItem("Waist");
trackersSelect.addItem("Waist + Legs");
trackersSelect.addItem("Waist + Legs + Chest");
trackersSelect.addItem("Waist + Legs + Knees");
trackersSelect.addItem("Waist + Legs + Chest + Knees");
switch(server.config.getInt("virtualtrackers", 3)) {
case 1:
trackersSelect.setSelectedIndex(0);
break;
case 3:
trackersSelect.setSelectedIndex(1);
break;
case 4:
trackersSelect.setSelectedIndex(2);
break;
case 5:
trackersSelect.setSelectedIndex(3);
break;
case 6:
trackersSelect.setSelectedIndex(4);
break;
}
trackersSelect.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
switch(trackersSelect.getSelectedIndex()) {
case 0:
server.config.setProperty("virtualtrackers", 1);
break;
case 1:
server.config.setProperty("virtualtrackers", 3);
break;
case 2:
server.config.setProperty("virtualtrackers", 4);
break;
case 3:
server.config.setProperty("virtualtrackers", 5);
break;
case 4:
server.config.setProperty("virtualtrackers", 6);
break;
}
server.saveConfig();
}
});
add(Box.createHorizontalStrut(10));
add(new JLabel("Trackers"));
add(new JLabel("Trackers list"));
add(trackersList);
add(Box.createVerticalGlue());
}});
@@ -193,6 +246,11 @@ public class VRServerGUI extends JFrame {
}
}
@AWTThread
private void resetFast() {
server.resetTrackersYaw();
}
@AWTThread
private void reset() {
ButtonTimer.runTimer(resetButton, 3, "RESET", server::resetTrackers);

View File

@@ -0,0 +1,512 @@
package io.eiren.gui.autobone;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import com.jme3.math.Vector3f;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.logging.LogManager;
import io.eiren.util.collections.FastList;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.HumanSkeleton;
import io.eiren.vr.processor.HumanSkeletonWithLegs;
import io.eiren.vr.processor.HumanSkeletonWithWaist;
import io.eiren.vr.processor.TrackerBodyPosition;
import io.eiren.vr.trackers.TrackerUtils;
public class AutoBone {
public class Epoch {
public final int epoch;
public final float epochError;
public Epoch(int epoch, float epochError) {
this.epoch = epoch;
this.epochError = epochError;
}
@Override
public String toString() {
return "Epoch: " + epoch + ", Epoch Error: " + epochError;
}
}
public int cursorIncrement = 1;
public int minDataDistance = 2;
public int maxDataDistance = 32;
public int numEpochs = 5;
public float initialAdjustRate = 2.5f;
public float adjustRateDecay = 1.01f;
public float slideErrorFactor = 1.0f;
public float offsetErrorFactor = 0.0f;
public float proportionErrorFactor = 0.2f;
public float heightErrorFactor = 0.1f;
public float positionErrorFactor = 0.0f;
public float positionOffsetErrorFactor = 0.0f;
/*
public float NECK_WAIST_RATIO_MIN = 0.2f;
public float NECK_WAIST_RATIO_MAX = 0.3f;
public float CHEST_WAIST_RATIO_MIN = 0.35f;
public float CHEST_WAIST_RATIO_MAX = 0.6f;
public float HIP_MIN = 0.08f;
public float HIP_WAIST_RATIO_MAX = 0.4f;
// Human average is 1.1235 (SD 0.07)
public float LEG_WAIST_RATIO_MIN = 1.1235f - ((0.07f * 3f) + 0.05f);
public float LEG_WAIST_RATIO_MAX = 1.1235f + ((0.07f * 3f) + 0.05f);
public float KNEE_LEG_RATIO_MIN = 0.42f;
public float KNEE_LEG_RATIO_MAX = 0.58f;
*/
protected final VRServer server;
protected HumanSkeletonWithLegs skeleton = null;
// This is filled by reloadConfigValues()
public final HashMap<String, Float> configs = new HashMap<String, Float>();
public final HashMap<String, Float> staticConfigs = new HashMap<String, Float>();
public final FastList<String> heightConfigs = new FastList<String>(new String[] {
"Neck",
"Waist",
"Legs length"
});
public AutoBone(VRServer server) {
this.server = server;
reloadConfigValues();
server.addSkeletonUpdatedCallback(this::skeletonUpdated);
}
public void reloadConfigValues() {
// Load waist configs
staticConfigs.put("Head", server.config.getFloat("body.headShift", HumanSkeletonWithWaist.HEAD_SHIFT_DEFAULT));
staticConfigs.put("Neck", server.config.getFloat("body.neckLength", HumanSkeletonWithWaist.NECK_LENGTH_DEFAULT));
configs.put("Waist", server.config.getFloat("body.waistDistance", 0.85f));
if (server.config.getBoolean("autobone.forceChestTracker", false) ||
TrackerUtils.findTrackerForBodyPosition(server.getAllTrackers(), TrackerBodyPosition.CHEST) != null) {
// If force enabled or has a chest tracker
configs.put("Chest", server.config.getFloat("body.chestDistance", 0.42f));
} else {
// Otherwise, make sure it's not used
configs.remove("Chest");
staticConfigs.put("Chest", server.config.getFloat("body.chestDistance", 0.42f));
}
// Load leg configs
staticConfigs.put("Hips width", server.config.getFloat("body.hipsWidth", HumanSkeletonWithLegs.HIPS_WIDTH_DEFAULT));
configs.put("Legs length", server.config.getFloat("body.legsLength", 0.84f));
configs.put("Knee height", server.config.getFloat("body.kneeHeight", 0.42f));
}
@ThreadSafe
public void skeletonUpdated(HumanSkeleton newSkeleton) {
if (newSkeleton instanceof HumanSkeletonWithLegs) {
skeleton = (HumanSkeletonWithLegs)newSkeleton;
applyConfigToSkeleton(newSkeleton);
LogManager.log.info("[AutoBone] Received updated skeleton");
}
}
public void applyConfig() {
if (!applyConfigToSkeleton(skeleton)) {
// Unable to apply to skeleton, save directly
saveConfigs();
}
}
public boolean applyConfigToSkeleton(HumanSkeleton skeleton) {
if (skeleton == null) {
return false;
}
for (Entry<String, Float> entry : configs.entrySet()) {
skeleton.setSkeletonConfig(entry.getKey(), entry.getValue());
}
server.saveConfig();
LogManager.log.info("[AutoBone] Configured skeleton bone lengths");
return true;
}
private void setConfig(String name, String path) {
Float value = configs.get(name);
if (value != null) {
server.config.setProperty(path, value);
}
}
// This doesn't require a skeleton, therefore can be used if skeleton is null
public void saveConfigs() {
setConfig("Head", "body.headShift");
setConfig("Neck", "body.neckLength");
setConfig("Waist", "body.waistDistance");
setConfig("Chest", "body.chestDistance");
setConfig("Hips width", "body.hipsWidth");
setConfig("Legs length", "body.legsLength");
setConfig("Knee height", "body.kneeHeight");
server.saveConfig();
}
public Float getConfig(String config) {
Float configVal = configs.get(config);
return configVal != null ? configVal : staticConfigs.get(config);
}
public Float getConfig(String config, Map<String, Float> configs, Map<String, Float> configsAlt) {
if (configs == null) {
throw new NullPointerException("Argument \"configs\" must not be null");
}
Float configVal = configs.get(config);
return configVal != null || configsAlt == null ? configVal : configsAlt.get(config);
}
public float getHeight(Map<String, Float> configs) {
return getHeight(configs, null);
}
public float getHeight(Map<String, Float> configs, Map<String, Float> configsAlt) {
float height = 0f;
for (String heightConfig : heightConfigs) {
Float length = getConfig(heightConfig, configs, configsAlt);
if (length != null) {
height += length;
}
}
return height;
}
public float getLengthSum(Map<String, Float> configs) {
float length = 0f;
for (float boneLength : configs.values()) {
length += boneLength;
}
return length;
}
public float getMaxHmdHeight(PoseFrame[] frames) {
float maxHeight = 0f;
for (PoseFrame frame : frames) {
if (frame.rootPos.y > maxHeight) {
maxHeight = frame.rootPos.y;
}
}
return maxHeight;
}
public void processFrames(PoseFrame[] frames) {
processFrames(frames, -1f);
}
public void processFrames(PoseFrame[] frames, Consumer<Epoch> epochCallback) {
processFrames(frames, -1f, epochCallback);
}
public void processFrames(PoseFrame[] frames, float targetHeight) {
processFrames(frames, true, targetHeight);
}
public void processFrames(PoseFrame[] frames, float targetHeight, Consumer<Epoch> epochCallback) {
processFrames(frames, true, targetHeight, epochCallback);
}
public float processFrames(PoseFrame[] frames, boolean calcInitError, float targetHeight) {
return processFrames(frames, calcInitError, targetHeight, null);
}
public float processFrames(PoseFrame[] frames, boolean calcInitError, float targetHeight, Consumer<Epoch> epochCallback) {
SimpleSkeleton skeleton1 = new SimpleSkeleton(configs, staticConfigs);
SimpleSkeleton skeleton2 = new SimpleSkeleton(configs, staticConfigs);
// If target height isn't specified, auto-detect
if (targetHeight < 0f) {
if (skeleton != null) {
targetHeight = getHeight(skeleton.getSkeletonConfig());
LogManager.log.warning("[AutoBone] Target height loaded from skeleton (Make sure you reset before running!): " + targetHeight);
} else {
float hmdHeight = getMaxHmdHeight(frames);
if (hmdHeight <= 0.50f) {
LogManager.log.warning("[AutoBone] Max headset height detected (Value seems too low, did you not stand up straight while measuring?): " + hmdHeight);
} else {
LogManager.log.info("[AutoBone] Max headset height detected: " + hmdHeight);
}
// Estimate target height from HMD height
targetHeight = hmdHeight;
}
}
for (int epoch = calcInitError ? -1 : 0; epoch < numEpochs; epoch++) {
float sumError = 0f;
int errorCount = 0;
float adjustRate = epoch >= 0 ? (float)(initialAdjustRate / Math.pow(adjustRateDecay, epoch)) : 0f;
for (int cursorOffset = minDataDistance; cursorOffset <= maxDataDistance && cursorOffset < frames.length; cursorOffset++) {
for (int frameCursor = 0; frameCursor < frames.length - cursorOffset; frameCursor += cursorIncrement) {
PoseFrame frame1 = frames[frameCursor];
PoseFrame frame2 = frames[frameCursor + cursorOffset];
// If there's missing data, throw an exception
if (frame1 == null || frame2 == null) {
throw new NullPointerException("Frames are missing from processing data");
}
skeleton1.setSkeletonConfigs(configs);
skeleton2.setSkeletonConfigs(configs);
skeleton1.setPoseFromFrame(frame1);
skeleton2.setPoseFromFrame(frame2);
float totalLength = getLengthSum(configs);
float curHeight = getHeight(configs, staticConfigs);
float errorDeriv = getErrorDeriv(frame1, frame2, skeleton1, skeleton2, targetHeight - curHeight);
float error = errorFunc(errorDeriv);
// In case of fire
if (Float.isNaN(error) || Float.isInfinite(error)) {
// Extinguish
LogManager.log.warning("[AutoBone] Error value is invalid, resetting variables to recover");
reloadConfigValues();
// Reset error sum values
sumError = 0f;
errorCount = 0;
// Continue on new data
continue;
}
// Store the error count for logging purposes
sumError += errorDeriv;
errorCount++;
float adjustVal = error * adjustRate;
for (Entry<String, Float> entry : configs.entrySet()) {
// Skip adjustment if the epoch is before starting (for logging only)
if (epoch < 0) {
break;
}
float originalLength = entry.getValue();
// Try positive and negative adjustments
boolean isHeightVar = heightConfigs.contains(entry.getKey());
float minError = errorDeriv;
float finalNewLength = -1f;
for (int i = 0; i < 2; i++) {
// Scale by the ratio for smooth adjustment and more stable results
float curAdjustVal = ((i == 0 ? adjustVal : -adjustVal) * originalLength) / totalLength;
float newLength = originalLength + curAdjustVal;
// No small or negative numbers!!! Bad algorithm!
if (newLength < 0.01f) {
continue;
}
updateSkeletonBoneLength(skeleton1, skeleton2, entry.getKey(), newLength);
float newHeight = isHeightVar ? curHeight + curAdjustVal : curHeight;
float newErrorDeriv = getErrorDeriv(frame1, frame2, skeleton1, skeleton2, targetHeight - newHeight);
if (newErrorDeriv < minError) {
minError = newErrorDeriv;
finalNewLength = newLength;
}
}
if (finalNewLength > 0f) {
entry.setValue(finalNewLength);
}
// Reset the length to minimize bias in other variables, it's applied later
updateSkeletonBoneLength(skeleton1, skeleton2, entry.getKey(), originalLength);
}
}
}
// Calculate average error over the epoch
float avgError = errorCount > 0 ? sumError / errorCount : -1f;
LogManager.log.info("[AutoBone] Epoch " + (epoch + 1) + " average error: " + avgError);
if (epochCallback != null) {
epochCallback.accept(new Epoch(epoch + 1, avgError));
}
}
float finalHeight = getHeight(configs, staticConfigs);
LogManager.log.info("[AutoBone] Target height: " + targetHeight + " New height: " + finalHeight);
return Math.abs(finalHeight - targetHeight);
}
// The change in position of the ankle over time
protected float getSlideErrorDeriv(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
float slideLeft = skeleton1.getLeftFootPos().distance(skeleton2.getLeftFootPos());
float slideRight = skeleton1.getRightFootPos().distance(skeleton2.getRightFootPos());
// Divide by 4 to halve and average, it's halved because you want to approach a midpoint, not the other point
return (slideLeft + slideRight) / 4f;
}
// The offset between both feet at one instant and over time
protected float getOffsetErrorDeriv(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
float dist1 = Math.abs(skeleton1.getLeftFootPos().y - skeleton1.getRightFootPos().y);
float dist2 = Math.abs(skeleton2.getLeftFootPos().y - skeleton2.getRightFootPos().y);
float dist3 = Math.abs(skeleton1.getLeftFootPos().y - skeleton2.getRightFootPos().y);
float dist4 = Math.abs(skeleton1.getLeftFootPos().y - skeleton2.getRightFootPos().y);
float dist5 = Math.abs(skeleton1.getLeftFootPos().y - skeleton2.getLeftFootPos().y);
float dist6 = Math.abs(skeleton1.getRightFootPos().y - skeleton2.getRightFootPos().y);
// Divide by 12 to halve and average, it's halved because you want to approach a midpoint, not the other point
return (dist1 + dist2 + dist3 + dist4 + dist5 + dist6) / 12f;
}
// The distance from average human proportions
protected float getProportionErrorDeriv(SimpleSkeleton skeleton) {
Float neckLength = skeleton.getSkeletonConfig("Neck");
Float chestLength = skeleton.getSkeletonConfig("Chest");
Float waistLength = skeleton.getSkeletonConfig("Waist");
Float legsLength = skeleton.getSkeletonConfig("Legs length");
Float kneeHeight = skeleton.getSkeletonConfig("Knee height");
float chestWaist = chestLength != null && waistLength != null ? Math.abs((chestLength / waistLength) - 0.5f) : 0f;
float legBody = legsLength != null && waistLength != null && neckLength != null ? Math.abs((legsLength / (waistLength + neckLength)) - 1.1235f) : 0f;
float kneeLeg = kneeHeight != null && legsLength != null ? Math.abs((kneeHeight / legsLength) - 0.5f) : 0f;
// SD of 0.07, capture 68% within range
float sdValue = 0.07f;
if (legBody <= sdValue) {
legBody = 0f;
} else {
legBody -= sdValue;
}
return (chestWaist + legBody + kneeLeg) / 3f;
}
// The distance of any points to the corresponding absolute position
protected float getPositionErrorDeriv(PoseFrame frame, SimpleSkeleton skeleton) {
float offset = 0f;
int offsetCount = 0;
if (frame.positions != null) {
for (Entry<String, Vector3f> entry : frame.positions.entrySet()) {
Vector3f nodePos = skeleton.getNodePosition(entry.getKey());
if (nodePos != null) {
offset += Math.abs(nodePos.distance(entry.getValue()));
offsetCount++;
}
}
}
return offsetCount > 0 ? offset / offsetCount : 0f;
}
// The difference between offset of absolute position and the corresponding point over time
protected float getPositionOffsetErrorDeriv(PoseFrame frame1, PoseFrame frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
float offset = 0f;
int offsetCount = 0;
if (frame1.positions != null && frame2.positions != null) {
for (Entry<String, Vector3f> entry : frame1.positions.entrySet()) {
Vector3f frame2Pos = frame2.positions.get(entry.getKey());
if (frame2Pos == null) {
continue;
}
Vector3f nodePos1 = skeleton1.getNodePosition(entry.getKey());
if (nodePos1 == null) {
continue;
}
Vector3f nodePos2 = skeleton2.getNodePosition(entry.getKey());
if (nodePos2 == null) {
continue;
}
float dist1 = Math.abs(nodePos1.distance(entry.getValue()));
float dist2 = Math.abs(nodePos2.distance(frame2Pos));
offset += Math.abs(dist2 - dist1);
offsetCount++;
}
}
return offsetCount > 0 ? offset / offsetCount : 0f;
}
protected float getErrorDeriv(PoseFrame frame1, PoseFrame frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2, float heightChange) {
float totalError = 0f;
float sumWeight = 0f;
if (slideErrorFactor > 0f) {
totalError += getSlideErrorDeriv(skeleton1, skeleton2) * slideErrorFactor;
sumWeight += slideErrorFactor;
}
if (offsetErrorFactor > 0f) {
totalError += getOffsetErrorDeriv(skeleton1, skeleton2) * offsetErrorFactor;
sumWeight += offsetErrorFactor;
}
if (proportionErrorFactor > 0f) {
// Either skeleton will work fine, skeleton1 is used as a default
totalError += getProportionErrorDeriv(skeleton1) * proportionErrorFactor;
sumWeight += proportionErrorFactor;
}
if (heightErrorFactor > 0f) {
totalError += Math.abs(heightChange) * heightErrorFactor;
sumWeight += heightErrorFactor;
}
if (positionErrorFactor > 0f) {
totalError += (getPositionErrorDeriv(frame1, skeleton1) + getPositionErrorDeriv(frame2, skeleton2) / 2f) * positionErrorFactor;
sumWeight += positionErrorFactor;
}
if (positionOffsetErrorFactor > 0f) {
totalError += getPositionOffsetErrorDeriv(frame1, frame2, skeleton1, skeleton2) * positionOffsetErrorFactor;
sumWeight += positionOffsetErrorFactor;
}
// Minimize sliding, minimize foot height offset, minimize change in total height
return sumWeight > 0f ? totalError / sumWeight : 0f;
}
// Mean square error function
protected static float errorFunc(float errorDeriv) {
return 0.5f * (errorDeriv * errorDeriv);
}
protected void updateSkeletonBoneLength(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2, String joint, float newLength) {
skeleton1.setSkeletonConfig(joint, newLength, true);
skeleton2.setSkeletonConfig(joint, newLength, true);
}
}

View File

@@ -0,0 +1,414 @@
package io.eiren.gui.autobone;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.JButton;
import javax.swing.border.EmptyBorder;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.Future;
import io.eiren.gui.EJBox;
import io.eiren.gui.SkeletonConfig;
import io.eiren.util.StringUtils;
import io.eiren.util.ann.AWTThread;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.VRServer;
import javax.swing.event.MouseInputAdapter;
import org.apache.commons.lang3.tuple.Pair;
public class AutoBoneWindow extends JFrame {
private static File saveDir = new File("Recordings");
private static File loadDir = new File("LoadRecordings");
private EJBox pane;
private final VRServer server;
private final SkeletonConfig skeletonConfig;
private final PoseRecorder poseRecorder;
private final AutoBone autoBone;
private Thread recordingThread = null;
private Thread saveRecordingThread = null;
private Thread autoBoneThread = null;
private JButton saveRecordingButton;
private JButton adjustButton;
private JButton applyButton;
private JLabel processLabel;
private JLabel lengthsLabel;
public AutoBoneWindow(VRServer server, SkeletonConfig skeletonConfig) {
super("Skeleton Auto-Configuration");
this.server = server;
this.skeletonConfig = skeletonConfig;
this.poseRecorder = new PoseRecorder(server);
this.autoBone = new AutoBone(server);
getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.PAGE_AXIS));
add(new JScrollPane(pane = new EJBox(BoxLayout.PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
build();
}
private String getLengthsString() {
boolean first = true;
StringBuilder configInfo = new StringBuilder("");
for (Entry<String, Float> entry : autoBone.configs.entrySet()) {
if (!first) {
configInfo.append(", ");
} else {
first = false;
}
configInfo.append(entry.getKey() + ": " + StringUtils.prettyNumber(entry.getValue() * 100f, 2));
}
return configInfo.toString();
}
private void saveRecording(PoseFrame[] frames) {
if (saveDir.isDirectory() || saveDir.mkdirs()) {
File saveRecording;
int recordingIndex = 1;
do {
saveRecording = new File(saveDir, "ABRecording" + recordingIndex++ + ".abf");
} while (saveRecording.exists());
LogManager.log.info("[AutoBone] Exporting frames to \"" + saveRecording.getPath() + "\"...");
if (PoseFrameIO.writeToFile(saveRecording, frames)) {
LogManager.log.info("[AutoBone] Done exporting! Recording can be found at \"" + saveRecording.getPath() + "\".");
} else {
LogManager.log.severe("[AutoBone] Failed to export the recording to \"" + saveRecording.getPath() + "\".");
}
} else {
LogManager.log.severe("[AutoBone] Failed to create the recording directory \"" + saveDir.getPath() + "\".");
}
}
private List<Pair<String, PoseFrame[]>> loadRecordings() {
List<Pair<String, PoseFrame[]>> recordings = new FastList<Pair<String, PoseFrame[]>>();
if (loadDir.isDirectory()) {
File[] files = loadDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && org.apache.commons.lang3.StringUtils.endsWithIgnoreCase(file.getName(), ".abf")) {
LogManager.log.info("[AutoBone] Detected recording at \"" + file.getPath() + "\", loading frames...");
PoseFrame[] frames = PoseFrameIO.readFromFile(file);
if (frames == null) {
LogManager.log.severe("Reading frames from \"" + file.getPath() + "\" failed...");
} else {
recordings.add(Pair.of(file.getName(), frames));
}
}
}
}
}
return recordings;
}
private float processFrames(PoseFrame[] frames) {
autoBone.reloadConfigValues();
autoBone.minDataDistance = server.config.getInt("autobone.minimumDataDistance", autoBone.minDataDistance);
autoBone.maxDataDistance = server.config.getInt("autobone.maximumDataDistance", autoBone.maxDataDistance);
autoBone.numEpochs = server.config.getInt("autobone.epochCount", autoBone.numEpochs);
autoBone.initialAdjustRate = server.config.getFloat("autobone.adjustRate", autoBone.initialAdjustRate);
autoBone.adjustRateDecay = server.config.getFloat("autobone.adjustRateDecay", autoBone.adjustRateDecay);
autoBone.slideErrorFactor = server.config.getFloat("autobone.slideErrorFactor", autoBone.slideErrorFactor);
autoBone.offsetErrorFactor = server.config.getFloat("autobone.offsetErrorFactor", autoBone.offsetErrorFactor);
autoBone.proportionErrorFactor = server.config.getFloat("autobone.proportionErrorFactor", autoBone.proportionErrorFactor);
autoBone.heightErrorFactor = server.config.getFloat("autobone.heightErrorFactor", autoBone.heightErrorFactor);
autoBone.positionErrorFactor = server.config.getFloat("autobone.positionErrorFactor", autoBone.positionErrorFactor);
autoBone.positionOffsetErrorFactor = server.config.getFloat("autobone.positionOffsetErrorFactor", autoBone.positionOffsetErrorFactor);
boolean calcInitError = server.config.getBoolean("autobone.calculateInitialError", true);
float targetHeight = server.config.getFloat("autobone.manualTargetHeight", -1f);
return autoBone.processFrames(frames, calcInitError, targetHeight, (epoch) -> {
processLabel.setText(epoch.toString());
lengthsLabel.setText(getLengthsString());
});
}
@AWTThread
private void build() {
pane.add(new EJBox(BoxLayout.LINE_AXIS) {{
setBorder(new EmptyBorder(i(5)));
add(new JButton("Start Recording") {{
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// Prevent running multiple times
if (!isEnabled() || recordingThread != null) {
return;
}
Thread thread = new Thread() {
@Override
public void run() {
try {
if (poseRecorder.isReadyToRecord()) {
setText("Recording...");
// 1000 samples at 20 ms per sample is 20 seconds
int sampleCount = server.config.getInt("autobone.sampleCount", 1000);
long sampleRate = server.config.getLong("autobone.sampleRateMs", 20L);
Future<PoseFrame[]> framesFuture = poseRecorder.startFrameRecording(sampleCount, sampleRate);
PoseFrame[] frames = framesFuture.get();
LogManager.log.info("[AutoBone] Done recording!");
saveRecordingButton.setEnabled(true);
adjustButton.setEnabled(true);
if (server.config.getBoolean("autobone.saveRecordings", false)) {
setText("Saving...");
saveRecording(frames);
}
} else {
setText("Not Ready...");
LogManager.log.severe("[AutoBone] Unable to record...");
Thread.sleep(3000); // Wait for 3 seconds
return;
}
} catch (Exception e) {
setText("Recording Failed...");
LogManager.log.severe("[AutoBone] Failed recording!", e);
try {
Thread.sleep(3000); // Wait for 3 seconds
} catch (Exception e1) {
// Ignore
}
} finally {
setText("Start Recording");
recordingThread = null;
}
}
};
recordingThread = thread;
thread.start();
}
});
}});
add(saveRecordingButton = new JButton("Save Recording") {{
setEnabled(poseRecorder.hasRecording());
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// Prevent running multiple times
if (!isEnabled() || saveRecordingThread != null) {
return;
}
Thread thread = new Thread() {
@Override
public void run() {
try {
Future<PoseFrame[]> framesFuture = poseRecorder.getFramesAsync();
if (framesFuture != null) {
setText("Waiting for Recording...");
PoseFrame[] frames = framesFuture.get();
if (frames.length <= 0) {
throw new IllegalStateException("Recording has no frames");
}
setText("Saving...");
saveRecording(frames);
} else {
setText("No Recording...");
LogManager.log.severe("[AutoBone] Unable to save, no recording was done...");
try {
Thread.sleep(3000); // Wait for 3 seconds
} catch (Exception e1) {
// Ignore
}
return;
}
} catch (Exception e) {
setText("Saving Failed...");
LogManager.log.severe("[AutoBone] Failed to save recording!", e);
try {
Thread.sleep(3000); // Wait for 3 seconds
} catch (Exception e1) {
// Ignore
}
} finally {
setText("Save Recording");
saveRecordingThread = null;
}
}
};
saveRecordingThread = thread;
thread.start();
}
});
}});
add(adjustButton = new JButton("Auto-Adjust") {{
// If there are files to load, enable the button
setEnabled(poseRecorder.hasRecording() || (loadDir.isDirectory() && loadDir.list().length > 0));
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// Prevent running multiple times
if (!isEnabled() || autoBoneThread != null) {
return;
}
Thread thread = new Thread() {
@Override
public void run() {
try {
setText("Load...");
List<Pair<String, PoseFrame[]>> frameRecordings = loadRecordings();
if (frameRecordings.size() > 0) {
LogManager.log.info("[AutoBone] Done loading frames!");
} else {
Future<PoseFrame[]> framesFuture = poseRecorder.getFramesAsync();
if (framesFuture != null) {
setText("Waiting for Recording...");
PoseFrame[] frames = framesFuture.get();
if (frames.length <= 0) {
throw new IllegalStateException("Recording has no frames");
}
frameRecordings.add(Pair.of("<Recording>", frames));
} else {
setText("No Recordings...");
LogManager.log.severe("[AutoBone] No recordings found in \"" + loadDir.getPath() + "\" and no recording was done...");
try {
Thread.sleep(3000); // Wait for 3 seconds
} catch (Exception e1) {
// Ignore
}
return;
}
}
setText("Processing...");
LogManager.log.info("[AutoBone] Processing frames...");
FastList<Float> heightPercentError = new FastList<Float>(frameRecordings.size());
for (Pair<String, PoseFrame[]> recording : frameRecordings) {
LogManager.log.info("[AutoBone] Processing frames from \"" + recording.getKey() + "\"...");
heightPercentError.add(processFrames(recording.getValue()));
LogManager.log.info("[AutoBone] Done processing!");
applyButton.setEnabled(true);
//#region Stats/Values
Float neckLength = autoBone.getConfig("Neck");
Float chestLength = autoBone.getConfig("Chest");
Float waistLength = autoBone.getConfig("Waist");
Float hipWidth = autoBone.getConfig("Hips width");
Float legsLength = autoBone.getConfig("Legs length");
Float kneeHeight = autoBone.getConfig("Knee height");
float neckWaist = neckLength != null && waistLength != null ? neckLength / waistLength : 0f;
float chestWaist = chestLength != null && waistLength != null ? chestLength / waistLength : 0f;
float hipWaist = hipWidth != null && waistLength != null ? hipWidth / waistLength : 0f;
float legWaist = legsLength != null && waistLength != null ? legsLength / waistLength : 0f;
float legBody = legsLength != null && waistLength != null && neckLength != null ? legsLength / (waistLength + neckLength) : 0f;
float kneeLeg = kneeHeight != null && legsLength != null ? kneeHeight / legsLength : 0f;
LogManager.log.info("[AutoBone] Ratios: [{Neck-Waist: " + StringUtils.prettyNumber(neckWaist) +
"}, {Chest-Waist: " + StringUtils.prettyNumber(chestWaist) +
"}, {Hip-Waist: " + StringUtils.prettyNumber(hipWaist) +
"}, {Leg-Waist: " + StringUtils.prettyNumber(legWaist) +
"}, {Leg-Body: " + StringUtils.prettyNumber(legBody) +
"}, {Knee-Leg: " + StringUtils.prettyNumber(kneeLeg) + "}]");
String lengthsString = getLengthsString();
LogManager.log.info("[AutoBone] Length values: " + lengthsString);
lengthsLabel.setText(lengthsString);
}
if (heightPercentError.size() > 0) {
float mean = 0f;
for (float val : heightPercentError) {
mean += val;
}
mean /= heightPercentError.size();
float std = 0f;
for (float val : heightPercentError) {
float stdVal = val - mean;
std += stdVal * stdVal;
}
std = (float)Math.sqrt(std / heightPercentError.size());
LogManager.log.info("[AutoBone] Average height error: " + StringUtils.prettyNumber(mean, 6) + " (SD " + StringUtils.prettyNumber(std, 6) + ")");
}
//#endregion
} catch (Exception e) {
setText("Failed...");
LogManager.log.severe("[AutoBone] Failed adjustment!", e);
try {
Thread.sleep(3000); // Wait for 3 seconds
} catch (Exception e1) {
// Ignore
}
} finally {
setText("Auto-Adjust");
autoBoneThread = null;
}
}
};
autoBoneThread = thread;
thread.start();
}
});
}});
add(applyButton = new JButton("Apply Values") {{
setEnabled(false);
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (!isEnabled()) {
return;
}
autoBone.applyConfig();
// Update GUI values after applying
skeletonConfig.refreshAll();
}
});
}});
}});
pane.add(new EJBox(BoxLayout.LINE_AXIS) {{
setBorder(new EmptyBorder(i(5)));
add(processLabel = new JLabel("Processing has not been started..."));
}});
pane.add(new EJBox(BoxLayout.LINE_AXIS) {{
setBorder(new EmptyBorder(i(5)));
add(lengthsLabel = new JLabel(getLengthsString()));
}});
// Pack and display
pack();
setLocationRelativeTo(null);
setVisible(false);
}
}

View File

@@ -0,0 +1,37 @@
package io.eiren.gui.autobone;
import java.util.HashMap;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.vr.processor.HumanSkeletonWithLegs;
import io.eiren.vr.processor.TransformNode;
public final class PoseFrame {
public final Vector3f rootPos;
public final HashMap<String, Quaternion> rotations;
public final HashMap<String, Vector3f> positions;
public PoseFrame(Vector3f rootPos, HashMap<String, Quaternion> rotations, HashMap<String, Vector3f> positions) {
this.rootPos = rootPos;
this.rotations = rotations;
this.positions = positions;
}
public PoseFrame(HumanSkeletonWithLegs skeleton) {
// Copy headset position
TransformNode rootNode = skeleton.getRootNode();
this.rootPos = new Vector3f(rootNode.localTransform.getTranslation());
// Copy all rotations
this.rotations = new HashMap<String, Quaternion>();
rootNode.depthFirstTraversal(visitor -> {
// Insert a copied quaternion so it isn't changed by reference
rotations.put(visitor.getName(), new Quaternion(visitor.localTransform.getRotation()));
});
this.positions = null;
}
}

View File

@@ -0,0 +1,174 @@
package io.eiren.gui.autobone;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map.Entry;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.util.logging.LogManager;
public final class PoseFrameIO {
private PoseFrameIO() {
// Do not allow instantiating
}
public static boolean writeToFile(File file, PoseFrame[] frames) {
try (DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
// Write every frame
outputStream.writeInt(frames.length);
for (PoseFrame frame : frames) {
writeFrame(outputStream, frame);
}
} catch (Exception e) {
LogManager.log.severe("Error writing frames to file", e);
return false;
}
return true;
}
public static boolean writeFrame(DataOutputStream outputStream, PoseFrame frame) {
try {
// Write root position vector
outputStream.writeFloat(frame.rootPos.x);
outputStream.writeFloat(frame.rootPos.y);
outputStream.writeFloat(frame.rootPos.z);
if (frame.rotations != null) {
// Write rotations
outputStream.writeInt(frame.rotations.size());
for (Entry<String, Quaternion> entry : frame.rotations.entrySet()) {
// Write the label string
outputStream.writeUTF(entry.getKey());
// Write the rotation quaternion
Quaternion quat = entry.getValue();
outputStream.writeFloat(quat.getX());
outputStream.writeFloat(quat.getY());
outputStream.writeFloat(quat.getZ());
outputStream.writeFloat(quat.getW());
}
} else {
outputStream.writeInt(0);
}
if (frame.positions != null) {
// Write positions
outputStream.writeInt(frame.positions.size());
for (Entry<String, Vector3f> entry : frame.positions.entrySet()) {
// Write the label string
outputStream.writeUTF(entry.getKey());
// Write the rotation quaternion
Vector3f vec = entry.getValue();
outputStream.writeFloat(vec.getX());
outputStream.writeFloat(vec.getY());
outputStream.writeFloat(vec.getZ());
}
} else {
outputStream.writeInt(0);
}
} catch (Exception e) {
LogManager.log.severe("Error writing frame to stream", e);
return false;
}
return true;
}
public static boolean writeFrame(File file, PoseFrame frame) {
try (DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
writeFrame(outputStream, frame);
} catch (Exception e) {
LogManager.log.severe("Error writing frame to file", e);
return false;
}
return true;
}
public static PoseFrame[] readFromFile(File file) {
try (DataInputStream inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {
int frameCount = inputStream.readInt();
PoseFrame[] frames = new PoseFrame[frameCount];
for (int i = 0; i < frameCount; i++) {
frames[i] = readFrame(inputStream);
}
return frames;
} catch (Exception e) {
LogManager.log.severe("Error reading frames from file", e);
}
return null;
}
public static PoseFrame readFrame(DataInputStream inputStream) {
try {
float vecX = inputStream.readFloat();
float vecY = inputStream.readFloat();
float vecZ = inputStream.readFloat();
Vector3f vector = new Vector3f(vecX, vecY, vecZ);
int rotationCount = inputStream.readInt();
HashMap<String, Quaternion> rotations = null;
if (rotationCount > 0) {
rotations = new HashMap<String, Quaternion>(rotationCount);
for (int j = 0; j < rotationCount; j++) {
String label = inputStream.readUTF();
float quatX = inputStream.readFloat();
float quatY = inputStream.readFloat();
float quatZ = inputStream.readFloat();
float quatW = inputStream.readFloat();
Quaternion quaternion = new Quaternion(quatX, quatY, quatZ, quatW);
rotations.put(label, quaternion);
}
}
int positionCount = inputStream.readInt();
HashMap<String, Vector3f> positions = null;
if (positionCount > 0) {
positions = new HashMap<String, Vector3f>(positionCount);
for (int j = 0; j < positionCount; j++) {
String label = inputStream.readUTF();
float posX = inputStream.readFloat();
float posY = inputStream.readFloat();
float posZ = inputStream.readFloat();
Vector3f position = new Vector3f(posX, posY, posZ);
positions.put(label, position);
}
}
return new PoseFrame(vector, rotations, positions);
} catch (Exception e) {
LogManager.log.severe("Error reading frame from stream", e);
}
return null;
}
public static PoseFrame readFrame(File file) {
try {
return readFrame(new DataInputStream(new BufferedInputStream(new FileInputStream(file))));
} catch (Exception e) {
LogManager.log.severe("Error reading frame from file", e);
}
return null;
}
}

View File

@@ -0,0 +1,136 @@
package io.eiren.gui.autobone;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.ann.VRServerThread;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.HumanSkeleton;
import io.eiren.vr.processor.HumanSkeletonWithLegs;
public class PoseRecorder {
protected final FastList<PoseFrame> frames = new FastList<PoseFrame>();
protected int numFrames = -1;
protected long frameRecordingInterval = 60L;
protected long nextFrameTimeMs = -1L;
protected CompletableFuture<PoseFrame[]> currentRecording;
protected final VRServer server;
HumanSkeletonWithLegs skeleton = null;
public PoseRecorder(VRServer server) {
this.server = server;
server.addOnTick(this::onTick);
server.addSkeletonUpdatedCallback(this::skeletonUpdated);
}
@VRServerThread
public void onTick() {
if (numFrames > 0) {
HumanSkeletonWithLegs skeleton = this.skeleton;
if (skeleton != null) {
if (frames.size() < numFrames) {
if (System.currentTimeMillis() >= nextFrameTimeMs) {
nextFrameTimeMs = System.currentTimeMillis() + frameRecordingInterval;
frames.add(new PoseFrame(skeleton));
// If done, send finished recording
if (frames.size() >= numFrames) {
internalStopRecording();
}
}
} else {
// If done and hasn't yet, send finished recording
internalStopRecording();
}
}
}
}
@ThreadSafe
public void skeletonUpdated(HumanSkeleton newSkeleton) {
if (newSkeleton instanceof HumanSkeletonWithLegs) {
skeleton = (HumanSkeletonWithLegs) newSkeleton;
}
}
public synchronized Future<PoseFrame[]> startFrameRecording(int numFrames, long interval) {
if (numFrames < 1) {
throw new IllegalArgumentException("numFrames must at least have a value of 1");
}
if (interval < 1) {
throw new IllegalArgumentException("interval must at least have a value of 1");
}
if (!isReadyToRecord()) {
throw new IllegalStateException("PoseRecorder isn't ready to record!");
}
cancelFrameRecording();
// Clear old frames and ensure new size can be held
frames.clear();
frames.ensureCapacity(numFrames);
this.numFrames = numFrames;
frameRecordingInterval = interval;
nextFrameTimeMs = -1L;
LogManager.log.info("[PoseRecorder] Recording " + numFrames + " samples at a " + interval + " ms frame interval");
currentRecording = new CompletableFuture<PoseFrame[]>();
return currentRecording;
}
private void internalStopRecording() {
CompletableFuture<PoseFrame[]> currentRecording = this.currentRecording;
if (currentRecording != null && !currentRecording.isDone()) {
// Stop the recording, returning the frames recorded
currentRecording.complete(frames.toArray(new PoseFrame[0]));
}
numFrames = -1;
}
public synchronized void stopFrameRecording() {
internalStopRecording();
}
public synchronized void cancelFrameRecording() {
CompletableFuture<PoseFrame[]> currentRecording = this.currentRecording;
if (currentRecording != null && !currentRecording.isDone()) {
// Cancel the current recording and return nothing
currentRecording.cancel(true);
}
numFrames = -1;
}
public boolean isReadyToRecord() {
return skeleton != null;
}
public boolean isRecording() {
return numFrames > frames.size();
}
public boolean hasRecording() {
return currentRecording != null;
}
public Future<PoseFrame[]> getFramesAsync() {
return currentRecording;
}
public PoseFrame[] getFrames() throws ExecutionException, InterruptedException {
CompletableFuture<PoseFrame[]> currentRecording = this.currentRecording;
return currentRecording != null ? currentRecording.get() : null;
}
}

View File

@@ -0,0 +1,291 @@
package io.eiren.gui.autobone;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.vr.processor.HumanSkeletonWithLegs;
import io.eiren.vr.processor.HumanSkeletonWithWaist;
import io.eiren.vr.processor.TransformNode;
import io.eiren.yaml.YamlFile;
public class SimpleSkeleton {
// Waist
protected final TransformNode hmdNode = new TransformNode("HMD", false);
protected final TransformNode headNode = new TransformNode("Head", false);
protected final TransformNode neckNode = new TransformNode("Neck", false);
protected final TransformNode waistNode = new TransformNode("Waist", false);
protected final TransformNode chestNode = new TransformNode("Chest", false);
protected float chestDistance = 0.42f;
/**
* Distance from eyes to waist
*/
protected float waistDistance = 0.85f;
/**
* Distance from eyes to the base of the neck
*/
protected float neckLength = HumanSkeletonWithWaist.NECK_LENGTH_DEFAULT;
/**
* Distance from eyes to ear
*/
protected float headShift = HumanSkeletonWithWaist.HEAD_SHIFT_DEFAULT;
// Legs
protected final TransformNode leftHipNode = new TransformNode("Left-Hip", false);
protected final TransformNode leftKneeNode = new TransformNode("Left-Knee", false);
protected final TransformNode leftAnkleNode = new TransformNode("Left-Ankle", false);
protected final TransformNode rightHipNode = new TransformNode("Right-Hip", false);
protected final TransformNode rightKneeNode = new TransformNode("Right-Knee", false);
protected final TransformNode rightAnkleNode = new TransformNode("Right-Ankle", false);
/**
* Distance between centers of both hips
*/
protected float hipsWidth = HumanSkeletonWithLegs.HIPS_WIDTH_DEFAULT;
/**
* Length from waist to knees
*/
protected float kneeHeight = 0.42f;
/**
* Distance from waist to ankle
*/
protected float legsLength = 0.84f;
protected final HashMap<String, TransformNode> nodes = new HashMap<String, TransformNode>();
public SimpleSkeleton() {
// Assemble skeleton to waist
hmdNode.attachChild(headNode);
headNode.localTransform.setTranslation(0, 0, headShift);
headNode.attachChild(neckNode);
neckNode.localTransform.setTranslation(0, -neckLength, 0);
neckNode.attachChild(chestNode);
chestNode.localTransform.setTranslation(0, -chestDistance, 0);
chestNode.attachChild(waistNode);
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
// Assemble skeleton to feet
waistNode.attachChild(leftHipNode);
leftHipNode.localTransform.setTranslation(-hipsWidth / 2, 0, 0);
waistNode.attachChild(rightHipNode);
rightHipNode.localTransform.setTranslation(hipsWidth / 2, 0, 0);
leftHipNode.attachChild(leftKneeNode);
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
rightHipNode.attachChild(rightKneeNode);
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
leftKneeNode.attachChild(leftAnkleNode);
leftAnkleNode.localTransform.setTranslation(0, -kneeHeight, 0);
rightKneeNode.attachChild(rightAnkleNode);
rightAnkleNode.localTransform.setTranslation(0, -kneeHeight, 0);
// Set up a HashMap to get nodes by name easily
hmdNode.depthFirstTraversal(visitor -> {
nodes.put(visitor.getName(), visitor);
});
}
public SimpleSkeleton(Iterable<Entry<String, Float>> configs, Iterable<Entry<String, Float>> altConfigs) {
// Initialize
this();
// Set configs
if (altConfigs != null) {
// Set alts first, so if there's any overlap it doesn't affect the values
setSkeletonConfigs(altConfigs);
}
setSkeletonConfigs(configs);
}
public SimpleSkeleton(Map<String, Float> configs, Map<String, Float> altConfigs) {
this(configs.entrySet(), altConfigs.entrySet());
}
public SimpleSkeleton(Iterable<Entry<String, Float>> configs) {
this(configs, null);
}
public SimpleSkeleton(Map<String, Float> configs) {
this(configs.entrySet());
}
public void setPoseFromSkeleton(HumanSkeletonWithLegs humanSkeleton) {
TransformNode rootNode = humanSkeleton.getRootNode();
// Copy headset position
hmdNode.localTransform.setTranslation(rootNode.localTransform.getTranslation());
// Copy all rotations
rootNode.depthFirstTraversal(visitor -> {
TransformNode targetNode = nodes.get(visitor.getName());
// Handle unexpected nodes gracefully
if (targetNode != null) {
targetNode.localTransform.setRotation(visitor.localTransform.getRotation());
}
});
}
public void setPoseFromFrame(PoseFrame frame) {
// Copy headset position
hmdNode.localTransform.setTranslation(frame.rootPos);
if (frame.rotations != null) {
// Copy all rotations
for (Entry<String, Quaternion> rotation : frame.rotations.entrySet()) {
TransformNode targetNode = nodes.get(rotation.getKey());
// Handle unexpected nodes gracefully
if (targetNode != null) {
targetNode.localTransform.setRotation(rotation.getValue());
}
}
}
updatePose();
}
public void setSkeletonConfigs(Iterable<Entry<String, Float>> configs) {
for (Entry<String, Float> config : configs) {
setSkeletonConfig(config.getKey(), config.getValue());
}
}
public void setSkeletonConfigs(Map<String, Float> configs) {
setSkeletonConfigs(configs.entrySet());
}
public void setSkeletonConfig(String joint, float newLength) {
setSkeletonConfig(joint, newLength, false);
}
public void setSkeletonConfig(String joint, float newLength, boolean updatePose) {
switch(joint) {
case "Head":
headShift = newLength;
headNode.localTransform.setTranslation(0, 0, headShift);
if (updatePose) {
headNode.update();
}
break;
case "Neck":
neckLength = newLength;
neckNode.localTransform.setTranslation(0, -neckLength, 0);
if (updatePose) {
neckNode.update();
}
break;
case "Waist":
waistDistance = newLength;
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
if (updatePose) {
waistNode.update();
}
break;
case "Chest":
chestDistance = newLength;
chestNode.localTransform.setTranslation(0, -chestDistance, 0);
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
if (updatePose) {
chestNode.update();
}
break;
case "Hips width":
hipsWidth = newLength;
leftHipNode.localTransform.setTranslation(-hipsWidth / 2, 0, 0);
rightHipNode.localTransform.setTranslation(hipsWidth / 2, 0, 0);
if (updatePose) {
leftHipNode.update();
rightHipNode.update();
}
break;
case "Knee height":
kneeHeight = newLength;
leftAnkleNode.localTransform.setTranslation(0, -kneeHeight, 0);
rightAnkleNode.localTransform.setTranslation(0, -kneeHeight, 0);
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
if (updatePose) {
leftKneeNode.update();
rightKneeNode.update();
}
break;
case "Legs length":
legsLength = newLength;
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
if (updatePose) {
leftKneeNode.update();
rightKneeNode.update();
}
break;
}
}
public Float getSkeletonConfig(String joint) {
switch(joint) {
case "Head":
return headShift;
case "Neck":
return neckLength;
case "Waist":
return waistDistance;
case "Chest":
return chestDistance;
case "Hips width":
return hipsWidth;
case "Knee height":
return kneeHeight;
case "Legs length":
return legsLength;
}
return null;
}
public void updatePose() {
hmdNode.update();
}
public Vector3f getNodePosition(String node) {
TransformNode transformNode = nodes.get(node);
return transformNode != null ? transformNode.worldTransform.getTranslation() : null;
}
public Vector3f getHMDPos() {
return hmdNode.worldTransform.getTranslation();
}
public Vector3f getLeftFootPos() {
return leftAnkleNode.worldTransform.getTranslation();
}
public Vector3f getRightFootPos() {
return rightAnkleNode.worldTransform.getTranslation();
}
public void saveConfigs(YamlFile config) {
// Save waist configs
config.setProperty("body.headShift", headShift);
config.setProperty("body.neckLength", neckLength);
config.setProperty("body.waistDistance", waistDistance);
config.setProperty("body.chestDistance", chestDistance);
// Save leg configs
config.setProperty("body.hipsWidth", hipsWidth);
config.setProperty("body.kneeHeight", kneeHeight);
config.setProperty("body.legsLength", legsLength);
}
}

View File

@@ -5,13 +5,13 @@ import java.io.File;
import io.eiren.gui.VRServerGUI;
import io.eiren.util.logging.LogManager;
public class Main {
public static String VERSION = "0.0.10";
public static String VERSION = "0.0.17";
public static VRServer vrServer;
@SuppressWarnings("unused")
public static void main(String[] args) {
System.setProperty("awt.useSystemAAFontSettings", "on");
System.setProperty("swing.aatext", "true");

View File

@@ -2,6 +2,7 @@ package io.eiren.vr;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
@@ -19,6 +20,7 @@ import io.eiren.util.ann.ThreadSecure;
import io.eiren.util.ann.VRServerThread;
import io.eiren.util.collections.FastList;
import io.eiren.vr.bridge.NamedPipeVRBridge;
import io.eiren.vr.bridge.SteamVRPipeInputBridge;
import io.eiren.vr.bridge.VMCBridge;
import io.eiren.vr.bridge.VRBridge;
import io.eiren.vr.processor.HumanPoseProcessor;
@@ -50,13 +52,17 @@ public class VRServer extends Thread {
hmdTracker = new HMDTracker("HMD");
hmdTracker.position.set(0, 1.8f, 0); // Set starting position for easier debugging
// TODO Multiple processors
humanPoseProcessor = new HumanPoseProcessor(this, hmdTracker);
humanPoseProcessor = new HumanPoseProcessor(this, hmdTracker, config.getInt("virtualtrackers", 3));
List<? extends Tracker> shareTrackers = humanPoseProcessor.getComputedTrackers();
// Create named pipe bridge for SteamVR driver
NamedPipeVRBridge driverBridge = new NamedPipeVRBridge(hmdTracker, shareTrackers, this);
tasks.add(() -> driverBridge.start());
bridges.add(driverBridge);
// Create named pipe bridge for SteamVR input
SteamVRPipeInputBridge steamVRInput = new SteamVRPipeInputBridge(this);
tasks.add(() -> steamVRInput.start());
bridges.add(steamVRInput);
// Create VMCBridge
try {
@@ -98,8 +104,8 @@ public class VRServer extends Thread {
private void loadConfig() {
try {
config.load(new FileInputStream(new File("vrconfig.yml")));
} catch(IOException e) {
e.printStackTrace();
} catch(FileNotFoundException e) {
// Config file didn't exist, is not an error
} catch(YamlException e) {
e.printStackTrace();
}
@@ -189,12 +195,13 @@ public class VRServer extends Thread {
break;
task.run();
} while(true);
for(int i = 0; i < onTick.size(); ++i) {
this.onTick.get(i).run();
}
for(int i = 0; i < bridges.size(); ++i)
bridges.get(i).dataRead();
for(int i = 0; i < trackers.size(); ++i)
trackers.get(i).tick();
humanPoseProcessor.update();
for(int i = 0; i < bridges.size(); ++i)
bridges.get(i).dataWrite();
@@ -234,6 +241,12 @@ public class VRServer extends Thread {
});
}
public void resetTrackersYaw() {
queueTask(() -> {
humanPoseProcessor.resetTrackersYaw();
});
}
public int getTrackersCount() {
return trackers.size();
}

View File

@@ -21,19 +21,20 @@ import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerStatus;
public class NamedPipeVRBridge extends Thread implements VRBridge {
private static final int MAX_COMMAND_LENGTH = 2048;
public static final String HMDPipeName = "\\\\.\\pipe\\HMDPipe";
public static final String TrackersPipeName = "\\\\.\\pipe\\TrackPipe";
public static final Charset ASCII = Charset.forName("ASCII");
private final byte[] buffer = new byte[1024];
private final byte[] buffArray = new byte[1024];
private final StringBuilder commandBuilder = new StringBuilder(1024);
private final StringBuilder sbBuffer = new StringBuilder(1024);
private final Vector3f vBuffer = new Vector3f();
private final Vector3f vBuffer2 = new Vector3f();
private final Quaternion qBuffer = new Quaternion();
private final Quaternion qBuffer2 = new Quaternion();
private final VRServer server;
private Pipe hmdPipe;
private final HMDTracker hmd;
private final List<Pipe> trackerPipes;
@@ -43,11 +44,8 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
private final HMDTracker internalHMDTracker = new HMDTracker("itnernal://HMD");
private final AtomicBoolean newHMDData = new AtomicBoolean(false);
private boolean spawnOneTracker = false;
public NamedPipeVRBridge(HMDTracker hmd, List<? extends Tracker> shareTrackers, VRServer server) {
super("Named Pipe VR Bridge");
this.server = server;
this.hmd = hmd;
this.shareTrackers = new FastList<>(shareTrackers);
this.trackerPipes = new FastList<>(shareTrackers.size());
@@ -58,26 +56,6 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
ct.setStatus(TrackerStatus.OK);
this.internalTrackers.add(ct);
}
this.spawnOneTracker = server.config.getBoolean("openvr.onetracker", spawnOneTracker);
}
public boolean isOneTrackerMode() {
return this.spawnOneTracker;
}
/**
* Makes OpenVR bridge spawn only 1 tracker instead of 3, for
* use with only waist/chest tracking. Requires restart.
*/
public void setSpawnOneTracker(boolean spawnOneTracker) {
if(spawnOneTracker == this.spawnOneTracker)
return;
this.spawnOneTracker = spawnOneTracker;
if(this.spawnOneTracker)
this.server.config.setProperty("openvr.onetracker", true);
else
this.server.config.removeProperty("openvr.onetracker");
this.server.saveConfig();
}
@Override
@@ -133,35 +111,31 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
if(tryOpeningPipe(trackerPipe))
initTrackerPipe(trackerPipe, i);
}
if(spawnOneTracker)
break;
}
}
public boolean updateHMD() {
public boolean updateHMD() throws IOException {
if(hmdPipe.state == PipeState.OPEN) {
IntByReference bytesAvailable = new IntByReference(0);
if(Kernel32.INSTANCE.PeekNamedPipe(hmdPipe.pipeHandle, null, 0, null, bytesAvailable, null)) {
if(bytesAvailable.getValue() > 0) {
if(Kernel32.INSTANCE.ReadFile(hmdPipe.pipeHandle, buffer, buffer.length, bytesAvailable, null)) {
String str = new String(buffer, 0, bytesAvailable.getValue() - 1, ASCII);
String[] split = str.split("\n")[0].split(" ");
try {
double x = Double.parseDouble(split[0]);
double y = Double.parseDouble(split[1]);
double z = Double.parseDouble(split[2]);
double qw = Double.parseDouble(split[3]);
double qx = Double.parseDouble(split[4]);
double qy = Double.parseDouble(split[5]);
double qz = Double.parseDouble(split[6]);
internalHMDTracker.position.set((float) x, (float) y, (float) z);
internalHMDTracker.rotation.set((float) qx, (float) qy, (float) qz, (float) qw);
internalHMDTracker.dataTick();
newHMDData.set(true);
} catch(NumberFormatException e) {
e.printStackTrace();
while(Kernel32.INSTANCE.ReadFile(hmdPipe.pipeHandle, buffArray, buffArray.length, bytesAvailable, null)) {
int bytesRead = bytesAvailable.getValue();
for(int i = 0; i < bytesRead; ++i) {
char c = (char) buffArray[i];
if(c == '\n') {
executeHMDInput();
commandBuilder.setLength(0);
} else {
commandBuilder.append(c);
if(commandBuilder.length() >= MAX_COMMAND_LENGTH) {
LogManager.log.severe("[VRBridge] Command from the pipe is too long, flushing buffer");
commandBuilder.setLength(0);
}
}
}
if(bytesRead < buffArray.length)
break; // Don't repeat, we read all available bytes
}
return true;
}
@@ -170,6 +144,30 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
return false;
}
private void executeHMDInput() throws IOException {
String[] split = commandBuilder.toString().split(" ");
if(split.length < 7) {
LogManager.log.severe("[VRBridge] Short HMD data recieved: " + commandBuilder.toString());
return;
}
try {
double x = Double.parseDouble(split[0]);
double y = Double.parseDouble(split[1]);
double z = Double.parseDouble(split[2]);
double qw = Double.parseDouble(split[3]);
double qx = Double.parseDouble(split[4]);
double qy = Double.parseDouble(split[5]);
double qz = Double.parseDouble(split[6]);
internalHMDTracker.position.set((float) x, (float) y, (float) z);
internalHMDTracker.rotation.set((float) qx, (float) qy, (float) qz, (float) qw);
internalHMDTracker.dataTick();
newHMDData.set(true);
} catch(NumberFormatException e) {
e.printStackTrace();
}
}
public void updateTracker(int trackerId, boolean hmdUpdated) {
Tracker sensor = internalTrackers.get(trackerId);
if(sensor.getStatus().sendData) {
@@ -181,10 +179,10 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
sbBuffer.append(vBuffer.x).append(' ').append(vBuffer.y).append(' ').append(vBuffer.z).append(' ');
sbBuffer.append(qBuffer.getW()).append(' ').append(qBuffer.getX()).append(' ').append(qBuffer.getY()).append(' ').append(qBuffer.getZ()).append('\n');
String str = sbBuffer.toString();
System.arraycopy(str.getBytes(ASCII), 0, buffer, 0, str.length());
buffer[str.length()] = '\0';
System.arraycopy(str.getBytes(ASCII), 0, buffArray, 0, str.length());
buffArray[str.length()] = '\0';
IntByReference lpNumberOfBytesWritten = new IntByReference(0);
Kernel32.INSTANCE.WriteFile(trackerPipe.pipeHandle, buffer, str.length() + 1, lpNumberOfBytesWritten, null);
Kernel32.INSTANCE.WriteFile(trackerPipe.pipeHandle, buffArray, str.length() + 1, lpNumberOfBytesWritten, null);
}
}
}
@@ -194,12 +192,12 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
}
private void initTrackerPipe(Pipe pipe, int trackerId) {
String trackerHello = (spawnOneTracker ? "1" : this.shareTrackers.size()) + " 0";
System.arraycopy(trackerHello.getBytes(ASCII), 0, buffer, 0, trackerHello.length());
buffer[trackerHello.length()] = '\0';
String trackerHello = this.shareTrackers.size() + " 0";
System.arraycopy(trackerHello.getBytes(ASCII), 0, buffArray, 0, trackerHello.length());
buffArray[trackerHello.length()] = '\0';
IntByReference lpNumberOfBytesWritten = new IntByReference(0);
Kernel32.INSTANCE.WriteFile(pipe.pipeHandle,
buffer,
buffArray,
trackerHello.length() + 1,
lpNumberOfBytesWritten,
null);
@@ -207,7 +205,7 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
private boolean tryOpeningPipe(Pipe pipe) {
if(Kernel32.INSTANCE.ConnectNamedPipe(pipe.pipeHandle, null)) {
pipe.state = NamedPipeVRBridge.PipeState.OPEN;
pipe.state = PipeState.OPEN;
LogManager.log.info("[VRBridge] Pipe " + pipe.name + " is open");
return true;
}
@@ -252,8 +250,6 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
throw new IOException("Can't open " + pipeName + " pipe: " + Kernel32.INSTANCE.GetLastError());
LogManager.log.info("[VRBridge] Pipe " + pipeName + " created");
trackerPipes.add(new Pipe(pipeHandle, pipeName));
if(spawnOneTracker)
break;
}
LogManager.log.info("[VRBridge] Pipes are open");
} catch(IOException e) {
@@ -272,21 +268,4 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
} catch(Exception e) {
}
}
private static class Pipe {
final String name;
final HANDLE pipeHandle;
PipeState state = PipeState.CREATED;
public Pipe(HANDLE pipeHandle, String name) {
this.pipeHandle = pipeHandle;
this.name = name;
}
}
private static enum PipeState {
CREATED,
OPEN,
ERROR;
}
}

View File

@@ -0,0 +1,14 @@
package io.eiren.vr.bridge;
import com.sun.jna.platform.win32.WinNT.HANDLE;
class Pipe {
final String name;
final HANDLE pipeHandle;
PipeState state = PipeState.CREATED;
public Pipe(HANDLE pipeHandle, String name) {
this.pipeHandle = pipeHandle;
this.name = name;
}
}

View File

@@ -0,0 +1,7 @@
package io.eiren.vr.bridge;
enum PipeState {
CREATED,
OPEN,
ERROR;
}

View File

@@ -0,0 +1,275 @@
package io.eiren.vr.bridge;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.StringUtils;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.ptr.IntByReference;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.TrackerBodyPosition;
import io.eiren.vr.trackers.SteamVRTracker;
import io.eiren.vr.trackers.TrackerStatus;
public class SteamVRPipeInputBridge extends Thread implements VRBridge {
private static final int MAX_COMMAND_LENGTH = 2048;
public static final String PipeName = "\\\\.\\pipe\\SlimeVRInput";
private final byte[] buffArray = new byte[1024];
private final VRServer server;
private final StringBuilder commandBuilder = new StringBuilder(1024);
private final List<SteamVRTracker> trackers = new FastList<>();
private final Map<Integer, SteamVRTracker> trackersInternal = new HashMap<>();
private AtomicBoolean newData = new AtomicBoolean(false);
private final Vector3f vBuffer = new Vector3f();
private final Quaternion qBuffer = new Quaternion();
private Pipe pipe;
public SteamVRPipeInputBridge(VRServer server) {
this.server = server;
}
@Override
public void run() {
try {
createPipes();
while(true) {
waitForPipesToOpen();
if(areAllPipesOpen()) {
boolean pipesUpdated = updatePipes(); // Update at HMDs frequency
if(!pipesUpdated) {
Thread.sleep(5); // Up to 200Hz
}
} else {
Thread.sleep(10);
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
private void waitForPipesToOpen() {
if(pipe.state == PipeState.CREATED) {
tryOpeningPipe(pipe);
}
}
public boolean updatePipes() throws IOException {
if(pipe.state == PipeState.OPEN) {
IntByReference bytesAvailable = new IntByReference(0);
if(Kernel32.INSTANCE.PeekNamedPipe(pipe.pipeHandle, null, 0, null, bytesAvailable, null)) {
if(bytesAvailable.getValue() > 0) {
while(Kernel32.INSTANCE.ReadFile(pipe.pipeHandle, buffArray, buffArray.length, bytesAvailable, null)) {
int bytesRead = bytesAvailable.getValue();
for(int i = 0; i < bytesRead; ++i) {
char c = (char) buffArray[i];
if(c == '\n') {
executeInputCommand();
commandBuilder.setLength(0);
} else {
commandBuilder.append(c);
if(commandBuilder.length() >= MAX_COMMAND_LENGTH) {
LogManager.log.severe("[SteamVRPipeInputBridge] Command from the pipe is too long, flushing buffer");
commandBuilder.setLength(0);
}
}
}
if(bytesRead < buffArray.length)
break; // Don't repeat, we read all available bytes
}
return true;
}
}
}
return false;
}
private void executeInputCommand() throws IOException {
String[] command = commandBuilder.toString().split(" ");
switch(command[0]) {
case "ADD": // Add new tracker
if(command.length < 4) {
LogManager.log.severe("[SteamVRPipeInputBridge] Error in ADD command. Command requires at least 4 arguments. Supplied: " + commandBuilder.toString());
return;
}
SteamVRTracker internalTracker = new SteamVRTracker(Integer.parseInt(command[1]), StringUtils.join(command, " ", 3, command.length));
int roleId = Integer.parseInt(command[2]);
if(roleId >= 0 && roleId < SteamVRInputRoles.values.length) {
SteamVRInputRoles svrRole = SteamVRInputRoles.values[roleId];
internalTracker.bodyPosition = svrRole.bodyPosition;
}
SteamVRTracker oldTracker;
synchronized(trackersInternal) {
oldTracker = trackersInternal.put(internalTracker.id, internalTracker);
}
if(oldTracker != null) {
LogManager.log.severe("[SteamVRPipeInputBridge] New tracker added with the same id. Supplied: " + commandBuilder.toString());
return;
}
newData.set(true);
break;
case "UPD": // Update tracker data
if(command.length < 9) {
LogManager.log.severe("[SteamVRPipeInputBridge] Error in UPD command. Command requires at least 9 arguments. Supplied: " + commandBuilder.toString());
return;
}
int id = Integer.parseInt(command[1]);
double x = Double.parseDouble(command[2]);
double y = Double.parseDouble(command[3]);
double z = Double.parseDouble(command[4]);
double qw = Double.parseDouble(command[5]);
double qx = Double.parseDouble(command[6]);
double qy = Double.parseDouble(command[7]);
double qz = Double.parseDouble(command[8]);
internalTracker = trackersInternal.get(id);
if(internalTracker != null) {
internalTracker.position.set((float) x, (float) y, (float) z);
internalTracker.rotation.set((float) qx, (float) qy, (float) qz, (float) qw);
internalTracker.dataTick();
newData.set(true);
}
break;
case "STA": // Update tracker status
if(command.length < 3) {
LogManager.log.severe("[SteamVRPipeInputBridge] Error in STA command. Command requires at least 3 arguments. Supplied: " + commandBuilder.toString());
return;
}
id = Integer.parseInt(command[1]);
int status = Integer.parseInt(command[2]);
TrackerStatus st = TrackerStatus.getById(status);
if(st == null) {
LogManager.log.severe("[SteamVRPipeInputBridge] Unrecognized status id. Supplied: " + commandBuilder.toString());
return;
}
internalTracker = trackersInternal.get(id);
if(internalTracker != null) {
internalTracker.setStatus(st);
newData.set(true);
}
break;
}
}
@Override
public void dataRead() {
// Not used, only input
}
@Override
public void dataWrite() {
if(newData.getAndSet(false)) {
if(trackers.size() < trackersInternal.size()) {
// Add new trackers
synchronized(trackersInternal) {
Iterator<SteamVRTracker> iterator = trackersInternal.values().iterator();
internal: while(iterator.hasNext()) {
SteamVRTracker internalTracker = iterator.next();
for(int i = 0; i < trackers.size(); ++i) {
SteamVRTracker t = trackers.get(i);
if(t.id == internalTracker.id)
continue internal;
}
// Tracker is not found in current trackers
SteamVRTracker tracker = new SteamVRTracker(internalTracker.id, internalTracker.getName());
tracker.bodyPosition = internalTracker.bodyPosition;
trackers.add(tracker);
server.registerTracker(tracker);
}
}
}
for(int i = 0; i < trackers.size(); ++i) {
SteamVRTracker tracker = trackers.get(i);
SteamVRTracker internal = trackersInternal.get(tracker.id);
if(internal == null)
throw new NullPointerException("Lost internal tracker somehow: " + tracker.id); // Shouln't really happen even, but better to catch it like this
if(internal.getPosition(vBuffer))
tracker.position.set(vBuffer);
if(internal.getRotation(qBuffer))
tracker.rotation.set(qBuffer);
tracker.dataTick();
}
}
}
private boolean tryOpeningPipe(Pipe pipe) {
if(Kernel32.INSTANCE.ConnectNamedPipe(pipe.pipeHandle, null)) {
pipe.state = PipeState.OPEN;
LogManager.log.info("[SteamVRPipeInputBridge] Pipe " + pipe.name + " is open");
return true;
}
LogManager.log.info("[SteamVRPipeInputBridge] Error connecting to pipe " + pipe.name + ": " + Kernel32.INSTANCE.GetLastError());
return false;
}
private boolean areAllPipesOpen() {
if(pipe == null || pipe.state == PipeState.CREATED) {
return false;
}
return true;
}
private void createPipes() throws IOException {
try {
pipe = new Pipe(Kernel32.INSTANCE.CreateNamedPipe(PipeName, WinBase.PIPE_ACCESS_DUPLEX, // dwOpenMode
WinBase.PIPE_TYPE_BYTE | WinBase.PIPE_READMODE_BYTE | WinBase.PIPE_WAIT, // dwPipeMode
1, // nMaxInstances,
1024 * 16, // nOutBufferSize,
1024 * 16, // nInBufferSize,
0, // nDefaultTimeOut,
null), PipeName); // lpSecurityAttributes
LogManager.log.info("[SteamVRPipeInputBridge] Pipe " + pipe.name + " created");
if(WinBase.INVALID_HANDLE_VALUE.equals(pipe.pipeHandle))
throw new IOException("Can't open " + PipeName + " pipe: " + Kernel32.INSTANCE.GetLastError());
LogManager.log.info("[SteamVRPipeInputBridge] Pipes are open");
} catch(IOException e) {
safeDisconnect(pipe);
throw e;
}
}
public static void safeDisconnect(Pipe pipe) {
try {
if(pipe != null && pipe.pipeHandle != null)
Kernel32.INSTANCE.DisconnectNamedPipe(pipe.pipeHandle);
} catch(Exception e) {
}
}
public enum SteamVRInputRoles {
HEAD(TrackerBodyPosition.HMD),
LEFT_HAND(TrackerBodyPosition.LEFT_CONTROLLER),
RIGHT_HAND(TrackerBodyPosition.RIGHT_CONTROLLER),
LEFT_FOOT(TrackerBodyPosition.LEFT_FOOT),
RIGHT_FOOT(TrackerBodyPosition.RIGHT_FOOT),
LEFT_SHOULDER(TrackerBodyPosition.NONE),
RIGHT_SHOULDER(TrackerBodyPosition.NONE),
LEFT_ELBOW(TrackerBodyPosition.NONE),
RIGHT_ELBOW(TrackerBodyPosition.NONE),
LEFT_KNEE(TrackerBodyPosition.LEFT_LEG),
RIGHT_KNEE(TrackerBodyPosition.RIGHT_LEG),
WAIST(TrackerBodyPosition.WAIST),
CHEST(TrackerBodyPosition.CHEST),
;
private static final SteamVRInputRoles[] values = values();
public final TrackerBodyPosition bodyPosition;
private SteamVRInputRoles(TrackerBodyPosition slimeVrPosition) {
this.bodyPosition = slimeVrPosition;
}
}
}

View File

@@ -5,5 +5,7 @@ public enum ComputedHumanPoseTrackerPosition {
WAIST,
CHEST,
LEFT_FOOT,
RIGHT_FOOT;
RIGHT_FOOT,
LEFT_KNEE,
RIGHT_KNEE;
}

View File

@@ -19,11 +19,24 @@ public class HumanPoseProcessor {
private final List<Consumer<HumanSkeleton>> onSkeletonUpdated = new FastList<>();
private HumanSkeleton skeleton;
public HumanPoseProcessor(VRServer server, HMDTracker hmd) {
public HumanPoseProcessor(VRServer server, HMDTracker hmd, int trackersAmount) {
this.server = server;
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.WAIST));
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.LEFT_FOOT));
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.RIGHT_FOOT));
if(trackersAmount > 2) {
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.LEFT_FOOT));
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.RIGHT_FOOT));
if(trackersAmount == 4 || trackersAmount >= 6) {
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.CHEST));
}
if(trackersAmount >= 5) {
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.LEFT_KNEE));
computedTrackers.add(new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.RIGHT_KNEE));
}
}
}
public HumanSkeleton getSkeleton() {
return skeleton;
}
@VRServerThread
@@ -76,24 +89,22 @@ public class HumanPoseProcessor {
boolean hasBothLegs = false;
List<Tracker> allTrackers = server.getAllTrackers();
Tracker waist = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
Tracker leftAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.LEFT_ANKLE);
Tracker rightAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.RIGHT_ANKLE);
Tracker leftLeg = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.LEFT_LEG);
Tracker rightLeg = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.RIGHT_LEG);
Tracker leftAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
Tracker rightAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
if(waist != null)
hasWaist = true;
if(leftAnkle != null && rightAnkle != null && leftLeg != null && rightLeg != null)
if(leftAnkle != null && rightAnkle != null)
hasBothLegs = true;
if(!hasWaist) {
skeleton = null; // Can't track anything without waist
} else if(hasBothLegs) {
disconnectAllTrackers();
skeleton = new HumanSekeletonWithLegs(server, computedTrackers);
skeleton = new HumanSkeletonWithLegs(server, computedTrackers);
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
onSkeletonUpdated.get(i).accept(skeleton);
} else {
disconnectAllTrackers();
skeleton = new HumanSkeleonWithWaist(server, computedTrackers);
skeleton = new HumanSkeletonWithWaist(server, computedTrackers);
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
onSkeletonUpdated.get(i).accept(skeleton);
}
@@ -117,4 +128,10 @@ public class HumanPoseProcessor {
if(skeleton != null)
skeleton.resetTrackersFull();
}
@VRServerThread
public void resetTrackersYaw() {
if(skeleton != null)
skeleton.resetTrackersYaw();
}
}

View File

@@ -24,4 +24,7 @@ public abstract class HumanSkeleton {
@VRServerThread
public abstract void resetTrackersFull();
@VRServerThread
public abstract void resetTrackersYaw();
}

View File

@@ -12,25 +12,28 @@ import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerStatus;
import io.eiren.vr.trackers.TrackerUtils;
public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
public static final float HIPS_WIDTH_DEFAULT = 0.3f;
public static final float FOOT_LENGTH_DEFAULT = 0.05f;
public static final float DEFAULT_FLOOR_OFFSET = 0.05f;
protected final float[] kneeAngles = new float[3];
protected final float[] hipAngles = new float[3];
protected final Quaternion hipBuf = new Quaternion();
protected final Quaternion kneeBuf = new Quaternion();
protected final Vector3f hipVector = new Vector3f();
protected final Vector3f ankleVector = new Vector3f();
protected final Quaternion kneeRotation = new Quaternion();
protected final Tracker leftLegTracker;
protected final Tracker leftAnkleTracker;
protected final Tracker leftFootTracker;
protected final ComputedHumanPoseTracker computedLeftFootTracker;
protected final ComputedHumanPoseTracker computedLeftKneeTracker;
protected final Tracker rightLegTracker;
protected final Tracker rightAnkleTracker;
protected final Tracker rightFootTracker;
protected final ComputedHumanPoseTracker computedRightFootTracker;
protected final ComputedHumanPoseTracker computedRightKneeTracker;
protected final TransformNode leftHipNode = new TransformNode("Left-Hip", false);
protected final TransformNode leftKneeNode = new TransformNode("Left-Knee", false);
@@ -59,33 +62,46 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
protected float maxKneePitch = 90f * FastMath.DEG_TO_RAD;
protected float kneeLerpFactor = 0.5f;
protected boolean extendedPelvisModel = true;
protected boolean extendedKneeModel = false;
public HumanSekeletonWithLegs(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
public HumanSkeletonWithLegs(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
super(server, computedTrackers);
List<Tracker> allTracekrs = server.getAllTrackers();
this.leftLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_LEG);
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_ANKLE);
this.leftLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_LEG, TrackerBodyPosition.LEFT_ANKLE);
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
this.leftFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_FOOT);
this.rightLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_LEG);
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_ANKLE);
this.rightLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_LEG, TrackerBodyPosition.RIGHT_ANKLE);
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
this.rightFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_FOOT);
ComputedHumanPoseTracker lat = null;
ComputedHumanPoseTracker rat = null;
ComputedHumanPoseTracker rkt = null;
ComputedHumanPoseTracker lkt = null;
for(int i = 0; i < computedTrackers.size(); ++i) {
ComputedHumanPoseTracker t = computedTrackers.get(i);
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.LEFT_FOOT)
lat = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.RIGHT_FOOT)
rat = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.LEFT_KNEE)
lkt = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.RIGHT_KNEE)
rkt = t;
}
computedLeftFootTracker = lat;
computedRightFootTracker = rat;
computedLeftKneeTracker = lkt;
computedRightKneeTracker = rkt;
lat.setStatus(TrackerStatus.OK);
rat.setStatus(TrackerStatus.OK);
hipsWidth = server.config.getFloat("body.hipsWidth", hipsWidth);
kneeHeight = server.config.getFloat("body.kneeHeight", kneeHeight);
legsLength = server.config.getFloat("body.legsLength", legsLength);
footLength = server.config.getFloat("body.footLength", footLength);
extendedPelvisModel = server.config.getBoolean("body.model.extendedPelvis", extendedPelvisModel);
extendedKneeModel = server.config.getBoolean("body.model.extendedKnee", extendedKneeModel);
waistNode.attachChild(leftHipNode);
leftHipNode.localTransform.setTranslation(-hipsWidth / 2, 0, 0);
@@ -181,6 +197,34 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
}
}
@Override
public boolean getSkeletonConfigBoolean(String config) {
switch(config) {
case "Extended pelvis model":
return extendedPelvisModel;
case "Extended knee model":
return extendedKneeModel;
}
return super.getSkeletonConfigBoolean(config);
}
@Override
public void setSkeletonConfigBoolean(String config, boolean newState) {
switch(config) {
case "Extended pelvis model":
extendedPelvisModel = newState;
server.config.setProperty("body.model.extendedPelvis", newState);
break;
case "Extended knee model":
extendedKneeModel = newState;
server.config.setProperty("body.model.extendedKnee", newState);
break;
default:
super.setSkeletonConfigBoolean(config, newState);
break;
}
}
@Override
public void updateLocalTransforms() {
super.updateLocalTransforms();
@@ -188,7 +232,8 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
leftLegTracker.getRotation(hipBuf);
leftAnkleTracker.getRotation(kneeBuf);
//calculateKneeLimits(hipBuf, kneeBuf, leftLegTracker.getConfidenceLevel(), leftAnkleTracker.getConfidenceLevel());
if(extendedKneeModel)
calculateKneeLimits(hipBuf, kneeBuf, leftLegTracker.getConfidenceLevel(), leftAnkleTracker.getConfidenceLevel());
leftHipNode.localTransform.setRotation(hipBuf);
leftKneeNode.localTransform.setRotation(kneeBuf);
@@ -205,7 +250,8 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
rightLegTracker.getRotation(hipBuf);
rightAnkleTracker.getRotation(kneeBuf);
//calculateKneeLimits(hipBuf, kneeBuf, rightLegTracker.getConfidenceLevel(), rightAnkleTracker.getConfidenceLevel());
if(extendedKneeModel)
calculateKneeLimits(hipBuf, kneeBuf, rightLegTracker.getConfidenceLevel(), rightAnkleTracker.getConfidenceLevel());
rightHipNode.localTransform.setRotation(hipBuf);
rightKneeNode.localTransform.setRotation(kneeBuf);
@@ -217,20 +263,36 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
rightAnkleNode.localTransform.setRotation(kneeBuf);
rightFootNode.localTransform.setRotation(kneeBuf);
}
// TODO Calculate waist node as some function between waist and hip rotations
if(extendedPelvisModel) {
// Average pelvis between two legs
leftHipNode.localTransform.getRotation(hipBuf);
rightHipNode.localTransform.getRotation(kneeBuf);
kneeBuf.slerp(hipBuf, 0.5f);
waistNode.localTransform.setRotation(kneeBuf);
// TODO : Use vectors to add like 50% of wasit tracker yaw to waist node to reduce drift and let user take weird poses
// TODO Set virtual waist node yaw to that of waist node
}
}
// Knee basically has only 1 DoF (pitch), average yaw between knee and hip
// Knee basically has only 1 DoF (pitch), average yaw and roll between knee and hip
protected void calculateKneeLimits(Quaternion hipBuf, Quaternion kneeBuf, float hipConfidense, float kneeConfidense) {
hipBuf.toAngles(hipAngles);
kneeBuf.toAngles(kneeAngles);
ankleVector.set(0, -1, 0);
hipVector.set(0, -1, 0);
hipBuf.multLocal(hipVector);
kneeBuf.multLocal(ankleVector);
kneeRotation.angleBetweenVectors(hipVector, ankleVector); // Find knee angle
hipAngles[1] = kneeAngles[1] = interpolateRadians(kneeLerpFactor, kneeAngles[1], hipAngles[1]);
//hipAngles[2] = kneeAngles[2] = interpolateRadians(kneeLerpFactor, kneeAngles[2], hipAngles[2]);
// Substract knee angle from knee rotation. With perfect leg and perfect
// sensors result should match hip rotation perfectly
kneeBuf.multLocal(kneeRotation.inverse());
hipBuf.fromAngles(hipAngles);
kneeBuf.fromAngles(kneeAngles);
// Average knee and hip with a slerp
hipBuf.slerp(kneeBuf, 0.5f); // TODO : Use confidence to calculate changeAmt
kneeBuf.set(hipBuf);
// Return knee angle into knee rotation
kneeBuf.multLocal(kneeRotation);
}
public static float normalizeRad(float angle) {
@@ -254,13 +316,29 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
protected void updateComputedTrackers() {
super.updateComputedTrackers();
computedLeftFootTracker.position.set(leftFootNode.worldTransform.getTranslation());
computedLeftFootTracker.rotation.set(leftFootNode.worldTransform.getRotation());
computedLeftFootTracker.dataTick();
if(computedLeftFootTracker != null) {
computedLeftFootTracker.position.set(leftFootNode.worldTransform.getTranslation());
computedLeftFootTracker.rotation.set(leftFootNode.worldTransform.getRotation());
computedLeftFootTracker.dataTick();
}
computedRightFootTracker.position.set(rightFootNode.worldTransform.getTranslation());
computedRightFootTracker.rotation.set(rightFootNode.worldTransform.getRotation());
computedRightFootTracker.dataTick();
if(computedLeftKneeTracker != null) {
computedLeftKneeTracker.position.set(leftKneeNode.worldTransform.getTranslation());
computedLeftKneeTracker.rotation.set(leftHipNode.worldTransform.getRotation());
computedLeftKneeTracker.dataTick();
}
if(computedRightFootTracker != null) {
computedRightFootTracker.position.set(rightFootNode.worldTransform.getTranslation());
computedRightFootTracker.rotation.set(rightFootNode.worldTransform.getRotation());
computedRightFootTracker.dataTick();
}
if(computedRightKneeTracker != null) {
computedRightKneeTracker.position.set(rightKneeNode.worldTransform.getTranslation());
computedRightKneeTracker.rotation.set(rightHipNode.worldTransform.getRotation());
computedRightKneeTracker.dataTick();
}
}
@Override
@@ -289,8 +367,39 @@ public class HumanSekeletonWithLegs extends HumanSkeleonWithWaist {
this.rightAnkleTracker.resetFull(referenceRotation);
this.rightAnkleTracker.getRotation(referenceRotation);
if(this.rightAnkleTracker != null) {
this.rightAnkleTracker.resetFull(referenceRotation);
if(this.rightFootTracker != null) {
this.rightFootTracker.resetFull(referenceRotation);
}
}
@Override
@VRServerThread
public void resetTrackersYaw() {
// Each tracker uses the tracker before it to adjust iteself,
// so trackers that don't need adjustments could be used too
super.resetTrackersYaw();
// Start with waist, it was reset in the parent
Quaternion referenceRotation = new Quaternion();
this.waistTracker.getRotation(referenceRotation);
this.leftLegTracker.resetYaw(referenceRotation);
this.rightLegTracker.resetYaw(referenceRotation);
this.leftLegTracker.getRotation(referenceRotation);
this.leftAnkleTracker.resetYaw(referenceRotation);
this.leftAnkleTracker.getRotation(referenceRotation);
if(this.leftFootTracker != null) {
this.leftFootTracker.resetYaw(referenceRotation);
}
this.rightLegTracker.getRotation(referenceRotation);
this.rightAnkleTracker.resetYaw(referenceRotation);
this.rightAnkleTracker.getRotation(referenceRotation);
if(this.rightFootTracker != null) {
this.rightFootTracker.resetYaw(referenceRotation);
}
}
}

View File

@@ -14,14 +14,14 @@ import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerStatus;
import io.eiren.vr.trackers.TrackerUtils;
public class HumanSkeleonWithWaist extends HumanSkeleton {
public class HumanSkeletonWithWaist extends HumanSkeleton {
public static final float HEAD_SHIFT_DEFAULT = 0.1f;
public static final float NECK_LENGTH_DEFAULT = 0.1f;
protected final Map<String, Float> configMap = new HashMap<>();
protected final VRServer server;
protected final float[] waistAngles = new float[3];
protected final Quaternion qBuf = new Quaternion();
protected final Vector3f vBuf = new Vector3f();
@@ -30,6 +30,7 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
protected final Tracker chestTracker;
protected final HMDTracker hmdTracker;
protected final ComputedHumanPoseTracker computedWaistTracker;
protected final ComputedHumanPoseTracker computedChestTracker;
protected final TransformNode hmdNode = new TransformNode("HMD", false);
protected final TransformNode headNode = new TransformNode("Head", false);
protected final TransformNode neckNode = new TransformNode("Neck", false);
@@ -56,20 +57,24 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
* Distance from eyes to ear
*/
protected float headShift = HEAD_SHIFT_DEFAULT;
public HumanSkeleonWithWaist(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
public HumanSkeletonWithWaist(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
List<Tracker> allTracekrs = server.getAllTrackers();
this.waistTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
this.chestTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.CHEST, TrackerBodyPosition.WAIST);
this.hmdTracker = server.hmdTracker;
this.server = server;
ComputedHumanPoseTracker cwt = null;
ComputedHumanPoseTracker cct = null;
for(int i = 0; i < computedTrackers.size(); ++i) {
ComputedHumanPoseTracker t = computedTrackers.get(i);
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.WAIST)
cwt = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.CHEST)
cct = t;
}
computedWaistTracker = cwt;
computedChestTracker = cct;
cwt.setStatus(TrackerStatus.OK);
headShift = server.config.getFloat("body.headShift", headShift);
neckLength = server.config.getFloat("body.neckLength", neckLength);
@@ -109,16 +114,16 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
resetSkeletonConfig("Waist");
resetSkeletonConfig("Chest");
break;
case "Head":
case "Head":
setSkeletonConfig(joint, HEAD_SHIFT_DEFAULT);
break;
case "Neck":
case "Neck":
setSkeletonConfig(joint, NECK_LENGTH_DEFAULT);
break;
case "Virtual waist":
case "Virtual waist":
setSkeletonConfig(joint, 0.0f);
break;
case "Chest":
case "Chest":
setSkeletonConfig(joint, waistDistance / 2.0f);
break;
case "Waist": // Puts Waist in the middle of the height
@@ -136,7 +141,7 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
public Map<String, Float> getSkeletonConfig() {
return configMap;
}
@Override
public void setSkeletonConfig(String joint, float newLength) {
configMap.put(joint, newLength);
@@ -172,6 +177,13 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
}
}
public boolean getSkeletonConfigBoolean(String config) {
return false;
}
public void setSkeletonConfigBoolean(String config, boolean newState) {
}
@Override
public TransformNode getRootNode() {
return hmdNode;
@@ -193,7 +205,7 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
hmdNode.localTransform.setRotation(qBuf);
headNode.localTransform.setRotation(qBuf);
}
if(chestTracker.getRotation(qBuf))
neckNode.localTransform.setRotation(qBuf);
@@ -205,9 +217,17 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
}
protected void updateComputedTrackers() {
computedWaistTracker.position.set(trackerWaistNode.worldTransform.getTranslation());
computedWaistTracker.rotation.set(trackerWaistNode.worldTransform.getRotation());
computedWaistTracker.dataTick();
if(computedWaistTracker != null) {
computedWaistTracker.position.set(trackerWaistNode.worldTransform.getTranslation());
computedWaistTracker.rotation.set(trackerWaistNode.worldTransform.getRotation());
computedWaistTracker.dataTick();
}
if(computedChestTracker != null) {
computedChestTracker.position.set(chestNode.worldTransform.getTranslation());
computedChestTracker.rotation.set(neckNode.worldTransform.getRotation());
computedChestTracker.dataTick();
}
}
@Override
@@ -223,4 +243,18 @@ public class HumanSkeleonWithWaist extends HumanSkeleton {
this.waistTracker.resetFull(referenceRotation);
}
@Override
@VRServerThread
public void resetTrackersYaw() {
// Each tracker uses the tracker before it to adjust iteself,
// so trackers that don't need adjustments could be used too
Quaternion referenceRotation = new Quaternion();
server.hmdTracker.getRotation(referenceRotation);
this.chestTracker.resetYaw(referenceRotation);
this.chestTracker.getRotation(referenceRotation);
this.waistTracker.resetYaw(referenceRotation);
}
}

View File

@@ -15,6 +15,8 @@ public enum TrackerBodyPosition {
RIGHT_ANKLE("body:right_ankle"),
LEFT_FOOT("body:left_foot"),
RIGHT_FOOT("body:right_foot"),
LEFT_CONTROLLER("body:left_controller"),
RIGHT_CONTROLLER("body:right_conroller"),
;
public final String designation;

View File

@@ -0,0 +1,21 @@
package io.eiren.vr.trackers;
public class BnoTap {
public final boolean doubleTap;
public BnoTap(int tapBits) {
doubleTap = (tapBits & 0x40) > 0;
}
@Override
public String toString() {
return "Tap{" + (doubleTap ? "double" : "") + "}";
}
public enum TapAxis {
X,
Y,
Z;
}
}

View File

@@ -80,4 +80,8 @@ public class ComputedTracker implements Tracker {
public boolean userEditable() {
return false;
}
@Override
public void tick() {
}
}

View File

@@ -1,29 +0,0 @@
package io.eiren.vr.trackers;
public class IMUReferenceAdjustedTracker<T extends IMUTracker & TrackerWithTPS & TrackerWithBattery> extends ReferenceAdjustedTracker<T> implements TrackerWithTPS, TrackerWithBattery {
public IMUReferenceAdjustedTracker(T tracker) {
super(tracker);
}
@Override
public float getBatteryLevel() {
return tracker.getBatteryLevel();
}
@Override
public float getBatteryVoltage() {
return tracker.getBatteryVoltage();
}
@Override
public float getTPS() {
return tracker.getTPS();
}
@Override
public void dataTick() {
tracker.dataTick();
}
}

View File

@@ -1,5 +1,6 @@
package io.eiren.vr.trackers;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
@@ -9,11 +10,15 @@ import io.eiren.vr.processor.TrackerBodyPosition;
public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
public static final float MAX_MAG_CORRECTION_ACCURACY = 5 * FastMath.RAD_TO_DEG;
public final Vector3f gyroVector = new Vector3f();
public final Vector3f accelVector = new Vector3f();
public final Vector3f magVector = new Vector3f();
public final Quaternion rotQuaternion = new Quaternion();
public final Quaternion rotMagQuaternion = new Quaternion();
protected final Quaternion rotAdjust = new Quaternion();
protected final Quaternion correction = new Quaternion();
protected TrackerMountingRotation mounting = null;
protected TrackerStatus status = TrackerStatus.OK;
@@ -21,6 +26,11 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
protected final TrackersUDPServer server;
protected float confidence = 0;
protected float batteryVoltage = 0;
public int calibrationStatus = 0;
public int magCalibrationStatus = 0;
public float magnetometerAccuracy = 0;
protected boolean magentometerCalibrated = false;
public boolean hasNewCorrectionData = false;
protected BufferedTimer timer = new BufferedTimer(1f);
public int ping = -1;
@@ -68,6 +78,18 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
}
}
@Override
public void tick() {
if(magentometerCalibrated && hasNewCorrectionData) {
hasNewCorrectionData = false;
if(magnetometerAccuracy <= MAX_MAG_CORRECTION_ACCURACY) {
// Adjust gyro rotation to match magnetometer rotation only if magnetometer
// accuracy is within the parameters
calculateLiveMagnetometerCorrection();
}
}
}
@Override
public String getName() {
return this.name;
@@ -82,9 +104,14 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
@Override
public boolean getRotation(Quaternion store) {
store.set(rotQuaternion);
//correction.mult(store, store); // Correction is not used now to preven accidental errors while debugging other things
store.multLocal(rotAdjust);
return true;
}
public void getCorrection(Quaternion store) {
store.set(correction);
}
@Override
public TrackerStatus getStatus() {
@@ -130,10 +157,31 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
@Override
public void resetFull(Quaternion reference) {
resetYaw(reference);
}
/**
* Does not perform actual gyro reset to reference, that's the task of
* reference adjusted tracker. Only aligns gyro with magnetometer if
* it's reliable
*/
@Override
public void resetYaw(Quaternion reference) {
if(magCalibrationStatus >= CalibrationAccuracy.HIGH.status) {
magentometerCalibrated = true;
// During calibration set correction to match magnetometer readings exactly
// TODO : Correct only yaw
correction.set(rotQuaternion).inverseLocal().multLocal(rotMagQuaternion);
}
}
/**
* Calculate correction between normal and magnetometer
* readings up to accuracy threshold
*/
protected void calculateLiveMagnetometerCorrection() {
// TODO Magic, correct only yaw
// TODO Print "jump" length when correcing if it's more than 1 degree
}
@Override
@@ -150,4 +198,31 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
public boolean userEditable() {
return true;
}
public enum CalibrationAccuracy {
UNRELIABLE(0),
LOW(1),
MEDIUM(2),
HIGH(3),
;
private static final CalibrationAccuracy[] byStatus = new CalibrationAccuracy[4];
public final int status;
private CalibrationAccuracy(int status) {
this.status = status;
}
public static CalibrationAccuracy getByStatus(int status) {
if(status < 0 || status > 3)
return null;
return byStatus[status];
}
static {
for(CalibrationAccuracy ca : values())
byStatus[ca.status] = ca;
}
}
}

View File

@@ -8,8 +8,9 @@ import io.eiren.vr.processor.TrackerBodyPosition;
public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
public final E tracker;
public final Quaternion adjustmentYaw = new Quaternion();
public final Quaternion adjustmentAttachment = new Quaternion();
public final Quaternion yawFix = new Quaternion();
public final Quaternion gyroFix = new Quaternion();
public final Quaternion attachmentFix = new Quaternion();
protected float confidenceMultiplier = 1.0f;
public ReferenceAdjustedTracker(E tracker) {
@@ -44,14 +45,15 @@ public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
*/
@Override
public void resetFull(Quaternion reference) {
tracker.resetFull(reference);
fixGyroscope();
Quaternion sensorRotation = new Quaternion();
tracker.getRotation(sensorRotation);
//float[] angles = new float[3];
//sensorRotation.toAngles(angles);
//sensorRotation.fromAngles(angles[0], 0, angles[2]);
adjustmentAttachment.set(sensorRotation).inverseLocal();
gyroFix.mult(sensorRotation, sensorRotation);
attachmentFix.set(sensorRotation).inverseLocal();
resetYaw(reference);
fixYaw(reference);
}
/**
@@ -63,6 +65,11 @@ public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
*/
@Override
public void resetYaw(Quaternion reference) {
tracker.resetYaw(reference);
fixYaw(reference);
}
private void fixYaw(Quaternion reference) {
// Use only yaw HMD rotation
Quaternion targetTrackerRotation = new Quaternion(reference);
float[] angles = new float[3];
@@ -71,21 +78,31 @@ public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
Quaternion sensorRotation = new Quaternion();
tracker.getRotation(sensorRotation);
adjustmentAttachment.mult(sensorRotation, sensorRotation);
//sensorRotation.multLocal(adjustmentAttachment);
gyroFix.mult(sensorRotation, sensorRotation);
sensorRotation.multLocal(attachmentFix);
sensorRotation.toAngles(angles);
sensorRotation.fromAngles(0, angles[1], 0);
adjustmentYaw.set(sensorRotation).inverseLocal().multLocal(targetTrackerRotation);
yawFix.set(sensorRotation).inverseLocal().multLocal(targetTrackerRotation);
}
private void fixGyroscope() {
float[] angles = new float[3];
confidenceMultiplier = 1.0f / tracker.getConfidenceLevel();
Quaternion sensorRotation = new Quaternion();
tracker.getRotation(sensorRotation);
sensorRotation.toAngles(angles);
sensorRotation.fromAngles(0, angles[1], 0);
gyroFix.set(sensorRotation).inverseLocal();
}
protected void adjustInternal(Quaternion store) {
//store.multLocal(adjustmentAttachment);
adjustmentAttachment.mult(store, store);
adjustmentYaw.mult(store, store);
gyroFix.mult(store, store);
store.multLocal(attachmentFix);
yawFix.mult(store, store);
}
@Override
@@ -124,4 +141,9 @@ public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
public void setBodyPosition(TrackerBodyPosition position) {
tracker.setBodyPosition(position);
}
@Override
public void tick() {
tracker.tick();
}
}

View File

@@ -0,0 +1,24 @@
package io.eiren.vr.trackers;
import io.eiren.util.BufferedTimer;
public class SteamVRTracker extends ComputedTracker implements TrackerWithTPS {
public final int id;
protected BufferedTimer timer = new BufferedTimer(1f);
public SteamVRTracker(int id, String name) {
super(name);
this.id = id;
}
@Override
public float getTPS() {
return timer.getAverageFPS();
}
@Override
public void dataTick() {
timer.update();
}
}

View File

@@ -25,6 +25,8 @@ public interface Tracker {
public void resetYaw(Quaternion reference);
public void tick();
public TrackerBodyPosition getBodyPosition();
public void setBodyPosition(TrackerBodyPosition position);

View File

@@ -2,15 +2,31 @@ package io.eiren.vr.trackers;
public enum TrackerStatus {
DISCONNECTED(false),
OK(true),
BUSY(true),
ERROR(false),
DISCONNECTED(0, false),
OK(1, true),
BUSY(2, true),
ERROR(3, false),
OCCLUDED(4, false),
;
private static final TrackerStatus byId[] = new TrackerStatus[5];
public final int id;
public final boolean sendData;
private TrackerStatus(boolean sendData) {
private TrackerStatus(int id, boolean sendData) {
this.sendData = sendData;
this.id = id;
}
public static TrackerStatus getById(int id) {
if(id < 0 || id >= byId.length)
return null;
return byId[id];
}
static {
for(TrackerStatus st : values())
byId[st.id] = st;
}
}

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
@@ -39,7 +40,7 @@ public class TrackersUDPServer extends Thread {
private final Quaternion buf = new Quaternion();
private final Random random = new Random();
private final List<TrackerConnection> trackers = new FastList<>();
private final Map<SocketAddress, TrackerConnection> trackersMap = new HashMap<>();
private final Map<InetAddress, TrackerConnection> trackersMap = new HashMap<>();
private final Map<Tracker, Consumer<String>> calibrationDataRequests = new HashMap<>();
private final Consumer<Tracker> trackersConsumer;
private final int port;
@@ -55,22 +56,27 @@ public class TrackersUDPServer extends Thread {
private void setUpNewSensor(DatagramPacket handshakePacket, ByteBuffer data) throws IOException {
System.out.println("[TrackerServer] Handshake recieved from " + handshakePacket.getAddress() + ":" + handshakePacket.getPort());
SocketAddress addr = handshakePacket.getSocketAddress();
InetAddress addr = handshakePacket.getAddress();
TrackerConnection sensor;
synchronized(trackers) {
sensor = trackersMap.get(addr);
}
if(sensor == null) {
boolean isOwo = false;
data.getLong(); // Skip packet number
int boardType = -1;
int imuType = -1;
int firmwareBuild = -1;
StringBuilder sb = new StringBuilder();
StringBuilder firmware = new StringBuilder();
byte[] mac = new byte[6];
String macString = null;
if(data.remaining() > 0) {
if(data.remaining() > 3)
boardType = data.getInt();
if(data.remaining() > 3)
imuType = data.getInt();
if(data.remaining() > 3)
data.getInt(); // MCU TYPE
if(data.remaining() > 11) {
data.getInt(); // IMU info
data.getInt();
@@ -78,38 +84,47 @@ public class TrackersUDPServer extends Thread {
}
if(data.remaining() > 3)
firmwareBuild = data.getInt();
while(true) {
if(data.remaining() == 0)
break;
int length = 0;
if(data.remaining() > 0)
length = data.get() & 0xFF; // firmware version length is 1 longer than that because it's nul-terminated
while(length > 0 && data.remaining() != 0) {
char c = (char) data.get();
if(c == 0)
break;
sb.append(c);
firmware.append(c);
length--;
}
if(data.remaining() > mac.length) {
data.get(mac);
macString = String.format("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
}
if(sb.length() == 0)
sb.append("owoTrack");
if(firmware.length() == 0) {
firmware.append("owoTrack");
isOwo = true;
}
IMUTracker imu = new IMUTracker("udp:/" + handshakePacket.getAddress().toString(), this);
IMUReferenceAdjustedTracker<IMUTracker> adjustedTracker = new IMUReferenceAdjustedTracker<>(imu);
ReferenceAdjustedTracker<IMUTracker> adjustedTracker = new ReferenceAdjustedTracker<>(imu);
trackersConsumer.accept(adjustedTracker);
sensor = new TrackerConnection(imu, addr);
sensor = new TrackerConnection(imu, handshakePacket.getSocketAddress());
sensor.isOwoTrack = isOwo;
int i = 0;
synchronized(trackers) {
i = trackers.size();
trackers.add(sensor);
trackersMap.put(addr, sensor);
}
System.out.println("[TrackerServer] Sensor " + i + " added with address " + addr + ". Board type: " + boardType + ", imu type: " + imuType + ", firmware: " + sb + " (" + firmwareBuild + ")");
System.out.println("[TrackerServer] Sensor " + i + " added with address " + handshakePacket.getSocketAddress() + ". Board type: " + boardType + ", imu type: " + imuType + ", firmware: " + firmware + " (" + firmwareBuild + "), mac: " + macString);
}
sensor.tracker.setStatus(TrackerStatus.OK);
socket.send(new DatagramPacket(HANDSHAKE_BUFFER, HANDSHAKE_BUFFER.length, handshakePacket.getAddress(), handshakePacket.getPort()));
}
private void setUpAuxialrySensor(TrackerConnection connection) throws IOException {
private void setUpAuxilarySensor(TrackerConnection connection) throws IOException {
System.out.println("[TrackerServer] Setting up auxilary sensor for " + connection.tracker.getName());
IMUTracker imu = new IMUTracker(connection.tracker.getName() + "/1", this);
connection.secondTracker = imu;
IMUReferenceAdjustedTracker<IMUTracker> adjustedTracker = new IMUReferenceAdjustedTracker<>(imu);
ReferenceAdjustedTracker<IMUTracker> adjustedTracker = new ReferenceAdjustedTracker<>(imu);
trackersConsumer.accept(adjustedTracker);
System.out.println("[TrackerServer] Sensor added with address " + imu.getName());
}
@@ -129,12 +144,13 @@ public class TrackersUDPServer extends Thread {
socket.receive(recieve);
bb.rewind();
TrackerConnection sensor;
TrackerConnection connection;
IMUTracker tracker = null;
synchronized(trackers) {
sensor = trackersMap.get(recieve.getSocketAddress());
connection = trackersMap.get(recieve.getAddress());
}
if(sensor != null)
sensor.lastPacket = System.currentTimeMillis();
if(connection != null)
connection.lastPacket = System.currentTimeMillis();
int packetId;
switch(packetId = bb.getInt()) {
case 0:
@@ -144,90 +160,154 @@ public class TrackersUDPServer extends Thread {
break;
case 1: // PACKET_ROTATION
case 16: // PACKET_ROTATION_2
if(sensor == null)
if(connection == null)
break;
bb.getLong();
buf.set(bb.getFloat(), bb.getFloat(), bb.getFloat(), bb.getFloat());
offset.mult(buf, buf);
IMUTracker tracker;
if(packetId == 1) {
tracker = sensor.tracker;
tracker = connection.tracker;
} else {
tracker = sensor.secondTracker;
tracker = connection.secondTracker;
}
if(tracker == null)
break;
tracker.rotQuaternion.set(buf);
tracker.dataTick();
break;
case 2:
if(sensor == null)
case 17: // PACKET_ROTATION_DATA
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
sensor.tracker.gyroVector.set(bb.getFloat(), bb.getFloat(), bb.getFloat());
int sensorId = bb.get() & 0xFF;
if(sensorId == 0) {
tracker = connection.tracker;
} else if(sensorId == 1) {
tracker = connection.secondTracker;
}
if(tracker == null)
break;
int dataType = bb.get() & 0xFF;
buf.set(bb.getFloat(), bb.getFloat(), bb.getFloat(), bb.getFloat());
offset.mult(buf, buf);
int calibrationInfo = bb.get() & 0xFF;
switch(dataType) {
case 1: // DATA_TYPE_NORMAL
tracker.rotQuaternion.set(buf);
tracker.calibrationStatus = calibrationInfo;
tracker.dataTick();
break;
case 2: // DATA_TYPE_CORRECTION
tracker.rotMagQuaternion.set(buf);
tracker.magCalibrationStatus = calibrationInfo;
tracker.hasNewCorrectionData = true;
break;
}
break;
case 18: // PACKET_MAGENTOMETER_ACCURACY
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
sensorId = bb.get() & 0xFF;
if(sensorId == 0) {
tracker = connection.tracker;
} else if(sensorId == 1) {
tracker = connection.secondTracker;
}
if(tracker == null)
break;
float accuracyInfo = bb.getFloat();
tracker.magnetometerAccuracy = accuracyInfo;
// TODO
break;
case 2:
if(connection == null)
break;
bb.getLong();
connection.tracker.gyroVector.set(bb.getFloat(), bb.getFloat(), bb.getFloat());
break;
case 4:
if(sensor == null)
if(connection == null)
break;
bb.getLong();
float x = bb.getFloat();
float z = bb.getFloat();
float y = bb.getFloat();
sensor.tracker.accelVector.set(x, y, z);
connection.tracker.accelVector.set(x, y, z);
break;
case 5:
if(sensor == null)
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
x = bb.getFloat();
z = bb.getFloat();
y = bb.getFloat();
sensor.tracker.magVector.set(x, y, z);
connection.tracker.magVector.set(x, y, z);
break;
case 6: // PACKET_RAW_CALIBRATION_DATA
if(sensor == null)
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
//sensor.rawCalibrationData.add(new double[] {bb.getInt(), bb.getInt(), bb.getInt(), bb.getInt(), bb.getInt(), bb.getInt()});
break;
case 7: // PACKET_GYRO_CALIBRATION_DATA
if(sensor == null)
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
//sensor.gyroCalibrationData = new double[] {bb.getFloat(), bb.getFloat(), bb.getFloat()};
break;
case 8: // PACKET_CONFIG
if(sensor == null)
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
MPUTracker.ConfigurationData data = new MPUTracker.ConfigurationData(bb);
Consumer<String> dataConsumer = calibrationDataRequests.remove(sensor.tracker);
Consumer<String> dataConsumer = calibrationDataRequests.remove(connection.tracker);
if(dataConsumer != null) {
dataConsumer.accept(data.toTextMatrix());
}
break;
case 9: // PACKET_RAW_MAGENTOMETER
if(sensor == null)
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
float mx = bb.getFloat();
float my = bb.getFloat();
float mz = bb.getFloat();
sensor.tracker.confidence = (float) Math.sqrt(mx * mx + my * my + mz * mz);
connection.tracker.confidence = (float) Math.sqrt(mx * mx + my * my + mz * mz);
break;
case 10: // PACKET_PING_PONG:
if(sensor == null)
if(connection == null)
break;
if(connection.isOwoTrack)
break;
int pingId = bb.getInt();
if(sensor.lastPingPacketId == pingId) {
tracker = sensor.tracker;
tracker.ping = (int) (System.currentTimeMillis() - sensor.lastPingPacketTime) / 2;
if(connection.lastPingPacketId == pingId) {
tracker = connection.tracker;
tracker.ping = (int) (System.currentTimeMillis() - connection.lastPingPacketTime) / 2;
}
break;
case 11: // PACKET_SERIAL
if(sensor == null)
if(connection == null)
break;
tracker = sensor.tracker;
if(connection.isOwoTrack)
break;
tracker = connection.tracker;
bb.getLong();
int length = bb.getInt();
for(int i = 0; i < length; ++i) {
@@ -243,44 +323,53 @@ public class TrackersUDPServer extends Thread {
}
break;
case 12: // PACKET_BATTERY_VOLTAGE
if(sensor == null)
if(connection == null)
break;
tracker = sensor.tracker;
tracker = connection.tracker;
bb.getLong();
tracker.setBatteryVoltage(bb.getFloat());
break;
case 13: // PACKET_TAP
if(sensor == null)
if(connection == null)
break;
tracker = sensor.tracker;
if(connection.isOwoTrack)
break;
tracker = connection.tracker;
bb.getLong();
byte tap = bb.get();
System.out.println("[TrackerServer] Tap packet received from " + tracker.getName() + ": b" + Integer.toBinaryString(tap));
sensorId = bb.get() & 0xFF;
if(sensorId == 0) {
tracker = connection.tracker;
} else if(sensorId == 1) {
tracker = connection.secondTracker;
}
int tap = bb.get() & 0xFF;
BnoTap tapObj = new BnoTap(tap);
System.out.println("[TrackerServer] Tap packet received from " + tracker.getName() + "/" + sensorId + ": " + tapObj + " (b" + Integer.toBinaryString(tap) + ")");
break;
case 14: // PACKET_RESET_REASON
bb.getLong();
byte reason = bb.get();
System.out.println("[TrackerServer] Reset recieved from " + recieve.getSocketAddress() + ": " + reason);
if(sensor == null)
if(connection == null)
break;
tracker = sensor.tracker;
tracker = connection.tracker;
tracker.setStatus(TrackerStatus.ERROR);
break;
case 15: // PACKET_SENSOR_INFO
if(sensor == null)
if(connection == null)
break;
bb.getLong();
int sensorId = bb.get() & 0xFF;
sensorId = bb.get() & 0xFF;
int sensorStatus = bb.get() & 0xFF;
if(sensorId == 1 && sensorStatus == 1 && sensor.secondTracker == null) {
setUpAuxialrySensor(sensor);
if(sensorId == 1 && sensorStatus == 1 && connection.secondTracker == null) {
setUpAuxilarySensor(connection);
}
bb.rewind();
bb.putInt(15);
bb.put((byte) sensorId);
bb.put((byte) sensorStatus);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), sensor.address));
System.out.println("[TrackerServer] Sensor info for " + sensor.tracker.getName() + "/" + sensorId + ": " + sensorStatus);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), connection.address));
System.out.println("[TrackerServer] Sensor info for " + connection.tracker.getName() + "/" + sensorId + ": " + sensorStatus);
break;
default:
System.out.println("[TrackerServer] Unknown data received: " + packetId + " from " + recieve.getSocketAddress());
@@ -343,6 +432,7 @@ public class TrackersUDPServer extends Thread {
public long lastPacket = System.currentTimeMillis();
public int lastPingPacketId = -1;
public long lastPingPacketTime = 0;
public boolean isOwoTrack = false;
public TrackerConnection(IMUTracker tracker, SocketAddress address) {
this.tracker = tracker;

View File

@@ -1,273 +0,0 @@
package io.eiren.unit;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import io.eiren.math.FloatMath;
import io.eiren.util.StringUtils;
import io.eiren.vr.processor.TransformNode;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.ReferenceAdjustedTracker;
import static org.junit.Assert.*;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
/**
* Tests {@link ReferenceAdjustedTracker#resetFull(Quaternion)}
*/
public class ReferenceAdjustmentsFullTests {
private Set<String> testedTrackerNames = new HashSet<>();
@Test
public void check0to0() {
yawTest(0, 0);
}
@Test
public void check45to0() {
yawTest(0, 45);
}
@Test
public void check90to0() {
yawTest(0, 90);
}
@Test
public void check180to0() {
yawTest(0, 180);
}
@Test
public void check270to0() {
yawTest(0, 270);
}
@Test
public void check0to45() {
yawTest(45, 0);
}
@Test
public void check45to45() {
yawTest(45, 45);
}
@Test
public void check90to45() {
yawTest(45, 90);
}
@Test
public void check180to45() {
yawTest(45, 180);
}
@Test
public void check270to45() {
yawTest(45, 270);
}
@Test
public void check0to90() {
yawTest(90, 0);
}
@Test
public void check45to90() {
yawTest(90, 45);
}
@Test
public void check90to90() {
yawTest(90, 90);
}
@Test
public void check180to90() {
yawTest(90, 180);
}
@Test
public void check270to90() {
yawTest(90, 270);
}
@Test
public void check0to180() {
yawTest(180, 0);
}
@Test
public void check45to180() {
yawTest(180, 45);
}
@Test
public void check90to180() {
yawTest(180, 90);
}
@Test
public void check180to180() {
yawTest(180, 180);
}
@Test
public void check270to180() {
yawTest(180, 270);
}
private void yawTest(int refYaw, int trackerYaw) {
checkReferenceAdjustmentFull(q(0, refYaw, 0), q(0, trackerYaw, 0), refYaw, "Tracker(0," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(0, refYaw, 15), q(0, trackerYaw, 0), refYaw, "Tracker(0," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(15, refYaw, 0), q(0, trackerYaw, 0), refYaw, "Tracker(0," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(15, refYaw, 15), q(0, trackerYaw, 0), refYaw, "Tracker(0," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(0, refYaw, 0), q(15, trackerYaw, 0), refYaw, "Tracker(15," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(0, refYaw, 15), q(0, trackerYaw, 15), refYaw, "Tracker(0," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(15, refYaw, 0), q(15, trackerYaw, 15), refYaw, "Tracker(15," + trackerYaw + ",0/" + refYaw + ")");
checkReferenceAdjustmentFull(q(15, refYaw, 15), q(0, trackerYaw, 15), refYaw, "Tracker(0," + trackerYaw + ",0/" + refYaw + ")");
}
public void checkReferenceAdjustmentFull(Quaternion referenceQuat, Quaternion trackerQuat, int refYaw, String name) {
ComputedTracker tracker = new ComputedTracker("test");
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetFull(referenceQuat);
Quaternion read = new Quaternion();
assertTrue("Adjusted tracker didn't return rotation", adj.getRotation(read));
// Use only yaw HMD rotation
Quaternion targetTrackerRotation = new Quaternion(referenceQuat);
float[] angles = new float[3];
targetTrackerRotation.toAngles(angles);
targetTrackerRotation.fromAngles(0, angles[1], 0);
assertEquals("Adjusted quat is not equal to reference quat (" + toDegs(targetTrackerRotation) + " vs " + toDegs(read) + ")", new QuatEqualFullWithEpsilon(targetTrackerRotation), new QuatEqualFullWithEpsilon(read));
testAdjustedTracker(tracker, adj, name, refYaw);
}
private static final boolean PRINT_TEST_RESULTS = false;
private static int errors = 0;
private static int successes = 0;
private void testAdjustedTracker(ComputedTracker tracker, ReferenceAdjustedTracker<ComputedTracker> adj, String name, int refYaw) {
if(!testedTrackerNames.add(name))
return;
final Quaternion trackerBase = new Quaternion();
trackerBase.set(tracker.rotation);
Quaternion rotation = new Quaternion();
Quaternion rotationCompare = new Quaternion();
Quaternion read = new Quaternion();
Quaternion diff = new Quaternion();
float[] angles = new float[3];
float[] anglesAdj = new float[3];
float[] anglesDiff = new float[3];
TransformNode trackerNode = new TransformNode(name, true);
TransformNode rotationNode = new TransformNode("Rot", true);
trackerNode.attachChild(rotationNode);
trackerNode.localTransform.setRotation(trackerBase);
for(int yaw = 0; yaw <= 360; yaw += 30) {
for(int pitch = -90; pitch <= 90; pitch += 15) {
for(int roll = -90; roll <= 90; roll += 15) {
rotation.fromAngles(pitch * FastMath.DEG_TO_RAD, yaw * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
rotationCompare.fromAngles(pitch * FastMath.DEG_TO_RAD, (yaw + refYaw) * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
rotationNode.localTransform.setRotation(rotation);
trackerNode.update();
rotationNode.update();
tracker.rotation.set(rotationNode.worldTransform.getRotation());
tracker.rotation.toAngles(angles);
adj.getRotation(read);
read.toAngles(anglesAdj);
diff.set(read).inverseLocal().multLocal(rotationCompare);
diff.toAngles(anglesDiff);
if(!PRINT_TEST_RESULTS) {
assertTrue(name(name, yaw, pitch, roll, angles, anglesAdj, anglesDiff),
FloatMath.equalsToZero(anglesDiff[0]) && FloatMath.equalsToZero(anglesDiff[1]) && FloatMath.equalsToZero(anglesDiff[2]));
} else {
if(FloatMath.equalsToZero(anglesDiff[0]) && FloatMath.equalsToZero(anglesDiff[1]) && FloatMath.equalsToZero(anglesDiff[2]))
successes++;
else
errors++;
System.out.println(name(name, yaw, pitch, roll, angles, anglesAdj, anglesDiff));
}
}
}
}
if(PRINT_TEST_RESULTS)
System.out.println("Errors: " + errors + ", successes: " + successes);
}
private static String name(String name, int yaw, int pitch, int roll, float[] angles, float[] anglesAdj, float[] anglesDiff) {
return name + ". Rot: " + yaw + "/" + pitch + ". "
+ "Angles: " + StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 1) + "/" + StringUtils.prettyNumber(anglesAdj[0] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 1) + "/" + StringUtils.prettyNumber(anglesAdj[1] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 1) + "/" + StringUtils.prettyNumber(anglesAdj[2] * FastMath.RAD_TO_DEG, 1) + ". Diff: "
+ StringUtils.prettyNumber(anglesDiff[0] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(anglesDiff[1] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(anglesDiff[2] * FastMath.RAD_TO_DEG, 1);
}
public static String toDegs(Quaternion q) {
float[] degs = new float[3];
q.toAngles(degs);
return StringUtils.prettyNumber(degs[0] * FastMath.RAD_TO_DEG, 0) + "," + StringUtils.prettyNumber(degs[1] * FastMath.RAD_TO_DEG, 0) + "," + StringUtils.prettyNumber(degs[2] * FastMath.RAD_TO_DEG, 0);
}
public static Quaternion q(float pitch, float yaw, float roll) {
return new Quaternion().fromAngles(pitch * FastMath.DEG_TO_RAD, yaw * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
}
public static class QuatEqualFullWithEpsilon {
private final Quaternion q;
public QuatEqualFullWithEpsilon(Quaternion q) {
this.q = q;
}
@Override
public String toString() {
return String.valueOf(q);
}
@Override
public int hashCode() {
return q.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Quaternion)
obj = new QuatEqualFullWithEpsilon((Quaternion) obj);
if(!(obj instanceof QuatEqualFullWithEpsilon))
return false;
Quaternion q2 = ((QuatEqualFullWithEpsilon) obj).q;
float[] degs1 = new float[3];
q.toAngles(degs1);
float[] degs2 = new float[3];
q2.toAngles(degs2);
if(degs1[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs1[1] += FastMath.TWO_PI;
if(degs2[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs2[1] += FastMath.TWO_PI;
return FloatMath.equalsWithEpsilon(degs1[0], degs2[0])
&& FloatMath.equalsWithEpsilon(degs1[1], degs2[1])
&& FloatMath.equalsWithEpsilon(degs1[2], degs2[2]);
}
}
}

View File

@@ -0,0 +1,265 @@
package io.eiren.unit;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import io.eiren.math.FloatMath;
import io.eiren.util.StringUtils;
import io.eiren.vr.processor.TransformNode;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.ReferenceAdjustedTracker;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
/**
* Tests {@link ReferenceAdjustedTracker#resetFull(Quaternion)}
*/
public class ReferenceAdjustmentsTests {
private static final int[] yaws = {0, 45, 90, 180, 270};
private static final int[] pitches = {0, 15, 35, -15, -35};
private static final int[] rolls = {0, 15, 35, -15, -35};
private static final boolean PRINT_TEST_RESULTS = false;
private static int errors = 0;
private static int successes = 0;
public static Stream<AnglesSet> getAnglesSet() {
return IntStream.of(yaws).mapToObj((yaw) ->
IntStream.of(pitches).mapToObj((pitch) ->
IntStream.of(rolls).mapToObj((roll) -> new AnglesSet(pitch, yaw, roll)
))).flatMap(Function.identity()).flatMap(Function.identity());
}
@TestFactory
Stream<DynamicTest> getTestsYaw() {
return getAnglesSet().map((p) ->
dynamicTest("Adjustment Yaw Test of Tracker(" + p.pitch + "," + p.yaw + "," + p.roll + ")",
() -> IntStream.of(yaws).forEach((refYaw) ->
checkReferenceAdjustmentYaw(q(p.pitch, p.yaw, p.roll), 0, refYaw, 0))
));
}
@TestFactory
Stream<DynamicTest> getTestsFull() {
return getAnglesSet().map((p) ->
dynamicTest("Adjustment Full Test of Tracker(" + p.pitch + "," + p.yaw + "," + p.roll + ")",
() -> getAnglesSet().forEach((ref) ->
checkReferenceAdjustmentFull(q(p.pitch, p.yaw, p.roll), ref.pitch, ref.yaw, ref.roll))
));
}
@TestFactory
Stream<DynamicTest> getTestsForRotation() {
return getAnglesSet().map((p) ->
IntStream.of(yaws).mapToObj((refYaw) ->
dynamicTest("Adjustment Rotation Test of Tracker(" + p.pitch + "," + p.yaw + "," + p.roll + "), Ref " + refYaw,
() -> testAdjustedTrackerRotation(q(p.pitch, p.yaw, p.roll), 0, refYaw, 0)
))).flatMap(Function.identity());
}
public void checkReferenceAdjustmentFull(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
ComputedTracker tracker = new ComputedTracker("test");
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetFull(referenceQuat);
Quaternion read = new Quaternion();
assertTrue(adj.getRotation(read), "Adjusted tracker didn't return rotation");
// Use only yaw HMD rotation
Quaternion targetTrackerRotation = new Quaternion(referenceQuat);
float[] angles = new float[3];
targetTrackerRotation.toAngles(angles);
targetTrackerRotation.fromAngles(0, angles[1], 0);
assertEquals(new QuatEqualFullWithEpsilon(read), new QuatEqualFullWithEpsilon(targetTrackerRotation),
"Adjusted quat is not equal to reference quat (" + toDegs(targetTrackerRotation) + " vs " + toDegs(read) + ")");
}
public void checkReferenceAdjustmentYaw(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
ComputedTracker tracker = new ComputedTracker("test");
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetYaw(referenceQuat);
Quaternion read = new Quaternion();
assertTrue(adj.getRotation(read), "Adjusted tracker didn't return rotation");
assertEquals(new QuatEqualYawWithEpsilon(referenceQuat), new QuatEqualYawWithEpsilon(read),
"Adjusted quat is not equal to reference quat (" + toDegs(referenceQuat) + " vs " + toDegs(read) + ")");
}
private void testAdjustedTrackerRotation(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
ComputedTracker tracker = new ComputedTracker("test");
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetFull(referenceQuat);
// Use only yaw HMD rotation
Quaternion targetTrackerRotation = new Quaternion(referenceQuat);
float[] angles = new float[3];
targetTrackerRotation.toAngles(angles);
targetTrackerRotation.fromAngles(0, angles[1], 0);
Quaternion read = new Quaternion();
Quaternion rotation = new Quaternion();
Quaternion rotationCompare = new Quaternion();
Quaternion diff = new Quaternion();
float[] anglesAdj = new float[3];
float[] anglesDiff = new float[3];
TransformNode trackerNode = new TransformNode("Tracker", true);
TransformNode rotationNode = new TransformNode("Rot", true);
rotationNode.attachChild(trackerNode);
trackerNode.localTransform.setRotation(tracker.rotation);
for(int yaw = 0; yaw <= 360; yaw += 30) {
for(int pitch = -90; pitch <= 90; pitch += 15) {
for(int roll = -90; roll <= 90; roll += 15) {
rotation.fromAngles(pitch * FastMath.DEG_TO_RAD, yaw * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
rotationCompare.fromAngles(pitch * FastMath.DEG_TO_RAD, (yaw + refYaw) * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
rotationNode.localTransform.setRotation(rotation);
rotationNode.update();
tracker.rotation.set(trackerNode.worldTransform.getRotation());
tracker.rotation.toAngles(angles);
adj.getRotation(read);
read.toAngles(anglesAdj);
diff.set(read).inverseLocal().multLocal(rotationCompare);
diff.toAngles(anglesDiff);
if(!PRINT_TEST_RESULTS) {
assertTrue(FloatMath.equalsToZero(anglesDiff[0]) && FloatMath.equalsToZero(anglesDiff[1]) && FloatMath.equalsToZero(anglesDiff[2]),
name(yaw, pitch, roll, angles, anglesAdj, anglesDiff));
} else {
if(FloatMath.equalsToZero(anglesDiff[0]) && FloatMath.equalsToZero(anglesDiff[1]) && FloatMath.equalsToZero(anglesDiff[2]))
successes++;
else
errors++;
System.out.println(name(yaw, pitch, roll, angles, anglesAdj, anglesDiff));
}
}
}
}
if(PRINT_TEST_RESULTS)
System.out.println("Errors: " + errors + ", successes: " + successes);
}
private static String name(int yaw, int pitch, int roll, float[] angles, float[] anglesAdj, float[] anglesDiff) {
return "Rot: " + yaw + "/" + pitch + "/" + roll + ". "
+ "Angles: " + StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 1) + "/" + StringUtils.prettyNumber(anglesAdj[0] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 1) + "/" + StringUtils.prettyNumber(anglesAdj[1] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 1) + "/" + StringUtils.prettyNumber(anglesAdj[2] * FastMath.RAD_TO_DEG, 1) + ". Diff: "
+ StringUtils.prettyNumber(anglesDiff[0] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(anglesDiff[1] * FastMath.RAD_TO_DEG, 1) + ", "
+ StringUtils.prettyNumber(anglesDiff[2] * FastMath.RAD_TO_DEG, 1);
}
public static Quaternion q(float pitch, float yaw, float roll) {
return new Quaternion().fromAngles(pitch * FastMath.DEG_TO_RAD, yaw * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
}
public static String toDegs(Quaternion q) {
float[] degs = new float[3];
q.toAngles(degs);
return StringUtils.prettyNumber(degs[0] * FastMath.RAD_TO_DEG, 0) + "," + StringUtils.prettyNumber(degs[1] * FastMath.RAD_TO_DEG, 0) + "," + StringUtils.prettyNumber(degs[2] * FastMath.RAD_TO_DEG, 0);
}
private static class QuatEqualYawWithEpsilon {
private final Quaternion q;
public QuatEqualYawWithEpsilon(Quaternion q) {
this.q = q;
}
@Override
public String toString() {
return String.valueOf(q);
}
@Override
public int hashCode() {
return q.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Quaternion)
obj = new QuatEqualYawWithEpsilon((Quaternion) obj);
if(!(obj instanceof QuatEqualYawWithEpsilon))
return false;
Quaternion q2 = ((QuatEqualYawWithEpsilon) obj).q;
float[] degs1 = new float[3];
q.toAngles(degs1);
float[] degs2 = new float[3];
q2.toAngles(degs2);
if(degs1[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs1[1] += FastMath.TWO_PI;
if(degs2[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs2[1] += FastMath.TWO_PI;
return FloatMath.equalsWithEpsilon(degs1[1], degs2[1]);
}
}
public static class QuatEqualFullWithEpsilon {
private final Quaternion q;
public QuatEqualFullWithEpsilon(Quaternion q) {
this.q = q;
}
@Override
public String toString() {
return String.valueOf(q);
}
@Override
public int hashCode() {
return q.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Quaternion)
obj = new QuatEqualFullWithEpsilon((Quaternion) obj);
if(!(obj instanceof QuatEqualFullWithEpsilon))
return false;
Quaternion q2 = ((QuatEqualFullWithEpsilon) obj).q;
float[] degs1 = new float[3];
q.toAngles(degs1);
float[] degs2 = new float[3];
q2.toAngles(degs2);
if(degs1[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs1[1] += FastMath.TWO_PI;
if(degs2[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs2[1] += FastMath.TWO_PI;
return FloatMath.equalsWithEpsilon(degs1[0], degs2[0])
&& FloatMath.equalsWithEpsilon(degs1[1], degs2[1])
&& FloatMath.equalsWithEpsilon(degs1[2], degs2[2]);
}
}
public static class AnglesSet {
public final int pitch;
public final int yaw;
public final int roll;
public AnglesSet(int pitch, int yaw, int roll) {
this.pitch = pitch;
this.yaw = yaw;
this.roll = roll;
}
}
}

View File

@@ -1,187 +0,0 @@
package io.eiren.unit;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import io.eiren.math.FloatMath;
import io.eiren.util.StringUtils;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.ReferenceAdjustedTracker;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Tests {@link ReferenceAdjustedTracker#resetYaw(Quaternion)}
*/
public class ReferenceAdjustmentsYawTests {
@Test
public void check0to0() {
yawTest(0, 0);
}
@Test
public void check45to0() {
yawTest(0, 45);
}
@Test
public void check90to0() {
yawTest(0, 90);
}
@Test
public void check180to0() {
yawTest(0, 180);
}
@Test
public void check270to0() {
yawTest(0, 270);
}
@Test
public void check0to45() {
yawTest(45, 0);
}
@Test
public void check45to45() {
yawTest(45, 45);
}
@Test
public void check90to45() {
yawTest(45, 90);
}
@Test
public void check180to45() {
yawTest(45, 180);
}
@Test
public void check270to45() {
yawTest(45, 270);
}
@Test
public void check0to90() {
yawTest(90, 0);
}
@Test
public void check45to90() {
yawTest(90, 45);
}
@Test
public void check90to90() {
yawTest(90, 90);
}
@Test
public void check180to90() {
yawTest(90, 180);
}
@Test
public void check270to90() {
yawTest(90, 270);
}
@Test
public void check0to180() {
yawTest(180, 0);
}
@Test
public void check45to180() {
yawTest(180, 45);
}
@Test
public void check90to180() {
yawTest(180, 90);
}
@Test
public void check180to180() {
yawTest(180, 180);
}
@Test
public void check270to180() {
yawTest(180, 270);
}
private void yawTest(float refYaw, float trackerTaw) {
checkReferenceAdjustmentYaw(q(0, refYaw, 0), q(0, trackerTaw, 0));
checkReferenceAdjustmentYaw(q(0, refYaw, 15), q(0, trackerTaw, 0));
checkReferenceAdjustmentYaw(q(15, refYaw, 0), q(0, trackerTaw, 0));
checkReferenceAdjustmentYaw(q(15, refYaw, 15), q(0, trackerTaw, 0));
checkReferenceAdjustmentYaw(q(0, refYaw, 0), q(15, trackerTaw, 0));
checkReferenceAdjustmentYaw(q(0, refYaw, 15), q(0, trackerTaw, 15));
checkReferenceAdjustmentYaw(q(15, refYaw, 0), q(15, trackerTaw, 15));
checkReferenceAdjustmentYaw(q(15, refYaw, 15), q(0, trackerTaw, 15));
}
public static void checkReferenceAdjustmentYaw(Quaternion referenceQuat, Quaternion trackerQuat) {
ComputedTracker tracker = new ComputedTracker("test");
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker adj = new ReferenceAdjustedTracker(tracker);
adj.resetYaw(referenceQuat);
Quaternion read = new Quaternion();
assertTrue("Adjusted tracker didn't return rotation", adj.getRotation(read));
assertEquals("Adjusted quat is not equal to reference quat (" + toDegs(referenceQuat) + " vs " + toDegs(read) + ")", new QuatEqualYawWithEpsilon(referenceQuat), new QuatEqualYawWithEpsilon(read));
}
public static String toDegs(Quaternion q) {
float[] degs = new float[3];
q.toAngles(degs);
return StringUtils.prettyNumber(degs[0] * FastMath.RAD_TO_DEG, 0) + "," + StringUtils.prettyNumber(degs[1] * FastMath.RAD_TO_DEG, 0) + "," + StringUtils.prettyNumber(degs[2] * FastMath.RAD_TO_DEG, 0);
}
public static Quaternion q(float pitch, float yaw, float roll) {
return new Quaternion().fromAngles(pitch * FastMath.DEG_TO_RAD, yaw * FastMath.DEG_TO_RAD, roll * FastMath.DEG_TO_RAD);
}
private static class QuatEqualYawWithEpsilon {
private final Quaternion q;
public QuatEqualYawWithEpsilon(Quaternion q) {
this.q = q;
}
@Override
public String toString() {
return String.valueOf(q);
}
@Override
public int hashCode() {
return q.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Quaternion)
obj = new QuatEqualYawWithEpsilon((Quaternion) obj);
if(!(obj instanceof QuatEqualYawWithEpsilon))
return false;
Quaternion q2 = ((QuatEqualYawWithEpsilon) obj).q;
float[] degs1 = new float[3];
q.toAngles(degs1);
float[] degs2 = new float[3];
q2.toAngles(degs2);
if(degs1[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs1[1] += FastMath.TWO_PI;
if(degs2[1] < -FloatMath.ANGLE_EPSILON_RAD)
degs2[1] += FastMath.TWO_PI;
return FloatMath.equalsWithEpsilon(degs1[1], degs2[1]);
}
}
}