Compare commits

...

126 Commits

Author SHA1 Message Date
Eiren Rain
56b8b58606 Bump version to 0.0.19 2021-09-18 16:51:49 +03:00
Eiren Rain
97bc9343c1 Code formatting, move some packages 2021-09-18 16:50:54 +03:00
Eiren Rain
18cea30f72 Merge pull request #49 from ButterscotchVanilla/autobone-positions
AutoBone PoseFrame file format rework and other related fixes
2021-09-18 03:04:29 +03:00
Eiren Rain
d5c048600e Fix SteamVR input bridge not setting tracker status properly 2021-09-18 02:58:22 +03:00
ButterscotchVanilla
6d103d4ff9 Get all trackers directly in setPoseFromFrame 2021-09-17 19:18:24 -04:00
Eiren Rain
7008197760 Merge pull request #48 from kitlith/target_java8_release
Fixes some compatibility issues when compiling with Java9+ jdk.
2021-09-17 15:32:21 +03:00
Kitlith
da66f33edc Fixes some compatibility issues when compiling with Java9+ jdk.
i.e.:
java.lang.NoSuchMethodError:
java.nio.ByteBuffer.rewind()Ljava/nio/ByteBuffer;
2021-09-16 16:36:00 -07:00
ButterscotchVanilla
4109d1c825 Add pelvis averaging for SimpleSkeleton and fix neck rotation 2021-09-16 00:30:22 -04:00
ButterscotchVanilla
a300663a9e Spelling fix and check for null in TrackerUtils 2021-09-14 10:36:35 -04:00
ButterscotchVanilla
cb33dac3b9 Handle getRotation and getPosition responses properly 2021-09-14 09:14:16 -04:00
ButterscotchVanilla
582bac8050 Check recording for chest tracker when loading AutoBone configs 2021-09-14 09:08:22 -04:00
ButterscotchVanilla
5e1c45bc09 Record individual trackers with PoseFrame and optimize iterations 2021-09-14 08:50:08 -04:00
ButterscotchVanilla
b3073e6938 Handle busy status and add better exception messages 2021-09-14 04:20:43 -04:00
ButterscotchVanilla
63e259689f Handle computed trackers better and handle tracker status in data collection 2021-09-14 04:01:10 -04:00
ButterscotchVanilla
d92ea0a39e Small clean-up and ignore computed trackers in PoseFrame by default 2021-09-14 02:52:06 -04:00
ButterscotchVanilla
81bbb4008b Only allow loading tracker configs if the tracker is user editable 2021-09-14 01:45:35 -04:00
ButterscotchVanilla
45ad0698b1 Allow multiple TrackerFrames with the same designation in PoseFrame, make TrackerFrame extend Tracker 2021-09-14 01:06:36 -04:00
ButterscotchVanilla
bc542a7bb1 Don't put null designations in the trackers 2021-09-12 13:19:33 -04:00
ButterscotchVanilla
efb065f558 Fix TrackerBodyPosition.getByDesignation capitalization 2021-09-12 13:19:33 -04:00
ButterscotchVanilla
00e63db029 Use HashMap directly 2021-09-12 13:19:32 -04:00
ButterscotchVanilla
f6a2926033 Remove comment and useless if statement 2021-09-12 13:19:32 -04:00
ButterscotchVanilla
5b0f8afa4e Change namespaces, change PoseRecorder format, use TrackerBodyPosition for designation 2021-09-12 13:19:32 -04:00
Eiren Rain
c5b4421eae Fix tracker info not updating for IMU trackers 2021-09-12 19:58:03 +03:00
Eiren Rain
4d3f04e227 Bump version to 0.0.18 Test 3 2021-09-12 12:11:20 +03:00
Eiren Rain
75ad29a68d Can select role for SteamVR trackers
Trackers now have info if they report position or rotation
Extended pelvis model is always on
2021-09-12 12:10:59 +03:00
Eiren Rain
62e1e65dda Merge pull request #46 from ButterscotchVanilla/pelvis-fix
Fix leg averaging for pelvis and add waist tracker averaging
2021-09-12 12:00:11 +03:00
ButterscotchVanilla
02f64314b8 Fix leg averaging for pelvis and add waist tracker averaging 2021-09-12 04:49:54 -04:00
Eiren Rain
12d7f191ee Fix NPE, added bat scripts to the build 2021-09-04 09:25:16 +03:00
Eiren Rain
37135e1c8e Merge pull request #45 from ButterscotchVanilla/main
AutoBone: Move hardcoded values to variables
2021-09-03 06:54:33 +03:00
ButterscotchVanilla
85a0c25d0e AutoBone: Move hardcoded values to variables 2021-09-02 22:17:19 -04:00
Eiren Rain
1f081392df Always have a skeleton with legs, can work with any trackers, fill in empty trackers with static or previous 2021-09-02 11:41:54 +03:00
Eiren Rain
c02f9b827d Merge branch 'main' of https://github.com/SlimeVR/SlimeVR-Server into main 2021-09-02 11:33:46 +03:00
Eiren Rain
7e95c9f999 Remember window size and position between restarts
Added window and taskbar icons
2021-09-02 11:33:27 +03:00
Eiren Rain
4836b025e9 Merge pull request #41 from JimWails/main
Add support for ch910xx
2021-08-29 17:08:04 +03:00
JimWails
9a76838602 Add support for ch910xx
Already test on ESP8266 which use CH9102X driver
2021-08-28 22:27:30 +08:00
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
49 changed files with 3181 additions and 270 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

