mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b8b58606 | ||
|
|
97bc9343c1 | ||
|
|
18cea30f72 | ||
|
|
d5c048600e | ||
|
|
6d103d4ff9 | ||
|
|
7008197760 | ||
|
|
da66f33edc | ||
|
|
4109d1c825 | ||
|
|
a300663a9e | ||
|
|
cb33dac3b9 | ||
|
|
582bac8050 | ||
|
|
5e1c45bc09 | ||
|
|
b3073e6938 | ||
|
|
63e259689f | ||
|
|
d92ea0a39e | ||
|
|
81bbb4008b | ||
|
|
45ad0698b1 | ||
|
|
bc542a7bb1 | ||
|
|
efb065f558 | ||
|
|
00e63db029 | ||
|
|
f6a2926033 | ||
|
|
5b0f8afa4e | ||
|
|
c5b4421eae | ||
|
|
4d3f04e227 | ||
|
|
75ad29a68d | ||
|
|
62e1e65dda | ||
|
|
02f64314b8 | ||
|
|
12d7f191ee | ||
|
|
37135e1c8e | ||
|
|
85a0c25d0e | ||
|
|
1f081392df | ||
|
|
c02f9b827d | ||
|
|
7e95c9f999 | ||
|
|
4836b025e9 | ||
|
|
9a76838602 |
@@ -21,6 +21,11 @@ javadoc.options.encoding = 'UTF-8'
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
if (JavaVersion.current().isJava9Compatible()) {
|
||||
// TODO: Gradle 6.6
|
||||
// options.release = 8
|
||||
options.compilerArgs.addAll(['--release', '8'])
|
||||
}
|
||||
}
|
||||
tasks.withType(Test) {
|
||||
systemProperty('file.encoding', 'UTF-8')
|
||||
|
||||
13
resources/firewall.bat
Normal file
13
resources/firewall.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
echo Installing firewall rules...
|
||||
|
||||
rem Rotational data default port
|
||||
netsh advfirewall firewall add rule name="UDP 6969 incoming" dir=in action=allow protocol=UDP localport=6969
|
||||
netsh advfirewall firewall add rule name="UDP 6969 outgoing" dir=out action=allow protocol=UDP localport=6969
|
||||
|
||||
rem Info server allowing automatic discovery
|
||||
netsh advfirewall firewall add rule name="UDP 35903 incoming" dir=in action=allow protocol=UDP localport=35903
|
||||
netsh advfirewall firewall add rule name="UDP 35903 outgoing" dir=out action=allow protocol=UDP localport=35903
|
||||
|
||||
echo Done!
|
||||
pause
|
||||
1
resources/run.bat
Normal file
1
resources/run.bat
Normal file
@@ -0,0 +1 @@
|
||||
@java -Xmx512M -jar slimevr.jar
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.eiren.gui.autobone;
|
||||
package dev.slimevr.autobone;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -7,6 +7,9 @@ import java.util.function.Consumer;
|
||||
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import dev.slimevr.poserecorder.PoseFrame;
|
||||
import dev.slimevr.poserecorder.TrackerFrame;
|
||||
import dev.slimevr.poserecorder.TrackerFrameData;
|
||||
import io.eiren.util.ann.ThreadSafe;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.eiren.util.collections.FastList;
|
||||
@@ -18,88 +21,79 @@ import io.eiren.vr.processor.TrackerBodyPosition;
|
||||
import io.eiren.vr.trackers.TrackerUtils;
|
||||
|
||||
public class AutoBone {
|
||||
|
||||
|
||||
public class Epoch {
|
||||
|
||||
|
||||
public final int epoch;
|
||||
public final float epochError;
|
||||
|
||||
|
||||
public Epoch(int epoch, float epochError) {
|
||||
this.epoch = epoch;
|
||||
this.epochError = epochError;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Epoch: " + epoch + ", Epoch Error: " + epochError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int cursorIncrement = 1;
|
||||
|
||||
|
||||
public int minDataDistance = 2;
|
||||
public int maxDataDistance = 32;
|
||||
|
||||
|
||||
public int numEpochs = 5;
|
||||
|
||||
|
||||
public float initialAdjustRate = 2.5f;
|
||||
public float adjustRateDecay = 1.01f;
|
||||
|
||||
|
||||
public float slideErrorFactor = 1.0f;
|
||||
public float offsetErrorFactor = 0.0f;
|
||||
public float proportionErrorFactor = 0.2f;
|
||||
public float heightErrorFactor = 0.1f;
|
||||
public float positionErrorFactor = 0.0f;
|
||||
public float positionOffsetErrorFactor = 0.0f;
|
||||
|
||||
/*
|
||||
public float NECK_WAIST_RATIO_MIN = 0.2f;
|
||||
public float NECK_WAIST_RATIO_MAX = 0.3f;
|
||||
|
||||
public float CHEST_WAIST_RATIO_MIN = 0.35f;
|
||||
public float CHEST_WAIST_RATIO_MAX = 0.6f;
|
||||
|
||||
public float HIP_MIN = 0.08f;
|
||||
public float HIP_WAIST_RATIO_MAX = 0.4f;
|
||||
|
||||
// Human average is 1.1235 (SD 0.07)
|
||||
public float LEG_WAIST_RATIO_MIN = 1.1235f - ((0.07f * 3f) + 0.05f);
|
||||
public float LEG_WAIST_RATIO_MAX = 1.1235f + ((0.07f * 3f) + 0.05f);
|
||||
|
||||
public float KNEE_LEG_RATIO_MIN = 0.42f;
|
||||
public float KNEE_LEG_RATIO_MAX = 0.58f;
|
||||
*/
|
||||
|
||||
|
||||
// Human average is probably 1.1235 (SD 0.07)
|
||||
public float legBodyRatio = 1.1235f;
|
||||
// SD of 0.07, capture 68% within range
|
||||
public float legBodyRatioRange = 0.07f;
|
||||
|
||||
// Assume these to be approximately half
|
||||
public float kneeLegRatio = 0.5f;
|
||||
public float chestWaistRatio = 0.5f;
|
||||
|
||||
protected final VRServer server;
|
||||
|
||||
|
||||
protected HumanSkeletonWithLegs skeleton = null;
|
||||
|
||||
|
||||
// This is filled by reloadConfigValues()
|
||||
public final HashMap<String, Float> configs = new HashMap<String, Float>();
|
||||
public final HashMap<String, Float> staticConfigs = new HashMap<String, Float>();
|
||||
|
||||
public final FastList<String> heightConfigs = new FastList<String>(new String[] {
|
||||
"Neck",
|
||||
"Waist",
|
||||
"Legs length"
|
||||
|
||||
public final FastList<String> heightConfigs = new FastList<String>(new String[]{"Neck", "Waist", "Legs length"
|
||||
});
|
||||
|
||||
|
||||
public AutoBone(VRServer server) {
|
||||
this.server = server;
|
||||
|
||||
|
||||
reloadConfigValues();
|
||||
|
||||
|
||||
server.addSkeletonUpdatedCallback(this::skeletonUpdated);
|
||||
}
|
||||
|
||||
|
||||
public void reloadConfigValues() {
|
||||
reloadConfigValues(null);
|
||||
}
|
||||
|
||||
public void reloadConfigValues(TrackerFrame[] frame) {
|
||||
// Load waist configs
|
||||
staticConfigs.put("Head", server.config.getFloat("body.headShift", HumanSkeletonWithWaist.HEAD_SHIFT_DEFAULT));
|
||||
staticConfigs.put("Neck", server.config.getFloat("body.neckLength", HumanSkeletonWithWaist.NECK_LENGTH_DEFAULT));
|
||||
configs.put("Waist", server.config.getFloat("body.waistDistance", 0.85f));
|
||||
|
||||
if (server.config.getBoolean("autobone.forceChestTracker", false) ||
|
||||
TrackerUtils.findTrackerForBodyPosition(server.getAllTrackers(), TrackerBodyPosition.CHEST) != null) {
|
||||
|
||||
if(server.config.getBoolean("autobone.forceChestTracker", false) || (frame != null && TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.CHEST) != null) || TrackerUtils.findTrackerForBodyPosition(server.getAllTrackers(), TrackerBodyPosition.CHEST) != null) {
|
||||
// If force enabled or has a chest tracker
|
||||
configs.put("Chest", server.config.getFloat("body.chestDistance", 0.42f));
|
||||
} else {
|
||||
@@ -107,51 +101,49 @@ public class AutoBone {
|
||||
configs.remove("Chest");
|
||||
staticConfigs.put("Chest", server.config.getFloat("body.chestDistance", 0.42f));
|
||||
}
|
||||
|
||||
|
||||
// Load leg configs
|
||||
staticConfigs.put("Hips width", server.config.getFloat("body.hipsWidth", HumanSkeletonWithLegs.HIPS_WIDTH_DEFAULT));
|
||||
configs.put("Legs length", server.config.getFloat("body.legsLength", 0.84f));
|
||||
configs.put("Knee height", server.config.getFloat("body.kneeHeight", 0.42f));
|
||||
}
|
||||
|
||||
|
||||
@ThreadSafe
|
||||
public void skeletonUpdated(HumanSkeleton newSkeleton) {
|
||||
if (newSkeleton instanceof HumanSkeletonWithLegs) {
|
||||
skeleton = (HumanSkeletonWithLegs)newSkeleton;
|
||||
if(newSkeleton instanceof HumanSkeletonWithLegs) {
|
||||
skeleton = (HumanSkeletonWithLegs) newSkeleton;
|
||||
applyConfigToSkeleton(newSkeleton);
|
||||
LogManager.log.info("[AutoBone] Received updated skeleton");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void applyConfig() {
|
||||
if (!applyConfigToSkeleton(skeleton)) {
|
||||
if(!applyConfigToSkeleton(skeleton)) {
|
||||
// Unable to apply to skeleton, save directly
|
||||
saveConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean applyConfigToSkeleton(HumanSkeleton skeleton) {
|
||||
if (skeleton == null) {
|
||||
if(skeleton == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Entry<String, Float> entry : configs.entrySet()) {
|
||||
skeleton.setSkeletonConfig(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
|
||||
configs.forEach(skeleton::setSkeletonConfig);
|
||||
|
||||
server.saveConfig();
|
||||
|
||||
|
||||
LogManager.log.info("[AutoBone] Configured skeleton bone lengths");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void setConfig(String name, String path) {
|
||||
Float value = configs.get(name);
|
||||
if (value != null) {
|
||||
if(value != null) {
|
||||
server.config.setProperty(path, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This doesn't require a skeleton, therefore can be used if skeleton is null
|
||||
public void saveConfigs() {
|
||||
setConfig("Head", "body.headShift");
|
||||
@@ -161,232 +153,242 @@ public class AutoBone {
|
||||
setConfig("Hips width", "body.hipsWidth");
|
||||
setConfig("Legs length", "body.legsLength");
|
||||
setConfig("Knee height", "body.kneeHeight");
|
||||
|
||||
|
||||
server.saveConfig();
|
||||
}
|
||||
|
||||
|
||||
public Float getConfig(String config) {
|
||||
Float configVal = configs.get(config);
|
||||
return configVal != null ? configVal : staticConfigs.get(config);
|
||||
}
|
||||
|
||||
|
||||
public Float getConfig(String config, Map<String, Float> configs, Map<String, Float> configsAlt) {
|
||||
if (configs == null) {
|
||||
if(configs == null) {
|
||||
throw new NullPointerException("Argument \"configs\" must not be null");
|
||||
}
|
||||
|
||||
|
||||
Float configVal = configs.get(config);
|
||||
return configVal != null || configsAlt == null ? configVal : configsAlt.get(config);
|
||||
}
|
||||
|
||||
|
||||
public float getHeight(Map<String, Float> configs) {
|
||||
return getHeight(configs, null);
|
||||
}
|
||||
|
||||
|
||||
public float getHeight(Map<String, Float> configs, Map<String, Float> configsAlt) {
|
||||
float height = 0f;
|
||||
|
||||
for (String heightConfig : heightConfigs) {
|
||||
|
||||
for(String heightConfig : heightConfigs) {
|
||||
Float length = getConfig(heightConfig, configs, configsAlt);
|
||||
if (length != null) {
|
||||
if(length != null) {
|
||||
height += length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
|
||||
public float getLengthSum(Map<String, Float> configs) {
|
||||
float length = 0f;
|
||||
|
||||
for (float boneLength : configs.values()) {
|
||||
|
||||
for(float boneLength : configs.values()) {
|
||||
length += boneLength;
|
||||
}
|
||||
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public float getMaxHmdHeight(PoseFrame[] frames) {
|
||||
|
||||
public float getMaxHmdHeight(PoseFrame frames) {
|
||||
float maxHeight = 0f;
|
||||
for (PoseFrame frame : frames) {
|
||||
if (frame.rootPos.y > maxHeight) {
|
||||
maxHeight = frame.rootPos.y;
|
||||
for(TrackerFrame[] frame : frames) {
|
||||
TrackerFrame hmd = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.HMD);
|
||||
if(hmd != null && hmd.hasData(TrackerFrameData.POSITION) && hmd.position.y > maxHeight) {
|
||||
maxHeight = hmd.position.y;
|
||||
}
|
||||
}
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
public void processFrames(PoseFrame[] frames) {
|
||||
|
||||
public void processFrames(PoseFrame frames) {
|
||||
processFrames(frames, -1f);
|
||||
}
|
||||
|
||||
public void processFrames(PoseFrame[] frames, Consumer<Epoch> epochCallback) {
|
||||
|
||||
public void processFrames(PoseFrame frames, Consumer<Epoch> epochCallback) {
|
||||
processFrames(frames, -1f, epochCallback);
|
||||
}
|
||||
|
||||
public void processFrames(PoseFrame[] frames, float targetHeight) {
|
||||
|
||||
public void processFrames(PoseFrame frames, float targetHeight) {
|
||||
processFrames(frames, true, targetHeight);
|
||||
}
|
||||
|
||||
public void processFrames(PoseFrame[] frames, float targetHeight, Consumer<Epoch> epochCallback) {
|
||||
|
||||
public void processFrames(PoseFrame frames, float targetHeight, Consumer<Epoch> epochCallback) {
|
||||
processFrames(frames, true, targetHeight, epochCallback);
|
||||
}
|
||||
|
||||
public float processFrames(PoseFrame[] frames, boolean calcInitError, float targetHeight) {
|
||||
|
||||
public float processFrames(PoseFrame frames, boolean calcInitError, float targetHeight) {
|
||||
return processFrames(frames, calcInitError, targetHeight, null);
|
||||
}
|
||||
|
||||
public float processFrames(PoseFrame[] frames, boolean calcInitError, float targetHeight, Consumer<Epoch> epochCallback) {
|
||||
SimpleSkeleton skeleton1 = new SimpleSkeleton(configs, staticConfigs);
|
||||
SimpleSkeleton skeleton2 = new SimpleSkeleton(configs, staticConfigs);
|
||||
|
||||
|
||||
public float processFrames(PoseFrame frames, boolean calcInitError, float targetHeight, Consumer<Epoch> epochCallback) {
|
||||
final int frameCount = frames.getMaxFrameCount();
|
||||
|
||||
final SimpleSkeleton skeleton1 = new SimpleSkeleton(configs, staticConfigs);
|
||||
final TrackerFrame[] trackerBuffer1 = new TrackerFrame[frames.getTrackerCount()];
|
||||
|
||||
frames.getFrames(0, trackerBuffer1);
|
||||
reloadConfigValues(trackerBuffer1); // Reload configs and detect chest tracker from the first frame
|
||||
|
||||
final SimpleSkeleton skeleton2 = new SimpleSkeleton(configs, staticConfigs);
|
||||
final TrackerFrame[] trackerBuffer2 = new TrackerFrame[frames.getTrackerCount()];
|
||||
|
||||
// If target height isn't specified, auto-detect
|
||||
if (targetHeight < 0f) {
|
||||
if (skeleton != null) {
|
||||
if(targetHeight < 0f) {
|
||||
if(skeleton != null) {
|
||||
targetHeight = getHeight(skeleton.getSkeletonConfig());
|
||||
LogManager.log.warning("[AutoBone] Target height loaded from skeleton (Make sure you reset before running!): " + targetHeight);
|
||||
} else {
|
||||
float hmdHeight = getMaxHmdHeight(frames);
|
||||
if (hmdHeight <= 0.50f) {
|
||||
if(hmdHeight <= 0.50f) {
|
||||
LogManager.log.warning("[AutoBone] Max headset height detected (Value seems too low, did you not stand up straight while measuring?): " + hmdHeight);
|
||||
} else {
|
||||
LogManager.log.info("[AutoBone] Max headset height detected: " + hmdHeight);
|
||||
}
|
||||
|
||||
|
||||
// Estimate target height from HMD height
|
||||
targetHeight = hmdHeight;
|
||||
}
|
||||
}
|
||||
|
||||
for (int epoch = calcInitError ? -1 : 0; epoch < numEpochs; epoch++) {
|
||||
|
||||
for(int epoch = calcInitError ? -1 : 0; epoch < numEpochs; epoch++) {
|
||||
float sumError = 0f;
|
||||
int errorCount = 0;
|
||||
|
||||
float adjustRate = epoch >= 0 ? (float)(initialAdjustRate / Math.pow(adjustRateDecay, epoch)) : 0f;
|
||||
|
||||
for (int cursorOffset = minDataDistance; cursorOffset <= maxDataDistance && cursorOffset < frames.length; cursorOffset++) {
|
||||
for (int frameCursor = 0; frameCursor < frames.length - cursorOffset; frameCursor += cursorIncrement) {
|
||||
PoseFrame frame1 = frames[frameCursor];
|
||||
PoseFrame frame2 = frames[frameCursor + cursorOffset];
|
||||
|
||||
// If there's missing data, throw an exception
|
||||
if (frame1 == null || frame2 == null) {
|
||||
throw new NullPointerException("Frames are missing from processing data");
|
||||
}
|
||||
|
||||
|
||||
float adjustRate = epoch >= 0 ? (float) (initialAdjustRate / Math.pow(adjustRateDecay, epoch)) : 0f;
|
||||
|
||||
for(int cursorOffset = minDataDistance; cursorOffset <= maxDataDistance && cursorOffset < frameCount; cursorOffset++) {
|
||||
for(int frameCursor = 0; frameCursor < frameCount - cursorOffset; frameCursor += cursorIncrement) {
|
||||
frames.getFrames(frameCursor, trackerBuffer1);
|
||||
frames.getFrames(frameCursor + cursorOffset, trackerBuffer2);
|
||||
|
||||
skeleton1.setSkeletonConfigs(configs);
|
||||
skeleton2.setSkeletonConfigs(configs);
|
||||
|
||||
skeleton1.setPoseFromFrame(frame1);
|
||||
skeleton2.setPoseFromFrame(frame2);
|
||||
|
||||
|
||||
skeleton1.setPoseFromFrame(trackerBuffer1);
|
||||
skeleton2.setPoseFromFrame(trackerBuffer2);
|
||||
|
||||
float totalLength = getLengthSum(configs);
|
||||
float curHeight = getHeight(configs, staticConfigs);
|
||||
float errorDeriv = getErrorDeriv(frame1, frame2, skeleton1, skeleton2, targetHeight - curHeight);
|
||||
float errorDeriv = getErrorDeriv(trackerBuffer1, trackerBuffer2, skeleton1, skeleton2, targetHeight - curHeight);
|
||||
float error = errorFunc(errorDeriv);
|
||||
|
||||
|
||||
// In case of fire
|
||||
if (Float.isNaN(error) || Float.isInfinite(error)) {
|
||||
if(Float.isNaN(error) || Float.isInfinite(error)) {
|
||||
// Extinguish
|
||||
LogManager.log.warning("[AutoBone] Error value is invalid, resetting variables to recover");
|
||||
reloadConfigValues();
|
||||
|
||||
reloadConfigValues(trackerBuffer1);
|
||||
|
||||
// Reset error sum values
|
||||
sumError = 0f;
|
||||
errorCount = 0;
|
||||
|
||||
|
||||
// Continue on new data
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Store the error count for logging purposes
|
||||
sumError += errorDeriv;
|
||||
errorCount++;
|
||||
|
||||
|
||||
float adjustVal = error * adjustRate;
|
||||
|
||||
for (Entry<String, Float> entry : configs.entrySet()) {
|
||||
|
||||
for(Entry<String, Float> entry : configs.entrySet()) {
|
||||
// Skip adjustment if the epoch is before starting (for logging only)
|
||||
if (epoch < 0) {
|
||||
if(epoch < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
float originalLength = entry.getValue();
|
||||
|
||||
|
||||
// Try positive and negative adjustments
|
||||
boolean isHeightVar = heightConfigs.contains(entry.getKey());
|
||||
float minError = errorDeriv;
|
||||
float finalNewLength = -1f;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for(int i = 0; i < 2; i++) {
|
||||
// Scale by the ratio for smooth adjustment and more stable results
|
||||
float curAdjustVal = ((i == 0 ? adjustVal : -adjustVal) * originalLength) / totalLength;
|
||||
float newLength = originalLength + curAdjustVal;
|
||||
|
||||
|
||||
// No small or negative numbers!!! Bad algorithm!
|
||||
if (newLength < 0.01f) {
|
||||
if(newLength < 0.01f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
updateSkeletonBoneLength(skeleton1, skeleton2, entry.getKey(), newLength);
|
||||
|
||||
|
||||
float newHeight = isHeightVar ? curHeight + curAdjustVal : curHeight;
|
||||
float newErrorDeriv = getErrorDeriv(frame1, frame2, skeleton1, skeleton2, targetHeight - newHeight);
|
||||
|
||||
if (newErrorDeriv < minError) {
|
||||
float newErrorDeriv = getErrorDeriv(trackerBuffer1, trackerBuffer2, skeleton1, skeleton2, targetHeight - newHeight);
|
||||
|
||||
if(newErrorDeriv < minError) {
|
||||
minError = newErrorDeriv;
|
||||
finalNewLength = newLength;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalNewLength > 0f) {
|
||||
|
||||
if(finalNewLength > 0f) {
|
||||
entry.setValue(finalNewLength);
|
||||
}
|
||||
|
||||
|
||||
// Reset the length to minimize bias in other variables, it's applied later
|
||||
updateSkeletonBoneLength(skeleton1, skeleton2, entry.getKey(), originalLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate average error over the epoch
|
||||
float avgError = errorCount > 0 ? sumError / errorCount : -1f;
|
||||
LogManager.log.info("[AutoBone] Epoch " + (epoch + 1) + " average error: " + avgError);
|
||||
|
||||
if (epochCallback != null) {
|
||||
|
||||
if(epochCallback != null) {
|
||||
epochCallback.accept(new Epoch(epoch + 1, avgError));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float finalHeight = getHeight(configs, staticConfigs);
|
||||
LogManager.log.info("[AutoBone] Target height: " + targetHeight + " New height: " + finalHeight);
|
||||
|
||||
|
||||
return Math.abs(finalHeight - targetHeight);
|
||||
}
|
||||
|
||||
|
||||
// The change in position of the ankle over time
|
||||
protected float getSlideErrorDeriv(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
|
||||
float slideLeft = skeleton1.getLeftFootPos().distance(skeleton2.getLeftFootPos());
|
||||
float slideRight = skeleton1.getRightFootPos().distance(skeleton2.getRightFootPos());
|
||||
|
||||
float slideLeft = skeleton1.getNodePosition(TrackerBodyPosition.LEFT_ANKLE).distance(skeleton2.getNodePosition(TrackerBodyPosition.LEFT_ANKLE));
|
||||
float slideRight = skeleton1.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE).distance(skeleton2.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE));
|
||||
|
||||
// Divide by 4 to halve and average, it's halved because you want to approach a midpoint, not the other point
|
||||
return (slideLeft + slideRight) / 4f;
|
||||
}
|
||||
|
||||
|
||||
// The offset between both feet at one instant and over time
|
||||
protected float getOffsetErrorDeriv(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
|
||||
float dist1 = Math.abs(skeleton1.getLeftFootPos().y - skeleton1.getRightFootPos().y);
|
||||
float dist2 = Math.abs(skeleton2.getLeftFootPos().y - skeleton2.getRightFootPos().y);
|
||||
|
||||
float dist3 = Math.abs(skeleton1.getLeftFootPos().y - skeleton2.getRightFootPos().y);
|
||||
float dist4 = Math.abs(skeleton1.getLeftFootPos().y - skeleton2.getRightFootPos().y);
|
||||
|
||||
float dist5 = Math.abs(skeleton1.getLeftFootPos().y - skeleton2.getLeftFootPos().y);
|
||||
float dist6 = Math.abs(skeleton1.getRightFootPos().y - skeleton2.getRightFootPos().y);
|
||||
|
||||
float skeleton1Left = skeleton1.getNodePosition(TrackerBodyPosition.LEFT_ANKLE).getY();
|
||||
float skeleton1Right = skeleton1.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE).getY();
|
||||
|
||||
float skeleton2Left = skeleton2.getNodePosition(TrackerBodyPosition.LEFT_ANKLE).getY();
|
||||
float skeleton2Right = skeleton2.getNodePosition(TrackerBodyPosition.RIGHT_ANKLE).getY();
|
||||
|
||||
float dist1 = Math.abs(skeleton1Left - skeleton1Right);
|
||||
float dist2 = Math.abs(skeleton2Left - skeleton2Right);
|
||||
|
||||
float dist3 = Math.abs(skeleton1Left - skeleton2Right);
|
||||
float dist4 = Math.abs(skeleton2Left - skeleton1Right);
|
||||
|
||||
float dist5 = Math.abs(skeleton1Left - skeleton2Left);
|
||||
float dist6 = Math.abs(skeleton1Right - skeleton2Right);
|
||||
|
||||
// Divide by 12 to halve and average, it's halved because you want to approach a midpoint, not the other point
|
||||
return (dist1 + dist2 + dist3 + dist4 + dist5 + dist6) / 12f;
|
||||
}
|
||||
|
||||
|
||||
// The distance from average human proportions
|
||||
protected float getProportionErrorDeriv(SimpleSkeleton skeleton) {
|
||||
Float neckLength = skeleton.getSkeletonConfig("Neck");
|
||||
@@ -394,117 +396,119 @@ public class AutoBone {
|
||||
Float waistLength = skeleton.getSkeletonConfig("Waist");
|
||||
Float legsLength = skeleton.getSkeletonConfig("Legs length");
|
||||
Float kneeHeight = skeleton.getSkeletonConfig("Knee height");
|
||||
|
||||
float chestWaist = chestLength != null && waistLength != null ? Math.abs((chestLength / waistLength) - 0.5f) : 0f;
|
||||
float legBody = legsLength != null && waistLength != null && neckLength != null ? Math.abs((legsLength / (waistLength + neckLength)) - 1.1235f) : 0f;
|
||||
float kneeLeg = kneeHeight != null && legsLength != null ? Math.abs((kneeHeight / legsLength) - 0.5f) : 0f;
|
||||
|
||||
// SD of 0.07, capture 68% within range
|
||||
float sdValue = 0.07f;
|
||||
if (legBody <= sdValue) {
|
||||
|
||||
float chestWaist = chestLength != null && waistLength != null ? Math.abs((chestLength / waistLength) - chestWaistRatio) : 0f;
|
||||
float legBody = legsLength != null && waistLength != null && neckLength != null ? Math.abs((legsLength / (waistLength + neckLength)) - legBodyRatio) : 0f;
|
||||
float kneeLeg = kneeHeight != null && legsLength != null ? Math.abs((kneeHeight / legsLength) - kneeLegRatio) : 0f;
|
||||
|
||||
if(legBody <= legBodyRatioRange) {
|
||||
legBody = 0f;
|
||||
} else {
|
||||
legBody -= sdValue;
|
||||
legBody -= legBodyRatioRange;
|
||||
}
|
||||
|
||||
|
||||
return (chestWaist + legBody + kneeLeg) / 3f;
|
||||
}
|
||||
|
||||
|
||||
// The distance of any points to the corresponding absolute position
|
||||
protected float getPositionErrorDeriv(PoseFrame frame, SimpleSkeleton skeleton) {
|
||||
protected float getPositionErrorDeriv(TrackerFrame[] frame, SimpleSkeleton skeleton) {
|
||||
float offset = 0f;
|
||||
int offsetCount = 0;
|
||||
|
||||
if (frame.positions != null) {
|
||||
for (Entry<String, Vector3f> entry : frame.positions.entrySet()) {
|
||||
Vector3f nodePos = skeleton.getNodePosition(entry.getKey());
|
||||
if (nodePos != null) {
|
||||
offset += Math.abs(nodePos.distance(entry.getValue()));
|
||||
offsetCount++;
|
||||
}
|
||||
|
||||
for(TrackerFrame trackerFrame : frame) {
|
||||
if(trackerFrame == null || !trackerFrame.hasData(TrackerFrameData.POSITION)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return offsetCount > 0 ? offset / offsetCount : 0f;
|
||||
}
|
||||
|
||||
// The difference between offset of absolute position and the corresponding point over time
|
||||
protected float getPositionOffsetErrorDeriv(PoseFrame frame1, PoseFrame frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
|
||||
float offset = 0f;
|
||||
int offsetCount = 0;
|
||||
|
||||
if (frame1.positions != null && frame2.positions != null) {
|
||||
for (Entry<String, Vector3f> entry : frame1.positions.entrySet()) {
|
||||
Vector3f frame2Pos = frame2.positions.get(entry.getKey());
|
||||
if (frame2Pos == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3f nodePos1 = skeleton1.getNodePosition(entry.getKey());
|
||||
if (nodePos1 == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3f nodePos2 = skeleton2.getNodePosition(entry.getKey());
|
||||
if (nodePos2 == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float dist1 = Math.abs(nodePos1.distance(entry.getValue()));
|
||||
float dist2 = Math.abs(nodePos2.distance(frame2Pos));
|
||||
|
||||
offset += Math.abs(dist2 - dist1);
|
||||
|
||||
Vector3f nodePos = skeleton.getNodePosition(trackerFrame.designation.designation);
|
||||
if(nodePos != null) {
|
||||
offset += Math.abs(nodePos.distance(trackerFrame.position));
|
||||
offsetCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return offsetCount > 0 ? offset / offsetCount : 0f;
|
||||
}
|
||||
|
||||
protected float getErrorDeriv(PoseFrame frame1, PoseFrame frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2, float heightChange) {
|
||||
|
||||
// The difference between offset of absolute position and the corresponding point over time
|
||||
protected float getPositionOffsetErrorDeriv(TrackerFrame[] frame1, TrackerFrame[] frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2) {
|
||||
float offset = 0f;
|
||||
int offsetCount = 0;
|
||||
|
||||
for(TrackerFrame trackerFrame1 : frame1) {
|
||||
if(trackerFrame1 == null || !trackerFrame1.hasData(TrackerFrameData.POSITION)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TrackerFrame trackerFrame2 = TrackerUtils.findTrackerForBodyPosition(frame2, trackerFrame1.designation);
|
||||
if(trackerFrame2 == null || !trackerFrame2.hasData(TrackerFrameData.POSITION)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3f nodePos1 = skeleton1.getNodePosition(trackerFrame1.designation);
|
||||
if(nodePos1 == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3f nodePos2 = skeleton2.getNodePosition(trackerFrame2.designation);
|
||||
if(nodePos2 == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float dist1 = Math.abs(nodePos1.distance(trackerFrame1.position));
|
||||
float dist2 = Math.abs(nodePos2.distance(trackerFrame2.position));
|
||||
|
||||
offset += Math.abs(dist2 - dist1);
|
||||
offsetCount++;
|
||||
}
|
||||
|
||||
return offsetCount > 0 ? offset / offsetCount : 0f;
|
||||
}
|
||||
|
||||
protected float getErrorDeriv(TrackerFrame[] frame1, TrackerFrame[] frame2, SimpleSkeleton skeleton1, SimpleSkeleton skeleton2, float heightChange) {
|
||||
float totalError = 0f;
|
||||
float sumWeight = 0f;
|
||||
|
||||
if (slideErrorFactor > 0f) {
|
||||
|
||||
if(slideErrorFactor > 0f) {
|
||||
totalError += getSlideErrorDeriv(skeleton1, skeleton2) * slideErrorFactor;
|
||||
sumWeight += slideErrorFactor;
|
||||
}
|
||||
|
||||
if (offsetErrorFactor > 0f) {
|
||||
|
||||
if(offsetErrorFactor > 0f) {
|
||||
totalError += getOffsetErrorDeriv(skeleton1, skeleton2) * offsetErrorFactor;
|
||||
sumWeight += offsetErrorFactor;
|
||||
}
|
||||
|
||||
if (proportionErrorFactor > 0f) {
|
||||
|
||||
if(proportionErrorFactor > 0f) {
|
||||
// Either skeleton will work fine, skeleton1 is used as a default
|
||||
totalError += getProportionErrorDeriv(skeleton1) * proportionErrorFactor;
|
||||
sumWeight += proportionErrorFactor;
|
||||
}
|
||||
|
||||
if (heightErrorFactor > 0f) {
|
||||
|
||||
if(heightErrorFactor > 0f) {
|
||||
totalError += Math.abs(heightChange) * heightErrorFactor;
|
||||
sumWeight += heightErrorFactor;
|
||||
}
|
||||
|
||||
if (positionErrorFactor > 0f) {
|
||||
|
||||
if(positionErrorFactor > 0f) {
|
||||
totalError += (getPositionErrorDeriv(frame1, skeleton1) + getPositionErrorDeriv(frame2, skeleton2) / 2f) * positionErrorFactor;
|
||||
sumWeight += positionErrorFactor;
|
||||
}
|
||||
|
||||
if (positionOffsetErrorFactor > 0f) {
|
||||
|
||||
if(positionOffsetErrorFactor > 0f) {
|
||||
totalError += getPositionOffsetErrorDeriv(frame1, frame2, skeleton1, skeleton2) * positionOffsetErrorFactor;
|
||||
sumWeight += positionOffsetErrorFactor;
|
||||
}
|
||||
|
||||
|
||||
// Minimize sliding, minimize foot height offset, minimize change in total height
|
||||
return sumWeight > 0f ? totalError / sumWeight : 0f;
|
||||
}
|
||||
|
||||
|
||||
// Mean square error function
|
||||
protected static float errorFunc(float errorDeriv) {
|
||||
return 0.5f * (errorDeriv * errorDeriv);
|
||||
}
|
||||
|
||||
|
||||
protected void updateSkeletonBoneLength(SimpleSkeleton skeleton1, SimpleSkeleton skeleton2, String joint, float newLength) {
|
||||
skeleton1.setSkeletonConfig(joint, newLength, true);
|
||||
skeleton2.setSkeletonConfig(joint, newLength, true);
|
||||
@@ -1,26 +1,29 @@
|
||||
package io.eiren.gui.autobone;
|
||||
package dev.slimevr.autobone;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import dev.slimevr.poserecorder.TrackerFrame;
|
||||
import dev.slimevr.poserecorder.TrackerFrameData;
|
||||
import io.eiren.vr.processor.HumanSkeletonWithLegs;
|
||||
import io.eiren.vr.processor.HumanSkeletonWithWaist;
|
||||
import io.eiren.vr.processor.TrackerBodyPosition;
|
||||
import io.eiren.vr.processor.TransformNode;
|
||||
import io.eiren.vr.trackers.TrackerUtils;
|
||||
import io.eiren.yaml.YamlFile;
|
||||
|
||||
public class SimpleSkeleton {
|
||||
|
||||
|
||||
// Waist
|
||||
protected final TransformNode hmdNode = new TransformNode("HMD", false);
|
||||
protected final TransformNode headNode = new TransformNode("Head", false);
|
||||
protected final TransformNode neckNode = new TransformNode("Neck", false);
|
||||
protected final TransformNode waistNode = new TransformNode("Waist", false);
|
||||
protected final TransformNode chestNode = new TransformNode("Chest", false);
|
||||
|
||||
|
||||
protected float chestDistance = 0.42f;
|
||||
/**
|
||||
* Distance from eyes to waist
|
||||
@@ -34,7 +37,7 @@ public class SimpleSkeleton {
|
||||
* 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);
|
||||
@@ -42,7 +45,7 @@ public class SimpleSkeleton {
|
||||
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
|
||||
*/
|
||||
@@ -55,142 +58,168 @@ public class SimpleSkeleton {
|
||||
* 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(Iterable<Entry<String, Float>> configs, Iterable<Entry<String, Float>> altConfigs) {
|
||||
|
||||
public SimpleSkeleton(Map<String, Float> configs, Map<String, Float> altConfigs) {
|
||||
// Initialize
|
||||
this();
|
||||
|
||||
|
||||
// Set configs
|
||||
if (altConfigs != null) {
|
||||
if(altConfigs != null) {
|
||||
// Set alts first, so if there's any overlap it doesn't affect the values
|
||||
setSkeletonConfigs(altConfigs);
|
||||
}
|
||||
setSkeletonConfigs(configs);
|
||||
}
|
||||
|
||||
public SimpleSkeleton(Map<String, Float> configs, Map<String, Float> altConfigs) {
|
||||
this(configs.entrySet(), altConfigs.entrySet());
|
||||
}
|
||||
|
||||
public SimpleSkeleton(Iterable<Entry<String, Float>> configs) {
|
||||
|
||||
public SimpleSkeleton(Map<String, Float> configs) {
|
||||
this(configs, null);
|
||||
}
|
||||
|
||||
public SimpleSkeleton(Map<String, Float> configs) {
|
||||
this(configs.entrySet());
|
||||
}
|
||||
|
||||
public void setPoseFromSkeleton(HumanSkeletonWithLegs humanSkeleton) {
|
||||
TransformNode rootNode = humanSkeleton.getRootNode();
|
||||
|
||||
// Copy headset position
|
||||
hmdNode.localTransform.setTranslation(rootNode.localTransform.getTranslation());
|
||||
|
||||
// Copy all rotations
|
||||
rootNode.depthFirstTraversal(visitor -> {
|
||||
TransformNode targetNode = nodes.get(visitor.getName());
|
||||
|
||||
// Handle unexpected nodes gracefully
|
||||
if (targetNode != null) {
|
||||
targetNode.localTransform.setRotation(visitor.localTransform.getRotation());
|
||||
|
||||
public void setPoseFromFrame(TrackerFrame[] frame) {
|
||||
|
||||
TrackerFrame hmd = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.HMD);
|
||||
|
||||
if(hmd != null) {
|
||||
if(hmd.hasData(TrackerFrameData.ROTATION)) {
|
||||
hmdNode.localTransform.setRotation(hmd.rotation);
|
||||
headNode.localTransform.setRotation(hmd.rotation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setPoseFromFrame(PoseFrame frame) {
|
||||
// Copy headset position
|
||||
hmdNode.localTransform.setTranslation(frame.rootPos);
|
||||
|
||||
if (frame.rotations != null) {
|
||||
// Copy all rotations
|
||||
for (Entry<String, Quaternion> rotation : frame.rotations.entrySet()) {
|
||||
TransformNode targetNode = nodes.get(rotation.getKey());
|
||||
|
||||
// Handle unexpected nodes gracefully
|
||||
if (targetNode != null) {
|
||||
targetNode.localTransform.setRotation(rotation.getValue());
|
||||
}
|
||||
|
||||
if(hmd.hasData(TrackerFrameData.POSITION)) {
|
||||
hmdNode.localTransform.setTranslation(hmd.position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TrackerFrame chest = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.CHEST, TrackerBodyPosition.WAIST);
|
||||
setRotation(chest, neckNode);
|
||||
|
||||
TrackerFrame waist = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
|
||||
setRotation(waist, chestNode);
|
||||
|
||||
TrackerFrame leftLeg = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.LEFT_LEG);
|
||||
TrackerFrame rightLeg = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.RIGHT_LEG);
|
||||
|
||||
averagePelvis(waist, leftLeg, rightLeg);
|
||||
|
||||
setRotation(leftLeg, leftHipNode);
|
||||
setRotation(rightLeg, rightHipNode);
|
||||
|
||||
TrackerFrame leftAnkle = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.LEFT_ANKLE);
|
||||
setRotation(leftAnkle, rightKneeNode);
|
||||
|
||||
TrackerFrame rightAnkle = TrackerUtils.findTrackerForBodyPosition(frame, TrackerBodyPosition.RIGHT_ANKLE);
|
||||
setRotation(rightAnkle, leftKneeNode);
|
||||
|
||||
updatePose();
|
||||
}
|
||||
|
||||
public void setSkeletonConfigs(Iterable<Entry<String, Float>> configs) {
|
||||
for (Entry<String, Float> config : configs) {
|
||||
setSkeletonConfig(config.getKey(), config.getValue());
|
||||
|
||||
public void setRotation(TrackerFrame trackerFrame, TransformNode node) {
|
||||
if(trackerFrame != null && trackerFrame.hasData(TrackerFrameData.ROTATION)) {
|
||||
node.localTransform.setRotation(trackerFrame.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSkeletonConfigs(Map<String, Float> configs) {
|
||||
setSkeletonConfigs(configs.entrySet());
|
||||
|
||||
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) {
|
||||
if(updatePose) {
|
||||
headNode.update();
|
||||
}
|
||||
break;
|
||||
case "Neck":
|
||||
neckLength = newLength;
|
||||
neckNode.localTransform.setTranslation(0, -neckLength, 0);
|
||||
if (updatePose) {
|
||||
if(updatePose) {
|
||||
neckNode.update();
|
||||
}
|
||||
break;
|
||||
case "Waist":
|
||||
waistDistance = newLength;
|
||||
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
|
||||
if (updatePose) {
|
||||
if(updatePose) {
|
||||
waistNode.update();
|
||||
}
|
||||
break;
|
||||
@@ -198,7 +227,7 @@ public class SimpleSkeleton {
|
||||
chestDistance = newLength;
|
||||
chestNode.localTransform.setTranslation(0, -chestDistance, 0);
|
||||
waistNode.localTransform.setTranslation(0, -(waistDistance - chestDistance), 0);
|
||||
if (updatePose) {
|
||||
if(updatePose) {
|
||||
chestNode.update();
|
||||
}
|
||||
break;
|
||||
@@ -206,7 +235,7 @@ public class SimpleSkeleton {
|
||||
hipsWidth = newLength;
|
||||
leftHipNode.localTransform.setTranslation(-hipsWidth / 2, 0, 0);
|
||||
rightHipNode.localTransform.setTranslation(hipsWidth / 2, 0, 0);
|
||||
if (updatePose) {
|
||||
if(updatePose) {
|
||||
leftHipNode.update();
|
||||
rightHipNode.update();
|
||||
}
|
||||
@@ -217,7 +246,7 @@ public class SimpleSkeleton {
|
||||
rightAnkleNode.localTransform.setTranslation(0, -kneeHeight, 0);
|
||||
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
|
||||
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
|
||||
if (updatePose) {
|
||||
if(updatePose) {
|
||||
leftKneeNode.update();
|
||||
rightKneeNode.update();
|
||||
}
|
||||
@@ -226,14 +255,14 @@ public class SimpleSkeleton {
|
||||
legsLength = newLength;
|
||||
leftKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
|
||||
rightKneeNode.localTransform.setTranslation(0, -(legsLength - kneeHeight), 0);
|
||||
if (updatePose) {
|
||||
if(updatePose) {
|
||||
leftKneeNode.update();
|
||||
rightKneeNode.update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Float getSkeletonConfig(String joint) {
|
||||
switch(joint) {
|
||||
case "Head":
|
||||
@@ -251,38 +280,70 @@ public class SimpleSkeleton {
|
||||
case "Legs length":
|
||||
return legsLength;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void updatePose() {
|
||||
hmdNode.update();
|
||||
}
|
||||
|
||||
|
||||
public TransformNode getNode(String node) {
|
||||
return nodes.get(node);
|
||||
}
|
||||
|
||||
public TransformNode getNode(TrackerBodyPosition bodyPosition) {
|
||||
return getNode(bodyPosition, false);
|
||||
}
|
||||
|
||||
public TransformNode getNode(TrackerBodyPosition bodyPosition, boolean rotationNode) {
|
||||
if(bodyPosition == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch(bodyPosition) {
|
||||
case HMD:
|
||||
return hmdNode;
|
||||
case CHEST:
|
||||
return rotationNode ? neckNode : chestNode;
|
||||
case WAIST:
|
||||
return rotationNode ? chestNode : waistNode;
|
||||
|
||||
case LEFT_LEG:
|
||||
return rotationNode ? leftHipNode : leftKneeNode;
|
||||
case RIGHT_LEG:
|
||||
return rotationNode ? rightHipNode : rightKneeNode;
|
||||
|
||||
case LEFT_ANKLE:
|
||||
return rotationNode ? leftKneeNode : leftAnkleNode;
|
||||
case RIGHT_ANKLE:
|
||||
return rotationNode ? rightKneeNode : rightAnkleNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Vector3f getNodePosition(String node) {
|
||||
TransformNode transformNode = nodes.get(node);
|
||||
TransformNode transformNode = getNode(node);
|
||||
return transformNode != null ? transformNode.worldTransform.getTranslation() : null;
|
||||
}
|
||||
|
||||
public Vector3f getHMDPos() {
|
||||
return hmdNode.worldTransform.getTranslation();
|
||||
|
||||
public Vector3f getNodePosition(TrackerBodyPosition bodyPosition) {
|
||||
TransformNode node = getNode(bodyPosition);
|
||||
if(node == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return node.worldTransform.getTranslation();
|
||||
}
|
||||
|
||||
public Vector3f getLeftFootPos() {
|
||||
return leftAnkleNode.worldTransform.getTranslation();
|
||||
}
|
||||
|
||||
public Vector3f getRightFootPos() {
|
||||
return rightAnkleNode.worldTransform.getTranslation();
|
||||
}
|
||||
|
||||
|
||||
public void saveConfigs(YamlFile config) {
|
||||
// Save waist configs
|
||||
config.setProperty("body.headShift", headShift);
|
||||
config.setProperty("body.neckLength", neckLength);
|
||||
config.setProperty("body.waistDistance", waistDistance);
|
||||
config.setProperty("body.chestDistance", chestDistance);
|
||||
|
||||
|
||||
// Save leg configs
|
||||
config.setProperty("body.hipsWidth", hipsWidth);
|
||||
config.setProperty("body.kneeHeight", kneeHeight);
|
||||
442
src/main/java/dev/slimevr/gui/AutoBoneWindow.java
Normal file
442
src/main/java/dev/slimevr/gui/AutoBoneWindow.java
Normal file
@@ -0,0 +1,442 @@
|
||||
package dev.slimevr.gui;
|
||||
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import io.eiren.gui.EJBox;
|
||||
import io.eiren.gui.SkeletonConfig;
|
||||
import io.eiren.util.StringUtils;
|
||||
import io.eiren.util.ann.AWTThread;
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.eiren.vr.VRServer;
|
||||
|
||||
import javax.swing.event.MouseInputAdapter;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import dev.slimevr.autobone.AutoBone;
|
||||
import dev.slimevr.poserecorder.PoseFrame;
|
||||
import dev.slimevr.poserecorder.PoseFrameIO;
|
||||
import dev.slimevr.poserecorder.PoseRecorder;
|
||||
|
||||
public class AutoBoneWindow extends JFrame {
|
||||
|
||||
private static File saveDir = new File("Recordings");
|
||||
private static File loadDir = new File("LoadRecordings");
|
||||
|
||||
private EJBox pane;
|
||||
|
||||
private final transient VRServer server;
|
||||
private final transient SkeletonConfig skeletonConfig;
|
||||
private final transient PoseRecorder poseRecorder;
|
||||
private final transient AutoBone autoBone;
|
||||
|
||||
private transient Thread recordingThread = null;
|
||||
private transient Thread saveRecordingThread = null;
|
||||
private transient Thread autoBoneThread = null;
|
||||
|
||||
private JButton saveRecordingButton;
|
||||
private JButton adjustButton;
|
||||
private JButton applyButton;
|
||||
|
||||
private JLabel processLabel;
|
||||
private JLabel lengthsLabel;
|
||||
|
||||
public AutoBoneWindow(VRServer server, SkeletonConfig skeletonConfig) {
|
||||
super("Skeleton Auto-Configuration");
|
||||
|
||||
this.server = server;
|
||||
this.skeletonConfig = skeletonConfig;
|
||||
this.poseRecorder = new PoseRecorder(server);
|
||||
this.autoBone = new AutoBone(server);
|
||||
|
||||
getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.PAGE_AXIS));
|
||||
add(new JScrollPane(pane = new EJBox(BoxLayout.PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
|
||||
|
||||
build();
|
||||
}
|
||||
|
||||
private String getLengthsString() {
|
||||
boolean first = true;
|
||||
StringBuilder configInfo = new StringBuilder("");
|
||||
for(Entry<String, Float> entry : autoBone.configs.entrySet()) {
|
||||
if(!first) {
|
||||
configInfo.append(", ");
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
configInfo.append(entry.getKey() + ": " + StringUtils.prettyNumber(entry.getValue() * 100f, 2));
|
||||
}
|
||||
|
||||
return configInfo.toString();
|
||||
}
|
||||
|
||||
private void saveRecording(PoseFrame frames) {
|
||||
if(saveDir.isDirectory() || saveDir.mkdirs()) {
|
||||
File saveRecording;
|
||||
int recordingIndex = 1;
|
||||
do {
|
||||
saveRecording = new File(saveDir, "ABRecording" + recordingIndex++ + ".pfr");
|
||||
} while(saveRecording.exists());
|
||||
|
||||
LogManager.log.info("[AutoBone] Exporting frames to \"" + saveRecording.getPath() + "\"...");
|
||||
if(PoseFrameIO.writeToFile(saveRecording, frames)) {
|
||||
LogManager.log.info("[AutoBone] Done exporting! Recording can be found at \"" + saveRecording.getPath() + "\".");
|
||||
} else {
|
||||
LogManager.log.severe("[AutoBone] Failed to export the recording to \"" + saveRecording.getPath() + "\".");
|
||||
}
|
||||
} else {
|
||||
LogManager.log.severe("[AutoBone] Failed to create the recording directory \"" + saveDir.getPath() + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Pair<String, PoseFrame>> loadRecordings() {
|
||||
List<Pair<String, PoseFrame>> recordings = new FastList<Pair<String, PoseFrame>>();
|
||||
if(loadDir.isDirectory()) {
|
||||
File[] files = loadDir.listFiles();
|
||||
if(files != null) {
|
||||
for(File file : files) {
|
||||
if(file.isFile() && org.apache.commons.lang3.StringUtils.endsWithIgnoreCase(file.getName(), ".pfr")) {
|
||||
LogManager.log.info("[AutoBone] Detected recording at \"" + file.getPath() + "\", loading frames...");
|
||||
PoseFrame frames = PoseFrameIO.readFromFile(file);
|
||||
|
||||
if(frames == null) {
|
||||
LogManager.log.severe("Reading frames from \"" + file.getPath() + "\" failed...");
|
||||
} else {
|
||||
recordings.add(Pair.of(file.getName(), frames));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recordings;
|
||||
}
|
||||
|
||||
private float processFrames(PoseFrame frames) {
|
||||
autoBone.minDataDistance = server.config.getInt("autobone.minimumDataDistance", autoBone.minDataDistance);
|
||||
autoBone.maxDataDistance = server.config.getInt("autobone.maximumDataDistance", autoBone.maxDataDistance);
|
||||
|
||||
autoBone.numEpochs = server.config.getInt("autobone.epochCount", autoBone.numEpochs);
|
||||
|
||||
autoBone.initialAdjustRate = server.config.getFloat("autobone.adjustRate", autoBone.initialAdjustRate);
|
||||
autoBone.adjustRateDecay = server.config.getFloat("autobone.adjustRateDecay", autoBone.adjustRateDecay);
|
||||
|
||||
autoBone.slideErrorFactor = server.config.getFloat("autobone.slideErrorFactor", autoBone.slideErrorFactor);
|
||||
autoBone.offsetErrorFactor = server.config.getFloat("autobone.offsetErrorFactor", autoBone.offsetErrorFactor);
|
||||
autoBone.proportionErrorFactor = server.config.getFloat("autobone.proportionErrorFactor", autoBone.proportionErrorFactor);
|
||||
autoBone.heightErrorFactor = server.config.getFloat("autobone.heightErrorFactor", autoBone.heightErrorFactor);
|
||||
autoBone.positionErrorFactor = server.config.getFloat("autobone.positionErrorFactor", autoBone.positionErrorFactor);
|
||||
autoBone.positionOffsetErrorFactor = server.config.getFloat("autobone.positionOffsetErrorFactor", autoBone.positionOffsetErrorFactor);
|
||||
|
||||
boolean calcInitError = server.config.getBoolean("autobone.calculateInitialError", true);
|
||||
float targetHeight = server.config.getFloat("autobone.manualTargetHeight", -1f);
|
||||
return autoBone.processFrames(frames, calcInitError, targetHeight, (epoch) -> {
|
||||
processLabel.setText(epoch.toString());
|
||||
lengthsLabel.setText(getLengthsString());
|
||||
});
|
||||
}
|
||||
|
||||
@AWTThread
|
||||
private void build() {
|
||||
pane.add(new EJBox(BoxLayout.LINE_AXIS) {
|
||||
{
|
||||
setBorder(new EmptyBorder(i(5)));
|
||||
add(new JButton("Start Recording") {
|
||||
{
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
// Prevent running multiple times
|
||||
if(!isEnabled() || recordingThread != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if(poseRecorder.isReadyToRecord()) {
|
||||
setText("Recording...");
|
||||
// 1000 samples at 20 ms per sample is 20 seconds
|
||||
int sampleCount = server.config.getInt("autobone.sampleCount", 1000);
|
||||
long sampleRate = server.config.getLong("autobone.sampleRateMs", 20L);
|
||||
Future<PoseFrame> framesFuture = poseRecorder.startFrameRecording(sampleCount, sampleRate);
|
||||
PoseFrame frames = framesFuture.get();
|
||||
LogManager.log.info("[AutoBone] Done recording!");
|
||||
|
||||
saveRecordingButton.setEnabled(true);
|
||||
adjustButton.setEnabled(true);
|
||||
|
||||
if(server.config.getBoolean("autobone.saveRecordings", false)) {
|
||||
setText("Saving...");
|
||||
saveRecording(frames);
|
||||
}
|
||||
} else {
|
||||
setText("Not Ready...");
|
||||
LogManager.log.severe("[AutoBone] Unable to record...");
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
return;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
setText("Recording Failed...");
|
||||
LogManager.log.severe("[AutoBone] Failed recording!", e);
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch(Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
setText("Start Recording");
|
||||
recordingThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
recordingThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
add(saveRecordingButton = new JButton("Save Recording") {
|
||||
{
|
||||
setEnabled(poseRecorder.hasRecording());
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
// Prevent running multiple times
|
||||
if(!isEnabled() || saveRecordingThread != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Future<PoseFrame> framesFuture = poseRecorder.getFramesAsync();
|
||||
if(framesFuture != null) {
|
||||
setText("Waiting for Recording...");
|
||||
PoseFrame frames = framesFuture.get();
|
||||
|
||||
if(frames.getTrackerCount() <= 0) {
|
||||
throw new IllegalStateException("Recording has no trackers");
|
||||
}
|
||||
|
||||
if(frames.getMaxFrameCount() <= 0) {
|
||||
throw new IllegalStateException("Recording has no frames");
|
||||
}
|
||||
|
||||
setText("Saving...");
|
||||
saveRecording(frames);
|
||||
|
||||
setText("Recording Saved!");
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch(Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} else {
|
||||
setText("No Recording...");
|
||||
LogManager.log.severe("[AutoBone] Unable to save, no recording was done...");
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch(Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
setText("Saving Failed...");
|
||||
LogManager.log.severe("[AutoBone] Failed to save recording!", e);
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch(Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
setText("Save Recording");
|
||||
saveRecordingThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
saveRecordingThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
add(adjustButton = new JButton("Auto-Adjust") {
|
||||
{
|
||||
// If there are files to load, enable the button
|
||||
setEnabled(poseRecorder.hasRecording() || (loadDir.isDirectory() && loadDir.list().length > 0));
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
// Prevent running multiple times
|
||||
if(!isEnabled() || autoBoneThread != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
setText("Load...");
|
||||
List<Pair<String, PoseFrame>> frameRecordings = loadRecordings();
|
||||
|
||||
if(!frameRecordings.isEmpty()) {
|
||||
LogManager.log.info("[AutoBone] Done loading frames!");
|
||||
} else {
|
||||
Future<PoseFrame> framesFuture = poseRecorder.getFramesAsync();
|
||||
if(framesFuture != null) {
|
||||
setText("Waiting for Recording...");
|
||||
PoseFrame frames = framesFuture.get();
|
||||
|
||||
if(frames.getTrackerCount() <= 0) {
|
||||
throw new IllegalStateException("Recording has no trackers");
|
||||
}
|
||||
|
||||
if(frames.getMaxFrameCount() <= 0) {
|
||||
throw new IllegalStateException("Recording has no frames");
|
||||
}
|
||||
|
||||
frameRecordings.add(Pair.of("<Recording>", frames));
|
||||
} else {
|
||||
setText("No Recordings...");
|
||||
LogManager.log.severe("[AutoBone] No recordings found in \"" + loadDir.getPath() + "\" and no recording was done...");
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch(Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setText("Processing...");
|
||||
LogManager.log.info("[AutoBone] Processing frames...");
|
||||
FastList<Float> heightPercentError = new FastList<Float>(frameRecordings.size());
|
||||
for(Pair<String, PoseFrame> recording : frameRecordings) {
|
||||
LogManager.log.info("[AutoBone] Processing frames from \"" + recording.getKey() + "\"...");
|
||||
|
||||
heightPercentError.add(processFrames(recording.getValue()));
|
||||
LogManager.log.info("[AutoBone] Done processing!");
|
||||
applyButton.setEnabled(true);
|
||||
|
||||
//#region Stats/Values
|
||||
Float neckLength = autoBone.getConfig("Neck");
|
||||
Float chestLength = autoBone.getConfig("Chest");
|
||||
Float waistLength = autoBone.getConfig("Waist");
|
||||
Float hipWidth = autoBone.getConfig("Hips width");
|
||||
Float legsLength = autoBone.getConfig("Legs length");
|
||||
Float kneeHeight = autoBone.getConfig("Knee height");
|
||||
|
||||
float neckWaist = neckLength != null && waistLength != null ? neckLength / waistLength : 0f;
|
||||
float chestWaist = chestLength != null && waistLength != null ? chestLength / waistLength : 0f;
|
||||
float hipWaist = hipWidth != null && waistLength != null ? hipWidth / waistLength : 0f;
|
||||
float legWaist = legsLength != null && waistLength != null ? legsLength / waistLength : 0f;
|
||||
float legBody = legsLength != null && waistLength != null && neckLength != null ? legsLength / (waistLength + neckLength) : 0f;
|
||||
float kneeLeg = kneeHeight != null && legsLength != null ? kneeHeight / legsLength : 0f;
|
||||
|
||||
LogManager.log.info("[AutoBone] Ratios: [{Neck-Waist: " + StringUtils.prettyNumber(neckWaist) + "}, {Chest-Waist: " + StringUtils.prettyNumber(chestWaist) + "}, {Hip-Waist: " + StringUtils.prettyNumber(hipWaist) + "}, {Leg-Waist: " + StringUtils.prettyNumber(legWaist) + "}, {Leg-Body: " + StringUtils.prettyNumber(legBody) + "}, {Knee-Leg: " + StringUtils.prettyNumber(kneeLeg) + "}]");
|
||||
|
||||
String lengthsString = getLengthsString();
|
||||
LogManager.log.info("[AutoBone] Length values: " + lengthsString);
|
||||
lengthsLabel.setText(lengthsString);
|
||||
}
|
||||
|
||||
if(!heightPercentError.isEmpty()) {
|
||||
float mean = 0f;
|
||||
for(float val : heightPercentError) {
|
||||
mean += val;
|
||||
}
|
||||
mean /= heightPercentError.size();
|
||||
|
||||
float std = 0f;
|
||||
for(float val : heightPercentError) {
|
||||
float stdVal = val - mean;
|
||||
std += stdVal * stdVal;
|
||||
}
|
||||
std = (float) Math.sqrt(std / heightPercentError.size());
|
||||
|
||||
LogManager.log.info("[AutoBone] Average height error: " + StringUtils.prettyNumber(mean, 6) + " (SD " + StringUtils.prettyNumber(std, 6) + ")");
|
||||
}
|
||||
//#endregion
|
||||
} catch(Exception e) {
|
||||
setText("Failed...");
|
||||
LogManager.log.severe("[AutoBone] Failed adjustment!", e);
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch(Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
setText("Auto-Adjust");
|
||||
autoBoneThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
autoBoneThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
add(applyButton = new JButton("Apply Values") {
|
||||
{
|
||||
setEnabled(false);
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if(!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoBone.applyConfig();
|
||||
// Update GUI values after applying
|
||||
skeletonConfig.refreshAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
pane.add(new EJBox(BoxLayout.LINE_AXIS) {
|
||||
{
|
||||
setBorder(new EmptyBorder(i(5)));
|
||||
add(processLabel = new JLabel("Processing has not been started..."));
|
||||
}
|
||||
});
|
||||
|
||||
pane.add(new EJBox(BoxLayout.LINE_AXIS) {
|
||||
{
|
||||
setBorder(new EmptyBorder(i(5)));
|
||||
add(lengthsLabel = new JLabel(getLengthsString()));
|
||||
}
|
||||
});
|
||||
|
||||
// Pack and display
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
143
src/main/java/dev/slimevr/poserecorder/PoseFrame.java
Normal file
143
src/main/java/dev/slimevr/poserecorder/PoseFrame.java
Normal file
@@ -0,0 +1,143 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.vr.trackers.Tracker;
|
||||
|
||||
public final class PoseFrame implements Iterable<TrackerFrame[]> {
|
||||
|
||||
private final FastList<PoseFrameTracker> trackers;
|
||||
|
||||
public PoseFrame(FastList<PoseFrameTracker> trackers) {
|
||||
this.trackers = trackers;
|
||||
}
|
||||
|
||||
public PoseFrame(int initialCapacity) {
|
||||
this.trackers = new FastList<PoseFrameTracker>(initialCapacity);
|
||||
}
|
||||
|
||||
public PoseFrame() {
|
||||
this(5);
|
||||
}
|
||||
|
||||
public PoseFrameTracker addTracker(PoseFrameTracker tracker) {
|
||||
trackers.add(tracker);
|
||||
return tracker;
|
||||
}
|
||||
|
||||
public PoseFrameTracker addTracker(Tracker tracker, int initialCapacity) {
|
||||
return addTracker(new PoseFrameTracker(tracker.getName(), initialCapacity));
|
||||
}
|
||||
|
||||
public PoseFrameTracker addTracker(Tracker tracker) {
|
||||
return addTracker(tracker, 5);
|
||||
}
|
||||
|
||||
public PoseFrameTracker removeTracker(int index) {
|
||||
return trackers.remove(index);
|
||||
}
|
||||
|
||||
public PoseFrameTracker removeTracker(PoseFrameTracker tracker) {
|
||||
trackers.remove(tracker);
|
||||
return tracker;
|
||||
}
|
||||
|
||||
public void clearTrackers() {
|
||||
trackers.clear();
|
||||
}
|
||||
|
||||
public void fakeClearTrackers() {
|
||||
trackers.fakeClear();
|
||||
}
|
||||
|
||||
public int getTrackerCount() {
|
||||
return trackers.size();
|
||||
}
|
||||
|
||||
public List<PoseFrameTracker> getTrackers() {
|
||||
return trackers;
|
||||
}
|
||||
|
||||
public int getMaxFrameCount() {
|
||||
int maxFrames = 0;
|
||||
|
||||
for(int i = 0; i < trackers.size(); i++) {
|
||||
PoseFrameTracker tracker = trackers.get(i);
|
||||
if(tracker != null && tracker.getFrameCount() > maxFrames) {
|
||||
maxFrames = tracker.getFrameCount();
|
||||
}
|
||||
}
|
||||
|
||||
return maxFrames;
|
||||
}
|
||||
|
||||
public int getFrames(int frameIndex, TrackerFrame[] buffer) {
|
||||
for(int i = 0; i < trackers.size(); i++) {
|
||||
PoseFrameTracker tracker = trackers.get(i);
|
||||
buffer[i] = tracker != null ? tracker.safeGetFrame(frameIndex) : null;
|
||||
}
|
||||
return trackers.size();
|
||||
}
|
||||
|
||||
public int getFrames(int frameIndex, List<TrackerFrame> buffer) {
|
||||
for(int i = 0; i < trackers.size(); i++) {
|
||||
PoseFrameTracker tracker = trackers.get(i);
|
||||
buffer.add(i, tracker != null ? tracker.safeGetFrame(frameIndex) : null);
|
||||
}
|
||||
return trackers.size();
|
||||
}
|
||||
|
||||
public TrackerFrame[] getFrames(int frameIndex) {
|
||||
TrackerFrame[] trackerFrames = new TrackerFrame[trackers.size()];
|
||||
getFrames(frameIndex, trackerFrames);
|
||||
return trackerFrames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<TrackerFrame[]> iterator() {
|
||||
return new PoseFrameIterator(this);
|
||||
}
|
||||
|
||||
public class PoseFrameIterator implements Iterator<TrackerFrame[]> {
|
||||
|
||||
private final PoseFrame poseFrame;
|
||||
private final TrackerFrame[] trackerFrameBuffer;
|
||||
|
||||
private int cursor = 0;
|
||||
|
||||
public PoseFrameIterator(PoseFrame poseFrame) {
|
||||
this.poseFrame = poseFrame;
|
||||
trackerFrameBuffer = new TrackerFrame[poseFrame.getTrackerCount()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if(trackers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int i = 0; i < trackers.size(); i++) {
|
||||
PoseFrameTracker tracker = trackers.get(i);
|
||||
if(tracker != null && cursor < tracker.getFrameCount()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackerFrame[] next() {
|
||||
if(!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
poseFrame.getFrames(cursor++, trackerFrameBuffer);
|
||||
|
||||
return trackerFrameBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/main/java/dev/slimevr/poserecorder/PoseFrameIO.java
Normal file
139
src/main/java/dev/slimevr/poserecorder/PoseFrameIO.java
Normal file
@@ -0,0 +1,139 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.eiren.vr.processor.TrackerBodyPosition;
|
||||
|
||||
public final class PoseFrameIO {
|
||||
|
||||
private PoseFrameIO() {
|
||||
// Do not allow instantiating
|
||||
}
|
||||
|
||||
public static boolean writeFrames(DataOutputStream outputStream, PoseFrame frames) {
|
||||
try {
|
||||
if(frames != null) {
|
||||
outputStream.writeInt(frames.getTrackerCount());
|
||||
for(PoseFrameTracker tracker : frames.getTrackers()) {
|
||||
|
||||
outputStream.writeUTF(tracker.name);
|
||||
outputStream.writeInt(tracker.getFrameCount());
|
||||
for(int i = 0; i < tracker.getFrameCount(); i++) {
|
||||
TrackerFrame trackerFrame = tracker.safeGetFrame(i);
|
||||
if(trackerFrame == null) {
|
||||
outputStream.writeInt(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
outputStream.writeInt(trackerFrame.getDataFlags());
|
||||
|
||||
if(trackerFrame.hasData(TrackerFrameData.DESIGNATION)) {
|
||||
outputStream.writeUTF(trackerFrame.designation.designation);
|
||||
}
|
||||
|
||||
if(trackerFrame.hasData(TrackerFrameData.ROTATION)) {
|
||||
outputStream.writeFloat(trackerFrame.rotation.getX());
|
||||
outputStream.writeFloat(trackerFrame.rotation.getY());
|
||||
outputStream.writeFloat(trackerFrame.rotation.getZ());
|
||||
outputStream.writeFloat(trackerFrame.rotation.getW());
|
||||
}
|
||||
|
||||
if(trackerFrame.hasData(TrackerFrameData.POSITION)) {
|
||||
outputStream.writeFloat(trackerFrame.position.getX());
|
||||
outputStream.writeFloat(trackerFrame.position.getY());
|
||||
outputStream.writeFloat(trackerFrame.position.getZ());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outputStream.writeInt(0);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
LogManager.log.severe("Error writing frame to stream", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean writeToFile(File file, PoseFrame frames) {
|
||||
try(DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
writeFrames(outputStream, frames);
|
||||
} catch(Exception e) {
|
||||
LogManager.log.severe("Error writing frames to file", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PoseFrame readFrames(DataInputStream inputStream) {
|
||||
try {
|
||||
|
||||
int trackerCount = inputStream.readInt();
|
||||
FastList<PoseFrameTracker> trackers = new FastList<PoseFrameTracker>(trackerCount);
|
||||
for(int i = 0; i < trackerCount; i++) {
|
||||
|
||||
String name = inputStream.readUTF();
|
||||
int trackerFrameCount = inputStream.readInt();
|
||||
FastList<TrackerFrame> trackerFrames = new FastList<TrackerFrame>(trackerFrameCount);
|
||||
for(int j = 0; j < trackerFrameCount; j++) {
|
||||
int dataFlags = inputStream.readInt();
|
||||
|
||||
TrackerBodyPosition designation = null;
|
||||
if(TrackerFrameData.DESIGNATION.check(dataFlags)) {
|
||||
designation = TrackerBodyPosition.getByDesignation(inputStream.readUTF());
|
||||
}
|
||||
|
||||
Quaternion rotation = null;
|
||||
if(TrackerFrameData.ROTATION.check(dataFlags)) {
|
||||
float quatX = inputStream.readFloat();
|
||||
float quatY = inputStream.readFloat();
|
||||
float quatZ = inputStream.readFloat();
|
||||
float quatW = inputStream.readFloat();
|
||||
rotation = new Quaternion(quatX, quatY, quatZ, quatW);
|
||||
}
|
||||
|
||||
Vector3f position = null;
|
||||
if(TrackerFrameData.POSITION.check(dataFlags)) {
|
||||
float posX = inputStream.readFloat();
|
||||
float posY = inputStream.readFloat();
|
||||
float posZ = inputStream.readFloat();
|
||||
position = new Vector3f(posX, posY, posZ);
|
||||
}
|
||||
|
||||
trackerFrames.add(new TrackerFrame(designation, rotation, position));
|
||||
}
|
||||
|
||||
trackers.add(new PoseFrameTracker(name, trackerFrames));
|
||||
}
|
||||
|
||||
return new PoseFrame(trackers);
|
||||
} catch(Exception e) {
|
||||
LogManager.log.severe("Error reading frame from stream", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PoseFrame readFromFile(File file) {
|
||||
try {
|
||||
return readFrames(new DataInputStream(new BufferedInputStream(new FileInputStream(file))));
|
||||
} catch(Exception e) {
|
||||
LogManager.log.severe("Error reading frame from file", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
233
src/main/java/dev/slimevr/poserecorder/PoseFrameTracker.java
Normal file
233
src/main/java/dev/slimevr/poserecorder/PoseFrameTracker.java
Normal file
@@ -0,0 +1,233 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.vr.processor.TrackerBodyPosition;
|
||||
import io.eiren.vr.trackers.Tracker;
|
||||
import io.eiren.vr.trackers.TrackerConfig;
|
||||
import io.eiren.vr.trackers.TrackerStatus;
|
||||
|
||||
public class PoseFrameTracker implements Tracker, Iterable<TrackerFrame> {
|
||||
|
||||
public final String name;
|
||||
|
||||
private final FastList<TrackerFrame> frames;
|
||||
private int frameCursor = 0;
|
||||
|
||||
public PoseFrameTracker(String name, FastList<TrackerFrame> frames) {
|
||||
if(frames == null) {
|
||||
throw new NullPointerException("frames must not be null");
|
||||
}
|
||||
|
||||
this.name = name != null ? name : "";
|
||||
this.frames = frames;
|
||||
}
|
||||
|
||||
public PoseFrameTracker(String name, int initialCapacity) {
|
||||
this(name, new FastList<TrackerFrame>(initialCapacity));
|
||||
}
|
||||
|
||||
public PoseFrameTracker(String name) {
|
||||
this(name, 5);
|
||||
}
|
||||
|
||||
private int limitCursor() {
|
||||
if(frameCursor < 0 || frames.isEmpty()) {
|
||||
frameCursor = 0;
|
||||
} else if(frameCursor >= frames.size()) {
|
||||
frameCursor = frames.size() - 1;
|
||||
}
|
||||
|
||||
return frameCursor;
|
||||
}
|
||||
|
||||
public int setCursor(int index) {
|
||||
frameCursor = index;
|
||||
return limitCursor();
|
||||
}
|
||||
|
||||
public int incrementCursor(int increment) {
|
||||
frameCursor += increment;
|
||||
return limitCursor();
|
||||
}
|
||||
|
||||
public int incrementCursor() {
|
||||
return incrementCursor(1);
|
||||
}
|
||||
|
||||
public int getCursor() {
|
||||
return frameCursor;
|
||||
}
|
||||
|
||||
public int getFrameCount() {
|
||||
return frames.size();
|
||||
}
|
||||
|
||||
public TrackerFrame addFrame(int index, TrackerFrame trackerFrame) {
|
||||
frames.add(index, trackerFrame);
|
||||
return trackerFrame;
|
||||
}
|
||||
|
||||
public TrackerFrame addFrame(int index, Tracker tracker) {
|
||||
return addFrame(index, TrackerFrame.fromTracker(tracker));
|
||||
}
|
||||
|
||||
public TrackerFrame addFrame(TrackerFrame trackerFrame) {
|
||||
frames.add(trackerFrame);
|
||||
return trackerFrame;
|
||||
}
|
||||
|
||||
public TrackerFrame addFrame(Tracker tracker) {
|
||||
return addFrame(TrackerFrame.fromTracker(tracker));
|
||||
}
|
||||
|
||||
public TrackerFrame removeFrame(int index) {
|
||||
TrackerFrame trackerFrame = frames.remove(index);
|
||||
limitCursor();
|
||||
return trackerFrame;
|
||||
}
|
||||
|
||||
public TrackerFrame removeFrame(TrackerFrame trackerFrame) {
|
||||
frames.remove(trackerFrame);
|
||||
limitCursor();
|
||||
return trackerFrame;
|
||||
}
|
||||
|
||||
public void clearFrames() {
|
||||
frames.clear();
|
||||
limitCursor();
|
||||
}
|
||||
|
||||
public void fakeClearFrames() {
|
||||
frames.fakeClear();
|
||||
limitCursor();
|
||||
}
|
||||
|
||||
public TrackerFrame getFrame(int index) {
|
||||
return frames.get(index);
|
||||
}
|
||||
|
||||
public TrackerFrame getFrame() {
|
||||
return getFrame(frameCursor);
|
||||
}
|
||||
|
||||
public TrackerFrame safeGetFrame(int index) {
|
||||
try {
|
||||
return getFrame(index);
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public TrackerFrame safeGetFrame() {
|
||||
return safeGetFrame(frameCursor);
|
||||
}
|
||||
|
||||
//#region Tracker Interface Implementation
|
||||
@Override
|
||||
public boolean getRotation(Quaternion store) {
|
||||
TrackerFrame frame = safeGetFrame();
|
||||
if(frame != null && frame.hasData(TrackerFrameData.ROTATION)) {
|
||||
store.set(frame.rotation);
|
||||
return true;
|
||||
}
|
||||
|
||||
store.set(0, 0, 0, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPosition(Vector3f store) {
|
||||
TrackerFrame frame = safeGetFrame();
|
||||
if(frame != null && frame.hasData(TrackerFrameData.POSITION)) {
|
||||
store.set(frame.position);
|
||||
return true;
|
||||
}
|
||||
|
||||
store.set(0, 0, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackerStatus getStatus() {
|
||||
return TrackerStatus.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadConfig(TrackerConfig config) {
|
||||
throw new UnsupportedOperationException("PoseFrameTracker does not implement configuration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveConfig(TrackerConfig config) {
|
||||
throw new UnsupportedOperationException("PoseFrameTracker does not implement configuration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getConfidenceLevel() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetFull(Quaternion reference) {
|
||||
throw new UnsupportedOperationException("PoseFrameTracker does not implement calibration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetYaw(Quaternion reference) {
|
||||
throw new UnsupportedOperationException("PoseFrameTracker does not implement calibration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
throw new UnsupportedOperationException("PoseFrameTracker does not implement this method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackerBodyPosition getBodyPosition() {
|
||||
TrackerFrame frame = safeGetFrame();
|
||||
return frame == null ? null : frame.designation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBodyPosition(TrackerBodyPosition position) {
|
||||
throw new UnsupportedOperationException("PoseFrameTracker does not allow setting the body position");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRotation() {
|
||||
TrackerFrame frame = safeGetFrame();
|
||||
return frame != null && frame.hasData(TrackerFrameData.ROTATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPosition() {
|
||||
TrackerFrame frame = safeGetFrame();
|
||||
return frame != null && frame.hasData(TrackerFrameData.POSITION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return true;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@Override
|
||||
public Iterator<TrackerFrame> iterator() {
|
||||
return frames.iterator();
|
||||
}
|
||||
}
|
||||
163
src/main/java/dev/slimevr/poserecorder/PoseRecorder.java
Normal file
163
src/main/java/dev/slimevr/poserecorder/PoseRecorder.java
Normal file
@@ -0,0 +1,163 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import io.eiren.util.ann.VRServerThread;
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.eiren.vr.VRServer;
|
||||
import io.eiren.vr.trackers.Tracker;
|
||||
|
||||
public class PoseRecorder {
|
||||
|
||||
protected PoseFrame poseFrame = null;
|
||||
|
||||
protected int numFrames = -1;
|
||||
protected int frameCursor = 0;
|
||||
protected long frameRecordingInterval = 60L;
|
||||
protected long nextFrameTimeMs = -1L;
|
||||
|
||||
protected CompletableFuture<PoseFrame> currentRecording;
|
||||
|
||||
protected final VRServer server;
|
||||
FastList<Pair<Tracker, PoseFrameTracker>> trackers = new FastList<Pair<Tracker, PoseFrameTracker>>();
|
||||
|
||||
public PoseRecorder(VRServer server) {
|
||||
this.server = server;
|
||||
server.addOnTick(this::onTick);
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
public void onTick() {
|
||||
if(numFrames > 0) {
|
||||
PoseFrame poseFrame = this.poseFrame;
|
||||
List<Pair<Tracker, PoseFrameTracker>> trackers = this.trackers;
|
||||
if(poseFrame != null && trackers != null) {
|
||||
if(frameCursor < numFrames) {
|
||||
if(System.currentTimeMillis() >= nextFrameTimeMs) {
|
||||
nextFrameTimeMs = System.currentTimeMillis() + frameRecordingInterval;
|
||||
|
||||
int cursor = frameCursor++;
|
||||
for(Pair<Tracker, PoseFrameTracker> tracker : trackers) {
|
||||
// Add a frame for each tracker
|
||||
tracker.getRight().addFrame(cursor, tracker.getLeft());
|
||||
}
|
||||
|
||||
// If done, send finished recording
|
||||
if(frameCursor >= numFrames) {
|
||||
internalStopRecording();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If done and hasn't yet, send finished recording
|
||||
internalStopRecording();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Future<PoseFrame> startFrameRecording(int numFrames, long interval) {
|
||||
return startFrameRecording(numFrames, interval, server.getAllTrackers());
|
||||
}
|
||||
|
||||
public synchronized Future<PoseFrame> startFrameRecording(int numFrames, long interval, List<Tracker> trackers) {
|
||||
if(numFrames < 1) {
|
||||
throw new IllegalArgumentException("numFrames must at least have a value of 1");
|
||||
}
|
||||
if(interval < 1) {
|
||||
throw new IllegalArgumentException("interval must at least have a value of 1");
|
||||
}
|
||||
if(trackers == null) {
|
||||
throw new IllegalArgumentException("trackers must not be null");
|
||||
}
|
||||
if(trackers.isEmpty()) {
|
||||
throw new IllegalArgumentException("trackers must have at least one entry");
|
||||
}
|
||||
if(!isReadyToRecord()) {
|
||||
throw new IllegalStateException("PoseRecorder isn't ready to record!");
|
||||
}
|
||||
|
||||
cancelFrameRecording();
|
||||
|
||||
poseFrame = new PoseFrame(trackers.size());
|
||||
|
||||
// Update tracker list
|
||||
this.trackers.ensureCapacity(trackers.size());
|
||||
for(Tracker tracker : trackers) {
|
||||
// Ignore null and computed trackers
|
||||
if(tracker == null || tracker.isComputed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pair tracker with recording
|
||||
this.trackers.add(Pair.of(tracker, poseFrame.addTracker(tracker, numFrames)));
|
||||
}
|
||||
|
||||
this.frameCursor = 0;
|
||||
this.numFrames = numFrames;
|
||||
|
||||
frameRecordingInterval = interval;
|
||||
nextFrameTimeMs = -1L;
|
||||
|
||||
LogManager.log.info("[PoseRecorder] Recording " + numFrames + " samples at a " + interval + " ms frame interval");
|
||||
|
||||
currentRecording = new CompletableFuture<PoseFrame>();
|
||||
return currentRecording;
|
||||
}
|
||||
|
||||
private void internalStopRecording() {
|
||||
CompletableFuture<PoseFrame> currentRecording = this.currentRecording;
|
||||
if(currentRecording != null && !currentRecording.isDone()) {
|
||||
// Stop the recording, returning the frames recorded
|
||||
currentRecording.complete(poseFrame);
|
||||
}
|
||||
|
||||
numFrames = -1;
|
||||
frameCursor = 0;
|
||||
trackers.clear();
|
||||
poseFrame = null;
|
||||
}
|
||||
|
||||
public synchronized void stopFrameRecording() {
|
||||
internalStopRecording();
|
||||
}
|
||||
|
||||
public synchronized void cancelFrameRecording() {
|
||||
CompletableFuture<PoseFrame> currentRecording = this.currentRecording;
|
||||
if(currentRecording != null && !currentRecording.isDone()) {
|
||||
// Cancel the current recording and return nothing
|
||||
currentRecording.cancel(true);
|
||||
}
|
||||
|
||||
numFrames = -1;
|
||||
frameCursor = 0;
|
||||
trackers.clear();
|
||||
poseFrame = null;
|
||||
}
|
||||
|
||||
public boolean isReadyToRecord() {
|
||||
return server.getTrackersCount() > 0;
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return numFrames > frameCursor;
|
||||
}
|
||||
|
||||
public boolean hasRecording() {
|
||||
return currentRecording != null;
|
||||
}
|
||||
|
||||
public Future<PoseFrame> getFramesAsync() {
|
||||
return currentRecording;
|
||||
}
|
||||
|
||||
public PoseFrame getFrames() throws ExecutionException, InterruptedException {
|
||||
CompletableFuture<PoseFrame> currentRecording = this.currentRecording;
|
||||
return currentRecording != null ? currentRecording.get() : null;
|
||||
}
|
||||
}
|
||||
173
src/main/java/dev/slimevr/poserecorder/TrackerFrame.java
Normal file
173
src/main/java/dev/slimevr/poserecorder/TrackerFrame.java
Normal file
@@ -0,0 +1,173 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import io.eiren.vr.processor.TrackerBodyPosition;
|
||||
import io.eiren.vr.trackers.Tracker;
|
||||
import io.eiren.vr.trackers.TrackerConfig;
|
||||
import io.eiren.vr.trackers.TrackerStatus;
|
||||
|
||||
public final class TrackerFrame implements Tracker {
|
||||
|
||||
private int dataFlags = 0;
|
||||
|
||||
public final TrackerBodyPosition designation;
|
||||
public final Quaternion rotation;
|
||||
public final Vector3f position;
|
||||
|
||||
public TrackerFrame(TrackerBodyPosition designation, Quaternion rotation, Vector3f position) {
|
||||
this.designation = designation;
|
||||
if(designation != null) {
|
||||
dataFlags |= TrackerFrameData.DESIGNATION.flag;
|
||||
}
|
||||
|
||||
this.rotation = rotation;
|
||||
if(rotation != null) {
|
||||
dataFlags |= TrackerFrameData.ROTATION.flag;
|
||||
}
|
||||
|
||||
this.position = position;
|
||||
if(position != null) {
|
||||
dataFlags |= TrackerFrameData.POSITION.flag;
|
||||
}
|
||||
}
|
||||
|
||||
public static TrackerFrame fromTracker(Tracker tracker) {
|
||||
if(tracker == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the tracker is not ready
|
||||
if(tracker.getStatus() != TrackerStatus.OK && tracker.getStatus() != TrackerStatus.BUSY && tracker.getStatus() != TrackerStatus.OCCLUDED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If tracker has no data
|
||||
if(tracker.getBodyPosition() == null && !tracker.hasRotation() && !tracker.hasPosition()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Quaternion rotation = null;
|
||||
if(tracker.hasRotation()) {
|
||||
rotation = new Quaternion();
|
||||
if(!tracker.getRotation(rotation)) {
|
||||
// If getting the rotation failed, set it back to null
|
||||
rotation = null;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3f position = null;
|
||||
if(tracker.hasPosition()) {
|
||||
position = new Vector3f();
|
||||
if(!tracker.getPosition(position)) {
|
||||
// If getting the position failed, set it back to null
|
||||
position = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new TrackerFrame(tracker.getBodyPosition(), rotation, position);
|
||||
}
|
||||
|
||||
public int getDataFlags() {
|
||||
return dataFlags;
|
||||
}
|
||||
|
||||
public boolean hasData(TrackerFrameData flag) {
|
||||
return flag.check(dataFlags);
|
||||
}
|
||||
|
||||
//#region Tracker Interface Implementation
|
||||
@Override
|
||||
public boolean getRotation(Quaternion store) {
|
||||
if(hasData(TrackerFrameData.ROTATION)) {
|
||||
store.set(rotation);
|
||||
return true;
|
||||
}
|
||||
|
||||
store.set(0, 0, 0, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPosition(Vector3f store) {
|
||||
if(hasData(TrackerFrameData.POSITION)) {
|
||||
store.set(position);
|
||||
return true;
|
||||
}
|
||||
|
||||
store.set(0, 0, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "TrackerFrame:/" + (designation != null ? designation.designation : "null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackerStatus getStatus() {
|
||||
return TrackerStatus.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadConfig(TrackerConfig config) {
|
||||
throw new UnsupportedOperationException("TrackerFrame does not implement configuration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveConfig(TrackerConfig config) {
|
||||
throw new UnsupportedOperationException("TrackerFrame does not implement configuration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getConfidenceLevel() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetFull(Quaternion reference) {
|
||||
throw new UnsupportedOperationException("TrackerFrame does not implement calibration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetYaw(Quaternion reference) {
|
||||
throw new UnsupportedOperationException("TrackerFrame does not implement calibration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
throw new UnsupportedOperationException("TrackerFrame does not implement this method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackerBodyPosition getBodyPosition() {
|
||||
return designation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBodyPosition(TrackerBodyPosition position) {
|
||||
throw new UnsupportedOperationException("TrackerFrame does not allow setting the body position");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRotation() {
|
||||
return hasData(TrackerFrameData.ROTATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPosition() {
|
||||
return hasData(TrackerFrameData.POSITION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return true;
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
19
src/main/java/dev/slimevr/poserecorder/TrackerFrameData.java
Normal file
19
src/main/java/dev/slimevr/poserecorder/TrackerFrameData.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
public enum TrackerFrameData {
|
||||
|
||||
DESIGNATION(0),
|
||||
ROTATION(1),
|
||||
POSITION(2),
|
||||
;
|
||||
|
||||
public final int flag;
|
||||
|
||||
TrackerFrameData(int id) {
|
||||
this.flag = 1 << id;
|
||||
}
|
||||
|
||||
public boolean check(int dataFlags) {
|
||||
return (dataFlags & this.flag) != 0;
|
||||
}
|
||||
}
|
||||
24
src/main/java/io/eiren/gui/AbstractComponentListener.java
Normal file
24
src/main/java/io/eiren/gui/AbstractComponentListener.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package io.eiren.gui;
|
||||
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.ComponentListener;
|
||||
|
||||
public abstract class AbstractComponentListener implements ComponentListener {
|
||||
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentMoved(ComponentEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentHidden(ComponentEvent e) {
|
||||
}
|
||||
|
||||
}
|
||||
35
src/main/java/io/eiren/gui/AbstractWindowListener.java
Normal file
35
src/main/java/io/eiren/gui/AbstractWindowListener.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package io.eiren.gui;
|
||||
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowListener;
|
||||
|
||||
public abstract class AbstractWindowListener implements WindowListener {
|
||||
|
||||
@Override
|
||||
public void windowOpened(WindowEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowIconified(WindowEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowDeiconified(WindowEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowDeactivated(WindowEvent e) {
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,17 @@
|
||||
package io.eiren.gui;
|
||||
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.event.MouseInputAdapter;
|
||||
|
||||
import io.eiren.gui.autobone.AutoBoneWindow;
|
||||
import dev.slimevr.gui.AutoBoneWindow;
|
||||
import io.eiren.util.StringUtils;
|
||||
import io.eiren.util.ann.ThreadSafe;
|
||||
import io.eiren.vr.VRServer;
|
||||
import io.eiren.vr.processor.HumanSkeletonWithLegs;
|
||||
import io.eiren.vr.processor.HumanSkeleton;
|
||||
|
||||
public class SkeletonConfig extends EJBag {
|
||||
@@ -43,6 +39,7 @@ public class SkeletonConfig extends EJBag {
|
||||
|
||||
int row = 0;
|
||||
|
||||
/**
|
||||
add(new JCheckBox("Extended pelvis model") {{
|
||||
addItemListener(new ItemListener() {
|
||||
@Override
|
||||
@@ -66,7 +63,7 @@ public class SkeletonConfig extends EJBag {
|
||||
}
|
||||
}}, s(c(0, row, 1), 3, 1));
|
||||
row++;
|
||||
|
||||
//*/
|
||||
/*
|
||||
add(new JCheckBox("Extended knee model") {{
|
||||
addItemListener(new ItemListener() {
|
||||
|
||||
@@ -206,13 +206,17 @@ public class TrackersList extends EJBox {
|
||||
}
|
||||
row++;
|
||||
}
|
||||
add(new JLabel("Rotation"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
add(new JLabel("Position"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
if(t.hasRotation())
|
||||
add(new JLabel("Rotation"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
if(t.hasPosition())
|
||||
add(new JLabel("Position"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
add(new JLabel("Ping"), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
add(new JLabel("TPS"), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
row++;
|
||||
add(rotation = new JLabel("0 0 0"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
add(position = new JLabel("0 0 0"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
if(t.hasRotation())
|
||||
add(rotation = new JLabel("0 0 0"), c(0, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
if(t.hasPosition())
|
||||
add(position = new JLabel("0 0 0"), c(1, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
add(ping = new JLabel(""), c(2, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
if(realTracker instanceof TrackerWithTPS) {
|
||||
add(tps = new JLabel("0"), c(3, row, 0, GridBagConstraints.FIRST_LINE_START));
|
||||
@@ -257,7 +261,7 @@ public class TrackersList extends EJBox {
|
||||
@SuppressWarnings("unchecked")
|
||||
@AWTThread
|
||||
public void update() {
|
||||
if(position == null)
|
||||
if(position == null && rotation == null)
|
||||
return;
|
||||
Tracker realTracker = t;
|
||||
if(t instanceof ReferenceAdjustedTracker)
|
||||
@@ -266,12 +270,14 @@ public class TrackersList extends EJBox {
|
||||
t.getPosition(v);
|
||||
q.toAngles(angles);
|
||||
|
||||
position.setText(StringUtils.prettyNumber(v.x, 1)
|
||||
+ " " + StringUtils.prettyNumber(v.y, 1)
|
||||
+ " " + StringUtils.prettyNumber(v.z, 1));
|
||||
rotation.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
|
||||
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
|
||||
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
|
||||
if(position != null)
|
||||
position.setText(StringUtils.prettyNumber(v.x, 1)
|
||||
+ " " + StringUtils.prettyNumber(v.y, 1)
|
||||
+ " " + StringUtils.prettyNumber(v.z, 1));
|
||||
if(rotation != null)
|
||||
rotation.setText(StringUtils.prettyNumber(angles[0] * FastMath.RAD_TO_DEG, 0)
|
||||
+ " " + StringUtils.prettyNumber(angles[1] * FastMath.RAD_TO_DEG, 0)
|
||||
+ " " + StringUtils.prettyNumber(angles[2] * FastMath.RAD_TO_DEG, 0));
|
||||
status.setText(t.getStatus().toString().toLowerCase());
|
||||
|
||||
if(realTracker instanceof TrackerWithTPS) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package io.eiren.gui;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.MouseInputAdapter;
|
||||
|
||||
import io.eiren.util.MacOSX;
|
||||
import io.eiren.util.OperatingSystem;
|
||||
import io.eiren.util.StringUtils;
|
||||
import io.eiren.util.ann.AWTThread;
|
||||
import io.eiren.vr.Main;
|
||||
@@ -13,20 +16,28 @@ import java.awt.Component;
|
||||
import java.awt.Container;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.GraphicsConfiguration;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static javax.swing.BoxLayout.PAGE_AXIS;
|
||||
import static javax.swing.BoxLayout.LINE_AXIS;
|
||||
|
||||
public class VRServerGUI extends JFrame {
|
||||
|
||||
public static final String TITLE = "SlimeVR Server (" + Main.VERSION + ")";
|
||||
|
||||
public final VRServer server;
|
||||
private final TrackersList trackersList;
|
||||
private final SkeletonList skeletonList;
|
||||
private JButton resetButton;
|
||||
private JScrollPane scroll;
|
||||
private EJBox pane;
|
||||
|
||||
private float zoom = 1.5f;
|
||||
@@ -34,13 +45,29 @@ public class VRServerGUI extends JFrame {
|
||||
|
||||
@AWTThread
|
||||
public VRServerGUI(VRServer server) {
|
||||
super("SlimeVR Server (" + Main.VERSION + ")");
|
||||
super(TITLE);
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//increaseFontSize();
|
||||
if(OperatingSystem.getCurrentPlatform() == OperatingSystem.OSX)
|
||||
MacOSX.setTitle(TITLE);
|
||||
try {
|
||||
List<BufferedImage> images = new ArrayList<BufferedImage>(6);
|
||||
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon16.png")));
|
||||
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon32.png")));
|
||||
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon48.png")));
|
||||
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon64.png")));
|
||||
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon128.png")));
|
||||
images.add(ImageIO.read(VRServerGUI.class.getResource("/icon256.png")));
|
||||
setIconImages(images);
|
||||
if(OperatingSystem.getCurrentPlatform() == OperatingSystem.OSX) {
|
||||
MacOSX.setIcons(images);
|
||||
}
|
||||
} catch(IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
|
||||
this.server = server;
|
||||
|
||||
@@ -55,13 +82,38 @@ public class VRServerGUI extends JFrame {
|
||||
this.trackersList = new TrackersList(server, this);
|
||||
this.skeletonList = new SkeletonList(server, this);
|
||||
|
||||
add(scroll = new JScrollPane(pane = new EJBox(PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
|
||||
add(new JScrollPane(pane = new EJBox(PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
|
||||
GraphicsConfiguration gc = getGraphicsConfiguration();
|
||||
Rectangle screenBounds = gc.getBounds();
|
||||
setMinimumSize(new Dimension(100, 100));
|
||||
setSize(Math.min(server.config.getInt("window.width", 800), screenBounds.width), Math.min(server.config.getInt("window.height", 800), screenBounds.height));
|
||||
setLocation(server.config.getInt("window.posx", screenBounds.x + (screenBounds.width - getSize().width) / 2), screenBounds.y + server.config.getInt("window.posy", (screenBounds.height - getSize().height) / 2));
|
||||
|
||||
setMinimumSize(new Dimension(1280, 1080));
|
||||
// Resize and close listeners to save position and size betwen launcher starts
|
||||
addComponentListener(new AbstractComponentListener() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
saveFrameInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void componentMoved(ComponentEvent e) {
|
||||
saveFrameInfo();
|
||||
}
|
||||
});
|
||||
|
||||
build();
|
||||
}
|
||||
|
||||
protected void saveFrameInfo() {
|
||||
Rectangle b = getBounds();
|
||||
server.config.setProperty("window.width", b.width);
|
||||
server.config.setProperty("window.height", b.height);
|
||||
server.config.setProperty("window.posx", b.x);
|
||||
server.config.setProperty("window.posy", b.y);
|
||||
server.saveConfig();
|
||||
}
|
||||
|
||||
public float getZoom() {
|
||||
return this.zoom;
|
||||
}
|
||||
@@ -196,7 +248,6 @@ public class VRServerGUI extends JFrame {
|
||||
}});
|
||||
|
||||
refresh();
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
server.addOnTick(trackersList::updateTrackers);
|
||||
server.addOnTick(skeletonList::updateBones);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class WiFiWindow extends JFrame {
|
||||
|
||||
SerialPort[] ports = SerialPort.getCommPorts();
|
||||
for(SerialPort port : ports) {
|
||||
if(port.getDescriptivePortName().toLowerCase().contains("ch340") || port.getDescriptivePortName().toLowerCase().contains("cp21")) {
|
||||
if(port.getDescriptivePortName().toLowerCase().contains("ch340") || port.getDescriptivePortName().toLowerCase().contains("cp21") || port.getDescriptivePortName().toLowerCase().contains("ch910")) {
|
||||
trackerPort = port;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
package io.eiren.gui.autobone;
|
||||
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import io.eiren.gui.EJBox;
|
||||
import io.eiren.gui.SkeletonConfig;
|
||||
import io.eiren.util.StringUtils;
|
||||
import io.eiren.util.ann.AWTThread;
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.eiren.vr.VRServer;
|
||||
import javax.swing.event.MouseInputAdapter;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
public class AutoBoneWindow extends JFrame {
|
||||
|
||||
private static File saveDir = new File("Recordings");
|
||||
private static File loadDir = new File("LoadRecordings");
|
||||
|
||||
private EJBox pane;
|
||||
|
||||
private final VRServer server;
|
||||
private final SkeletonConfig skeletonConfig;
|
||||
private final PoseRecorder poseRecorder;
|
||||
private final AutoBone autoBone;
|
||||
|
||||
private Thread recordingThread = null;
|
||||
private Thread saveRecordingThread = null;
|
||||
private Thread autoBoneThread = null;
|
||||
|
||||
private JButton saveRecordingButton;
|
||||
private JButton adjustButton;
|
||||
private JButton applyButton;
|
||||
|
||||
private JLabel processLabel;
|
||||
private JLabel lengthsLabel;
|
||||
|
||||
public AutoBoneWindow(VRServer server, SkeletonConfig skeletonConfig) {
|
||||
super("Skeleton Auto-Configuration");
|
||||
|
||||
this.server = server;
|
||||
this.skeletonConfig = skeletonConfig;
|
||||
this.poseRecorder = new PoseRecorder(server);
|
||||
this.autoBone = new AutoBone(server);
|
||||
|
||||
getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.PAGE_AXIS));
|
||||
add(new JScrollPane(pane = new EJBox(BoxLayout.PAGE_AXIS), ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
|
||||
|
||||
build();
|
||||
}
|
||||
|
||||
private String getLengthsString() {
|
||||
boolean first = true;
|
||||
StringBuilder configInfo = new StringBuilder("");
|
||||
for (Entry<String, Float> entry : autoBone.configs.entrySet()) {
|
||||
if (!first) {
|
||||
configInfo.append(", ");
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
configInfo.append(entry.getKey() + ": " + StringUtils.prettyNumber(entry.getValue() * 100f, 2));
|
||||
}
|
||||
|
||||
return configInfo.toString();
|
||||
}
|
||||
|
||||
private void saveRecording(PoseFrame[] frames) {
|
||||
if (saveDir.isDirectory() || saveDir.mkdirs()) {
|
||||
File saveRecording;
|
||||
int recordingIndex = 1;
|
||||
do {
|
||||
saveRecording = new File(saveDir, "ABRecording" + recordingIndex++ + ".abf");
|
||||
} while (saveRecording.exists());
|
||||
|
||||
LogManager.log.info("[AutoBone] Exporting frames to \"" + saveRecording.getPath() + "\"...");
|
||||
if (PoseFrameIO.writeToFile(saveRecording, frames)) {
|
||||
LogManager.log.info("[AutoBone] Done exporting! Recording can be found at \"" + saveRecording.getPath() + "\".");
|
||||
} else {
|
||||
LogManager.log.severe("[AutoBone] Failed to export the recording to \"" + saveRecording.getPath() + "\".");
|
||||
}
|
||||
} else {
|
||||
LogManager.log.severe("[AutoBone] Failed to create the recording directory \"" + saveDir.getPath() + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Pair<String, PoseFrame[]>> loadRecordings() {
|
||||
List<Pair<String, PoseFrame[]>> recordings = new FastList<Pair<String, PoseFrame[]>>();
|
||||
if (loadDir.isDirectory()) {
|
||||
File[] files = loadDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isFile() && org.apache.commons.lang3.StringUtils.endsWithIgnoreCase(file.getName(), ".abf")) {
|
||||
LogManager.log.info("[AutoBone] Detected recording at \"" + file.getPath() + "\", loading frames...");
|
||||
PoseFrame[] frames = PoseFrameIO.readFromFile(file);
|
||||
|
||||
if (frames == null) {
|
||||
LogManager.log.severe("Reading frames from \"" + file.getPath() + "\" failed...");
|
||||
} else {
|
||||
recordings.add(Pair.of(file.getName(), frames));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recordings;
|
||||
}
|
||||
|
||||
private float processFrames(PoseFrame[] frames) {
|
||||
autoBone.reloadConfigValues();
|
||||
|
||||
autoBone.minDataDistance = server.config.getInt("autobone.minimumDataDistance", autoBone.minDataDistance);
|
||||
autoBone.maxDataDistance = server.config.getInt("autobone.maximumDataDistance", autoBone.maxDataDistance);
|
||||
|
||||
autoBone.numEpochs = server.config.getInt("autobone.epochCount", autoBone.numEpochs);
|
||||
|
||||
autoBone.initialAdjustRate = server.config.getFloat("autobone.adjustRate", autoBone.initialAdjustRate);
|
||||
autoBone.adjustRateDecay = server.config.getFloat("autobone.adjustRateDecay", autoBone.adjustRateDecay);
|
||||
|
||||
autoBone.slideErrorFactor = server.config.getFloat("autobone.slideErrorFactor", autoBone.slideErrorFactor);
|
||||
autoBone.offsetErrorFactor = server.config.getFloat("autobone.offsetErrorFactor", autoBone.offsetErrorFactor);
|
||||
autoBone.proportionErrorFactor = server.config.getFloat("autobone.proportionErrorFactor", autoBone.proportionErrorFactor);
|
||||
autoBone.heightErrorFactor = server.config.getFloat("autobone.heightErrorFactor", autoBone.heightErrorFactor);
|
||||
autoBone.positionErrorFactor = server.config.getFloat("autobone.positionErrorFactor", autoBone.positionErrorFactor);
|
||||
autoBone.positionOffsetErrorFactor = server.config.getFloat("autobone.positionOffsetErrorFactor", autoBone.positionOffsetErrorFactor);
|
||||
|
||||
boolean calcInitError = server.config.getBoolean("autobone.calculateInitialError", true);
|
||||
float targetHeight = server.config.getFloat("autobone.manualTargetHeight", -1f);
|
||||
return autoBone.processFrames(frames, calcInitError, targetHeight, (epoch) -> {
|
||||
processLabel.setText(epoch.toString());
|
||||
lengthsLabel.setText(getLengthsString());
|
||||
});
|
||||
}
|
||||
|
||||
@AWTThread
|
||||
private void build() {
|
||||
pane.add(new EJBox(BoxLayout.LINE_AXIS) {{
|
||||
setBorder(new EmptyBorder(i(5)));
|
||||
add(new JButton("Start Recording") {{
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
// Prevent running multiple times
|
||||
if (!isEnabled() || recordingThread != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (poseRecorder.isReadyToRecord()) {
|
||||
setText("Recording...");
|
||||
// 1000 samples at 20 ms per sample is 20 seconds
|
||||
int sampleCount = server.config.getInt("autobone.sampleCount", 1000);
|
||||
long sampleRate = server.config.getLong("autobone.sampleRateMs", 20L);
|
||||
Future<PoseFrame[]> framesFuture = poseRecorder.startFrameRecording(sampleCount, sampleRate);
|
||||
PoseFrame[] frames = framesFuture.get();
|
||||
LogManager.log.info("[AutoBone] Done recording!");
|
||||
|
||||
saveRecordingButton.setEnabled(true);
|
||||
adjustButton.setEnabled(true);
|
||||
|
||||
if (server.config.getBoolean("autobone.saveRecordings", false)) {
|
||||
setText("Saving...");
|
||||
saveRecording(frames);
|
||||
}
|
||||
} else {
|
||||
setText("Not Ready...");
|
||||
LogManager.log.severe("[AutoBone] Unable to record...");
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
setText("Recording Failed...");
|
||||
LogManager.log.severe("[AutoBone] Failed recording!", e);
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch (Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
setText("Start Recording");
|
||||
recordingThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
recordingThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
}});
|
||||
|
||||
add(saveRecordingButton = new JButton("Save Recording") {{
|
||||
setEnabled(poseRecorder.hasRecording());
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
// Prevent running multiple times
|
||||
if (!isEnabled() || saveRecordingThread != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Future<PoseFrame[]> framesFuture = poseRecorder.getFramesAsync();
|
||||
if (framesFuture != null) {
|
||||
setText("Waiting for Recording...");
|
||||
PoseFrame[] frames = framesFuture.get();
|
||||
|
||||
if (frames.length <= 0) {
|
||||
throw new IllegalStateException("Recording has no frames");
|
||||
}
|
||||
|
||||
setText("Saving...");
|
||||
saveRecording(frames);
|
||||
} else {
|
||||
setText("No Recording...");
|
||||
LogManager.log.severe("[AutoBone] Unable to save, no recording was done...");
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch (Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
setText("Saving Failed...");
|
||||
LogManager.log.severe("[AutoBone] Failed to save recording!", e);
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch (Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
setText("Save Recording");
|
||||
saveRecordingThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
saveRecordingThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
}});
|
||||
|
||||
add(adjustButton = new JButton("Auto-Adjust") {{
|
||||
// If there are files to load, enable the button
|
||||
setEnabled(poseRecorder.hasRecording() || (loadDir.isDirectory() && loadDir.list().length > 0));
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
// Prevent running multiple times
|
||||
if (!isEnabled() || autoBoneThread != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
setText("Load...");
|
||||
List<Pair<String, PoseFrame[]>> frameRecordings = loadRecordings();
|
||||
|
||||
if (frameRecordings.size() > 0) {
|
||||
LogManager.log.info("[AutoBone] Done loading frames!");
|
||||
} else {
|
||||
Future<PoseFrame[]> framesFuture = poseRecorder.getFramesAsync();
|
||||
if (framesFuture != null) {
|
||||
setText("Waiting for Recording...");
|
||||
PoseFrame[] frames = framesFuture.get();
|
||||
|
||||
if (frames.length <= 0) {
|
||||
throw new IllegalStateException("Recording has no frames");
|
||||
}
|
||||
|
||||
frameRecordings.add(Pair.of("<Recording>", frames));
|
||||
} else {
|
||||
setText("No Recordings...");
|
||||
LogManager.log.severe("[AutoBone] No recordings found in \"" + loadDir.getPath() + "\" and no recording was done...");
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch (Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setText("Processing...");
|
||||
LogManager.log.info("[AutoBone] Processing frames...");
|
||||
FastList<Float> heightPercentError = new FastList<Float>(frameRecordings.size());
|
||||
for (Pair<String, PoseFrame[]> recording : frameRecordings) {
|
||||
LogManager.log.info("[AutoBone] Processing frames from \"" + recording.getKey() + "\"...");
|
||||
|
||||
heightPercentError.add(processFrames(recording.getValue()));
|
||||
LogManager.log.info("[AutoBone] Done processing!");
|
||||
applyButton.setEnabled(true);
|
||||
|
||||
//#region Stats/Values
|
||||
Float neckLength = autoBone.getConfig("Neck");
|
||||
Float chestLength = autoBone.getConfig("Chest");
|
||||
Float waistLength = autoBone.getConfig("Waist");
|
||||
Float hipWidth = autoBone.getConfig("Hips width");
|
||||
Float legsLength = autoBone.getConfig("Legs length");
|
||||
Float kneeHeight = autoBone.getConfig("Knee height");
|
||||
|
||||
float neckWaist = neckLength != null && waistLength != null ? neckLength / waistLength : 0f;
|
||||
float chestWaist = chestLength != null && waistLength != null ? chestLength / waistLength : 0f;
|
||||
float hipWaist = hipWidth != null && waistLength != null ? hipWidth / waistLength : 0f;
|
||||
float legWaist = legsLength != null && waistLength != null ? legsLength / waistLength : 0f;
|
||||
float legBody = legsLength != null && waistLength != null && neckLength != null ? legsLength / (waistLength + neckLength) : 0f;
|
||||
float kneeLeg = kneeHeight != null && legsLength != null ? kneeHeight / legsLength : 0f;
|
||||
|
||||
LogManager.log.info("[AutoBone] Ratios: [{Neck-Waist: " + StringUtils.prettyNumber(neckWaist) +
|
||||
"}, {Chest-Waist: " + StringUtils.prettyNumber(chestWaist) +
|
||||
"}, {Hip-Waist: " + StringUtils.prettyNumber(hipWaist) +
|
||||
"}, {Leg-Waist: " + StringUtils.prettyNumber(legWaist) +
|
||||
"}, {Leg-Body: " + StringUtils.prettyNumber(legBody) +
|
||||
"}, {Knee-Leg: " + StringUtils.prettyNumber(kneeLeg) + "}]");
|
||||
|
||||
String lengthsString = getLengthsString();
|
||||
LogManager.log.info("[AutoBone] Length values: " + lengthsString);
|
||||
lengthsLabel.setText(lengthsString);
|
||||
}
|
||||
|
||||
if (heightPercentError.size() > 0) {
|
||||
float mean = 0f;
|
||||
for (float val : heightPercentError) {
|
||||
mean += val;
|
||||
}
|
||||
mean /= heightPercentError.size();
|
||||
|
||||
float std = 0f;
|
||||
for (float val : heightPercentError) {
|
||||
float stdVal = val - mean;
|
||||
std += stdVal * stdVal;
|
||||
}
|
||||
std = (float)Math.sqrt(std / heightPercentError.size());
|
||||
|
||||
LogManager.log.info("[AutoBone] Average height error: " + StringUtils.prettyNumber(mean, 6) + " (SD " + StringUtils.prettyNumber(std, 6) + ")");
|
||||
}
|
||||
//#endregion
|
||||
} catch (Exception e) {
|
||||
setText("Failed...");
|
||||
LogManager.log.severe("[AutoBone] Failed adjustment!", e);
|
||||
try {
|
||||
Thread.sleep(3000); // Wait for 3 seconds
|
||||
} catch (Exception e1) {
|
||||
// Ignore
|
||||
}
|
||||
} finally {
|
||||
setText("Auto-Adjust");
|
||||
autoBoneThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
autoBoneThread = thread;
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
}});
|
||||
|
||||
add(applyButton = new JButton("Apply Values") {{
|
||||
setEnabled(false);
|
||||
addMouseListener(new MouseInputAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoBone.applyConfig();
|
||||
// Update GUI values after applying
|
||||
skeletonConfig.refreshAll();
|
||||
}
|
||||
});
|
||||
}});
|
||||
}});
|
||||
|
||||
pane.add(new EJBox(BoxLayout.LINE_AXIS) {{
|
||||
setBorder(new EmptyBorder(i(5)));
|
||||
add(processLabel = new JLabel("Processing has not been started..."));
|
||||
}});
|
||||
|
||||
pane.add(new EJBox(BoxLayout.LINE_AXIS) {{
|
||||
setBorder(new EmptyBorder(i(5)));
|
||||
add(lengthsLabel = new JLabel(getLengthsString()));
|
||||
}});
|
||||
|
||||
// Pack and display
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package io.eiren.gui.autobone;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import io.eiren.vr.processor.HumanSkeletonWithLegs;
|
||||
import io.eiren.vr.processor.TransformNode;
|
||||
|
||||
public final class PoseFrame {
|
||||
|
||||
public final Vector3f rootPos;
|
||||
public final HashMap<String, Quaternion> rotations;
|
||||
public final HashMap<String, Vector3f> positions;
|
||||
|
||||
public PoseFrame(Vector3f rootPos, HashMap<String, Quaternion> rotations, HashMap<String, Vector3f> positions) {
|
||||
this.rootPos = rootPos;
|
||||
this.rotations = rotations;
|
||||
this.positions = positions;
|
||||
}
|
||||
|
||||
public PoseFrame(HumanSkeletonWithLegs skeleton) {
|
||||
// Copy headset position
|
||||
TransformNode rootNode = skeleton.getRootNode();
|
||||
this.rootPos = new Vector3f(rootNode.localTransform.getTranslation());
|
||||
|
||||
// Copy all rotations
|
||||
this.rotations = new HashMap<String, Quaternion>();
|
||||
rootNode.depthFirstTraversal(visitor -> {
|
||||
// Insert a copied quaternion so it isn't changed by reference
|
||||
rotations.put(visitor.getName(), new Quaternion(visitor.localTransform.getRotation()));
|
||||
});
|
||||
|
||||
this.positions = null;
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package io.eiren.gui.autobone;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import io.eiren.util.logging.LogManager;
|
||||
|
||||
public final class PoseFrameIO {
|
||||
|
||||
private PoseFrameIO() {
|
||||
// Do not allow instantiating
|
||||
}
|
||||
|
||||
public static boolean writeToFile(File file, PoseFrame[] frames) {
|
||||
try (DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
// Write every frame
|
||||
outputStream.writeInt(frames.length);
|
||||
for (PoseFrame frame : frames) {
|
||||
writeFrame(outputStream, frame);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogManager.log.severe("Error writing frames to file", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean writeFrame(DataOutputStream outputStream, PoseFrame frame) {
|
||||
try {
|
||||
// Write root position vector
|
||||
outputStream.writeFloat(frame.rootPos.x);
|
||||
outputStream.writeFloat(frame.rootPos.y);
|
||||
outputStream.writeFloat(frame.rootPos.z);
|
||||
|
||||
if (frame.rotations != null) {
|
||||
// Write rotations
|
||||
outputStream.writeInt(frame.rotations.size());
|
||||
for (Entry<String, Quaternion> entry : frame.rotations.entrySet()) {
|
||||
// Write the label string
|
||||
outputStream.writeUTF(entry.getKey());
|
||||
|
||||
// Write the rotation quaternion
|
||||
Quaternion quat = entry.getValue();
|
||||
outputStream.writeFloat(quat.getX());
|
||||
outputStream.writeFloat(quat.getY());
|
||||
outputStream.writeFloat(quat.getZ());
|
||||
outputStream.writeFloat(quat.getW());
|
||||
}
|
||||
} else {
|
||||
outputStream.writeInt(0);
|
||||
}
|
||||
|
||||
if (frame.positions != null) {
|
||||
// Write positions
|
||||
outputStream.writeInt(frame.positions.size());
|
||||
for (Entry<String, Vector3f> entry : frame.positions.entrySet()) {
|
||||
// Write the label string
|
||||
outputStream.writeUTF(entry.getKey());
|
||||
|
||||
// Write the rotation quaternion
|
||||
Vector3f vec = entry.getValue();
|
||||
outputStream.writeFloat(vec.getX());
|
||||
outputStream.writeFloat(vec.getY());
|
||||
outputStream.writeFloat(vec.getZ());
|
||||
}
|
||||
} else {
|
||||
outputStream.writeInt(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogManager.log.severe("Error writing frame to stream", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean writeFrame(File file, PoseFrame frame) {
|
||||
try (DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
writeFrame(outputStream, frame);
|
||||
} catch (Exception e) {
|
||||
LogManager.log.severe("Error writing frame to file", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PoseFrame[] readFromFile(File file) {
|
||||
try (DataInputStream inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {
|
||||
int frameCount = inputStream.readInt();
|
||||
|
||||
PoseFrame[] frames = new PoseFrame[frameCount];
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
frames[i] = readFrame(inputStream);
|
||||
}
|
||||
|
||||
return frames;
|
||||
} catch (Exception e) {
|
||||
LogManager.log.severe("Error reading frames from file", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PoseFrame readFrame(DataInputStream inputStream) {
|
||||
try {
|
||||
float vecX = inputStream.readFloat();
|
||||
float vecY = inputStream.readFloat();
|
||||
float vecZ = inputStream.readFloat();
|
||||
|
||||
Vector3f vector = new Vector3f(vecX, vecY, vecZ);
|
||||
|
||||
int rotationCount = inputStream.readInt();
|
||||
HashMap<String, Quaternion> rotations = null;
|
||||
if (rotationCount > 0) {
|
||||
rotations = new HashMap<String, Quaternion>(rotationCount);
|
||||
for (int j = 0; j < rotationCount; j++) {
|
||||
String label = inputStream.readUTF();
|
||||
|
||||
float quatX = inputStream.readFloat();
|
||||
float quatY = inputStream.readFloat();
|
||||
float quatZ = inputStream.readFloat();
|
||||
float quatW = inputStream.readFloat();
|
||||
Quaternion quaternion = new Quaternion(quatX, quatY, quatZ, quatW);
|
||||
|
||||
rotations.put(label, quaternion);
|
||||
}
|
||||
}
|
||||
|
||||
int positionCount = inputStream.readInt();
|
||||
HashMap<String, Vector3f> positions = null;
|
||||
if (positionCount > 0) {
|
||||
positions = new HashMap<String, Vector3f>(positionCount);
|
||||
for (int j = 0; j < positionCount; j++) {
|
||||
String label = inputStream.readUTF();
|
||||
|
||||
float posX = inputStream.readFloat();
|
||||
float posY = inputStream.readFloat();
|
||||
float posZ = inputStream.readFloat();
|
||||
Vector3f position = new Vector3f(posX, posY, posZ);
|
||||
|
||||
positions.put(label, position);
|
||||
}
|
||||
}
|
||||
|
||||
return new PoseFrame(vector, rotations, positions);
|
||||
} catch (Exception e) {
|
||||
LogManager.log.severe("Error reading frame from stream", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static PoseFrame readFrame(File file) {
|
||||
try {
|
||||
return readFrame(new DataInputStream(new BufferedInputStream(new FileInputStream(file))));
|
||||
} catch (Exception e) {
|
||||
LogManager.log.severe("Error reading frame from file", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package io.eiren.gui.autobone;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import io.eiren.util.ann.ThreadSafe;
|
||||
import io.eiren.util.ann.VRServerThread;
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.eiren.vr.VRServer;
|
||||
import io.eiren.vr.processor.HumanSkeleton;
|
||||
import io.eiren.vr.processor.HumanSkeletonWithLegs;
|
||||
|
||||
public class PoseRecorder {
|
||||
|
||||
protected final FastList<PoseFrame> frames = new FastList<PoseFrame>();
|
||||
|
||||
protected int numFrames = -1;
|
||||
protected long frameRecordingInterval = 60L;
|
||||
protected long nextFrameTimeMs = -1L;
|
||||
|
||||
protected CompletableFuture<PoseFrame[]> currentRecording;
|
||||
|
||||
protected final VRServer server;
|
||||
HumanSkeletonWithLegs skeleton = null;
|
||||
|
||||
public PoseRecorder(VRServer server) {
|
||||
this.server = server;
|
||||
server.addOnTick(this::onTick);
|
||||
server.addSkeletonUpdatedCallback(this::skeletonUpdated);
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
public void onTick() {
|
||||
if (numFrames > 0) {
|
||||
HumanSkeletonWithLegs skeleton = this.skeleton;
|
||||
if (skeleton != null) {
|
||||
if (frames.size() < numFrames) {
|
||||
if (System.currentTimeMillis() >= nextFrameTimeMs) {
|
||||
nextFrameTimeMs = System.currentTimeMillis() + frameRecordingInterval;
|
||||
frames.add(new PoseFrame(skeleton));
|
||||
|
||||
// If done, send finished recording
|
||||
if (frames.size() >= numFrames) {
|
||||
internalStopRecording();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If done and hasn't yet, send finished recording
|
||||
internalStopRecording();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
public void skeletonUpdated(HumanSkeleton newSkeleton) {
|
||||
if (newSkeleton instanceof HumanSkeletonWithLegs) {
|
||||
skeleton = (HumanSkeletonWithLegs) newSkeleton;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Future<PoseFrame[]> startFrameRecording(int numFrames, long interval) {
|
||||
if (numFrames < 1) {
|
||||
throw new IllegalArgumentException("numFrames must at least have a value of 1");
|
||||
}
|
||||
if (interval < 1) {
|
||||
throw new IllegalArgumentException("interval must at least have a value of 1");
|
||||
}
|
||||
if (!isReadyToRecord()) {
|
||||
throw new IllegalStateException("PoseRecorder isn't ready to record!");
|
||||
}
|
||||
|
||||
cancelFrameRecording();
|
||||
|
||||
// Clear old frames and ensure new size can be held
|
||||
frames.clear();
|
||||
frames.ensureCapacity(numFrames);
|
||||
|
||||
this.numFrames = numFrames;
|
||||
|
||||
frameRecordingInterval = interval;
|
||||
nextFrameTimeMs = -1L;
|
||||
|
||||
LogManager.log.info("[PoseRecorder] Recording " + numFrames + " samples at a " + interval + " ms frame interval");
|
||||
|
||||
currentRecording = new CompletableFuture<PoseFrame[]>();
|
||||
return currentRecording;
|
||||
}
|
||||
|
||||
private void internalStopRecording() {
|
||||
CompletableFuture<PoseFrame[]> currentRecording = this.currentRecording;
|
||||
if (currentRecording != null && !currentRecording.isDone()) {
|
||||
// Stop the recording, returning the frames recorded
|
||||
currentRecording.complete(frames.toArray(new PoseFrame[0]));
|
||||
}
|
||||
|
||||
numFrames = -1;
|
||||
}
|
||||
|
||||
public synchronized void stopFrameRecording() {
|
||||
internalStopRecording();
|
||||
}
|
||||
|
||||
public synchronized void cancelFrameRecording() {
|
||||
CompletableFuture<PoseFrame[]> currentRecording = this.currentRecording;
|
||||
if (currentRecording != null && !currentRecording.isDone()) {
|
||||
// Cancel the current recording and return nothing
|
||||
currentRecording.cancel(true);
|
||||
}
|
||||
|
||||
numFrames = -1;
|
||||
}
|
||||
|
||||
public boolean isReadyToRecord() {
|
||||
return skeleton != null;
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return numFrames > frames.size();
|
||||
}
|
||||
|
||||
public boolean hasRecording() {
|
||||
return currentRecording != null;
|
||||
}
|
||||
|
||||
public Future<PoseFrame[]> getFramesAsync() {
|
||||
return currentRecording;
|
||||
}
|
||||
|
||||
public PoseFrame[] getFrames() throws ExecutionException, InterruptedException {
|
||||
CompletableFuture<PoseFrame[]> currentRecording = this.currentRecording;
|
||||
return currentRecording != null ? currentRecording.get() : null;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import io.eiren.util.logging.LogManager;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static String VERSION = "0.0.17";
|
||||
public static String VERSION = "0.0.19";
|
||||
|
||||
public static VRServer vrServer;
|
||||
|
||||
@@ -29,6 +29,12 @@ public class Main {
|
||||
new VRServerGUI(vrServer);
|
||||
} catch(Throwable e) {
|
||||
e.printStackTrace();
|
||||
try {
|
||||
Thread.sleep(2000L);
|
||||
} catch(InterruptedException e2) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.exit(1); // Exit in case error happened on init and window not appeared, but some thread started
|
||||
} finally {
|
||||
try {
|
||||
Thread.sleep(2000L);
|
||||
|
||||
@@ -149,7 +149,7 @@ public class VRServer extends Thread {
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
public void saveConfig() {
|
||||
public synchronized void saveConfig() {
|
||||
List<YamlNode> nodes = config.getNodeList("trackers", null);
|
||||
List<Map<String, Object>> trackersConfig = new FastList<>(nodes.size());
|
||||
for(int i = 0; i < nodes.size(); ++i) {
|
||||
|
||||
@@ -52,7 +52,7 @@ public class NamedPipeVRBridge extends Thread implements VRBridge {
|
||||
this.internalTrackers = new FastList<>(shareTrackers.size());
|
||||
for(int i = 0; i < shareTrackers.size(); ++i) {
|
||||
Tracker t = shareTrackers.get(i);
|
||||
ComputedTracker ct = new ComputedTracker("internal://" + t.getName());
|
||||
ComputedTracker ct = new ComputedTracker("internal://" + t.getName(), true, true);
|
||||
ct.setStatus(TrackerStatus.OK);
|
||||
this.internalTrackers.add(ct);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class SteamVRPipeInputBridge extends Thread implements VRBridge {
|
||||
LogManager.log.severe("[SteamVRPipeInputBridge] Error in ADD command. Command requires at least 4 arguments. Supplied: " + commandBuilder.toString());
|
||||
return;
|
||||
}
|
||||
SteamVRTracker internalTracker = new SteamVRTracker(Integer.parseInt(command[1]), StringUtils.join(command, " ", 3, command.length));
|
||||
SteamVRTracker internalTracker = new SteamVRTracker(Integer.parseInt(command[1]), StringUtils.join(command, " ", 3, command.length), true, true);
|
||||
int roleId = Integer.parseInt(command[2]);
|
||||
if(roleId >= 0 && roleId < SteamVRInputRoles.values.length) {
|
||||
SteamVRInputRoles svrRole = SteamVRInputRoles.values[roleId];
|
||||
@@ -183,7 +183,7 @@ public class SteamVRPipeInputBridge extends Thread implements VRBridge {
|
||||
continue internal;
|
||||
}
|
||||
// Tracker is not found in current trackers
|
||||
SteamVRTracker tracker = new SteamVRTracker(internalTracker.id, internalTracker.getName());
|
||||
SteamVRTracker tracker = new SteamVRTracker(internalTracker.id, internalTracker.getName(), true, true);
|
||||
tracker.bodyPosition = internalTracker.bodyPosition;
|
||||
trackers.add(tracker);
|
||||
server.registerTracker(tracker);
|
||||
@@ -199,6 +199,7 @@ public class SteamVRPipeInputBridge extends Thread implements VRBridge {
|
||||
tracker.position.set(vBuffer);
|
||||
if(internal.getRotation(qBuffer))
|
||||
tracker.rotation.set(qBuffer);
|
||||
tracker.setStatus(internal.getStatus());
|
||||
tracker.dataTick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public class ComputedHumanPoseTracker extends ComputedTracker implements Tracker
|
||||
protected BufferedTimer timer = new BufferedTimer(1f);
|
||||
|
||||
public ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition skeletonPosition) {
|
||||
super("human://" + skeletonPosition.name());
|
||||
super("human://" + skeletonPosition.name(), true, true);
|
||||
this.skeletonPosition = skeletonPosition;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import io.eiren.vr.VRServer;
|
||||
import io.eiren.vr.trackers.HMDTracker;
|
||||
import io.eiren.vr.trackers.Tracker;
|
||||
import io.eiren.vr.trackers.TrackerStatus;
|
||||
import io.eiren.vr.trackers.TrackerUtils;
|
||||
|
||||
public class HumanPoseProcessor {
|
||||
|
||||
@@ -85,29 +84,10 @@ public class HumanPoseProcessor {
|
||||
|
||||
@VRServerThread
|
||||
private void updateSekeltonModel() {
|
||||
boolean hasWaist = false;
|
||||
boolean hasBothLegs = false;
|
||||
List<Tracker> allTrackers = server.getAllTrackers();
|
||||
Tracker waist = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
|
||||
Tracker leftAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
|
||||
Tracker rightAnkle = TrackerUtils.findTrackerForBodyPosition(allTrackers, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
|
||||
if(waist != null)
|
||||
hasWaist = true;
|
||||
if(leftAnkle != null && rightAnkle != null)
|
||||
hasBothLegs = true;
|
||||
if(!hasWaist) {
|
||||
skeleton = null; // Can't track anything without waist
|
||||
} else if(hasBothLegs) {
|
||||
disconnectAllTrackers();
|
||||
skeleton = new HumanSkeletonWithLegs(server, computedTrackers);
|
||||
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
|
||||
onSkeletonUpdated.get(i).accept(skeleton);
|
||||
} else {
|
||||
disconnectAllTrackers();
|
||||
skeleton = new HumanSkeletonWithWaist(server, computedTrackers);
|
||||
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
|
||||
onSkeletonUpdated.get(i).accept(skeleton);
|
||||
}
|
||||
disconnectAllTrackers();
|
||||
skeleton = new HumanSkeletonWithLegs(server, computedTrackers);
|
||||
for(int i = 0; i < onSkeletonUpdated.size(); ++i)
|
||||
onSkeletonUpdated.get(i).accept(skeleton);
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
|
||||
@@ -69,11 +69,11 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
|
||||
public HumanSkeletonWithLegs(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
|
||||
super(server, computedTrackers);
|
||||
List<Tracker> allTracekrs = server.getAllTrackers();
|
||||
this.leftLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_LEG, TrackerBodyPosition.LEFT_ANKLE);
|
||||
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
|
||||
this.leftLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.LEFT_LEG, TrackerBodyPosition.LEFT_ANKLE);
|
||||
this.leftAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.LEFT_ANKLE, TrackerBodyPosition.LEFT_LEG);
|
||||
this.leftFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.LEFT_FOOT);
|
||||
this.rightLegTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_LEG, TrackerBodyPosition.RIGHT_ANKLE);
|
||||
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
|
||||
this.rightLegTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.RIGHT_LEG, TrackerBodyPosition.RIGHT_ANKLE);
|
||||
this.rightAnkleTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.RIGHT_ANKLE, TrackerBodyPosition.RIGHT_LEG);
|
||||
this.rightFootTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.RIGHT_FOOT);
|
||||
ComputedHumanPoseTracker lat = null;
|
||||
ComputedHumanPoseTracker rat = null;
|
||||
@@ -90,6 +90,10 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
|
||||
if(t.skeletonPosition == ComputedHumanPoseTrackerPosition.RIGHT_KNEE)
|
||||
rkt = t;
|
||||
}
|
||||
if(lat == null)
|
||||
lat = new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.LEFT_FOOT);
|
||||
if(rat == null)
|
||||
rat = new ComputedHumanPoseTracker(ComputedHumanPoseTrackerPosition.RIGHT_FOOT);
|
||||
computedLeftFootTracker = lat;
|
||||
computedRightFootTracker = rat;
|
||||
computedLeftKneeTracker = lkt;
|
||||
@@ -100,7 +104,7 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
|
||||
kneeHeight = server.config.getFloat("body.kneeHeight", kneeHeight);
|
||||
legsLength = server.config.getFloat("body.legsLength", legsLength);
|
||||
footLength = server.config.getFloat("body.footLength", footLength);
|
||||
extendedPelvisModel = server.config.getBoolean("body.model.extendedPelvis", extendedPelvisModel);
|
||||
//extendedPelvisModel = server.config.getBoolean("body.model.extendedPelvis", extendedPelvisModel);
|
||||
extendedKneeModel = server.config.getBoolean("body.model.extendedKnee", extendedKneeModel);
|
||||
|
||||
waistNode.attachChild(leftHipNode);
|
||||
@@ -268,7 +272,9 @@ public class HumanSkeletonWithLegs extends HumanSkeletonWithWaist {
|
||||
// Average pelvis between two legs
|
||||
leftHipNode.localTransform.getRotation(hipBuf);
|
||||
rightHipNode.localTransform.getRotation(kneeBuf);
|
||||
kneeBuf.slerp(hipBuf, 0.5f);
|
||||
kneeBuf.nlerp(hipBuf, 0.5f);
|
||||
chestNode.localTransform.getRotation(hipBuf);
|
||||
kneeBuf.nlerp(hipBuf, 0.3333333f);
|
||||
waistNode.localTransform.setRotation(kneeBuf);
|
||||
// TODO : Use vectors to add like 50% of wasit tracker yaw to waist node to reduce drift and let user take weird poses
|
||||
// TODO Set virtual waist node yaw to that of waist node
|
||||
|
||||
@@ -60,8 +60,8 @@ public class HumanSkeletonWithWaist extends HumanSkeleton {
|
||||
|
||||
public HumanSkeletonWithWaist(VRServer server, List<ComputedHumanPoseTracker> computedTrackers) {
|
||||
List<Tracker> allTracekrs = server.getAllTrackers();
|
||||
this.waistTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
|
||||
this.chestTracker = TrackerUtils.findTrackerForBodyPosition(allTracekrs, TrackerBodyPosition.CHEST, TrackerBodyPosition.WAIST);
|
||||
this.waistTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.WAIST, TrackerBodyPosition.CHEST);
|
||||
this.chestTracker = TrackerUtils.findTrackerForBodyPositionOrEmpty(allTracekrs, TrackerBodyPosition.CHEST, TrackerBodyPosition.WAIST);
|
||||
this.hmdTracker = server.hmdTracker;
|
||||
this.server = server;
|
||||
ComputedHumanPoseTracker cwt = null;
|
||||
|
||||
@@ -34,6 +34,6 @@ public enum TrackerBodyPosition {
|
||||
|
||||
static {
|
||||
for(TrackerBodyPosition tbp : values())
|
||||
byDesignation.put(tbp.designation, tbp);
|
||||
byDesignation.put(tbp.designation.toLowerCase(), tbp);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,13 @@ public class ComputedTracker implements Tracker {
|
||||
protected final String name;
|
||||
protected TrackerStatus status = TrackerStatus.DISCONNECTED;
|
||||
public TrackerBodyPosition bodyPosition = null;
|
||||
protected final boolean hasRotation;
|
||||
protected final boolean hasPosition;
|
||||
|
||||
public ComputedTracker(String name) {
|
||||
public ComputedTracker(String name, boolean hasRotation, boolean hasPosition) {
|
||||
this.name = name;
|
||||
this.hasRotation = hasRotation;
|
||||
this.hasPosition = hasPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -24,7 +28,10 @@ public class ComputedTracker implements Tracker {
|
||||
|
||||
@Override
|
||||
public void loadConfig(TrackerConfig config) {
|
||||
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
|
||||
// Loading a config is an act of user editing, therefore it shouldn't not be allowed if editing is not allowed
|
||||
if (userEditable()) {
|
||||
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,4 +91,19 @@ public class ComputedTracker implements Tracker {
|
||||
@Override
|
||||
public void tick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRotation() {
|
||||
return hasRotation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPosition() {
|
||||
return hasPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public class HMDTracker extends ComputedTracker implements TrackerWithTPS {
|
||||
protected BufferedTimer timer = new BufferedTimer(1f);
|
||||
|
||||
public HMDTracker(String name) {
|
||||
super(name);
|
||||
super(name, true, true);
|
||||
setBodyPosition(TrackerBodyPosition.HMD);
|
||||
}
|
||||
|
||||
@@ -21,4 +21,9 @@ public class HMDTracker extends ComputedTracker implements TrackerWithTPS {
|
||||
public void dataTick() {
|
||||
timer.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,20 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
|
||||
|
||||
@Override
|
||||
public void loadConfig(TrackerConfig config) {
|
||||
if(config.mountingRotation != null) {
|
||||
mounting = TrackerMountingRotation.valueOf(config.mountingRotation);
|
||||
if(mounting != null) {
|
||||
rotAdjust.set(mounting.quaternion);
|
||||
// Loading a config is an act of user editing, therefore it shouldn't not be allowed if editing is not allowed
|
||||
if (userEditable()) {
|
||||
if(config.mountingRotation != null) {
|
||||
mounting = TrackerMountingRotation.valueOf(config.mountingRotation);
|
||||
if(mounting != null) {
|
||||
rotAdjust.set(mounting.quaternion);
|
||||
} else {
|
||||
rotAdjust.loadIdentity();
|
||||
}
|
||||
} else {
|
||||
rotAdjust.loadIdentity();
|
||||
}
|
||||
} else {
|
||||
rotAdjust.loadIdentity();
|
||||
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
|
||||
}
|
||||
bodyPosition = TrackerBodyPosition.getByDesignation(config.designation);
|
||||
}
|
||||
|
||||
public TrackerMountingRotation getMountingRotation() {
|
||||
@@ -225,4 +228,19 @@ public class IMUTracker implements Tracker, TrackerWithTPS, TrackerWithBattery {
|
||||
byStatus[ca.status] = ca;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRotation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPosition() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,4 +146,19 @@ public class ReferenceAdjustedTracker<E extends Tracker> implements Tracker {
|
||||
public void tick() {
|
||||
tracker.tick();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRotation() {
|
||||
return tracker.hasRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPosition() {
|
||||
return tracker.hasPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return tracker.isComputed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ public class SteamVRTracker extends ComputedTracker implements TrackerWithTPS {
|
||||
public final int id;
|
||||
protected BufferedTimer timer = new BufferedTimer(1f);
|
||||
|
||||
public SteamVRTracker(int id, String name) {
|
||||
super(name);
|
||||
public SteamVRTracker(int id, String name, boolean hasRotation, boolean hasPosition) {
|
||||
super(name, hasRotation, hasPosition);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@@ -21,4 +21,14 @@ public class SteamVRTracker extends ComputedTracker implements TrackerWithTPS {
|
||||
public void dataTick() {
|
||||
timer.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean userEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isComputed() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,10 @@ public interface Tracker {
|
||||
public void setBodyPosition(TrackerBodyPosition position);
|
||||
|
||||
public boolean userEditable();
|
||||
|
||||
public boolean hasRotation();
|
||||
|
||||
public boolean hasPosition();
|
||||
|
||||
public boolean isComputed();
|
||||
}
|
||||
|
||||
@@ -8,20 +8,56 @@ public class TrackerUtils {
|
||||
|
||||
private TrackerUtils() {
|
||||
}
|
||||
|
||||
public static Tracker findTrackerForBodyPosition(List<Tracker> allTrackers, TrackerBodyPosition position) {
|
||||
for(int i = 0; i < allTrackers.size(); ++i) {
|
||||
Tracker t = allTrackers.get(i);
|
||||
if(t.getBodyPosition() == position)
|
||||
|
||||
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerBodyPosition position) {
|
||||
for(int i = 0; i < allTrackers.length; ++i) {
|
||||
T t = allTrackers[i];
|
||||
if(t != null && t.getBodyPosition() == position)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Tracker findTrackerForBodyPosition(List<Tracker> allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
|
||||
Tracker t = findTrackerForBodyPosition(allTrackers, position);
|
||||
|
||||
public static <T extends Tracker> T findTrackerForBodyPosition(List<T> allTrackers, TrackerBodyPosition position) {
|
||||
for(int i = 0; i < allTrackers.size(); ++i) {
|
||||
T t = allTrackers.get(i);
|
||||
if(t != null && t.getBodyPosition() == position)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T extends Tracker> T findTrackerForBodyPosition(List<T> allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
|
||||
T t = findTrackerForBodyPosition(allTrackers, position);
|
||||
if(t != null)
|
||||
return t;
|
||||
return findTrackerForBodyPosition(allTrackers, altPosition);
|
||||
}
|
||||
|
||||
public static <T extends Tracker> T findTrackerForBodyPosition(T[] allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
|
||||
T t = findTrackerForBodyPosition(allTrackers, position);
|
||||
if(t != null)
|
||||
return t;
|
||||
return findTrackerForBodyPosition(allTrackers, altPosition);
|
||||
}
|
||||
|
||||
public static Tracker findTrackerForBodyPositionOrEmpty(List<? extends Tracker> allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
|
||||
Tracker t = findTrackerForBodyPosition(allTrackers, position);
|
||||
if(t != null)
|
||||
return t;
|
||||
t = findTrackerForBodyPosition(allTrackers, altPosition);
|
||||
if(t != null)
|
||||
return t;
|
||||
return new ComputedTracker("Empty tracker", false, false);
|
||||
}
|
||||
|
||||
public static Tracker findTrackerForBodyPositionOrEmpty(Tracker[] allTrackers, TrackerBodyPosition position, TrackerBodyPosition altPosition) {
|
||||
Tracker t = findTrackerForBodyPosition(allTrackers, position);
|
||||
if(t != null)
|
||||
return t;
|
||||
t = findTrackerForBodyPosition(allTrackers, altPosition);
|
||||
if(t != null)
|
||||
return t;
|
||||
return new ComputedTracker("Empty tracker", false, false);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/main/resources/icon128.png
Normal file
BIN
src/main/resources/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/main/resources/icon16.png
Normal file
BIN
src/main/resources/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 429 B |
BIN
src/main/resources/icon256.png
Normal file
BIN
src/main/resources/icon256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src/main/resources/icon32.png
Normal file
BIN
src/main/resources/icon32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 689 B |
BIN
src/main/resources/icon48.png
Normal file
BIN
src/main/resources/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/main/resources/icon64.png
Normal file
BIN
src/main/resources/icon64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -67,7 +67,7 @@ public class ReferenceAdjustmentsTests {
|
||||
|
||||
public void checkReferenceAdjustmentFull(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
|
||||
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
|
||||
ComputedTracker tracker = new ComputedTracker("test");
|
||||
ComputedTracker tracker = new ComputedTracker("test", true, false);
|
||||
tracker.rotation.set(trackerQuat);
|
||||
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
|
||||
adj.resetFull(referenceQuat);
|
||||
@@ -86,7 +86,7 @@ public class ReferenceAdjustmentsTests {
|
||||
|
||||
public void checkReferenceAdjustmentYaw(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
|
||||
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
|
||||
ComputedTracker tracker = new ComputedTracker("test");
|
||||
ComputedTracker tracker = new ComputedTracker("test", true, false);
|
||||
tracker.rotation.set(trackerQuat);
|
||||
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
|
||||
adj.resetYaw(referenceQuat);
|
||||
@@ -98,7 +98,7 @@ public class ReferenceAdjustmentsTests {
|
||||
|
||||
private void testAdjustedTrackerRotation(Quaternion trackerQuat, int refPitch, int refYaw, int refRoll) {
|
||||
Quaternion referenceQuat = q(refPitch, refYaw, refRoll);
|
||||
ComputedTracker tracker = new ComputedTracker("test");
|
||||
ComputedTracker tracker = new ComputedTracker("test", true, false);
|
||||
tracker.rotation.set(trackerQuat);
|
||||
ReferenceAdjustedTracker<ComputedTracker> adj = new ReferenceAdjustedTracker<>(tracker);
|
||||
adj.resetFull(referenceQuat);
|
||||
|
||||
Reference in New Issue
Block a user