Compare commits

...

108 Commits

Author SHA1 Message Date
Eiren Rain
4f14f01830 Merge pull request #114 from SlimeVR/test
Network refactoring
2022-01-27 21:51:56 +02:00
Eiren Rain
930b5c701a Cleanup UDP responses 2022-01-27 21:38:49 +02:00
Eiren Rain
bd9e2c47a3 Merge branch 'main' into test 2022-01-27 21:30:39 +02:00
Eiren Rain
53ca2cf881 Move UDP packets and parsing to own calsses
Cleanup UDP networking significantly
2022-01-27 21:30:25 +02:00
Eiren Rain
55e17e7625 Minor network fixes 2022-01-27 19:52:30 +02:00
Eiren Rain
13b37aa2a9 Improved debug, added checkbox to display sensors debug info 2022-01-27 19:31:13 +02:00
Eiren Rain
fe4dde69ea Merge pull request #113 from Louka3000/patch-1
fix link setup
2022-01-27 02:21:07 +02:00
Erimel
0268a5a3ec fix link setup 2022-01-26 19:13:52 -05:00
Eiren Rain
4bddb529d4 Fix ping not working 2022-01-25 22:00:02 +02:00
Eiren Rain
435f5d1751 Don't create new trackers if tracker's IP changed while server is running, hand over old trackers to the new connection
Implements #70
2022-01-25 21:46:50 +02:00
Eiren Rain
af8ce60dbe Fix signal strength reading 2022-01-20 20:57:03 +02:00
Eiren Rain
25f53232cd Fix packet number reading 2022-01-20 20:27:23 +02:00
Eiren Rain
012cb518b3 Fix merge issues, track packets order, improve logging in UDP server 2022-01-20 18:03:53 +02:00
Eiren Rain
2d1ffbc5b0 Merge branch 'main' into test
# Conflicts:
#	src/main/java/dev/slimevr/vr/trackers/TrackersUDPServer.java
2022-01-20 17:54:56 +02:00
Eiren Rain
c88a6802a9 Minor debug stuff 2022-01-20 17:50:08 +02:00
Eiren Rain
f5d608ac6a Merge pull request #111 from ButterscotchV/autobone-fix
Fix AutoBone overwriting configs & improve code documentation
2022-01-20 14:19:46 +03:00
Butterscotch!
5d49bbfb29 Fix AutoBone overwriting configs & improve code documentation 2022-01-19 19:20:47 -05:00
Eiren Rain
5ce520a316 Merge pull request #110 from deiteris/main
Fix decimal places for battery voltage
2022-01-20 00:44:49 +03:00
Yury
98c2c6e202 Merge branch 'main' of https://github.com/deiteris/SlimeVR-Server 2022-01-20 00:25:12 +03:00
Yury
a2fc809d71 Fix decimal places for battery voltage 2022-01-20 00:24:47 +03:00
Eiren Rain
eb302aaef1 Merge pull request #107 from Louka3000/main
Updated default body proportions (again
2022-01-16 02:12:36 +03:00
Louka
3b354f103a Update SkeletonConfigValue.java 2022-01-15 18:00:50 -05:00
Eiren Rain
03c24a5d39 Merge pull request #106 from deiteris/main
Fix log in firewall_uninstall.bat
2022-01-13 01:11:38 +03:00
Yury
a8f13bb570 Fix log in firewall_uninstall.bat 2022-01-13 00:56:08 +03:00
Eiren Rain
f8e35e0a72 Merge pull request #105 from deiteris/main
Show battery level reported by tracker
2022-01-13 00:32:10 +03:00
Yury
27c153f5d3 Show battery level reported by tracker 2022-01-12 19:30:51 +03:00
Eiren Rain
82fdedfa14 Minor changes 2022-01-10 12:54:25 +02:00
Eiren Rain
f5bfbb13e2 Added contributions notice to the README 2022-01-10 12:50:02 +02:00
Eiren Rain
80de578334 Fix missing refactoring changes 2022-01-09 12:27:32 +02:00
Eiren Rain
3b0acbe406 Create new trackers only when sensor info packet received 2022-01-09 12:25:57 +02:00
Eiren Rain
1062361612 Merge pull request #99 from deiteris/main
Add RSSI to trackers
2022-01-09 13:17:57 +03:00
Eiren Rain
7d81fe6f92 Merge pull request #102 from Louka3000/main
Skeleton offset
2022-01-09 11:13:42 +03:00
Eiren Rain
0285eca613 Merge pull request #103 from ColdIce1605/fix-typos
Fix typos
2022-01-08 09:03:10 +03:00
James R
b0aea9ba89 fix typos 2022-01-07 20:54:34 -06:00
Louka
b98eafb66f Remove vertical foot offset
Only keep skeleton offset
2022-01-07 09:12:06 -05:00
Louka
566df6793c fixed foot rotation (I broke it)
I still think there's a better way to do this horizontal foot offset thing
2022-01-06 23:43:26 -05:00
Louka
4949e0a7f3 Skeleton offset and vertical foot offset 2022-01-06 22:30:20 -05:00
Yury
572dcdf1bb Add RSSI to trackers 2022-01-02 23:25:56 +03:00
Eiren Rain
80ce825494 Merge pull request #97 from carl-anders/keybindings-wrap-even-more-no-more-panics-plz
Keybindings: Catch even more errors, and manually check that operating system is Windows
2021-12-27 02:23:13 +03:00
Carl Andersson
bdc3b1971c Fix accidental inversion of check 2021-12-26 21:49:46 +01:00
Carl Andersson
cee400a4c6 Simplify OS & error checks 2021-12-26 21:48:52 +01:00
Carl Andersson
e58706d212 Keybindings: Catch even more errors, and manually check that operating system is Windows 2021-12-26 06:05:27 +01:00
Eiren Rain
ad03caa064 Bump version to 0.1.3 2021-12-24 21:26:03 +02:00
Eiren Rain
e2d6189547 Code formatting 2021-12-24 21:25:40 +02:00
Eiren Rain
7b15d242f7 Move rest of the classes to dev.slimev package, comment out wrong unit-test, minor cleanups 2021-12-24 21:18:58 +02:00
Eiren Rain
b81458d034 Merge pull request #95 from ButterscotchVanilla/skeleton-refactor
Skeleton refactor
2021-12-24 22:09:57 +03:00
Eiren Rain
0595422f69 Merge pull request #96 from deiteris/main
Send empty packets when there are no active trackers
2021-12-18 20:46:43 +03:00
Yury
8e58adb279 Send empty packets when there are no active trackers 2021-12-18 16:33:49 +03:00
ButterscotchVanilla
a5e4b4d8e2 Fix all config "Reset" buttons being timed 2021-12-16 21:33:48 -05:00
ButterscotchVanilla
be2c010b5a Remove comment, this was fixed 2021-12-16 19:14:37 -05:00
ButterscotchVanilla
e107326fee Fix merge conflict 2021-12-16 18:54:20 -05:00
Eiren Rain
4da54f6dec Merge pull request #94 from Louka3000/main
Changed default body proportions
2021-12-16 18:15:42 +03:00
ButterscotchVanilla
1a3a955e10 Change StringBuilder.isEmpty() to length check instead for Java 8 compatibility 2021-12-16 02:26:02 -05:00
ButterscotchVanilla
3f304f7275 Optimize offset calculation for bulk config set & finish AutoBone implementation 2021-12-16 02:06:55 -05:00
ButterscotchVanilla
0690d742c7 Properly save SkeletonConfig values 2021-12-16 01:38:16 -05:00
ButterscotchVanilla
43bbd4b4dd Update config defaults from @Louka3000 2021-12-16 00:27:19 -05:00
ButterscotchVanilla
77fa27a698 Remove String skeleton config & fix null exception with SkeletonConfig 2021-12-15 23:00:08 -05:00
Louka
8991e4f9f8 Changed default body proportions 2021-12-15 20:43:13 -05:00
ButterscotchVanilla
c9740651ba Remove other HumanSkeleton implementations 2021-12-08 23:35:21 -05:00
ButterscotchVanilla
473550ba07 Move skeleton namespace 2021-12-08 23:12:43 -05:00
ButterscotchVanilla
a7cbe91e73 Make SimpleSkeleton compatible with HumanSkeleton 2021-12-08 23:10:54 -05:00
ButterscotchVanilla
40281f68b9 Use config enum instead of strings for AutoBone 2021-12-08 22:00:43 -05:00
ButterscotchVanilla
d3049751ba Update AutoBone to use SkeletonConfig 2021-12-08 21:26:32 -05:00
ButterscotchVanilla
68164756c2 Add more ways to set SkeletonConfig values 2021-12-08 19:53:07 -05:00
ButterscotchVanilla
91c0ddef28 Add new SkeletonConfig class for configuring bone lengths and toggles 2021-12-08 17:57:53 -05:00
Eiren Rain
0641ca1b7b Bump version to 0.1.2 2021-12-07 14:26:20 +02:00
ButterscotchVanilla
e3d9eb6ac9 Add new config for offset slide error & disable dist scaling 2021-12-06 19:37:47 -05:00
ButterscotchVanilla
7eec89bd53 AutoBone: Add offset slide error 2021-12-04 20:59:42 -05:00
ButterscotchVanilla
289a7f8313 AutoBone: Scale distances by height difference 2021-12-04 02:55:18 -05:00
ButterscotchVanilla
f49b2556ae Add full tracker functionality to SimpleSkeleton 2021-12-04 00:45:37 -05:00
Eiren Rain
318c43077c Merge pull request #89 from ButterscotchVanilla/bvh-standard
Add functional BVH recording
2021-12-02 23:54:10 +03:00
Eiren Rain
5691b68166 Merge pull request #90 from ButterscotchVanilla/patch-1
Update GitHub Actions workflow
2021-12-02 23:52:31 +03:00
Butterscotch!
0ea44f988c Update GitHub Actions workflow 2021-12-02 15:45:11 -05:00
ButterscotchVanilla
59e2f796eb Add reference to ViRe in a comment 2021-12-02 12:14:45 -05:00
ButterscotchVanilla
f1a75a98d0 Add extra Spine node for rotation 2021-12-02 03:37:13 -05:00
ButterscotchVanilla
76ac3fcf55 Fix angle calculations 2021-12-01 23:50:47 -05:00
ButterscotchVanilla
6cc3c8e84b Add attempted Euler conversion 2021-12-01 23:19:00 -05:00
ButterscotchVanilla
36907c3244 Add closeOutput for specific stream 2021-12-01 20:21:55 -05:00
ButterscotchVanilla
dfeb02c1a7 Add local rotation calculations to TransformNodeWrapper 2021-12-01 20:17:36 -05:00
ButterscotchVanilla
6adf5f4090 Move node hierarchy wrapping to TransformNodeWrapper 2021-12-01 20:17:36 -05:00
ButterscotchVanilla
e44ce3fb0b Add getParent to TransformNode and add StdBVHFileStream 2021-12-01 20:17:36 -05:00
ButterscotchVanilla
76ab69e44e Wrap TransformNodes for different PoseStream hierarchy requirements 2021-12-01 20:17:35 -05:00
Eiren Rain
57d009df5c Fix bug with wrong trackers being read in skeleton if no leg trackers are attached 2021-12-02 02:11:01 +02:00
Eiren Rain
b4d07b0b7e Merge pull request #88 from deiteris/main
Improve bat script error checking
2021-12-01 16:46:07 +03:00
Yury
da3afa6f8e Improve bat script error checking 2021-12-01 11:43:35 +03:00
Eiren Rain
ec1c491e93 Merge pull request #87 from Louka3000/main
autobone hip-only doesn't affect waist distance anymore
2021-12-01 10:27:45 +03:00
Louka
baccb556e8 autobone hip-only doesn't affect waist distance anymore
- Now autobone checks if user has BOTH waist and hip tracker to add the waist distance value.
- Also renamed certain variable, replacing "hip" by "torso"
2021-11-29 20:08:08 -05:00
Eiren Rain
eedfa61d74 Merge pull request #85 from deiteris/main
Prevent path change when running as admin
2021-11-24 08:39:44 +03:00
Eiren Rain
2a9225178f Merge pull request #84 from Louka3000/main
Changed body proportions: Torso, Chest, Waist
2021-11-23 23:36:31 +03:00
Louka
259190e478 Changed body proportions: Torso, Chest, Waist
Body proportions have been changed: - Torso length is now the base value, replacing waist length in earlier versions.
- Hip length is now waist distance. Waist distance is only used when using a hip tracker
- Chest distance/length is the same. It is only used when using a chest tracker
- Autobone support with any mix-n-match configuration :)
- Virtual Waist changed its name to "Hip offset". It still behaves the same.
2021-11-23 15:11:16 -05:00
Yury
24a0c3b136 Prevent path change when running as admin 2021-11-23 12:11:09 +03:00
Louka
f46f2bc913 Tries to get the waist first for the rotation node for Autobone
When you want the rotation node, it should give the node that has the rotation that affects the bone you want
2021-11-22 22:48:43 -05:00
Eiren Rain
77f048c48e Merge pull request #83 from Louka3000/hip-tracker
Hip tracker support
2021-11-23 04:00:14 +03:00
Louka
4055d51758 Autobone support
Added basic autobone support for the hip tracker, fixed "allTracekrs" and changed initial values of upper body.
2021-11-21 18:14:19 -05:00
Louka
21eff5e1ba Better reset and initial values
owo
2021-11-20 21:32:47 -05:00
Louka
34174b442f Virtual waist affected by kneebuf 2021-11-20 20:49:32 -05:00
Louka
77d37ab2a7 Cleanup and support for hip tracker alone/
why would you want hip alone instead of waist? idk
2021-11-20 16:35:22 -05:00
Louka
350fdbce9d Can use chest tracker alone
hip tracker will default to the waist tracker's position (waist or chest) if no hip tracker is found
2021-11-20 15:03:33 -05:00
Louka
e19cec4d3e Initial commit
added hip tracking support.
Independent from waist.
Has a hip length value going from waist to hip.
Legs depend on hip instead of waist
should work normally without hip tracker.
Should work just fine with a virtual waist offset.
2021-11-20 00:04:33 -05:00
Eiren Rain
e56d7665ed Merge pull request #81 from carl-anders/softer-keybinding-failure-mode
Keybinding: If JIntellitype fails to load, still allow server to run
2021-11-12 02:59:46 +02:00
Carl Andersson
a0e23bfbe9 Keybinding: Be even better at catching errors from JIntellitype 2021-11-12 00:35:41 +01:00
Carl Andersson
b7dc33f79e Keybinding: If JIntellitype fails to load, still allow server to run 2021-11-11 06:31:35 +01:00
Eiren Rain
d8c31eec81 Merge pull request #80 from deiteris/main
Display information message in case Java is not installed
2021-11-08 20:32:42 +02:00
Yury
e84ee760b1 Display information message in case Java is not installed 2021-11-08 21:25:26 +03:00
Eiren Rain
6ba1cc6bdb Merge pull request #79 from deiteris/main
Add forgotten imports
2021-11-08 17:22:31 +02:00
Yury
1b5e534592 Add forgotten imports 2021-11-08 18:01:43 +03:00
Eiren Rain
1a3e21007b Merge pull request #78 from deiteris/main
Prevent server from running if required ports are busy
2021-11-06 20:26:58 +02:00
Yury
55e11ffb5c Prevent server from running if required ports are busy 2021-11-06 21:22:24 +03:00
100 changed files with 3887 additions and 2163 deletions

View File

@@ -10,12 +10,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v2.1.0
uses: actions/setup-java@v2.4.0
with:
java-version: '11'
distribution: 'adopt'
@@ -31,12 +31,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v2.1.0
uses: actions/setup-java@v2.4.0
with:
java-version: '11'
distribution: 'adopt'

View File