@@ -26,9 +26,9 @@ git clone https://github.com/Eirenliel/slime-java-commons.git
# Enter the directory and build the runnable server JAR
cd SlimeVR-Server
gradlew serverJar
gradlew shadowJar
```
Open Slime VR Server project in Eclipse or Intellij Idea
run gradle command `serverJar` to build a runnable server JAR
run gradle command `shadowJar` to build a runnable server JAR

View File

@@ -7,53 +7,69 @@
*/
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'
if (JavaVersion.current().isJava9Compatible()) {
// TODO: Gradle 6.6
// options.release = 8
options.compilerArgs.addAll(['--release', '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.
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 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
// 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()
useJUnitPlatform()
}
subprojects.each { subproject -> evaluationDependsOn(subproject.path) }
task serverJar (type: Jar, dependsOn: subprojects.tasks['build']) {
// Make the JAR runnable
manifest {
attributes 'Main-Class': 'io.eiren.vr.Main'
}
// Pack all dependencies within the JAR
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
// Add this project's classes in the JAR
with jar
shadowJar {
archiveBaseName.set('slimevr')
archiveClassifier.set('')
archiveVersion.set('')
}
application {
mainClassName = 'io.eiren.vr.Main'
}

13
resources/firewall.bat Normal file
View File

@@ -0,0 +1,13 @@
@echo off
echo Installing firewall rules...
rem Rotational data default port
netsh advfirewall firewall add rule name="UDP 6969 incoming" dir=in action=allow protocol=UDP localport=6969
netsh advfirewall firewall add rule name="UDP 6969 outgoing" dir=out action=allow protocol=UDP localport=6969
rem Info server allowing automatic discovery
netsh advfirewall firewall add rule name="UDP 35903 incoming" dir=in action=allow protocol=UDP localport=35903
netsh advfirewall firewall add rule name="UDP 35903 outgoing" dir=out action=allow protocol=UDP localport=35903
echo Done!
pause

1
resources/run.bat Normal file
View File

@@ -0,0 +1 @@
@java -Xmx512M -jar slimevr.jar

View File

@@ -0,0 +1,516 @@
package dev.slimevr.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 dev.slimevr.poserecorder.PoseFrame;
import dev.slimevr.poserecorder.TrackerFrame;
import dev.slimevr.poserecorder.TrackerFrameData;
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;
// Human average is probably 1.1235 (SD 0.07)
public float legBodyRatio = 1.1235f;
// SD of 0.07, capture 68% within range
public float legBodyRatioRange = 0.07f;
// Assume these to be approximately half
public float kneeLegRatio = 0.5f;
public float chestWaistRatio = 0.5f;
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() {
reloadConfigValues(null);
}
public void reloadConfigValues(TrackerFrame[] frame) {
// 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) || (frame != null && TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.CHEST) != null) || 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;
}
configs.forEach(skeleton::setSkeletonConfig);
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(TrackerFrame[] frame : frames) {
TrackerFrame hmd = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.HMD);
if(hmd != null && hmd.hasData(TrackerFrameData.POSITION) && hmd.position.y > maxHeight) {
maxHeight = hmd.position.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) {
final int frameCount = frames.getMaxFrameCount();
final SimpleSkeleton skeleton1 = new SimpleSkeleton(configs, staticConfigs);
final TrackerFrame[] trackerBuffer1 = new TrackerFrame[frames.getTrackerCount()];
frames.getFrames(0, trackerBuffer1);
reloadConfigValues(trackerBuffer1); // Reload configs and detect chest tracker from the first frame
final SimpleSkeleton skeleton2 = new SimpleSkeleton(configs, staticConfigs);
final TrackerFrame[] trackerBuffer2 = new TrackerFrame[frames.getTrackerCount()];
// 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 < frameCount; cursorOffset++) {
for(int frameCursor = 0; frameCursor < frameCount - cursorOffset; frameCursor += cursorIncrement) {
frames.getFrames(frameCursor, trackerBuffer1);
frames.getFrames(frameCursor + cursorOffset, trackerBuffer2);
skeleton1.setSkeletonConfigs(configs);
skeleton2.setSkeletonConfigs(configs);
skeleton1.setPoseFromFrame(trackerBuffer1);
skeleton2.setPoseFromFrame(trackerBuffer2);
float totalLength = getLengthSum(configs);
float curHeight = getHeight(configs, staticConfigs);
float errorDeriv = getErrorDeriv(trackerBuffer1, trackerBuffer2, 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(trackerBuffer1);
// 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(trackerBuffer1, trackerBuffer2, 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.getNodePosition(TrackerBodyPosition.LEFT_ANKLE).distance(skeleton2.getNodePosition(TrackerBodyPosition.LEFT_ANKLE));
float slideRight = skeleton1.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE).distance(skeleton2.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE));
// 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 skeleton1Left = skeleton1.getNodePosition(TrackerBodyPosition.LEFT_ANKLE).getY();
float skeleton1Right = skeleton1.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE).getY();
float skeleton2Left = skeleton2.getNodePosition(TrackerBodyPosition.LEFT_ANKLE).getY();
float skeleton2Right = skeleton2.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE).getY();
float dist1 = Math.abs(skeleton1Left - skeleton1Right);
float dist2 = Math.abs(skeleton2Left - skeleton2Right);
float dist3 = Math.abs(skeleton1Left - skeleton2Right);
float dist4 = Math.abs(skeleton2Left - skeleton1Right);
float dist5 = Math.abs(skeleton1Left - skeleton2Left);
float dist6 = Math.abs(skeleton1Right - skeleton2Right);
// 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) - chestWaistRatio) : 0f;
float legBody = legsLength != null && waistLength != null && neckLength != null ? Math.abs((legsLength / (waistLength + neckLength)) - legBodyRatio) : 0f;
float kneeLeg = kneeHeight != null && legsLength != null ? Math.abs((kneeHeight / legsLength) - kneeLegRatio) : 0f;
if(legBody <= legBodyRatioRange) {
legBody = 0f;
} else {
legBody -= legBodyRatioRange;
}
return (chestWaist + legBody + kneeLeg) / 3f;
}
// The distance of any points to the corresponding absolute position
protected float getPositionErrorDeriv(TrackerFrame[] frame, SimpleSkeleton skeleton) {
float offset = 0f;
int offsetCount = 0;
for(TrackerFrame trackerFrame : frame) {
if(trackerFrame == null || !trackerFrame.hasData(TrackerFrameData.POSITION)) {
continue;
}
Vector3f nodePos = skeleton.getNodePosition(trackerFrame.designation.designation);
if(nodePos != null) {
offset += Math.abs(nodePos.distance(trackerFrame.position));
offsetCount++;
}
}
return offsetCount > 0 ? offset / offsetCount : 0f;
}
// The difference between offset of absolute position and the corresponding point over time
protected float getPositionOffsetErrorDeriv(TrackerFrame[] frame1, TrackerFrame[] frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
float offset = 0f;
int offsetCount = 0;
for(TrackerFrame trackerFrame1 : frame1) {
if(trackerFrame1 == null || !trackerFrame1.hasData(TrackerFrameData.POSITION)) {
continue;
}
TrackerFrame trackerFrame2 = TrackerUtils.findTrackerForBodyPosition(frame2, trackerFrame1.designation);
if(trackerFrame2 == null || !trackerFrame2.hasData(TrackerFrameData.POSITION)) {
continue;
}
Vector3f nodePos1 = skeleton1.getNodePosition(trackerFrame1.designation);
if(nodePos1 == null) {
continue;
}
Vector3f nodePos2 = skeleton2.getNodePosition(trackerFrame2.designation);
if(nodePos2 == null) {
continue;
}
float dist1 = Math.abs(nodePos1.distance(trackerFrame1.position));
float dist2 = Math.abs(nodePos2.distance(trackerFrame2.position));
offset += Math.abs(dist2 - dist1);
offsetCount++;
}
return offsetCount > 0 ? offset / offsetCount : 0f;
}
protected float getErrorDeriv(TrackerFrame[] frame1, TrackerFrame[] 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,352 @@
package dev.slimevr.autobone;
import java.util.HashMap;
import java.util.Map;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.poserecorder.TrackerFrame;
import dev.slimevr.poserecorder.TrackerFrameData;
import io.eiren.vr.processor.HumanSkeletonWithLegs;
import io.eiren.vr.processor.HumanSkeletonWithWaist;
import io.eiren.vr.processor.TrackerBodyPosition;
import io.eiren.vr.processor.TransformNode;
import io.eiren.vr.trackers.TrackerUtils;
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>();
private Quaternion rotBuf1 = new Quaternion();
private Quaternion rotBuf2 = new Quaternion();
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(Map<String, Float> configs, Map<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) {
this(configs, null);
}
public void setPoseFromFrame(TrackerFrame[] frame) {
TrackerFrame hmd = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.HMD);
if(hmd != null) {
if(hmd.hasData(TrackerFrameData.ROTATION)) {
hmdNode.localTransform.setRotation(hmd.rotation);
headNode.localTransform.setRotation(hmd.rotation);
}
if(hmd.hasData(TrackerFrameData.POSITION)) {
hmdNode.localTransform.setTranslation(hmd.position);
}
}
TrackerFrame chest = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.CHEST, TrackerBodyPosition.WAIST);
setRotation(chest, neckNode);
TrackerFrame waist = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
setRotation(waist, chestNode);
TrackerFrame leftLeg = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.LEFT_LEG);
TrackerFrame rightLeg = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.RIGHT_LEG);
averagePelvis(waist, leftLeg, rightLeg);
setRotation(leftLeg, leftHipNode);
setRotation(rightLeg, rightHipNode);
TrackerFrame leftAnkle = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.LEFT_ANKLE);
setRotation(leftAnkle, rightKneeNode);
TrackerFrame rightAnkle = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.RIGHT_ANKLE);
setRotation(rightAnkle, leftKneeNode);
updatePose();
}
public void setRotation(TrackerFrame trackerFrame, TransformNode node) {
if(trackerFrame != null && trackerFrame.hasData(TrackerFrameData.ROTATION)) {
node.localTransform.setRotation(trackerFrame.rotation);
}
}
public void averagePelvis(TrackerFrame waist, TrackerFrame leftLeg, TrackerFrame rightLeg) {
if((leftLeg == null || rightLeg == null) || (!leftLeg.hasData(TrackerFrameData.ROTATION) || !rightLeg.hasData(TrackerFrameData.ROTATION))) {
setRotation(waist, waistNode);
return;
}
if(waist == null || !waist.hasData(TrackerFrameData.ROTATION)) {
if(leftLeg.hasData(TrackerFrameData.ROTATION) && rightLeg.hasData(TrackerFrameData.ROTATION)) {
leftLeg.getRotation(rotBuf1);
rightLeg.getRotation(rotBuf2);
rotBuf1.nlerp(rotBuf2, 0.5f);
waistNode.localTransform.setRotation(rotBuf1);
}
return;
}
// Average the pelvis with the waist rotation
leftLeg.getRotation(rotBuf1);
rightLeg.getRotation(rotBuf2);
rotBuf1.nlerp(rotBuf2, 0.5f);
waist.getRotation(rotBuf2);
rotBuf1.nlerp(rotBuf2, 0.3333333f);
waistNode.localTransform.setRotation(rotBuf1);
}
public void setSkeletonConfigs(Map<String, Float> configs) {
configs.forEach(this::setSkeletonConfig);
}
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 TransformNode getNode(String node) {
return nodes.get(node);
}
public TransformNode getNode(TrackerBodyPosition bodyPosition) {
return getNode(bodyPosition, false);
}
public TransformNode getNode(TrackerBodyPosition bodyPosition, boolean rotationNode) {
if(bodyPosition == null) {
return null;
}
switch(bodyPosition) {
case HMD:
return hmdNode;
case CHEST:
return rotationNode ? neckNode : chestNode;
case WAIST:
return rotationNode ? chestNode : waistNode;
case LEFT_LEG:
return rotationNode ? leftHipNode : leftKneeNode;
case RIGHT_LEG:
return rotationNode ? rightHipNode : rightKneeNode;
case LEFT_ANKLE:
return rotationNode ? leftKneeNode : leftAnkleNode;
case RIGHT_ANKLE:
return rotationNode ? rightKneeNode : rightAnkleNode;
}
return null;
}
public Vector3f getNodePosition(String node) {
TransformNode transformNode = getNode(node);
return transformNode != null ? transformNode.worldTransform.getTranslation() : null;
}
public Vector3f getNodePosition(TrackerBodyPosition bodyPosition) {
TransformNode node = getNode(bodyPosition);
if(node == null) {
return null;
}
return node.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

@@ -0,0 +1,442 @@
package dev.slimevr.gui;
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;
import dev.slimevr.autobone.AutoBone;
import dev.slimevr.poserecorder.PoseFrame;
import dev.slimevr.poserecorder.PoseFrameIO;
import dev.slimevr.poserecorder.PoseRecorder;
public class AutoBoneWindow extends JFrame {
private static File saveDir = new File("Recordings");
private static File loadDir = new File("LoadRecordings");
private EJBox pane;
private final transient VRServer server;
private final transient SkeletonConfig skeletonConfig;
private final transient PoseRecorder poseRecorder;
private final transient AutoBone autoBone;
private transient Thread recordingThread = null;
private transient Thread saveRecordingThread = null;
private transient 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++ + ".pfr");
} 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(), ".pfr")) {
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.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.getTrackerCount() <= 0) {
throw new IllegalStateException("Recording has no trackers");
}
if(frames.getMaxFrameCount() <= 0) {
throw new IllegalStateException("Recording has no frames");
}
setText("Saving...");
saveRecording(frames);
setText("Recording Saved!");
try {
Thread.sleep(3000); // Wait for 3 seconds
} catch(Exception e1) {
// Ignore
}
} 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.isEmpty()) {
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.getTrackerCount() <= 0) {
throw new IllegalStateException("Recording has no trackers");
}
if(frames.getMaxFrameCount() <= 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.isEmpty()) {
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,143 @@
package dev.slimevr.poserecorder;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import io.eiren.util.collections.FastList;
import io.eiren.vr.trackers.Tracker;
public final class PoseFrame implements Iterable<TrackerFrame[]> {
private final FastList<PoseFrameTracker> trackers;
public PoseFrame(FastList<PoseFrameTracker> trackers) {
this.trackers = trackers;
}
public PoseFrame(int initialCapacity) {
this.trackers = new FastList<PoseFrameTracker>(initialCapacity);
}
public PoseFrame() {
this(5);
}
public PoseFrameTracker addTracker(PoseFrameTracker tracker) {
trackers.add(tracker);
return tracker;
}
public PoseFrameTracker addTracker(Tracker tracker, int initialCapacity) {
return addTracker(new PoseFrameTracker(tracker.getName(), initialCapacity));
}
public PoseFrameTracker addTracker(Tracker tracker) {
return addTracker(tracker, 5);
}
public PoseFrameTracker removeTracker(int index) {
return trackers.remove(index);
}
public PoseFrameTracker removeTracker(PoseFrameTracker tracker) {
trackers.remove(tracker);
return tracker;
}
public void clearTrackers() {
trackers.clear();
}
public void fakeClearTrackers() {
trackers.fakeClear();
}
public int getTrackerCount() {
return trackers.size();
}
public List<PoseFrameTracker> getTrackers() {
return trackers;
}
public int getMaxFrameCount() {
int maxFrames = 0;
for(int i = 0; i < trackers.size(); i++) {
PoseFrameTracker tracker = trackers.get(i);
if(tracker != null && tracker.getFrameCount() > maxFrames) {
maxFrames = tracker.getFrameCount();
}
}
return maxFrames;
}
public int getFrames(int frameIndex, TrackerFrame[] buffer) {
for(int i = 0; i < trackers.size(); i++) {
PoseFrameTracker tracker = trackers.get(i);
buffer[i] = tracker != null ? tracker.safeGetFrame(frameIndex) : null;
}
return trackers.size();
}
public int getFrames(int frameIndex, List<TrackerFrame> buffer) {
for(int i = 0; i < trackers.size(); i++) {
PoseFrameTracker tracker = trackers.get(i);
buffer.add(i, tracker != null ? tracker.safeGetFrame(frameIndex) : null);
}
return trackers.size();
}
public TrackerFrame[] getFrames(int frameIndex) {
TrackerFrame[] trackerFrames = new TrackerFrame[trackers.size()];
getFrames(frameIndex, trackerFrames);
return trackerFrames;
}
@Override
public Iterator<TrackerFrame[]> iterator() {
return new PoseFrameIterator(this);
}
public class PoseFrameIterator implements Iterator<TrackerFrame[]> {
private final PoseFrame poseFrame;
private final TrackerFrame[] trackerFrameBuffer;
private int cursor = 0;
public PoseFrameIterator(PoseFrame poseFrame) {
this.poseFrame = poseFrame;
trackerFrameBuffer = new TrackerFrame[poseFrame.getTrackerCount()];
}
@Override
public boolean hasNext() {
if(trackers.isEmpty()) {
return false;
}
for(int i = 0; i < trackers.size(); i++) {
PoseFrameTracker tracker = trackers.get(i);
if(tracker != null && cursor < tracker.getFrameCount()) {
return true;
}
}
return false;
}
@Override
public TrackerFrame[] next() {
if(!hasNext()) {
throw new NoSuchElementException();
}
poseFrame.getFrames(cursor++, trackerFrameBuffer);
return trackerFrameBuffer;
}
}
}

View File

@@ -0,0 +1,139 @@
package dev.slimevr.poserecorder;
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 com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.processor.TrackerBodyPosition;
public final class PoseFrameIO {
private PoseFrameIO() {
// Do not allow instantiating
}
public static boolean writeFrames(DataOutputStream outputStream, PoseFrame frames) {
try {
if(frames != null) {
outputStream.writeInt(frames.getTrackerCount());
for(PoseFrameTracker tracker : frames.getTrackers()) {
outputStream.writeUTF(tracker.name);
outputStream.writeInt(tracker.getFrameCount());
for(int i = 0; i < tracker.getFrameCount(); i++) {
TrackerFrame trackerFrame = tracker.safeGetFrame(i);
if(trackerFrame == null) {
outputStream.writeInt(0);
continue;
}
outputStream.writeInt(trackerFrame.getDataFlags());
if(trackerFrame.hasData(TrackerFrameData.DESIGNATION)) {
outputStream.writeUTF(trackerFrame.designation.designation);
}
if(trackerFrame.hasData(TrackerFrameData.ROTATION)) {
outputStream.writeFloat(trackerFrame.rotation.getX());
outputStream.writeFloat(trackerFrame.rotation.getY());
outputStream.writeFloat(trackerFrame.rotation.getZ());
outputStream.writeFloat(trackerFrame.rotation.getW());
}
if(trackerFrame.hasData(TrackerFrameData.POSITION)) {
outputStream.writeFloat(trackerFrame.position.getX());
outputStream.writeFloat(trackerFrame.position.getY());
outputStream.writeFloat(trackerFrame.position.getZ());
}
}
}
} else {
outputStream.writeInt(0);
}
} catch(Exception e) {
LogManager.log.severe("Error writing frame to stream", e);
return false;
}
return true;
}
public static boolean writeToFile(File file, PoseFrame frames) {
try(DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
writeFrames(outputStream, frames);
} catch(Exception e) {
LogManager.log.severe("Error writing frames to file", e);
return false;
}
return true;
}
public static PoseFrame readFrames(DataInputStream inputStream) {
try {
int trackerCount = inputStream.readInt();
FastList<PoseFrameTracker> trackers = new FastList<PoseFrameTracker>(trackerCount);
for(int i = 0; i < trackerCount; i++) {
String name = inputStream.readUTF();
int trackerFrameCount = inputStream.readInt();
FastList<TrackerFrame> trackerFrames = new FastList<TrackerFrame>(trackerFrameCount);
for(int j = 0; j < trackerFrameCount; j++) {
int dataFlags = inputStream.readInt();
TrackerBodyPosition designation = null;
if(TrackerFrameData.DESIGNATION.check(dataFlags)) {
designation = TrackerBodyPosition.getByDesignation(inputStream.readUTF());
}
Quaternion rotation = null;
if(TrackerFrameData.ROTATION.check(dataFlags)) {
float quatX = inputStream.readFloat();
float quatY = inputStream.readFloat();
float quatZ = inputStream.readFloat();
float quatW = inputStream.readFloat();
rotation = new Quaternion(quatX, quatY, quatZ, quatW);
}
Vector3f position = null;
if(TrackerFrameData.POSITION.check(dataFlags)) {
float posX = inputStream.readFloat();
float posY = inputStream.readFloat();
float posZ = inputStream.readFloat();
position = new Vector3f(posX, posY, posZ);
}
trackerFrames.add(new TrackerFrame(designation, rotation, position));
}
trackers.add(new PoseFrameTracker(name, trackerFrames));
}
return new PoseFrame(trackers);
} catch(Exception e) {
LogManager.log.severe("Error reading frame from stream", e);
}
return null;
}
public static PoseFrame readFromFile(File file) {
try {
return readFrames(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,233 @@
package dev.slimevr.poserecorder;
import java.util.Iterator;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.util.collections.FastList;
import io.eiren.vr.processor.TrackerBodyPosition;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerConfig;
import io.eiren.vr.trackers.TrackerStatus;
public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
public final String name;
private final FastList<TrackerFrame> frames;
private int frameCursor = 0;
public PoseFrameTracker(String name, FastList<TrackerFrame> frames) {
if(frames == null) {
throw new NullPointerException("frames must not be null");
}
this.name = name != null ? name : "";
this.frames = frames;
}
public PoseFrameTracker(String name, int initialCapacity) {
this(name, new FastList<TrackerFrame>(initialCapacity));
}
public PoseFrameTracker(String name) {
this(name, 5);
}
private int limitCursor() {
if(frameCursor < 0 || frames.isEmpty()) {
frameCursor = 0;
} else if(frameCursor >= frames.size()) {
frameCursor = frames.size() - 1;
}
return frameCursor;
}
public int setCursor(int index) {
frameCursor = index;
return limitCursor();
}
public int incrementCursor(int increment) {
frameCursor += increment;
return limitCursor();
}
public int incrementCursor() {
return incrementCursor(1);
}
public int getCursor() {
return frameCursor;
}
public int getFrameCount() {
return frames.size();
}
public TrackerFrame addFrame(int index, TrackerFrame trackerFrame) {
frames.add(index, trackerFrame);
return trackerFrame;
}
public TrackerFrame addFrame(int index, Tracker tracker) {
return addFrame(index, TrackerFrame.fromTracker(tracker));
}
public TrackerFrame addFrame(TrackerFrame trackerFrame) {
frames.add(trackerFrame);
return trackerFrame;
}
public TrackerFrame addFrame(Tracker tracker) {
return addFrame(TrackerFrame.fromTracker(tracker));
}
public TrackerFrame removeFrame(int index) {
TrackerFrame trackerFrame = frames.remove(index);
limitCursor();
return trackerFrame;
}
public TrackerFrame removeFrame(TrackerFrame trackerFrame) {
frames.remove(trackerFrame);
limitCursor();
return trackerFrame;
}
public void clearFrames() {
frames.clear();
limitCursor();
}
public void fakeClearFrames() {
frames.fakeClear();
limitCursor();
}
public TrackerFrame getFrame(int index) {
return frames.get(index);
}
public TrackerFrame getFrame() {
return getFrame(frameCursor);
}
public TrackerFrame safeGetFrame(int index) {
try {
return getFrame(index);
} catch(Exception e) {
return null;
}
}
public TrackerFrame safeGetFrame() {
return safeGetFrame(frameCursor);
}
//#region Tracker Interface Implementation
@Override
public boolean getRotation(Quaternion store) {
TrackerFrame frame = safeGetFrame();
if(frame != null && frame.hasData(TrackerFrameData.ROTATION)) {
store.set(frame.rotation);
return true;
}
store.set(0, 0, 0, 1);
return false;
}
@Override
public boolean getPosition(Vector3f store) {
TrackerFrame frame = safeGetFrame();
if(frame != null && frame.hasData(TrackerFrameData.POSITION)) {
store.set(frame.position);
return true;
}
store.set(0, 0, 0);
return false;
}
@Override
public String getName() {
return name;
}
@Override
public TrackerStatus getStatus() {
return TrackerStatus.OK;
}
@Override
public void loadConfig(TrackerConfig config) {
throw new UnsupportedOperationException("PoseFrameTracker does not implement configuration");
}
@Override
public void saveConfig(TrackerConfig config) {
throw new UnsupportedOperationException("PoseFrameTracker does not implement configuration");
}
@Override
public float getConfidenceLevel() {
return 0;
}
@Override
public void resetFull(Quaternion reference) {
throw new UnsupportedOperationException("PoseFrameTracker does not implement calibration");
}
@Override
public void resetYaw(Quaternion reference) {
throw new UnsupportedOperationException("PoseFrameTracker does not implement calibration");
}
@Override
public void tick() {
throw new UnsupportedOperationException("PoseFrameTracker does not implement this method");
}
@Override
public TrackerBodyPosition getBodyPosition() {
TrackerFrame frame = safeGetFrame();
return frame == null ? null : frame.designation;
}
@Override
public void setBodyPosition(TrackerBodyPosition position) {
throw new UnsupportedOperationException("PoseFrameTracker does not allow setting the body position");
}
@Override
public boolean userEditable() {
return false;
}
@Override
public boolean hasRotation() {
TrackerFrame frame = safeGetFrame();
return frame != null && frame.hasData(TrackerFrameData.ROTATION);
}
@Override
public boolean hasPosition() {
TrackerFrame frame = safeGetFrame();
return frame != null && frame.hasData(TrackerFrameData.POSITION);
}
@Override
public boolean isComputed() {
return true;
}
//#endregion
@Override
public Iterator<TrackerFrame> iterator() {
return frames.iterator();
}
}

View File

@@ -0,0 +1,163 @@
package dev.slimevr.poserecorder;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.apache.commons.lang3.tuple.Pair;
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.trackers.Tracker;
public class PoseRecorder {
protected PoseFrame poseFrame = null;
protected int numFrames = -1;
protected int frameCursor = 0;
protected long frameRecordingInterval = 60L;
protected long nextFrameTimeMs = -1L;
protected CompletableFuture<PoseFrame> currentRecording;
protected final VRServer server;
FastList<Pair<Tracker, PoseFrameTracker>> trackers = new FastList<Pair<Tracker, PoseFrameTracker>>();
public PoseRecorder(VRServer server) {
this.server = server;
server.addOnTick(this::onTick);
}
@VRServerThread
public void onTick() {
if(numFrames > 0) {
PoseFrame poseFrame = this.poseFrame;
List<Pair<Tracker, PoseFrameTracker>> trackers = this.trackers;
if(poseFrame != null && trackers != null) {
if(frameCursor < numFrames) {
if(System.currentTimeMillis() >= nextFrameTimeMs) {
nextFrameTimeMs = System.currentTimeMillis() + frameRecordingInterval;
int cursor = frameCursor++;
for(Pair<Tracker, PoseFrameTracker> tracker : trackers) {
// Add a frame for each tracker
tracker.getRight().addFrame(cursor, tracker.getLeft());
}
// If done, send finished recording
if(frameCursor >= numFrames) {
internalStopRecording();
}
}
} else {
// If done and hasn't yet, send finished recording
internalStopRecording();
}
}
}
}
public synchronized Future<PoseFrame> startFrameRecording(int numFrames, long interval) {
return startFrameRecording(numFrames, interval, server.getAllTrackers());
}
public synchronized Future<PoseFrame> startFrameRecording(int numFrames, long interval, List<Tracker> trackers) {
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(trackers == null) {
throw new IllegalArgumentException("trackers must not be null");
}
if(trackers.isEmpty()) {
throw new IllegalArgumentException("trackers must have at least one entry");
}
if(!isReadyToRecord()) {
throw new IllegalStateException("PoseRecorder isn't ready to record!");
}
cancelFrameRecording();
poseFrame = new PoseFrame(trackers.size());
// Update tracker list
this.trackers.ensureCapacity(trackers.size());
for(Tracker tracker : trackers) {
// Ignore null and computed trackers
if(tracker == null || tracker.isComputed()) {
continue;
}
// Pair tracker with recording
this.trackers.add(Pair.of(tracker, poseFrame.addTracker(tracker, numFrames)));
}
this.frameCursor = 0;
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(poseFrame);
}
numFrames = -1;
frameCursor = 0;
trackers.clear();
poseFrame = null;
}
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;
frameCursor = 0;
trackers.clear();
poseFrame = null;
}
public boolean isReadyToRecord() {
return server.getTrackersCount() > 0;
}
public boolean isRecording() {
return numFrames > frameCursor;
}
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,173 @@
package dev.slimevr.poserecorder;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.vr.processor.TrackerBodyPosition;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerConfig;
import io.eiren.vr.trackers.TrackerStatus;
public final class TrackerFrame implements Tracker {
private int dataFlags = 0;
public final TrackerBodyPosition designation;
public final Quaternion rotation;
public final Vector3f position;
public TrackerFrame(TrackerBodyPosition designation, Quaternion rotation, Vector3f position) {
this.designation = designation;
if(designation != null) {
dataFlags |= TrackerFrameData.DESIGNATION.flag;
}
this.rotation = rotation;
if(rotation != null) {
dataFlags |= TrackerFrameData.ROTATION.flag;
}
this.position = position;
if(position != null) {
dataFlags |= TrackerFrameData.POSITION.flag;
}
}
public static TrackerFrame fromTracker(Tracker tracker) {
if(tracker == null) {
return null;
}
// If the tracker is not ready
if(tracker.getStatus() != TrackerStatus.OK && tracker.getStatus() != TrackerStatus.BUSY && tracker.getStatus() != TrackerStatus.OCCLUDED) {
return null;
}
// If tracker has no data
if(tracker.getBodyPosition() == null && !tracker.hasRotation() && !tracker.hasPosition()) {
return null;
}
Quaternion rotation = null;
if(tracker.hasRotation()) {
rotation = new Quaternion();
if(!tracker.getRotation(rotation)) {
// If getting the rotation failed, set it back to null
rotation = null;
}
}
Vector3f position = null;
if(tracker.hasPosition()) {
position = new Vector3f();
if(!tracker.getPosition(position)) {
// If getting the position failed, set it back to null
position = null;
}
}
return new TrackerFrame(tracker.getBodyPosition(), rotation, position);
}
public int getDataFlags() {
return dataFlags;
}
public boolean hasData(TrackerFrameData flag) {
return flag.check(dataFlags);
}
//#region Tracker Interface Implementation
@Override
public boolean getRotation(Quaternion store) {
if(hasData(TrackerFrameData.ROTATION)) {
store.set(rotation);
return true;
}
store.set(0, 0, 0, 1);
return false;
}
@Override
public boolean getPosition(Vector3f store) {
if(hasData(TrackerFrameData.POSITION)) {
store.set(position);
return true;
}
store.set(0, 0, 0);
return false;
}
@Override
public String getName() {
return "TrackerFrame:/" + (designation != null ? designation.designation : "null");
}
@Override
public TrackerStatus getStatus() {
return TrackerStatus.OK;
}
@Override
public void loadConfig(TrackerConfig config) {
throw new UnsupportedOperationException("TrackerFrame does not implement configuration");
}
@Override
public void saveConfig(TrackerConfig config) {
throw new UnsupportedOperationException("TrackerFrame does not implement configuration");
}
@Override
public float getConfidenceLevel() {
return 0;
}
@Override
public void resetFull(Quaternion reference) {
throw new UnsupportedOperationException("TrackerFrame does not implement calibration");
}
@Override
public void resetYaw(Quaternion reference) {
throw new UnsupportedOperationException("TrackerFrame does not implement calibration");
}
@Override
public void tick() {
throw new UnsupportedOperationException("TrackerFrame does not implement this method");
}
@Override
public TrackerBodyPosition getBodyPosition() {
return designation;
}
@Override
public void setBodyPosition(TrackerBodyPosition position) {
throw new UnsupportedOperationException("TrackerFrame does not allow setting the body position");
}
@Override
public boolean userEditable() {
return false;
}
@Override
public boolean hasRotation() {
return hasData(TrackerFrameData.ROTATION);
}
@Override
public boolean hasPosition() {
return hasData(TrackerFrameData.POSITION);
}
@Override
public boolean isComputed() {
return true;
}
//#endregion
}

View File

@@ -0,0 +1,19 @@
package dev.slimevr.poserecorder;
public enum TrackerFrameData {
DESIGNATION(0),
ROTATION(1),
POSITION(2),
;
public final int flag;
TrackerFrameData(int id) {
this.flag = 1 << id;
}
public boolean check(int dataFlags) {
return (dataFlags & this.flag) != 0;
}
}

View File

@@ -0,0 +1,24 @@
package io.eiren.gui;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
public abstract class AbstractComponentListener implements ComponentListener {
@Override
public void componentResized(ComponentEvent e) {
}
@Override
public void componentMoved(ComponentEvent e) {
}
@Override
public void componentShown(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) {
}
}

View File

@@ -0,0 +1,35 @@
package io.eiren.gui;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
public abstract class AbstractWindowListener implements WindowListener {
@Override
public void windowOpened(WindowEvent e) {
}
@Override
public void windowClosing(WindowEvent e) {
}
@Override
public void windowClosed(WindowEvent e) {
}
@Override
public void windowIconified(WindowEvent e) {
}
@Override
public void windowDeiconified(WindowEvent e) {
}
@Override
public void windowActivated(WindowEvent e) {
}
@Override
public void windowDeactivated(WindowEvent e) {
}
}

View File

@@ -1,45 +1,45 @@
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 dev.slimevr.gui.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 JCheckBox("Extended pelvis model") {{
addItemListener(new ItemListener() {
@Override
@@ -63,7 +63,7 @@ public class SkeletonConfig extends EJBag {
}
}}, s(c(0, row, 1), 3, 1));
row++;
//*/
/*
add(new JCheckBox("Extended knee model") {{
addItemListener(new ItemListener() {
@@ -89,17 +89,26 @@ public class SkeletonConfig extends EJBag {
}}, 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));
@@ -148,25 +157,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();
@@ -180,17 +198,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() {
@@ -201,9 +219,9 @@ public class SkeletonConfig extends EJBag {
});
}
}
private class ResetButton extends JButton {
public ResetButton(String text, String joint) {
super(text);
addMouseListener(new MouseInputAdapter() {
@@ -214,9 +232,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;
@@ -110,21 +143,23 @@ public class TrackersList extends EJBox {
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, row, 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()) {
@@ -171,13 +206,17 @@ public class TrackersList extends EJBox {
}
row++;
}
add(new JLabel("Rotation"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Position"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
if(t.hasRotation())
add(new JLabel("Rotation"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
if(t.hasPosition())
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));
if(t.hasRotation())
add(rotation = new JLabel("0 0 0"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
if(t.hasPosition())
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));
@@ -222,7 +261,7 @@ public class TrackersList extends EJBox {
@SuppressWarnings("unchecked")
@AWTThread
public void update() {
if(position == null)
if(position == null && rotation == null)
return;
Tracker realTracker = t;
if(t instanceof ReferenceAdjustedTracker)
@@ -231,12 +270,14 @@ public class TrackersList extends EJBox {
t.getPosition(v);
q.toAngles(angles);
position.setText(StringUtils.prettyNumber(v.x, 1)
+ " " + StringUtils.prettyNumber(v.y, 1)
+ " " + StringUtils.prettyNumber(v.z, 1));
rotation.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(position != null)
position.setText(StringUtils.prettyNumber(v.x, 1)
+ " " + StringUtils.prettyNumber(v.y, 1)
+ " " + StringUtils.prettyNumber(v.z, 1));
if(rotation != null)
rotation.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));
status.setText(t.getStatus().toString().toLowerCase());
if(realTracker instanceof TrackerWithTPS) {
@@ -279,14 +320,14 @@ public class TrackersList extends EJBox {
}
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

@@ -1,9 +1,12 @@
package io.eiren.gui;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.MouseInputAdapter;
import io.eiren.util.MacOSX;
import io.eiren.util.OperatingSystem;
import io.eiren.util.StringUtils;
import io.eiren.util.ann.AWTThread;
import io.eiren.vr.Main;
@@ -11,21 +14,30 @@ import io.eiren.vr.VRServer;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static javax.swing.BoxLayout.PAGE_AXIS;
import static javax.swing.BoxLayout.LINE_AXIS;
public class VRServerGUI extends JFrame {
public static final String TITLE = "SlimeVR Server (" + Main.VERSION + ")";
public final VRServer server;
private final TrackersList trackersList;
private final SkeletonList skeletonList;
private JButton resetButton;
private JScrollPane scroll;
private EJBox pane;
private float zoom = 1.5f;
@@ -33,13 +45,29 @@ public class VRServerGUI extends JFrame {
@AWTThread
public VRServerGUI(VRServer server) {
super("SlimeVR Server (" + Main.VERSION + ")");
super(TITLE);
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch(Exception e) {
e.printStackTrace();
}
//increaseFontSize();
if(OperatingSystem.getCurrentPlatform() == OperatingSystem.OSX)
MacOSX.setTitle(TITLE);
try {
List<BufferedImage> images = new ArrayList<BufferedImage>(6);
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon16.png")));
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon32.png")));
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon48.png")));
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon64.png")));
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon128.png")));
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon256.png")));
setIconImages(images);
if(OperatingSystem.getCurrentPlatform() == OperatingSystem.OSX) {
MacOSX.setIcons(images);
}
} catch(IOException e1) {
e1.printStackTrace();
}
this.server = server;
@@ -54,18 +82,45 @@ public class VRServerGUI extends JFrame {
this.trackersList = new TrackersList(server, this);
this.skeletonList = new SkeletonList(server, this);
add(scroll = new JScrollPane(pane = new EJBox(PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
add(new JScrollPane(pane = new EJBox(PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
GraphicsConfiguration gc = getGraphicsConfiguration();
Rectangle screenBounds = gc.getBounds();
setMinimumSize(new Dimension(100, 100));
setSize(Math.min(server.config.getInt("window.width", 800), screenBounds.width), Math.min(server.config.getInt("window.height", 800), screenBounds.height));
setLocation(server.config.getInt("window.posx", screenBounds.x + (screenBounds.width - getSize().width) / 2), screenBounds.y + server.config.getInt("window.posy", (screenBounds.height - getSize().height) / 2));
// Resize and close listeners to save position and size betwen launcher starts
addComponentListener(new AbstractComponentListener() {
@Override
public void componentResized(ComponentEvent e) {
saveFrameInfo();
}
@Override
public void componentMoved(ComponentEvent e) {
saveFrameInfo();
}
});
build();
}
protected void saveFrameInfo() {
Rectangle b = getBounds();
server.config.setProperty("window.width", b.width);
server.config.setProperty("window.height", b.height);
server.config.setProperty("window.posx", b.x);
server.config.setProperty("window.posy", b.y);
server.saveConfig();
}
public float getZoom() {
return this.zoom;
}
public void refresh() {
// Pack and display
pack();
//pack();
setVisible(true);
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
@@ -131,10 +186,10 @@ public class VRServerGUI extends JFrame {
JComboBox<String> trackersSelect;
add(trackersSelect = new JComboBox<>());
trackersSelect.addItem("Waist");
trackersSelect.addItem("Waist + Feet");
trackersSelect.addItem("Waist + Feet + Chest");
trackersSelect.addItem("Waist + Feet + Knees");
trackersSelect.addItem("Waist + Feet + Chest + Knees");
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);
@@ -193,7 +248,6 @@ public class VRServerGUI extends JFrame {
}});
refresh();
setLocationRelativeTo(null);
server.addOnTick(trackersList::updateTrackers);
server.addOnTick(skeletonList::updateBones);

View File

@@ -53,7 +53,7 @@ public class WiFiWindow extends JFrame {
SerialPort[] ports = SerialPort.getCommPorts();
for(SerialPort port : ports) {
if(port.getDescriptivePortName().toLowerCase().contains("ch340") || port.getDescriptivePortName().toLowerCase().contains("cp21")) {
if(port.getDescriptivePortName().toLowerCase().contains("ch340") || port.getDescriptivePortName().toLowerCase().contains("cp21") || port.getDescriptivePortName().toLowerCase().contains("ch910")) {
trackerPort = port;
break;
}

View File

@@ -7,7 +7,7 @@ import io.eiren.util.logging.LogManager;
public class Main {
public static String VERSION = "0.0.14";
public static String VERSION = "0.0.19";
public static VRServer vrServer;
@@ -29,6 +29,12 @@ public class Main {
new VRServerGUI(vrServer);
} catch(Throwable e) {
e.printStackTrace();
try {
Thread.sleep(2000L);
} catch(InterruptedException e2) {
e.printStackTrace();
}
System.exit(1); // Exit in case error happened on init and window not appeared, but some thread started
} finally {
try {
Thread.sleep(2000L);

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;
@@ -57,6 +59,10 @@ public class VRServer extends Thread {
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();
}
@@ -143,7 +149,7 @@ public class VRServer extends Thread {
}
@ThreadSafe
public void saveConfig() {
public synchronized void saveConfig() {
List<YamlNode> nodes = config.getNodeList("trackers", null);
List<Map<String, Object>> trackersConfig = new FastList<>(nodes.size());
for(int i = 0; i < nodes.size(); ++i) {
@@ -189,14 +195,13 @@ public class VRServer extends Thread {
break;
task.run();
} while(true);
for(int i = 0; i < trackers.size(); ++i)
trackers.get(i).tick();
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();

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;
@@ -45,14 +46,13 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
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());
this.internalTrackers = new FastList<>(shareTrackers.size());
for(int i = 0; i < shareTrackers.size(); ++i) {
Tracker t = shareTrackers.get(i);
ComputedTracker ct = new ComputedTracker("internal://" + t.getName());
ComputedTracker ct = new ComputedTracker("internal://" + t.getName(), true, true);
ct.setStatus(TrackerStatus.OK);
this.internalTrackers.add(ct);
}
@@ -114,30 +114,28 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
}
}
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;
}
@@ -146,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) {
@@ -157,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);
}
}
}
@@ -171,11 +193,11 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
private void initTrackerPipe(Pipe pipe, int trackerId) {
String trackerHello = this.shareTrackers.size() + " 0";
System.arraycopy(trackerHello.getBytes(ASCII), 0, buffer, 0, trackerHello.length());
buffer[trackerHello.length()] = '\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);
@@ -183,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;
}
@@ -246,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,276 @@
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), true, true);
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(), true, true);
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.setStatus(internal.getStatus());
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

@@ -10,7 +10,7 @@ public class ComputedHumanPoseTracker extends ComputedTracker implements Tracker
protected BufferedTimer timer = new BufferedTimer(1f);
public ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition skeletonPosition) {
super("human://" + skeletonPosition.name());
super("human://" + skeletonPosition.name(), true, true);
this.skeletonPosition = skeletonPosition;
}

View File

@@ -10,7 +10,6 @@ import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerStatus;
import io.eiren.vr.trackers.TrackerUtils;
public class HumanPoseProcessor {
@@ -85,29 +84,10 @@ public class HumanPoseProcessor {
@VRServerThread
private void updateSekeltonModel() {
boolean hasWaist = false;
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, TrackerBodyPosition.LEFT_LEG);
Tracker rightAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
if(waist != null)
hasWaist = true;
if(leftAnkle != null && rightAnkle != null)
hasBothLegs = true;
if(!hasWaist) {
skeleton = null; // Can't track anything without waist
} else if(hasBothLegs) {
disconnectAllTrackers();
skeleton = new HumanSkeletonWithLegs(server, computedTrackers);
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
onSkeletonUpdated.get(i).accept(skeleton);
} else {
disconnectAllTrackers();
skeleton = new HumanSkeletonWithWaist(server, computedTrackers);
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
onSkeletonUpdated.get(i).accept(skeleton);
}
disconnectAllTrackers();
skeleton = new HumanSkeletonWithLegs(server, computedTrackers);
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
onSkeletonUpdated.get(i).accept(skeleton);
}
@VRServerThread

View File

@@ -69,11 +69,11 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
public HumanSkeletonWithLegs(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
super(server, computedTrackers);
List<Tracker> allTracekrs = server.getAllTrackers();
this.leftLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_LEG, TrackerBodyPosition.LEFT_ANKLE);
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
this.leftLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.LEFT_LEG, TrackerBodyPosition.LEFT_ANKLE);
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
this.leftFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_FOOT);
this.rightLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_LEG, TrackerBodyPosition.RIGHT_ANKLE);
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
this.rightLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.RIGHT_LEG, TrackerBodyPosition.RIGHT_ANKLE);
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
this.rightFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_FOOT);
ComputedHumanPoseTracker lat = null;
ComputedHumanPoseTracker rat = null;
@@ -90,6 +90,10 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.RIGHT_KNEE)
rkt = t;
}
if(lat == null)
lat = new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.LEFT_FOOT);
if(rat == null)
rat = new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.RIGHT_FOOT);
computedLeftFootTracker = lat;
computedRightFootTracker = rat;
computedLeftKneeTracker = lkt;
@@ -100,7 +104,7 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
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);
//extendedPelvisModel = server.config.getBoolean("body.model.extendedPelvis", extendedPelvisModel);
extendedKneeModel = server.config.getBoolean("body.model.extendedKnee", extendedKneeModel);
waistNode.attachChild(leftHipNode);
@@ -197,6 +201,7 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
}
}
@Override
public boolean getSkeletonConfigBoolean(String config) {
switch(config) {
case "Extended pelvis model":
@@ -204,9 +209,10 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
case "Extended knee model":
return extendedKneeModel;
}
return false;
return super.getSkeletonConfigBoolean(config);
}
@Override
public void setSkeletonConfigBoolean(String config, boolean newState) {
switch(config) {
case "Extended pelvis model":
@@ -216,6 +222,10 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
case "Extended knee model":
extendedKneeModel = newState;
server.config.setProperty("body.model.extendedKnee", newState);
break;
default:
super.setSkeletonConfigBoolean(config, newState);
break;
}
}
@@ -262,9 +272,12 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
// Average pelvis between two legs
leftHipNode.localTransform.getRotation(hipBuf);
rightHipNode.localTransform.getRotation(kneeBuf);
kneeBuf.slerp(hipBuf, 0.5f);
kneeBuf.nlerp(hipBuf, 0.5f);
chestNode.localTransform.getRotation(hipBuf);
kneeBuf.nlerp(hipBuf, 0.3333333f);
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
}
}

View File

@@ -21,7 +21,7 @@ public class HumanSkeletonWithWaist extends HumanSkeleton {
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();
@@ -57,11 +57,11 @@ public class HumanSkeletonWithWaist extends HumanSkeleton {
* Distance from eyes to ear
*/
protected float headShift = HEAD_SHIFT_DEFAULT;
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.waistTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
this.chestTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.CHEST, TrackerBodyPosition.WAIST);
this.hmdTracker = server.hmdTracker;
this.server = server;
ComputedHumanPoseTracker cwt = null;
@@ -114,16 +114,16 @@ public class HumanSkeletonWithWaist 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
@@ -141,7 +141,7 @@ public class HumanSkeletonWithWaist extends HumanSkeleton {
public Map<String, Float> getSkeletonConfig() {
return configMap;
}
@Override
public void setSkeletonConfig(String joint, float newLength) {
configMap.put(joint, newLength);
@@ -177,6 +177,13 @@ public class HumanSkeletonWithWaist extends HumanSkeleton {
}
}
public boolean getSkeletonConfigBoolean(String config) {
return false;
}
public void setSkeletonConfigBoolean(String config, boolean newState) {
}
@Override
public TransformNode getRootNode() {
return hmdNode;
@@ -198,7 +205,7 @@ public class HumanSkeletonWithWaist extends HumanSkeleton {
hmdNode.localTransform.setRotation(qBuf);
headNode.localTransform.setRotation(qBuf);
}
if(chestTracker.getRotation(qBuf))
neckNode.localTransform.setRotation(qBuf);

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;
@@ -32,6 +34,6 @@ public enum TrackerBodyPosition {
static {
for(TrackerBodyPosition tbp : values())
byDesignation.put(tbp.designation, tbp);
byDesignation.put(tbp.designation.toLowerCase(), tbp);
}
}

View File

@@ -12,9 +12,13 @@ public class ComputedTracker implements Tracker {
protected final String name;
protected TrackerStatus status = TrackerStatus.DISCONNECTED;
public TrackerBodyPosition bodyPosition = null;
protected final boolean hasRotation;
protected final boolean hasPosition;
public ComputedTracker(String name) {
public ComputedTracker(String name, boolean hasRotation, boolean hasPosition) {
this.name = name;
this.hasRotation = hasRotation;
this.hasPosition = hasPosition;
}
@Override
@@ -24,7 +28,10 @@ public class ComputedTracker implements Tracker {
@Override
public void loadConfig(TrackerConfig config) {
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
// Loading a config is an act of user editing, therefore it shouldn't not be allowed if editing is not allowed
if (userEditable()) {
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
}
}
@Override
@@ -84,4 +91,19 @@ public class ComputedTracker implements Tracker {
@Override
public void tick() {
}
@Override
public boolean hasRotation() {
return hasRotation;
}
@Override
public boolean hasPosition() {
return hasPosition;
}
@Override
public boolean isComputed() {
return true;
}
}

View File

@@ -8,7 +8,7 @@ public class HMDTracker extends ComputedTracker implements TrackerWithTPS {
protected BufferedTimer timer = new BufferedTimer(1f);
public HMDTracker(String name) {
super(name);
super(name, true, true);
setBodyPosition(TrackerBodyPosition.HMD);
}
@@ -21,4 +21,9 @@ public class HMDTracker extends ComputedTracker implements TrackerWithTPS {
public void dataTick() {
timer.update();
}
@Override
public boolean isComputed() {
return false;
}
}

View File

@@ -52,17 +52,20 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
@Override
public void loadConfig(TrackerConfig config) {
if(config.mountingRotation != null) {
mounting = TrackerMountingRotation.valueOf(config.mountingRotation);
if(mounting != null) {
rotAdjust.set(mounting.quaternion);
// Loading a config is an act of user editing, therefore it shouldn't not be allowed if editing is not allowed
if (userEditable()) {
if(config.mountingRotation != null) {
mounting = TrackerMountingRotation.valueOf(config.mountingRotation);
if(mounting != null) {
rotAdjust.set(mounting.quaternion);
} else {
rotAdjust.loadIdentity();
}
} else {
rotAdjust.loadIdentity();
}
} else {
rotAdjust.loadIdentity();
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
}
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
}
public TrackerMountingRotation getMountingRotation() {
@@ -225,4 +228,19 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
byStatus[ca.status] = ca;
}
}
@Override
public boolean hasRotation() {
return true;
}
@Override
public boolean hasPosition() {
return false;
}
@Override
public boolean isComputed() {
return false;
}
}

View File

@@ -146,4 +146,19 @@ public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
public void tick() {
tracker.tick();
}
}
@Override
public boolean hasRotation() {
return tracker.hasRotation();
}
@Override
public boolean hasPosition() {
return tracker.hasPosition();
}
@Override
public boolean isComputed() {
return tracker.isComputed();
}
}

View File

@@ -0,0 +1,34 @@
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, boolean hasRotation, boolean hasPosition) {
super(name, hasRotation, hasPosition);
this.id = id;
}
@Override
public float getTPS() {
return timer.getAverageFPS();
}
@Override
public void dataTick() {
timer.update();
}
@Override
public boolean userEditable() {
return true;
}
@Override
public boolean isComputed() {
return false;
}
}

View File

@@ -32,4 +32,10 @@ public interface Tracker {
public void setBodyPosition(TrackerBodyPosition position);
public boolean userEditable();
public boolean hasRotation();
public boolean hasPosition();
public boolean isComputed();
}

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

@@ -8,20 +8,56 @@ public class TrackerUtils {
private TrackerUtils() {
}
public static Tracker findTrackerForBodyPosition(List<Tracker> allTrackers, TrackerBodyPosition position) {
for(int i = 0; i < allTrackers.size(); ++i) {
Tracker t = allTrackers.get(i);
if(t.getBodyPosition() == position)
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerBodyPosition position) {
for(int i = 0; i < allTrackers.length; ++i) {
T t = allTrackers[i];
if(t != null && t.getBodyPosition() == position)
return t;
}
return null;
}
public static Tracker findTrackerForBodyPosition(List<Tracker> allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
Tracker t = findTrackerForBodyPosition(allTrackers, position);
public static <T extends Tracker> T findTrackerForBodyPosition(List<T> allTrackers, TrackerBodyPosition position) {
for(int i = 0; i < allTrackers.size(); ++i) {
T t = allTrackers.get(i);
if(t != null && t.getBodyPosition() == position)
return t;
}
return null;
}
public static <T extends Tracker> T findTrackerForBodyPosition(List<T> allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
T t = findTrackerForBodyPosition(allTrackers, position);
if(t != null)
return t;
return findTrackerForBodyPosition(allTrackers, altPosition);
}
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
T t = findTrackerForBodyPosition(allTrackers, position);
if(t != null)
return t;
return findTrackerForBodyPosition(allTrackers, altPosition);
}
public static Tracker findTrackerForBodyPositionOrEmpty(List<? extends Tracker> allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
Tracker t = findTrackerForBodyPosition(allTrackers, position);
if(t != null)
return t;
t = findTrackerForBodyPosition(allTrackers, altPosition);
if(t != null)
return t;
return new ComputedTracker("Empty tracker", false, false);
}
public static Tracker findTrackerForBodyPositionOrEmpty(Tracker[] allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
Tracker t = findTrackerForBodyPosition(allTrackers, position);
if(t != null)
return t;
t = findTrackerForBodyPosition(allTrackers, altPosition);
if(t != null)
return t;
return new ComputedTracker("Empty tracker", false, false);
}
}

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,12 +56,13 @@ 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;
@@ -97,25 +99,28 @@ public class TrackersUDPServer extends Thread {
macString = String.format("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
}
if(firmware.length() == 0)
if(firmware.length() == 0) {
firmware.append("owoTrack");
isOwo = true;
}
IMUTracker imu = new IMUTracker("udp:/" + handshakePacket.getAddress().toString(), this);
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: " + firmware + " (" + firmwareBuild + "), mac: " + macString);
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;
@@ -142,7 +147,7 @@ public class TrackersUDPServer extends Thread {
TrackerConnection connection;
IMUTracker tracker = null;
synchronized(trackers) {
connection = trackersMap.get(recieve.getSocketAddress());
connection = trackersMap.get(recieve.getAddress());
}
if(connection != null)
connection.lastPacket = System.currentTimeMillis();
@@ -173,6 +178,8 @@ public class TrackersUDPServer extends Thread {
case 17: // PACKET_ROTATION_DATA
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
int sensorId = bb.get() & 0xFF;
if(sensorId == 0) {
@@ -204,6 +211,8 @@ public class TrackersUDPServer extends Thread {
case 18: // PACKET_MAGENTOMETER_ACCURACY
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
sensorId = bb.get() & 0xFF;
if(sensorId == 0) {
@@ -235,6 +244,8 @@ public class TrackersUDPServer extends Thread {
case 5:
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
x = bb.getFloat();
z = bb.getFloat();
@@ -244,18 +255,24 @@ public class TrackersUDPServer extends Thread {
case 6: // PACKET_RAW_CALIBRATION_DATA
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(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(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
MPUTracker.ConfigurationData data = new MPUTracker.ConfigurationData(bb);
Consumer<String> dataConsumer = calibrationDataRequests.remove(connection.tracker);
@@ -266,6 +283,8 @@ public class TrackersUDPServer extends Thread {
case 9: // PACKET_RAW_MAGENTOMETER
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
float mx = bb.getFloat();
float my = bb.getFloat();
@@ -275,6 +294,8 @@ public class TrackersUDPServer extends Thread {
case 10: // PACKET_PING_PONG:
if(connection == null)
break;
if(connection.isOwoTrack)
break;
int pingId = bb.getInt();
if(connection.lastPingPacketId == pingId) {
tracker = connection.tracker;
@@ -284,6 +305,8 @@ public class TrackersUDPServer extends Thread {
case 11: // PACKET_SERIAL
if(connection == null)
break;
if(connection.isOwoTrack)
break;
tracker = connection.tracker;
bb.getLong();
int length = bb.getInt();
@@ -309,6 +332,8 @@ public class TrackersUDPServer extends Thread {
case 13: // PACKET_TAP
if(connection == null)
break;
if(connection.isOwoTrack)
break;
tracker = connection.tracker;
bb.getLong();
sensorId = bb.get() & 0xFF;
@@ -337,7 +362,7 @@ public class TrackersUDPServer extends Thread {
sensorId = bb.get() & 0xFF;
int sensorStatus = bb.get() & 0xFF;
if(sensorId == 1 && sensorStatus == 1 && connection.secondTracker == null) {
setUpAuxialrySensor(connection);
setUpAuxilarySensor(connection);
}
bb.rewind();
bb.putInt(15);
@@ -407,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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -67,7 +67,7 @@ public class ReferenceAdjustmentsTests {
public void checkReferenceAdjustmentFull(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
ComputedTracker tracker = new ComputedTracker("test");
ComputedTracker tracker = new ComputedTracker("test", true, false);
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetFull(referenceQuat);
@@ -86,7 +86,7 @@ public class ReferenceAdjustmentsTests {
public void checkReferenceAdjustmentYaw(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
ComputedTracker tracker = new ComputedTracker("test");
ComputedTracker tracker = new ComputedTracker("test", true, false);
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetYaw(referenceQuat);
@@ -98,7 +98,7 @@ public class ReferenceAdjustmentsTests {
private void testAdjustedTrackerRotation(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
ComputedTracker tracker = new ComputedTracker("test");
ComputedTracker tracker = new ComputedTracker("test", true, false);
tracker.rotation.set(trackerQuat);
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
adj.resetFull(referenceQuat);