@@ -15,7 +15,7 @@ Integrations:
It's recommended to download installer from here: https://github.com/SlimeVR/SlimeVR-Installer/releases/latest/download/slimevr_web_installer.exe
Latest instructions are [on our site](https://docs.slimevr.dev/slimevr-setup.html).
Latest instructions are [on our site](https://docs.slimevr.dev/server-setup/slimevr-setup.html).
## How to build
@@ -45,3 +45,7 @@ run gradle command `shadowJar` to build a runnable server JAR
* You must provide a copy of the original license (see LICENSE file)
* You don't have to release your own software under MIT License or even open source at all, but you have to state that it's based on SlimeVR
* This applies even if you distribute software without the source code
## Contributions
By contributing to this project you are placing all your code under MIT or less restricting licenses, and you certify that the code you have used is compatible with those licenses or is authored by you. If you're doing so on your work time, you certify that your employer is okay with this.

View File

@@ -75,5 +75,5 @@ shadowJar {
archiveVersion.set('')
}
application {
mainClassName = 'io.eiren.vr.Main'
mainClassName = 'dev.slimevr.Main'
}

View File

@@ -1,5 +1,5 @@
@echo off
echo Installing firewall rules...
echo Uninstalling firewall rules...
rem Discovery defauly port
netsh advfirewall firewall delete rule name="SlimeVR UDP 35903 incoming"

View File

@@ -1 +1,19 @@
@java -Xmx512M -jar slimevr.jar
@echo off
setlocal enableextensions
cd /d "%~dp0"
where java >nul 2>&1
if %errorlevel% EQU 0 (
java -Xmx512M -jar slimevr.jar
) else (
echo Java was not found in your system.
echo.
echo Either use SlimeVR Installer to install the server by following this link:
echo https://github.com/SlimeVR/SlimeVR-Installer/releases/latest/download/slimevr_web_installer.exe
echo.
echo Or download Java 11 by following this link:
echo https://adoptium.net/releases.html?variant=openjdk11^&jvmVariant=hotspot
)
if %errorlevel% NEQ 0 (
pause
)

View File

@@ -1,18 +1,21 @@
package io.eiren.vr;
package dev.slimevr;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import javax.swing.JOptionPane;
import org.apache.commons.lang3.JavaVersion;
import org.apache.commons.lang3.SystemUtils;
import dev.slimevr.gui.Keybinding;
import dev.slimevr.gui.VRServerGUI;
import io.eiren.util.logging.LogManager;
public class Main {
public static String VERSION = "0.1.1";
public static String VERSION = "0.1.4";
public static VRServer vrServer;
@@ -34,6 +37,16 @@ public class Main {
return;
}
try {
new ServerSocket(6969).close();
new ServerSocket(35903).close();
new ServerSocket(21110).close();
} catch (IOException e) {
LogManager.log.severe("SlimeVR start-up error! Required ports are busy. Make sure there is no other instance of SlimeVR Server running.");
JOptionPane.showMessageDialog(null, "SlimeVR start-up error! Required ports are busy. Make sure there is no other instance of SlimeVR Server running.", "SlimeVR: Ports are busy", JOptionPane.ERROR_MESSAGE);
return;
}
try {
vrServer = new VRServer();
vrServer.start();

View File

@@ -0,0 +1,9 @@
package dev.slimevr;
public enum NetworkProtocol {
OWO_LEGACY,
SLIMEVR_RAW,
SLIMEVR_FLATBUFFER,
SLIMEVR_WEBSOCKET;
}

View File

@@ -1,4 +1,4 @@
package io.eiren.vr;
package dev.slimevr;
import java.io.File;
import java.io.FileInputStream;
@@ -20,21 +20,21 @@ import dev.slimevr.bridge.NamedPipeBridge;
import dev.slimevr.bridge.SteamVRPipeInputBridge;
import dev.slimevr.bridge.VMCBridge;
import dev.slimevr.bridge.WebSocketVRBridge;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.processor.HumanPoseProcessor;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerConfig;
import dev.slimevr.vr.trackers.udp.TrackersUDPServer;
import io.eiren.util.OperatingSystem;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.ann.ThreadSecure;
import io.eiren.util.ann.VRServerThread;
import io.eiren.util.collections.FastList;
import io.eiren.vr.processor.HumanPoseProcessor;
import io.eiren.vr.processor.HumanSkeleton;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.TrackersUDPServer;
import io.eiren.yaml.YamlException;
import io.eiren.yaml.YamlFile;
import io.eiren.yaml.YamlNode;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerConfig;
public class VRServer extends Thread {

View File

@@ -1,24 +1,29 @@
package dev.slimevr.autobone;
import java.util.HashMap;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import dev.slimevr.VRServer;
import dev.slimevr.poserecorder.PoseFrameSkeleton;
import dev.slimevr.poserecorder.PoseFrameTracker;
import dev.slimevr.poserecorder.PoseFrames;
import dev.slimevr.poserecorder.TrackerFrame;
import dev.slimevr.poserecorder.TrackerFrameData;
import io.eiren.util.ann.ThreadSafe;
import dev.slimevr.vr.processor.HumanPoseProcessor;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
import dev.slimevr.vr.processor.skeleton.SkeletonConfig;
import dev.slimevr.vr.processor.skeleton.SkeletonConfigValue;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerRole;
import dev.slimevr.vr.trackers.TrackerUtils;
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.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerUtils;
public class AutoBone {
@@ -49,12 +54,17 @@ public class AutoBone {
public float adjustRateDecay = 1.01f;
public float slideErrorFactor = 1.0f;
public float offsetSlideErrorFactor = 0.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;
// TODO Needs much more work, probably going to rethink how the errors work to avoid this barely functional workaround @ButterscotchVanilla
// For scaling distances, since smaller sizes will cause smaller distances
//private float totalLengthBase = 2f;
// Human average is probably 1.1235 (SD 0.07)
public float legBodyRatio = 1.1235f;
// SD of 0.07, capture 68% within range
@@ -62,63 +72,79 @@ public class AutoBone {
// Assume these to be approximately half
public float kneeLegRatio = 0.5f;
public float chestWaistRatio = 0.5f;
public float chestTorsoRatio = 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 EnumMap<SkeletonConfigValue, Float> configs = new EnumMap<SkeletonConfigValue, Float>(SkeletonConfigValue.class);
public final EnumMap<SkeletonConfigValue, Float> staticConfigs = new EnumMap<SkeletonConfigValue, Float>(SkeletonConfigValue.class);
public final FastList<String> heightConfigs = new FastList<String>(new String[]{"Neck", "Waist", "Legs length"
});
public final FastList<SkeletonConfigValue> heightConfigs = new FastList<SkeletonConfigValue>(new SkeletonConfigValue[]{
SkeletonConfigValue.NECK, SkeletonConfigValue.TORSO, SkeletonConfigValue.LEGS_LENGTH});
public final FastList<SkeletonConfigValue> lengthConfigs = new FastList<SkeletonConfigValue>(new SkeletonConfigValue[]{
SkeletonConfigValue.HEAD, SkeletonConfigValue.NECK, SkeletonConfigValue.TORSO, SkeletonConfigValue.HIPS_WIDTH, SkeletonConfigValue.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, TrackerPosition.CHEST) != null) || TrackerUtils.findTrackerForBodyPosition(server.getAllTrackers(), TrackerPosition.CHEST) != null) {
private float readFromConfig(SkeletonConfigValue configValue) {
return server.config.getFloat(configValue.configKey, configValue.defaultValue);
}
public void reloadConfigValues(List<PoseFrameTracker> trackers) {
// Load torso configs
staticConfigs.put(SkeletonConfigValue.HEAD, readFromConfig(SkeletonConfigValue.HEAD));
staticConfigs.put(SkeletonConfigValue.NECK, readFromConfig(SkeletonConfigValue.NECK));
configs.put(SkeletonConfigValue.TORSO, readFromConfig(SkeletonConfigValue.TORSO));
if(server.config.getBoolean("autobone.forceChestTracker", false) || (trackers != null && TrackerUtils.findTrackerForBodyPosition(trackers, TrackerPosition.CHEST) != null)) {
// If force enabled or has a chest tracker
configs.put("Chest", server.config.getFloat("body.chestDistance", 0.42f));
staticConfigs.remove(SkeletonConfigValue.CHEST);
configs.put(SkeletonConfigValue.CHEST, readFromConfig(SkeletonConfigValue.CHEST));
} else {
// Otherwise, make sure it's not used
configs.remove("Chest");
staticConfigs.put("Chest", server.config.getFloat("body.chestDistance", 0.42f));
configs.remove(SkeletonConfigValue.CHEST);
staticConfigs.put(SkeletonConfigValue.CHEST, readFromConfig(SkeletonConfigValue.CHEST));
}
if(server.config.getBoolean("autobone.forceHipTracker", false) || (trackers != null && TrackerUtils.findTrackerForBodyPosition(trackers, TrackerPosition.HIP) != null && TrackerUtils.findTrackerForBodyPosition(trackers, TrackerPosition.WAIST) != null)) {
// If force enabled or has a hip tracker and waist tracker
staticConfigs.remove(SkeletonConfigValue.WAIST);
configs.put(SkeletonConfigValue.WAIST, readFromConfig(SkeletonConfigValue.WAIST));
} else {
// Otherwise, make sure it's not used
configs.remove(SkeletonConfigValue.WAIST);
staticConfigs.put(SkeletonConfigValue.WAIST, readFromConfig(SkeletonConfigValue.WAIST));
}
// 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));
staticConfigs.put(SkeletonConfigValue.HIPS_WIDTH, readFromConfig(SkeletonConfigValue.HIPS_WIDTH));
configs.put(SkeletonConfigValue.LEGS_LENGTH, readFromConfig(SkeletonConfigValue.LEGS_LENGTH));
configs.put(SkeletonConfigValue.KNEE_HEIGHT, readFromConfig(SkeletonConfigValue.KNEE_HEIGHT));
// Keep "feet" at ankles
staticConfigs.put(SkeletonConfigValue.FOOT_LENGTH, 0f);
staticConfigs.put(SkeletonConfigValue.FOOT_OFFSET, 0f);
staticConfigs.put(SkeletonConfigValue.SKELETON_OFFSET, 0f);
}
@ThreadSafe
public void skeletonUpdated(HumanSkeleton newSkeleton) {
if(newSkeleton instanceof HumanSkeletonWithLegs) {
skeleton = (HumanSkeletonWithLegs) newSkeleton;
applyConfigToSkeleton(newSkeleton);
LogManager.log.info("[AutoBone] Received updated skeleton");
}
/**
* A simple utility method to get the {@link HumanSkeleton} from the {@link VRServer}
* @return The {@link HumanSkeleton} associated with the {@link VRServer}, or null if there is none available
* @see {@link VRServer}, {@link HumanSkeleton}
*/
private HumanSkeleton getSkeleton() {
HumanPoseProcessor humanPoseProcessor = server != null ? server.humanPoseProcessor : null;
return humanPoseProcessor != null ? humanPoseProcessor.getSkeleton() : null;
}
public void applyConfig() {
if(!applyConfigToSkeleton(skeleton)) {
if(!applyConfigToSkeleton(getSkeleton())) {
// Unable to apply to skeleton, save directly
saveConfigs();
}
@@ -129,40 +155,37 @@ public class AutoBone {
return false;
}
configs.forEach(skeleton::setSkeletonConfig);
SkeletonConfig skeletonConfig = skeleton.getSkeletonConfig();
skeletonConfig.setConfigs(configs, null);
skeletonConfig.saveToConfig(server.config);
server.saveConfig();
LogManager.log.info("[AutoBone] Configured skeleton bone lengths");
return true;
}
private void setConfig(String name, String path) {
Float value = configs.get(name);
private void setConfig(SkeletonConfigValue config) {
Float value = configs.get(config);
if(value != null) {
server.config.setProperty(path, value);
server.config.setProperty(config.configKey, 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");
for(SkeletonConfigValue config : SkeletonConfigValue.values) {
setConfig(config);
}
server.saveConfig();
}
public Float getConfig(String config) {
public Float getConfig(SkeletonConfigValue 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) {
public Float getConfig(SkeletonConfigValue config, Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigValue, Float> configsAlt) {
if(configs == null) {
throw new NullPointerException("Argument \"configs\" must not be null");
}
@@ -171,27 +194,46 @@ public class AutoBone {
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;
public float sumSelectConfigs(List<SkeletonConfigValue> selection, Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigValue, Float> configsAlt) {
float sum = 0f;
for(String heightConfig : heightConfigs) {
Float length = getConfig(heightConfig, configs, configsAlt);
for(SkeletonConfigValue config : selection) {
Float length = getConfig(config, configs, configsAlt);
if(length != null) {
height += length;
sum += length;
}
}
return height;
return sum;
}
public float getLengthSum(Map<String, Float> configs) {
public float sumSelectConfigs(List<SkeletonConfigValue> selection, SkeletonConfig skeletonConfig) {
float sum = 0f;
for(SkeletonConfigValue config : selection) {
sum += skeletonConfig.getConfig(config);
}
return sum;
}
public float getLengthSum(Map<SkeletonConfigValue, Float> configs) {
return getLengthSum(configs, null);
}
public float getLengthSum(Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigValue, Float> configsAlt) {
float length = 0f;
for(float boneLength : configs.values()) {
if(configsAlt != null) {
for(Entry<SkeletonConfigValue, Float> config : configsAlt.entrySet()) {
// If there isn't a duplicate config
if(!configs.containsKey(config.getKey())) {
length += config.getValue();
}
}
}
for(Float boneLength : configs.values()) {
length += boneLength;
}
@@ -232,21 +274,22 @@ public class AutoBone {
public float processFrames(PoseFrames 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()];
List<PoseFrameTracker> trackers = frames.getTrackers();
reloadConfigValues(trackers); // Reload configs and detect chest tracker from the first frame
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()];
final PoseFrameSkeleton skeleton1 = new PoseFrameSkeleton(trackers, null, configs, staticConfigs);
final PoseFrameSkeleton skeleton2 = new PoseFrameSkeleton(trackers, null, configs, staticConfigs);
// If target height isn't specified, auto-detect
if(targetHeight < 0f) {
// Get the current skeleton from the server
HumanSkeleton skeleton = getSkeleton();
if(skeleton != null) {
targetHeight = getHeight(skeleton.getSkeletonConfig());
// If there is a skeleton available, calculate the target height from its configs
targetHeight = sumSelectConfigs(heightConfigs, skeleton.getSkeletonConfig());
LogManager.log.warning("[AutoBone] Target height loaded from skeleton (Make sure you reset before running!): " + targetHeight);
} else {
// Otherwise if there is no skeleton available, attempt to get the max HMD height from the recording
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);
@@ -259,33 +302,38 @@ public class AutoBone {
}
}
// Epoch loop, each epoch is one full iteration over the full dataset
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;
float adjustRate = epoch >= 0 ? (initialAdjustRate / FastMath.pow(adjustRateDecay, epoch)) : 0f;
// Iterate over the frames using a cursor and an offset for comparing frames a certain number of frames apart
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);
int frameCursor2 = frameCursor + cursorOffset;
skeleton1.setSkeletonConfigs(configs);
skeleton2.setSkeletonConfigs(configs);
skeleton1.skeletonConfig.setConfigs(configs, null);
skeleton2.skeletonConfig.setConfigs(configs, null);
skeleton1.setPoseFromFrame(trackerBuffer1);
skeleton2.setPoseFromFrame(trackerBuffer2);
skeleton1.setCursor(frameCursor);
skeleton1.updatePose();
skeleton2.setCursor(frameCursor2);
skeleton2.updatePose();
float totalLength = getLengthSum(configs);
float curHeight = getHeight(configs, staticConfigs);
float errorDeriv = getErrorDeriv(trackerBuffer1, trackerBuffer2, skeleton1, skeleton2, targetHeight - curHeight);
float curHeight = sumSelectConfigs(heightConfigs, configs, staticConfigs);
//float scaleLength = sumSelectConfigs(lengthConfigs, configs, staticConfigs);
float errorDeriv = getErrorDeriv(frames, frameCursor, frameCursor2, skeleton1, skeleton2, targetHeight - curHeight, 1f);
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);
reloadConfigValues(trackers);
// Reset error sum values
sumError = 0f;
@@ -301,7 +349,12 @@ public class AutoBone {
float adjustVal = error * adjustRate;
for(Entry<String, Float> entry : configs.entrySet()) {
// If there is no adjustment whatsoever, skip this
if(adjustVal == 0f) {
continue;
}
for(Entry<SkeletonConfigValue, Float> entry : configs.entrySet()) {
// Skip adjustment if the epoch is before starting (for logging only)
if(epoch < 0) {
break;
@@ -311,6 +364,7 @@ public class AutoBone {
// Try positive and negative adjustments
boolean isHeightVar = heightConfigs.contains(entry.getKey());
//boolean isLengthVar = lengthConfigs.contains(entry.getKey());
float minError = errorDeriv;
float finalNewLength = -1f;
for(int i = 0; i < 2; i++) {
@@ -326,7 +380,8 @@ public class AutoBone {
updateSkeletonBoneLength(skeleton1, skeleton2, entry.getKey(), newLength);
float newHeight = isHeightVar ? curHeight + curAdjustVal : curHeight;
float newErrorDeriv = getErrorDeriv(trackerBuffer1, trackerBuffer2, skeleton1, skeleton2, targetHeight - newHeight);
//float newScaleLength = isLengthVar ? scaleLength + curAdjustVal : scaleLength;
float newErrorDeriv = getErrorDeriv(frames, frameCursor, frameCursor2, skeleton1, skeleton2, targetHeight - newHeight, 1f);
if(newErrorDeriv < minError) {
minError = newErrorDeriv;
@@ -353,53 +408,80 @@ public class AutoBone {
}
}
float finalHeight = getHeight(configs, staticConfigs);
float finalHeight = sumSelectConfigs(heightConfigs, configs, staticConfigs);
LogManager.log.info("[AutoBone] Target height: " + targetHeight + " New height: " + finalHeight);
return Math.abs(finalHeight - targetHeight);
return FastMath.abs(finalHeight - targetHeight);
}
// The change in position of the ankle over time
protected float getSlideErrorDeriv(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
float slideLeft = skeleton1.getNodePosition(TrackerPosition.LEFT_ANKLE).distance(skeleton2.getNodePosition(TrackerPosition.LEFT_ANKLE));
float slideRight = skeleton1.getNodePosition(TrackerPosition.RIGHT_ANKLE).distance(skeleton2.getNodePosition(TrackerPosition.RIGHT_ANKLE));
protected float getSlideErrorDeriv(PoseFrameSkeleton skeleton1, PoseFrameSkeleton skeleton2) {
float slideLeft = skeleton1.getComputedTracker(TrackerRole.LEFT_FOOT).position.distance(skeleton2.getComputedTracker(TrackerRole.LEFT_FOOT).position);
float slideRight = skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position.distance(skeleton2.getComputedTracker(TrackerRole.RIGHT_FOOT).position);
// 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 change in distance between both of the ankles over time
protected float getOffsetSlideErrorDeriv(PoseFrameSkeleton skeleton1, PoseFrameSkeleton skeleton2) {
Vector3f leftFoot1 = skeleton1.getComputedTracker(TrackerRole.LEFT_FOOT).position;
Vector3f rightFoot1 = skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position;
Vector3f leftFoot2 = skeleton2.getComputedTracker(TrackerRole.LEFT_FOOT).position;
Vector3f rightFoot2 = skeleton2.getComputedTracker(TrackerRole.RIGHT_FOOT).position;
float slideDist1 = leftFoot1.distance(rightFoot1);
float slideDist2 = leftFoot2.distance(rightFoot2);
float slideDist3 = leftFoot1.distance(rightFoot2);
float slideDist4 = leftFoot2.distance(rightFoot1);
float dist1 = FastMath.abs(slideDist1 - slideDist2);
float dist2 = FastMath.abs(slideDist3 - slideDist4);
float dist3 = FastMath.abs(slideDist1 - slideDist3);
float dist4 = FastMath.abs(slideDist1 - slideDist4);
float dist5 = FastMath.abs(slideDist2 - slideDist3);
float dist6 = FastMath.abs(slideDist2 - slideDist4);
// 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 offset between both feet at one instant and over time
protected float getOffsetErrorDeriv(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
float skeleton1Left = skeleton1.getNodePosition(TrackerPosition.LEFT_ANKLE).getY();
float skeleton1Right = skeleton1.getNodePosition(TrackerPosition.RIGHT_ANKLE).getY();
protected float getOffsetErrorDeriv(PoseFrameSkeleton skeleton1, PoseFrameSkeleton skeleton2) {
float leftFoot1 = skeleton1.getComputedTracker(TrackerRole.LEFT_FOOT).position.getY();
float rightFoot1 = skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position.getY();
float skeleton2Left = skeleton2.getNodePosition(TrackerPosition.LEFT_ANKLE).getY();
float skeleton2Right = skeleton2.getNodePosition(TrackerPosition.RIGHT_ANKLE).getY();
float leftFoot2 = skeleton2.getComputedTracker(TrackerRole.LEFT_FOOT).position.getY();
float rightFoot2 = skeleton2.getComputedTracker(TrackerRole.RIGHT_FOOT).position.getY();
float dist1 = Math.abs(skeleton1Left - skeleton1Right);
float dist2 = Math.abs(skeleton2Left - skeleton2Right);
float dist1 = FastMath.abs(leftFoot1 - rightFoot1);
float dist2 = FastMath.abs(leftFoot2 - rightFoot2);
float dist3 = Math.abs(skeleton1Left - skeleton2Right);
float dist4 = Math.abs(skeleton2Left - skeleton1Right);
float dist3 = FastMath.abs(leftFoot1 - rightFoot2);
float dist4 = FastMath.abs(leftFoot2 - rightFoot1);
float dist5 = Math.abs(skeleton1Left - skeleton2Left);
float dist6 = Math.abs(skeleton1Right - skeleton2Right);
float dist5 = FastMath.abs(leftFoot1 - leftFoot2);
float dist6 = FastMath.abs(rightFoot1 - rightFoot2);
// 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");
protected float getProportionErrorDeriv(SkeletonConfig skeleton) {
float neckLength = skeleton.getConfig(SkeletonConfigValue.NECK);
float chestLength = skeleton.getConfig(SkeletonConfigValue.CHEST);
float torsoLength = skeleton.getConfig(SkeletonConfigValue.TORSO);
float legsLength = skeleton.getConfig(SkeletonConfigValue.LEGS_LENGTH);
float kneeHeight = skeleton.getConfig(SkeletonConfigValue.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;
float chestTorso = FastMath.abs((chestLength / torsoLength) - chestTorsoRatio);
float legBody = FastMath.abs((legsLength / (torsoLength + neckLength)) - legBodyRatio);
float kneeLeg = FastMath.abs((kneeHeight / legsLength) - kneeLegRatio);
if(legBody <= legBodyRatioRange) {
legBody = 0f;
@@ -407,22 +489,26 @@ public class AutoBone {
legBody -= legBodyRatioRange;
}
return (chestWaist + legBody + kneeLeg) / 3f;
return (chestTorso + legBody + kneeLeg) / 3f;
}
// The distance of any points to the corresponding absolute position
protected float getPositionErrorDeriv(TrackerFrame[] frame, SimpleSkeleton skeleton) {
protected float getPositionErrorDeriv(PoseFrames frames, int cursor, PoseFrameSkeleton skeleton) {
float offset = 0f;
int offsetCount = 0;
for(TrackerFrame trackerFrame : frame) {
List<PoseFrameTracker> trackers = frames.getTrackers();
for(int i = 0; i < trackers.size(); i++) {
PoseFrameTracker tracker = trackers.get(i);
TrackerFrame trackerFrame = tracker.safeGetFrame(cursor);
if(trackerFrame == null || !trackerFrame.hasData(TrackerFrameData.POSITION)) {
continue;
}
Vector3f nodePos = skeleton.getNodePosition(trackerFrame.designation.designation);
Vector3f nodePos = skeleton.getComputedTracker(trackerFrame.designation.trackerRole).position;
if(nodePos != null) {
offset += Math.abs(nodePos.distance(trackerFrame.position));
offset += FastMath.abs(nodePos.distance(trackerFrame.position));
offsetCount++;
}
}
@@ -431,76 +517,91 @@ public class AutoBone {
}
// The difference between offset of absolute position and the corresponding point over time
protected float getPositionOffsetErrorDeriv(TrackerFrame[] frame1, TrackerFrame[] frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
protected float getPositionOffsetErrorDeriv(PoseFrames frames, int cursor1, int cursor2, PoseFrameSkeleton skeleton1, PoseFrameSkeleton skeleton2) {
float offset = 0f;
int offsetCount = 0;
for(TrackerFrame trackerFrame1 : frame1) {
List<PoseFrameTracker> trackers = frames.getTrackers();
for(int i = 0; i < trackers.size(); i++) {
PoseFrameTracker tracker = trackers.get(i);
TrackerFrame trackerFrame1 = tracker.safeGetFrame(cursor1);
if(trackerFrame1 == null || !trackerFrame1.hasData(TrackerFrameData.POSITION)) {
continue;
}
TrackerFrame trackerFrame2 = TrackerUtils.findTrackerForBodyPosition(frame2, trackerFrame1.designation);
TrackerFrame trackerFrame2 = tracker.safeGetFrame(cursor2);
if(trackerFrame2 == null || !trackerFrame2.hasData(TrackerFrameData.POSITION)) {
continue;
}
Vector3f nodePos1 = skeleton1.getNodePosition(trackerFrame1.designation);
Vector3f nodePos1 = skeleton1.getComputedTracker(trackerFrame1.designation.trackerRole).position;
if(nodePos1 == null) {
continue;
}
Vector3f nodePos2 = skeleton2.getNodePosition(trackerFrame2.designation);
Vector3f nodePos2 = skeleton2.getComputedTracker(trackerFrame2.designation.trackerRole).position;
if(nodePos2 == null) {
continue;
}
float dist1 = Math.abs(nodePos1.distance(trackerFrame1.position));
float dist2 = Math.abs(nodePos2.distance(trackerFrame2.position));
float dist1 = FastMath.abs(nodePos1.distance(trackerFrame1.position));
float dist2 = FastMath.abs(nodePos2.distance(trackerFrame2.position));
offset += Math.abs(dist2 - dist1);
offset += FastMath.abs(dist2 - dist1);
offsetCount++;
}
return offsetCount > 0 ? offset / offsetCount : 0f;
}
protected float getErrorDeriv(TrackerFrame[] frame1, TrackerFrame[] frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2, float heightChange) {
protected float getErrorDeriv(PoseFrames frames, int cursor1, int cursor2, PoseFrameSkeleton skeleton1, PoseFrameSkeleton skeleton2, float heightChange, float distScale) {
float totalError = 0f;
float sumWeight = 0f;
if(slideErrorFactor > 0f) {
totalError += getSlideErrorDeriv(skeleton1, skeleton2) * slideErrorFactor;
// This is the main error function, this calculates the distance between the foot positions on both frames
totalError += getSlideErrorDeriv(skeleton1, skeleton2) * distScale * slideErrorFactor;
sumWeight += slideErrorFactor;
}
if(offsetSlideErrorFactor > 0f) {
// This error function compares the distance between the feet on each frame and returns the offset between them
totalError += getOffsetSlideErrorDeriv(skeleton1, skeleton2) * distScale * offsetSlideErrorFactor;
sumWeight += offsetSlideErrorFactor;
}
if(offsetErrorFactor > 0f) {
totalError += getOffsetErrorDeriv(skeleton1, skeleton2) * offsetErrorFactor;
// This error function compares the height of each foot in each frame
totalError += getOffsetErrorDeriv(skeleton1, skeleton2) * distScale * offsetErrorFactor;
sumWeight += offsetErrorFactor;
}
if(proportionErrorFactor > 0f) {
// This error function compares the current values to general expected proportions to keep measurements in line
// Either skeleton will work fine, skeleton1 is used as a default
totalError += getProportionErrorDeriv(skeleton1) * proportionErrorFactor;
totalError += getProportionErrorDeriv(skeleton1.skeletonConfig) * proportionErrorFactor;
sumWeight += proportionErrorFactor;
}
if(heightErrorFactor > 0f) {
totalError += Math.abs(heightChange) * heightErrorFactor;
// This error function compares the height change to the actual measured height of the headset
totalError += FastMath.abs(heightChange) * heightErrorFactor;
sumWeight += heightErrorFactor;
}
if(positionErrorFactor > 0f) {
totalError += (getPositionErrorDeriv(frame1, skeleton1) + getPositionErrorDeriv(frame2, skeleton2) / 2f) * positionErrorFactor;
// This error function compares the position of an assigned tracker with the position on the skeleton
totalError += (getPositionErrorDeriv(frames, cursor1, skeleton1) + getPositionErrorDeriv(frames, cursor2, skeleton2) / 2f) * distScale * positionErrorFactor;
sumWeight += positionErrorFactor;
}
if(positionOffsetErrorFactor > 0f) {
totalError += getPositionOffsetErrorDeriv(frame1, frame2, skeleton1, skeleton2) * positionOffsetErrorFactor;
// This error function compares the offset of the position of an assigned tracker with the position on the skeleton
totalError += getPositionOffsetErrorDeriv(frames, cursor1, cursor2, skeleton1, skeleton2) * distScale * positionOffsetErrorFactor;
sumWeight += positionOffsetErrorFactor;
}
// Minimize sliding, minimize foot height offset, minimize change in total height
return sumWeight > 0f ? totalError / sumWeight : 0f;
}
@@ -509,8 +610,11 @@ public class AutoBone {
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);
protected void updateSkeletonBoneLength(PoseFrameSkeleton skeleton1, PoseFrameSkeleton skeleton2, SkeletonConfigValue config, float newLength) {
skeleton1.skeletonConfig.setConfig(config, newLength);
skeleton1.updatePoseAffectedByConfig(config);
skeleton2.skeletonConfig.setConfig(config, newLength);
skeleton2.updatePoseAffectedByConfig(config);
}
}

View File

@@ -1,352 +0,0 @@
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.TransformNode;
import io.eiren.vr.trackers.TrackerPosition;
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, TrackerPosition.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, TrackerPosition.CHEST, TrackerPosition.WAIST);
setRotation(chest, neckNode);
TrackerFrame waist = TrackerUtils.findTrackerForBodyPosition(frame, TrackerPosition.WAIST, TrackerPosition.CHEST);
setRotation(waist, chestNode);
TrackerFrame leftLeg = TrackerUtils.findTrackerForBodyPosition(frame, TrackerPosition.LEFT_LEG);
TrackerFrame rightLeg = TrackerUtils.findTrackerForBodyPosition(frame, TrackerPosition.RIGHT_LEG);
averagePelvis(waist, leftLeg, rightLeg);
setRotation(leftLeg, leftHipNode);
setRotation(rightLeg, rightHipNode);
TrackerFrame leftAnkle = TrackerUtils.findTrackerForBodyPosition(frame, TrackerPosition.LEFT_ANKLE);
setRotation(leftAnkle, rightKneeNode);
TrackerFrame rightAnkle = TrackerUtils.findTrackerForBodyPosition(frame, TrackerPosition.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(TrackerPosition bodyPosition) {
return getNode(bodyPosition, false);
}
public TransformNode getNode(TrackerPosition 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(TrackerPosition 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

@@ -1,7 +1,7 @@
package dev.slimevr.bridge;
import io.eiren.util.ann.VRServerThread;
import io.eiren.vr.trackers.ShareableTracker;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.trackers.ShareableTracker;
/**
* Bridge handles sending and recieving tracker data

View File

@@ -9,17 +9,17 @@ import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.ptr.IntByReference;
import dev.slimevr.Main;
import dev.slimevr.bridge.Pipe.PipeState;
import dev.slimevr.bridge.ProtobufMessages.ProtobufMessage;
import dev.slimevr.bridge.ProtobufMessages.TrackerAdded;
import io.eiren.util.ann.VRServerThread;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerRole;
import dev.slimevr.vr.trackers.VRTracker;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.Main;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerRole;
import io.eiren.vr.trackers.VRTracker;
public class NamedPipeBridge extends ProtobufBridge<VRTracker> implements Runnable {

View File

@@ -12,15 +12,15 @@ import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import dev.slimevr.VRServer;
import dev.slimevr.bridge.Pipe.PipeState;
import dev.slimevr.vr.trackers.ComputedTracker;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerStatus;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerStatus;
public class NamedPipeVRBridge extends Thread implements Bridge {
@@ -43,7 +43,7 @@ public class NamedPipeVRBridge extends Thread implements Bridge {
private final List<? extends Tracker> shareTrackers;
private final List<ComputedTracker> internalTrackers;
private final HMDTracker internalHMDTracker = new HMDTracker("itnernal://HMD");
private final HMDTracker internalHMDTracker = new HMDTracker("internal://HMD");
private final AtomicBoolean newHMDData = new AtomicBoolean(false);
public NamedPipeVRBridge(HMDTracker hmd, List<? extends Tracker> shareTrackers, VRServer server) {
@@ -149,7 +149,7 @@ public class NamedPipeVRBridge extends Thread implements Bridge {
private void executeHMDInput() throws IOException {
String[] split = commandBuilder.toString().split(" ");
if(split.length < 7) {
LogManager.log.severe("[VRBridge] Short HMD data recieved: " + commandBuilder.toString());
LogManager.log.severe("[VRBridge] Short HMD data received: " + commandBuilder.toString());
return;
}
try {

View File

@@ -1,7 +1,7 @@
package dev.slimevr.bridge;
import io.eiren.util.ann.VRServerThread;
import io.eiren.vr.trackers.ShareableTracker;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.trackers.ShareableTracker;
public class OpenVRNativeBridge implements Bridge {

View File

@@ -11,21 +11,21 @@ import java.util.concurrent.LinkedBlockingQueue;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.Main;
import dev.slimevr.bridge.ProtobufMessages.Position;
import dev.slimevr.bridge.ProtobufMessages.ProtobufMessage;
import dev.slimevr.bridge.ProtobufMessages.TrackerAdded;
import dev.slimevr.bridge.ProtobufMessages.TrackerStatus;
import dev.slimevr.bridge.ProtobufMessages.UserAction;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.trackers.ComputedTracker;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.TrackerRole;
import dev.slimevr.vr.trackers.VRTracker;
import io.eiren.util.ann.Synchronize;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.ann.VRServerThread;
import io.eiren.util.collections.FastList;
import io.eiren.vr.Main;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.TrackerRole;
import io.eiren.vr.trackers.VRTracker;
public abstract class ProtobufBridge<T extends VRTracker> implements Bridge {
@@ -189,7 +189,7 @@ public abstract class ProtobufBridge<T extends VRTracker> implements Bridge {
protected void trackerStatusRecieved(TrackerStatus trackerStatus) {
T tracker = getInternalRemoteTrackerById(trackerStatus.getTrackerId());
if(tracker != null) {
tracker.setStatus(io.eiren.vr.trackers.TrackerStatus.getById(trackerStatus.getStatusValue()));
tracker.setStatus(dev.slimevr.vr.trackers.TrackerStatus.getById(trackerStatus.getStatusValue()));
}
}
@@ -214,11 +214,11 @@ public abstract class ProtobufBridge<T extends VRTracker> implements Bridge {
synchronized(remoteTrackersByTrackerId) {
Iterator<Entry<Integer, T>> iterator = remoteTrackersByTrackerId.entrySet().iterator();
while(iterator.hasNext()) {
iterator.next().getValue().setStatus(io.eiren.vr.trackers.TrackerStatus.DISCONNECTED);
iterator.next().getValue().setStatus(dev.slimevr.vr.trackers.TrackerStatus.DISCONNECTED);
}
}
if(hmdTracker != null) {
hmd.setStatus(io.eiren.vr.trackers.TrackerStatus.DISCONNECTED);
hmd.setStatus(dev.slimevr.vr.trackers.TrackerStatus.DISCONNECTED);
}
}

View File

@@ -2506,7 +2506,7 @@ public final class ProtobufMessages {
}
private com.google.protobuf.MapField<java.lang.String, java.lang.String>
internalGetMutableActionArguments() {
onChanged();;
onChanged();
if (actionArguments_ == null) {
actionArguments_ = com.google.protobuf.MapField.newMapField(
ActionArgumentsDefaultEntryHolder.defaultEntry);
@@ -4632,7 +4632,7 @@ public final class ProtobufMessages {
}
private com.google.protobuf.MapField<java.lang.String, java.lang.String>
internalGetMutableExtra() {
onChanged();;
onChanged();
if (extra_ == null) {
extra_ = com.google.protobuf.MapField.newMapField(
ExtraDefaultEntryHolder.defaultEntry);
@@ -5099,7 +5099,7 @@ public final class ProtobufMessages {
public int getNumber() {
return this.value;
}
};
}
@Override
public MessageCase
@@ -5791,7 +5791,7 @@ public final class ProtobufMessages {
message_ = null;
}
messageCase_ = 1;
onChanged();;
onChanged();
return positionBuilder_;
}
@@ -5932,7 +5932,7 @@ public final class ProtobufMessages {
message_ = null;
}
messageCase_ = 2;
onChanged();;
onChanged();
return userActionBuilder_;
}
@@ -6073,7 +6073,7 @@ public final class ProtobufMessages {
message_ = null;
}
messageCase_ = 3;
onChanged();;
onChanged();
return trackerAddedBuilder_;
}
@@ -6214,7 +6214,7 @@ public final class ProtobufMessages {
message_ = null;
}
messageCase_ = 4;
onChanged();;
onChanged();
return trackerStatusBuilder_;
}
@java.lang.Override

View File

@@ -16,14 +16,14 @@ import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinError;
import com.sun.jna.ptr.IntByReference;
import dev.slimevr.VRServer;
import dev.slimevr.bridge.Pipe.PipeState;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerStatus;
import dev.slimevr.vr.trackers.VRTracker;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.VRTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerStatus;
public class SteamVRPipeInputBridge extends Thread implements Bridge {

View File

@@ -2,8 +2,8 @@ package dev.slimevr.bridge;
import java.net.InetAddress;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.Tracker;
public class VMCBridge extends Thread implements Bridge {

View File

@@ -17,15 +17,15 @@ import org.json.JSONObject;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.Main;
import dev.slimevr.VRServer;
import dev.slimevr.vr.trackers.ComputedTracker;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerStatus;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.Main;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerStatus;
public class WebSocketVRBridge extends WebSocketServer implements Bridge {

View File

@@ -20,5 +20,4 @@ public abstract class AbstractComponentListener implements ComponentListener {
@Override
public void componentHidden(ComponentEvent e) {
}
}
}

View File

@@ -32,4 +32,4 @@ public abstract class AbstractWindowListener implements WindowListener {
@Override
public void windowDeactivated(WindowEvent e) {
}
}
}

View File

@@ -10,24 +10,24 @@ 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.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.VRServer;
import dev.slimevr.autobone.AutoBone;
import dev.slimevr.gui.swing.EJBox;
import dev.slimevr.poserecorder.PoseFrames;
import dev.slimevr.poserecorder.PoseFrameIO;
import dev.slimevr.poserecorder.PoseRecorder;
import dev.slimevr.vr.processor.skeleton.SkeletonConfigValue;
public class AutoBoneWindow extends JFrame {
@@ -37,7 +37,7 @@ public class AutoBoneWindow extends JFrame {
private EJBox pane;
private final transient VRServer server;
private final transient SkeletonConfig skeletonConfig;
private final transient SkeletonConfigGUI skeletonConfig;
private final transient PoseRecorder poseRecorder;
private final transient AutoBone autoBone;
@@ -52,7 +52,7 @@ public class AutoBoneWindow extends JFrame {
private JLabel processLabel;
private JLabel lengthsLabel;
public AutoBoneWindow(VRServer server, SkeletonConfig skeletonConfig) {
public AutoBoneWindow(VRServer server, SkeletonConfigGUI skeletonConfig) {
super("Skeleton Auto-Configuration");
this.server = server;
@@ -67,17 +67,14 @@ public class AutoBoneWindow extends JFrame {
}
private String getLengthsString() {
boolean first = true;
StringBuilder configInfo = new StringBuilder("");
for(Entry<String, Float> entry : autoBone.configs.entrySet()) {
if(!first) {
final StringBuilder configInfo = new StringBuilder();
autoBone.configs.forEach((key, value) -> {
if(configInfo.length() > 0) {
configInfo.append(", ");
} else {
first = false;
}
configInfo.append(entry.getKey() + ": " + StringUtils.prettyNumber(entry.getValue() * 100f, 2));
}
configInfo.append(key.stringVal + ": " + StringUtils.prettyNumber(value * 100f, 2));
});
return configInfo.toString();
}
@@ -134,6 +131,7 @@ public class AutoBoneWindow extends JFrame {
autoBone.adjustRateDecay = server.config.getFloat("autobone.adjustRateDecay", autoBone.adjustRateDecay);
autoBone.slideErrorFactor = server.config.getFloat("autobone.slideErrorFactor", autoBone.slideErrorFactor);
autoBone.offsetSlideErrorFactor = server.config.getFloat("autobone.offsetSlideErrorFactor", autoBone.offsetSlideErrorFactor);
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);
@@ -339,21 +337,21 @@ public class AutoBoneWindow extends JFrame {
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 neckLength = autoBone.getConfig(SkeletonConfigValue.NECK);
Float chestDistance = autoBone.getConfig(SkeletonConfigValue.CHEST);
Float torsoLength = autoBone.getConfig(SkeletonConfigValue.TORSO);
Float hipWidth = autoBone.getConfig(SkeletonConfigValue.HIPS_WIDTH);
Float legsLength = autoBone.getConfig(SkeletonConfigValue.LEGS_LENGTH);
Float kneeHeight = autoBone.getConfig(SkeletonConfigValue.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 neckTorso = neckLength != null && torsoLength != null ? neckLength / torsoLength : 0f;
float chestTorso = chestDistance != null && torsoLength != null ? chestDistance / torsoLength : 0f;
float torsoWaist = hipWidth != null && torsoLength != null ? hipWidth / torsoLength : 0f;
float legTorso = legsLength != null && torsoLength != null ? legsLength / torsoLength : 0f;
float legBody = legsLength != null && torsoLength != null && neckLength != null ? legsLength / (torsoLength + 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) + "}]");
LogManager.log.info("[AutoBone] Ratios: [{Neck-Torso: " + StringUtils.prettyNumber(neckTorso) + "}, {Chest-Torso: " + StringUtils.prettyNumber(chestTorso) + "}, {Torso-Waist: " + StringUtils.prettyNumber(torsoWaist) + "}, {Leg-Torso: " + StringUtils.prettyNumber(legTorso) + "}, {Leg-Body: " + StringUtils.prettyNumber(legBody) + "}, {Knee-Leg: " + StringUtils.prettyNumber(kneeLeg) + "}]");
String lengthsString = getLengthsString();
LogManager.log.info("[AutoBone] Length values: " + lengthsString);

View File

@@ -13,9 +13,9 @@ import javax.swing.border.EmptyBorder;
import javax.swing.event.MouseInputAdapter;
import dev.slimevr.gui.swing.EJBox;
import dev.slimevr.vr.trackers.CalibratingTracker;
import dev.slimevr.vr.trackers.Tracker;
import io.eiren.util.ann.AWTThread;
import io.eiren.vr.trackers.CalibratingTracker;
import io.eiren.vr.trackers.Tracker;
public class CalibrationWindow extends JFrame {

View File

@@ -0,0 +1,64 @@
package dev.slimevr.gui;
import com.melloware.jintellitype.HotkeyListener;
import com.melloware.jintellitype.JIntellitype;
import dev.slimevr.VRServer;
import io.eiren.util.OperatingSystem;
import io.eiren.util.ann.AWTThread;
import io.eiren.util.logging.LogManager;
public class Keybinding implements HotkeyListener {
public final VRServer server;
private static final int RESET = 1;
private static final int QUICK_RESET = 2;
@AWTThread
public Keybinding(VRServer server) {
this.server = server;
if (OperatingSystem.getCurrentPlatform() != OperatingSystem.WINDOWS) {
LogManager.log.info("[Keybinding] Currently only supported on Windows. Keybindings will be disabled.");
return;
}
try {
if(JIntellitype.getInstance() instanceof JIntellitype) {
JIntellitype.getInstance().addHotKeyListener(this);
String resetBinding = this.server.config.getString("keybindings.reset");
if(resetBinding == null) {
resetBinding = "CTRL+ALT+SHIFT+Y";
this.server.config.setProperty("keybindings.reset", resetBinding);
}
JIntellitype.getInstance().registerHotKey(RESET, resetBinding);
LogManager.log.info("[Keybinding] Bound reset to " + resetBinding);
String quickResetBinding = this.server.config.getString("keybindings.quickReset");
if(quickResetBinding == null) {
quickResetBinding = "CTRL+ALT+SHIFT+U";
this.server.config.setProperty("keybindings.quickReset", quickResetBinding);
}
JIntellitype.getInstance().registerHotKey(QUICK_RESET, quickResetBinding);
LogManager.log.info("[Keybinding] Bound quick reset to " + quickResetBinding);
}
} catch(Throwable e) {
LogManager.log.info("[Keybinding] JIntellitype initialization failed. Keybindings will be disabled. Try restarting your computer.");
}
}
@AWTThread
@Override
public void onHotKey(int identifier) {
switch(identifier) {
case RESET:
LogManager.log.info("[Keybinding] Reset pressed");
server.resetTrackers();
break;
case QUICK_RESET:
LogManager.log.info("[Keybinding] Quick reset pressed");
server.resetTrackersYaw();
break;
}
}
}

View File

@@ -22,7 +22,7 @@ public class ScalableFont extends Font {
super(font);
if(font instanceof ScalableFont) {
ScalableFont sourceFont = (ScalableFont)font;
ScalableFont sourceFont = (ScalableFont) font;
this.initSize = sourceFont.getInitSize();
this.initPointSize = sourceFont.getInitSize2D();
@@ -39,7 +39,7 @@ public class ScalableFont extends Font {
super(font);
if(font instanceof ScalableFont) {
ScalableFont sourceFont = (ScalableFont)font;
ScalableFont sourceFont = (ScalableFont) font;
this.initSize = sourceFont.getInitSize();
this.initPointSize = sourceFont.getInitSize2D();
@@ -84,8 +84,7 @@ public class ScalableFont extends Font {
float newPointSize = initPointSize * scale;
this.size = (int)(newPointSize + 0.5);
this.size = (int) (newPointSize + 0.5);
this.pointSize = newPointSize;
}
}

View File

@@ -8,21 +8,22 @@ import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.event.MouseInputAdapter;
import dev.slimevr.VRServer;
import dev.slimevr.gui.swing.ButtonTimer;
import dev.slimevr.gui.swing.EJBagNoStretch;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
import dev.slimevr.vr.processor.skeleton.SkeletonConfigValue;
import io.eiren.util.StringUtils;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.HumanSkeleton;
public class SkeletonConfig extends EJBagNoStretch {
public class SkeletonConfigGUI extends EJBagNoStretch {
private final VRServer server;
private final VRServerGUI gui;
private final AutoBoneWindow autoBone;
private Map<String, SkeletonLabel> labels = new HashMap<>();
private Map<SkeletonConfigValue, SkeletonLabel> labels = new HashMap<>();
public SkeletonConfig(VRServer server, VRServerGUI gui) {
public SkeletonConfigGUI(VRServer server, VRServerGUI gui) {
super(false, true);
this.server = server;
this.gui = gui;
@@ -91,7 +92,7 @@ public class SkeletonConfig extends EJBagNoStretch {
row++;
//*/
add(new TimedResetButton("Reset All", "All"), s(c(1, row, 2), 3, 1));
add(new TimedResetButton("Reset All"), s(c(1, row, 2), 3, 1));
add(new JButton("Auto") {{
addMouseListener(new MouseInputAdapter() {
@Override
@@ -103,75 +104,25 @@ public class SkeletonConfig extends EJBagNoStretch {
}}, s(c(4, row, 2), 3, 1));
row++;
add(new JLabel("Chest"), c(0, row, 2));
add(new AdjButton("+", "Chest", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Chest"), c(2, row, 2));
add(new AdjButton("-", "Chest", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Chest"), c(4, row, 2));
row++;
for (SkeletonConfigValue config : SkeletonConfigValue.values) {
add(new JLabel(config.label), c(0, row, 2));
add(new AdjButton("+", config, 0.01f), c(1, row, 2));
add(new SkeletonLabel(config), c(2, row, 2));
add(new AdjButton("-", config, -0.01f), c(3, row, 2));
add(new JLabel("Waist"), c(0, row, 2));
add(new AdjButton("+", "Waist", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Waist"), c(2, row, 2));
add(new AdjButton("-", "Waist", -0.01f), c(3, row, 2));
add(new TimedResetButton("Reset", "Waist"), c(4, row, 2));
row++;
// Only use a timer on configs that need time to get into position for
switch (config) {
case TORSO:
case LEGS_LENGTH:
add(new TimedResetButton("Reset", config), c(4, row, 2));
break;
default:
add(new ResetButton("Reset", config), c(4, row, 2));
break;
}
add(new JLabel("Hips width"), c(0, row, 2));
add(new AdjButton("+", "Hips width", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Hips width"), c(2, row, 2));
add(new AdjButton("-", "Hips width", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Hips width"), c(4, row, 2));
row++;
add(new JLabel("Legs length"), c(0, row, 2));
add(new AdjButton("+", "Legs length", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Legs length"), c(2, row, 2));
add(new AdjButton("-", "Legs length", -0.01f), c(3, row, 2));
add(new TimedResetButton("Reset", "Legs length"), c(4, row, 2));
row++;
add(new JLabel("Knee height"), c(0, row, 2));
add(new AdjButton("+", "Knee height", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Knee height"), c(2, row, 2));
add(new AdjButton("-", "Knee height", -0.01f), c(3, row, 2));
add(new TimedResetButton("Reset", "Knee height"), c(4, row, 2));
row++;
add(new JLabel("Foot length"), c(0, row, 2));
add(new AdjButton("+", "Foot length", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Foot length"), c(2, row, 2));
add(new AdjButton("-", "Foot length", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Foot length"), c(4, row, 2));
row++;
add(new JLabel("Foot offset"), c(0, row, 2));
add(new AdjButton("+", "Foot offset", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Foot offset"), c(2, row, 2));
add(new AdjButton("-", "Foot offset", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Foot offset"), c(4, row, 2));
row++;
add(new JLabel("Head offset"), c(0, row, 2));
add(new AdjButton("+", "Head", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Head"), c(2, row, 2));
add(new AdjButton("-", "Head", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Head"), c(4, row, 2));
row++;
add(new JLabel("Neck length"), c(0, row, 2));
add(new AdjButton("+", "Neck", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Neck"), c(2, row, 2));
add(new AdjButton("-", "Neck", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Neck"), c(4, row, 2));
row++;
add(new JLabel("Virtual waist"), c(0, row, 2));
add(new AdjButton("+", "Virtual waist", 0.01f), c(1, row, 2));
add(new SkeletonLabel("Virtual waist"), c(2, row, 2));
add(new AdjButton("-", "Virtual waist", -0.01f), c(3, row, 2));
add(new ResetButton("Reset", "Virtual waist"), c(4, row, 2));
row++;
row++;
}
gui.refresh();
});
@@ -186,30 +137,41 @@ public class SkeletonConfig extends EJBagNoStretch {
});
}
private void change(String joint, float diff) {
private void change(SkeletonConfigValue joint, float diff) {
// Update config value
float current = server.humanPoseProcessor.getSkeletonConfig(joint);
server.humanPoseProcessor.setSkeletonConfig(joint, current + diff);
server.humanPoseProcessor.getSkeletonConfig().saveToConfig(server.config);
server.saveConfig();
// Update GUI
labels.get(joint).setText(StringUtils.prettyNumber((current + diff) * 100, 0));
}
private void reset(String joint) {
private void reset(SkeletonConfigValue joint) {
// Update config value
server.humanPoseProcessor.resetSkeletonConfig(joint);
server.humanPoseProcessor.getSkeletonConfig().saveToConfig(server.config);
server.saveConfig();
if(!"All".equals(joint)) {
float current = server.humanPoseProcessor.getSkeletonConfig(joint);
labels.get(joint).setText(StringUtils.prettyNumber((current) * 100, 0));
} else {
labels.forEach((jnt, label) -> {
float current = server.humanPoseProcessor.getSkeletonConfig(jnt);
label.setText(StringUtils.prettyNumber((current) * 100, 0));
});
}
// Update GUI
float current = server.humanPoseProcessor.getSkeletonConfig(joint);
labels.get(joint).setText(StringUtils.prettyNumber((current) * 100, 0));
}
private void resetAll() {
// Update config value
server.humanPoseProcessor.resetAllSkeletonConfigs();
server.humanPoseProcessor.getSkeletonConfig().saveToConfig(server.config);
server.saveConfig();
// Update GUI
refreshAll();
}
private class SkeletonLabel extends JLabel {
public SkeletonLabel(String joint) {
public SkeletonLabel(SkeletonConfigValue joint) {
super(StringUtils.prettyNumber(server.humanPoseProcessor.getSkeletonConfig(joint) * 100, 0));
labels.put(joint, this);
}
@@ -217,7 +179,7 @@ public class SkeletonConfig extends EJBagNoStretch {
private class AdjButton extends JButton {
public AdjButton(String text, String joint, float diff) {
public AdjButton(String text, SkeletonConfigValue joint, float diff) {
super(text);
addMouseListener(new MouseInputAdapter() {
@Override
@@ -230,7 +192,7 @@ public class SkeletonConfig extends EJBagNoStretch {
private class ResetButton extends JButton {
public ResetButton(String text, String joint) {
public ResetButton(String text, SkeletonConfigValue joint) {
super(text);
addMouseListener(new MouseInputAdapter() {
@Override
@@ -243,7 +205,7 @@ public class SkeletonConfig extends EJBagNoStretch {
private class TimedResetButton extends JButton {
public TimedResetButton(String text, String joint) {
public TimedResetButton(String text, SkeletonConfigValue joint) {
super(text);
addMouseListener(new MouseInputAdapter() {
@Override
@@ -252,5 +214,15 @@ public class SkeletonConfig extends EJBagNoStretch {
}
});
}
public TimedResetButton(String text) {
super(text);
addMouseListener(new MouseInputAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
ButtonTimer.runTimer(TimedResetButton.this, 3, text, () -> resetAll());
}
});
}
}
}

View File

@@ -9,14 +9,14 @@ import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.VRServer;
import dev.slimevr.gui.swing.EJBagNoStretch;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.processor.TransformNode;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
import io.eiren.util.StringUtils;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.ann.VRServerThread;
import io.eiren.util.collections.FastList;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.HumanSkeleton;
import io.eiren.vr.processor.TransformNode;
public class SkeletonList extends EJBagNoStretch {

View File

@@ -17,23 +17,23 @@ import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.VRServer;
import dev.slimevr.gui.swing.EJBagNoStretch;
import dev.slimevr.gui.swing.EJBoxNoStretch;
import dev.slimevr.vr.trackers.ComputedTracker;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.IMUTracker;
import dev.slimevr.vr.trackers.ReferenceAdjustedTracker;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerConfig;
import dev.slimevr.vr.trackers.TrackerMountingRotation;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerWithBattery;
import dev.slimevr.vr.trackers.TrackerWithTPS;
import io.eiren.util.StringUtils;
import io.eiren.util.ann.AWTThread;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.collections.FastList;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.ReferenceAdjustedTracker;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.IMUTracker;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerConfig;
import io.eiren.vr.trackers.TrackerMountingRotation;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerWithBattery;
import io.eiren.vr.trackers.TrackerWithTPS;
public class TrackersList extends EJBoxNoStretch {
@@ -48,6 +48,7 @@ public class TrackersList extends EJBoxNoStretch {
private final VRServer server;
private final VRServerGUI gui;
private long lastUpdate = 0;
private boolean debug = false;
public TrackersList(VRServer server, VRServerGUI gui) {
super(BoxLayout.PAGE_AXIS, false, true);
@@ -59,6 +60,12 @@ public class TrackersList extends EJBoxNoStretch {
server.addNewTrackerConsumer(this::newTrackerAdded);
}
@AWTThread
public void setDebug(boolean debug) {
this.debug = debug;
build();
}
@AWTThread
private void build() {
removeAll();
@@ -142,7 +149,12 @@ public class TrackersList extends EJBoxNoStretch {
JLabel magAccuracy;
JLabel adj;
JLabel adjYaw;
JLabel adjGyro;
JLabel correction;
JLabel signalStrength;
JLabel rotQuat;
JLabel rotAdj;
JLabel temperature;
@AWTThread
public TrackerPanel(Tracker t) {
@@ -216,6 +228,7 @@ public class TrackersList extends EJBoxNoStretch {
add(new JLabel("TPS"), c(3, row, 2, GridBagConstraints.FIRST_LINE_START));
if(realTracker instanceof IMUTracker) {
add(new JLabel("Ping"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Signal"), c(4, row, 2, GridBagConstraints.FIRST_LINE_START));
}
row++;
if(t.hasRotation())
@@ -224,6 +237,7 @@ public class TrackersList extends EJBoxNoStretch {
add(position = new JLabel("0 0 0"), c(1, row, 2, GridBagConstraints.FIRST_LINE_START));
if(realTracker instanceof IMUTracker) {
add(ping = new JLabel(""), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(signalStrength = new JLabel(""), c(4, row, 2, GridBagConstraints.FIRST_LINE_START));
}
if(realTracker instanceof TrackerWithTPS) {
add(tps = new JLabel("0"), c(3, row, 2, GridBagConstraints.FIRST_LINE_START));
@@ -240,11 +254,18 @@ public class TrackersList extends EJBoxNoStretch {
row++;
add(new JLabel("Raw:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
add(raw = new JLabel("0 0 0"), s(c(1, row, 2, GridBagConstraints.FIRST_LINE_START), 3, 1));
if(debug && realTracker instanceof IMUTracker) {
add(new JLabel("Quat:"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(rotQuat = new JLabel("0"), c(3, row, 2, GridBagConstraints.FIRST_LINE_START));
}
row++;
/*
if(realTracker instanceof IMUTracker) {
if(debug && realTracker instanceof IMUTracker) {
add(new JLabel("Raw mag:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
add(rawMag = new JLabel("0 0 0"), s(c(1, row, 2, GridBagConstraints.FIRST_LINE_START), 3, 1));
add(new JLabel("Gyro fix:"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(new JLabel(String.format("0x%8x", realTracker.hashCode())), s(c(3, row, 2, GridBagConstraints.FIRST_LINE_START), 3, 1));
row++;
add(new JLabel("Cal:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
add(calibration = new JLabel("0"), c(1, row, 2, GridBagConstraints.FIRST_LINE_START));
@@ -253,18 +274,22 @@ public class TrackersList extends EJBoxNoStretch {
row++;
add(new JLabel("Correction:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
add(correction = new JLabel("0 0 0"), s(c(1, row, 2, GridBagConstraints.FIRST_LINE_START), 3, 1));
add(new JLabel("RotAdj:"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(rotAdj = new JLabel("0"), c(3, row, 2, GridBagConstraints.FIRST_LINE_START));
row++;
}
//*/
/*
if(t instanceof ReferenceAdjustedTracker) {
add(new JLabel("Adj:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
if(debug && t instanceof ReferenceAdjustedTracker) {
add(new JLabel("Att fix:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
add(adj = new JLabel("0 0 0 0"), c(1, row, 2, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("AdjY:"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Yaw Fix:"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(adjYaw = new JLabel("0 0 0 0"), c(3, row, 2, GridBagConstraints.FIRST_LINE_START));
row++;
add(new JLabel("Gyro Fix:"), c(0, row, 2, GridBagConstraints.FIRST_LINE_START));
add(adjGyro = new JLabel("0 0 0 0"), c(1, row, 2, GridBagConstraints.FIRST_LINE_START));
add(new JLabel("Temp:"), c(2, row, 2, GridBagConstraints.FIRST_LINE_START));
add(temperature = new JLabel("?"), c(3, row, 2, GridBagConstraints.FIRST_LINE_START));
}
//*/
setBorder(BorderFactory.createLineBorder(new Color(0x663399), 2, false));
TrackersList.this.add(this);
@@ -297,22 +322,42 @@ public class TrackersList extends EJBoxNoStretch {
tps.setText(StringUtils.prettyNumber(((TrackerWithTPS) realTracker).getTPS(), 1));
}
if(realTracker instanceof TrackerWithBattery)
bat.setText(StringUtils.prettyNumber(((TrackerWithBattery) realTracker).getBatteryVoltage(), 1));
bat.setText(String.format("%d%% (%sV)", Math.round(((TrackerWithBattery) realTracker).getBatteryLevel()), StringUtils.prettyNumber(((TrackerWithBattery) realTracker).getBatteryVoltage(), 2)));
if(t instanceof ReferenceAdjustedTracker) {
((ReferenceAdjustedTracker<Tracker>) t).attachmentFix.toAngles(angles);
if(adj != null)
ReferenceAdjustedTracker<Tracker> rat = (ReferenceAdjustedTracker<Tracker>) t;
if(adj != null) {
rat.attachmentFix.toAngles(angles);
adj.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
((ReferenceAdjustedTracker<Tracker>) t).yawFix.toAngles(angles);
if(adjYaw != null)
}
if(adjYaw != null) {
rat.yawFix.toAngles(angles);
adjYaw.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
}
if(adjGyro != null) {
rat.gyroFix.toAngles(angles);
adjGyro.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
}
}
if(realTracker instanceof IMUTracker) {
if(ping != null)
ping.setText(String.valueOf(((IMUTracker) realTracker).ping));
if(signalStrength != null) {
int signal = ((IMUTracker) realTracker).signalStrength;
if (signal == -1) {
signalStrength.setText("N/A");
} else {
// -40 dBm is excellent, -95 dBm is very poor
int percentage = (signal - -95) * (100 - 0) / (-40 - -95) + 0;
percentage = Math.max(Math.min(percentage, 100), 0);
signalStrength.setText(String.valueOf(percentage) + "% " + "(" + String.valueOf(signal) + " dBm" + ")");
}
}
}
realTracker.getRotation(q);
q.toAngles(angles);
@@ -320,21 +365,44 @@ public class TrackersList extends EJBoxNoStretch {
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
if(realTracker instanceof IMUTracker) {
((IMUTracker) realTracker).rotMagQuaternion.toAngles(angles);
if(rawMag != null)
IMUTracker imu = (IMUTracker) realTracker;
if(rawMag != null) {
imu.rotMagQuaternion.toAngles(angles);
rawMag.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
}
if(calibration != null)
calibration.setText(((IMUTracker) realTracker).calibrationStatus + " / " + ((IMUTracker) realTracker).magCalibrationStatus);
calibration.setText(imu.calibrationStatus + " / " + imu.magCalibrationStatus);
if(magAccuracy != null)
magAccuracy.setText(StringUtils.prettyNumber(((IMUTracker) realTracker).magnetometerAccuracy * FastMath.RAD_TO_DEG, 1) + "°");
((IMUTracker) realTracker).getCorrection(q);
q.toAngles(angles);
if(correction != null)
magAccuracy.setText(StringUtils.prettyNumber(imu.magnetometerAccuracy * FastMath.RAD_TO_DEG, 1) + "°");
if(correction != null) {
imu.getCorrection(q);
q.toAngles(angles);
correction.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
}
if(rotQuat != null) {
imu.rotQuaternion.toAngles(angles);
rotQuat.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(rotAdj != null) {
imu.rotAdjust.toAngles(angles);
rotAdj.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(temperature != null) {
if(imu.temperature == 0.0f) {
// Can't be exact 0, so no info received
temperature.setText("?");
} else {
temperature.setText(StringUtils.prettyNumber(imu.temperature, 1) + "∘C");
}
}
}
}
}

View File

@@ -5,18 +5,18 @@ import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.MouseInputAdapter;
import dev.slimevr.Main;
import dev.slimevr.VRServer;
import dev.slimevr.bridge.NamedPipeBridge;
import dev.slimevr.gui.swing.ButtonTimer;
import dev.slimevr.gui.swing.EJBagNoStretch;
import dev.slimevr.gui.swing.EJBox;
import dev.slimevr.gui.swing.EJBoxNoStretch;
import dev.slimevr.vr.trackers.TrackerRole;
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;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.TrackerRole;
import java.awt.Component;
import java.awt.Container;
@@ -198,11 +198,22 @@ public class VRServerGUI extends JFrame {
add(new EJBoxNoStretch(PAGE_AXIS, false, true) {{
setAlignmentY(TOP_ALIGNMENT);
JCheckBox debugCb;
add(debugCb = new JCheckBox("Show debug information"));
debugCb.setSelected(false);
debugCb.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
trackersList.setDebug(debugCb.isSelected());
}
});
JLabel l;
add(l = new JLabel("Body proportions"));
l.setFont(l.getFont().deriveFont(Font.BOLD));
l.setAlignmentX(0.5f);
add(new SkeletonConfig(server, VRServerGUI.this));
add(new SkeletonConfigGUI(server, VRServerGUI.this));
add(Box.createVerticalStrut(10));
if(server.hasBridge(NamedPipeBridge.class)) {
NamedPipeBridge br = server.getVRBridge(NamedPipeBridge.class);

View File

@@ -1,4 +1,4 @@
package io.eiren.hardware.magentometer;
package dev.slimevr.hardware.magentometer;
import com.sun.jna.Library;
import com.sun.jna.Native;

View File

@@ -11,9 +11,9 @@ import java.io.FileOutputStream;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.vr.trackers.TrackerPosition;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.trackers.TrackerPosition;
public final class PoseFrameIO {
@@ -26,7 +26,6 @@ public final class PoseFrameIO {
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++) {

View File

@@ -0,0 +1,72 @@
package dev.slimevr.poserecorder;
import java.util.List;
import java.util.Map;
import dev.slimevr.VRServer;
import dev.slimevr.vr.processor.ComputedHumanPoseTracker;
import dev.slimevr.vr.processor.skeleton.SimpleSkeleton;
import dev.slimevr.vr.processor.skeleton.SkeletonConfigValue;
import dev.slimevr.vr.trackers.Tracker;
public class PoseFrameSkeleton extends SimpleSkeleton {
private int frameCursor = 0;
protected PoseFrameSkeleton(List<? extends ComputedHumanPoseTracker> computedTrackers) {
super(computedTrackers);
}
public PoseFrameSkeleton(VRServer server, List<? extends ComputedHumanPoseTracker> computedTrackers) {
super(server, computedTrackers);
}
public PoseFrameSkeleton(List<? extends Tracker> trackers, List<? extends ComputedHumanPoseTracker> computedTrackers) {
super(trackers, computedTrackers);
}
public PoseFrameSkeleton(List<? extends Tracker> trackers, List<? extends ComputedHumanPoseTracker> computedTrackers, Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigValue, Float> altConfigs) {
super(trackers, computedTrackers, configs, altConfigs);
}
public PoseFrameSkeleton(List<? extends Tracker> trackers, List<? extends ComputedHumanPoseTracker> computedTrackers, Map<SkeletonConfigValue, Float> configs) {
super(trackers, computedTrackers, configs);
}
private int limitCursor() {
if(frameCursor < 0) {
frameCursor = 0;
}
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;
}
// Get tracker for specific frame
@Override
protected Tracker trackerPreUpdate(Tracker tracker) {
if(tracker instanceof PoseFrameTracker) {
// Return frame if available, otherwise return the original tracker
TrackerFrame frame = ((PoseFrameTracker) tracker).safeGetFrame(frameCursor);
return frame == null ? tracker : frame;
}
return tracker;
}
}

View File

@@ -5,11 +5,11 @@ import java.util.Iterator;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerConfig;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerStatus;
import io.eiren.util.collections.FastList;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerConfig;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerStatus;
public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
@@ -137,7 +137,7 @@ public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
return true;
}
store.set(0, 0, 0, 1);
store.set(Quaternion.IDENTITY);
return false;
}
@@ -149,7 +149,7 @@ public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
return true;
}
store.set(0, 0, 0);
store.set(Vector3f.ZERO);
return false;
}
@@ -175,7 +175,7 @@ public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
@Override
public float getConfidenceLevel() {
return 0;
return 1f;
}
@Override
@@ -231,7 +231,7 @@ public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
public Iterator<TrackerFrame> iterator() {
return frames.iterator();
}
@Override
public int getTrackerId() {
return this.trackerId;

View File

@@ -4,8 +4,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import dev.slimevr.vr.trackers.Tracker;
import io.eiren.util.collections.FastList;
import io.eiren.vr.trackers.Tracker;
public final class PoseFrames implements Iterable<TrackerFrame[]> {

View File

@@ -7,82 +7,82 @@ import java.util.concurrent.Future;
import org.apache.commons.lang3.tuple.Pair;
import io.eiren.util.ann.VRServerThread;
import dev.slimevr.VRServer;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.trackers.Tracker;
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 PoseFrames poseFrame = null;
protected int numFrames = -1;
protected int frameCursor = 0;
protected long frameRecordingInterval = 60L;
protected long nextFrameTimeMs = -1L;
protected CompletableFuture<PoseFrames> 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) {
if(numFrames <= 0) {
return;
}
PoseFrames poseFrame = this.poseFrame;
List<Pair<Tracker, PoseFrameTracker>> trackers = this.trackers;
if (poseFrame == null || trackers == null) {
if(poseFrame == null || trackers == null) {
return;
}
if (frameCursor >= numFrames) {
if(frameCursor >= numFrames) {
// If done and hasn't yet, send finished recording
stopFrameRecording();
return;
}
long curTime = System.currentTimeMillis();
if (curTime < nextFrameTimeMs) {
if(curTime < nextFrameTimeMs) {
return;
}
nextFrameTimeMs += frameRecordingInterval;
// To prevent duplicate frames, make sure the frame time is always in the future
if (nextFrameTimeMs <= curTime) {
if(nextFrameTimeMs <= curTime) {
nextFrameTimeMs = curTime + frameRecordingInterval;
}
// Make sure it's synchronized since this is the server thread interacting with
// an unknown outside thread controlling this class
synchronized (this) {
synchronized(this) {
// A stopped recording will be accounted for by an empty "trackers" list
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) {
stopFrameRecording();
}
}
}
public synchronized Future<PoseFrames> startFrameRecording(int numFrames, long intervalMs) {
return startFrameRecording(numFrames, intervalMs, server.getAllTrackers());
}
public synchronized Future<PoseFrames> startFrameRecording(int numFrames, long intervalMs, List<Tracker> trackers) {
if(numFrames < 1) {
throw new IllegalArgumentException("numFrames must at least have a value of 1");
@@ -99,11 +99,11 @@ public class PoseRecorder {
if(!isReadyToRecord()) {
throw new IllegalStateException("PoseRecorder isn't ready to record!");
}
cancelFrameRecording();
poseFrame = new PoseFrames(trackers.size());
// Update tracker list
this.trackers.ensureCapacity(trackers.size());
for(Tracker tracker : trackers) {
@@ -111,65 +111,65 @@ public class PoseRecorder {
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 = intervalMs;
nextFrameTimeMs = -1L;
LogManager.log.info("[PoseRecorder] Recording " + numFrames + " samples at a " + intervalMs + " ms frame interval");
currentRecording = new CompletableFuture<PoseFrames>();
return currentRecording;
}
public synchronized void stopFrameRecording() {
CompletableFuture<PoseFrames> 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 cancelFrameRecording() {
CompletableFuture<PoseFrames> 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 synchronized boolean isReadyToRecord() {
return server.getTrackersCount() > 0;
}
public synchronized boolean isRecording() {
return numFrames > frameCursor;
}
public synchronized boolean hasRecording() {
return currentRecording != null;
}
public synchronized Future<PoseFrames> getFramesAsync() {
return currentRecording;
}
public synchronized PoseFrames getFrames() throws ExecutionException, InterruptedException {
CompletableFuture<PoseFrames> currentRecording = this.currentRecording;
return currentRecording != null ? currentRecording.get() : null;

View File

@@ -3,10 +3,10 @@ package dev.slimevr.poserecorder;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerConfig;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerStatus;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerConfig;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerStatus;
public final class TrackerFrame implements Tracker {
@@ -86,7 +86,7 @@ public final class TrackerFrame implements Tracker {
return true;
}
store.set(0, 0, 0, 1);
store.set(Quaternion.IDENTITY);
return false;
}
@@ -97,7 +97,7 @@ public final class TrackerFrame implements Tracker {
return true;
}
store.set(0, 0, 0);
store.set(Vector3f.ZERO);
return false;
}
@@ -123,7 +123,7 @@ public final class TrackerFrame implements Tracker {
@Override
public float getConfidenceLevel() {
return 0;
return 1f;
}
@Override
@@ -171,7 +171,7 @@ public final class TrackerFrame implements Tracker {
return true;
}
//#endregion
@Override
public int getTrackerId() {
return this.trackerId;

View File

@@ -8,166 +8,257 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import org.apache.commons.lang3.StringUtils;
import io.eiren.vr.processor.HumanSkeleton;
import io.eiren.vr.processor.TransformNode;
import dev.slimevr.vr.processor.TransformNode;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
public class BVHFileStream extends PoseDataStream {
private static final int LONG_MAX_VALUE_DIGITS = Long.toString(Long.MAX_VALUE).length();
private static final float POS_SCALE = 10f;
private static final float OFFSET_SCALE = 100f;
private static final float POSITION_SCALE = 100f;
private long frameCount = 0;
private final BufferedWriter writer;
private long frameCountOffset;
private float[] angleBuf = new float[3];
private Quaternion rotBuf = new Quaternion();
private HumanSkeleton wrappedSkeleton;
private TransformNodeWrapper rootNode;
public BVHFileStream(OutputStream outputStream) {
super(outputStream);
writer = new BufferedWriter(new OutputStreamWriter(outputStream), 4096);
}
public BVHFileStream(File file) throws FileNotFoundException {
super(file);
writer = new BufferedWriter(new OutputStreamWriter(outputStream), 4096);
}
public BVHFileStream(String file) throws FileNotFoundException {
super(file);
writer = new BufferedWriter(new OutputStreamWriter(outputStream), 4096);
}
private String getBufferedFrameCount(long frameCount) {
String frameString = Long.toString(frameCount);
int bufferCount = LONG_MAX_VALUE_DIGITS - frameString.length();
return bufferCount > 0 ? frameString + StringUtils.repeat(' ', bufferCount) : frameString;
}
private void writeTransformNodeHierarchy(TransformNode node) throws IOException {
writeTransformNodeHierarchy(node, 0);
private TransformNodeWrapper wrapSkeletonIfNew(HumanSkeleton skeleton) {
TransformNodeWrapper wrapper = rootNode;
// If the wrapped skeleton is missing or the skeleton is updated
if(wrapper == null || skeleton != wrappedSkeleton) {
wrapper = wrapSkeleton(skeleton);
}
return wrapper;
}
private void writeTransformNodeHierarchy(TransformNode node, int level) throws IOException {
private TransformNodeWrapper wrapSkeleton(HumanSkeleton skeleton) {
TransformNodeWrapper wrapper = wrapSkeletonNodes(skeleton.getRootNode());
wrappedSkeleton = skeleton;
rootNode = wrapper;
return wrapper;
}
protected TransformNodeWrapper wrapSkeletonNodes(TransformNode rootNode) {
return TransformNodeWrapper.wrapFullHierarchy(rootNode);
}
private void writeNodeHierarchy(TransformNodeWrapper node) throws IOException {
writeNodeHierarchy(node, 0);
}
private void writeNodeHierarchy(TransformNodeWrapper node, int level) throws IOException {
// Don't write end sites at populated nodes
if(node.children.isEmpty() && node.getParent().children.size() > 1) {
return;
}
String indentLevel = StringUtils.repeat("\t", level);
String nextIndentLevel = indentLevel + "\t";
// Handle ends
if (node.children.isEmpty()) {
if(node.children.isEmpty()) {
writer.write(indentLevel + "End Site\n");
} else {
writer.write((level > 0 ? indentLevel + "JOINT " : "ROOT ") + node.getName() + "\n");
}
writer.write(indentLevel + "{\n");
if (level > 0) {
// Ignore the root offset and original root offset
if(level > 0 && node.wrappedNode.getParent() != null) {
Vector3f offset = node.localTransform.getTranslation();
writer.write(nextIndentLevel + "OFFSET " + Float.toString(offset.getX() * POS_SCALE) + " " + Float.toString(offset.getY() * POS_SCALE) + " " + Float.toString(offset.getZ() * POS_SCALE) + "\n");
float reverseMultiplier = node.hasReversedHierarchy() ? -1 : 1;
writer.write(nextIndentLevel + "OFFSET " + Float.toString(offset.getX() * OFFSET_SCALE * reverseMultiplier) + " " + Float.toString(offset.getY() * OFFSET_SCALE * reverseMultiplier) + " " + Float.toString(offset.getZ() * OFFSET_SCALE * reverseMultiplier) + "\n");
} else {
writer.write(nextIndentLevel + "OFFSET 0.0 0.0 0.0\n");
}
// Handle ends
if (!node.children.isEmpty()) {
if(!node.children.isEmpty()) {
// Only give position for root
if (level > 0) {
if(level > 0) {
writer.write(nextIndentLevel + "CHANNELS 3 Zrotation Xrotation Yrotation\n");
} else {
writer.write(nextIndentLevel + "CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n");
}
for (TransformNode childNode : node.children) {
writeTransformNodeHierarchy(childNode, level + 1);
for(TransformNodeWrapper childNode : node.children) {
writeNodeHierarchy(childNode, level + 1);
}
}
writer.write(indentLevel + "}\n");
}
@Override
public void writeHeader(HumanSkeleton skeleton, PoseStreamer streamer) throws IOException {
if (skeleton == null) {
if(skeleton == null) {
throw new NullPointerException("skeleton must not be null");
}
if (streamer == null) {
if(streamer == null) {
throw new NullPointerException("streamer must not be null");
}
writer.write("HIERARCHY\n");
writeTransformNodeHierarchy(skeleton.getRootNode());
writeNodeHierarchy(wrapSkeletonIfNew(skeleton));
writer.write("MOTION\n");
writer.write("Frames: ");
// Get frame offset for finishing writing the file
if (outputStream instanceof FileOutputStream) {
FileOutputStream fileOutputStream = (FileOutputStream)outputStream;
if(outputStream instanceof FileOutputStream) {
FileOutputStream fileOutputStream = (FileOutputStream) outputStream;
// Flush buffer to get proper offset
writer.flush();
frameCountOffset = fileOutputStream.getChannel().position();
}
writer.write(getBufferedFrameCount(frameCount) + "\n");
// Frame time in seconds
writer.write("Frame Time: " + (streamer.frameRecordingInterval / 1000d) + "\n");
}
private void writeTransformHierarchyRotation(TransformNode node, Quaternion inverseRootRot) throws IOException {
rotBuf = node.localTransform.getRotation(rotBuf);
// Adjust to local rotation
if (inverseRootRot != null) {
rotBuf = inverseRootRot.mult(rotBuf, rotBuf);
// Roughly based off code from https://github.com/TrackLab/ViRe/blob/50a987eff4db31036b2ebaeb5a28983cd473f267/Assets/Scripts/BVH/BVHRecorder.cs
private float[] quatToXyzAngles(Quaternion q, float[] angles) {
if(angles == null) {
angles = new float[3];
} else if(angles.length != 3) {
throw new IllegalArgumentException("Angles array must have three elements");
}
angleBuf = rotBuf.toAngles(angleBuf);
writer.write(Float.toString((float)Math.toDegrees(angleBuf[2])) + " " + Float.toString((float)Math.toDegrees(angleBuf[0])) + " " + Float.toString((float)Math.toDegrees(angleBuf[1])));
// Get inverse rotation for child local rotations
Quaternion inverseRot = node.localTransform.getRotation().inverse();
for (TransformNode childNode : node.children) {
if (childNode.children.isEmpty()) {
// If it's an end node, skip
continue;
float x = q.getX();
float y = q.getY();
float z = q.getZ();
float w = q.getW();
// Roll (X)
float sinrCosp = -2f * (x * y - w * z);
float cosrCosp = w * w - x * x + y * y - z * z;
angles[0] = FastMath.atan2(sinrCosp, cosrCosp);
// Pitch (Y)
float sinp = 2f * (y * z + w * x);
// Use 90 degrees if out of range
angles[1] = FastMath.abs(sinp) >= 1f ? FastMath.copysign(FastMath.PI / 2f, sinp) : FastMath.asin(sinp);
// Yaw (Z)
float sinyCosp = -2f * (x * z - w * y);
float cosyCosp = w * w - x * x - y * y + z * z;
angles[2] = FastMath.atan2(sinyCosp, cosyCosp);
return angles;
}
private void writeNodeHierarchyRotation(TransformNodeWrapper node, Quaternion inverseRootRot) throws IOException {
Transform transform = node.worldTransform;
/*
if (node.hasReversedHierarchy()) {
for (TransformNodeWrapper childNode : node.children) {
// If the hierarchy is fully reversed, set the rotation for the upper bone
if (childNode.hasReversedHierarchy()) {
transform = childNode.worldTransform;
break;
}
}
}
*/
rotBuf = transform.getRotation(rotBuf);
// Adjust to local rotation
if(inverseRootRot != null) {
rotBuf = rotBuf.multLocal(inverseRootRot);
}
// Yaw (Z), roll (X), pitch (Y) (intrinsic)
// angleBuf = rotBuf.toAngles(angleBuf);
// Roll (X), pitch (Y), yaw (Z) (intrinsic)
angleBuf = quatToXyzAngles(rotBuf.normalizeLocal(), angleBuf);
// Output in order of roll (Z), pitch (X), yaw (Y) (extrinsic)
writer.write(Float.toString(angleBuf[0] * FastMath.RAD_TO_DEG) + " " + Float.toString(angleBuf[1] * FastMath.RAD_TO_DEG) + " " + Float.toString(angleBuf[2] * FastMath.RAD_TO_DEG));
// Get inverse rotation for child local rotations
if(!node.children.isEmpty()) {
Quaternion inverseRot = transform.getRotation().inverse();
for(TransformNodeWrapper childNode : node.children) {
if(childNode.children.isEmpty()) {
// If it's an end node, skip
continue;
}
// Add spacing
writer.write(" ");
writeNodeHierarchyRotation(childNode, inverseRot);
}
// Add spacing
writer.write(" ");
writeTransformHierarchyRotation(childNode, inverseRot);
}
}
@Override
public void writeFrame(HumanSkeleton skeleton) throws IOException {
if (skeleton == null) {
if(skeleton == null) {
throw new NullPointerException("skeleton must not be null");
}
TransformNode root = skeleton.getRootNode();
Vector3f rootPos = root.localTransform.getTranslation();
TransformNodeWrapper rootNode = wrapSkeletonIfNew(skeleton);
Vector3f rootPos = rootNode.worldTransform.getTranslation();
// Write root position
writer.write(Float.toString(rootPos.getX() * POS_SCALE) + " " + Float.toString(rootPos.getY() * POS_SCALE) + " " + Float.toString(rootPos.getZ() * POS_SCALE) + " ");
writeTransformHierarchyRotation(root, null);
writer.write(Float.toString(rootPos.getX() * POSITION_SCALE) + " " + Float.toString(rootPos.getY() * POSITION_SCALE) + " " + Float.toString(rootPos.getZ() * POSITION_SCALE) + " ");
writeNodeHierarchyRotation(rootNode, null);
writer.newLine();
frameCount++;
}
@Override
public void writeFooter(HumanSkeleton skeleton) throws IOException {
// Write the final frame count for files
if (outputStream instanceof FileOutputStream) {
FileOutputStream fileOutputStream = (FileOutputStream)outputStream;
if(outputStream instanceof FileOutputStream) {
FileOutputStream fileOutputStream = (FileOutputStream) outputStream;
// Flush before anything else
writer.flush();
// Seek to the count offset
@@ -176,7 +267,7 @@ public class BVHFileStream extends PoseDataStream {
writer.write(Long.toString(frameCount));
}
}
@Override
public void close() throws IOException {
writer.close();

View File

@@ -6,37 +6,37 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import io.eiren.vr.processor.HumanSkeleton;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
public abstract class PoseDataStream implements AutoCloseable {
protected boolean closed = false;
protected final OutputStream outputStream;
protected PoseDataStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
protected PoseDataStream(File file) throws FileNotFoundException {
this(new FileOutputStream(file));
}
protected PoseDataStream(String file) throws FileNotFoundException {
this(new FileOutputStream(file));
}
public void writeHeader(HumanSkeleton skeleton, PoseStreamer streamer) throws IOException {
}
abstract void writeFrame(HumanSkeleton skeleton) throws IOException;
public void writeFooter(HumanSkeleton skeleton) throws IOException {
}
public boolean isClosed() {
return closed;
}
@Override
public void close() throws IOException {
outputStream.close();

View File

@@ -2,109 +2,115 @@ package dev.slimevr.posestreamer;
import java.io.IOException;
import io.eiren.util.ann.VRServerThread;
import dev.slimevr.VRServer;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
import io.eiren.util.logging.LogManager;
import io.eiren.vr.VRServer;
import io.eiren.vr.processor.HumanSkeleton;
public class PoseStreamer {
protected long frameRecordingInterval = 60L;
protected long nextFrameTimeMs = -1L;
private HumanSkeleton skeleton;
private PoseDataStream poseFileStream;
protected final VRServer server;
public PoseStreamer(VRServer server) {
this.server = server;
// Register callbacks/events
server.addSkeletonUpdatedCallback(this::onSkeletonUpdated);
server.addOnTick(this::onTick);
}
@VRServerThread
public void onSkeletonUpdated(HumanSkeleton skeleton) {
this.skeleton = skeleton;
}
@VRServerThread
public void onTick() {
PoseDataStream poseFileStream = this.poseFileStream;
if (poseFileStream == null) {
if(poseFileStream == null) {
return;
}
HumanSkeleton skeleton = this.skeleton;
if (skeleton == null) {
if(skeleton == null) {
return;
}
long curTime = System.currentTimeMillis();
if (curTime < nextFrameTimeMs) {
if(curTime < nextFrameTimeMs) {
return;
}
nextFrameTimeMs += frameRecordingInterval;
// To prevent duplicate frames, make sure the frame time is always in the future
if (nextFrameTimeMs <= curTime) {
if(nextFrameTimeMs <= curTime) {
nextFrameTimeMs = curTime + frameRecordingInterval;
}
// Make sure it's synchronized since this is the server thread interacting with
// an unknown outside thread controlling this class
synchronized (this) {
synchronized(this) {
// Make sure the stream is open before trying to write
if (poseFileStream.isClosed()) {
if(poseFileStream.isClosed()) {
return;
}
try {
poseFileStream.writeFrame(skeleton);
} catch (Exception e) {
} catch(Exception e) {
// Handle any exceptions without crashing the program
LogManager.log.severe("[PoseStreamer] Exception while saving frame", e);
}
}
}
public synchronized void setFrameInterval(long intervalMs) {
if(intervalMs < 1) {
throw new IllegalArgumentException("intervalMs must at least have a value of 1");
}
this.frameRecordingInterval = intervalMs;
}
public synchronized long getFrameInterval() {
return frameRecordingInterval;
}
public synchronized void setOutput(PoseDataStream poseFileStream) throws IOException {
poseFileStream.writeHeader(skeleton, this);
this.poseFileStream = poseFileStream;
nextFrameTimeMs = -1L; // Reset the frame timing
}
public synchronized void setOutput(PoseDataStream poseFileStream, long intervalMs) throws IOException {
setFrameInterval(intervalMs);
setOutput(poseFileStream);
}
public synchronized PoseDataStream getOutput() {
return poseFileStream;
}
public synchronized void closeOutput() throws IOException {
PoseDataStream poseFileStream = this.poseFileStream;
if (poseFileStream != null) {
poseFileStream.writeFooter(skeleton);
poseFileStream.close();
if(poseFileStream != null) {
closeOutput(poseFileStream);
this.poseFileStream = null;
}
}
public synchronized void closeOutput(PoseDataStream poseFileStream) throws IOException {
if(poseFileStream != null) {
poseFileStream.writeFooter(skeleton);
poseFileStream.close();
}
}
}

View File

@@ -0,0 +1,63 @@
package dev.slimevr.posestreamer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import dev.slimevr.vr.processor.TransformNode;
public class StdBVHFileStream extends BVHFileStream {
public StdBVHFileStream(OutputStream outputStream) {
super(outputStream);
}
public StdBVHFileStream(File file) throws FileNotFoundException {
super(file);
}
public StdBVHFileStream(String file) throws FileNotFoundException {
super(file);
}
@Override
protected TransformNodeWrapper wrapSkeletonNodes(TransformNode rootNode) {
TransformNode newRoot = getNodeFromHierarchy(rootNode, "Hip");
if(newRoot == null) {
return null;
}
TransformNodeWrapper wrappedRoot = TransformNodeWrapper.wrapHierarchyDown(newRoot);
/*
// If should wrap up hierarchy
if (newRoot.getParent() != null) {
// Create an extra node for full proper rotation
TransformNodeWrapper spineWrapper = new TransformNodeWrapper(new TransformNode("Spine", false), true, 1);
wrappedRoot.attachChild(spineWrapper);
// Wrap up on top of the spine node
TransformNodeWrapper.wrapNodeHierarchyUp(newRoot, spineWrapper);
}
*/
TransformNodeWrapper.wrapNodeHierarchyUp(wrappedRoot);
return wrappedRoot;
}
private TransformNode getNodeFromHierarchy(TransformNode node, String name) {
if(node.getName().equalsIgnoreCase(name)) {
return node;
}
for(TransformNode child : node.children) {
TransformNode result = getNodeFromHierarchy(child, name);
if(result != null) {
return result;
}
}
return null;
}
}

View File

@@ -0,0 +1,158 @@
package dev.slimevr.posestreamer;
import java.util.List;
import com.jme3.math.Quaternion;
import com.jme3.math.Transform;
import dev.slimevr.vr.processor.TransformNode;
import io.eiren.util.collections.FastList;
public class TransformNodeWrapper {
public final TransformNode wrappedNode;
protected String name;
public final Transform localTransform;
public final Transform worldTransform;
private boolean reversedHierarchy = false;
protected TransformNodeWrapper parent;
public final List<TransformNodeWrapper> children;
public TransformNodeWrapper(TransformNode nodeToWrap, String name, boolean reversedHierarchy, int initialChildCapacity) {
this.wrappedNode = nodeToWrap;
this.name = name;
this.localTransform = nodeToWrap.localTransform;
this.worldTransform = nodeToWrap.worldTransform;
this.reversedHierarchy = reversedHierarchy;
this.children = new FastList<>(initialChildCapacity);
}
public TransformNodeWrapper(TransformNode nodeToWrap, String name, int initialChildCapacity) {
this(nodeToWrap, name, false, initialChildCapacity);
}
public TransformNodeWrapper(TransformNode nodeToWrap, String name) {
this(nodeToWrap, name, false, 5);
}
public TransformNodeWrapper(TransformNode nodeToWrap, boolean reversedHierarchy, int initialChildCapacity) {
this(nodeToWrap, nodeToWrap.getName(), reversedHierarchy, initialChildCapacity);
}
public TransformNodeWrapper(TransformNode nodeToWrap, boolean reversedHierarchy) {
this(nodeToWrap, nodeToWrap.getName(), reversedHierarchy, 5);
}
public TransformNodeWrapper(TransformNode nodeToWrap, int initialChildCapacity) {
this(nodeToWrap, nodeToWrap.getName(), initialChildCapacity);
}
public TransformNodeWrapper(TransformNode nodeToWrap) {
this(nodeToWrap, nodeToWrap.getName());
}
public static TransformNodeWrapper wrapFullHierarchy(TransformNode root) {
return wrapNodeHierarchyUp(wrapHierarchyDown(root));
}
public static TransformNodeWrapper wrapHierarchyDown(TransformNode root) {
return wrapNodeHierarchyDown(new TransformNodeWrapper(root, root.children.size()));
}
public static TransformNodeWrapper wrapNodeHierarchyDown(TransformNodeWrapper root) {
for(TransformNode child : root.wrappedNode.children) {
root.attachChild(wrapHierarchyDown(child));
}
return root;
}
public static TransformNodeWrapper wrapHierarchyUp(TransformNode root) {
return wrapNodeHierarchyUp(new TransformNodeWrapper(root, root.getParent() != null ? 1 : 0));
}
public static TransformNodeWrapper wrapNodeHierarchyUp(TransformNodeWrapper root) {
return wrapNodeHierarchyUp(root.wrappedNode, root);
}
public static TransformNodeWrapper wrapNodeHierarchyUp(TransformNode root, TransformNodeWrapper target) {
TransformNode parent = root.getParent();
if(parent == null) {
return target;
}
// Flip the offset for these reversed nodes
TransformNodeWrapper wrapper = new TransformNodeWrapper(parent, true, (parent.getParent() != null ? 1 : 0) + Math.max(0, parent.children.size() - 1));
target.attachChild(wrapper);
// Re-attach other children
if(parent.children.size() > 1) {
for(TransformNode child : parent.children) {
// Skip the original node
if(child == target.wrappedNode) {
continue;
}
wrapper.attachChild(wrapHierarchyDown(child));
}
}
// Continue up the hierarchy
wrapNodeHierarchyUp(wrapper);
// Return original node
return target;
}
public boolean hasReversedHierarchy() {
return reversedHierarchy;
}
public void setReversedHierarchy(boolean reversedHierarchy) {
this.reversedHierarchy = reversedHierarchy;
}
public boolean hasLocalRotation() {
return wrappedNode.localRotation;
}
public Quaternion calculateLocalRotation(Quaternion relativeTo, Quaternion result) {
return calculateLocalRotationInverse(relativeTo.inverse(), result);
}
public Quaternion calculateLocalRotationInverse(Quaternion inverseRelativeTo, Quaternion result) {
if(result == null) {
result = new Quaternion();
}
return worldTransform.getRotation().mult(inverseRelativeTo, result);
}
public void attachChild(TransformNodeWrapper node) {
if(node.parent != null) {
throw new IllegalArgumentException("The child node must not already have a parent");
}
this.children.add(node);
node.parent = this;
}
public TransformNodeWrapper getParent() {
return parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -1,4 +1,4 @@
package io.eiren.util.ann;
package dev.slimevr.util.ann;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

View File

@@ -1,17 +1,17 @@
package io.eiren.vr.processor;
package dev.slimevr.vr.processor;
import dev.slimevr.vr.trackers.ComputedTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.TrackerRole;
import dev.slimevr.vr.trackers.TrackerWithTPS;
import io.eiren.util.BufferedTimer;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.TrackerRole;
import io.eiren.vr.trackers.TrackerWithTPS;
public class ComputedHumanPoseTracker extends ComputedTracker implements TrackerWithTPS, ShareableTracker {
public final ComputedHumanPoseTrackerPosition skeletonPosition;
protected final TrackerRole trackerRole;
protected BufferedTimer timer = new BufferedTimer(1f);
public ComputedHumanPoseTracker(int trackerId, ComputedHumanPoseTrackerPosition skeletonPosition, TrackerRole role) {
super(trackerId, "human://" + skeletonPosition.name(), true, true);
this.skeletonPosition = skeletonPosition;
@@ -27,7 +27,7 @@ public class ComputedHumanPoseTracker extends ComputedTracker implements Tracker
public void dataTick() {
timer.update();
}
@Override
public TrackerRole getTrackerRole() {
return trackerRole;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.processor;
package dev.slimevr.vr.processor;
public enum ComputedHumanPoseTrackerPosition {

View File

@@ -1,17 +1,21 @@
package io.eiren.vr.processor;
package dev.slimevr.vr.processor;
import java.util.List;
import java.util.function.Consumer;
import dev.slimevr.VRServer;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.processor.skeleton.HumanSkeleton;
import dev.slimevr.vr.processor.skeleton.SimpleSkeleton;
import dev.slimevr.vr.processor.skeleton.SkeletonConfig;
import dev.slimevr.vr.processor.skeleton.SkeletonConfigValue;
import dev.slimevr.vr.trackers.HMDTracker;
import dev.slimevr.vr.trackers.ShareableTracker;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerRole;
import dev.slimevr.vr.trackers.TrackerStatus;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.ann.VRServerThread;
import io.eiren.util.collections.FastList;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.ShareableTracker;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerRole;
import io.eiren.vr.trackers.TrackerStatus;
public class HumanPoseProcessor {
@@ -19,7 +23,7 @@ public class HumanPoseProcessor {
private final List<ComputedHumanPoseTracker> computedTrackers = new FastList<>();
private final List<Consumer<HumanSkeleton>> onSkeletonUpdated = new FastList<>();
private HumanSkeleton skeleton;
public HumanPoseProcessor(VRServer server, HMDTracker hmd) {
this.server = server;
computedTrackers.add(new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.WAIST, TrackerRole.WAIST));
@@ -33,7 +37,7 @@ public class HumanPoseProcessor {
public HumanSkeleton getSkeleton() {
return skeleton;
}
@VRServerThread
public void addSkeletonUpdatedCallback(Consumer<HumanSkeleton> consumer) {
onSkeletonUpdated.add(consumer);
@@ -42,23 +46,32 @@ public class HumanPoseProcessor {
}
@ThreadSafe
public void setSkeletonConfig(String key, float newLength) {
public void setSkeletonConfig(SkeletonConfigValue key, float newLength) {
if(skeleton != null)
skeleton.setSkeletonConfig(key, newLength);
skeleton.getSkeletonConfig().setConfig(key, newLength);
}
@ThreadSafe
public void resetSkeletonConfig(String key) {
public void resetSkeletonConfig(SkeletonConfigValue key) {
if(skeleton != null)
skeleton.resetSkeletonConfig(key);
}
@ThreadSafe
public float getSkeletonConfig(String key) {
public void resetAllSkeletonConfigs() {
if(skeleton != null)
skeleton.resetAllSkeletonConfigs();
}
@ThreadSafe
public SkeletonConfig getSkeletonConfig() {
return skeleton.getSkeletonConfig();
}
@ThreadSafe
public float getSkeletonConfig(SkeletonConfigValue key) {
if(skeleton != null) {
Number f = skeleton.getSkeletonConfig().get(key);
if(f != null)
return f.floatValue();
return skeleton.getSkeletonConfig().getConfig(key);
}
return 0.0f;
}
@@ -67,44 +80,44 @@ public class HumanPoseProcessor {
public List<? extends ShareableTracker> getComputedTrackers() {
return computedTrackers;
}
@VRServerThread
public void trackerAdded(Tracker tracker) {
updateSekeltonModel();
}
@VRServerThread
public void trackerUpdated(Tracker tracker) {
updateSekeltonModel();
}
@VRServerThread
private void updateSekeltonModel() {
disconnectAllTrackers();
skeleton = new HumanSkeletonWithLegs(server, computedTrackers);
skeleton = new SimpleSkeleton(server, computedTrackers);
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
onSkeletonUpdated.get(i).accept(skeleton);
}
@VRServerThread
private void disconnectAllTrackers() {
for(int i = 0; i < computedTrackers.size(); ++i) {
computedTrackers.get(i).setStatus(TrackerStatus.DISCONNECTED);
}
}
@VRServerThread
public void update() {
if(skeleton != null)
skeleton.updatePose();
}
@VRServerThread
public void resetTrackers() {
if(skeleton != null)
skeleton.resetTrackersFull();
}
@VRServerThread
public void resetTrackersYaw() {
if(skeleton != null)

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.processor;
package dev.slimevr.vr.processor;
import java.util.List;
import java.util.function.Consumer;
@@ -26,10 +26,18 @@ public class TransformNode {
}
public void attachChild(TransformNode node) {
if(node.parent != null) {
throw new IllegalArgumentException("The child node must not already have a parent");
}
this.children.add(node);
node.parent = this;
}
public TransformNode getParent() {
return parent;
}
public void update() {
updateWorldTransforms(); // Call update on each frame because we have relatively few nodes
for(int i = 0; i < children.size(); ++i)
@@ -58,14 +66,11 @@ public class TransformNode {
public String getName() {
return name;
}
public void combineWithParentGlobalRotation(Transform parent) {
worldTransform.getScale().multLocal(parent.getScale());
worldTransform.getTranslation().multLocal(parent.getScale());
parent
.getRotation()
.multLocal(worldTransform.getTranslation())
.addLocal(parent.getTranslation());
}
public void combineWithParentGlobalRotation(Transform parent) {
worldTransform.getScale().multLocal(parent.getScale());
worldTransform.getTranslation().multLocal(parent.getScale());
parent.getRotation().multLocal(worldTransform.getTranslation()).addLocal(parent.getTranslation());
}
}

View File

@@ -0,0 +1,33 @@
package dev.slimevr.vr.processor.skeleton;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.processor.TransformNode;
import io.eiren.util.ann.ThreadSafe;
public abstract class HumanSkeleton {
@VRServerThread
public abstract void updatePose();
@ThreadSafe
public abstract TransformNode getRootNode();
@ThreadSafe
public abstract SkeletonConfig getSkeletonConfig();
@ThreadSafe
public abstract void resetSkeletonConfig(SkeletonConfigValue config);
@ThreadSafe
public void resetAllSkeletonConfigs() {
for(SkeletonConfigValue config : SkeletonConfigValue.values) {
resetSkeletonConfig(config);
}
}
@VRServerThread
public abstract void resetTrackersFull();
@VRServerThread
public abstract void resetTrackersYaw();
}

View File

@@ -0,0 +1,826 @@
package dev.slimevr.vr.processor.skeleton;
import java.util.List;
import java.util.Map;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.VRServer;
import dev.slimevr.util.ann.VRServerThread;
import dev.slimevr.vr.processor.ComputedHumanPoseTracker;
import dev.slimevr.vr.processor.ComputedHumanPoseTrackerPosition;
import dev.slimevr.vr.processor.TransformNode;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerPosition;
import dev.slimevr.vr.trackers.TrackerRole;
import dev.slimevr.vr.trackers.TrackerStatus;
import dev.slimevr.vr.trackers.TrackerUtils;
import io.eiren.util.collections.FastList;
public class SimpleSkeleton extends HumanSkeleton implements SkeletonConfigCallback {
public static final float DEFAULT_FLOOR_OFFSET = 0.05f;
//#region Upper body nodes (torso)
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 chestNode = new TransformNode("Chest", false);
protected final TransformNode trackerChestNode = new TransformNode("Chest-Tracker", false);
protected final TransformNode waistNode = new TransformNode("Waist", false);
protected final TransformNode hipNode = new TransformNode("Hip", false);
protected final TransformNode trackerWaistNode = new TransformNode("Waist-Tracker", false);
//#endregion
//#region Lower body nodes (legs)
protected final TransformNode leftHipNode = new TransformNode("Left-Hip", false);
protected final TransformNode leftKneeNode = new TransformNode("Left-Knee", false);
protected final TransformNode trackerLeftKneeNode = new TransformNode("Left-Knee-Tracker", false);
protected final TransformNode leftAnkleNode = new TransformNode("Left-Ankle", false);
protected final TransformNode leftFootNode = new TransformNode("Left-Foot", false);
protected final TransformNode trackerLeftFootNode = new TransformNode("Left-Foot-Tracker", false);
protected final TransformNode rightHipNode = new TransformNode("Right-Hip", false);
protected final TransformNode rightKneeNode = new TransformNode("Right-Knee", false);
protected final TransformNode trackerRightKneeNode = new TransformNode("Right-Knee-Tracker", false);
protected final TransformNode rightAnkleNode = new TransformNode("Right-Ankle", false);
protected final TransformNode rightFootNode = new TransformNode("Right-Foot", false);
protected final TransformNode trackerRightFootNode = new TransformNode("Right-Foot-Tracker", false);
protected float minKneePitch = 0f * FastMath.DEG_TO_RAD;
protected float maxKneePitch = 90f * FastMath.DEG_TO_RAD;
protected float kneeLerpFactor = 0.5f;
//#endregion
//#region Tracker Input
protected Tracker hmdTracker;
protected Tracker chestTracker;
protected Tracker waistTracker;
protected Tracker hipTracker;
protected Tracker leftLegTracker;
protected Tracker leftAnkleTracker;
protected Tracker leftFootTracker;
protected Tracker rightLegTracker;
protected Tracker rightAnkleTracker;
protected Tracker rightFootTracker;
//#endregion
//#region Tracker Output
protected ComputedHumanPoseTracker computedChestTracker;
protected ComputedHumanPoseTracker computedWaistTracker;
protected ComputedHumanPoseTracker computedLeftKneeTracker;
protected ComputedHumanPoseTracker computedLeftFootTracker;
protected ComputedHumanPoseTracker computedRightKneeTracker;
protected ComputedHumanPoseTracker computedRightFootTracker;
//#endregion
protected boolean extendedPelvisModel = true;
protected boolean extendedKneeModel = false;
public final SkeletonConfig skeletonConfig;
//#region Buffers
private Vector3f posBuf = new Vector3f();
private Quaternion rotBuf1 = new Quaternion();
private Quaternion rotBuf2 = new Quaternion();
protected final Vector3f hipVector = new Vector3f();
protected final Vector3f ankleVector = new Vector3f();
protected final Quaternion kneeRotation = new Quaternion();
//#endregion
//#region Constructors
protected SimpleSkeleton(List<? extends ComputedHumanPoseTracker> computedTrackers) {
//#region Assemble skeleton to hip
hmdNode.attachChild(headNode);
headNode.attachChild(neckNode);
neckNode.attachChild(chestNode);
chestNode.attachChild(waistNode);
waistNode.attachChild(hipNode);
//#endregion
//#region Assemble skeleton to feet
hipNode.attachChild(leftHipNode);
hipNode.attachChild(rightHipNode);
leftHipNode.attachChild(leftKneeNode);
rightHipNode.attachChild(rightKneeNode);
leftKneeNode.attachChild(leftAnkleNode);
rightKneeNode.attachChild(rightAnkleNode);
leftAnkleNode.attachChild(leftFootNode);
rightAnkleNode.attachChild(rightFootNode);
//#endregion
//#region Attach tracker nodes for offsets
chestNode.attachChild(trackerChestNode);
hipNode.attachChild(trackerWaistNode);
leftKneeNode.attachChild(trackerLeftKneeNode);
rightKneeNode.attachChild(trackerRightKneeNode);
leftFootNode.attachChild(trackerLeftFootNode);
rightFootNode.attachChild(trackerRightFootNode);
//#endregion
// Set default skeleton configuration (callback automatically sets initial offsets)
skeletonConfig = new SkeletonConfig(true, this);
if(computedTrackers != null) {
setComputedTrackers(computedTrackers);
}
fillNullComputedTrackers(true);
}
public SimpleSkeleton(VRServer server, List<? extends ComputedHumanPoseTracker> computedTrackers) {
this(computedTrackers);
setTrackersFromServer(server);
skeletonConfig.loadFromConfig(server.config);
}
public SimpleSkeleton(List<? extends Tracker> trackers, List<? extends ComputedHumanPoseTracker> computedTrackers) {
this(computedTrackers);
if(trackers != null) {
setTrackersFromList(trackers);
} else {
setTrackersFromList(new FastList<Tracker>(0));
}
}
public SimpleSkeleton(List<? extends Tracker> trackers, List<? extends ComputedHumanPoseTracker> computedTrackers, Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigValue, Float> altConfigs) {
// Initialize
this(trackers, computedTrackers);
// Set configs
if(altConfigs != null) {
// Set alts first, so if there's any overlap it doesn't affect the values
skeletonConfig.setConfigs(altConfigs, null);
}
skeletonConfig.setConfigs(configs, null);
}
public SimpleSkeleton(List<? extends Tracker> trackers, List<? extends ComputedHumanPoseTracker> computedTrackers, Map<SkeletonConfigValue, Float> configs) {
this(trackers, computedTrackers, configs, null);
}
//#endregion
//#region Set Trackers
public void setTrackersFromList(List<? extends Tracker> trackers, boolean setHmd) {
if(setHmd) {
this.hmdTracker = TrackerUtils.findTrackerForBodyPosition(trackers, TrackerPosition.HMD);
}
this.chestTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.CHEST, TrackerPosition.WAIST, TrackerPosition.HIP);
this.waistTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.WAIST, TrackerPosition.CHEST, TrackerPosition.HIP);
this.hipTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.HIP, TrackerPosition.WAIST, TrackerPosition.CHEST);
this.leftLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.LEFT_LEG, TrackerPosition.LEFT_ANKLE, null);
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.LEFT_ANKLE, TrackerPosition.LEFT_LEG, null);
this.leftFootTracker = TrackerUtils.findTrackerForBodyPosition(trackers, TrackerPosition.LEFT_FOOT);
this.rightLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.RIGHT_LEG, TrackerPosition.RIGHT_ANKLE, null);
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(trackers, TrackerPosition.RIGHT_ANKLE, TrackerPosition.RIGHT_LEG, null);
this.rightFootTracker = TrackerUtils.findTrackerForBodyPosition(trackers, TrackerPosition.RIGHT_FOOT);
}
public void setTrackersFromList(List<? extends Tracker> trackers) {
setTrackersFromList(trackers, true);
}
public void setTrackersFromServer(VRServer server) {
this.hmdTracker = server.hmdTracker;
setTrackersFromList(server.getAllTrackers(), false);
}
public void setComputedTracker(ComputedHumanPoseTracker tracker) {
switch(tracker.getTrackerRole()) {
case CHEST:
computedChestTracker = tracker;
break;
case WAIST:
computedWaistTracker = tracker;
break;
case LEFT_KNEE:
computedLeftKneeTracker = tracker;
break;
case LEFT_FOOT:
computedLeftFootTracker = tracker;
break;
case RIGHT_KNEE:
computedRightKneeTracker = tracker;
break;
case RIGHT_FOOT:
computedRightFootTracker = tracker;
break;
}
}
public void setComputedTrackers(List<? extends ComputedHumanPoseTracker> trackers) {
for(int i = 0; i < trackers.size(); ++i) {
setComputedTracker(trackers.get(i));
}
}
public void setComputedTrackersAndFillNull(List<? extends ComputedHumanPoseTracker> trackers, boolean onlyFillWaistAndFeet) {
setComputedTrackers(trackers);
fillNullComputedTrackers(onlyFillWaistAndFeet);
}
public void fillNullComputedTrackers(boolean onlyFillWaistAndFeet) {
if(computedWaistTracker == null) {
computedWaistTracker = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.WAIST, TrackerRole.WAIST);
computedWaistTracker.setStatus(TrackerStatus.OK);
}
if(computedLeftFootTracker == null) {
computedLeftFootTracker = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.LEFT_FOOT, TrackerRole.LEFT_FOOT);
computedLeftFootTracker.setStatus(TrackerStatus.OK);
}
if(computedRightFootTracker == null) {
computedRightFootTracker = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.RIGHT_FOOT, TrackerRole.RIGHT_FOOT);
computedRightFootTracker.setStatus(TrackerStatus.OK);
}
if(!onlyFillWaistAndFeet) {
if(computedChestTracker == null) {
computedChestTracker = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.CHEST, TrackerRole.CHEST);
computedChestTracker.setStatus(TrackerStatus.OK);
}
if(computedLeftKneeTracker == null) {
computedLeftKneeTracker = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.LEFT_KNEE, TrackerRole.LEFT_KNEE);
computedLeftKneeTracker.setStatus(TrackerStatus.OK);
}
if(computedRightKneeTracker == null) {
computedRightKneeTracker = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.RIGHT_KNEE, TrackerRole.RIGHT_KNEE);
computedRightKneeTracker.setStatus(TrackerStatus.OK);
}
}
}
//#endregion
//#region Get Trackers
public ComputedHumanPoseTracker getComputedTracker(TrackerRole trackerRole) {
switch(trackerRole) {
case CHEST:
return computedChestTracker;
case WAIST:
return computedWaistTracker;
case LEFT_KNEE:
return computedLeftKneeTracker;
case LEFT_FOOT:
return computedLeftFootTracker;
case RIGHT_KNEE:
return computedRightKneeTracker;
case RIGHT_FOOT:
return computedRightFootTracker;
}
return null;
}
//#endregion
//#region Processing
// Useful for sub-classes that need to return a sub-tracker (like PoseFrameTracker -> TrackerFrame)
protected Tracker trackerPreUpdate(Tracker tracker) {
return tracker;
}
// Updates the pose from tracker positions
@VRServerThread
@Override
public void updatePose() {
updateLocalTransforms();
hmdNode.update();
updateComputedTrackers();
}
//#region Update the node transforms from the trackers
protected void updateLocalTransforms() {
//#region Pass all trackers through trackerPreUpdate
Tracker hmdTracker = trackerPreUpdate(this.hmdTracker);
Tracker chestTracker = trackerPreUpdate(this.chestTracker);
Tracker waistTracker = trackerPreUpdate(this.waistTracker);
Tracker hipTracker = trackerPreUpdate(this.hipTracker);
Tracker leftLegTracker = trackerPreUpdate(this.leftLegTracker);
Tracker leftAnkleTracker = trackerPreUpdate(this.leftAnkleTracker);
Tracker leftFootTracker = trackerPreUpdate(this.leftFootTracker);
Tracker rightLegTracker = trackerPreUpdate(this.rightLegTracker);
Tracker rightAnkleTracker = trackerPreUpdate(this.rightAnkleTracker);
Tracker rightFootTracker = trackerPreUpdate(this.rightFootTracker);
//#endregion
if(hmdTracker != null) {
if(hmdTracker.getPosition(posBuf)) {
hmdNode.localTransform.setTranslation(posBuf);
}
if(hmdTracker.getRotation(rotBuf1)) {
hmdNode.localTransform.setRotation(rotBuf1);
headNode.localTransform.setRotation(rotBuf1);
}
} else {
// Set to zero
hmdNode.localTransform.setTranslation(Vector3f.ZERO);
hmdNode.localTransform.setRotation(Quaternion.IDENTITY);
headNode.localTransform.setRotation(Quaternion.IDENTITY);
}
if(chestTracker.getRotation(rotBuf1)) {
neckNode.localTransform.setRotation(rotBuf1);
}
if(waistTracker.getRotation(rotBuf1)) {
chestNode.localTransform.setRotation(rotBuf1);
trackerChestNode.localTransform.setRotation(rotBuf1);
}
if(hipTracker.getRotation(rotBuf1)) {
waistNode.localTransform.setRotation(rotBuf1);
trackerWaistNode.localTransform.setRotation(rotBuf1);
hipNode.localTransform.setRotation(rotBuf1);
}
// Left Leg
leftLegTracker.getRotation(rotBuf1);
leftAnkleTracker.getRotation(rotBuf2);
if(extendedKneeModel)
calculateKneeLimits(rotBuf1, rotBuf2, leftLegTracker.getConfidenceLevel(), leftAnkleTracker.getConfidenceLevel());
leftHipNode.localTransform.setRotation(rotBuf1);
leftKneeNode.localTransform.setRotation(rotBuf2);
leftAnkleNode.localTransform.setRotation(rotBuf2);
leftFootNode.localTransform.setRotation(rotBuf2);
trackerLeftKneeNode.localTransform.setRotation(rotBuf2);
trackerLeftFootNode.localTransform.setRotation(rotBuf2);
if(leftFootTracker != null) {
leftFootTracker.getRotation(rotBuf2);
leftAnkleNode.localTransform.setRotation(rotBuf2);
leftFootNode.localTransform.setRotation(rotBuf2);
trackerLeftFootNode.localTransform.setRotation(rotBuf2);
}
// Right Leg
rightLegTracker.getRotation(rotBuf1);
rightAnkleTracker.getRotation(rotBuf2);
if(extendedKneeModel)
calculateKneeLimits(rotBuf1, rotBuf2, rightLegTracker.getConfidenceLevel(), rightAnkleTracker.getConfidenceLevel());
rightHipNode.localTransform.setRotation(rotBuf1);
rightKneeNode.localTransform.setRotation(rotBuf2);
rightAnkleNode.localTransform.setRotation(rotBuf2);
rightFootNode.localTransform.setRotation(rotBuf2);
trackerRightKneeNode.localTransform.setRotation(rotBuf2);
trackerRightFootNode.localTransform.setRotation(rotBuf2);
if(rightFootTracker != null) {
rightFootTracker.getRotation(rotBuf2);
rightAnkleNode.localTransform.setRotation(rotBuf2);
rightFootNode.localTransform.setRotation(rotBuf2);
trackerRightFootNode.localTransform.setRotation(rotBuf2);
}
if(extendedPelvisModel) {
// Average pelvis between two legs
leftHipNode.localTransform.getRotation(rotBuf1);
rightHipNode.localTransform.getRotation(rotBuf2);
rotBuf2.nlerp(rotBuf1, 0.5f);
chestNode.localTransform.getRotation(rotBuf1);
rotBuf2.nlerp(rotBuf1, 0.3333333f);
hipNode.localTransform.setRotation(rotBuf2);
//trackerWaistNode.localTransform.setRotation(rotBuf2); // <== Provides cursed results from my test in VRChat when sitting or laying down -Erimel
// TODO : Correct the trackerWaistNode without getting cursed results (only correct yaw?)
// TODO : Use vectors to add like 50% of waist tracker yaw to waist node to reduce drift and let user take weird poses
}
}
//#endregion
//#region Knee Model
// Knee basically has only 1 DoF (pitch), average yaw and roll between knee and hip
protected void calculateKneeLimits(Quaternion hipBuf, Quaternion kneeBuf, float hipConfidence, float kneeConfidence) {
ankleVector.set(0, -1, 0);
hipVector.set(0, -1, 0);
hipBuf.multLocal(hipVector);
kneeBuf.multLocal(ankleVector);
kneeRotation.angleBetweenVectors(hipVector, ankleVector); // Find knee angle
// Substract knee angle from knee rotation. With perfect leg and perfect
// sensors result should match hip rotation perfectly
kneeBuf.multLocal(kneeRotation.inverse());
// Average knee and hip with a slerp
hipBuf.slerp(kneeBuf, 0.5f); // TODO : Use confidence to calculate changeAmt
kneeBuf.set(hipBuf);
// Return knee angle into knee rotation
kneeBuf.multLocal(kneeRotation);
}
public static float normalizeRad(float angle) {
return FastMath.normalize(angle, -FastMath.PI, FastMath.PI);
}
public static float interpolateRadians(float factor, float start, float end) {
float angle = FastMath.abs(end - start);
if(angle > FastMath.PI) {
if(end > start) {
start += FastMath.TWO_PI;
} else {
end += FastMath.TWO_PI;
}
}
float val = start + (end - start) * factor;
return normalizeRad(val);
}
//#endregion
//#region Update the output trackers
protected void updateComputedTrackers() {
if(computedChestTracker != null) {
computedChestTracker.position.set(trackerChestNode.worldTransform.getTranslation());
computedChestTracker.rotation.set(neckNode.worldTransform.getRotation());
computedChestTracker.dataTick();
}
if(computedWaistTracker != null) {
computedWaistTracker.position.set(trackerWaistNode.worldTransform.getTranslation());
computedWaistTracker.rotation.set(trackerWaistNode.worldTransform.getRotation());
computedWaistTracker.dataTick();
}
if(computedLeftKneeTracker != null) {
computedLeftKneeTracker.position.set(trackerLeftKneeNode.worldTransform.getTranslation());
computedLeftKneeTracker.rotation.set(leftHipNode.worldTransform.getRotation());
computedLeftKneeTracker.dataTick();
}
if(computedLeftFootTracker != null) {
computedLeftFootTracker.position.set(trackerLeftFootNode.worldTransform.getTranslation());
computedLeftFootTracker.rotation.set(trackerLeftFootNode.worldTransform.getRotation());
computedLeftFootTracker.dataTick();
}
if(computedRightKneeTracker != null) {
computedRightKneeTracker.position.set(trackerRightKneeNode.worldTransform.getTranslation());
computedRightKneeTracker.rotation.set(rightHipNode.worldTransform.getRotation());
computedRightKneeTracker.dataTick();
}
if(computedRightFootTracker != null) {
computedRightFootTracker.position.set(trackerRightFootNode.worldTransform.getTranslation());
computedRightFootTracker.rotation.set(trackerRightFootNode.worldTransform.getRotation());
computedRightFootTracker.dataTick();
}
}
//#endregion
//#endregion
//#region Skeleton Config
@Override
public void updateConfigState(SkeletonConfigValue config, float newValue) {
// Do nothing, the node offset callback handles all that's needed
}
@Override
public void updateToggleState(SkeletonConfigToggle configToggle, boolean newValue) {
if(configToggle == null) {
return;
}
// Cache the values of these configs
switch(configToggle) {
case EXTENDED_PELVIS_MODEL:
extendedPelvisModel = newValue;
break;
case EXTENDED_KNEE_MODEL:
extendedKneeModel = newValue;
break;
}
}
@Override
public void updateNodeOffset(SkeletonNodeOffset nodeOffset, Vector3f offset) {
if(nodeOffset == null) {
return;
}
switch(nodeOffset) {
case HEAD:
headNode.localTransform.setTranslation(offset);
break;
case NECK:
neckNode.localTransform.setTranslation(offset);
break;
case CHEST:
chestNode.localTransform.setTranslation(offset);
break;
case CHEST_TRACKER:
trackerChestNode.localTransform.setTranslation(offset);
break;
case WAIST:
waistNode.localTransform.setTranslation(offset);
break;
case HIP:
hipNode.localTransform.setTranslation(offset);
break;
case HIP_TRACKER:
trackerWaistNode.localTransform.setTranslation(offset);
break;
case LEFT_HIP:
leftHipNode.localTransform.setTranslation(offset);
break;
case RIGHT_HIP:
rightHipNode.localTransform.setTranslation(offset);
break;
case KNEE:
leftKneeNode.localTransform.setTranslation(offset);
rightKneeNode.localTransform.setTranslation(offset);
break;
case KNEE_TRACKER:
trackerLeftKneeNode.localTransform.setTranslation(offset);
trackerRightKneeNode.localTransform.setTranslation(offset);
break;
case ANKLE:
leftAnkleNode.localTransform.setTranslation(offset);
rightAnkleNode.localTransform.setTranslation(offset);
break;
case FOOT:
leftFootNode.localTransform.setTranslation(offset);
rightFootNode.localTransform.setTranslation(offset);
break;
case FOOT_TRACKER:
trackerLeftFootNode.localTransform.setTranslation(offset);
trackerRightFootNode.localTransform.setTranslation(offset);
break;
}
}
public void updatePoseAffectedByConfig(SkeletonConfigValue config) {
switch(config) {
case HEAD:
headNode.update();
updateComputedTrackers();
break;
case NECK:
neckNode.update();
updateComputedTrackers();
break;
case TORSO:
hipNode.update();
updateComputedTrackers();
break;
case CHEST:
chestNode.update();
updateComputedTrackers();
break;
case WAIST:
waistNode.update();
updateComputedTrackers();
break;
case HIP_OFFSET:
trackerWaistNode.update();
updateComputedTrackers();
break;
case HIPS_WIDTH:
leftHipNode.update();
rightHipNode.update();
updateComputedTrackers();
break;
case KNEE_HEIGHT:
leftKneeNode.update();
rightKneeNode.update();
break;
case LEGS_LENGTH:
leftKneeNode.update();
rightKneeNode.update();
updateComputedTrackers();
break;
case FOOT_LENGTH:
leftFootNode.update();
rightFootNode.update();
updateComputedTrackers();
break;
case FOOT_OFFSET:
leftAnkleNode.update();
rightAnkleNode.update();
updateComputedTrackers();
break;
case SKELETON_OFFSET:
trackerChestNode.update();
trackerWaistNode.update();
trackerLeftKneeNode.update();
trackerRightKneeNode.update();
trackerLeftFootNode.update();
trackerRightFootNode.update();
updateComputedTrackers();
break;
}
}
//#endregion
@Override
public TransformNode getRootNode() {
return hmdNode;
}
@Override
public SkeletonConfig getSkeletonConfig() {
return skeletonConfig;
}
@Override
public void resetSkeletonConfig(SkeletonConfigValue config) {
if(config == null) {
return;
}
Vector3f vec;
float height;
switch(config) {
case HEAD:
skeletonConfig.setConfig(SkeletonConfigValue.HEAD, null);
break;
case NECK:
skeletonConfig.setConfig(SkeletonConfigValue.NECK, null);
break;
case TORSO: // Distance from shoulders to hip (full torso length)
vec = new Vector3f();
hmdTracker.getPosition(vec);
height = vec.y;
if(height > 0.5f) { // Reset only if floor level is right, TODO: read floor level from SteamVR if it's not 0
skeletonConfig.setConfig(SkeletonConfigValue.TORSO, ((height) / 2.0f) - skeletonConfig.getConfig(SkeletonConfigValue.NECK));
} else// if floor level is incorrect
{
skeletonConfig.setConfig(SkeletonConfigValue.TORSO, null);
}
break;
case CHEST: //Chest is roughly half of the upper body (shoulders to chest)
skeletonConfig.setConfig(SkeletonConfigValue.CHEST, skeletonConfig.getConfig(SkeletonConfigValue.TORSO) / 2.0f);
break;
case WAIST: // waist length is from hips to waist
skeletonConfig.setConfig(SkeletonConfigValue.WAIST, null);
break;
case HIP_OFFSET:
skeletonConfig.setConfig(SkeletonConfigValue.HIP_OFFSET, null);
break;
case HIPS_WIDTH:
skeletonConfig.setConfig(SkeletonConfigValue.HIPS_WIDTH, null);
break;
case FOOT_LENGTH:
skeletonConfig.setConfig(SkeletonConfigValue.FOOT_LENGTH, null);
break;
case FOOT_OFFSET:
skeletonConfig.setConfig(SkeletonConfigValue.FOOT_OFFSET, null);
break;
case SKELETON_OFFSET:
skeletonConfig.setConfig(SkeletonConfigValue.SKELETON_OFFSET, null);
break;
case LEGS_LENGTH: // Set legs length to be 5cm above floor level
vec = new Vector3f();
hmdTracker.getPosition(vec);
height = vec.y;
if(height > 0.5f) { // Reset only if floor level is right, todo: read floor level from SteamVR if it's not 0
skeletonConfig.setConfig(SkeletonConfigValue.LEGS_LENGTH, height - skeletonConfig.getConfig(SkeletonConfigValue.NECK) - skeletonConfig.getConfig(SkeletonConfigValue.TORSO) - DEFAULT_FLOOR_OFFSET);
} else //if floor level is incorrect
{
skeletonConfig.setConfig(SkeletonConfigValue.LEGS_LENGTH, null);
}
resetSkeletonConfig(SkeletonConfigValue.KNEE_HEIGHT);
break;
case KNEE_HEIGHT: // Knees are at 50% of the legs by default
skeletonConfig.setConfig(SkeletonConfigValue.KNEE_HEIGHT, skeletonConfig.getConfig(SkeletonConfigValue.LEGS_LENGTH) / 2.0f);
break;
}
}
@Override
public void resetTrackersFull() {
//#region Pass all trackers through trackerPreUpdate
Tracker hmdTracker = trackerPreUpdate(this.hmdTracker);
Tracker chestTracker = trackerPreUpdate(this.chestTracker);
Tracker waistTracker = trackerPreUpdate(this.waistTracker);
Tracker hipTracker = trackerPreUpdate(this.hipTracker);
Tracker leftLegTracker = trackerPreUpdate(this.leftLegTracker);
Tracker leftAnkleTracker = trackerPreUpdate(this.leftAnkleTracker);
Tracker leftFootTracker = trackerPreUpdate(this.leftFootTracker);
Tracker rightLegTracker = trackerPreUpdate(this.rightLegTracker);
Tracker rightAnkleTracker = trackerPreUpdate(this.rightAnkleTracker);
Tracker rightFootTracker = trackerPreUpdate(this.rightFootTracker);
//#endregion
// Each tracker uses the tracker before it to adjust itself,
// so trackers that don't need adjustments could be used too
Quaternion referenceRotation = new Quaternion();
hmdTracker.getRotation(referenceRotation);
chestTracker.resetFull(referenceRotation);
chestTracker.getRotation(referenceRotation);
waistTracker.resetFull(referenceRotation);
waistTracker.getRotation(referenceRotation);
hipTracker.resetFull(referenceRotation);
hipTracker.getRotation(referenceRotation);
leftLegTracker.resetFull(referenceRotation);
rightLegTracker.resetFull(referenceRotation);
leftLegTracker.getRotation(referenceRotation);
leftAnkleTracker.resetFull(referenceRotation);
leftAnkleTracker.getRotation(referenceRotation);
if(leftFootTracker != null) {
leftFootTracker.resetFull(referenceRotation);
}
rightLegTracker.getRotation(referenceRotation);
rightAnkleTracker.resetFull(referenceRotation);
rightAnkleTracker.getRotation(referenceRotation);
if(rightFootTracker != null) {
rightFootTracker.resetFull(referenceRotation);
}
}
@Override
@VRServerThread
public void resetTrackersYaw() {
//#region Pass all trackers through trackerPreUpdate
Tracker hmdTracker = trackerPreUpdate(this.hmdTracker);
Tracker chestTracker = trackerPreUpdate(this.chestTracker);
Tracker waistTracker = trackerPreUpdate(this.waistTracker);
Tracker hipTracker = trackerPreUpdate(this.hipTracker);
Tracker leftLegTracker = trackerPreUpdate(this.leftLegTracker);
Tracker leftAnkleTracker = trackerPreUpdate(this.leftAnkleTracker);
Tracker leftFootTracker = trackerPreUpdate(this.leftFootTracker);
Tracker rightLegTracker = trackerPreUpdate(this.rightLegTracker);
Tracker rightAnkleTracker = trackerPreUpdate(this.rightAnkleTracker);
Tracker rightFootTracker = trackerPreUpdate(this.rightFootTracker);
//#endregion
// Each tracker uses the tracker before it to adjust itself,
// so trackers that don't need adjustments could be used too
Quaternion referenceRotation = new Quaternion();
hmdTracker.getRotation(referenceRotation);
chestTracker.resetYaw(referenceRotation);
chestTracker.getRotation(referenceRotation);
waistTracker.resetYaw(referenceRotation);
waistTracker.getRotation(referenceRotation);
hipTracker.resetYaw(referenceRotation);
hipTracker.getRotation(referenceRotation);
leftLegTracker.resetYaw(referenceRotation);
rightLegTracker.resetYaw(referenceRotation);
leftLegTracker.getRotation(referenceRotation);
leftAnkleTracker.resetYaw(referenceRotation);
leftAnkleTracker.getRotation(referenceRotation);
if(leftFootTracker != null) {
leftFootTracker.resetYaw(referenceRotation);
}
rightLegTracker.getRotation(referenceRotation);
rightAnkleTracker.resetYaw(referenceRotation);
rightAnkleTracker.getRotation(referenceRotation);
if(rightFootTracker != null) {
rightFootTracker.resetYaw(referenceRotation);
}
}
}

View File

@@ -0,0 +1,375 @@
package dev.slimevr.vr.processor.skeleton;
import java.util.EnumMap;
import java.util.Map;
import com.jme3.math.Vector3f;
import io.eiren.util.logging.LogManager;
import io.eiren.yaml.YamlFile;
public class SkeletonConfig {
protected final EnumMap<SkeletonConfigValue, Float> configs = new EnumMap<SkeletonConfigValue, Float>(SkeletonConfigValue.class);
protected final EnumMap<SkeletonConfigToggle, Boolean> toggles = new EnumMap<SkeletonConfigToggle, Boolean>(SkeletonConfigToggle.class);
protected final EnumMap<SkeletonNodeOffset, Vector3f> nodeOffsets = new EnumMap<SkeletonNodeOffset, Vector3f>(SkeletonNodeOffset.class);
protected final boolean autoUpdateOffsets;
protected final SkeletonConfigCallback callback;
public SkeletonConfig(boolean autoUpdateOffsets) {
this.autoUpdateOffsets = autoUpdateOffsets;
this.callback = null;
callCallbackOnAll(true);
if(autoUpdateOffsets) {
computeAllNodeOffsets();
}
}
public SkeletonConfig(boolean autoUpdateOffsets, SkeletonConfigCallback callback) {
this.autoUpdateOffsets = autoUpdateOffsets;
this.callback = callback;
callCallbackOnAll(true);
if(autoUpdateOffsets) {
computeAllNodeOffsets();
}
}
public SkeletonConfig(Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigToggle, Boolean> toggles, boolean autoUpdateOffsets, SkeletonConfigCallback callback) {
this.autoUpdateOffsets = autoUpdateOffsets;
this.callback = callback;
setConfigs(configs, toggles);
callCallbackOnAll(true);
}
public SkeletonConfig(Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigToggle, Boolean> toggles, boolean autoUpdateOffsets) {
this(configs, toggles, autoUpdateOffsets, null);
}
public SkeletonConfig(SkeletonConfig skeletonConfig, boolean autoUpdateOffsets, SkeletonConfigCallback callback) {
this.autoUpdateOffsets = autoUpdateOffsets;
this.callback = callback;
setConfigs(skeletonConfig);
callCallbackOnAll(true);
}
public SkeletonConfig(SkeletonConfig skeletonConfig, boolean autoUpdateOffsets) {
this(skeletonConfig, autoUpdateOffsets, null);
}
private void callCallbackOnAll(boolean defaultOnly) {
if(callback == null) {
return;
}
for(SkeletonConfigValue config : SkeletonConfigValue.values) {
try {
Float val = configs.get(config);
if(!defaultOnly || val == null) {
callback.updateConfigState(config, val == null ? config.defaultValue : val);
}
} catch(Exception e) {
LogManager.log.severe("[SkeletonConfig] Exception while calling callback", e);
}
}
for(SkeletonConfigToggle config : SkeletonConfigToggle.values) {
try {
Boolean val = toggles.get(config);
if(!defaultOnly || val == null) {
callback.updateToggleState(config, val == null ? config.defaultValue : val);
}
} catch(Exception e) {
LogManager.log.severe("[SkeletonConfig] Exception while calling callback", e);
}
}
}
public Float setConfig(SkeletonConfigValue config, Float newValue, boolean computeOffsets) {
Float origVal = newValue != null ? configs.put(config, newValue) : configs.remove(config);
// Re-compute the affected offsets
if(computeOffsets && autoUpdateOffsets && config.affectedOffsets != null) {
for(SkeletonNodeOffset offset : config.affectedOffsets) {
computeNodeOffset(offset);
}
}
if(callback != null) {
try {
callback.updateConfigState(config, newValue != null ? newValue : config.defaultValue);
} catch(Exception e) {
LogManager.log.severe("[SkeletonConfig] Exception while calling callback", e);
}
}
return origVal;
}
public Float setConfig(SkeletonConfigValue config, Float newValue) {
return setConfig(config, newValue, true);
}
public Float setConfig(String config, Float newValue) {
return setConfig(SkeletonConfigValue.getByStringValue(config), newValue);
}
public float getConfig(SkeletonConfigValue config) {
if(config == null) {
return 0f;
}
// IMPORTANT!! This null check is necessary, getOrDefault seems to randomly decide to return null at times, so this is a secondary check
Float val = configs.getOrDefault(config, config.defaultValue);
return val != null ? val : config.defaultValue;
}
public float getConfig(String config) {
if(config == null) {
return 0f;
}
return getConfig(SkeletonConfigValue.getByStringValue(config));
}
public Boolean setToggle(SkeletonConfigToggle config, Boolean newValue) {
Boolean origVal = newValue != null ? toggles.put(config, newValue) : toggles.remove(config);
if(callback != null) {
try {
callback.updateToggleState(config, newValue != null ? newValue : config.defaultValue);
} catch(Exception e) {
LogManager.log.severe("[SkeletonConfig] Exception while calling callback", e);
}
}
return origVal;
}
public Boolean setToggle(String config, Boolean newValue) {
return setToggle(SkeletonConfigToggle.getByStringValue(config), newValue);
}
public boolean getToggle(SkeletonConfigToggle config) {
if(config == null) {
return false;
}
// IMPORTANT!! This null check is necessary, getOrDefault seems to randomly decide to return null at times, so this is a secondary check
Boolean val = toggles.getOrDefault(config, config.defaultValue);
return val != null ? val : config.defaultValue;
}
public boolean getToggle(String config) {
if(config == null) {
return false;
}
return getToggle(SkeletonConfigToggle.getByStringValue(config));
}
protected void setNodeOffset(SkeletonNodeOffset nodeOffset, float x, float y, float z) {
Vector3f offset = nodeOffsets.get(nodeOffset);
if(offset == null) {
offset = new Vector3f(x, y, z);
nodeOffsets.put(nodeOffset, offset);
} else {
offset.set(x, y, z);
}
if(callback != null) {
try {
callback.updateNodeOffset(nodeOffset, offset);
} catch(Exception e) {
LogManager.log.severe("[SkeletonConfig] Exception while calling callback", e);
}
}
}
protected void setNodeOffset(SkeletonNodeOffset nodeOffset, Vector3f offset) {
if(offset == null) {
setNodeOffset(nodeOffset, 0f, 0f, 0f);
return;
}
setNodeOffset(nodeOffset, offset.x, offset.y, offset.z);
}
public Vector3f getNodeOffset(SkeletonNodeOffset nodeOffset) {
return nodeOffsets.getOrDefault(nodeOffset, Vector3f.ZERO);
}
public void computeNodeOffset(SkeletonNodeOffset nodeOffset) {
switch(nodeOffset) {
case HEAD:
setNodeOffset(nodeOffset, 0, 0, getConfig(SkeletonConfigValue.HEAD));
break;
case NECK:
setNodeOffset(nodeOffset, 0, -getConfig(SkeletonConfigValue.NECK), 0);
break;
case CHEST:
setNodeOffset(nodeOffset, 0, -getConfig(SkeletonConfigValue.CHEST), 0);
break;
case CHEST_TRACKER:
setNodeOffset(nodeOffset, 0, 0, -getConfig(SkeletonConfigValue.SKELETON_OFFSET));
break;
case WAIST:
setNodeOffset(nodeOffset, 0, (getConfig(SkeletonConfigValue.CHEST) - getConfig(SkeletonConfigValue.TORSO) + getConfig(SkeletonConfigValue.WAIST)), 0);
break;
case HIP:
setNodeOffset(nodeOffset, 0, -getConfig(SkeletonConfigValue.WAIST), 0);
break;
case HIP_TRACKER:
setNodeOffset(nodeOffset, 0, getConfig(SkeletonConfigValue.HIP_OFFSET), -getConfig(SkeletonConfigValue.SKELETON_OFFSET));
break;
case LEFT_HIP:
setNodeOffset(nodeOffset, -getConfig(SkeletonConfigValue.HIPS_WIDTH) / 2f, 0, 0);
break;
case RIGHT_HIP:
setNodeOffset(nodeOffset, getConfig(SkeletonConfigValue.HIPS_WIDTH) / 2f, 0, 0);
break;
case KNEE:
setNodeOffset(nodeOffset, 0, -(getConfig(SkeletonConfigValue.LEGS_LENGTH) - getConfig(SkeletonConfigValue.KNEE_HEIGHT)), 0);
break;
case KNEE_TRACKER:
setNodeOffset(nodeOffset, 0, 0, -getConfig(SkeletonConfigValue.SKELETON_OFFSET));
break;
case ANKLE:
setNodeOffset(nodeOffset, 0, -getConfig(SkeletonConfigValue.KNEE_HEIGHT), -getConfig(SkeletonConfigValue.FOOT_OFFSET));
break;
case FOOT:
setNodeOffset(nodeOffset, 0, 0, -getConfig(SkeletonConfigValue.FOOT_LENGTH));
break;
case FOOT_TRACKER:
setNodeOffset(nodeOffset, 0, 0, -getConfig(SkeletonConfigValue.SKELETON_OFFSET));
break;
}
}
public void computeAllNodeOffsets() {
for(SkeletonNodeOffset offset : SkeletonNodeOffset.values) {
computeNodeOffset(offset);
}
}
public void setConfigs(Map<SkeletonConfigValue, Float> configs, Map<SkeletonConfigToggle, Boolean> toggles) {
if(configs != null) {
configs.forEach((key, value) -> {
// Do not recalculate the offsets, these are done in bulk at the end
setConfig(key, value, false);
});
}
if(toggles != null) {
toggles.forEach(this::setToggle);
}
if(autoUpdateOffsets) {
computeAllNodeOffsets();
}
}
public void setStringConfigs(Map<String, Float> configs, Map<String, Boolean> toggles) {
if(configs != null) {
configs.forEach((key, value) -> {
// Do not recalculate the offsets, these are done in bulk at the end
setConfig(SkeletonConfigValue.getByStringValue(key), value, false);
});
}
if(toggles != null) {
toggles.forEach((key, value) -> {
setToggle(SkeletonConfigToggle.getByStringValue(key), value);
});
}
if(autoUpdateOffsets) {
computeAllNodeOffsets();
}
}
public void setConfigs(SkeletonConfig skeletonConfig) {
setConfigs(skeletonConfig.configs, skeletonConfig.toggles);
}
//#region Cast utilities for config reading
private static Float castFloat(Object o) {
if(o == null) {
return null;
} else if(o instanceof Float) {
return (Float) o;
} else if(o instanceof Double) {
return ((Double) o).floatValue();
} else if(o instanceof Byte) {
return (float) (Byte) o;
} else if(o instanceof Integer) {
return (float) (Integer) o;
} else if(o instanceof Long) {
return (float) (Long) o;
} else {
return null;
}
}
private static Boolean castBoolean(Object o) {
if(o == null) {
return null;
} else if(o instanceof Boolean) {
return (Boolean) o;
} else {
return null;
}
}
//#endregion
public void loadFromConfig(YamlFile config) {
for(SkeletonConfigValue configValue : SkeletonConfigValue.values) {
Float val = castFloat(config.getProperty(configValue.configKey));
if(val != null) {
// Do not recalculate the offsets, these are done in bulk at the end
setConfig(configValue, val, false);
}
}
for(SkeletonConfigToggle configValue : SkeletonConfigToggle.values) {
Boolean val = castBoolean(config.getProperty(configValue.configKey));
if(val != null) {
setToggle(configValue, val);
}
}
if(autoUpdateOffsets) {
computeAllNodeOffsets();
}
}
public void saveToConfig(YamlFile config) {
// Write all possible values, this keeps configs consistent even if defaults were changed
for(SkeletonConfigValue value : SkeletonConfigValue.values) {
config.setProperty(value.configKey, getConfig(value));
}
for(SkeletonConfigToggle value : SkeletonConfigToggle.values) {
config.setProperty(value.configKey, getToggle(value));
}
}
public void resetConfigs() {
configs.clear();
toggles.clear();
callCallbackOnAll(false);
if(autoUpdateOffsets) {
computeAllNodeOffsets();
}
}
}

View File

@@ -0,0 +1,12 @@
package dev.slimevr.vr.processor.skeleton;
import com.jme3.math.Vector3f;
public interface SkeletonConfigCallback {
public void updateConfigState(SkeletonConfigValue config, float newValue);
public void updateToggleState(SkeletonConfigToggle configToggle, boolean newValue);
public void updateNodeOffset(SkeletonNodeOffset nodeOffset, Vector3f offset);
}

View File

@@ -0,0 +1,38 @@
package dev.slimevr.vr.processor.skeleton;
import java.util.HashMap;
import java.util.Map;
public enum SkeletonConfigToggle {
EXTENDED_PELVIS_MODEL("Extended pelvis model", "extendedPelvis", true),
EXTENDED_KNEE_MODEL("Extended knee model", "extendedKnee", false),
;
private static final String CONFIG_PREFIX = "body.model.";
public final String stringVal;
public final String configKey;
public final boolean defaultValue;
public static final SkeletonConfigToggle[] values = values();
private static final Map<String, SkeletonConfigToggle> byStringVal = new HashMap<>();
private SkeletonConfigToggle(String stringVal, String configKey, boolean defaultValue) {
this.stringVal = stringVal;
this.configKey = CONFIG_PREFIX + configKey;
this.defaultValue = defaultValue;
}
public static SkeletonConfigToggle getByStringValue(String stringVal) {
return stringVal == null ? null : byStringVal.get(stringVal.toLowerCase());
}
static {
for(SkeletonConfigToggle configVal : values()) {
byStringVal.put(configVal.stringVal.toLowerCase(), configVal);
}
}
}

View File

@@ -0,0 +1,54 @@
package dev.slimevr.vr.processor.skeleton;
import java.util.HashMap;
import java.util.Map;
public enum SkeletonConfigValue {
HEAD("Head", "headShift", "Head shift", 0.1f, new SkeletonNodeOffset[]{SkeletonNodeOffset.HEAD}),
NECK("Neck", "neckLength", "Neck length", 0.1f, new SkeletonNodeOffset[]{SkeletonNodeOffset.NECK}),
TORSO("Torso", "torsoLength", "Torso length", 0.6f, new SkeletonNodeOffset[]{SkeletonNodeOffset.WAIST}),
CHEST("Chest", "chestDistance", "Chest distance", 0.3f, new SkeletonNodeOffset[]{SkeletonNodeOffset.CHEST, SkeletonNodeOffset.WAIST}),
WAIST("Waist", "waistDistance", "Waist distance", 0.05f, new SkeletonNodeOffset[]{SkeletonNodeOffset.WAIST, SkeletonNodeOffset.HIP}),
HIP_OFFSET("Hip offset", "hipOffset", "Hip offset", 0.0f, new SkeletonNodeOffset[]{SkeletonNodeOffset.HIP_TRACKER}),
HIPS_WIDTH("Hips width", "hipsWidth", "Hips width", 0.28f, new SkeletonNodeOffset[]{SkeletonNodeOffset.LEFT_HIP, SkeletonNodeOffset.RIGHT_HIP}),
LEGS_LENGTH("Legs length", "legsLength", "Legs length", 0.88f, new SkeletonNodeOffset[]{SkeletonNodeOffset.KNEE}),
KNEE_HEIGHT("Knee height", "kneeHeight", "Knee height", 0.44f, new SkeletonNodeOffset[]{SkeletonNodeOffset.KNEE, SkeletonNodeOffset.ANKLE}),
FOOT_LENGTH("Foot length", "footLength", "Foot length", 0.05f, new SkeletonNodeOffset[]{SkeletonNodeOffset.FOOT}),
FOOT_OFFSET("Foot offset", "footOffset", "Foot offset", 0.0f, new SkeletonNodeOffset[]{SkeletonNodeOffset.ANKLE}),
SKELETON_OFFSET("Skeleton offset", "skeletonOffset", "Skeleton offset", 0.0f, new SkeletonNodeOffset[]{SkeletonNodeOffset.CHEST_TRACKER, SkeletonNodeOffset.HIP_TRACKER, SkeletonNodeOffset.KNEE_TRACKER, SkeletonNodeOffset.FOOT_TRACKER}),
;
private static final String CONFIG_PREFIX = "body.";
public final String stringVal;
public final String configKey;
public final String label;
public final float defaultValue;
public final SkeletonNodeOffset[] affectedOffsets;
public static final SkeletonConfigValue[] values = values();
private static final Map<String, SkeletonConfigValue> byStringVal = new HashMap<>();
private SkeletonConfigValue(String stringVal, String configKey, String label, float defaultValue, SkeletonNodeOffset[] affectedOffsets) {
this.stringVal = stringVal;
this.configKey = CONFIG_PREFIX + configKey;
this.label = label;
this.defaultValue = defaultValue;
this.affectedOffsets = affectedOffsets == null ? new SkeletonNodeOffset[0] : affectedOffsets;
}
public static SkeletonConfigValue getByStringValue(String stringVal) {
return stringVal == null ? null : byStringVal.get(stringVal.toLowerCase());
}
static {
for(SkeletonConfigValue configVal : values()) {
byStringVal.put(configVal.stringVal.toLowerCase(), configVal);
}
}
}

View File

@@ -0,0 +1,22 @@
package dev.slimevr.vr.processor.skeleton;
public enum SkeletonNodeOffset {
HEAD,
NECK,
CHEST,
CHEST_TRACKER,
WAIST,
HIP,
HIP_TRACKER,
LEFT_HIP,
RIGHT_HIP,
KNEE,
KNEE_TRACKER,
ANKLE,
FOOT,
FOOT_TRACKER,
;
public static final SkeletonNodeOffset[] values = values();
}

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import java.util.function.Consumer;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public enum DeviceType {
HMD,

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import io.eiren.util.BufferedTimer;

View File

@@ -1,22 +1,22 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.math.FloatMath;
import dev.slimevr.vr.trackers.udp.TrackersUDPServer;
import io.eiren.util.BufferedTimer;
public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
public static final float MAX_MAG_CORRECTION_ACCURACY = 5 * FastMath.RAD_TO_DEG;
public final Vector3f gyroVector = new Vector3f();
public final Vector3f accelVector = new Vector3f();
//public final Vector3f gyroVector = new Vector3f();
//public final Vector3f accelVector = new Vector3f();
public final Vector3f magVector = new Vector3f();
public final Quaternion rotQuaternion = new Quaternion();
public final Quaternion rotMagQuaternion = new Quaternion();
protected final Quaternion rotAdjust = new Quaternion();
public final Quaternion rotAdjust = new Quaternion();
protected final Quaternion correction = new Quaternion();
protected TrackerMountingRotation mounting = null;
protected TrackerStatus status = TrackerStatus.OK;
@@ -27,6 +27,7 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
protected final TrackersUDPServer server;
protected float confidence = 0;
protected float batteryVoltage = 0;
protected float batteryLevel = 0;
public int calibrationStatus = 0;
public int magCalibrationStatus = 0;
public float magnetometerAccuracy = 0;
@@ -35,9 +36,9 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
protected BufferedTimer timer = new BufferedTimer(1f);
public int ping = -1;
public int signalStrength = -1;
public float temperature = 0;
public StringBuilder serialBuffer = new StringBuilder();
long lastSerialUpdate = 0;
public TrackerPosition bodyPosition = null;
public IMUTracker(int trackerId, String name, String descriptiveName, TrackersUDPServer server) {
@@ -149,7 +150,7 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
@Override
public float getBatteryLevel() {
return FloatMath.mapValue(getBatteryVoltage(), 3.6f, 4.2f, 0f, 1f);
return batteryLevel;
}
@Override
@@ -157,6 +158,10 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
return batteryVoltage;
}
public void setBatteryLevel(float level) {
this.batteryLevel = level;
}
public void setBatteryVoltage(float voltage) {
this.batteryVoltage = voltage;
}

View File

@@ -1,7 +1,9 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import java.nio.ByteBuffer;
import dev.slimevr.vr.trackers.udp.TrackersUDPServer;
public class MPUTracker extends IMUTracker {
public ConfigurationData newCalibrationData;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;

View File

@@ -1,10 +1,10 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public class BnoTap {
public class SensorTap {
public final boolean doubleTap;
public BnoTap(int tapBits) {
public SensorTap(int tapBits) {
doubleTap = (tapBits & 0x40) > 0;
}

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public interface ShareableTracker extends Tracker {

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import java.util.concurrent.atomic.AtomicInteger;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import com.jme3.math.Quaternion;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import java.util.EnumMap;
import java.util.HashMap;
@@ -10,6 +10,7 @@ public enum TrackerPosition {
HMD("HMD", TrackerRole.HMD),
CHEST("body:chest", TrackerRole.CHEST),
WAIST("body:waist", TrackerRole.WAIST),
HIP("body:hip", null),
LEFT_LEG("body:left_leg", TrackerRole.LEFT_KNEE),
RIGHT_LEG("body:right_leg", TrackerRole.RIGHT_KNEE),
LEFT_ANKLE("body:left_ankle", null),

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public enum TrackerRole {

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public enum TrackerStatus {

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import java.util.List;
@@ -8,6 +8,8 @@ public class TrackerUtils {
}
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerPosition position) {
if(position == null)
return null;
for(int i = 0; i < allTrackers.length; ++i) {
T t = allTrackers[i];
if(t != null && t.getBodyPosition() == position)
@@ -17,6 +19,8 @@ public class TrackerUtils {
}
public static <T extends Tracker> T findTrackerForBodyPosition(List<T> allTrackers, TrackerPosition position) {
if(position == null)
return null;
for(int i = 0; i < allTrackers.size(); ++i) {
T t = allTrackers.get(i);
if(t != null && t.getBodyPosition() == position)
@@ -32,18 +36,24 @@ public class TrackerUtils {
return findTrackerForBodyPosition(allTrackers, altPosition);
}
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerPosition position, TrackerPosition altPosition) {
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerPosition position, TrackerPosition altPosition, TrackerPosition secondAltPosition) {
T t = findTrackerForBodyPosition(allTrackers, position);
if(t != null)
return t;
return findTrackerForBodyPosition(allTrackers, altPosition);
t = findTrackerForBodyPosition(allTrackers, altPosition);
if(t != null)
return t;
return findTrackerForBodyPosition(allTrackers, secondAltPosition);
}
public static Tracker findTrackerForBodyPositionOrEmpty(List<? extends Tracker> allTrackers, TrackerPosition position, TrackerPosition altPosition) {
public static Tracker findTrackerForBodyPositionOrEmpty(List<? extends Tracker> allTrackers, TrackerPosition position, TrackerPosition altPosition, TrackerPosition secondAltPosition) {
Tracker t = findTrackerForBodyPosition(allTrackers, position);
if(t != null)
return t;
t = findTrackerForBodyPosition(allTrackers, altPosition);
if(t != null)
return t;
t = findTrackerForBodyPosition(allTrackers, secondAltPosition);
if(t != null)
return t;
return new ComputedTracker(Tracker.getNextLocalTrackerId(), "Empty tracker", false, false);

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public interface TrackerWithBattery {

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
public interface TrackerWithTPS {

View File

@@ -1,4 +1,4 @@
package io.eiren.vr.trackers;
package dev.slimevr.vr.trackers;
import io.eiren.util.BufferedTimer;

View File

@@ -0,0 +1,15 @@
package dev.slimevr.vr.trackers.udp;
public interface SensorSpecificPacket {
public int getSensorId();
/**
* Sensor with id 255 is "global" representing a whole device
* @param sensorId
* @return
*/
public static boolean isGlobal(int sensorId) {
return sensorId == 255;
}
}

View File

@@ -0,0 +1,43 @@
package dev.slimevr.vr.trackers.udp;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.Map;
import dev.slimevr.NetworkProtocol;
import dev.slimevr.vr.trackers.IMUTracker;
public class TrackerUDPConnection {
public Map<Integer, IMUTracker> sensors = new HashMap<>();
public SocketAddress address;
public InetAddress ipAddress;
public long lastPacket = System.currentTimeMillis();
public int lastPingPacketId = -1;
public long lastPingPacketTime = 0;
public String name;
public String descriptiveName;
public StringBuilder serialBuffer = new StringBuilder();
public long lastSerialUpdate = 0;
public long lastPacketNumber = -1;
public NetworkProtocol protocol = null;
public int firmwareBuild = 0;
public TrackerUDPConnection(SocketAddress address, InetAddress ipAddress) {
this.address = address;
this.ipAddress = ipAddress;
}
public boolean isNextPacket(long packetId) {
if(packetId != 0 && packetId <= lastPacketNumber)
return false;
lastPacketNumber = packetId;
return true;
}
@Override
public String toString() {
return "udp:/" + ipAddress;
}
}

View File

@@ -0,0 +1,429 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Consumer;
import org.apache.commons.lang3.ArrayUtils;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import dev.slimevr.NetworkProtocol;
import dev.slimevr.vr.trackers.IMUTracker;
import dev.slimevr.vr.trackers.ReferenceAdjustedTracker;
import dev.slimevr.vr.trackers.Tracker;
import dev.slimevr.vr.trackers.TrackerStatus;
import io.eiren.util.Util;
import io.eiren.util.collections.FastList;
import io.eiren.util.logging.LogManager;
/**
* Recieves trackers data by UDP using extended owoTrack protocol.
*/
public class TrackersUDPServer extends Thread {
/**
* Change between IMU axises and OpenGL/SteamVR axises
*/
private static final Quaternion offset = new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
private final Quaternion buf = new Quaternion();
private final Random random = new Random();
private final List<TrackerUDPConnection> connections = new FastList<>();
private final Map<InetAddress, TrackerUDPConnection> connectionsByAddress = new HashMap<>();
private final Map<String, TrackerUDPConnection> connectionsByMAC = new HashMap<>();
private final Consumer<Tracker> trackersConsumer;
private final int port;
private final ArrayList<SocketAddress> broadcastAddresses = new ArrayList<>();
private final UDPProtocolParser parser = new UDPProtocolParser();
private final byte[] rcvBuffer = new byte[512];
private final ByteBuffer bb = ByteBuffer.wrap(rcvBuffer).order(ByteOrder.BIG_ENDIAN);
protected DatagramSocket socket = null;
protected long lastKeepup = System.currentTimeMillis();
public TrackersUDPServer(int port, String name, Consumer<Tracker> trackersConsumer) {
super(name);
this.port = port;
this.trackersConsumer = trackersConsumer;
try {
Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
while(ifaces.hasMoreElements()) {
NetworkInterface iface = ifaces.nextElement();
// Ignore loopback, PPP, virtual and disabled devices
if(iface.isLoopback() || !iface.isUp() || iface.isPointToPoint() || iface.isVirtual()) {
continue;
}
Enumeration<InetAddress> iaddrs = iface.getInetAddresses();
while(iaddrs.hasMoreElements()) {
InetAddress iaddr = iaddrs.nextElement();
// Ignore IPv6 addresses
if(iaddr instanceof Inet6Address) {
continue;
}
String[] iaddrParts = iaddr.getHostAddress().split("\\.");
broadcastAddresses.add(new InetSocketAddress(String.format("%s.%s.%s.255", iaddrParts[0], iaddrParts[1], iaddrParts[2]), port));
}
}
} catch(Exception e) {
LogManager.log.severe("[TrackerServer] Can't enumerate network interfaces", e);
}
}
private void setUpNewConnection(DatagramPacket handshakePacket, UDPPacket3Handshake handshake) throws IOException {
LogManager.log.info("[TrackerServer] Handshake recieved from " + handshakePacket.getAddress() + ":" + handshakePacket.getPort());
InetAddress addr = handshakePacket.getAddress();
TrackerUDPConnection connection;
synchronized(connections) {
connection = connectionsByAddress.get(addr);
}
if(connection == null) {
connection = new TrackerUDPConnection(handshakePacket.getSocketAddress(), addr);
connection.firmwareBuild = handshake.firmwareBuild;
if(handshake.firmware == null || handshake.firmware.length() == 0) {
// Only old owoTrack doesn't report firmware and have differenet packet IDs with SlimeVR
connection.protocol = NetworkProtocol.OWO_LEGACY;
} else {
connection.protocol = NetworkProtocol.SLIMEVR_RAW;
}
connection.name = handshake.macString != null ? "udp://" + handshake.macString : "udp:/" + handshakePacket.getAddress().toString();
connection.descriptiveName = "udp:/" + handshakePacket.getAddress().toString();
int i = 0;
synchronized(connections) {
if(handshake.macString != null && connectionsByMAC.containsKey(handshake.macString)) {
TrackerUDPConnection previousConnection = connectionsByMAC.get(handshake.macString);
i = connections.indexOf(previousConnection);
connectionsByAddress.remove(previousConnection.ipAddress);
previousConnection.lastPacketNumber = 0;
previousConnection.ipAddress = addr;
previousConnection.address = handshakePacket.getSocketAddress();
previousConnection.name = connection.name;
previousConnection.descriptiveName = connection.descriptiveName;
connectionsByAddress.put(addr, previousConnection);
LogManager.log.info("[TrackerServer] Tracker " + i + " handed over to address " + handshakePacket.getSocketAddress() + ". Board type: " + handshake.boardType + ", imu type: " + handshake.imuType + ", firmware: " + handshake.firmware + " (" + connection.firmwareBuild + "), mac: " + handshake.macString + ", name: " + previousConnection.name);
} else {
i = connections.size();
connections.add(connection);
connectionsByAddress.put(addr, connection);
if(handshake.macString != null) {
connectionsByMAC.put(handshake.macString, connection);
}
LogManager.log.info("[TrackerServer] Tracker " + i + " added with address " + handshakePacket.getSocketAddress() + ". Board type: " + handshake.boardType + ", imu type: " + handshake.imuType + ", firmware: " + handshake.firmware + " (" + connection.firmwareBuild + "), mac: " + handshake.macString + ", name: " + connection.name);
}
}
if(connection.protocol == NetworkProtocol.OWO_LEGACY || connection.firmwareBuild < 8) {
// Set up new sensor for older firmware
// Firmware after 7 should send sensor status packet and sensor will be created when it's received
setUpSensor(connection, 0, handshake.imuType, 1);
}
}
bb.limit(bb.capacity());
bb.rewind();
parser.writeHandshakeResponse(bb, connection);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), connection.address));
}
private void setUpSensor(TrackerUDPConnection connection, int trackerId, int sensorType, int sensorStatus) throws IOException {
LogManager.log.info("[TrackerServer] Sensor " + trackerId + " for " + connection.name + " status: " + sensorStatus);
IMUTracker imu = connection.sensors.get(trackerId);
if(imu == null) {
imu = new IMUTracker(Tracker.getNextLocalTrackerId(), connection.name + "/" + trackerId, connection.descriptiveName + "/" + trackerId, this);
connection.sensors.put(trackerId, imu);
ReferenceAdjustedTracker<IMUTracker> adjustedTracker = new ReferenceAdjustedTracker<>(imu);
trackersConsumer.accept(adjustedTracker);
LogManager.log.info("[TrackerServer] Added sensor " + trackerId + " for " + connection.name + ", type " + sensorType);
}
TrackerStatus status = UDPPacket15SensorInfo.getStatus(sensorStatus);
if(status != null)
imu.setStatus(status);
}
@Override
public void run() {
StringBuilder serialBuffer2 = new StringBuilder();
try {
socket = new DatagramSocket(port);
long prevPacketTime = System.currentTimeMillis();
socket.setSoTimeout(250);
while(true) {
DatagramPacket received = null;
try {
boolean hasActiveTrackers = false;
for(TrackerUDPConnection tracker : connections) {
if(tracker.sensors.size() > 0) {
hasActiveTrackers = true;
break;
}
}
if(!hasActiveTrackers) {
long discoveryPacketTime = System.currentTimeMillis();
if((discoveryPacketTime - prevPacketTime) >= 2000) {
for(SocketAddress addr : broadcastAddresses) {
bb.limit(bb.capacity());
bb.rewind();
parser.write(bb, null, new UDPPacket0Heartbeat());
socket.send(new DatagramPacket(rcvBuffer, bb.position(), addr));
}
prevPacketTime = discoveryPacketTime;
}
}
received = new DatagramPacket(rcvBuffer, rcvBuffer.length);
socket.receive(received);
bb.limit(received.getLength());
bb.rewind();
TrackerUDPConnection connection;
synchronized(connections) {
connection = connectionsByAddress.get(received.getAddress());
}
UDPPacket packet = parser.parse(bb, connection);
if(packet != null) {
processPacket(received, packet, connection);
}
} catch(SocketTimeoutException e) {
} catch(Exception e) {
LogManager.log.warning("Error parsing packet " + packetToString(received), e);
}
if(lastKeepup + 500 < System.currentTimeMillis()) {
lastKeepup = System.currentTimeMillis();
synchronized(connections) {
for(int i = 0; i < connections.size(); ++i) {
TrackerUDPConnection conn = connections.get(i);
bb.limit(bb.capacity());
bb.rewind();
parser.write(bb, conn, new UDPPacket1Heartbeat());
socket.send(new DatagramPacket(rcvBuffer, bb.position(), conn.address));
if(conn.lastPacket + 1000 < System.currentTimeMillis()) {
Iterator<IMUTracker> iterator = conn.sensors.values().iterator();
while(iterator.hasNext()) {
IMUTracker tracker = iterator.next();
if(tracker.getStatus() == TrackerStatus.OK)
tracker.setStatus(TrackerStatus.DISCONNECTED);
}
} else {
Iterator<IMUTracker> iterator = conn.sensors.values().iterator();
while(iterator.hasNext()) {
IMUTracker tracker = iterator.next();
if(tracker.getStatus() == TrackerStatus.DISCONNECTED)
tracker.setStatus(TrackerStatus.OK);
}
}
if(conn.serialBuffer.length() > 0) {
if(conn.lastSerialUpdate + 500L < System.currentTimeMillis()) {
serialBuffer2.append('[').append(conn.name).append("] ").append(conn.serialBuffer);
System.out.println(serialBuffer2.toString());
serialBuffer2.setLength(0);
conn.serialBuffer.setLength(0);
}
}
if(conn.lastPingPacketTime + 500 < System.currentTimeMillis()) {
conn.lastPingPacketId = random.nextInt();
conn.lastPingPacketTime = System.currentTimeMillis();
bb.limit(bb.capacity());
bb.rewind();
bb.putInt(10);
bb.putLong(0);
bb.putInt(conn.lastPingPacketId);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), conn.address));
}
}
}
}
}
} catch(Exception e) {
e.printStackTrace();
} finally {
Util.close(socket);
}
}
protected void processPacket(DatagramPacket received, UDPPacket packet, TrackerUDPConnection connection) throws IOException {
IMUTracker tracker = null;
switch(packet.getPacketId()) {
case UDPProtocolParser.PACKET_HEARTBEAT:
break;
case UDPProtocolParser.PACKET_HANDSHAKE:
setUpNewConnection(received, (UDPPacket3Handshake) packet);
break;
case UDPProtocolParser.PACKET_ROTATION:
case UDPProtocolParser.PACKET_ROTATION_2:
if(connection == null)
break;
UDPPacket1Rotation rotationPacket = (UDPPacket1Rotation) packet;
buf.set(rotationPacket.rotation);
offset.mult(buf, buf);
tracker = connection.sensors.get(rotationPacket.getSensorId());
if(tracker == null)
break;
tracker.rotQuaternion.set(buf);
tracker.dataTick();
break;
case UDPProtocolParser.PACKET_ROTATION_DATA:
if(connection == null)
break;
UDPPacket17RotationData rotationData = (UDPPacket17RotationData) packet;
tracker = connection.sensors.get(rotationData.getSensorId());
if(tracker == null)
break;
buf.set(rotationData.rotation);
offset.mult(buf, buf);
switch(rotationData.dataType) {
case UDPPacket17RotationData.DATA_TYPE_NORMAL:
tracker.rotQuaternion.set(buf);
tracker.calibrationStatus = rotationData.calibrationInfo;
tracker.dataTick();
break;
case UDPPacket17RotationData.DATA_TYPE_CORRECTION:
tracker.rotMagQuaternion.set(buf);
tracker.magCalibrationStatus = rotationData.calibrationInfo;
tracker.hasNewCorrectionData = true;
break;
}
break;
case UDPProtocolParser.PACKET_MAGNETOMETER_ACCURACY:
if(connection == null)
break;
UDPPacket18MagnetometerAccuracy magAccuracy = (UDPPacket18MagnetometerAccuracy) packet;
tracker = connection.sensors.get(magAccuracy.getSensorId());
if(tracker == null)
break;
tracker.magnetometerAccuracy = magAccuracy.accuracyInfo;
break;
case 2: // PACKET_GYRO
case 4: // PACKET_ACCEL
case 5: // PACKET_MAG
case 9: // PACKET_RAW_MAGENTOMETER
break; // None of these packets are used by SlimeVR trackers and are deprecated, use more generic PACKET_ROTATION_DATA
case 8: // PACKET_CONFIG
if(connection == null)
break;
break;
case UDPProtocolParser.PACKET_PING_PONG: // PACKET_PING_PONG:
if(connection == null)
break;
UDPPacket10PingPong ping = (UDPPacket10PingPong) packet;
if(connection.lastPingPacketId == ping.pingId) {
for(int i = 0; i < connection.sensors.size(); ++i) {
tracker = connection.sensors.get(i);
tracker.ping = (int) (System.currentTimeMillis() - connection.lastPingPacketTime) / 2;
tracker.dataTick();
}
} else {
LogManager.log.debug("[TrackerServer] Wrog ping id " + ping.pingId + " != " + connection.lastPingPacketId);
}
break;
case UDPProtocolParser.PACKET_SERIAL:
if(connection == null)
break;
UDPPacket11Serial serial = (UDPPacket11Serial) packet;
System.out.println("[" + connection.name + "] " + serial.serial);
break;
case UDPProtocolParser.PACKET_BATTERY_LEVEL:
if(connection == null)
break;
UDPPacket12BatteryLevel battery = (UDPPacket12BatteryLevel) packet;
if(connection.sensors.size() > 0) {
Collection<IMUTracker> trackers = connection.sensors.values();
Iterator<IMUTracker> iterator = trackers.iterator();
while(iterator.hasNext()) {
IMUTracker tr = iterator.next();
tr.setBatteryVoltage(battery.voltage);
tr.setBatteryLevel(battery.level * 100);
}
}
break;
case UDPProtocolParser.PACKET_TAP:
if(connection == null)
break;
UDPPacket13Tap tap = (UDPPacket13Tap) packet;
tracker = connection.sensors.get(tap.getSensorId());
if(tracker == null)
break;
LogManager.log.info("[TrackerServer] Tap packet received from " + tracker.getName() + ": " + tap.tap);
break;
case UDPProtocolParser.PACKET_ERROR:
UDPPacket14Error error = (UDPPacket14Error) packet;
LogManager.log.severe("[TrackerServer] Error recieved from " + received.getSocketAddress() + ": " + error.errorNumber);
if(connection == null)
break;
tracker = connection.sensors.get(error.getSensorId());
if(tracker == null)
break;
tracker.setStatus(TrackerStatus.ERROR);
break;
case UDPProtocolParser.PACKET_SENSOR_INFO:
if(connection == null)
break;
UDPPacket15SensorInfo info = (UDPPacket15SensorInfo) packet;
setUpSensor(connection, info.getSensorId(), info.sensorType, info.sensorStatus);
// Send ack
bb.limit(bb.capacity());
bb.rewind();
parser.writeSensorInfoResponse(bb, connection, info);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), connection.address));
LogManager.log.info("[TrackerServer] Sensor info for " + connection.descriptiveName + "/" + info.getSensorId() + ": " + info.sensorStatus);
break;
case UDPProtocolParser.PACKET_SIGNAL_STRENGTH:
if(connection == null)
break;
UDPPacket19SignalStrength signalStrength = (UDPPacket19SignalStrength) packet;
if(connection.sensors.size() > 0) {
Collection<IMUTracker> trackers = connection.sensors.values();
Iterator<IMUTracker> iterator = trackers.iterator();
while(iterator.hasNext()) {
IMUTracker tr = iterator.next();
tr.signalStrength = signalStrength.signalStrength;
}
}
break;
case UDPProtocolParser.PACKET_TEMPERATURE:
if(connection == null)
break;
UDPPacket20Temperature temp = (UDPPacket20Temperature) packet;
tracker = connection.sensors.get(temp.getSensorId());
if(tracker == null)
break;
tracker.temperature = temp.temperature;
break;
default:
LogManager.log.warning("[TrackerServer] Skipped packet " + packet);
break;
}
}
private static String packetToString(DatagramPacket packet) {
StringBuilder sb = new StringBuilder();
sb.append("DatagramPacket{");
sb.append(packet.getAddress().toString());
sb.append(packet.getPort());
sb.append(',');
sb.append(packet.getLength());
sb.append(',');
sb.append(ArrayUtils.toString(packet.getData()));
sb.append('}');
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public abstract class UDPPacket {
public abstract int getPacketId();
public abstract void readData(ByteBuffer buf) throws IOException;
public abstract void writeData(ByteBuffer buf) throws IOException;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append('{');
sb.append(getPacketId());
if(this instanceof SensorSpecificPacket) {
sb.append(",sensor:");
sb.append(((SensorSpecificPacket) this).getSensorId());
}
sb.append('}');
return sb.toString();
}
/**
* Naively read null-terminated ASCII string from the byte buffer
* @param buf
* @return
* @throws IOException
*/
public static String readASCIIString(ByteBuffer buf) throws IOException {
StringBuilder sb = new StringBuilder();
while(true) {
char c = (char) (buf.get() & 0xFF);
if(c == 0)
break;
sb.append(c);
}
return sb.toString();
}
public static String readASCIIString(ByteBuffer buf, int length) throws IOException {
StringBuilder sb = new StringBuilder();
while(length-- > 0) {
char c = (char) (buf.get() & 0xFF);
if(c == 0)
break;
sb.append(c);
}
return sb.toString();
}
/**
* Naively write null-terminated ASCII string to byte buffer
* @param str
* @param buf
* @throws IOException
*/
public static void writeASCIIString(String str, ByteBuffer buf) throws IOException {
for(int i = 0; i < str.length(); ++i) {
char c = str.charAt(i);
buf.put((byte) (c & 0xFF));
}
buf.put((byte) 0);
}
}

View File

@@ -0,0 +1,25 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket0Heartbeat extends UDPPacket {
public UDPPacket0Heartbeat() {
}
@Override
public int getPacketId() {
return 0;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
// Empty packet
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Empty packet
}
}

View File

@@ -0,0 +1,32 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket10PingPong extends UDPPacket {
public int pingId;
public UDPPacket10PingPong() {
}
public UDPPacket10PingPong(int pingId) {
this.pingId = pingId;
}
@Override
public int getPacketId() {
return 10;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
pingId = buf.getInt();
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
buf.putInt(pingId);
}
}

View File

@@ -0,0 +1,34 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket11Serial extends UDPPacket {
public String serial;
public UDPPacket11Serial() {
}
@Override
public int getPacketId() {
return 11;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
int length = buf.getInt();
StringBuilder sb = new StringBuilder(length);
for(int i = 0; i < length; ++i) {
char ch = (char) buf.get();
sb.append(ch);
}
serial = sb.toString();
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
}

View File

@@ -0,0 +1,30 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket12BatteryLevel extends UDPPacket {
public float voltage;
public float level;
public UDPPacket12BatteryLevel() {
}
@Override
public int getPacketId() {
return 12;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
voltage = buf.getFloat();
if(buf.remaining() > 3)
level = buf.getFloat();
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
}

View File

@@ -0,0 +1,36 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
import dev.slimevr.vr.trackers.SensorTap;
public class UDPPacket13Tap extends UDPPacket implements SensorSpecificPacket {
public int sensorId;
public SensorTap tap;
public UDPPacket13Tap() {
}
@Override
public int getPacketId() {
return 13;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
tap = new SensorTap(buf.get() & 0xFF);
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
}

View File

@@ -0,0 +1,35 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket14Error extends UDPPacket implements SensorSpecificPacket {
public int sensorId;
public int errorNumber;
public UDPPacket14Error() {
}
@Override
public int getPacketId() {
return 14;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
errorNumber = buf.get() & 0xFF;
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
}

View File

@@ -0,0 +1,51 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
import dev.slimevr.vr.trackers.TrackerStatus;
public class UDPPacket15SensorInfo extends UDPPacket implements SensorSpecificPacket {
public int sensorId;
public int sensorStatus;
public int sensorType;
public UDPPacket15SensorInfo() {
}
@Override
public int getPacketId() {
return 15;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
sensorStatus = buf.get() & 0xFF;
sensorType = buf.get() & 0xFF;
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
public static TrackerStatus getStatus(int sensorStatus) {
switch(sensorStatus) {
case 0:
return TrackerStatus.DISCONNECTED;
case 1:
return TrackerStatus.OK;
case 2:
return TrackerStatus.ERROR;
}
return null;
}
}

View File

@@ -0,0 +1,17 @@
package dev.slimevr.vr.trackers.udp;
public class UDPPacket16Rotation2 extends UDPPacket1Rotation {
public UDPPacket16Rotation2() {
}
@Override
public int getPacketId() {
return 16;
}
@Override
public int getSensorId() {
return 2;
}
}

View File

@@ -0,0 +1,43 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.jme3.math.Quaternion;
public class UDPPacket17RotationData extends UDPPacket implements SensorSpecificPacket {
public static final int DATA_TYPE_NORMAL = 1;
public static final int DATA_TYPE_CORRECTION = 2;
public int sensorId;
public int dataType;
public final Quaternion rotation = new Quaternion();
public int calibrationInfo;
public UDPPacket17RotationData() {
}
@Override
public int getPacketId() {
return 17;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
dataType = buf.get() & 0xFF;
rotation.set(buf.getFloat(), buf.getFloat(), buf.getFloat(), buf.getFloat());
calibrationInfo = buf.get() & 0xFF;
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
}

View File

@@ -0,0 +1,34 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket18MagnetometerAccuracy extends UDPPacket implements SensorSpecificPacket {
public int sensorId;
public float accuracyInfo;
public UDPPacket18MagnetometerAccuracy() {
}
@Override
public int getPacketId() {
return 18;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
accuracyInfo = buf.getFloat();
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
}

View File

@@ -0,0 +1,34 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket19SignalStrength extends UDPPacket implements SensorSpecificPacket {
public int sensorId;
public int signalStrength;
public UDPPacket19SignalStrength() {
}
@Override
public int getPacketId() {
return 19;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
signalStrength = buf.get();
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
}

View File

@@ -0,0 +1,9 @@
package dev.slimevr.vr.trackers.udp;
public class UDPPacket1Heartbeat extends UDPPacket0Heartbeat {
@Override
public int getPacketId() {
return 1;
}
}

View File

@@ -0,0 +1,34 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.jme3.math.Quaternion;
public class UDPPacket1Rotation extends UDPPacket implements SensorSpecificPacket {
public final Quaternion rotation = new Quaternion();
public UDPPacket1Rotation() {
}
@Override
public int getPacketId() {
return 1;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
rotation.set(buf.getFloat(), buf.getFloat(), buf.getFloat(), buf.getFloat());
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return 0;
}
}

View File

@@ -0,0 +1,31 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket200ProtocolChange extends UDPPacket {
public int targetProtocol;
public int targetProtocolVersion;
public UDPPacket200ProtocolChange() {
}
@Override
public int getPacketId() {
return 200;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
targetProtocol = buf.get() & 0xFF;
targetProtocolVersion = buf.get() & 0xFF;
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
buf.put((byte) targetProtocol);
buf.put((byte) targetProtocolVersion);
}
}

View File

@@ -0,0 +1,35 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket20Temperature extends UDPPacket implements SensorSpecificPacket {
public int sensorId;
public float temperature;
public UDPPacket20Temperature() {
}
@Override
public int getPacketId() {
return 20;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
sensorId = buf.get() & 0xFF;
temperature = buf.getFloat();
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
}
@Override
public int getSensorId() {
return sensorId;
}
}

View File

@@ -0,0 +1,59 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
public class UDPPacket3Handshake extends UDPPacket {
public int boardType;
public int imuType;
public int mcuType;
public int firmwareBuild;
public String firmware;
public String macString;
public UDPPacket3Handshake() {
}
@Override
public int getPacketId() {
return 3;
}
@Override
public void readData(ByteBuffer buf) throws IOException {
if(buf.remaining() > 0) {
byte[] mac = new byte[6];
if(buf.remaining() > 3)
boardType = buf.getInt();
if(buf.remaining() > 3)
imuType = buf.getInt();
if(buf.remaining() > 3)
mcuType = buf.getInt(); // MCU TYPE
if(buf.remaining() > 11) {
buf.getInt(); // IMU info
buf.getInt();
buf.getInt();
}
if(buf.remaining() > 3)
firmwareBuild = buf.getInt();
int length = 0;
if(buf.remaining() > 0)
length = buf.get(); // firmware version length is 1 longer than that because it's nul-terminated
firmware = readASCIIString(buf, length);
if(buf.remaining() >= mac.length) {
buf.get(mac);
macString = String.format("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
if(macString.equals("00:00:00:00:00:00"))
macString = null;
}
}
}
@Override
public void writeData(ByteBuffer buf) throws IOException {
// Never sent back in current protocol
// Handshake for RAW SlimeVR and legacy owoTrack has different packet id byte order from normal packets
// So it's handled by raw protocol call
}
}

View File

@@ -0,0 +1,120 @@
package dev.slimevr.vr.trackers.udp;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import io.eiren.util.logging.LogManager;
public class UDPProtocolParser {
public static final int PACKET_HEARTBEAT = 0;
public static final int PACKET_ROTATION = 1; // Deprecated
//public static final int PACKET_GYRO = 2; // Deprecated
public static final int PACKET_HANDSHAKE = 3;
//public static final int PACKET_ACCEL = 4; // Not parsed by server
//public static final int PACKET_MAG = 5; // Deprecated
//public static final int PACKET_RAW_CALIBRATION_DATA = 6; // Not parsed by server
//public static final int PACKET_CALIBRATION_FINISHED = 7; // Not parsed by server
//public static final int PACKET_CONFIG = 8; // Not parsed by server
//public static final int PACKET_RAW_MAGNETOMETER = 9 // Deprecated
public static final int PACKET_PING_PONG = 10;
public static final int PACKET_SERIAL = 11;
public static final int PACKET_BATTERY_LEVEL = 12;
public static final int PACKET_TAP = 13;
public static final int PACKET_ERROR = 14;
public static final int PACKET_SENSOR_INFO = 15;
public static final int PACKET_ROTATION_2 = 16; // Deprecated
public static final int PACKET_ROTATION_DATA = 17;
public static final int PACKET_MAGNETOMETER_ACCURACY = 18;
public static final int PACKET_SIGNAL_STRENGTH = 19;
public static final int PACKET_TEMPERATURE = 20;
public static final int PACKET_PROTOCOL_CHANGE = 200;
private static final byte[] HANDSHAKE_BUFFER = new byte[64];
public UDPProtocolParser() {
}
public UDPPacket parse(ByteBuffer buf, TrackerUDPConnection connection) throws IOException {
int packetId = buf.getInt();
long packetNumber = buf.getLong();
if(connection != null) {
if(!connection.isNextPacket(packetNumber)) {
// Skip packet because it's not next
throw new IOException("Out of order packet received: id " + packetId + ", number " + packetNumber + ", last " + connection.lastPacketNumber + ", from " + connection);
}
connection.lastPacket = System.currentTimeMillis();
}
UDPPacket newPacket = getNewPacket(packetId);
if(newPacket != null) {
newPacket.readData(buf);
} else {
LogManager.log.debug("[UDPPorotocolParser] Skipped packet id " + packetId + " from " + connection);
}
return newPacket;
}
public void write(ByteBuffer buf, TrackerUDPConnection connection, UDPPacket packet) throws IOException {
buf.putInt(packet.getPacketId());
buf.putLong(0); // Packet number is always 0 when sending data to trackers
packet.writeData(buf);
}
public void writeHandshakeResponse(ByteBuffer buf, TrackerUDPConnection connection) throws IOException {
buf.put(HANDSHAKE_BUFFER);
}
public void writeSensorInfoResponse(ByteBuffer buf, TrackerUDPConnection connection, UDPPacket15SensorInfo packet) throws IOException {
buf.putInt(packet.getPacketId());
buf.put((byte) packet.sensorId);
buf.put((byte) packet.sensorStatus);
}
protected UDPPacket getNewPacket(int packetId) {
switch(packetId) {
case PACKET_HEARTBEAT:
return new UDPPacket0Heartbeat();
case PACKET_ROTATION:
return new UDPPacket1Rotation();
case PACKET_HANDSHAKE:
return new UDPPacket3Handshake();
case PACKET_PING_PONG:
return new UDPPacket10PingPong();
case PACKET_SERIAL:
return new UDPPacket11Serial();
case PACKET_BATTERY_LEVEL:
return new UDPPacket12BatteryLevel();
case PACKET_TAP:
return new UDPPacket13Tap();
case PACKET_ERROR:
return new UDPPacket14Error();
case PACKET_SENSOR_INFO:
return new UDPPacket15SensorInfo();
case PACKET_ROTATION_2:
return new UDPPacket16Rotation2();
case PACKET_ROTATION_DATA:
return new UDPPacket17RotationData();
case PACKET_MAGNETOMETER_ACCURACY:
return new UDPPacket18MagnetometerAccuracy();
case PACKET_SIGNAL_STRENGTH:
return new UDPPacket19SignalStrength();
case PACKET_TEMPERATURE:
return new UDPPacket20Temperature();
case PACKET_PROTOCOL_CHANGE:
return new UDPPacket200ProtocolChange();
}
return null;
}
static {
try {
HANDSHAKE_BUFFER[0] = 3;
byte[] str = "Hey OVR =D 5".getBytes("ASCII");
System.arraycopy(str, 0, HANDSHAKE_BUFFER, 1, str.length);
} catch(UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -1,52 +0,0 @@
package io.eiren.vr;
import com.melloware.jintellitype.JIntellitype;
import com.melloware.jintellitype.HotkeyListener;
import io.eiren.util.ann.AWTThread;
import io.eiren.util.logging.LogManager;
public class Keybinding implements HotkeyListener {
public final VRServer server;
private static final int RESET = 1;
private static final int QUICK_RESET = 2;
@AWTThread
public Keybinding(VRServer server) {
this.server = server;
if(JIntellitype.isJIntellitypeSupported()) {
JIntellitype.getInstance().addHotKeyListener(this);
String resetBinding = this.server.config.getString("keybindings.reset");
if(resetBinding == null) {
resetBinding = "CTRL+ALT+SHIFT+Y";
this.server.config.setProperty("keybindings.reset", resetBinding);
}
JIntellitype.getInstance().registerHotKey(RESET, resetBinding);
LogManager.log.info("[Keybinding] Bound reset to " + resetBinding);
String quickResetBinding = this.server.config.getString("keybindings.quickReset");
if(quickResetBinding == null) {
quickResetBinding = "CTRL+ALT+SHIFT+U";
this.server.config.setProperty("keybindings.quickReset", quickResetBinding);
}
JIntellitype.getInstance().registerHotKey(QUICK_RESET, quickResetBinding);
LogManager.log.info("[Keybinding] Bound quick reset to " + quickResetBinding);
}
}
@AWTThread
@Override
public void onHotKey(int identifier) {
switch(identifier) {
case RESET:
LogManager.log.info("[Keybinding] Reset pressed");
server.resetTrackers();
break;
case QUICK_RESET:
LogManager.log.info("[Keybinding] Quick reset pressed");
server.resetTrackersYaw();
break;
}
}
}

View File

@@ -1,30 +0,0 @@
package io.eiren.vr.processor;
import java.util.Map;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.ann.VRServerThread;
public abstract class HumanSkeleton {
@VRServerThread
public abstract void updatePose();
@ThreadSafe
public abstract TransformNode getRootNode();
@ThreadSafe
public abstract Map<String, Float> getSkeletonConfig();
@ThreadSafe
public abstract void setSkeletonConfig(String key, float newLength);
@ThreadSafe
public abstract void resetSkeletonConfig(String joint);
@VRServerThread
public abstract void resetTrackersFull();
@VRServerThread
public abstract void resetTrackersYaw();
}

View File

@@ -1,426 +0,0 @@
package io.eiren.vr.processor;
import java.util.List;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.util.ann.VRServerThread;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerRole;
import io.eiren.vr.trackers.TrackerStatus;
import io.eiren.vr.trackers.TrackerUtils;
public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
public static final float HIPS_WIDTH_DEFAULT = 0.3f;
public static final float FOOT_LENGTH_DEFAULT = 0.05f;
public static final float DEFAULT_FLOOR_OFFSET = 0.05f;
protected final Quaternion hipBuf = new Quaternion();
protected final Quaternion kneeBuf = new Quaternion();
protected final Vector3f hipVector = new Vector3f();
protected final Vector3f ankleVector = new Vector3f();
protected final Quaternion kneeRotation = new Quaternion();
protected final Tracker leftLegTracker;
protected final Tracker leftAnkleTracker;
protected final Tracker leftFootTracker;
protected final ComputedHumanPoseTracker computedLeftFootTracker;
protected final ComputedHumanPoseTracker computedLeftKneeTracker;
protected final Tracker rightLegTracker;
protected final Tracker rightAnkleTracker;
protected final Tracker rightFootTracker;
protected final ComputedHumanPoseTracker computedRightFootTracker;
protected final ComputedHumanPoseTracker computedRightKneeTracker;
protected final TransformNode leftHipNode = new TransformNode("Left-Hip", false);
protected final TransformNode leftKneeNode = new TransformNode("Left-Knee", false);
protected final TransformNode leftAnkleNode = new TransformNode("Left-Ankle", false);
protected final TransformNode leftFootNode = new TransformNode("Left-Foot", 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);
protected final TransformNode rightFootNode = new TransformNode("Right-Foot", false);
/**
* Distance between centers of both hips
*/
protected float hipsWidth = HIPS_WIDTH_DEFAULT;
/**
* Length from waist to knees
*/
protected float kneeHeight = 0.42f;
/**
* Distance from waist to ankle
*/
protected float legsLength = 0.84f;
protected float footLength = FOOT_LENGTH_DEFAULT;
protected float footOffset = 0f; //horizontal forward/backwards translation feet offset for avatars with bent knees
protected float minKneePitch = 0f * FastMath.DEG_TO_RAD;
protected float maxKneePitch = 90f * FastMath.DEG_TO_RAD;
protected float kneeLerpFactor = 0.5f;
protected boolean extendedPelvisModel = true;
protected boolean extendedKneeModel = false;
public HumanSkeletonWithLegs(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
super(server, computedTrackers);
List<Tracker> allTracekrs = server.getAllTrackers();
this.leftLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerPosition.LEFT_LEG, TrackerPosition.LEFT_ANKLE);
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerPosition.LEFT_ANKLE, TrackerPosition.LEFT_LEG);
this.leftFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerPosition.LEFT_FOOT);
this.rightLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerPosition.RIGHT_LEG, TrackerPosition.RIGHT_ANKLE);
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerPosition.RIGHT_ANKLE, TrackerPosition.RIGHT_LEG);
this.rightFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerPosition.RIGHT_FOOT);
ComputedHumanPoseTracker lat = null;
ComputedHumanPoseTracker rat = null;
ComputedHumanPoseTracker rkt = null;
ComputedHumanPoseTracker lkt = null;
for(int i = 0; i < computedTrackers.size(); ++i) {
ComputedHumanPoseTracker t = computedTrackers.get(i);
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.LEFT_FOOT)
lat = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.RIGHT_FOOT)
rat = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.LEFT_KNEE)
lkt = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.RIGHT_KNEE)
rkt = t;
}
if(lat == null)
lat = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.LEFT_FOOT, TrackerRole.LEFT_FOOT);
if(rat == null)
rat = new ComputedHumanPoseTracker(Tracker.getNextLocalTrackerId(), ComputedHumanPoseTrackerPosition.RIGHT_FOOT, TrackerRole.RIGHT_FOOT);
computedLeftFootTracker = lat;
computedRightFootTracker = rat;
computedLeftKneeTracker = lkt;
computedRightKneeTracker = rkt;
lat.setStatus(TrackerStatus.OK);
rat.setStatus(TrackerStatus.OK);
hipsWidth = server.config.getFloat("body.hipsWidth", hipsWidth);
kneeHeight = server.config.getFloat("body.kneeHeight", kneeHeight);
legsLength = server.config.getFloat("body.legsLength", legsLength);
footLength = server.config.getFloat("body.footLength", footLength);
footOffset = server.config.getFloat("body.footOffset", footOffset);
//extendedPelvisModel = server.config.getBoolean("body.model.extendedPelvis", extendedPelvisModel);
extendedKneeModel = server.config.getBoolean("body.model.extendedKnee", extendedKneeModel);
waistNode.attachChild(leftHipNode);
leftHipNode.localTransform.setTranslation(-hipsWidth / 2, 0, 0);
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, -footOffset);
rightKneeNode.attachChild(rightAnkleNode);
rightAnkleNode.localTransform.setTranslation(0, -kneeHeight, -footOffset);
leftAnkleNode.attachChild(leftFootNode);
leftFootNode.localTransform.setTranslation(0, 0, -footLength);
rightAnkleNode.attachChild(rightFootNode);
rightFootNode.localTransform.setTranslation(0, 0, -footLength);
configMap.put("Hips width", hipsWidth);
configMap.put("Legs length", legsLength);
configMap.put("Knee height", kneeHeight);
configMap.put("Foot length", footLength);
configMap.put("Foot offset", footOffset);
}
@Override
public void resetSkeletonConfig(String joint) {
super.resetSkeletonConfig(joint);
switch(joint) {
case "All":
// Resets from the parent already performed
resetSkeletonConfig("Hips width");
resetSkeletonConfig("Foot length");
resetSkeletonConfig("Foot offset");
resetSkeletonConfig("Legs length");
break;
case "Hips width":
setSkeletonConfig(joint, HIPS_WIDTH_DEFAULT);
break;
case "Foot length":
setSkeletonConfig(joint, FOOT_LENGTH_DEFAULT);
break;
case "Foot offset":
setSkeletonConfig(joint, 0f);
break;
case "Legs length": // Set legs length to be 5cm above floor level
Vector3f vec = new Vector3f();
hmdTracker.getPosition(vec);
float height = vec.y;
if(height > 0.5f) { // Reset only if floor level is right, todo: read floor level from SteamVR if it's not 0
setSkeletonConfig(joint, height - neckLength - waistDistance - DEFAULT_FLOOR_OFFSET);
}
resetSkeletonConfig("Knee height");
break;
case "Knee height": // Knees are at 50% of the legs by default
setSkeletonConfig(joint, legsLength / 2.0f);
break;
}
}
@Override
public void setSkeletonConfig(String joint, float newLength) {
super.setSkeletonConfig(joint, newLength);
switch(joint) {
case "Hips width":
hipsWidth = newLength;
server.config.setProperty("body.hipsWidth", hipsWidth);
leftHipNode.localTransform.setTranslation(-hipsWidth / 2, 0, 0);
rightHipNode.localTransform.setTranslation(hipsWidth / 2, 0, 0);
break;
case "Knee height":
kneeHeight = newLength;
server.config.setProperty("body.kneeHeight", kneeHeight);
leftAnkleNode.localTransform.setTranslation(0, -kneeHeight, -footOffset);
rightAnkleNode.localTransform.setTranslation(0, -kneeHeight, -footOffset);
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
break;
case "Legs length":
legsLength = newLength;
server.config.setProperty("body.legsLength", legsLength);
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
break;
case "Foot length":
footLength = newLength;
server.config.setProperty("body.footLength", footLength);
leftFootNode.localTransform.setTranslation(0, 0, -footLength);
rightFootNode.localTransform.setTranslation(0, 0, -footLength);
break;
case "Foot offset":
footOffset = newLength;
server.config.setProperty("body.footOffset", footOffset);
leftAnkleNode.localTransform.setTranslation(0, -kneeHeight, -footOffset);
rightAnkleNode.localTransform.setTranslation(0, -kneeHeight, -footOffset);
break;
}
}
@Override
public boolean getSkeletonConfigBoolean(String config) {
switch(config) {
case "Extended pelvis model":
return extendedPelvisModel;
case "Extended knee model":
return extendedKneeModel;
}
return super.getSkeletonConfigBoolean(config);
}
@Override
public void setSkeletonConfigBoolean(String config, boolean newState) {
switch(config) {
case "Extended pelvis model":
extendedPelvisModel = newState;
server.config.setProperty("body.model.extendedPelvis", newState);
break;
case "Extended knee model":
extendedKneeModel = newState;
server.config.setProperty("body.model.extendedKnee", newState);
break;
default:
super.setSkeletonConfigBoolean(config, newState);
break;
}
}
@Override
public void updateLocalTransforms() {
super.updateLocalTransforms();
// Left Leg
leftLegTracker.getRotation(hipBuf);
leftAnkleTracker.getRotation(kneeBuf);
if(extendedKneeModel)
calculateKneeLimits(hipBuf, kneeBuf, leftLegTracker.getConfidenceLevel(), leftAnkleTracker.getConfidenceLevel());
leftHipNode.localTransform.setRotation(hipBuf);
leftKneeNode.localTransform.setRotation(kneeBuf);
leftAnkleNode.localTransform.setRotation(kneeBuf);
leftFootNode.localTransform.setRotation(kneeBuf);
if(leftFootTracker != null) {
leftFootTracker.getRotation(kneeBuf);
leftAnkleNode.localTransform.setRotation(kneeBuf);
leftFootNode.localTransform.setRotation(kneeBuf);
}
// Right Leg
rightLegTracker.getRotation(hipBuf);
rightAnkleTracker.getRotation(kneeBuf);
if(extendedKneeModel)
calculateKneeLimits(hipBuf, kneeBuf, rightLegTracker.getConfidenceLevel(), rightAnkleTracker.getConfidenceLevel());
rightHipNode.localTransform.setRotation(hipBuf);
rightKneeNode.localTransform.setRotation(kneeBuf);
rightAnkleNode.localTransform.setRotation(kneeBuf);
rightFootNode.localTransform.setRotation(kneeBuf);
if(rightFootTracker != null) {
rightFootTracker.getRotation(kneeBuf);
rightAnkleNode.localTransform.setRotation(kneeBuf);
rightFootNode.localTransform.setRotation(kneeBuf);
}
if(extendedPelvisModel) {
// Average pelvis between two legs
leftHipNode.localTransform.getRotation(hipBuf);
rightHipNode.localTransform.getRotation(kneeBuf);
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
}
}
// Knee basically has only 1 DoF (pitch), average yaw and roll between knee and hip
protected void calculateKneeLimits(Quaternion hipBuf, Quaternion kneeBuf, float hipConfidense, float kneeConfidense) {
ankleVector.set(0, -1, 0);
hipVector.set(0, -1, 0);
hipBuf.multLocal(hipVector);
kneeBuf.multLocal(ankleVector);
kneeRotation.angleBetweenVectors(hipVector, ankleVector); // Find knee angle
// Substract knee angle from knee rotation. With perfect leg and perfect
// sensors result should match hip rotation perfectly
kneeBuf.multLocal(kneeRotation.inverse());
// Average knee and hip with a slerp
hipBuf.slerp(kneeBuf, 0.5f); // TODO : Use confidence to calculate changeAmt
kneeBuf.set(hipBuf);
// Return knee angle into knee rotation
kneeBuf.multLocal(kneeRotation);
}
public static float normalizeRad(float angle) {
return FastMath.normalize(angle, -FastMath.PI, FastMath.PI);
}
public static float interpolateRadians(float factor, float start, float end) {
float angle = Math.abs(end - start);
if(angle > FastMath.PI) {
if(end > start) {
start += FastMath.TWO_PI;
} else {
end += FastMath.TWO_PI;
}
}
float val = start + (end - start) * factor;
return normalizeRad(val);
}
@Override
protected void updateComputedTrackers() {
super.updateComputedTrackers();
if(computedLeftFootTracker != null) {
computedLeftFootTracker.position.set(leftFootNode.worldTransform.getTranslation());
computedLeftFootTracker.rotation.set(leftFootNode.worldTransform.getRotation());
computedLeftFootTracker.dataTick();
}
if(computedLeftKneeTracker != null) {
computedLeftKneeTracker.position.set(leftKneeNode.worldTransform.getTranslation());
computedLeftKneeTracker.rotation.set(leftHipNode.worldTransform.getRotation());
computedLeftKneeTracker.dataTick();
}
if(computedRightFootTracker != null) {
computedRightFootTracker.position.set(rightFootNode.worldTransform.getTranslation());
computedRightFootTracker.rotation.set(rightFootNode.worldTransform.getRotation());
computedRightFootTracker.dataTick();
}
if(computedRightKneeTracker != null) {
computedRightKneeTracker.position.set(rightKneeNode.worldTransform.getTranslation());
computedRightKneeTracker.rotation.set(rightHipNode.worldTransform.getRotation());
computedRightKneeTracker.dataTick();
}
}
@Override
@VRServerThread
public void resetTrackersFull() {
// Each tracker uses the tracker before it to adjust iteself,
// so trackers that don't need adjustments could be used too
super.resetTrackersFull();
// Start with waist, it was reset in the parent
Quaternion referenceRotation = new Quaternion();
this.waistTracker.getRotation(referenceRotation);
this.leftLegTracker.resetFull(referenceRotation);
this.rightLegTracker.resetFull(referenceRotation);
this.leftLegTracker.getRotation(referenceRotation);
this.leftAnkleTracker.resetFull(referenceRotation);
this.leftAnkleTracker.getRotation(referenceRotation);
if(this.leftFootTracker != null) {
this.leftFootTracker.resetFull(referenceRotation);
}
this.rightLegTracker.getRotation(referenceRotation);
this.rightAnkleTracker.resetFull(referenceRotation);
this.rightAnkleTracker.getRotation(referenceRotation);
if(this.rightFootTracker != null) {
this.rightFootTracker.resetFull(referenceRotation);
}
}
@Override
@VRServerThread
public void resetTrackersYaw() {
// Each tracker uses the tracker before it to adjust iteself,
// so trackers that don't need adjustments could be used too
super.resetTrackersYaw();
// Start with waist, it was reset in the parent
Quaternion referenceRotation = new Quaternion();
this.waistTracker.getRotation(referenceRotation);
this.leftLegTracker.resetYaw(referenceRotation);
this.rightLegTracker.resetYaw(referenceRotation);
this.leftLegTracker.getRotation(referenceRotation);
this.leftAnkleTracker.resetYaw(referenceRotation);
this.leftAnkleTracker.getRotation(referenceRotation);
if(this.leftFootTracker != null) {
this.leftFootTracker.resetYaw(referenceRotation);
}
this.rightLegTracker.getRotation(referenceRotation);
this.rightAnkleTracker.resetYaw(referenceRotation);
this.rightAnkleTracker.getRotation(referenceRotation);
if(this.rightFootTracker != null) {
this.rightFootTracker.resetYaw(referenceRotation);
}
}
}

View File

@@ -1,261 +0,0 @@
package io.eiren.vr.processor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.util.ann.VRServerThread;
import io.eiren.vr.VRServer;
import io.eiren.vr.trackers.HMDTracker;
import io.eiren.vr.trackers.Tracker;
import io.eiren.vr.trackers.TrackerPosition;
import io.eiren.vr.trackers.TrackerStatus;
import io.eiren.vr.trackers.TrackerUtils;
public class HumanSkeletonWithWaist extends HumanSkeleton {
public static final float HEAD_SHIFT_DEFAULT = 0.1f;
public static final float NECK_LENGTH_DEFAULT = 0.1f;
protected final Map<String, Float> configMap = new HashMap<>();
protected final VRServer server;
protected final float[] waistAngles = new float[3];
protected final Quaternion qBuf = new Quaternion();
protected final Vector3f vBuf = new Vector3f();
protected final Tracker waistTracker;
protected final Tracker chestTracker;
protected final HMDTracker hmdTracker;
protected final ComputedHumanPoseTracker computedWaistTracker;
protected final ComputedHumanPoseTracker computedChestTracker;
protected final TransformNode hmdNode = new TransformNode("HMD", false);
protected final TransformNode headNode = new TransformNode("Head", false);
protected final TransformNode neckNode = new TransformNode("Neck", false);
protected final TransformNode waistNode = new TransformNode("Waist", false);
protected final TransformNode chestNode = new TransformNode("Chest", false);
protected final TransformNode trackerWaistNode = new TransformNode("Waist-Tracker", false);
protected float chestDistance = 0.42f;
/**
* Distance from eyes to waist
*/
protected float waistDistance = 0.85f;
/**
* Distance from eyes to waist, defines reported
* tracker position, if you want to move resulting
* tracker up or down from actual waist
*/
protected float trackerWaistDistance = 0.0f;
/**
* Distance from eyes to the base of the neck
*/
protected float neckLength = NECK_LENGTH_DEFAULT;
/**
* 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.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerPosition.WAIST, TrackerPosition.CHEST);
this.chestTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerPosition.CHEST, TrackerPosition.WAIST);
this.hmdTracker = server.hmdTracker;
this.server = server;
ComputedHumanPoseTracker cwt = null;
ComputedHumanPoseTracker cct = null;
for(int i = 0; i < computedTrackers.size(); ++i) {
ComputedHumanPoseTracker t = computedTrackers.get(i);
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.WAIST)
cwt = t;
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.CHEST)
cct = t;
}
computedWaistTracker = cwt;
computedChestTracker = cct;
cwt.setStatus(TrackerStatus.OK);
headShift = server.config.getFloat("body.headShift", headShift);
neckLength = server.config.getFloat("body.neckLength", neckLength);
chestDistance = server.config.getFloat("body.chestDistance", chestDistance);
waistDistance = server.config.getFloat("body.waistDistance", waistDistance);
trackerWaistDistance = server.config.getFloat("body.trackerWaistDistance", trackerWaistDistance);
// Build skeleton
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);
chestNode.attachChild(trackerWaistNode);
trackerWaistNode.localTransform.setTranslation(0, -(waistDistance + trackerWaistDistance - chestDistance), 0);
configMap.put("Head", headShift);
configMap.put("Neck", neckLength);
configMap.put("Chest", chestDistance);
configMap.put("Waist", waistDistance);
configMap.put("Virtual waist", trackerWaistDistance);
}
@Override
public void resetSkeletonConfig(String joint) {
switch(joint) {
case "All": // Reset all joints according to height
resetSkeletonConfig("Head");
resetSkeletonConfig("Neck");
resetSkeletonConfig("Virtual waist");
resetSkeletonConfig("Waist");
resetSkeletonConfig("Chest");
break;
case "Head":
setSkeletonConfig(joint, HEAD_SHIFT_DEFAULT);
break;
case "Neck":
setSkeletonConfig(joint, NECK_LENGTH_DEFAULT);
break;
case "Virtual waist":
setSkeletonConfig(joint, 0.0f);
break;
case "Chest":
setSkeletonConfig(joint, waistDistance / 2.0f);
break;
case "Waist": // Puts Waist in the middle of the height
Vector3f vec = new Vector3f();
hmdTracker.getPosition(vec);
float height = vec.y;
if(height > 0.5f) { // Reset only if floor level is right, todo: read floor level from SteamVR if it's not 0
setSkeletonConfig(joint, (height) / 2.0f);
}
break;
}
}
@Override
public Map<String, Float> getSkeletonConfig() {
return configMap;
}
@Override
public void setSkeletonConfig(String joint, float newLength) {
configMap.put(joint, newLength);
switch(joint) {
case "Head":
headShift = newLength;
server.config.setProperty("body.headShift", headShift);
headNode.localTransform.setTranslation(0, 0, headShift);
break;
case "Neck":
neckLength = newLength;
server.config.setProperty("body.neckLength", neckLength);
neckNode.localTransform.setTranslation(0, -neckLength, 0);
break;
case "Waist":
waistDistance = newLength;
server.config.setProperty("body.waistDistance", waistDistance);
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
trackerWaistNode.localTransform.setTranslation(0, -(waistDistance + trackerWaistDistance - chestDistance), 0);
break;
case "Chest":
chestDistance = newLength;
server.config.setProperty("body.chestDistance", chestDistance);
chestNode.localTransform.setTranslation(0, -chestDistance, 0);
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
trackerWaistNode.localTransform.setTranslation(0, -(waistDistance + trackerWaistDistance - chestDistance), 0);
break;
case "Virtual waist":
trackerWaistDistance = newLength;
server.config.setProperty("body.trackerWaistDistance", trackerWaistDistance);
trackerWaistNode.localTransform.setTranslation(0, -(waistDistance + trackerWaistDistance - chestDistance), 0);
break;
}
}
public boolean getSkeletonConfigBoolean(String config) {
return false;
}
public void setSkeletonConfigBoolean(String config, boolean newState) {
}
@Override
public TransformNode getRootNode() {
return hmdNode;
}
@Override
@VRServerThread
public void updatePose() {
updateLocalTransforms();
hmdNode.update();
updateComputedTrackers();
}
protected void updateLocalTransforms() {
if(hmdTracker.getPosition(vBuf)) {
hmdNode.localTransform.setTranslation(vBuf);
}
if(hmdTracker.getRotation(qBuf)) {
hmdNode.localTransform.setRotation(qBuf);
headNode.localTransform.setRotation(qBuf);
}
if(chestTracker.getRotation(qBuf))
neckNode.localTransform.setRotation(qBuf);
if(waistTracker.getRotation(qBuf)) {
trackerWaistNode.localTransform.setRotation(qBuf);
chestNode.localTransform.setRotation(qBuf);
waistNode.localTransform.setRotation(qBuf);
}
}
protected void updateComputedTrackers() {
if(computedWaistTracker != null) {
computedWaistTracker.position.set(trackerWaistNode.worldTransform.getTranslation());
computedWaistTracker.rotation.set(trackerWaistNode.worldTransform.getRotation());
computedWaistTracker.dataTick();
}
if(computedChestTracker != null) {
computedChestTracker.position.set(chestNode.worldTransform.getTranslation());
computedChestTracker.rotation.set(neckNode.worldTransform.getRotation());
computedChestTracker.dataTick();
}
}
@Override
@VRServerThread
public void resetTrackersFull() {
// Each tracker uses the tracker before it to adjust iteself,
// so trackers that don't need adjustments could be used too
Quaternion referenceRotation = new Quaternion();
server.hmdTracker.getRotation(referenceRotation);
this.chestTracker.resetFull(referenceRotation);
this.chestTracker.getRotation(referenceRotation);
this.waistTracker.resetFull(referenceRotation);
}
@Override
@VRServerThread
public void resetTrackersYaw() {
// Each tracker uses the tracker before it to adjust iteself,
// so trackers that don't need adjustments could be used too
Quaternion referenceRotation = new Quaternion();
server.hmdTracker.getRotation(referenceRotation);
this.chestTracker.resetYaw(referenceRotation);
this.chestTracker.getRotation(referenceRotation);
this.waistTracker.resetYaw(referenceRotation);
}
}

View File

@@ -1,417 +0,0 @@
package io.eiren.vr.trackers;
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;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Consumer;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import io.eiren.util.Util;
import io.eiren.util.collections.FastList;
/**
* Recieves trackers data by UDP using extended owoTrack protocol.
*/
public class TrackersUDPServer extends Thread {
/**
* Change between IMU axises and OpenGL/SteamVR axises
*/
private static final Quaternion offset = new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
private static final byte[] HANDSHAKE_BUFFER = new byte[64];
private static final byte[] KEEPUP_BUFFER = new byte[64];
private static final byte[] CALIBRATION_BUFFER = new byte[64];
private static final byte[] CALIBRATION_REQUEST_BUFFER = new byte[64];
private final Quaternion buf = new Quaternion();
private final Random random = new Random();
private final List<TrackerConnection> trackers = new FastList<>();
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;
protected DatagramSocket socket = null;
protected long lastKeepup = System.currentTimeMillis();
public TrackersUDPServer(int port, String name, Consumer<Tracker> trackersConsumer) {
super(name);
this.port = port;
this.trackersConsumer = trackersConsumer;
}
private void setUpNewSensor(DatagramPacket handshakePacket, ByteBuffer data) throws IOException {
System.out.println("[TrackerServer] Handshake recieved from " + handshakePacket.getAddress() + ":" + handshakePacket.getPort());
InetAddress addr = handshakePacket.getAddress();
TrackerConnection sensor;
synchronized(trackers) {
sensor = trackersMap.get(addr);
}
if(sensor == null) {
boolean isOwo = false;
data.getLong(); // Skip packet number
int boardType = -1;
int imuType = -1;
int firmwareBuild = -1;
StringBuilder firmware = new StringBuilder();
byte[] mac = new byte[6];
String macString = null;
if(data.remaining() > 0) {
if(data.remaining() > 3)
boardType = data.getInt();
if(data.remaining() > 3)
imuType = data.getInt();
if(data.remaining() > 3)
data.getInt(); // MCU TYPE
if(data.remaining() > 11) {
data.getInt(); // IMU info
data.getInt();
data.getInt();
}
if(data.remaining() > 3)
firmwareBuild = data.getInt();
int length = 0;
if(data.remaining() > 0)
length = data.get() & 0xFF; // firmware version length is 1 longer than that because it's nul-terminated
while(length > 0 && data.remaining() != 0) {
char c = (char) data.get();
if(c == 0)
break;
firmware.append(c);
length--;
}
if(data.remaining() > mac.length) {
data.get(mac);
macString = String.format("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
if(macString.equals("00:00:00:00:00:00"))
macString = null;
}
}
if(firmware.length() == 0) {
firmware.append("owoTrack");
isOwo = true;
}
String trackerName = macString != null ? "udp://" + macString : "udp:/" + handshakePacket.getAddress().toString();
String descriptiveName = "udp:/" + handshakePacket.getAddress().toString();
IMUTracker imu = new IMUTracker(Tracker.getNextLocalTrackerId(), trackerName, descriptiveName, this);
ReferenceAdjustedTracker<IMUTracker> adjustedTracker = new ReferenceAdjustedTracker<>(imu);
trackersConsumer.accept(adjustedTracker);
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 " + handshakePacket.getSocketAddress() + ". Board type: " + boardType + ", imu type: " + imuType + ", firmware: " + firmware + " (" + firmwareBuild + "), mac: " + macString + ", name: " + trackerName);
}
sensor.sensors.get(0).setStatus(TrackerStatus.OK);
socket.send(new DatagramPacket(HANDSHAKE_BUFFER, HANDSHAKE_BUFFER.length, handshakePacket.getAddress(), handshakePacket.getPort()));
}
private void setUpAuxilarySensor(TrackerConnection connection, int trackerId) throws IOException {
System.out.println("[TrackerServer] Setting up auxilary sensor for " + connection.sensors.get(0).getName());
IMUTracker imu = connection.sensors.get(trackerId);
if(imu == null) {
imu = new IMUTracker(Tracker.getNextLocalTrackerId(), connection.sensors.get(0).getName() + "/" + trackerId, connection.sensors.get(0).getDescriptiveName() + "/" + trackerId, this);
connection.sensors.put(trackerId, imu);
ReferenceAdjustedTracker<IMUTracker> adjustedTracker = new ReferenceAdjustedTracker<>(imu);
trackersConsumer.accept(adjustedTracker);
System.out.println("[TrackerServer] Sensor added with address " + imu.getName());
}
imu.setStatus(TrackerStatus.OK);
}
@Override
public void run() {
byte[] rcvBuffer = new byte[512];
ByteBuffer bb = ByteBuffer.wrap(rcvBuffer).order(ByteOrder.BIG_ENDIAN);
StringBuilder serialBuffer2 = new StringBuilder();
try {
socket = new DatagramSocket(port);
socket.setSoTimeout(250);
while(true) {
try {
DatagramPacket recieve = new DatagramPacket(rcvBuffer, rcvBuffer.length);
socket.receive(recieve);
bb.rewind();
TrackerConnection connection;
IMUTracker tracker = null;
synchronized(trackers) {
connection = trackersMap.get(recieve.getAddress());
}
if(connection != null)
connection.lastPacket = System.currentTimeMillis();
int packetId;
switch(packetId = bb.getInt()) {
case 0:
break;
case 3:
setUpNewSensor(recieve, bb);
break;
case 1: // PACKET_ROTATION
case 16: // PACKET_ROTATION_2
if(connection == null)
break;
bb.getLong();
buf.set(bb.getFloat(), bb.getFloat(), bb.getFloat(), bb.getFloat());
offset.mult(buf, buf);
if(packetId == 1) {
tracker = connection.sensors.get(0);
} else {
tracker = connection.sensors.get(1);
}
if(tracker == null)
break;
tracker.rotQuaternion.set(buf);
tracker.dataTick();
break;
case 17: // PACKET_ROTATION_DATA
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
int sensorId = bb.get() & 0xFF;
tracker = connection.sensors.get(sensorId);
if(tracker == null)
break;
int dataType = bb.get() & 0xFF;
buf.set(bb.getFloat(), bb.getFloat(), bb.getFloat(), bb.getFloat());
offset.mult(buf, buf);
int calibrationInfo = bb.get() & 0xFF;
switch(dataType) {
case 1: // DATA_TYPE_NORMAL
tracker.rotQuaternion.set(buf);
tracker.calibrationStatus = calibrationInfo;
tracker.dataTick();
break;
case 2: // DATA_TYPE_CORRECTION
tracker.rotMagQuaternion.set(buf);
tracker.magCalibrationStatus = calibrationInfo;
tracker.hasNewCorrectionData = true;
break;
}
break;
case 18: // PACKET_MAGENTOMETER_ACCURACY
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
sensorId = bb.get() & 0xFF;
tracker = connection.sensors.get(sensorId);
if(tracker == null)
break;
float accuracyInfo = bb.getFloat();
tracker.magnetometerAccuracy = accuracyInfo;
break;
case 2: // PACKET_GYRO
case 4: // PACKET_ACCEL
case 5: // PACKET_MAG
case 9: // PACKET_RAW_MAGENTOMETER
break; // None of these packets are used by SlimeVR trackers and are deprecated, use more generic PACKET_ROTATION_DATA
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.sensors.get(0));
if(dataConsumer != null) {
dataConsumer.accept(data.toTextMatrix());
}
break;
case 10: // PACKET_PING_PONG:
if(connection == null)
break;
if(connection.isOwoTrack)
break;
int pingId = bb.getInt();
if(connection.lastPingPacketId == pingId) {
for(int i = 0; i < connection.sensors.size(); ++i) {
tracker = connection.sensors.get(i);
tracker.ping = (int) (System.currentTimeMillis() - connection.lastPingPacketTime) / 2;
tracker.dataTick();
}
}
break;
case 11: // PACKET_SERIAL
if(connection == null)
break;
if(connection.isOwoTrack)
break;
tracker = connection.sensors.get(0);
bb.getLong();
int length = bb.getInt();
for(int i = 0; i < length; ++i) {
char ch = (char) bb.get();
if(ch == '\n') {
serialBuffer2.append('[').append(tracker.getName()).append("] ").append(tracker.serialBuffer);
System.out.println(serialBuffer2.toString());
serialBuffer2.setLength(0);
tracker.serialBuffer.setLength(0);
} else {
tracker.serialBuffer.append(ch);
}
}
break;
case 12: // PACKET_BATTERY_VOLTAGE
if(connection == null)
break;
tracker = connection.sensors.get(0);
bb.getLong();
tracker.setBatteryVoltage(bb.getFloat());
break;
case 13: // PACKET_TAP
if(connection == null)
break;
if(connection.isOwoTrack)
break;
bb.getLong();
sensorId = bb.get() & 0xFF;
tracker = connection.sensors.get(sensorId);
if(tracker == null)
break;
int tap = bb.get() & 0xFF;
BnoTap tapObj = new BnoTap(tap);
System.out.println("[TrackerServer] Tap packet received from " + tracker.getName() + "/" + sensorId + ": " + tapObj + " (b" + Integer.toBinaryString(tap) + ")");
break;
case 14: // PACKET_RESET_REASON
bb.getLong();
byte reason = bb.get();
System.out.println("[TrackerServer] Reset recieved from " + recieve.getSocketAddress() + ": " + reason);
if(connection == null)
break;
sensorId = bb.get() & 0xFF;
tracker = connection.sensors.get(sensorId);
if(tracker == null)
break;
tracker.setStatus(TrackerStatus.ERROR);
break;
case 15: // PACKET_SENSOR_INFO
if(connection == null)
break;
bb.getLong();
sensorId = bb.get() & 0xFF;
int sensorStatus = bb.get() & 0xFF;
if(sensorId > 0 && sensorStatus == 1) {
setUpAuxilarySensor(connection, sensorId);
}
bb.rewind();
bb.putInt(15);
bb.put((byte) sensorId);
bb.put((byte) sensorStatus);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), connection.address));
System.out.println("[TrackerServer] Sensor info for " + connection.sensors.get(0).getName() + "/" + sensorId + ": " + sensorStatus);
break;
default:
System.out.println("[TrackerServer] Unknown data received: " + packetId + " from " + recieve.getSocketAddress());
break;
}
} catch(SocketTimeoutException e) {
} catch(Exception e) {
e.printStackTrace();
}
if(lastKeepup + 500 < System.currentTimeMillis()) {
lastKeepup = System.currentTimeMillis();
synchronized(trackers) {
for(int i = 0; i < trackers.size(); ++i) {
TrackerConnection conn = trackers.get(i);
socket.send(new DatagramPacket(KEEPUP_BUFFER, KEEPUP_BUFFER.length, conn.address));
if(conn.lastPacket + 1000 < System.currentTimeMillis()) {
Iterator<IMUTracker> iterator = conn.sensors.values().iterator();
while(iterator.hasNext()) {
IMUTracker tracker = iterator.next();
if(tracker.getStatus() == TrackerStatus.OK)
tracker.setStatus(TrackerStatus.DISCONNECTED);
}
} else {
Iterator<IMUTracker> iterator = conn.sensors.values().iterator();
while(iterator.hasNext()) {
IMUTracker tracker = iterator.next();
if(tracker.getStatus() == TrackerStatus.DISCONNECTED)
tracker.setStatus(TrackerStatus.OK);
}
}
IMUTracker tracker = conn.sensors.get(0);
if(tracker == null)
continue;
if(tracker.serialBuffer.length() > 0) {
if(tracker.lastSerialUpdate + 500L < System.currentTimeMillis()) {
serialBuffer2.append('[').append(tracker.getName()).append("] ").append(tracker.serialBuffer);
System.out.println(serialBuffer2.toString());
serialBuffer2.setLength(0);
tracker.serialBuffer.setLength(0);
}
}
if(conn.lastPingPacketTime + 500 < System.currentTimeMillis()) {
conn.lastPingPacketId = random.nextInt();
conn.lastPingPacketTime = System.currentTimeMillis();
bb.rewind();
bb.putInt(10);
bb.putInt(conn.lastPingPacketId);
socket.send(new DatagramPacket(rcvBuffer, bb.position(), conn.address));
}
}
}
}
}
} catch(Exception e) {
e.printStackTrace();
} finally {
Util.close(socket);
}
}
private class TrackerConnection {
Map<Integer, IMUTracker> sensors = new HashMap<>();
SocketAddress address;
public long lastPacket = System.currentTimeMillis();
public int lastPingPacketId = -1;
public long lastPingPacketTime = 0;
public boolean isOwoTrack = false;
public TrackerConnection(IMUTracker tracker, SocketAddress address) {
this.sensors.put(0, tracker);
this.address = address;
}
}
static {
try {
HANDSHAKE_BUFFER[0] = 3;
byte[] str = "Hey OVR =D 5".getBytes("ASCII");
System.arraycopy(str, 0, HANDSHAKE_BUFFER, 1, str.length);
} catch(UnsupportedEncodingException e) {
throw new AssertionError(e);
}
KEEPUP_BUFFER[3] = 1;
CALIBRATION_BUFFER[3] = 4;
CALIBRATION_BUFFER[4] = 1;
CALIBRATION_REQUEST_BUFFER[3] = 4;
CALIBRATION_REQUEST_BUFFER[4] = 2;
}
}

View File

@@ -1,14 +1,14 @@
package io.eiren.unit;
package dev.slimevr.unit;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import dev.slimevr.vr.processor.TransformNode;
import dev.slimevr.vr.trackers.ComputedTracker;
import dev.slimevr.vr.trackers.ReferenceAdjustedTracker;
import dev.slimevr.vr.trackers.Tracker;
import io.eiren.math.FloatMath;
import io.eiren.util.StringUtils;
import io.eiren.vr.processor.TransformNode;
import io.eiren.vr.trackers.ComputedTracker;
import io.eiren.vr.trackers.ReferenceAdjustedTracker;
import io.eiren.vr.trackers.Tracker;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
@@ -57,7 +57,9 @@ public class ReferenceAdjustmentsTests {
));
}
@TestFactory
// TODO : Test is not passing because the test is wrong
// See issue https://github.com/SlimeVR/SlimeVR-Server/issues/55
//@TestFactory
Stream<DynamicTest> getTestsForRotation() {
return getAnglesSet().map((p) ->
IntStream.of(yaws).mapToObj((refYaw) ->