mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Burn everything and start fresh
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
perSystem = { pkgs, ... }:
|
||||
let
|
||||
runtimeLibs = pkgs: (with pkgs; [
|
||||
jdk17
|
||||
jdk22
|
||||
|
||||
alsa-lib at-spi2-atk at-spi2-core cairo cups dbus expat
|
||||
gdk-pixbuf glib gtk3 libdrm libgbm libglvnd libnotify
|
||||
@@ -33,8 +33,8 @@
|
||||
name = "slimevr-env";
|
||||
targetPkgs = runtimeLibs;
|
||||
profile = ''
|
||||
export JAVA_HOME=${pkgs.jdk17}
|
||||
export PATH="${pkgs.jdk17}/bin:$PATH"
|
||||
export JAVA_HOME=${pkgs.jdk22}
|
||||
export PATH="${pkgs.jdk22}/bin:$PATH"
|
||||
|
||||
# Tell electron-builder to use system tools instead of downloading them
|
||||
export USE_SYSTEM_FPM=true
|
||||
|
||||
@@ -22,12 +22,12 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
languageVersion.set(JavaLanguageVersion.of(22))
|
||||
}
|
||||
}
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
languageVersion.set(JavaLanguageVersion.of(22))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ val deleteTempKeyStore = tasks.register<Delete>("deleteTempKeyStore") {
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
jvmTarget.set(JvmTarget.JVM_22)
|
||||
freeCompilerArgs.set(listOf("-Xvalue-classes"))
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_22
|
||||
targetCompatibility = JavaVersion.VERSION_22
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,19 @@ plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
// FIXME: Please replace these to Java 11 as that's what they actually are
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
languageVersion.set(JavaLanguageVersion.of(22))
|
||||
}
|
||||
}
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
languageVersion.set(JavaLanguageVersion.of(22))
|
||||
}
|
||||
}
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
jvmTarget.set(JvmTarget.JVM_22)
|
||||
freeCompilerArgs.set(listOf("-Xvalue-classes"))
|
||||
}
|
||||
}
|
||||
@@ -60,17 +59,7 @@ allprojects {
|
||||
dependencies {
|
||||
implementation(project(":solarxr-protocol"))
|
||||
|
||||
// This dependency is used internally,
|
||||
// and not exposed to consumers on their own compile classpath.
|
||||
implementation("com.google.flatbuffers:flatbuffers-java:22.10.26")
|
||||
implementation("commons-cli:commons-cli:1.11.0")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.21.0")
|
||||
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.0")
|
||||
|
||||
implementation("com.github.jonpeterson:jackson-module-model-versioning:1.2.2")
|
||||
implementation("org.apache.commons:commons-math3:3.6.1")
|
||||
implementation("org.apache.commons:commons-lang3:3.20.0")
|
||||
implementation("org.apache.commons:commons-collections4:4.5.0")
|
||||
|
||||
implementation("com.illposed.osc:javaosc-core:0.9")
|
||||
implementation("org.java-websocket:Java-WebSocket:1.+")
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package dev.slimevr
|
||||
|
||||
import com.melloware.jintellitype.HotkeyListener
|
||||
import com.melloware.jintellitype.JIntellitype
|
||||
import dev.slimevr.config.KeybindingsConfig
|
||||
import dev.slimevr.tracking.trackers.TrackerUtils
|
||||
import io.eiren.util.OperatingSystem
|
||||
import io.eiren.util.OperatingSystem.Companion.currentPlatform
|
||||
import io.eiren.util.ann.AWTThread
|
||||
import io.eiren.util.logging.LogManager
|
||||
|
||||
class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
|
||||
val config: KeybindingsConfig = server.configManager.vrConfig.keybindings
|
||||
|
||||
init {
|
||||
if (currentPlatform != OperatingSystem.WINDOWS) {
|
||||
LogManager
|
||||
.info(
|
||||
"[Keybinding] Currently only supported on Windows. Keybindings will be disabled.",
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
if (JIntellitype.getInstance() != null) {
|
||||
JIntellitype.getInstance().addHotKeyListener(this)
|
||||
|
||||
val fullResetBinding = config.fullResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(FULL_RESET, fullResetBinding)
|
||||
LogManager.info("[Keybinding] Bound full reset to $fullResetBinding")
|
||||
|
||||
val yawResetBinding = config.yawResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(YAW_RESET, yawResetBinding)
|
||||
LogManager.info("[Keybinding] Bound yaw reset to $yawResetBinding")
|
||||
|
||||
val mountingResetBinding = config.mountingResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(MOUNTING_RESET, mountingResetBinding)
|
||||
LogManager.info("[Keybinding] Bound reset mounting to $mountingResetBinding")
|
||||
|
||||
val feetMountingResetBinding = config.feetMountingResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(FEET_MOUNTING_RESET, feetMountingResetBinding)
|
||||
LogManager.info("[Keybinding] Bound feet reset mounting to $feetMountingResetBinding")
|
||||
|
||||
val pauseTrackingBinding = config.pauseTrackingBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(PAUSE_TRACKING, pauseTrackingBinding)
|
||||
LogManager.info("[Keybinding] Bound pause tracking to $pauseTrackingBinding")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
LogManager
|
||||
.warning(
|
||||
"[Keybinding] JIntellitype initialization failed. Keybindings will be disabled. Try restarting your computer.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AWTThread
|
||||
override fun onHotKey(identifier: Int) {
|
||||
when (identifier) {
|
||||
FULL_RESET -> server.scheduleResetTrackersFull(RESET_SOURCE_NAME, config.fullResetDelay)
|
||||
|
||||
YAW_RESET -> server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, config.yawResetDelay)
|
||||
|
||||
MOUNTING_RESET -> server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
config.mountingResetDelay,
|
||||
)
|
||||
|
||||
FEET_MOUNTING_RESET -> server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
config.feetMountingResetDelay,
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
|
||||
PAUSE_TRACKING ->
|
||||
server
|
||||
.scheduleTogglePauseTracking(
|
||||
RESET_SOURCE_NAME,
|
||||
config.pauseTrackingDelay,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RESET_SOURCE_NAME = "Keybinding"
|
||||
|
||||
private const val FULL_RESET = 1
|
||||
private const val YAW_RESET = 2
|
||||
private const val MOUNTING_RESET = 3
|
||||
private const val FEET_MOUNTING_RESET = 4
|
||||
private const val PAUSE_TRACKING = 5
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package dev.slimevr
|
||||
|
||||
data class NetworkInfo(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val category: NetworkCategory?,
|
||||
val connectivity: Set<ConnectivityFlags>?,
|
||||
val connected: Boolean?,
|
||||
)
|
||||
|
||||
/**
|
||||
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/ne-netlistmgr-nlm_network_category">NLM_NETWORK_CATEGORY enumeration (netlistmgr.h)</a>
|
||||
*/
|
||||
enum class NetworkCategory(val value: Int) {
|
||||
PUBLIC(0),
|
||||
PRIVATE(1),
|
||||
DOMAIN_AUTHENTICATED(2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int) = values().find { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see <a href="https://learn.microsoft.com/en-us/windows/win32/api/netlistmgr/ne-netlistmgr-nlm_connectivity">NLM_CONNECTIVITY enumeration (netlistmgr.h)</a>
|
||||
*/
|
||||
enum class ConnectivityFlags(val value: Int) {
|
||||
DISCONNECTED(0),
|
||||
IPV4_NOTRAFFIC(0x1),
|
||||
IPV6_NOTRAFFIC(0x2),
|
||||
IPV4_SUBNET(0x10),
|
||||
IPV4_LOCALNETWORK(0x20),
|
||||
IPV4_INTERNET(0x40),
|
||||
IPV6_SUBNET(0x100),
|
||||
IPV6_LOCALNETWORK(0x200),
|
||||
IPV6_INTERNET(0x400),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int): Set<ConnectivityFlags> = if (value == 0) {
|
||||
setOf(DISCONNECTED)
|
||||
} else {
|
||||
values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NetworkProfileChecker {
|
||||
abstract val isSupported: Boolean
|
||||
abstract val publicNetworks: List<NetworkInfo>
|
||||
}
|
||||
|
||||
class NetworkProfileCheckerStub : NetworkProfileChecker() {
|
||||
override val isSupported: Boolean
|
||||
get() = false
|
||||
override val publicNetworks: List<NetworkInfo>
|
||||
get() = listOf()
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package dev.slimevr;
|
||||
|
||||
public enum NetworkProtocol {
|
||||
OWO_LEGACY,
|
||||
SLIMEVR_RAW,
|
||||
SLIMEVR_FLATBUFFER,
|
||||
SLIMEVR_WEBSOCKET
|
||||
}
|
||||
@@ -1,487 +1,4 @@
|
||||
package dev.slimevr
|
||||
|
||||
import com.jme3.system.NanoTimer
|
||||
import dev.slimevr.autobone.AutoBoneHandler
|
||||
import dev.slimevr.bridge.Bridge
|
||||
import dev.slimevr.bridge.ISteamVRBridge
|
||||
import dev.slimevr.config.ConfigManager
|
||||
import dev.slimevr.firmware.FirmwareUpdateHandler
|
||||
import dev.slimevr.firmware.SerialFlashingHandler
|
||||
import dev.slimevr.games.vrchat.VRCConfigHandler
|
||||
import dev.slimevr.games.vrchat.VRCConfigHandlerStub
|
||||
import dev.slimevr.games.vrchat.VRChatConfigManager
|
||||
import dev.slimevr.guards.ServerGuards
|
||||
import dev.slimevr.osc.OSCHandler
|
||||
import dev.slimevr.osc.OSCRouter
|
||||
import dev.slimevr.osc.VMCHandler
|
||||
import dev.slimevr.osc.VRCOSCHandler
|
||||
import dev.slimevr.posestreamer.BVHRecorder
|
||||
import dev.slimevr.protocol.ProtocolAPI
|
||||
import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler
|
||||
import dev.slimevr.reset.ResetHandler
|
||||
import dev.slimevr.reset.ResetTimerManager
|
||||
import dev.slimevr.reset.resetTimer
|
||||
import dev.slimevr.serial.ProvisioningHandler
|
||||
import dev.slimevr.serial.SerialHandler
|
||||
import dev.slimevr.serial.SerialHandlerStub
|
||||
import dev.slimevr.setup.HandshakeHandler
|
||||
import dev.slimevr.setup.TapSetupHandler
|
||||
import dev.slimevr.status.StatusSystem
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import dev.slimevr.tracking.trackers.*
|
||||
import dev.slimevr.tracking.trackers.udp.TrackersUDPServer
|
||||
import dev.slimevr.trackingchecklist.TrackingChecklistManager
|
||||
import dev.slimevr.util.ann.VRServerThread
|
||||
import dev.slimevr.websocketapi.WebSocketVRBridge
|
||||
import io.eiren.util.ann.ThreadSafe
|
||||
import io.eiren.util.ann.ThreadSecure
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import solarxr_protocol.datatypes.TrackerIdT
|
||||
import solarxr_protocol.rpc.ResetType
|
||||
import java.util.*
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.function.Consumer
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
typealias BridgeProvider = (
|
||||
server: VRServer,
|
||||
computedTrackers: List<Tracker>,
|
||||
) -> Sequence<Bridge>
|
||||
|
||||
const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR"
|
||||
|
||||
class VRServer @JvmOverloads constructor(
|
||||
bridgeProvider: BridgeProvider = { _, _ -> sequence {} },
|
||||
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
|
||||
flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null },
|
||||
vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() },
|
||||
networkProfileProvider: (VRServer) -> NetworkProfileChecker = { _ -> NetworkProfileCheckerStub() },
|
||||
acquireMulticastLock: () -> Any? = { null },
|
||||
@JvmField val configManager: ConfigManager,
|
||||
) : Thread("VRServer") {
|
||||
|
||||
@JvmField
|
||||
val humanPoseManager: HumanPoseManager
|
||||
private val trackers: MutableList<Tracker> = FastList()
|
||||
val trackersServer: TrackersUDPServer
|
||||
private val bridges: MutableList<Bridge> = FastList()
|
||||
private val tasks: Queue<Runnable> = LinkedBlockingQueue()
|
||||
private val newTrackersConsumers: MutableList<Consumer<Tracker>> = FastList()
|
||||
private val trackerStatusListeners: MutableList<TrackerStatusListener> = FastList()
|
||||
private val onTick: MutableList<Runnable> = FastList()
|
||||
private val lock = acquireMulticastLock()
|
||||
val oSCRouter: OSCRouter
|
||||
|
||||
@JvmField
|
||||
val vrcOSCHandler: VRCOSCHandler
|
||||
val vMCHandler: VMCHandler
|
||||
|
||||
@JvmField
|
||||
val deviceManager: DeviceManager
|
||||
|
||||
@JvmField
|
||||
val bvhRecorder: BVHRecorder
|
||||
|
||||
@JvmField
|
||||
val serialHandler: SerialHandler
|
||||
|
||||
var serialFlashingHandler: SerialFlashingHandler?
|
||||
|
||||
val firmwareUpdateHandler: FirmwareUpdateHandler
|
||||
|
||||
val vrcConfigManager: VRChatConfigManager
|
||||
|
||||
@JvmField
|
||||
val autoBoneHandler: AutoBoneHandler
|
||||
|
||||
@JvmField
|
||||
val tapSetupHandler: TapSetupHandler
|
||||
|
||||
@JvmField
|
||||
val protocolAPI: ProtocolAPI
|
||||
private val timer = Timer()
|
||||
private val resetTimerManager = ResetTimerManager()
|
||||
val fpsTimer = NanoTimer()
|
||||
|
||||
@JvmField
|
||||
val provisioningHandler: ProvisioningHandler
|
||||
|
||||
@JvmField
|
||||
val resetHandler: ResetHandler
|
||||
|
||||
@JvmField
|
||||
val statusSystem = StatusSystem()
|
||||
|
||||
@JvmField
|
||||
val handshakeHandler = HandshakeHandler()
|
||||
|
||||
val trackingChecklistManager: TrackingChecklistManager
|
||||
|
||||
val networkProfileChecker: NetworkProfileChecker
|
||||
|
||||
val serverGuards = ServerGuards()
|
||||
|
||||
init {
|
||||
// UwU
|
||||
deviceManager = DeviceManager(this)
|
||||
serialHandler = serialHandlerProvider(this)
|
||||
serialFlashingHandler = flashingHandlerProvider(this)
|
||||
provisioningHandler = ProvisioningHandler(this)
|
||||
resetHandler = ResetHandler()
|
||||
tapSetupHandler = TapSetupHandler()
|
||||
humanPoseManager = HumanPoseManager(this)
|
||||
// AutoBone requires HumanPoseManager first
|
||||
autoBoneHandler = AutoBoneHandler(this)
|
||||
firmwareUpdateHandler = FirmwareUpdateHandler(this)
|
||||
vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this))
|
||||
networkProfileChecker = networkProfileProvider(this)
|
||||
trackingChecklistManager = TrackingChecklistManager(this)
|
||||
protocolAPI = ProtocolAPI(this)
|
||||
val computedTrackers = humanPoseManager.computedTrackers
|
||||
|
||||
// Start server for SlimeVR trackers
|
||||
val trackerPort = configManager.vrConfig.server.trackerPort
|
||||
LogManager.info("Starting the tracker server on port $trackerPort...")
|
||||
trackersServer = TrackersUDPServer(
|
||||
trackerPort,
|
||||
"Sensors UDP server",
|
||||
) { tracker: Tracker -> registerTracker(tracker) }
|
||||
|
||||
// Start bridges and WebSocket server
|
||||
for (bridge in bridgeProvider(this, computedTrackers) + sequenceOf(WebSocketVRBridge(computedTrackers, this))) {
|
||||
tasks.add(Runnable { bridge.startBridge() })
|
||||
bridges.add(bridge)
|
||||
}
|
||||
|
||||
// Initialize OSC handlers
|
||||
vrcOSCHandler = VRCOSCHandler(
|
||||
this,
|
||||
configManager.vrConfig.vrcOSC,
|
||||
computedTrackers,
|
||||
)
|
||||
vMCHandler = VMCHandler(
|
||||
this,
|
||||
humanPoseManager,
|
||||
configManager.vrConfig.vmc,
|
||||
)
|
||||
|
||||
// Initialize OSC router
|
||||
val oscHandlers = FastList<OSCHandler>()
|
||||
oscHandlers.add(vrcOSCHandler)
|
||||
oscHandlers.add(vMCHandler)
|
||||
oSCRouter = OSCRouter(configManager.vrConfig.oscRouter, oscHandlers)
|
||||
bvhRecorder = BVHRecorder(this)
|
||||
for (tracker in computedTrackers) {
|
||||
registerTracker(tracker)
|
||||
}
|
||||
|
||||
instance = this
|
||||
}
|
||||
|
||||
fun hasBridge(bridgeClass: Class<out Bridge?>): Boolean {
|
||||
for (bridge in bridges) {
|
||||
if (bridgeClass.isAssignableFrom(bridge.javaClass)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FIXME: Code using this function normally uses this to get the SteamVR driver but
|
||||
// that's because we first save the SteamVR driver bridge and then the feeder in the array.
|
||||
// Not really a great thing to have.
|
||||
@ThreadSafe
|
||||
fun <E : Bridge?> getVRBridge(bridgeClass: Class<E>): E? {
|
||||
for (bridge in bridges) {
|
||||
if (bridgeClass.isAssignableFrom(bridge.javaClass)) {
|
||||
return bridgeClass.cast(bridge)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun addOnTick(runnable: Runnable) {
|
||||
onTick.add(runnable)
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
fun addNewTrackerConsumer(consumer: Consumer<Tracker>) {
|
||||
queueTask {
|
||||
newTrackersConsumers.add(consumer)
|
||||
for (tracker in trackers) {
|
||||
consumer.accept(tracker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
fun trackerUpdated(tracker: Tracker?) {
|
||||
queueTask {
|
||||
humanPoseManager.trackerUpdated(tracker)
|
||||
updateSkeletonModel()
|
||||
refreshTrackersDriftCompensationEnabled()
|
||||
configManager.vrConfig.writeTrackerConfig(tracker)
|
||||
configManager.saveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
fun addSkeletonUpdatedCallback(consumer: Consumer<HumanSkeleton>) {
|
||||
queueTask { humanPoseManager.addSkeletonUpdatedCallback(consumer) }
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
override fun run() {
|
||||
trackersServer.start()
|
||||
while (true) {
|
||||
// final long start = System.currentTimeMillis();
|
||||
fpsTimer.update()
|
||||
do {
|
||||
val task = tasks.poll() ?: break
|
||||
task.run()
|
||||
} while (true)
|
||||
for (task in onTick) {
|
||||
task.run()
|
||||
}
|
||||
for (bridge in bridges) {
|
||||
bridge.dataRead()
|
||||
}
|
||||
for (tracker in trackers) {
|
||||
tracker.tick(fpsTimer.timePerFrame)
|
||||
}
|
||||
humanPoseManager.update()
|
||||
for (bridge in bridges) {
|
||||
bridge.dataWrite()
|
||||
}
|
||||
vrcOSCHandler.update()
|
||||
vMCHandler.update()
|
||||
// final long time = System.currentTimeMillis() - start;
|
||||
try {
|
||||
sleep(1) // 1000Hz
|
||||
} catch (error: InterruptedException) {
|
||||
LogManager.info("VRServer thread interrupted")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
fun queueTask(r: Runnable) {
|
||||
tasks.add(r)
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
private fun trackerAdded(tracker: Tracker) {
|
||||
humanPoseManager.trackerAdded(tracker)
|
||||
updateSkeletonModel()
|
||||
if (tracker.isComputed) {
|
||||
vMCHandler.addComputedTracker(tracker)
|
||||
}
|
||||
refreshTrackersDriftCompensationEnabled()
|
||||
}
|
||||
|
||||
@ThreadSecure
|
||||
fun registerTracker(tracker: Tracker) {
|
||||
configManager.vrConfig.readTrackerConfig(tracker)
|
||||
queueTask {
|
||||
trackers.add(tracker)
|
||||
trackerAdded(tracker)
|
||||
for (tc in newTrackersConsumers) {
|
||||
tc.accept(tracker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
fun updateSkeletonModel() {
|
||||
queueTask {
|
||||
humanPoseManager.updateSkeletonModelFromServer()
|
||||
vrcOSCHandler.setHeadTracker(TrackerUtils.getTrackerForSkeleton(trackers, TrackerPosition.HEAD))
|
||||
if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) {
|
||||
RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetTrackersFull(resetSourceName: String?, bodyParts: List<Int> = ArrayList()) {
|
||||
queueTask { humanPoseManager.resetTrackersFull(resetSourceName, bodyParts) }
|
||||
}
|
||||
|
||||
fun resetTrackersYaw(resetSourceName: String?, bodyParts: List<Int> = TrackerUtils.allBodyPartsButFingers) {
|
||||
queueTask { humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts) }
|
||||
}
|
||||
|
||||
fun resetTrackersMounting(resetSourceName: String?, bodyParts: List<Int>? = null) {
|
||||
queueTask { humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts) }
|
||||
}
|
||||
|
||||
fun clearTrackersMounting(resetSourceName: String?) {
|
||||
queueTask { humanPoseManager.clearTrackersMounting(resetSourceName) }
|
||||
}
|
||||
|
||||
fun getPauseTracking(): Boolean = humanPoseManager.getPauseTracking()
|
||||
|
||||
fun setPauseTracking(pauseTracking: Boolean, sourceName: String?) {
|
||||
queueTask {
|
||||
humanPoseManager.setPauseTracking(pauseTracking, sourceName)
|
||||
// Toggle trackers as they don't toggle when tracking is paused
|
||||
if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) {
|
||||
RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePauseTracking(sourceName: String?) {
|
||||
queueTask {
|
||||
humanPoseManager.togglePauseTracking(sourceName)
|
||||
// Toggle trackers as they don't toggle when tracking is paused
|
||||
if (this.getVRBridge(ISteamVRBridge::class.java)?.updateShareSettingsAutomatically() == true) {
|
||||
RPCSettingsHandler.sendSteamVRUpdatedSettings(protocolAPI, protocolAPI.rpcHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersFull(resetSourceName: String?, delay: Long, bodyParts: List<Int> = ArrayList()) {
|
||||
resetTimer(
|
||||
resetTimerManager,
|
||||
delay,
|
||||
onTick = { progress ->
|
||||
resetHandler.sendStarted(ResetType.Full, bodyParts, progress, delay.toInt())
|
||||
},
|
||||
onComplete = {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersFull(resetSourceName, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Full, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersYaw(resetSourceName: String?, delay: Long, bodyParts: List<Int> = TrackerUtils.allBodyPartsButFingers) {
|
||||
resetTimer(
|
||||
resetTimerManager,
|
||||
delay,
|
||||
onTick = { progress ->
|
||||
resetHandler.sendStarted(ResetType.Yaw, bodyParts, progress, delay.toInt())
|
||||
},
|
||||
onComplete = {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Yaw, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleResetTrackersMounting(resetSourceName: String?, delay: Long, bodyParts: List<Int>? = null) {
|
||||
resetTimer(
|
||||
resetTimerManager,
|
||||
delay,
|
||||
onTick = { progress ->
|
||||
resetHandler.sendStarted(ResetType.Mounting, bodyParts, progress, delay.toInt())
|
||||
},
|
||||
onComplete = {
|
||||
queueTask {
|
||||
humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts)
|
||||
resetHandler.sendFinished(ResetType.Mounting, bodyParts, delay.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleSetPauseTracking(pauseTracking: Boolean, sourceName: String?, delay: Long) {
|
||||
timer.schedule(delay) {
|
||||
queueTask { humanPoseManager.setPauseTracking(pauseTracking, sourceName) }
|
||||
}
|
||||
}
|
||||
|
||||
fun scheduleTogglePauseTracking(sourceName: String?, delay: Long) {
|
||||
timer.schedule(delay) {
|
||||
queueTask { humanPoseManager.togglePauseTracking(sourceName) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setLegTweaksEnabled(value: Boolean) {
|
||||
queueTask { humanPoseManager.setLegTweaksEnabled(value) }
|
||||
}
|
||||
|
||||
fun setSkatingReductionEnabled(value: Boolean) {
|
||||
queueTask { humanPoseManager.setSkatingCorrectionEnabled(value) }
|
||||
}
|
||||
|
||||
fun setFloorClipEnabled(value: Boolean) {
|
||||
queueTask { humanPoseManager.setFloorClipEnabled(value) }
|
||||
}
|
||||
|
||||
val trackersCount: Int
|
||||
get() = trackers.size
|
||||
val allTrackers: List<Tracker>
|
||||
get() = FastList(trackers)
|
||||
|
||||
fun getTrackerById(id: TrackerIdT): Tracker? {
|
||||
for (tracker in trackers) {
|
||||
if (tracker.trackerNum != id.trackerNum) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle synthetic devices
|
||||
if (id.deviceId == null && tracker.device == null) {
|
||||
return tracker
|
||||
}
|
||||
if (tracker.device != null && id.deviceId != null && id.deviceId.id == tracker.device.id) {
|
||||
// This is a physical tracker, and both device id and the
|
||||
// tracker num match
|
||||
return tracker
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun clearTrackersDriftCompensation() {
|
||||
for (t in allTrackers) {
|
||||
if (t.isImu()) {
|
||||
t.resetsHandler.clearDriftCompensation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshTrackersDriftCompensationEnabled() {
|
||||
for (t in allTrackers) {
|
||||
if (t.isImu()) {
|
||||
t.resetsHandler.refreshDriftCompensationEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun trackerStatusChanged(tracker: Tracker, oldStatus: TrackerStatus, newStatus: TrackerStatus) {
|
||||
trackerStatusListeners.forEach { it.onTrackerStatusChanged(tracker, oldStatus, newStatus) }
|
||||
}
|
||||
|
||||
fun addTrackerStatusListener(listener: TrackerStatusListener) {
|
||||
trackerStatusListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeTrackerStatusListener(listener: TrackerStatusListener) {
|
||||
trackerStatusListeners.removeIf { listener == it }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val nextLocalTrackerId = AtomicInteger()
|
||||
lateinit var instance: VRServer
|
||||
private set
|
||||
|
||||
val instanceInitialized: Boolean
|
||||
get() = ::instance.isInitialized
|
||||
|
||||
@JvmStatic
|
||||
fun getNextLocalTrackerId(): Int = nextLocalTrackerId.incrementAndGet()
|
||||
|
||||
@JvmStatic
|
||||
val currentLocalTrackerId: Int
|
||||
get() = nextLocalTrackerId.get()
|
||||
}
|
||||
class VRServer {
|
||||
}
|
||||
|
||||
@@ -1,698 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import dev.slimevr.SLIMEVR_IDENTIFIER
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.autobone.errors.*
|
||||
import dev.slimevr.config.AutoBoneConfig
|
||||
import dev.slimevr.config.SkeletonConfig
|
||||
import dev.slimevr.poseframeformat.PfrIO
|
||||
import dev.slimevr.poseframeformat.PfsIO
|
||||
import dev.slimevr.poseframeformat.PoseFrames
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
|
||||
import dev.slimevr.tracking.trackers.TrackerRole
|
||||
import io.eiren.util.OperatingSystem
|
||||
import io.eiren.util.StringUtils
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Function
|
||||
import kotlin.math.*
|
||||
|
||||
class AutoBone(private val server: VRServer) {
|
||||
// This is filled by loadConfigValues()
|
||||
val offsets = EnumMap<SkeletonConfigOffsets, Float>(
|
||||
SkeletonConfigOffsets::class.java,
|
||||
)
|
||||
val adjustOffsets = FastList(
|
||||
arrayOf(
|
||||
SkeletonConfigOffsets.HEAD,
|
||||
SkeletonConfigOffsets.NECK,
|
||||
SkeletonConfigOffsets.UPPER_CHEST,
|
||||
SkeletonConfigOffsets.CHEST,
|
||||
SkeletonConfigOffsets.WAIST,
|
||||
SkeletonConfigOffsets.HIP,
|
||||
// HIPS_WIDTH now works when using body proportion error! It's not the
|
||||
// best still, but it is somewhat functional
|
||||
SkeletonConfigOffsets.HIPS_WIDTH,
|
||||
SkeletonConfigOffsets.UPPER_LEG,
|
||||
SkeletonConfigOffsets.LOWER_LEG,
|
||||
),
|
||||
)
|
||||
|
||||
var estimatedHeight: Float = 1f
|
||||
|
||||
// The total height of the normalized adjusted offsets
|
||||
var adjustedHeightNormalized: Float = 1f
|
||||
|
||||
// #region Error functions
|
||||
var slideError = SlideError()
|
||||
var offsetSlideError = OffsetSlideError()
|
||||
var footHeightOffsetError = FootHeightOffsetError()
|
||||
var bodyProportionError = BodyProportionError()
|
||||
var heightError = HeightError()
|
||||
var positionError = PositionError()
|
||||
var positionOffsetError = PositionOffsetError()
|
||||
// #endregion
|
||||
|
||||
val globalConfig: AutoBoneConfig = server.configManager.vrConfig.autoBone
|
||||
val globalSkeletonConfig: SkeletonConfig = server.configManager.vrConfig.skeleton
|
||||
|
||||
init {
|
||||
loadConfigValues()
|
||||
}
|
||||
|
||||
private fun loadConfigValues() {
|
||||
// Remove all previous values
|
||||
offsets.clear()
|
||||
|
||||
// Get current or default skeleton configs
|
||||
val skeleton = server.humanPoseManager
|
||||
// Still compensate for a null skeleton, as it may not be initialized yet
|
||||
val getOffset: Function<SkeletonConfigOffsets, Float> =
|
||||
if (skeleton != null) {
|
||||
Function { key: SkeletonConfigOffsets -> skeleton.getOffset(key) }
|
||||
} else {
|
||||
val defaultConfig = SkeletonConfigManager(false)
|
||||
Function { config: SkeletonConfigOffsets ->
|
||||
defaultConfig.getOffset(config)
|
||||
}
|
||||
}
|
||||
for (bone in adjustOffsets) {
|
||||
val offset = getOffset.apply(bone)
|
||||
if (offset > 0f) {
|
||||
offsets[bone] = offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyConfig(
|
||||
humanPoseManager: HumanPoseManager,
|
||||
offsets: Map<SkeletonConfigOffsets, Float> = this.offsets,
|
||||
) {
|
||||
for ((offset, value) in offsets) {
|
||||
humanPoseManager.setOffset(offset, value)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun applyAndSaveConfig(humanPoseManager: HumanPoseManager? = this.server.humanPoseManager): Boolean {
|
||||
if (humanPoseManager == null) return false
|
||||
applyConfig(humanPoseManager)
|
||||
humanPoseManager.saveConfig()
|
||||
server.configManager.saveConfig()
|
||||
LogManager.info("[AutoBone] Configured skeleton bone lengths")
|
||||
return true
|
||||
}
|
||||
|
||||
fun calcTargetHmdHeight(
|
||||
frames: PoseFrames,
|
||||
config: AutoBoneConfig = globalConfig,
|
||||
): Float {
|
||||
val targetHeight: Float
|
||||
// Get the current skeleton from the server
|
||||
val humanPoseManager = server.humanPoseManager
|
||||
// Still compensate for a null skeleton, as it may not be initialized yet
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (config.useSkeletonHeight && humanPoseManager != null) {
|
||||
// If there is a skeleton available, calculate the target height
|
||||
// from its configs
|
||||
targetHeight = humanPoseManager.userHeightFromConfig
|
||||
LogManager
|
||||
.warning(
|
||||
"[AutoBone] Target height loaded from skeleton (Make sure you reset before running!): $targetHeight",
|
||||
)
|
||||
} else {
|
||||
// Otherwise if there is no skeleton available, attempt to get the
|
||||
// max HMD height from the recording
|
||||
val hmdHeight = frames.maxHmdHeight
|
||||
if (hmdHeight <= MIN_HEIGHT) {
|
||||
LogManager
|
||||
.warning(
|
||||
"[AutoBone] Max headset height detected (Value seems too low, did you not stand up straight while measuring?): $hmdHeight",
|
||||
)
|
||||
} else {
|
||||
LogManager.info("[AutoBone] Max headset height detected: $hmdHeight")
|
||||
}
|
||||
|
||||
// Estimate target height from HMD height
|
||||
targetHeight = hmdHeight
|
||||
}
|
||||
return targetHeight
|
||||
}
|
||||
|
||||
private fun updateRecordingScale(step: PoseFrameStep<AutoBoneStep>, scale: Float) {
|
||||
step.framePlayer1.setScales(scale)
|
||||
step.framePlayer2.setScales(scale)
|
||||
step.skeleton1.update()
|
||||
step.skeleton2.update()
|
||||
}
|
||||
|
||||
fun filterFrames(frames: PoseFrames, step: PoseFrameStep<AutoBoneStep>) {
|
||||
// Calculate the initial frame errors and recording stats
|
||||
val frameErrors = FloatArray(frames.maxFrameCount)
|
||||
val frameStats = StatsCalculator()
|
||||
val recordingStats = StatsCalculator()
|
||||
for (i in 0 until frames.maxFrameCount) {
|
||||
frameStats.reset()
|
||||
for (j in 0 until frames.maxFrameCount) {
|
||||
if (i == j) continue
|
||||
|
||||
step.setCursors(
|
||||
i,
|
||||
j,
|
||||
updatePlayerCursors = true,
|
||||
)
|
||||
|
||||
frameStats.addValue(getErrorDeriv(step))
|
||||
}
|
||||
frameErrors[i] = frameStats.mean
|
||||
recordingStats.addValue(frameStats.mean)
|
||||
// LogManager.info("[AutoBone] Frame: ${i + 1}, Mean error: ${frameStats.mean} (SD ${frameStats.standardDeviation})")
|
||||
}
|
||||
LogManager.info("[AutoBone] Full recording mean error: ${frameStats.mean} (SD ${frameStats.standardDeviation})")
|
||||
|
||||
// Remove outlier frames
|
||||
val sdMult = 1.4f
|
||||
val mean = recordingStats.mean
|
||||
val sd = recordingStats.standardDeviation * sdMult
|
||||
for (i in frameErrors.size - 1 downTo 0) {
|
||||
val err = frameErrors[i]
|
||||
if (err < mean - sd || err > mean + sd) {
|
||||
for (frameHolder in frames.frameHolders) {
|
||||
frameHolder.frames.removeAt(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
step.maxFrameCount = frames.maxFrameCount
|
||||
|
||||
// Calculate and print the resulting recording stats
|
||||
recordingStats.reset()
|
||||
for (i in 0 until frames.maxFrameCount) {
|
||||
frameStats.reset()
|
||||
for (j in 0 until frames.maxFrameCount) {
|
||||
if (i == j) continue
|
||||
|
||||
step.setCursors(
|
||||
i,
|
||||
j,
|
||||
updatePlayerCursors = true,
|
||||
)
|
||||
|
||||
frameStats.addValue(getErrorDeriv(step))
|
||||
}
|
||||
recordingStats.addValue(frameStats.mean)
|
||||
}
|
||||
LogManager.info("[AutoBone] Full recording after mean error: ${frameStats.mean} (SD ${frameStats.standardDeviation})")
|
||||
}
|
||||
|
||||
@Throws(AutoBoneException::class)
|
||||
fun processFrames(
|
||||
frames: PoseFrames,
|
||||
config: AutoBoneConfig = globalConfig,
|
||||
skeletonConfig: SkeletonConfig = globalSkeletonConfig,
|
||||
epochCallback: Consumer<Epoch>? = null,
|
||||
): AutoBoneResults {
|
||||
check(frames.frameHolders.isNotEmpty()) { "Recording has no trackers." }
|
||||
check(frames.maxFrameCount > 0) { "Recording has no frames." }
|
||||
|
||||
// Load current values for adjustable configs
|
||||
loadConfigValues()
|
||||
|
||||
// Set the target heights either from config or calculate them
|
||||
val targetHmdHeight = if (skeletonConfig.userHeight > MIN_HEIGHT) {
|
||||
skeletonConfig.userHeight
|
||||
} else {
|
||||
calcTargetHmdHeight(frames, config)
|
||||
}
|
||||
check(targetHmdHeight > MIN_HEIGHT) { "Configured height ($targetHmdHeight) is too small (<= $MIN_HEIGHT)." }
|
||||
|
||||
// Set up the current state, making all required players and setting up the
|
||||
// skeletons appropriately
|
||||
val step = PoseFrameStep<AutoBoneStep>(
|
||||
config = config,
|
||||
serverConfig = server.configManager,
|
||||
frames = frames,
|
||||
preEpoch = { step ->
|
||||
// Set the current adjust rate based on the current epoch
|
||||
step.data.adjustRate = decayFunc(step.config.initialAdjustRate, step.config.adjustRateDecay, step.epoch)
|
||||
},
|
||||
onStep = this::step,
|
||||
postEpoch = { step -> epoch(step, epochCallback) },
|
||||
randomSeed = config.randSeed,
|
||||
data = AutoBoneStep(
|
||||
targetHmdHeight = targetHmdHeight,
|
||||
adjustRate = 1f,
|
||||
),
|
||||
)
|
||||
|
||||
// Normalize the skeletons and get the normalized height for adjusted offsets
|
||||
scaleSkeleton(step.skeleton1)
|
||||
scaleSkeleton(step.skeleton2)
|
||||
adjustedHeightNormalized = sumAdjustedHeightOffsets(step.skeleton1)
|
||||
|
||||
// Normalize offsets based on the initial normalized skeleton
|
||||
scaleOffsets()
|
||||
|
||||
// Apply the initial normalized config values
|
||||
applyConfig(step.skeleton1)
|
||||
applyConfig(step.skeleton2)
|
||||
|
||||
// Initialize normalization to the set target height (also updates skeleton)
|
||||
estimatedHeight = targetHmdHeight
|
||||
updateRecordingScale(step, 1f / targetHmdHeight)
|
||||
|
||||
if (config.useFrameFiltering) {
|
||||
filterFrames(frames, step)
|
||||
}
|
||||
|
||||
// Iterate frames now that it's set up
|
||||
PoseFrameIterator.iterateFrames(step)
|
||||
|
||||
// Scale the normalized offsets to the estimated height for the final result
|
||||
for (entry in offsets.entries) {
|
||||
entry.setValue(entry.value * estimatedHeight)
|
||||
}
|
||||
|
||||
LogManager
|
||||
.info(
|
||||
"[AutoBone] Target height: ${step.data.targetHmdHeight}, Final height: $estimatedHeight",
|
||||
)
|
||||
if (step.data.errorStats.mean > config.maxFinalError) {
|
||||
throw AutoBoneException("The final epoch error value (${step.data.errorStats.mean}) has exceeded the maximum allowed value (${config.maxFinalError}).")
|
||||
}
|
||||
|
||||
return AutoBoneResults(
|
||||
estimatedHeight,
|
||||
step.data.targetHmdHeight,
|
||||
offsets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun epoch(
|
||||
step: PoseFrameStep<AutoBoneStep>,
|
||||
epochCallback: Consumer<Epoch>? = null,
|
||||
) {
|
||||
val config = step.config
|
||||
val epoch = step.epoch
|
||||
|
||||
// Calculate average error over the epoch
|
||||
if (epoch <= 0 || epoch >= config.numEpochs - 1 || (epoch + 1) % config.printEveryNumEpochs == 0) {
|
||||
LogManager
|
||||
.info(
|
||||
"[AutoBone] Epoch: ${epoch + 1}, Mean error: ${step.data.errorStats.mean} (SD ${step.data.errorStats.standardDeviation}), Adjust rate: ${step.data.adjustRate}",
|
||||
)
|
||||
LogManager
|
||||
.info(
|
||||
"[AutoBone] Target height: ${step.data.targetHmdHeight}, Estimated height: $estimatedHeight",
|
||||
)
|
||||
}
|
||||
|
||||
if (epochCallback != null) {
|
||||
// Scale the normalized offsets to the estimated height for the callback
|
||||
val scaledOffsets = EnumMap(offsets)
|
||||
for (entry in scaledOffsets.entries) {
|
||||
entry.setValue(entry.value * estimatedHeight)
|
||||
}
|
||||
epochCallback.accept(Epoch(epoch + 1, config.numEpochs, step.data.errorStats, scaledOffsets))
|
||||
}
|
||||
}
|
||||
|
||||
private fun step(step: PoseFrameStep<AutoBoneStep>) {
|
||||
// Pull frequently used variables out of trainingStep to reduce call length
|
||||
val skeleton1 = step.skeleton1
|
||||
val skeleton2 = step.skeleton2
|
||||
|
||||
// Scaling each step used to mean enforcing the target height, so keep that
|
||||
// behaviour to retain predictability
|
||||
if (!step.config.scaleEachStep) {
|
||||
// Try to estimate a new height by calculating the height with the lowest
|
||||
// error between adding or subtracting from the height
|
||||
val maxHeight = step.data.targetHmdHeight + 0.2f
|
||||
val minHeight = step.data.targetHmdHeight - 0.2f
|
||||
|
||||
step.data.hmdHeight = estimatedHeight
|
||||
val heightErrorDeriv = getErrorDeriv(step)
|
||||
val heightAdjust = errorFunc(heightErrorDeriv) * step.data.adjustRate
|
||||
|
||||
val negHeight = (estimatedHeight - heightAdjust).coerceIn(minHeight, maxHeight)
|
||||
updateRecordingScale(step, 1f / negHeight)
|
||||
step.data.hmdHeight = negHeight
|
||||
val negHeightErrorDeriv = getErrorDeriv(step)
|
||||
|
||||
val posHeight = (estimatedHeight + heightAdjust).coerceIn(minHeight, maxHeight)
|
||||
updateRecordingScale(step, 1f / posHeight)
|
||||
step.data.hmdHeight = posHeight
|
||||
val posHeightErrorDeriv = getErrorDeriv(step)
|
||||
|
||||
if (negHeightErrorDeriv < heightErrorDeriv && negHeightErrorDeriv < posHeightErrorDeriv) {
|
||||
estimatedHeight = negHeight
|
||||
// Apply the negative height scale
|
||||
updateRecordingScale(step, 1f / negHeight)
|
||||
} else if (posHeightErrorDeriv < heightErrorDeriv) {
|
||||
estimatedHeight = posHeight
|
||||
// The last estimated height set was the positive adjustment, so no need to apply it again
|
||||
} else {
|
||||
// Reset to the initial scale
|
||||
updateRecordingScale(step, 1f / estimatedHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the heights used for error calculations
|
||||
step.data.hmdHeight = estimatedHeight
|
||||
|
||||
val errorDeriv = getErrorDeriv(step)
|
||||
val error = errorFunc(errorDeriv)
|
||||
|
||||
// In case of fire
|
||||
if (java.lang.Float.isNaN(error) || java.lang.Float.isInfinite(error)) {
|
||||
// Extinguish
|
||||
LogManager
|
||||
.warning(
|
||||
"[AutoBone] Error value is invalid, resetting variables to recover",
|
||||
)
|
||||
// Reset adjustable config values
|
||||
loadConfigValues()
|
||||
|
||||
// Reset error sum values
|
||||
step.data.errorStats.reset()
|
||||
|
||||
// Continue on new data
|
||||
return
|
||||
}
|
||||
|
||||
// Store the error count for logging purposes
|
||||
step.data.errorStats.addValue(errorDeriv)
|
||||
val adjustVal = error * step.data.adjustRate
|
||||
|
||||
// If there is no adjustment whatsoever, skip this
|
||||
if (adjustVal == 0f) {
|
||||
return
|
||||
}
|
||||
|
||||
val slideL = skeleton2.getComputedTracker(TrackerRole.LEFT_FOOT).position -
|
||||
skeleton1.getComputedTracker(TrackerRole.LEFT_FOOT).position
|
||||
val slideLLen = slideL.len()
|
||||
val slideLUnit: Vector3? = if (slideLLen > MIN_SLIDE_DIST) slideL / slideLLen else null
|
||||
|
||||
val slideR = skeleton2.getComputedTracker(TrackerRole.RIGHT_FOOT).position -
|
||||
skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position
|
||||
val slideRLen = slideR.len()
|
||||
val slideRUnit: Vector3? = if (slideRLen > MIN_SLIDE_DIST) slideR / slideRLen else null
|
||||
|
||||
val intermediateOffsets = EnumMap(offsets)
|
||||
for (entry in intermediateOffsets.entries) {
|
||||
// Skip adjustment if the epoch is before starting (for logging only) or
|
||||
// if there are no BoneTypes for this value
|
||||
if (step.epoch < 0 || entry.key.affectedOffsets.isEmpty()) {
|
||||
break
|
||||
}
|
||||
val originalLength = entry.value
|
||||
|
||||
// Calculate the total effect of the bone based on change in rotation
|
||||
val slideDot = BoneContribution.getSlideDot(
|
||||
skeleton1,
|
||||
skeleton2,
|
||||
entry.key,
|
||||
slideLUnit,
|
||||
slideRUnit,
|
||||
)
|
||||
val dotLength = originalLength * slideDot
|
||||
|
||||
// Scale by the total effect of the bone
|
||||
val curAdjustVal = adjustVal * -dotLength
|
||||
if (curAdjustVal == 0f) {
|
||||
continue
|
||||
}
|
||||
|
||||
val newLength = originalLength + curAdjustVal
|
||||
// No small or negative numbers!!! Bad algorithm!
|
||||
if (newLength < 0.01f) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply new offset length
|
||||
skeleton1.setOffset(entry.key, newLength)
|
||||
skeleton2.setOffset(entry.key, newLength)
|
||||
scaleSkeleton(skeleton1, onlyAdjustedHeight = true)
|
||||
scaleSkeleton(skeleton2, onlyAdjustedHeight = true)
|
||||
|
||||
// Update the skeleton poses for the new offset length
|
||||
skeleton1.update()
|
||||
skeleton2.update()
|
||||
|
||||
val newErrorDeriv = getErrorDeriv(step)
|
||||
if (newErrorDeriv < errorDeriv) {
|
||||
// Apply the adjusted length to the current adjusted offsets
|
||||
entry.setValue(newLength)
|
||||
}
|
||||
|
||||
// Reset the skeleton values to minimize bias in other variables, it's applied later
|
||||
applyConfig(skeleton1)
|
||||
applyConfig(skeleton2)
|
||||
}
|
||||
|
||||
// Update the offsets from the adjusted ones
|
||||
offsets.putAll(intermediateOffsets)
|
||||
|
||||
// Normalize the scale, it will be upscaled to the target height later
|
||||
// We only need to scale height offsets, as other offsets are not affected by height
|
||||
scaleOffsets(onlyHeightOffsets = true)
|
||||
|
||||
// Apply the normalized offsets to the skeleton
|
||||
applyConfig(skeleton1)
|
||||
applyConfig(skeleton2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums only the adjusted height offsets of the provided HumanPoseManager
|
||||
*/
|
||||
private fun sumAdjustedHeightOffsets(humanPoseManager: HumanPoseManager): Float {
|
||||
var sum = 0f
|
||||
SkeletonConfigManager.HEIGHT_OFFSETS.forEach {
|
||||
if (!adjustOffsets.contains(it)) return@forEach
|
||||
sum += humanPoseManager.getOffset(it)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums only the height offsets of the provided offset map
|
||||
*/
|
||||
private fun sumHeightOffsets(offsets: EnumMap<SkeletonConfigOffsets, Float> = this.offsets): Float {
|
||||
var sum = 0f
|
||||
SkeletonConfigManager.HEIGHT_OFFSETS.forEach {
|
||||
sum += offsets[it] ?: return@forEach
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
private fun scaleSkeleton(humanPoseManager: HumanPoseManager, targetHeight: Float = 1f, onlyAdjustedHeight: Boolean = false) {
|
||||
// Get the scale to apply for the appropriate offsets
|
||||
val scale = if (onlyAdjustedHeight) {
|
||||
// Only adjusted height offsets
|
||||
val adjHeight = sumAdjustedHeightOffsets(humanPoseManager)
|
||||
// Remove the constant from the target, leaving only the target for adjusted height offsets
|
||||
val adjTarget = targetHeight - (humanPoseManager.userHeightFromConfig - adjHeight)
|
||||
// Return only the scale for adjusted offsets
|
||||
adjTarget / adjHeight
|
||||
} else {
|
||||
targetHeight / humanPoseManager.userHeightFromConfig
|
||||
}
|
||||
|
||||
val offsets = if (onlyAdjustedHeight) SkeletonConfigManager.HEIGHT_OFFSETS else SkeletonConfigOffsets.values
|
||||
for (offset in offsets) {
|
||||
if (onlyAdjustedHeight && !adjustOffsets.contains(offset)) continue
|
||||
humanPoseManager.setOffset(offset, humanPoseManager.getOffset(offset) * scale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scaleOffsets(offsets: EnumMap<SkeletonConfigOffsets, Float> = this.offsets, targetHeight: Float = adjustedHeightNormalized, onlyHeightOffsets: Boolean = false) {
|
||||
// Get the scale to apply for the appropriate offsets
|
||||
val scale = targetHeight / sumHeightOffsets(offsets)
|
||||
|
||||
for (entry in offsets.entries) {
|
||||
if (onlyHeightOffsets && !SkeletonConfigManager.HEIGHT_OFFSETS.contains(entry.key)) continue
|
||||
entry.setValue(entry.value * scale)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(AutoBoneException::class)
|
||||
private fun getErrorDeriv(step: PoseFrameStep<AutoBoneStep>): Float {
|
||||
val config = step.config
|
||||
var sumError = 0f
|
||||
if (config.slideErrorFactor > 0f) {
|
||||
sumError += slideError.getStepError(step) * config.slideErrorFactor
|
||||
}
|
||||
if (config.offsetSlideErrorFactor > 0f) {
|
||||
sumError += (
|
||||
offsetSlideError.getStepError(step) *
|
||||
config.offsetSlideErrorFactor
|
||||
)
|
||||
}
|
||||
if (config.footHeightOffsetErrorFactor > 0f) {
|
||||
sumError += (
|
||||
footHeightOffsetError.getStepError(step) *
|
||||
config.footHeightOffsetErrorFactor
|
||||
)
|
||||
}
|
||||
if (config.bodyProportionErrorFactor > 0f) {
|
||||
sumError += (
|
||||
bodyProportionError.getStepError(step) *
|
||||
config.bodyProportionErrorFactor
|
||||
)
|
||||
}
|
||||
if (config.heightErrorFactor > 0f) {
|
||||
sumError += heightError.getStepError(step) * config.heightErrorFactor
|
||||
}
|
||||
if (config.positionErrorFactor > 0f) {
|
||||
sumError += (
|
||||
positionError.getStepError(step) *
|
||||
config.positionErrorFactor
|
||||
)
|
||||
}
|
||||
if (config.positionOffsetErrorFactor > 0f) {
|
||||
sumError += (
|
||||
positionOffsetError.getStepError(step) *
|
||||
config.positionOffsetErrorFactor
|
||||
)
|
||||
}
|
||||
return sumError
|
||||
}
|
||||
|
||||
val lengthsString: String
|
||||
get() {
|
||||
val configInfo = StringBuilder()
|
||||
offsets.forEach { (key, value) ->
|
||||
if (configInfo.isNotEmpty()) {
|
||||
configInfo.append(", ")
|
||||
}
|
||||
configInfo
|
||||
.append(key.configKey)
|
||||
.append(": ")
|
||||
.append(StringUtils.prettyNumber(value * 100f, 2))
|
||||
}
|
||||
return configInfo.toString()
|
||||
}
|
||||
|
||||
fun saveRecording(frames: PoseFrames, recordingFile: File) {
|
||||
if (saveDir.isDirectory || saveDir.mkdirs()) {
|
||||
LogManager
|
||||
.info("[AutoBone] Exporting frames to \"${recordingFile.path}\"...")
|
||||
if (PfsIO.tryWriteToFile(recordingFile, frames)) {
|
||||
LogManager
|
||||
.info(
|
||||
"[AutoBone] Done exporting! Recording can be found at \"${recordingFile.path}\".",
|
||||
)
|
||||
} else {
|
||||
LogManager
|
||||
.severe(
|
||||
"[AutoBone] Failed to export the recording to \"${recordingFile.path}\".",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LogManager
|
||||
.severe(
|
||||
"[AutoBone] Failed to create the recording directory \"${saveDir.path}\".",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveRecording(frames: PoseFrames, recordingFileName: String) {
|
||||
saveRecording(frames, File(saveDir, recordingFileName))
|
||||
}
|
||||
|
||||
fun saveRecording(frames: PoseFrames) {
|
||||
var recordingFile: File
|
||||
var recordingIndex = 1
|
||||
do {
|
||||
recordingFile = File(saveDir, "ABRecording${recordingIndex++}.pfs")
|
||||
} while (recordingFile.exists())
|
||||
saveRecording(frames, recordingFile)
|
||||
}
|
||||
|
||||
fun loadRecordings(): FastList<Pair<String, PoseFrames>> {
|
||||
val recordings = FastList<Pair<String, PoseFrames>>()
|
||||
|
||||
loadDir.listFiles()?.forEach { file ->
|
||||
if (!file.isFile) return@forEach
|
||||
|
||||
val frames = if (file.name.endsWith(".pfs", ignoreCase = true)) {
|
||||
LogManager.info("[AutoBone] Loading PFS recording from \"${file.path}\"...")
|
||||
PfsIO.tryReadFromFile(file)
|
||||
} else if (file.name.endsWith(".pfr", ignoreCase = true)) {
|
||||
LogManager.info("[AutoBone] Loading PFR recording from \"${file.path}\"...")
|
||||
PfrIO.tryReadFromFile(file)
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (frames == null) {
|
||||
LogManager.severe("[AutoBone] Failed to load recording from \"${file.path}\".")
|
||||
} else {
|
||||
recordings.add(Pair.of(file.name, frames))
|
||||
LogManager.info("[AutoBone] Loaded recording from \"${file.path}\".")
|
||||
}
|
||||
}
|
||||
|
||||
return recordings
|
||||
}
|
||||
|
||||
inner class Epoch(
|
||||
val epoch: Int,
|
||||
val totalEpochs: Int,
|
||||
val epochError: StatsCalculator,
|
||||
val configValues: EnumMap<SkeletonConfigOffsets, Float>,
|
||||
) {
|
||||
override fun toString(): String = "Epoch: $epoch, Epoch error: $epochError"
|
||||
}
|
||||
|
||||
inner class AutoBoneResults(
|
||||
val finalHeight: Float,
|
||||
val targetHeight: Float,
|
||||
val configValues: EnumMap<SkeletonConfigOffsets, Float>,
|
||||
) {
|
||||
val heightDifference: Float
|
||||
get() = abs(targetHeight - finalHeight)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_HEIGHT = 0.4f
|
||||
const val MIN_SLIDE_DIST = 0.002f
|
||||
const val AUTOBONE_FOLDER = "AutoBone Recordings"
|
||||
const val LOADAUTOBONE_FOLDER = "Load AutoBone Recordings"
|
||||
|
||||
// FIXME: Won't work on iOS and Android, maybe fix resolveConfigDirectory more than this
|
||||
val saveDir = File(
|
||||
OperatingSystem.resolveConfigDirectory(SLIMEVR_IDENTIFIER)?.resolve(
|
||||
AUTOBONE_FOLDER,
|
||||
)?.toString() ?: AUTOBONE_FOLDER,
|
||||
)
|
||||
val loadDir = File(
|
||||
OperatingSystem.resolveConfigDirectory(SLIMEVR_IDENTIFIER)?.resolve(
|
||||
LOADAUTOBONE_FOLDER,
|
||||
)?.toString() ?: LOADAUTOBONE_FOLDER,
|
||||
)
|
||||
|
||||
// Mean square error function
|
||||
private fun errorFunc(errorDeriv: Float): Float = 0.5f * (errorDeriv * errorDeriv)
|
||||
|
||||
private fun decayFunc(initialAdjustRate: Float, adjustRateDecay: Float, epoch: Int): Float = if (epoch >= 0) initialAdjustRate / (1 + (adjustRateDecay * epoch)) else 0.0f
|
||||
|
||||
val SYMM_CONFIGS = arrayOf(
|
||||
SkeletonConfigOffsets.HIPS_WIDTH,
|
||||
SkeletonConfigOffsets.SHOULDERS_WIDTH,
|
||||
SkeletonConfigOffsets.SHOULDERS_DISTANCE,
|
||||
SkeletonConfigOffsets.UPPER_ARM,
|
||||
SkeletonConfigOffsets.LOWER_ARM,
|
||||
SkeletonConfigOffsets.UPPER_LEG,
|
||||
SkeletonConfigOffsets.LOWER_LEG,
|
||||
SkeletonConfigOffsets.FOOT_LENGTH,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.autobone.AutoBone.AutoBoneResults
|
||||
import dev.slimevr.autobone.AutoBone.Companion.loadDir
|
||||
import dev.slimevr.autobone.errors.AutoBoneException
|
||||
import dev.slimevr.poseframeformat.PoseFrames
|
||||
import dev.slimevr.poseframeformat.PoseRecorder
|
||||
import dev.slimevr.poseframeformat.PoseRecorder.RecordingProgress
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrameData
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
|
||||
import io.eiren.util.StringUtils
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class AutoBoneHandler(private val server: VRServer) {
|
||||
private val poseRecorder: PoseRecorder = PoseRecorder(server)
|
||||
private val autoBone: AutoBone = AutoBone(server)
|
||||
|
||||
private val recordingLock = ReentrantLock()
|
||||
private var recordingThread: Thread? = null
|
||||
private val saveRecordingLock = ReentrantLock()
|
||||
private var saveRecordingThread: Thread? = null
|
||||
private val autoBoneLock = ReentrantLock()
|
||||
private var autoBoneThread: Thread? = null
|
||||
|
||||
private val listeners = CopyOnWriteArrayList<AutoBoneListener>()
|
||||
|
||||
fun addListener(listener: AutoBoneListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: AutoBoneListener) {
|
||||
listeners.removeIf { listener == it }
|
||||
}
|
||||
|
||||
private fun announceProcessStatus(
|
||||
processType: AutoBoneProcessType,
|
||||
message: String? = null,
|
||||
current: Long = -1L,
|
||||
total: Long = -1L,
|
||||
eta: Float = -1f,
|
||||
completed: Boolean = false,
|
||||
success: Boolean = true,
|
||||
) {
|
||||
listeners.forEach {
|
||||
it.onAutoBoneProcessStatus(
|
||||
processType,
|
||||
message,
|
||||
current,
|
||||
total,
|
||||
eta,
|
||||
completed,
|
||||
success,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(AutoBoneException::class)
|
||||
private fun processFrames(frames: PoseFrames): AutoBoneResults = autoBone
|
||||
.processFrames(frames) { epoch ->
|
||||
listeners.forEach { listener -> listener.onAutoBoneEpoch(epoch) }
|
||||
}
|
||||
|
||||
fun startProcessByType(processType: AutoBoneProcessType?): Boolean {
|
||||
when (processType) {
|
||||
AutoBoneProcessType.RECORD -> startRecording()
|
||||
|
||||
AutoBoneProcessType.SAVE -> saveRecording()
|
||||
|
||||
AutoBoneProcessType.PROCESS -> processRecording()
|
||||
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun startRecording() {
|
||||
recordingLock.withLock {
|
||||
// Prevent running multiple times
|
||||
if (recordingThread != null) {
|
||||
return
|
||||
}
|
||||
recordingThread = thread(start = true) { startRecordingThread() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRecordingThread() {
|
||||
try {
|
||||
if (poseRecorder.isReadyToRecord) {
|
||||
announceProcessStatus(AutoBoneProcessType.RECORD, "Recording...")
|
||||
|
||||
// ex. 1000 samples at 20 ms per sample is 20 seconds
|
||||
val sampleCount = autoBone.globalConfig.sampleCount
|
||||
val sampleRate = autoBone.globalConfig.sampleRateMs / 1000f
|
||||
// Calculate total time in seconds
|
||||
val totalTime: Float = sampleCount * sampleRate
|
||||
|
||||
val framesFuture = poseRecorder
|
||||
.startFrameRecording(
|
||||
sampleCount,
|
||||
sampleRate,
|
||||
) { progress: RecordingProgress ->
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.RECORD,
|
||||
current = progress.frame.toLong(),
|
||||
total = progress.totalFrames.toLong(),
|
||||
eta = totalTime - (progress.frame * totalTime / progress.totalFrames),
|
||||
)
|
||||
}
|
||||
val frames = framesFuture.get()
|
||||
LogManager.info("[AutoBone] Done recording!")
|
||||
|
||||
// Save a recurring recording for users to send as debug info
|
||||
announceProcessStatus(AutoBoneProcessType.RECORD, "Saving recording...")
|
||||
autoBone.saveRecording(frames, "LastABRecording.pfs")
|
||||
if (autoBone.globalConfig.saveRecordings) {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.RECORD,
|
||||
"Saving recording (from config option)...",
|
||||
)
|
||||
autoBone.saveRecording(frames)
|
||||
}
|
||||
listeners.forEach { listener: AutoBoneListener -> listener.onAutoBoneRecordingEnd(frames) }
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.RECORD,
|
||||
"Done recording!",
|
||||
completed = true,
|
||||
success = true,
|
||||
)
|
||||
} else {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.RECORD,
|
||||
"The server is not ready to record",
|
||||
completed = true,
|
||||
success = false,
|
||||
)
|
||||
LogManager.severe("[AutoBone] Unable to record...")
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.RECORD,
|
||||
"Recording failed: ${e.message}",
|
||||
completed = true,
|
||||
success = false,
|
||||
)
|
||||
LogManager.severe("[AutoBone] Failed recording!", e)
|
||||
} finally {
|
||||
recordingThread = null
|
||||
}
|
||||
}
|
||||
|
||||
fun stopRecording() {
|
||||
if (poseRecorder.isRecording) {
|
||||
poseRecorder.stopFrameRecording()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelRecording() {
|
||||
if (poseRecorder.isRecording) {
|
||||
poseRecorder.cancelFrameRecording()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveRecording() {
|
||||
saveRecordingLock.withLock {
|
||||
// Prevent running multiple times
|
||||
if (saveRecordingThread != null) {
|
||||
return
|
||||
}
|
||||
saveRecordingThread = thread(start = true) { saveRecordingThread() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveRecordingThread() {
|
||||
try {
|
||||
val framesFuture = poseRecorder.framesAsync
|
||||
if (framesFuture != null) {
|
||||
announceProcessStatus(AutoBoneProcessType.SAVE, "Waiting for recording...")
|
||||
val frames = framesFuture.get()
|
||||
check(frames.frameHolders.isNotEmpty()) { "Recording has no trackers." }
|
||||
check(frames.maxFrameCount > 0) { "Recording has no frames." }
|
||||
announceProcessStatus(AutoBoneProcessType.SAVE, "Saving recording...")
|
||||
autoBone.saveRecording(frames)
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.SAVE,
|
||||
"Recording saved!",
|
||||
completed = true,
|
||||
success = true,
|
||||
)
|
||||
} else {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.SAVE,
|
||||
"No recording found",
|
||||
completed = true,
|
||||
success = false,
|
||||
)
|
||||
LogManager.severe("[AutoBone] Unable to save, no recording was done...")
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.SAVE,
|
||||
"Failed to save recording: ${e.message}",
|
||||
completed = true,
|
||||
success = false,
|
||||
)
|
||||
LogManager.severe("[AutoBone] Failed to save recording!", e)
|
||||
} finally {
|
||||
saveRecordingThread = null
|
||||
}
|
||||
}
|
||||
|
||||
fun processRecording() {
|
||||
autoBoneLock.withLock {
|
||||
// Prevent running multiple times
|
||||
if (autoBoneThread != null) {
|
||||
return
|
||||
}
|
||||
autoBoneThread = thread(start = true) { processRecordingThread() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRecordingThread() {
|
||||
try {
|
||||
announceProcessStatus(AutoBoneProcessType.PROCESS, "Loading recordings...")
|
||||
val frameRecordings = autoBone.loadRecordings()
|
||||
if (!frameRecordings.isEmpty()) {
|
||||
LogManager.info("[AutoBone] Done loading frames!")
|
||||
} else {
|
||||
val framesFuture = poseRecorder.framesAsync
|
||||
if (framesFuture != null) {
|
||||
announceProcessStatus(AutoBoneProcessType.PROCESS, "Waiting for recording...")
|
||||
val frames = framesFuture.get()
|
||||
frameRecordings.add(Pair.of("<Recording>", frames))
|
||||
} else {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.PROCESS,
|
||||
"No recordings found...",
|
||||
completed = true,
|
||||
success = false,
|
||||
)
|
||||
LogManager
|
||||
.severe(
|
||||
"[AutoBone] No recordings found in \"${loadDir.path}\" and no recording was done...",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
announceProcessStatus(AutoBoneProcessType.PROCESS, "Processing recording(s)...")
|
||||
LogManager.info("[AutoBone] Processing frames...")
|
||||
val errorStats = StatsCalculator()
|
||||
val offsetStats = EnumMap<SkeletonConfigOffsets, StatsCalculator>(
|
||||
SkeletonConfigOffsets::class.java,
|
||||
)
|
||||
val skeletonConfigManagerBuffer = SkeletonConfigManager(false)
|
||||
for ((key, value) in frameRecordings) {
|
||||
LogManager.info("[AutoBone] Processing frames from \"$key\"...")
|
||||
// Output tracker info for the recording
|
||||
printTrackerInfo(value.frameHolders)
|
||||
|
||||
// Actually process the recording
|
||||
val autoBoneResults = processFrames(value)
|
||||
LogManager.info("[AutoBone] Done processing!")
|
||||
|
||||
// #region Stats/Values
|
||||
// Accumulate height error
|
||||
errorStats.addValue(autoBoneResults.heightDifference)
|
||||
|
||||
// Accumulate length values
|
||||
for (offset in autoBoneResults.configValues) {
|
||||
val statCalc = offsetStats.getOrPut(offset.key) {
|
||||
StatsCalculator()
|
||||
}
|
||||
// Multiply by 100 to get cm
|
||||
statCalc.addValue(offset.value * 100f)
|
||||
}
|
||||
|
||||
// Calculate and output skeleton ratios
|
||||
skeletonConfigManagerBuffer.setOffsets(autoBoneResults.configValues)
|
||||
printSkeletonRatios(skeletonConfigManagerBuffer)
|
||||
|
||||
LogManager.info("[AutoBone] Length values: ${autoBone.lengthsString}")
|
||||
}
|
||||
// Length value stats
|
||||
val averageLengthVals = StringBuilder()
|
||||
offsetStats.forEach { (key, value) ->
|
||||
if (averageLengthVals.isNotEmpty()) {
|
||||
averageLengthVals.append(", ")
|
||||
}
|
||||
averageLengthVals
|
||||
.append(key.configKey)
|
||||
.append(": ")
|
||||
.append(StringUtils.prettyNumber(value.mean, 2))
|
||||
.append(" (SD ")
|
||||
.append(StringUtils.prettyNumber(value.standardDeviation, 2))
|
||||
.append(")")
|
||||
}
|
||||
LogManager.info("[AutoBone] Average length values: $averageLengthVals")
|
||||
|
||||
// Height error stats
|
||||
LogManager
|
||||
.info(
|
||||
"[AutoBone] Average height error: ${
|
||||
StringUtils.prettyNumber(errorStats.mean, 6)
|
||||
} (SD ${StringUtils.prettyNumber(errorStats.standardDeviation, 6)})",
|
||||
)
|
||||
// #endregion
|
||||
listeners.forEach { listener: AutoBoneListener -> listener.onAutoBoneEnd(autoBone.offsets) }
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.PROCESS,
|
||||
"Done processing!",
|
||||
completed = true,
|
||||
success = true,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
announceProcessStatus(
|
||||
AutoBoneProcessType.PROCESS,
|
||||
"Processing failed: ${e.message}",
|
||||
completed = true,
|
||||
success = false,
|
||||
)
|
||||
LogManager.severe("[AutoBone] Failed adjustment!", e)
|
||||
} finally {
|
||||
autoBoneThread = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun printTrackerInfo(trackers: FastList<TrackerFrames>) {
|
||||
val trackerInfo = StringBuilder()
|
||||
for (tracker in trackers) {
|
||||
val frame = tracker?.tryGetFrame(0) ?: continue
|
||||
|
||||
// Add a comma if this is not the first item listed
|
||||
if (trackerInfo.isNotEmpty()) {
|
||||
trackerInfo.append(", ")
|
||||
}
|
||||
|
||||
trackerInfo.append(frame.tryGetTrackerPosition()?.designation ?: "unassigned")
|
||||
|
||||
// Represent the data flags
|
||||
val trackerFlags = StringBuilder()
|
||||
if (frame.hasData(TrackerFrameData.ROTATION)) {
|
||||
trackerFlags.append("R")
|
||||
}
|
||||
if (frame.hasData(TrackerFrameData.POSITION)) {
|
||||
trackerFlags.append("P")
|
||||
}
|
||||
if (frame.hasData(TrackerFrameData.ACCELERATION)) {
|
||||
trackerFlags.append("A")
|
||||
}
|
||||
if (frame.hasData(TrackerFrameData.RAW_ROTATION)) {
|
||||
trackerFlags.append("r")
|
||||
}
|
||||
|
||||
// If there are data flags, print them in brackets after the designation
|
||||
if (trackerFlags.isNotEmpty()) {
|
||||
trackerInfo.append(" (").append(trackerFlags).append(")")
|
||||
}
|
||||
}
|
||||
LogManager.info("[AutoBone] (${trackers.size} trackers) [$trackerInfo]")
|
||||
}
|
||||
|
||||
private fun printSkeletonRatios(skeleton: SkeletonConfigManager) {
|
||||
val neckLength = skeleton.getOffset(SkeletonConfigOffsets.NECK)
|
||||
val upperChestLength = skeleton.getOffset(SkeletonConfigOffsets.UPPER_CHEST)
|
||||
val chestLength = skeleton.getOffset(SkeletonConfigOffsets.CHEST)
|
||||
val waistLength = skeleton.getOffset(SkeletonConfigOffsets.WAIST)
|
||||
val hipLength = skeleton.getOffset(SkeletonConfigOffsets.HIP)
|
||||
val torsoLength = upperChestLength + chestLength + waistLength + hipLength
|
||||
val hipWidth = skeleton.getOffset(SkeletonConfigOffsets.HIPS_WIDTH)
|
||||
val legLength = skeleton.getOffset(SkeletonConfigOffsets.UPPER_LEG) +
|
||||
skeleton.getOffset(SkeletonConfigOffsets.LOWER_LEG)
|
||||
val lowerLegLength = skeleton.getOffset(SkeletonConfigOffsets.LOWER_LEG)
|
||||
|
||||
val neckTorso = neckLength / torsoLength
|
||||
val chestTorso = (upperChestLength + chestLength) / torsoLength
|
||||
val torsoWaist = hipWidth / torsoLength
|
||||
val legTorso = legLength / torsoLength
|
||||
val legBody = legLength / (torsoLength + neckLength)
|
||||
val kneeLeg = lowerLegLength / legLength
|
||||
|
||||
LogManager.info(
|
||||
"[AutoBone] Ratios: [{Neck-Torso: ${
|
||||
StringUtils.prettyNumber(neckTorso)}}, {Chest-Torso: ${
|
||||
StringUtils.prettyNumber(chestTorso)}}, {Torso-Waist: ${
|
||||
StringUtils.prettyNumber(torsoWaist)}}, {Leg-Torso: ${
|
||||
StringUtils.prettyNumber(legTorso)}}, {Leg-Body: ${
|
||||
StringUtils.prettyNumber(legBody)}}, {Knee-Leg: ${
|
||||
StringUtils.prettyNumber(kneeLeg)}}]",
|
||||
)
|
||||
}
|
||||
|
||||
fun applyValues() {
|
||||
autoBone.applyAndSaveConfig()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import dev.slimevr.autobone.AutoBone.Epoch
|
||||
import dev.slimevr.poseframeformat.PoseFrames
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
|
||||
import java.util.*
|
||||
|
||||
interface AutoBoneListener {
|
||||
fun onAutoBoneProcessStatus(
|
||||
processType: AutoBoneProcessType,
|
||||
message: String?,
|
||||
current: Long,
|
||||
total: Long,
|
||||
eta: Float,
|
||||
completed: Boolean,
|
||||
success: Boolean,
|
||||
)
|
||||
|
||||
fun onAutoBoneRecordingEnd(recording: PoseFrames)
|
||||
fun onAutoBoneEpoch(epoch: Epoch)
|
||||
fun onAutoBoneEnd(configValues: EnumMap<SkeletonConfigOffsets, Float>)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
enum class AutoBoneProcessType(val id: Int) {
|
||||
NONE(0),
|
||||
RECORD(1),
|
||||
SAVE(2),
|
||||
PROCESS(3),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun getById(id: Int): AutoBoneProcessType? = byId[id]
|
||||
}
|
||||
}
|
||||
|
||||
private val byId = AutoBoneProcessType.values().associateBy { it.id }
|
||||
@@ -1,13 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
class AutoBoneStep(
|
||||
var hmdHeight: Float = 1f,
|
||||
val targetHmdHeight: Float = 1f,
|
||||
var adjustRate: Float = 0f,
|
||||
) {
|
||||
|
||||
val errorStats = StatsCalculator()
|
||||
|
||||
val heightOffset: Float
|
||||
get() = targetHmdHeight - hmdHeight
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import dev.slimevr.autobone.AutoBone.Companion.MIN_SLIDE_DIST
|
||||
import dev.slimevr.autobone.AutoBone.Companion.SYMM_CONFIGS
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
|
||||
object BoneContribution {
|
||||
/**
|
||||
* Computes the local tail position of the bone after rotation.
|
||||
*/
|
||||
fun getBoneLocalTail(
|
||||
skeleton: HumanPoseManager,
|
||||
boneType: BoneType,
|
||||
): Vector3 {
|
||||
val bone = skeleton.getBone(boneType)
|
||||
return bone.getTailPosition() - bone.getPosition()
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the direction of the bone tail's movement between skeletons 1 and 2.
|
||||
*/
|
||||
fun getBoneLocalTailDir(
|
||||
skeleton1: HumanPoseManager,
|
||||
skeleton2: HumanPoseManager,
|
||||
boneType: BoneType,
|
||||
): Vector3? {
|
||||
val boneOff = getBoneLocalTail(skeleton2, boneType) - getBoneLocalTail(skeleton1, boneType)
|
||||
val boneOffLen = boneOff.len()
|
||||
// If the offset is approx 0, just return null so it can be easily ignored
|
||||
return if (boneOffLen > MIN_SLIDE_DIST) boneOff / boneOffLen else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicts how much the provided config should be affecting the slide offsets
|
||||
* of the left and right ankles.
|
||||
*/
|
||||
fun getSlideDot(
|
||||
skeleton1: HumanPoseManager,
|
||||
skeleton2: HumanPoseManager,
|
||||
config: SkeletonConfigOffsets,
|
||||
slideL: Vector3?,
|
||||
slideR: Vector3?,
|
||||
): Float {
|
||||
var slideDot = 0f
|
||||
// Used for right offset if not a symmetric bone
|
||||
var boneOffL: Vector3? = null
|
||||
|
||||
// Treat null as 0
|
||||
if (slideL != null) {
|
||||
boneOffL = getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[0])
|
||||
|
||||
// Treat null as 0
|
||||
if (boneOffL != null) {
|
||||
slideDot += slideL.dot(boneOffL)
|
||||
}
|
||||
}
|
||||
|
||||
// Treat null as 0
|
||||
if (slideR != null) {
|
||||
// IMPORTANT: This assumption for acquiring BoneType only works if
|
||||
// SkeletonConfigOffsets is set up to only affect one BoneType, make sure no
|
||||
// changes to SkeletonConfigOffsets goes against this assumption, please!
|
||||
val boneOffR = if (SYMM_CONFIGS.contains(config)) {
|
||||
getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[1])
|
||||
} else if (slideL != null) {
|
||||
// Use cached offset if slideL was used
|
||||
boneOffL
|
||||
} else {
|
||||
// Compute offset if missing because of slideL
|
||||
getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[0])
|
||||
}
|
||||
|
||||
// Treat null as 0
|
||||
if (boneOffR != null) {
|
||||
slideDot += slideR.dot(boneOffR)
|
||||
}
|
||||
}
|
||||
|
||||
return slideDot / 2f
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import kotlin.random.Random
|
||||
|
||||
object PoseFrameIterator {
|
||||
fun <T> iterateFrames(
|
||||
step: PoseFrameStep<T>,
|
||||
) {
|
||||
check(step.frames.frameHolders.isNotEmpty()) { "Recording has no trackers." }
|
||||
check(step.maxFrameCount > 0) { "Recording has no frames." }
|
||||
|
||||
// Epoch loop, each epoch is one full iteration over the full dataset
|
||||
for (epoch in (if (step.config.calcInitError) -1 else 0) until step.config.numEpochs) {
|
||||
// Set the current epoch to process
|
||||
step.epoch = epoch
|
||||
// Process the epoch
|
||||
epoch(step)
|
||||
}
|
||||
}
|
||||
|
||||
private fun randomIndices(count: Int, random: Random): IntArray {
|
||||
val randIndices = IntArray(count)
|
||||
|
||||
var zeroPos = -1
|
||||
for (i in 0 until count) {
|
||||
var index = random.nextInt(count)
|
||||
if (i > 0) {
|
||||
while (index == zeroPos || randIndices[index] > 0) {
|
||||
index = random.nextInt(count)
|
||||
}
|
||||
} else {
|
||||
zeroPos = index
|
||||
}
|
||||
randIndices[index] = i
|
||||
}
|
||||
|
||||
return randIndices
|
||||
}
|
||||
|
||||
private fun <T> epoch(step: PoseFrameStep<T>) {
|
||||
val config = step.config
|
||||
val frameCount = step.maxFrameCount
|
||||
|
||||
// Perform any setup that needs to be done before the current epoch
|
||||
step.preEpoch?.accept(step)
|
||||
|
||||
val randIndices = if (config.randomizeFrameOrder) {
|
||||
randomIndices(step.maxFrameCount, step.random)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Iterate over the frames using a cursor and an offset for comparing
|
||||
// frames a certain number of frames apart
|
||||
var cursorOffset = config.minDataDistance
|
||||
while (cursorOffset <= config.maxDataDistance &&
|
||||
cursorOffset < frameCount
|
||||
) {
|
||||
var frameCursor = 0
|
||||
while (frameCursor < frameCount - cursorOffset) {
|
||||
val frameCursor2 = frameCursor + cursorOffset
|
||||
|
||||
// Then set the frame cursors and apply them to both skeletons
|
||||
if (config.randomizeFrameOrder && randIndices != null) {
|
||||
step
|
||||
.setCursors(
|
||||
randIndices[frameCursor],
|
||||
randIndices[frameCursor2],
|
||||
updatePlayerCursors = true,
|
||||
)
|
||||
} else {
|
||||
step.setCursors(
|
||||
frameCursor,
|
||||
frameCursor2,
|
||||
updatePlayerCursors = true,
|
||||
)
|
||||
}
|
||||
|
||||
// Process the iteration
|
||||
step.onStep.accept(step)
|
||||
|
||||
// Move on to the next iteration
|
||||
frameCursor += config.cursorIncrement
|
||||
}
|
||||
cursorOffset++
|
||||
}
|
||||
|
||||
step.postEpoch?.accept(step)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import dev.slimevr.config.AutoBoneConfig
|
||||
import dev.slimevr.config.ConfigManager
|
||||
import dev.slimevr.poseframeformat.PoseFrames
|
||||
import dev.slimevr.poseframeformat.player.TrackerFramesPlayer
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import java.util.function.Consumer
|
||||
import kotlin.random.Random
|
||||
|
||||
class PoseFrameStep<T>(
|
||||
val config: AutoBoneConfig,
|
||||
/** The config to initialize skeletons. */
|
||||
serverConfig: ConfigManager? = null,
|
||||
val frames: PoseFrames,
|
||||
/** The consumer run before each epoch. */
|
||||
val preEpoch: Consumer<PoseFrameStep<T>>? = null,
|
||||
/** The consumer run for each step. */
|
||||
val onStep: Consumer<PoseFrameStep<T>>,
|
||||
/** The consumer run after each epoch. */
|
||||
val postEpoch: Consumer<PoseFrameStep<T>>? = null,
|
||||
/** The current epoch. */
|
||||
var epoch: Int = 0,
|
||||
/** The current frame cursor position in [frames] for skeleton1. */
|
||||
var cursor1: Int = 0,
|
||||
/** The current frame cursor position in [frames] for skeleton2. */
|
||||
var cursor2: Int = 0,
|
||||
randomSeed: Long = 0,
|
||||
val data: T,
|
||||
) {
|
||||
var maxFrameCount = frames.maxFrameCount
|
||||
|
||||
val framePlayer1 = TrackerFramesPlayer(frames)
|
||||
val framePlayer2 = TrackerFramesPlayer(frames)
|
||||
|
||||
val trackers1 = framePlayer1.trackers.toList()
|
||||
val trackers2 = framePlayer2.trackers.toList()
|
||||
|
||||
val skeleton1 = HumanPoseManager(trackers1)
|
||||
val skeleton2 = HumanPoseManager(trackers2)
|
||||
|
||||
val random = Random(randomSeed)
|
||||
|
||||
init {
|
||||
// Load server configs into the skeleton
|
||||
if (serverConfig != null) {
|
||||
skeleton1.loadFromConfig(serverConfig)
|
||||
skeleton2.loadFromConfig(serverConfig)
|
||||
}
|
||||
// Disable leg tweaks and IK solver, these will mess with the resulting positions
|
||||
skeleton1.setLegTweaksEnabled(false)
|
||||
skeleton2.setLegTweaksEnabled(false)
|
||||
}
|
||||
|
||||
fun setCursors(cursor1: Int, cursor2: Int, updatePlayerCursors: Boolean) {
|
||||
this.cursor1 = cursor1
|
||||
this.cursor2 = cursor2
|
||||
|
||||
if (updatePlayerCursors) {
|
||||
updatePlayerCursors()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePlayerCursors() {
|
||||
framePlayer1.setCursors(cursor1)
|
||||
framePlayer2.setCursors(cursor2)
|
||||
skeleton1.update()
|
||||
skeleton2.update()
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package dev.slimevr.autobone
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* This is a stat calculator based on Welford's online algorithm
|
||||
* https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford%27s_online_algorithm
|
||||
*/
|
||||
class StatsCalculator {
|
||||
private var count = 0
|
||||
var mean = 0f
|
||||
private set
|
||||
private var m2 = 0f
|
||||
|
||||
fun reset() {
|
||||
count = 0
|
||||
mean = 0f
|
||||
m2 = 0f
|
||||
}
|
||||
|
||||
fun addValue(newValue: Float) {
|
||||
count += 1
|
||||
val delta = newValue - mean
|
||||
mean += delta / count
|
||||
val delta2 = newValue - mean
|
||||
m2 += delta * delta2
|
||||
}
|
||||
|
||||
val variance: Float
|
||||
get() = if (count < 1) {
|
||||
Float.NaN
|
||||
} else {
|
||||
m2 / count
|
||||
}
|
||||
val sampleVariance: Float
|
||||
get() = if (count < 2) {
|
||||
Float.NaN
|
||||
} else {
|
||||
m2 / (count - 1)
|
||||
}
|
||||
val standardDeviation: Float
|
||||
get() = sqrt(variance)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
class AutoBoneException : Exception {
|
||||
constructor()
|
||||
constructor(message: String?) : super(message)
|
||||
constructor(cause: Throwable?) : super(cause)
|
||||
constructor(message: String?, cause: Throwable?) : super(message, cause)
|
||||
constructor(
|
||||
message: String?,
|
||||
cause: Throwable?,
|
||||
enableSuppression: Boolean,
|
||||
writableStackTrace: Boolean,
|
||||
) : super(message, cause, enableSuppression, writableStackTrace)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import dev.slimevr.autobone.errors.proportions.ProportionLimiter
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
|
||||
import kotlin.math.*
|
||||
|
||||
// The distance from average human proportions
|
||||
class BodyProportionError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float = getBodyProportionError(
|
||||
step.skeleton1,
|
||||
// Skeletons are now normalized to reduce bias, so height is always 1
|
||||
1f,
|
||||
)
|
||||
|
||||
fun getBodyProportionError(humanPoseManager: HumanPoseManager, fullHeight: Float): Float {
|
||||
var sum = 0f
|
||||
for (limiter in proportionLimits) {
|
||||
sum += abs(limiter.getProportionError(humanPoseManager, fullHeight))
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
companion object {
|
||||
// The headset height is not the full height! This value compensates for the
|
||||
// offset from the headset height to the user full height
|
||||
// From Drillis and Contini (1966)
|
||||
@JvmField
|
||||
var eyeHeightToHeightRatio = 0.936f
|
||||
|
||||
val defaultHeight = SkeletonConfigManager.HEIGHT_OFFSETS.sumOf {
|
||||
it.defaultValue.toDouble()
|
||||
}.toFloat()
|
||||
|
||||
private fun makeLimiter(
|
||||
offset: SkeletonConfigOffsets,
|
||||
range: Float,
|
||||
scaleByHeight: Boolean = true,
|
||||
) = ProportionLimiter(
|
||||
if (scaleByHeight) {
|
||||
offset.defaultValue / defaultHeight
|
||||
} else {
|
||||
offset.defaultValue
|
||||
},
|
||||
offset,
|
||||
range,
|
||||
scaleByHeight,
|
||||
)
|
||||
|
||||
// "Expected" are values from Drillis and Contini (1966)
|
||||
// Default are values from experimentation by the SlimeVR community
|
||||
|
||||
/**
|
||||
* Proportions are based off the headset height (or eye height), not the total height of the user.
|
||||
* To use the total height of the user, multiply it by [eyeHeightToHeightRatio] and use that in the limiters.
|
||||
*/
|
||||
val proportionLimits = arrayOf<ProportionLimiter>(
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.HEAD,
|
||||
0.01f,
|
||||
scaleByHeight = false,
|
||||
),
|
||||
// Expected: 0.052
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.NECK,
|
||||
0.002f,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.SHOULDERS_WIDTH,
|
||||
0.04f,
|
||||
scaleByHeight = false,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.UPPER_ARM,
|
||||
0.02f,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.LOWER_ARM,
|
||||
0.02f,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.UPPER_CHEST,
|
||||
0.01f,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.CHEST,
|
||||
0.01f,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.WAIST,
|
||||
0.05f,
|
||||
),
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.HIP,
|
||||
0.01f,
|
||||
),
|
||||
// Expected: 0.191
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.HIPS_WIDTH,
|
||||
0.04f,
|
||||
scaleByHeight = false,
|
||||
),
|
||||
// Expected: 0.245
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.UPPER_LEG,
|
||||
0.02f,
|
||||
),
|
||||
// Expected: 0.246 (0.285 including below ankle, could use a separate
|
||||
// offset?)
|
||||
makeLimiter(
|
||||
SkeletonConfigOffsets.LOWER_LEG,
|
||||
0.02f,
|
||||
),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
val proportionLimitMap = proportionLimits.associateBy { it.skeletonConfigOffset }
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import kotlin.math.*
|
||||
|
||||
// The offset between the height both feet at one instant and over time
|
||||
class FootHeightOffsetError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float = getSlideError(
|
||||
step.skeleton1.skeleton,
|
||||
step.skeleton2.skeleton,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun getSlideError(skeleton1: HumanSkeleton, skeleton2: HumanSkeleton): Float = getFootHeightError(
|
||||
skeleton1.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(),
|
||||
skeleton1.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(),
|
||||
skeleton2.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(),
|
||||
skeleton2.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(),
|
||||
)
|
||||
|
||||
fun getFootHeightError(
|
||||
leftFoot1: Vector3,
|
||||
rightFoot1: Vector3,
|
||||
leftFoot2: Vector3,
|
||||
rightFoot2: Vector3,
|
||||
): Float {
|
||||
val lFoot1Y = leftFoot1.y
|
||||
val rFoot1Y = rightFoot1.y
|
||||
val lFoot2Y = leftFoot2.y
|
||||
val rFoot2Y = rightFoot2.y
|
||||
|
||||
// Compute all combinations of heights
|
||||
val dist1 = abs(lFoot1Y - rFoot1Y)
|
||||
val dist2 = abs(lFoot1Y - lFoot2Y)
|
||||
val dist3 = abs(lFoot1Y - rFoot2Y)
|
||||
val dist4 = abs(rFoot1Y - lFoot2Y)
|
||||
val dist5 = abs(rFoot1Y - rFoot2Y)
|
||||
val dist6 = abs(lFoot2Y - rFoot2Y)
|
||||
|
||||
// Divide by 12 (6 values * 2 to halve) 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import kotlin.math.*
|
||||
|
||||
// The difference from the current height to the target height
|
||||
class HeightError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float = getHeightError(
|
||||
step.data.hmdHeight,
|
||||
step.data.targetHmdHeight,
|
||||
)
|
||||
|
||||
fun getHeightError(currentHeight: Float, targetHeight: Float): Float = abs(targetHeight - currentHeight)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
|
||||
interface IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import kotlin.math.*
|
||||
|
||||
// The change in distance between both of the ankles over time
|
||||
class OffsetSlideError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float = getSlideError(
|
||||
step.skeleton1.skeleton,
|
||||
step.skeleton2.skeleton,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun getSlideError(skeleton1: HumanSkeleton, skeleton2: HumanSkeleton): Float = getSlideError(
|
||||
skeleton1.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(),
|
||||
skeleton1.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(),
|
||||
skeleton2.getBone(BoneType.LEFT_LOWER_LEG).getTailPosition(),
|
||||
skeleton2.getBone(BoneType.RIGHT_LOWER_LEG).getTailPosition(),
|
||||
)
|
||||
|
||||
fun getSlideError(
|
||||
leftFoot1: Vector3,
|
||||
rightFoot1: Vector3,
|
||||
leftFoot2: Vector3,
|
||||
rightFoot2: Vector3,
|
||||
): Float {
|
||||
val slideDist1 = (rightFoot1 - leftFoot1).len()
|
||||
val slideDist2 = (rightFoot2 - leftFoot2).len()
|
||||
val slideDist3 = (rightFoot2 - leftFoot1).len()
|
||||
val slideDist4 = (rightFoot1 - leftFoot2).len()
|
||||
|
||||
// Compute all combinations of distances
|
||||
val dist1 = abs(slideDist1 - slideDist2)
|
||||
val dist2 = abs(slideDist1 - slideDist3)
|
||||
val dist3 = abs(slideDist1 - slideDist4)
|
||||
val dist4 = abs(slideDist2 - slideDist3)
|
||||
val dist5 = abs(slideDist2 - slideDist4)
|
||||
val dist6 = abs(slideDist3 - slideDist4)
|
||||
|
||||
// Divide by 12 (6 values * 2 to halve) 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
|
||||
// The distance of any points to the corresponding absolute position
|
||||
class PositionError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float {
|
||||
val trackers = step.frames.frameHolders
|
||||
return (
|
||||
(
|
||||
getPositionError(
|
||||
trackers,
|
||||
step.cursor1,
|
||||
step.skeleton1.skeleton,
|
||||
) +
|
||||
getPositionError(
|
||||
trackers,
|
||||
step.cursor2,
|
||||
step.skeleton2.skeleton,
|
||||
)
|
||||
) /
|
||||
2f
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getPositionError(
|
||||
trackers: List<TrackerFrames>,
|
||||
cursor: Int,
|
||||
skeleton: HumanSkeleton,
|
||||
): Float {
|
||||
var offset = 0f
|
||||
var offsetCount = 0
|
||||
for (tracker in trackers) {
|
||||
val trackerFrame = tracker.tryGetFrame(cursor) ?: continue
|
||||
val position = trackerFrame.tryGetPosition() ?: continue
|
||||
val trackerRole = trackerFrame.tryGetTrackerPosition()?.trackerRole ?: continue
|
||||
|
||||
try {
|
||||
val computedTracker = skeleton.getComputedTracker(trackerRole)
|
||||
|
||||
offset += (position - computedTracker.position).len()
|
||||
offsetCount++
|
||||
} catch (_: Exception) {
|
||||
// Ignore unsupported positions
|
||||
}
|
||||
}
|
||||
return if (offsetCount > 0) offset / offsetCount else 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import kotlin.math.*
|
||||
|
||||
// The difference between offset of absolute position and the corresponding point over time
|
||||
class PositionOffsetError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float {
|
||||
val trackers = step.frames.frameHolders
|
||||
return getPositionOffsetError(
|
||||
trackers,
|
||||
step.cursor1,
|
||||
step.cursor2,
|
||||
step.skeleton1.skeleton,
|
||||
step.skeleton2.skeleton,
|
||||
)
|
||||
}
|
||||
|
||||
fun getPositionOffsetError(
|
||||
trackers: List<TrackerFrames>,
|
||||
cursor1: Int,
|
||||
cursor2: Int,
|
||||
skeleton1: HumanSkeleton,
|
||||
skeleton2: HumanSkeleton,
|
||||
): Float {
|
||||
var offset = 0f
|
||||
var offsetCount = 0
|
||||
for (tracker in trackers) {
|
||||
val trackerFrame1 = tracker.tryGetFrame(cursor1) ?: continue
|
||||
val position1 = trackerFrame1.tryGetPosition() ?: continue
|
||||
val trackerRole1 = trackerFrame1.tryGetTrackerPosition()?.trackerRole ?: continue
|
||||
|
||||
val trackerFrame2 = tracker.tryGetFrame(cursor2) ?: continue
|
||||
val position2 = trackerFrame2.tryGetPosition() ?: continue
|
||||
val trackerRole2 = trackerFrame2.tryGetTrackerPosition()?.trackerRole ?: continue
|
||||
|
||||
try {
|
||||
val computedTracker1 = skeleton1.getComputedTracker(trackerRole1)
|
||||
val computedTracker2 = skeleton2.getComputedTracker(trackerRole2)
|
||||
|
||||
val dist1 = (position1 - computedTracker1.position).len()
|
||||
val dist2 = (position2 - computedTracker2.position).len()
|
||||
offset += abs(dist2 - dist1)
|
||||
offsetCount++
|
||||
} catch (_: Exception) {
|
||||
// Ignore unsupported positions
|
||||
}
|
||||
}
|
||||
return if (offsetCount > 0) offset / offsetCount else 0f
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package dev.slimevr.autobone.errors
|
||||
|
||||
import dev.slimevr.autobone.AutoBoneStep
|
||||
import dev.slimevr.autobone.PoseFrameStep
|
||||
import dev.slimevr.tracking.processor.Bone
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
|
||||
// The change in position of the ankle over time
|
||||
class SlideError : IAutoBoneError {
|
||||
@Throws(AutoBoneException::class)
|
||||
override fun getStepError(step: PoseFrameStep<AutoBoneStep>): Float = getSlideError(
|
||||
step.skeleton1.skeleton,
|
||||
step.skeleton2.skeleton,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun getSlideError(skeleton1: HumanSkeleton, skeleton2: HumanSkeleton): Float {
|
||||
// Calculate and average between both feet
|
||||
return (
|
||||
getSlideError(skeleton1, skeleton2, BoneType.LEFT_LOWER_LEG) +
|
||||
getSlideError(skeleton1, skeleton2, BoneType.RIGHT_LOWER_LEG)
|
||||
) /
|
||||
2f
|
||||
}
|
||||
|
||||
fun getSlideError(
|
||||
skeleton1: HumanSkeleton,
|
||||
skeleton2: HumanSkeleton,
|
||||
bone: BoneType,
|
||||
): Float {
|
||||
// Calculate and average between both feet
|
||||
return getSlideError(
|
||||
skeleton1.getBone(bone),
|
||||
skeleton2.getBone(bone),
|
||||
)
|
||||
}
|
||||
|
||||
fun getSlideError(bone1: Bone, bone2: Bone): Float {
|
||||
// Return the midpoint distance
|
||||
return (bone2.getTailPosition() - bone1.getTailPosition()).len() / 2f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package dev.slimevr.autobone.errors.proportions
|
||||
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
|
||||
import kotlin.math.*
|
||||
|
||||
class ProportionLimiter {
|
||||
val targetRatio: Float
|
||||
val skeletonConfigOffset: SkeletonConfigOffsets
|
||||
val scaleByHeight: Boolean
|
||||
|
||||
val positiveRange: Float
|
||||
val negativeRange: Float
|
||||
|
||||
/**
|
||||
* @param targetRatio The bone to height ratio to target
|
||||
* @param skeletonConfigOffset The SkeletonConfigOffset to use for the length
|
||||
* @param range The range from the target ratio to accept (ex. 0.1)
|
||||
* @param scaleByHeight True if the bone length will be scaled by the height
|
||||
*/
|
||||
constructor(
|
||||
targetRatio: Float,
|
||||
skeletonConfigOffset: SkeletonConfigOffsets,
|
||||
range: Float,
|
||||
scaleByHeight: Boolean = true,
|
||||
) {
|
||||
this.targetRatio = targetRatio
|
||||
this.skeletonConfigOffset = skeletonConfigOffset
|
||||
this.scaleByHeight = scaleByHeight
|
||||
|
||||
// Handle if someone puts in a negative value
|
||||
val absRange = abs(range)
|
||||
positiveRange = absRange
|
||||
negativeRange = -absRange
|
||||
}
|
||||
|
||||
/**
|
||||
* @param targetRatio The bone to height ratio to target
|
||||
* @param skeletonConfigOffset The SkeletonConfigOffset to use for the length
|
||||
* @param positiveRange The positive range from the target ratio to accept
|
||||
* (ex. 0.1)
|
||||
* @param negativeRange The negative range from the target ratio to accept
|
||||
* (ex. -0.1)
|
||||
* @param scaleByHeight True if the bone length will be scaled by the height
|
||||
*/
|
||||
constructor(
|
||||
targetRatio: Float,
|
||||
skeletonConfigOffset: SkeletonConfigOffsets,
|
||||
positiveRange: Float,
|
||||
negativeRange: Float,
|
||||
scaleByHeight: Boolean = true,
|
||||
) {
|
||||
// If the positive range is less than the negative range, something is wrong
|
||||
require(positiveRange >= negativeRange) { "positiveRange must not be less than negativeRange" }
|
||||
|
||||
this.targetRatio = targetRatio
|
||||
this.skeletonConfigOffset = skeletonConfigOffset
|
||||
this.scaleByHeight = scaleByHeight
|
||||
|
||||
this.positiveRange = positiveRange
|
||||
this.negativeRange = negativeRange
|
||||
}
|
||||
|
||||
fun getProportionError(humanPoseManager: HumanPoseManager, height: Float): Float {
|
||||
val boneLength = humanPoseManager.getOffset(skeletonConfigOffset)
|
||||
val ratioOffset = if (scaleByHeight) {
|
||||
targetRatio - boneLength / height
|
||||
} else {
|
||||
targetRatio - boneLength
|
||||
}
|
||||
|
||||
// If the range is exceeded, return the offset from the range limit
|
||||
if (ratioOffset > positiveRange) {
|
||||
return ratioOffset - positiveRange
|
||||
} else if (ratioOffset < negativeRange) {
|
||||
return ratioOffset - negativeRange
|
||||
}
|
||||
return 0f
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package dev.slimevr.bridge
|
||||
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerRole
|
||||
import dev.slimevr.util.ann.VRServerThread
|
||||
|
||||
/**
|
||||
* Bridge handles sending and receiving tracker data between SlimeVR and other
|
||||
* systems like VR APIs (SteamVR, OpenXR, etc), apps and protocols (VMC,
|
||||
* WebSocket, TIP). It can create and manage tracker received from the **remote
|
||||
* side** or send shared **local trackers** to the other side.
|
||||
*/
|
||||
interface Bridge {
|
||||
@VRServerThread
|
||||
fun dataRead()
|
||||
|
||||
@VRServerThread
|
||||
fun dataWrite()
|
||||
|
||||
/**
|
||||
* Adds shared tracker to the bridge. Bridge should notify the other side of
|
||||
* this tracker, if it's the type of tracker this bridge serves, and start
|
||||
* sending data each update
|
||||
*
|
||||
* @param tracker
|
||||
*/
|
||||
@VRServerThread
|
||||
fun addSharedTracker(tracker: Tracker?)
|
||||
|
||||
/**
|
||||
* Removes tracker from a bridge. If the other side supports tracker
|
||||
* removal, bridge should notify it and stop sending new data. If it doesn't
|
||||
* support tracker removal, the bridge can either stop sending new data, or
|
||||
* keep sending it if it's available.
|
||||
*
|
||||
* @param tracker
|
||||
*/
|
||||
@VRServerThread
|
||||
fun removeSharedTracker(tracker: Tracker?)
|
||||
|
||||
@VRServerThread
|
||||
fun startBridge()
|
||||
|
||||
fun isConnected(): Boolean
|
||||
}
|
||||
|
||||
interface ISteamVRBridge : Bridge {
|
||||
fun getShareSetting(role: TrackerRole): Boolean
|
||||
|
||||
fun changeShareSettings(role: TrackerRole?, share: Boolean)
|
||||
|
||||
fun updateShareSettingsAutomatically(): Boolean
|
||||
|
||||
fun getAutomaticSharedTrackers(): Boolean
|
||||
fun setAutomaticSharedTrackers(value: Boolean)
|
||||
|
||||
fun getBridgeConfigKey(): String
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package dev.slimevr.bridge;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
|
||||
@Retention(value = RetentionPolicy.SOURCE)
|
||||
public @interface BridgeThread {
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class AutoBoneConfig {
|
||||
var cursorIncrement = 2
|
||||
var minDataDistance = 1
|
||||
var maxDataDistance = 1
|
||||
var numEpochs = 50
|
||||
var printEveryNumEpochs = 25
|
||||
var initialAdjustRate = 10.0f
|
||||
var adjustRateDecay = 1.0f
|
||||
var slideErrorFactor = 1.0f
|
||||
var offsetSlideErrorFactor = 0.0f
|
||||
var footHeightOffsetErrorFactor = 0.0f
|
||||
var bodyProportionErrorFactor = 0.05f
|
||||
var heightErrorFactor = 0.0f
|
||||
var positionErrorFactor = 0.0f
|
||||
var positionOffsetErrorFactor = 0.0f
|
||||
var calcInitError = false
|
||||
var randomizeFrameOrder = true
|
||||
var scaleEachStep = true
|
||||
var sampleCount = 1500
|
||||
var sampleRateMs = 20L
|
||||
var saveRecordings = false
|
||||
var useSkeletonHeight = false
|
||||
var randSeed = 4L
|
||||
var useFrameFiltering = false
|
||||
var maxFinalError = 0.03f
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdKeySerializers;
|
||||
import dev.slimevr.config.serializers.BooleanMapDeserializer;
|
||||
import dev.slimevr.tracking.trackers.TrackerRole;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class BridgeConfig {
|
||||
|
||||
@JsonDeserialize(using = BooleanMapDeserializer.class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class)
|
||||
public Map<String, Boolean> trackers = new HashMap<>();
|
||||
public boolean automaticSharedTrackersToggling = true;
|
||||
|
||||
public BridgeConfig() {
|
||||
}
|
||||
|
||||
public boolean getBridgeTrackerRole(TrackerRole role, boolean def) {
|
||||
return trackers.getOrDefault(role.name().toLowerCase(), def);
|
||||
}
|
||||
|
||||
public void setBridgeTrackerRole(TrackerRole role, boolean val) {
|
||||
this.trackers.put(role.name().toLowerCase(), val);
|
||||
}
|
||||
|
||||
public Map<String, Boolean> getTrackers() {
|
||||
return trackers;
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
|
||||
import com.github.jonpeterson.jackson.module.versioning.VersioningModule;
|
||||
import dev.slimevr.config.serializers.QuaternionDeserializer;
|
||||
import dev.slimevr.config.serializers.QuaternionSerializer;
|
||||
import io.eiren.util.ann.ThreadSafe;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import io.github.axisangles.ktmath.ObjectQuaternion;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private final String configPath;
|
||||
|
||||
private final ObjectMapper om;
|
||||
|
||||
private VRConfig vrConfig;
|
||||
|
||||
|
||||
public ConfigManager(String configPath) {
|
||||
this.configPath = configPath;
|
||||
om = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.SPLIT_LINES));
|
||||
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
om.registerModule(new VersioningModule());
|
||||
SimpleModule quaternionModule = new SimpleModule();
|
||||
quaternionModule.addSerializer(ObjectQuaternion.class, new QuaternionSerializer());
|
||||
quaternionModule.addDeserializer(ObjectQuaternion.class, new QuaternionDeserializer());
|
||||
om.registerModule(quaternionModule);
|
||||
}
|
||||
|
||||
public void loadConfig() {
|
||||
try {
|
||||
this.vrConfig = om
|
||||
.readValue(new FileInputStream(configPath), VRConfig.class);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Config file didn't exist, is not an error
|
||||
} catch (IOException e) {
|
||||
// Log the exception
|
||||
LogManager.severe("Config failed to load: " + e);
|
||||
// Make a backup of the erroneous config
|
||||
backupConfig();
|
||||
}
|
||||
|
||||
if (this.vrConfig == null) {
|
||||
this.vrConfig = new VRConfig();
|
||||
}
|
||||
}
|
||||
|
||||
static public void atomicMove(Path from, Path to) throws IOException {
|
||||
try {
|
||||
// Atomic move to overwrite
|
||||
Files.move(from, to, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException | FileAlreadyExistsException e) {
|
||||
// Atomic move not supported or does not replace, try just replacing
|
||||
Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void backupConfig() {
|
||||
Path cfgFile = Paths.get(configPath);
|
||||
Path tmpBakCfgFile = Paths.get(configPath + ".bak.tmp");
|
||||
Path bakCfgFile = Paths.get(configPath + ".bak");
|
||||
|
||||
try {
|
||||
Files
|
||||
.copy(
|
||||
cfgFile,
|
||||
tmpBakCfgFile,
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
StandardCopyOption.COPY_ATTRIBUTES
|
||||
);
|
||||
LogManager.info("Made a backup copy of config to \"" + tmpBakCfgFile + "\"");
|
||||
} catch (IOException e) {
|
||||
LogManager
|
||||
.severe(
|
||||
"Unable to make backup copy of config from \""
|
||||
+ cfgFile
|
||||
+ "\" to \""
|
||||
+ tmpBakCfgFile
|
||||
+ "\"",
|
||||
e
|
||||
);
|
||||
return; // Abort write
|
||||
}
|
||||
|
||||
try {
|
||||
atomicMove(tmpBakCfgFile, bakCfgFile);
|
||||
} catch (IOException e) {
|
||||
LogManager
|
||||
.severe(
|
||||
"Unable to move backup config from \""
|
||||
+ tmpBakCfgFile
|
||||
+ "\" to \""
|
||||
+ bakCfgFile
|
||||
+ "\"",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
public synchronized void saveConfig() {
|
||||
Path tmpCfgFile = Paths.get(configPath + ".tmp");
|
||||
Path cfgFile = Paths.get(configPath);
|
||||
|
||||
// Serialize config
|
||||
try {
|
||||
// delete accidental folder caused by PR
|
||||
// https://github.com/SlimeVR/SlimeVR-Server/pull/1176
|
||||
var cfgFileMaybeFolder = cfgFile.toFile();
|
||||
if (cfgFileMaybeFolder.isDirectory()) {
|
||||
try (Stream<Path> pathStream = Files.walk(cfgFile)) {
|
||||
// Can't use .toList() on Android
|
||||
var list = pathStream
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.collect(Collectors.toList());
|
||||
for (var path : list) {
|
||||
Files.delete(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogManager
|
||||
.severe(
|
||||
"Unable to delete folder that has same name as the config file on path \""
|
||||
+ cfgFile
|
||||
+ "\""
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
var cfgFolder = cfgFile.toAbsolutePath().getParent().toFile();
|
||||
if (!cfgFolder.exists() && !cfgFolder.mkdirs()) {
|
||||
LogManager
|
||||
.severe("Unable to create folders for config on path \"" + cfgFile + "\"");
|
||||
return;
|
||||
}
|
||||
om.writeValue(tmpCfgFile.toFile(), this.vrConfig);
|
||||
} catch (IOException e) {
|
||||
LogManager.severe("Unable to write serialized config to \"" + tmpCfgFile + "\"", e);
|
||||
return; // Abort write
|
||||
}
|
||||
|
||||
// Overwrite old config
|
||||
try {
|
||||
atomicMove(tmpCfgFile, cfgFile);
|
||||
} catch (IOException e) {
|
||||
LogManager
|
||||
.severe(
|
||||
"Unable to move new config from \"" + tmpCfgFile + "\" to \"" + cfgFile + "\"",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void resetConfig() {
|
||||
this.vrConfig = new VRConfig();
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public VRConfig getVrConfig() {
|
||||
return vrConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.*;
|
||||
import com.github.jonpeterson.jackson.module.versioning.VersionedModelConverter;
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets;
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class CurrentVRConfigConverter implements VersionedModelConverter {
|
||||
|
||||
@Override
|
||||
public ObjectNode convert(
|
||||
ObjectNode modelData,
|
||||
String modelVersion,
|
||||
String targetModelVersion,
|
||||
JsonNodeFactory nodeFactory
|
||||
) {
|
||||
try {
|
||||
int version = Integer.parseInt(modelVersion);
|
||||
|
||||
// Configs with old versions need a migration to the latest config
|
||||
if (version < 2) {
|
||||
// Move zoom to the window config
|
||||
ObjectNode windowNode = (ObjectNode) modelData.get("window");
|
||||
DoubleNode zoomNode = (DoubleNode) modelData.get("zoom");
|
||||
if (windowNode != null && zoomNode != null) {
|
||||
windowNode.set("zoom", zoomNode);
|
||||
modelData.remove("zoom");
|
||||
}
|
||||
|
||||
// Change trackers list to map
|
||||
ArrayNode oldTrackersNode = modelData.withArray("trackers");
|
||||
if (oldTrackersNode != null) {
|
||||
var trackersIter = oldTrackersNode.iterator();
|
||||
ObjectNode trackersNode = nodeFactory.objectNode();
|
||||
while (trackersIter.hasNext()) {
|
||||
JsonNode node = trackersIter.next();
|
||||
JsonNode resultNode = TrackerConfig.toV2(node, nodeFactory);
|
||||
trackersNode.set(node.get("name").asText(), resultNode);
|
||||
}
|
||||
modelData.set("trackers", trackersNode);
|
||||
}
|
||||
|
||||
// Rename bridge to bridges
|
||||
ObjectNode bridgeNode = (ObjectNode) modelData.get("bridge");
|
||||
if (bridgeNode != null) {
|
||||
modelData.set("bridges", bridgeNode);
|
||||
modelData.remove("bridge");
|
||||
}
|
||||
|
||||
// Move body to skeleton (and merge it to current skeleton)
|
||||
ObjectNode bodyNode = (ObjectNode) modelData.get("body");
|
||||
if (bodyNode != null) {
|
||||
var bodyIter = bodyNode.fields();
|
||||
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
|
||||
if (skeletonNode == null) {
|
||||
skeletonNode = nodeFactory.objectNode();
|
||||
}
|
||||
|
||||
ObjectNode offsetsNode = nodeFactory.objectNode();
|
||||
while (bodyIter.hasNext()) {
|
||||
Map.Entry<String, JsonNode> node = bodyIter.next();
|
||||
// Filter only number values because other types would
|
||||
// be stuff that didn't get migrated correctly before
|
||||
if (node.getValue().isNumber()) {
|
||||
offsetsNode.set(node.getKey(), node.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// Fix calibration wolf typos
|
||||
offsetsNode.set("shouldersWidth", bodyNode.get("shoulersWidth"));
|
||||
offsetsNode.set("shouldersDistance", bodyNode.get("shoulersDistance"));
|
||||
offsetsNode.remove("shoulersWidth");
|
||||
offsetsNode.remove("shoulersDistance");
|
||||
skeletonNode.set("offsets", offsetsNode);
|
||||
modelData.set("skeleton", skeletonNode);
|
||||
modelData.remove("body");
|
||||
}
|
||||
}
|
||||
if (version < 3) {
|
||||
// Check for out-of-bound filtering amount
|
||||
ObjectNode filtersNode = (ObjectNode) modelData.get("filters");
|
||||
if (filtersNode != null && filtersNode.get("amount").floatValue() > 2f) {
|
||||
filtersNode.set("amount", new FloatNode(0.2f));
|
||||
}
|
||||
}
|
||||
if (version < 4) {
|
||||
// Change mountingRotation to mountingOrientation
|
||||
ObjectNode oldTrackersNode = (ObjectNode) modelData.get("trackers");
|
||||
if (oldTrackersNode != null) {
|
||||
var trackersIter = oldTrackersNode.iterator();
|
||||
var fieldNamesIter = oldTrackersNode.fieldNames();
|
||||
ObjectNode trackersNode = nodeFactory.objectNode();
|
||||
String fieldName;
|
||||
while (trackersIter.hasNext()) {
|
||||
ObjectNode node = (ObjectNode) trackersIter.next();
|
||||
fieldName = fieldNamesIter.next();
|
||||
node.set("mountingOrientation", node.get("mountingRotation"));
|
||||
node.remove("mountingRotation");
|
||||
trackersNode.set(fieldName, node);
|
||||
}
|
||||
modelData.set("trackers", trackersNode);
|
||||
}
|
||||
}
|
||||
if (version < 5) {
|
||||
// Migrate old skeleton offsets to new ones
|
||||
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
|
||||
if (skeletonNode != null) {
|
||||
ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets");
|
||||
if (offsetsNode != null) {
|
||||
// torsoLength, chestDistance and waistDistance become
|
||||
// chestLength, waistLength and hipLength.
|
||||
float torsoLength = SkeletonConfigOffsets.CHEST.defaultValue
|
||||
+ SkeletonConfigOffsets.WAIST.defaultValue
|
||||
+ SkeletonConfigOffsets.HIP.defaultValue;
|
||||
float chestDistance = SkeletonConfigOffsets.CHEST.defaultValue;
|
||||
float waistDistance = SkeletonConfigOffsets.HIP.defaultValue;
|
||||
JsonNode torsoNode = offsetsNode.get("torsoLength");
|
||||
if (torsoNode != null)
|
||||
torsoLength = torsoNode.floatValue();
|
||||
JsonNode chestNode = offsetsNode.get("chestDistance");
|
||||
if (chestNode != null)
|
||||
chestDistance = chestNode.floatValue();
|
||||
JsonNode waistNode = offsetsNode.get("waistDistance");
|
||||
if (waistNode != null)
|
||||
waistDistance = waistNode.floatValue();
|
||||
offsetsNode.set("chestLength", offsetsNode.get("chestDistance"));
|
||||
offsetsNode
|
||||
.set(
|
||||
"waistLength",
|
||||
new FloatNode(torsoLength - chestDistance - waistDistance)
|
||||
);
|
||||
offsetsNode.set("hipLength", offsetsNode.get("waistDistance"));
|
||||
offsetsNode.remove("torsoLength");
|
||||
offsetsNode.remove("chestDistance");
|
||||
offsetsNode.remove("waistDistance");
|
||||
|
||||
// legsLength and kneeHeight become
|
||||
// upperLegLength and lowerLegLength
|
||||
float legsLength = SkeletonConfigOffsets.UPPER_LEG.defaultValue
|
||||
+ SkeletonConfigOffsets.LOWER_LEG.defaultValue;
|
||||
float kneeHeight = SkeletonConfigOffsets.LOWER_LEG.defaultValue;
|
||||
JsonNode legsNode = offsetsNode.get("legsLength");
|
||||
if (legsNode != null)
|
||||
legsLength = legsNode.floatValue();
|
||||
JsonNode kneesNode = offsetsNode.get("kneeHeight");
|
||||
if (kneesNode != null)
|
||||
kneeHeight = kneesNode.floatValue();
|
||||
offsetsNode.set("upperLegLength", new FloatNode(legsLength - kneeHeight));
|
||||
offsetsNode.set("lowerLegLength", new FloatNode(kneeHeight));
|
||||
offsetsNode.remove("legsLength");
|
||||
offsetsNode.remove("kneeHeight");
|
||||
|
||||
skeletonNode.set("offsets", offsetsNode);
|
||||
modelData.set("skeleton", skeletonNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version < 6) {
|
||||
// Migrate controllers offsets to hands offsets
|
||||
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
|
||||
if (skeletonNode != null) {
|
||||
ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets");
|
||||
if (offsetsNode != null) {
|
||||
offsetsNode.set("handDistanceY", offsetsNode.get("controllerDistanceY"));
|
||||
offsetsNode.set("handDistanceZ", offsetsNode.get("controllerDistanceZ"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version < 7) {
|
||||
// Chest, hip, and elbow offsets now go the opposite direction
|
||||
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
|
||||
if (skeletonNode != null) {
|
||||
ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets");
|
||||
if (offsetsNode != null) {
|
||||
JsonNode chestNode = offsetsNode.get("chestOffset");
|
||||
if (chestNode != null)
|
||||
offsetsNode.set("chestOffset", new FloatNode(-chestNode.floatValue()));
|
||||
JsonNode hipNode = offsetsNode.get("hipOffset");
|
||||
if (hipNode != null)
|
||||
offsetsNode.set("hipOffset", new FloatNode(-hipNode.floatValue()));
|
||||
JsonNode elbowNode = offsetsNode.get("elbowOffset");
|
||||
if (elbowNode != null)
|
||||
offsetsNode.set("elbowOffset", new FloatNode(-elbowNode.floatValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version < 8) {
|
||||
// reset > fullReset, quickReset > yawReset
|
||||
ObjectNode keybindingsNode = (ObjectNode) modelData.get("keybindings");
|
||||
if (keybindingsNode != null) {
|
||||
JsonNode fullResetNode = keybindingsNode.get("resetBinding");
|
||||
if (fullResetNode != null)
|
||||
keybindingsNode.set("fullResetBinding", fullResetNode);
|
||||
JsonNode yawResetNode = keybindingsNode.get("quickResetBinding");
|
||||
if (yawResetNode != null)
|
||||
keybindingsNode.set("yawResetBinding", yawResetNode);
|
||||
JsonNode mountingResetNode = keybindingsNode.get("resetMountingBinding");
|
||||
if (mountingResetNode != null)
|
||||
keybindingsNode.set("mountingResetBinding", mountingResetNode);
|
||||
|
||||
JsonNode fullDelayNode = keybindingsNode.get("resetDelay");
|
||||
if (fullDelayNode != null)
|
||||
keybindingsNode.set("fullResetDelay", fullDelayNode);
|
||||
JsonNode yawDelayNode = keybindingsNode.get("quickResetDelay");
|
||||
if (yawDelayNode != null)
|
||||
keybindingsNode.set("yawResetDelay", yawDelayNode);
|
||||
JsonNode mountingDelayNode = keybindingsNode.get("resetMountingDelay");
|
||||
if (mountingDelayNode != null)
|
||||
keybindingsNode.set("mountingResetDelay", mountingDelayNode);
|
||||
}
|
||||
|
||||
ObjectNode tapDetectionNode = (ObjectNode) modelData.get("tapDetection");
|
||||
if (tapDetectionNode != null) {
|
||||
tapDetectionNode.set("yawResetDelay", tapDetectionNode.get("quickResetDelay"));
|
||||
tapDetectionNode.set("fullResetDelay", tapDetectionNode.get("resetDelay"));
|
||||
tapDetectionNode
|
||||
.set("yawResetEnabled", tapDetectionNode.get("quickResetEnabled"));
|
||||
tapDetectionNode.set("fullResetEnabled", tapDetectionNode.get("resetEnabled"));
|
||||
tapDetectionNode.set("yawResetTaps", tapDetectionNode.get("quickResetTaps"));
|
||||
tapDetectionNode.set("fullResetTaps", tapDetectionNode.get("resetTaps"));
|
||||
}
|
||||
}
|
||||
if (version < 9) {
|
||||
// split chest into 2 offsets
|
||||
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
|
||||
if (skeletonNode != null) {
|
||||
ObjectNode offsetsNode = (ObjectNode) skeletonNode.get("offsets");
|
||||
if (offsetsNode != null) {
|
||||
JsonNode chestNode = offsetsNode.get("chestLength");
|
||||
if (chestNode != null) {
|
||||
offsetsNode
|
||||
.set("chestLength", new FloatNode(chestNode.floatValue() / 2f));
|
||||
offsetsNode
|
||||
.set(
|
||||
"upperChestLength",
|
||||
new FloatNode(chestNode.floatValue() / 2f)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version < 10) {
|
||||
// Change default AutoBone recording length from 20 to 30
|
||||
// seconds
|
||||
ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone");
|
||||
if (autoBoneNode != null) {
|
||||
JsonNode sampleCountNode = autoBoneNode.get("sampleCount");
|
||||
if (sampleCountNode != null && sampleCountNode.intValue() == 1000) {
|
||||
autoBoneNode.set("sampleCount", new IntNode(1500));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version < 11) {
|
||||
// Sets HMD's designation to "body:head"
|
||||
ObjectNode trackersNode = (ObjectNode) modelData.get("trackers");
|
||||
if (trackersNode != null) {
|
||||
ObjectNode HMDNode = (ObjectNode) trackersNode.get("HMD");
|
||||
if (HMDNode != null) {
|
||||
HMDNode
|
||||
.set(
|
||||
"designation",
|
||||
new TextNode(TrackerPosition.HEAD.getDesignation())
|
||||
);
|
||||
trackersNode.set("HMD", HMDNode);
|
||||
modelData.set("trackers", trackersNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version < 12) {
|
||||
// Update AutoBone defaults
|
||||
ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone");
|
||||
if (autoBoneNode != null) {
|
||||
JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor");
|
||||
if (offsetSlideNode != null && offsetSlideNode.floatValue() == 2.0f) {
|
||||
autoBoneNode.set("offsetSlideErrorFactor", new FloatNode(1.0f));
|
||||
}
|
||||
JsonNode bodyProportionsNode = autoBoneNode.get("bodyProportionErrorFactor");
|
||||
if (bodyProportionsNode != null && bodyProportionsNode.floatValue() == 0.825f) {
|
||||
autoBoneNode.set("bodyProportionErrorFactor", new FloatNode(0.25f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (version < 13) {
|
||||
ObjectNode oldTrackersNode = (ObjectNode) modelData.get("trackers");
|
||||
if (oldTrackersNode != null) {
|
||||
var fieldNamesIter = oldTrackersNode.fieldNames();
|
||||
String trackerId;
|
||||
final String macAddressRegex = "udp://((?:[a-zA-Z\\d]{2}:){5}[a-zA-Z\\d]{2})/0";
|
||||
final Pattern pattern = Pattern.compile(macAddressRegex);
|
||||
while (fieldNamesIter.hasNext()) {
|
||||
trackerId = fieldNamesIter.next();
|
||||
var matcher = pattern.matcher(trackerId);
|
||||
if (!matcher.find())
|
||||
continue;
|
||||
|
||||
modelData.withArray("knownDevices").add(matcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (version < 14) {
|
||||
// Update AutoBone defaults
|
||||
ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone");
|
||||
if (autoBoneNode != null) {
|
||||
// Move HMD height to skeleton
|
||||
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
|
||||
if (skeletonNode != null) {
|
||||
JsonNode targetHmdHeight = autoBoneNode.get("targetHmdHeight");
|
||||
if (targetHmdHeight != null) {
|
||||
skeletonNode.set("hmdHeight", targetHmdHeight);
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor");
|
||||
JsonNode slideNode = autoBoneNode.get("slideErrorFactor");
|
||||
if (
|
||||
offsetSlideNode != null
|
||||
&& slideNode != null
|
||||
&& offsetSlideNode.floatValue() == 1.0f
|
||||
&& slideNode.floatValue() == 0.0f
|
||||
) {
|
||||
autoBoneNode.set("offsetSlideErrorFactor", new FloatNode(0.0f));
|
||||
autoBoneNode.set("slideErrorFactor", new FloatNode(1.0f));
|
||||
}
|
||||
JsonNode bodyProportionsNode = autoBoneNode.get("bodyProportionErrorFactor");
|
||||
if (bodyProportionsNode != null && bodyProportionsNode.floatValue() == 0.25f) {
|
||||
autoBoneNode.set("bodyProportionErrorFactor", new FloatNode(0.05f));
|
||||
}
|
||||
JsonNode numEpochsNode = autoBoneNode.get("numEpochs");
|
||||
if (numEpochsNode != null && numEpochsNode.intValue() == 100) {
|
||||
autoBoneNode.set("numEpochs", new IntNode(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (version < 15) {
|
||||
ObjectNode checklistNode = (ObjectNode) modelData.get("trackingChecklist");
|
||||
if (checklistNode != null) {
|
||||
ArrayNode ignoredStepsArray = (ArrayNode) checklistNode.get("ignoredStepsIds");
|
||||
if (ignoredStepsArray != null)
|
||||
ignoredStepsArray.removeAll();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogManager.severe("Error during config migration: " + e);
|
||||
}
|
||||
|
||||
return modelData;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
|
||||
class DriftCompensationConfig {
|
||||
|
||||
// Is drift compensation enabled
|
||||
var enabled = false
|
||||
|
||||
// Is drift prediction enabled
|
||||
var prediction = false
|
||||
|
||||
// Amount of drift compensation applied
|
||||
var amount = 0.8f
|
||||
|
||||
// Max resets for the calculated average drift
|
||||
var maxResets = 6
|
||||
|
||||
fun updateTrackersDriftCompensation() {
|
||||
for (t in VRServer.instance.allTrackers) {
|
||||
if (t.isImu()) {
|
||||
t.resetsHandler.readDriftCompensationConfig(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
|
||||
class FiltersConfig {
|
||||
|
||||
// Type of filtering applied (none, smoothing or prediction)
|
||||
var type = "prediction"
|
||||
|
||||
// Amount/Intensity of the specified filtering (0 to 1)
|
||||
var amount = 0.2f
|
||||
|
||||
fun updateTrackersFilters() {
|
||||
for (tracker in VRServer.instance.allTrackers) {
|
||||
if (tracker.allowFiltering) {
|
||||
tracker.filteringHandler.readFilteringConfig(this, tracker.getRotation())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
|
||||
class HIDConfig {
|
||||
var trackersOverHID = false
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
public class KeybindingsConfig {
|
||||
|
||||
private String fullResetBinding = "CTRL+ALT+SHIFT+Y";
|
||||
|
||||
private String yawResetBinding = "CTRL+ALT+SHIFT+U";
|
||||
|
||||
private String mountingResetBinding = "CTRL+ALT+SHIFT+I";
|
||||
|
||||
private String feetMountingResetBinding = "CTRL+ALT+SHIFT+P";
|
||||
|
||||
private String pauseTrackingBinding = "CTRL+ALT+SHIFT+O";
|
||||
|
||||
private long fullResetDelay = 0L;
|
||||
|
||||
private long yawResetDelay = 0L;
|
||||
|
||||
private long mountingResetDelay = 0L;
|
||||
|
||||
private long feetMountingResetDelay = 0L;
|
||||
|
||||
private long pauseTrackingDelay = 0L;
|
||||
|
||||
|
||||
public KeybindingsConfig() {
|
||||
}
|
||||
|
||||
public String getFullResetBinding() {
|
||||
return fullResetBinding;
|
||||
}
|
||||
|
||||
public String getYawResetBinding() {
|
||||
return yawResetBinding;
|
||||
}
|
||||
|
||||
public String getMountingResetBinding() {
|
||||
return mountingResetBinding;
|
||||
}
|
||||
|
||||
public String getFeetMountingResetBinding() {
|
||||
return feetMountingResetBinding;
|
||||
}
|
||||
|
||||
public String getPauseTrackingBinding() {
|
||||
return pauseTrackingBinding;
|
||||
}
|
||||
|
||||
public long getFullResetDelay() {
|
||||
return fullResetDelay;
|
||||
}
|
||||
|
||||
public void setFullResetDelay(long delay) {
|
||||
fullResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getYawResetDelay() {
|
||||
return yawResetDelay;
|
||||
}
|
||||
|
||||
public void setYawResetDelay(long delay) {
|
||||
yawResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getMountingResetDelay() {
|
||||
return mountingResetDelay;
|
||||
}
|
||||
|
||||
public void setMountingResetDelay(long delay) {
|
||||
mountingResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getFeetMountingResetDelay() {
|
||||
return feetMountingResetDelay;
|
||||
}
|
||||
|
||||
public void setFeetMountingResetDelay(long delay) {
|
||||
feetMountingResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getPauseTrackingDelay() {
|
||||
return pauseTrackingDelay;
|
||||
}
|
||||
|
||||
public void setPauseTrackingDelay(long delay) {
|
||||
pauseTrackingDelay = delay;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class LegTweaksConfig {
|
||||
var correctionStrength = 0.3f
|
||||
var alwaysUseFloorclip = false
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
open class OSCConfig {
|
||||
|
||||
// Are the OSC receiver and sender enabled?
|
||||
var enabled = false
|
||||
|
||||
// Port to receive OSC messages from
|
||||
var portIn = 0
|
||||
|
||||
// Port to send out OSC messages at
|
||||
var portOut = 0
|
||||
|
||||
// Address to send out OSC messages at
|
||||
var address = "127.0.0.1"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
public class OverlayConfig {
|
||||
|
||||
private boolean isMirrored = false;
|
||||
private boolean isVisible = false;
|
||||
|
||||
|
||||
public boolean isMirrored() {
|
||||
return isMirrored;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
public void setMirrored(boolean mirrored) {
|
||||
isMirrored = mirrored;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
isVisible = visible;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
|
||||
enum class ArmsResetModes(val id: Int) {
|
||||
// Upper arm going back and forearm going forward
|
||||
BACK(0),
|
||||
|
||||
// Arms going forward
|
||||
FORWARD(1),
|
||||
|
||||
// Arms going up to the sides into a tpose
|
||||
TPOSE_UP(2),
|
||||
|
||||
// Arms going down to the sides from a tpose
|
||||
TPOSE_DOWN(3),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val values = entries.toTypedArray()
|
||||
|
||||
@JvmStatic
|
||||
fun fromId(id: Int): ArmsResetModes? {
|
||||
for (filter in values) {
|
||||
if (filter.id == id) return filter
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class MountingMethods(val id: Int) {
|
||||
MANUAL(0),
|
||||
AUTOMATIC(1),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val values = MountingMethods.entries.toTypedArray()
|
||||
|
||||
@JvmStatic
|
||||
fun fromId(id: Int): MountingMethods? {
|
||||
for (filter in values) {
|
||||
if (filter.id == id) return filter
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResetsConfig {
|
||||
|
||||
// Always reset mounting for feet
|
||||
var resetMountingFeet = false
|
||||
|
||||
// Reset mode used for the arms
|
||||
var mode = ArmsResetModes.BACK
|
||||
|
||||
// Yaw reset smoothing time in seconds
|
||||
var yawResetSmoothTime = 0.0f
|
||||
|
||||
// Save automatic mounting reset calibration
|
||||
var saveMountingReset = false
|
||||
|
||||
// Reset the HMD's pitch upon full reset
|
||||
var resetHmdPitch = false
|
||||
|
||||
var lastMountingMethod = MountingMethods.AUTOMATIC
|
||||
|
||||
var yawResetDelay = 0.0f
|
||||
var fullResetDelay = 3.0f
|
||||
var mountingResetDelay = 3.0f
|
||||
|
||||
fun updateTrackersResetsSettings() {
|
||||
for (t in VRServer.instance.allTrackers) {
|
||||
t.resetsHandler.readResetConfig(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.tracking.trackers.udp.MagnetometerStatus
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
class ServerConfig {
|
||||
val trackerPort: Int = 6969
|
||||
|
||||
var useMagnetometerOnAllTrackers: Boolean = false
|
||||
private set
|
||||
|
||||
private val magMutex = Mutex()
|
||||
suspend fun defineMagOnAllTrackers(state: Boolean) = coroutineScope {
|
||||
magMutex.lock()
|
||||
try {
|
||||
if (useMagnetometerOnAllTrackers == state) return@coroutineScope
|
||||
|
||||
VRServer.instance.deviceManager.devices.filter { it.magSupport }.map {
|
||||
async {
|
||||
// Not using 255 as it sometimes could make one of the sensors go into
|
||||
// error mode (if there is more than one sensor inside the device)
|
||||
if (!state) {
|
||||
val trackers = it.trackers.filterValues {
|
||||
it.magStatus != MagnetometerStatus.NOT_SUPPORTED
|
||||
}
|
||||
// if(trackers.size == it.trackers.size) {
|
||||
// it.setMag(false)
|
||||
// } else {
|
||||
trackers.map { (_, t) ->
|
||||
async { it.setMag(false, t.trackerNum) }
|
||||
}.awaitAll()
|
||||
// }
|
||||
return@async
|
||||
}
|
||||
|
||||
// val every = it.trackers.all { (_, t) -> t.config.shouldHaveMagEnabled == true
|
||||
// && t.magStatus != MagnetometerStatus.NOT_SUPPORTED }
|
||||
// if (every) {
|
||||
// it.setMag(true)
|
||||
// return@async
|
||||
// }
|
||||
|
||||
it.trackers.filterValues {
|
||||
it.config.shouldHaveMagEnabled == true &&
|
||||
it.magStatus != MagnetometerStatus.NOT_SUPPORTED
|
||||
}
|
||||
.map { (_, t) ->
|
||||
async {
|
||||
// FIXME: Tracker gets restarted after each setMag, what will happen for devices with 3 trackers?
|
||||
it.setMag(true, t.trackerNum)
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
useMagnetometerOnAllTrackers = state
|
||||
VRServer.instance.configManager.saveConfig()
|
||||
} finally {
|
||||
magMutex.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdKeySerializers;
|
||||
import dev.slimevr.config.serializers.BooleanMapDeserializer;
|
||||
import dev.slimevr.config.serializers.FloatMapDeserializer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class SkeletonConfig {
|
||||
|
||||
@JsonDeserialize(using = BooleanMapDeserializer.class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class)
|
||||
public Map<String, Boolean> toggles = new HashMap<>();
|
||||
|
||||
@JsonDeserialize(using = FloatMapDeserializer.class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class)
|
||||
public Map<String, Float> values = new HashMap<>();
|
||||
|
||||
@JsonDeserialize(using = FloatMapDeserializer.class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class)
|
||||
public Map<String, Float> offsets = new HashMap<>();
|
||||
|
||||
private float hmdHeight = 0f;
|
||||
private float floorHeight = 0f;
|
||||
|
||||
public Map<String, Boolean> getToggles() {
|
||||
return toggles;
|
||||
}
|
||||
|
||||
public Map<String, Float> getOffsets() {
|
||||
return offsets;
|
||||
}
|
||||
|
||||
public Map<String, Float> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public float getHmdHeight() {
|
||||
return hmdHeight;
|
||||
}
|
||||
|
||||
public void setHmdHeight(float hmdHeight) {
|
||||
this.hmdHeight = hmdHeight;
|
||||
}
|
||||
|
||||
public float getFloorHeight() {
|
||||
return floorHeight;
|
||||
}
|
||||
|
||||
public void setFloorHeight(float hmdHeight) {
|
||||
this.floorHeight = hmdHeight;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public float getUserHeight() {
|
||||
return hmdHeight - floorHeight;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
|
||||
class StayAlignedConfig {
|
||||
|
||||
/**
|
||||
* Apply yaw correction
|
||||
*/
|
||||
var enabled = false
|
||||
|
||||
/**
|
||||
* Temporarily hide the yaw correction from Stay Aligned.
|
||||
*
|
||||
* Players can enable this to compare to when Stay Aligned is not enabled. Useful to
|
||||
* verify if Stay Aligned improved the situation. Also useful to prevent players
|
||||
* from saying "Stay Aligned screwed up my trackers!!" when it's actually a tracker
|
||||
* that is drifting extremely badly.
|
||||
*
|
||||
* Do not serialize to config so that when the server restarts, it is always false.
|
||||
*/
|
||||
@JsonIgnore
|
||||
var hideYawCorrection = false
|
||||
|
||||
/**
|
||||
* Standing relaxed pose
|
||||
*/
|
||||
val standingRelaxedPose = StayAlignedRelaxedPoseConfig()
|
||||
|
||||
/**
|
||||
* Sitting relaxed pose
|
||||
*/
|
||||
val sittingRelaxedPose = StayAlignedRelaxedPoseConfig()
|
||||
|
||||
/**
|
||||
* Flat relaxed pose
|
||||
*/
|
||||
val flatRelaxedPose = StayAlignedRelaxedPoseConfig()
|
||||
|
||||
/**
|
||||
* Whether setup has been completed
|
||||
*/
|
||||
var setupComplete = false
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class StayAlignedRelaxedPoseConfig {
|
||||
|
||||
/**
|
||||
* Whether Stay Aligned should adjust the tracker yaws when the player is in this
|
||||
* pose.
|
||||
*/
|
||||
var enabled = false
|
||||
|
||||
/**
|
||||
* Angle between the upper leg yaw and the center yaw.
|
||||
*/
|
||||
var upperLegAngleInDeg = 0.0f
|
||||
|
||||
/**
|
||||
* Angle between the lower leg yaw and the center yaw.
|
||||
*/
|
||||
var lowerLegAngleInDeg = 0.0f
|
||||
|
||||
/**
|
||||
* Angle between the foot and the center yaw.
|
||||
*/
|
||||
var footAngleInDeg = 0.0f
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.jme3.math.FastMath
|
||||
|
||||
// handles the tap detection config
|
||||
// this involves the number of taps, the delay, and whether or not the feature is enabled
|
||||
// for each reset type
|
||||
class TapDetectionConfig {
|
||||
var yawResetDelay = 0.2f
|
||||
var fullResetDelay = 1.0f
|
||||
var mountingResetDelay = 1.0f
|
||||
var yawResetEnabled = true
|
||||
var fullResetEnabled = true
|
||||
var mountingResetEnabled = true
|
||||
var setupMode = false
|
||||
var yawResetTaps = 2
|
||||
set(yawResetTaps) {
|
||||
field = yawResetTaps.coerceIn(2, 10)
|
||||
}
|
||||
var fullResetTaps = 3
|
||||
set(fullResetTaps) {
|
||||
field = fullResetTaps.coerceIn(2, 10)
|
||||
}
|
||||
var mountingResetTaps = 3
|
||||
set(mountingResetTaps) {
|
||||
field = mountingResetTaps.coerceIn(2, 10)
|
||||
}
|
||||
var numberTrackersOverThreshold = 1
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import io.github.axisangles.ktmath.ObjectQuaternion
|
||||
|
||||
class TrackerConfig {
|
||||
var customName: String? = null
|
||||
var designation: String? = null
|
||||
|
||||
@get:JvmName("isHide")
|
||||
var hide: Boolean = false
|
||||
var adjustment: ObjectQuaternion? = null
|
||||
var mountingOrientation: ObjectQuaternion? = null
|
||||
var mountingResetOrientation: ObjectQuaternion? = null
|
||||
var allowDriftCompensation: Boolean? = null
|
||||
|
||||
/**
|
||||
* Only checked if [ServerConfig.useMagnetometerOnAllTrackers] enabled
|
||||
*/
|
||||
var shouldHaveMagEnabled: Boolean? = null
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(tracker: Tracker) {
|
||||
this.designation = if (tracker.trackerPosition != null) tracker.trackerPosition!!.designation else null
|
||||
this.customName = tracker.customName
|
||||
allowDriftCompensation = tracker.isImu()
|
||||
shouldHaveMagEnabled = tracker.isImu()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun toV2(v1: JsonNode, factory: JsonNodeFactory): JsonNode {
|
||||
val node = factory.objectNode()
|
||||
if (v1.has("customName")) node.set<JsonNode>("customName", v1["customName"])
|
||||
if (v1.has("designation")) node.set<JsonNode>("designation", v1["designation"])
|
||||
if (v1.has("hide")) node.set<JsonNode>("hide", v1["hide"])
|
||||
if (v1.has("mountingRotation")) node.set<JsonNode>("mountingRotation", v1["mountingRotation"])
|
||||
if (v1.has("adjustment")) node.set<JsonNode>("adjustment", v1["adjustment"])
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Tracker.config: TrackerConfig
|
||||
get() = VRServer.instance.configManager.vrConfig.getTracker(this)
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class TrackingChecklistConfig {
|
||||
val ignoredStepsIds: MutableList<Int> = mutableListOf()
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class VMCConfig : OSCConfig() {
|
||||
|
||||
// Anchor the tracking at the hip?
|
||||
var anchorHip = true
|
||||
|
||||
// JSON part of the VRM to be used
|
||||
var vrmJson: String? = null
|
||||
|
||||
// Mirror the tracking before sending it (turn left <=> turn right, left leg <=> right leg)
|
||||
var mirrorTracking = false
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
class VRCConfig {
|
||||
// List of fields ignored in vrc warnings - @see VRCConfigValidity
|
||||
val mutedWarnings: MutableList<String> = mutableListOf()
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.ser.std.StdKeySerializers
|
||||
import dev.slimevr.config.serializers.BooleanMapDeserializer
|
||||
import dev.slimevr.tracking.trackers.TrackerRole
|
||||
import java.util.*
|
||||
|
||||
class VRCOSCConfig : OSCConfig() {
|
||||
|
||||
// Which trackers' data to send
|
||||
@JsonDeserialize(using = BooleanMapDeserializer::class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class)
|
||||
var trackers: MutableMap<String, Boolean> = HashMap()
|
||||
|
||||
var oscqueryEnabled: Boolean = true
|
||||
|
||||
fun getOSCTrackerRole(role: TrackerRole, def: Boolean): Boolean = trackers.getOrDefault(role.name.lowercase(Locale.getDefault()), def)
|
||||
|
||||
fun setOSCTrackerRole(role: TrackerRole, `val`: Boolean) {
|
||||
trackers[role.name.lowercase(Locale.getDefault())] = `val`
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.ser.std.StdKeySerializers
|
||||
import com.github.jonpeterson.jackson.module.versioning.JsonVersionedModel
|
||||
import dev.slimevr.config.serializers.BridgeConfigMapDeserializer
|
||||
import dev.slimevr.config.serializers.TrackerConfigMapDeserializer
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerRole
|
||||
|
||||
@JsonVersionedModel(
|
||||
currentVersion = "15",
|
||||
defaultDeserializeToVersion = "15",
|
||||
toCurrentConverterClass = CurrentVRConfigConverter::class,
|
||||
)
|
||||
class VRConfig {
|
||||
val server: ServerConfig = ServerConfig()
|
||||
|
||||
val filters: FiltersConfig = FiltersConfig()
|
||||
|
||||
val driftCompensation: DriftCompensationConfig = DriftCompensationConfig()
|
||||
|
||||
val oscRouter: OSCConfig = OSCConfig()
|
||||
|
||||
val vrcOSC: VRCOSCConfig = VRCOSCConfig()
|
||||
|
||||
@get:JvmName("getVMC")
|
||||
val vmc: VMCConfig = VMCConfig()
|
||||
|
||||
val autoBone: AutoBoneConfig = AutoBoneConfig()
|
||||
|
||||
val keybindings: KeybindingsConfig = KeybindingsConfig()
|
||||
|
||||
val skeleton: SkeletonConfig = SkeletonConfig()
|
||||
|
||||
val legTweaks: LegTweaksConfig = LegTweaksConfig()
|
||||
|
||||
val tapDetection: TapDetectionConfig = TapDetectionConfig()
|
||||
|
||||
val resetsConfig: ResetsConfig = ResetsConfig()
|
||||
|
||||
val stayAlignedConfig = StayAlignedConfig()
|
||||
|
||||
val hidConfig = HIDConfig()
|
||||
|
||||
@JsonDeserialize(using = TrackerConfigMapDeserializer::class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class)
|
||||
private val trackers: MutableMap<String, TrackerConfig> = HashMap()
|
||||
|
||||
@JsonDeserialize(using = BridgeConfigMapDeserializer::class)
|
||||
@JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer::class)
|
||||
private val bridges: MutableMap<String, BridgeConfig> = HashMap()
|
||||
|
||||
val knownDevices: MutableSet<String> = mutableSetOf()
|
||||
|
||||
val overlay: OverlayConfig = OverlayConfig()
|
||||
|
||||
val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig()
|
||||
|
||||
val vrcConfig: VRCConfig = VRCConfig()
|
||||
|
||||
init {
|
||||
// Initialize default settings for OSC Router
|
||||
oscRouter.portIn = 9002
|
||||
oscRouter.portOut = 9000
|
||||
|
||||
// Initialize default settings for VRC OSC
|
||||
vrcOSC.portIn = 9001
|
||||
vrcOSC.portOut = 9000
|
||||
vrcOSC
|
||||
.setOSCTrackerRole(
|
||||
TrackerRole.WAIST,
|
||||
vrcOSC.getOSCTrackerRole(TrackerRole.WAIST, true),
|
||||
)
|
||||
vrcOSC
|
||||
.setOSCTrackerRole(
|
||||
TrackerRole.LEFT_FOOT,
|
||||
vrcOSC.getOSCTrackerRole(TrackerRole.WAIST, true),
|
||||
)
|
||||
vrcOSC
|
||||
.setOSCTrackerRole(
|
||||
TrackerRole.RIGHT_FOOT,
|
||||
vrcOSC.getOSCTrackerRole(TrackerRole.WAIST, true),
|
||||
)
|
||||
|
||||
// Initialize default settings for VMC
|
||||
vmc.portIn = 39540
|
||||
vmc.portOut = 39539
|
||||
}
|
||||
|
||||
fun getTrackers(): Map<String, TrackerConfig> = trackers
|
||||
|
||||
fun getBridges(): Map<String, BridgeConfig> = bridges
|
||||
|
||||
fun hasTrackerByName(name: String): Boolean = trackers.containsKey(name)
|
||||
|
||||
fun getTracker(tracker: Tracker): TrackerConfig {
|
||||
var config = trackers[tracker.name]
|
||||
if (config == null) {
|
||||
config = TrackerConfig(tracker)
|
||||
trackers[tracker.name] = config
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
fun readTrackerConfig(tracker: Tracker) {
|
||||
if (tracker.userEditable) {
|
||||
val config = getTracker(tracker)
|
||||
tracker.readConfig(config)
|
||||
if (tracker.isImu()) tracker.resetsHandler.readDriftCompensationConfig(driftCompensation)
|
||||
tracker.resetsHandler.readResetConfig(resetsConfig)
|
||||
if (tracker.allowReset) {
|
||||
tracker.saveMountingResetOrientation(config)
|
||||
}
|
||||
if (tracker.allowFiltering) {
|
||||
tracker
|
||||
.filteringHandler
|
||||
.readFilteringConfig(filters, tracker.getRotation())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writeTrackerConfig(tracker: Tracker?) {
|
||||
if (tracker?.userEditable == true) {
|
||||
val tc = getTracker(tracker)
|
||||
tracker.writeConfig(tc)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBridge(bridgeKey: String): BridgeConfig {
|
||||
var config = bridges[bridgeKey]
|
||||
if (config == null) {
|
||||
config = BridgeConfig()
|
||||
bridges[bridgeKey] = config
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
fun isKnownDevice(mac: String?): Boolean = knownDevices.contains(mac)
|
||||
|
||||
fun addKnownDevice(mac: String): Boolean = knownDevices.add(mac)
|
||||
|
||||
fun forgetKnownDevice(mac: String): Boolean = knownDevices.remove(mac)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
/**
|
||||
* This class allows the use of the utility super class MapDeserializer that
|
||||
* takes the Value of a map as its Generic parameter. It is so you can use that
|
||||
* class in a @JsonDeserialize annotation on the Map field inside the config
|
||||
* instance
|
||||
*
|
||||
* @see dev.slimevr.config.VRConfig
|
||||
*/
|
||||
public class BooleanMapDeserializer extends MapDeserializer<Boolean> {
|
||||
public BooleanMapDeserializer() {
|
||||
super(Boolean.class);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
import dev.slimevr.config.BridgeConfig;
|
||||
|
||||
|
||||
/**
|
||||
* This class allows the use of the utility super class MapDeserializer that
|
||||
* takes the Value of a map as its Generic parameter. It is so you can use that
|
||||
* class in a @JsonDeserialize annotation on the Map field inside the config
|
||||
* instance
|
||||
*
|
||||
* @see dev.slimevr.config.VRConfig
|
||||
*/
|
||||
public class BridgeConfigMapDeserializer extends MapDeserializer<BridgeConfig> {
|
||||
public BridgeConfigMapDeserializer() {
|
||||
super(BridgeConfig.class);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
/**
|
||||
* This class allows the use of the utility super class MapDeserializer that
|
||||
* takes the Value of a map as its Generic parameter. It is so you can use that
|
||||
* class in a @JsonDeserialize annotation on the Map field inside the config
|
||||
* instance
|
||||
*
|
||||
* @see dev.slimevr.config.VRConfig
|
||||
*/
|
||||
public class FloatMapDeserializer extends MapDeserializer<Float> {
|
||||
public FloatMapDeserializer() {
|
||||
super(Float.class);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.type.MapType;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
/**
|
||||
* This class is a utility class that allows to write Map serializers easily to
|
||||
* be used in the VRConfig (@see {@link dev.slimevr.config.VRConfig})
|
||||
*
|
||||
* @see BooleanMapDeserializer to see how it is used
|
||||
*/
|
||||
public abstract class MapDeserializer<T> extends JsonDeserializer<HashMap<String, T>> {
|
||||
|
||||
private final Class<T> valueClass;
|
||||
|
||||
public MapDeserializer(Class<T> valueClass) {
|
||||
super();
|
||||
this.valueClass = valueClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<String, T> deserialize(JsonParser p, DeserializationContext dc)
|
||||
throws IOException {
|
||||
TypeFactory typeFactory = dc.getTypeFactory();
|
||||
MapType mapType = typeFactory
|
||||
.constructMapType(HashMap.class, String.class, valueClass);
|
||||
return dc.readValue(p, mapType);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import io.github.axisangles.ktmath.ObjectQuaternion;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class QuaternionDeserializer extends JsonDeserializer<ObjectQuaternion> {
|
||||
@Override
|
||||
public ObjectQuaternion deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
throws IOException, JacksonException {
|
||||
JsonNode node = p.getCodec().readTree(p);
|
||||
|
||||
return new ObjectQuaternion(
|
||||
(float) node.get("w").asDouble(),
|
||||
(float) node.get("x").asDouble(),
|
||||
(float) node.get("y").asDouble(),
|
||||
(float) node.get("z").asDouble()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import io.github.axisangles.ktmath.ObjectQuaternion;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class QuaternionSerializer extends JsonSerializer<ObjectQuaternion> {
|
||||
|
||||
@Override
|
||||
public void serialize(ObjectQuaternion value, JsonGenerator gen, SerializerProvider serializers)
|
||||
throws IOException {
|
||||
gen.writeStartObject();
|
||||
gen.writeNumberField("x", value.getX());
|
||||
gen.writeNumberField("y", value.getY());
|
||||
gen.writeNumberField("z", value.getZ());
|
||||
gen.writeNumberField("w", value.getW());
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package dev.slimevr.config.serializers;
|
||||
|
||||
import dev.slimevr.config.TrackerConfig;
|
||||
|
||||
|
||||
/**
|
||||
* This class allows the use of the utility super class MapDeserializer that
|
||||
* takes the Value of a map as its Generic parameter. It is so you can use that
|
||||
* class in a @JsonDeserialize annotation on the Map field inside the config
|
||||
* instance
|
||||
*
|
||||
* @see dev.slimevr.config.VRConfig
|
||||
*/
|
||||
public class TrackerConfigMapDeserializer extends MapDeserializer<TrackerConfig> {
|
||||
public TrackerConfigMapDeserializer() {
|
||||
super(TrackerConfig.class);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package dev.slimevr.filtering;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
* If you use this code, please consider notifying isak at du-preez dot com with
|
||||
* a brief description of your application.
|
||||
* <p>
|
||||
* This is free and unencumbered software released into the public domain.
|
||||
* Anyone is free to copy, modify, publish, use, compile, sell, or distribute
|
||||
* this software, either in source code form or as a compiled binary, for any
|
||||
* purpose, commercial or non-commercial, and by any means.
|
||||
*/
|
||||
|
||||
public class CircularArrayList<E> extends AbstractList<E> implements RandomAccess {
|
||||
|
||||
private final int n; // buffer length
|
||||
private final List<E> buf; // a List implementing RandomAccess
|
||||
private int head = 0;
|
||||
private int tail = 0;
|
||||
|
||||
public CircularArrayList(int capacity) {
|
||||
n = capacity + 1;
|
||||
buf = new ArrayList<>(Collections.nCopies(n, null));
|
||||
}
|
||||
|
||||
public int capacity() {
|
||||
return n - 1;
|
||||
}
|
||||
|
||||
private int wrapIndex(int i) {
|
||||
int m = i % n;
|
||||
if (m < 0) { // java modulus can be negative
|
||||
m += n;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// This method is O(n) but will never be called if the
|
||||
// CircularArrayList is used in its typical/intended role.
|
||||
private void shiftBlock(int startIndex, int endIndex) {
|
||||
assert (endIndex > startIndex);
|
||||
for (int i = endIndex - 1; i >= startIndex; i--) {
|
||||
set(i + 1, get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return tail - head + (tail < head ? n : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E get(int i) {
|
||||
if (i < 0 || i >= size()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
return buf.get(wrapIndex(head + i));
|
||||
}
|
||||
|
||||
public E getLatest() {
|
||||
return buf.get(wrapIndex(head + size() - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(int i, E e) {
|
||||
if (i < 0 || i >= size()) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
return buf.set(wrapIndex(head + i), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int i, E e) {
|
||||
int s = size();
|
||||
if (s == n - 1) {
|
||||
throw new IllegalStateException(
|
||||
"CircularArrayList is filled to capacity. "
|
||||
+ "(You may want to remove from front"
|
||||
+ " before adding more to back.)"
|
||||
);
|
||||
}
|
||||
if (i < 0 || i > s) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
tail = wrapIndex(tail + 1);
|
||||
if (i < s) {
|
||||
shiftBlock(i, s);
|
||||
}
|
||||
set(i, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E remove(int i) {
|
||||
int s = size();
|
||||
if (i < 0 || i >= s) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
E e = get(i);
|
||||
if (i > 0) {
|
||||
shiftBlock(0, i);
|
||||
}
|
||||
head = wrapIndex(head + 1);
|
||||
return e;
|
||||
}
|
||||
|
||||
public E removeLast() {
|
||||
int s = size();
|
||||
if (0 == s) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
E e = get(0);
|
||||
head = wrapIndex(head + 1);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package dev.slimevr.filtering
|
||||
|
||||
import com.jme3.system.NanoTimer
|
||||
import dev.slimevr.VRServer
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY
|
||||
|
||||
// influences the range of smoothFactor.
|
||||
private const val SMOOTH_MULTIPLIER = 42f
|
||||
private const val SMOOTH_MIN = 11f
|
||||
|
||||
// influences the range of predictFactor
|
||||
private const val PREDICT_MULTIPLIER = 15f
|
||||
private const val PREDICT_MIN = 10f
|
||||
|
||||
// how many past rotations are used for prediction.
|
||||
private const val PREDICT_BUFFER = 6
|
||||
|
||||
class QuaternionMovingAverage(
|
||||
val type: TrackerFilters,
|
||||
var amount: Float = 0f,
|
||||
initialRotation: Quaternion = IDENTITY,
|
||||
) {
|
||||
var filteredQuaternion = IDENTITY
|
||||
var filteringImpact = 0f
|
||||
private var smoothFactor = 0f
|
||||
private var predictFactor = 0f
|
||||
private var rotBuffer: CircularArrayList<Quaternion>? = null
|
||||
private var latestQuaternion = IDENTITY
|
||||
private var smoothingQuaternion = IDENTITY
|
||||
private val fpsTimer = if (VRServer.instanceInitialized) VRServer.instance.fpsTimer else NanoTimer()
|
||||
private var timeSinceUpdate = 0f
|
||||
|
||||
init {
|
||||
// amount should range from 0 to 1.
|
||||
// GUI should clamp it from 0.01 (1%) or 0.1 (10%)
|
||||
// to 1 (100%).
|
||||
amount = amount.coerceAtLeast(0f)
|
||||
if (type == TrackerFilters.SMOOTHING) {
|
||||
// lower smoothFactor = more smoothing
|
||||
smoothFactor = SMOOTH_MULTIPLIER * (1 - amount.coerceAtMost(1f)) + SMOOTH_MIN
|
||||
// Totally a hack
|
||||
if (amount > 1) {
|
||||
smoothFactor /= amount
|
||||
}
|
||||
}
|
||||
if (type == TrackerFilters.PREDICTION) {
|
||||
// higher predictFactor = more prediction
|
||||
predictFactor = PREDICT_MULTIPLIER * amount + PREDICT_MIN
|
||||
rotBuffer = CircularArrayList(PREDICT_BUFFER)
|
||||
}
|
||||
|
||||
// We have no reference at the start, so just use the initial rotation
|
||||
resetQuats(initialRotation, initialRotation)
|
||||
}
|
||||
|
||||
// Runs at up to 1000hz. We use a timer to make it framerate-independent
|
||||
// since it runs a bit below 1000hz in practice.
|
||||
@Synchronized
|
||||
fun update() {
|
||||
if (type == TrackerFilters.PREDICTION) {
|
||||
val rotBuf = rotBuffer
|
||||
if (rotBuf != null && rotBuf.isNotEmpty()) {
|
||||
// Applies the past rotations to the current rotation
|
||||
val predictRot = rotBuf.fold(latestQuaternion) { buf, rot -> buf * rot }
|
||||
|
||||
// Calculate how much to slerp
|
||||
// Limit slerp by a reasonable amount so low TPS doesn't break tracking
|
||||
val amt = (predictFactor * fpsTimer.timePerFrame).coerceAtMost(1f)
|
||||
|
||||
// Slerps the target rotation to that predicted rotation by amt
|
||||
filteredQuaternion = filteredQuaternion.interpQ(predictRot, amt)
|
||||
}
|
||||
} else if (type == TrackerFilters.SMOOTHING) {
|
||||
// Make it framerate-independent
|
||||
timeSinceUpdate += fpsTimer.timePerFrame
|
||||
|
||||
// Calculate the slerp factor based off the smoothFactor and smoothingCounter
|
||||
// limit to 1 to not overshoot
|
||||
val amt = (smoothFactor * timeSinceUpdate).coerceAtMost(1f)
|
||||
|
||||
// Smooth towards the target rotation by the slerp factor
|
||||
filteredQuaternion = smoothingQuaternion.interpQ(latestQuaternion, amt)
|
||||
}
|
||||
|
||||
filteringImpact = latestQuaternion.angleToR(filteredQuaternion)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addQuaternion(q: Quaternion) {
|
||||
val oldQ = latestQuaternion
|
||||
val newQ = q.twinNearest(oldQ)
|
||||
latestQuaternion = newQ
|
||||
|
||||
if (type == TrackerFilters.PREDICTION) {
|
||||
if (rotBuffer!!.size == rotBuffer!!.capacity()) {
|
||||
rotBuffer?.removeLast()
|
||||
}
|
||||
|
||||
// Gets and stores the rotation between the last 2 quaternions
|
||||
rotBuffer?.add(oldQ.inv().times(newQ))
|
||||
} else if (type == TrackerFilters.SMOOTHING) {
|
||||
timeSinceUpdate = 0f
|
||||
smoothingQuaternion = filteredQuaternion
|
||||
} else {
|
||||
// No filtering; just keep track of rotations (for going over 180 degrees)
|
||||
filteredQuaternion = newQ
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the quaternion space of [q] to the [reference] and sets the latest
|
||||
* [filteredQuaternion] immediately
|
||||
*/
|
||||
@Synchronized
|
||||
fun resetQuats(q: Quaternion, reference: Quaternion) {
|
||||
// Assume a rotation within 180 degrees of the reference
|
||||
// TODO: Currently the reference is the headset, this restricts all trackers to
|
||||
// have at most a 180 degree rotation from the HMD during a reset, we can
|
||||
// probably do better using a hierarchy
|
||||
val rot = q.twinNearest(reference)
|
||||
rotBuffer?.clear()
|
||||
latestQuaternion = rot
|
||||
filteredQuaternion = rot
|
||||
addQuaternion(rot)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package dev.slimevr.filtering
|
||||
|
||||
import java.util.*
|
||||
|
||||
enum class TrackerFilters(val id: Int, val configKey: String) {
|
||||
NONE(0, "none"),
|
||||
SMOOTHING(1, "smoothing"),
|
||||
PREDICTION(2, "prediction"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private val byConfigkey: MutableMap<String, TrackerFilters> = HashMap()
|
||||
|
||||
init {
|
||||
for (configVal in values()) {
|
||||
byConfigkey[configVal.configKey.lowercase(Locale.getDefault())] =
|
||||
configVal
|
||||
}
|
||||
}
|
||||
|
||||
val values = values()
|
||||
|
||||
@JvmStatic
|
||||
fun fromId(id: Int): TrackerFilters? {
|
||||
for (filter in values) {
|
||||
if (filter.id == id) return filter
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getByConfigkey(configKey: String?): TrackerFilters? = if (configKey == null) null else byConfigkey[configKey.lowercase(Locale.getDefault())]
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
import com.mayakapps.kache.InMemoryKache
|
||||
import com.mayakapps.kache.KacheStrategy
|
||||
import dev.llelievr.espflashkotlin.Flasher
|
||||
import dev.llelievr.espflashkotlin.FlashingProgressListener
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.serial.ProvisioningListener
|
||||
import dev.slimevr.serial.ProvisioningStatus
|
||||
import dev.slimevr.serial.SerialPort
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus
|
||||
import dev.slimevr.tracking.trackers.TrackerStatusListener
|
||||
import dev.slimevr.tracking.trackers.udp.UDPDevice
|
||||
import io.eiren.util.logging.LogManager
|
||||
import kotlinx.coroutines.*
|
||||
import solarxr_protocol.rpc.FirmwarePartT
|
||||
import solarxr_protocol.rpc.FirmwareUpdateRequestT
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.stream.Collectors
|
||||
import kotlin.concurrent.scheduleAtFixedRate
|
||||
|
||||
data class DownloadedFirmwarePart(
|
||||
val firmware: ByteArray,
|
||||
val offset: Long?,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DownloadedFirmwarePart
|
||||
|
||||
if (!firmware.contentEquals(other.firmware)) return false
|
||||
if (offset != other.offset) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = firmware.contentHashCode()
|
||||
result = 31 * result + (offset?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class FirmwareUpdateHandler(private val server: VRServer) :
|
||||
TrackerStatusListener,
|
||||
ProvisioningListener,
|
||||
SerialRebootListener {
|
||||
|
||||
private val updateTickTimer = Timer("StatusUpdateTimer")
|
||||
private val runningJobs: MutableList<Job> = CopyOnWriteArrayList()
|
||||
private val watchRestartQueue: MutableList<Pair<UpdateDeviceId<*>, () -> Unit>> =
|
||||
CopyOnWriteArrayList()
|
||||
private val updatingDevicesStatus: MutableMap<UpdateDeviceId<*>, UpdateStatusEvent<*>> =
|
||||
ConcurrentHashMap()
|
||||
private val listeners: MutableList<FirmwareUpdateListener> = CopyOnWriteArrayList()
|
||||
private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob())
|
||||
private var clearJob: Deferred<Unit>? = null
|
||||
|
||||
private var serialRebootHandler: SerialRebootHandler = SerialRebootHandler(watchRestartQueue, server, this)
|
||||
|
||||
fun addListener(channel: FirmwareUpdateListener) {
|
||||
listeners.add(channel)
|
||||
}
|
||||
|
||||
fun removeListener(channel: FirmwareUpdateListener) {
|
||||
listeners.removeIf { channel == it }
|
||||
}
|
||||
|
||||
init {
|
||||
server.addTrackerStatusListener(this)
|
||||
server.provisioningHandler.addListener(this)
|
||||
server.serialHandler.addListener(serialRebootHandler)
|
||||
|
||||
this.updateTickTimer.scheduleAtFixedRate(0, 1000) {
|
||||
checkUpdateTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startOtaUpdate(
|
||||
part: DownloadedFirmwarePart,
|
||||
deviceId: UpdateDeviceId<Int>,
|
||||
): Unit = suspendCancellableCoroutine { c ->
|
||||
val udpDevice: UDPDevice? =
|
||||
(server.deviceManager.devices.find { device -> device is UDPDevice && device.id == deviceId.id }) as UDPDevice?
|
||||
|
||||
if (udpDevice == null) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND,
|
||||
),
|
||||
)
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
val task = OTAUpdateTask(
|
||||
part.firmware,
|
||||
deviceId,
|
||||
udpDevice.ipAddress,
|
||||
::onStatusChange,
|
||||
)
|
||||
c.invokeOnCancellation {
|
||||
task.cancel()
|
||||
}
|
||||
task.run()
|
||||
}
|
||||
|
||||
private fun startSerialUpdate(
|
||||
firmwares: Array<DownloadedFirmwarePart>,
|
||||
deviceId: UpdateDeviceId<String>,
|
||||
needManualReboot: Boolean,
|
||||
ssid: String,
|
||||
password: String,
|
||||
) {
|
||||
// Can't use .toList() on Android
|
||||
val serialPort = this.server.serialHandler.knownPorts.collect(Collectors.toList())
|
||||
.find { port -> deviceId.id == port.portLocation }
|
||||
|
||||
if (serialPort == null) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val flashingHandler = this.server.serialFlashingHandler
|
||||
|
||||
if (flashingHandler == null) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_UNSUPPORTED_METHOD,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val flasher = Flasher(flashingHandler)
|
||||
|
||||
for (part in firmwares) {
|
||||
if (part.offset == null) {
|
||||
error("Offset is empty")
|
||||
}
|
||||
flasher.addBin(part.firmware, part.offset.toInt())
|
||||
}
|
||||
|
||||
flasher.addProgressListener(object : FlashingProgressListener {
|
||||
override fun progress(progress: Float) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.UPLOADING,
|
||||
(progress * 100).toInt(),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.SYNCING_WITH_MCU,
|
||||
),
|
||||
)
|
||||
flasher.flash(serialPort)
|
||||
if (needManualReboot) {
|
||||
if (watchRestartQueue.find { it.first == deviceId } != null) {
|
||||
LogManager.info("[FirmwareUpdateHandler] Device is already updating, skipping")
|
||||
}
|
||||
|
||||
onStatusChange(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.NEED_MANUAL_REBOOT))
|
||||
server.serialHandler.openSerial(deviceId.id, false)
|
||||
watchRestartQueue.add(
|
||||
Pair(deviceId) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.REBOOTING,
|
||||
),
|
||||
)
|
||||
server.provisioningHandler.start(
|
||||
ssid,
|
||||
password,
|
||||
serialPort.portLocation,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
onStatusChange(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.REBOOTING))
|
||||
server.provisioningHandler.start(ssid, password, serialPort.portLocation)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("[FirmwareUpdateHandler] Upload failed", e)
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_UPLOAD_FAILED,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun queueFirmwareUpdate(
|
||||
request: FirmwareUpdateRequestT,
|
||||
deviceId: UpdateDeviceId<*>,
|
||||
) = mainScope.launch {
|
||||
val method = FirmwareUpdateMethod.getById(request.method.type) ?: error("Unknown method")
|
||||
|
||||
clearJob?.await()
|
||||
if (method == FirmwareUpdateMethod.OTA) {
|
||||
if (watchRestartQueue.find { it.first == deviceId } != null) {
|
||||
LogManager.info("[FirmwareUpdateHandler] Device is already updating, skipping")
|
||||
}
|
||||
|
||||
val udpDevice: UDPDevice? =
|
||||
(server.deviceManager.devices.find { device -> device is UDPDevice && device.id == deviceId.id }) as UDPDevice?
|
||||
if (udpDevice === null) {
|
||||
error("invalid state - device does not exist")
|
||||
}
|
||||
|
||||
if (udpDevice.protocolVersion <= 20) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.NEED_MANUAL_REBOOT,
|
||||
),
|
||||
)
|
||||
watchRestartQueue.add(
|
||||
Pair(deviceId) {
|
||||
mainScope.launch {
|
||||
startFirmwareUpdateJob(
|
||||
request,
|
||||
deviceId,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
startFirmwareUpdateJob(
|
||||
request,
|
||||
deviceId,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (updatingDevicesStatus[deviceId] != null) {
|
||||
LogManager.info("[FirmwareUpdateHandler] Device is already updating, skipping")
|
||||
return@launch
|
||||
}
|
||||
|
||||
startFirmwareUpdateJob(
|
||||
request,
|
||||
deviceId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelUpdates() {
|
||||
val oldClearJob = clearJob
|
||||
clearJob = mainScope.async {
|
||||
oldClearJob?.await()
|
||||
watchRestartQueue.clear()
|
||||
runningJobs.forEach { it.cancelAndJoin() }
|
||||
runningJobs.clear()
|
||||
LogManager.info("[FirmwareUpdateHandler] Update jobs canceled")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFirmwareParts(request: FirmwareUpdateRequestT): ArrayList<FirmwarePartT> {
|
||||
val parts = ArrayList<FirmwarePartT>()
|
||||
val method = FirmwareUpdateMethod.getById(request.method.type) ?: error("Unknown method")
|
||||
when (method) {
|
||||
FirmwareUpdateMethod.OTA -> {
|
||||
val updateReq = request.method.asOTAFirmwareUpdate()
|
||||
parts.add(updateReq.firmwarePart)
|
||||
}
|
||||
|
||||
FirmwareUpdateMethod.SERIAL -> {
|
||||
val updateReq = request.method.asSerialFirmwareUpdate()
|
||||
parts.addAll(updateReq.firmwarePart)
|
||||
}
|
||||
|
||||
FirmwareUpdateMethod.NONE -> error("Method should not be NONE")
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
private suspend fun startFirmwareUpdateJob(
|
||||
request: FirmwareUpdateRequestT,
|
||||
deviceId: UpdateDeviceId<*>,
|
||||
) = coroutineScope {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.DOWNLOADING,
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
val toDownloadParts = getFirmwareParts(request)
|
||||
val firmwareParts = try {
|
||||
withTimeoutOrNull(30_000) {
|
||||
toDownloadParts.map {
|
||||
val firmware = downloadFirmware(it.url, it.digest)
|
||||
DownloadedFirmwarePart(
|
||||
firmware,
|
||||
it.offset,
|
||||
)
|
||||
}.toTypedArray()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED,
|
||||
),
|
||||
)
|
||||
LogManager.severe("[FirmwareUpdateHandler] Unable to download firmware", e)
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
val job = launch {
|
||||
withTimeout(2 * 60 * 1000) {
|
||||
if (firmwareParts.isNullOrEmpty()) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED,
|
||||
),
|
||||
)
|
||||
return@withTimeout
|
||||
}
|
||||
|
||||
val method = FirmwareUpdateMethod.getById(request.method.type) ?: error("Unknown method")
|
||||
when (method) {
|
||||
FirmwareUpdateMethod.NONE -> error("unsupported method")
|
||||
|
||||
FirmwareUpdateMethod.OTA -> {
|
||||
if (deviceId.id !is Int) {
|
||||
error("invalid state, the device id is not an int")
|
||||
}
|
||||
if (firmwareParts.size > 1) {
|
||||
error("invalid state, ota only use one firmware file")
|
||||
}
|
||||
startOtaUpdate(
|
||||
firmwareParts.first(),
|
||||
UpdateDeviceId(
|
||||
FirmwareUpdateMethod.OTA,
|
||||
deviceId.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
FirmwareUpdateMethod.SERIAL -> {
|
||||
val req = request.method.asSerialFirmwareUpdate()
|
||||
if (deviceId.id !is String) {
|
||||
error("invalid state, the device id is not a string")
|
||||
}
|
||||
startSerialUpdate(
|
||||
firmwareParts,
|
||||
UpdateDeviceId(
|
||||
FirmwareUpdateMethod.SERIAL,
|
||||
deviceId.id,
|
||||
),
|
||||
req.needManualReboot,
|
||||
req.ssid,
|
||||
req.password,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
runningJobs.add(job)
|
||||
} catch (e: Exception) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
if (e is TimeoutCancellationException) FirmwareUpdateStatus.ERROR_TIMEOUT else FirmwareUpdateStatus.ERROR_UNKNOWN,
|
||||
),
|
||||
)
|
||||
if (e !is TimeoutCancellationException) {
|
||||
LogManager.severe("[FirmwareUpdateHandler] Update process timed out", e)
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@coroutineScope
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> onStatusChange(event: UpdateStatusEvent<T>) {
|
||||
this.updatingDevicesStatus[event.deviceId] = event
|
||||
|
||||
if (event.status == FirmwareUpdateStatus.DONE || event.status.isError()) {
|
||||
this.updatingDevicesStatus.remove(event.deviceId)
|
||||
|
||||
// we remove the device from the restart queue
|
||||
val queuedDevice = watchRestartQueue.find { it.first.id == event.deviceId }
|
||||
if (queuedDevice != null) {
|
||||
watchRestartQueue.remove(queuedDevice)
|
||||
if (event.deviceId.type == FirmwareUpdateMethod.SERIAL && server.serialHandler.isConnected) {
|
||||
server.serialHandler.closeSerial()
|
||||
}
|
||||
}
|
||||
|
||||
// We make sure to stop the provisioning routine if the tracker is done
|
||||
// flashing
|
||||
if (event.deviceId.type == FirmwareUpdateMethod.SERIAL) {
|
||||
this.server.provisioningHandler.stop()
|
||||
}
|
||||
}
|
||||
listeners.forEach { l -> l.onUpdateStatusChange(event) }
|
||||
}
|
||||
|
||||
private fun checkUpdateTimeout() {
|
||||
updatingDevicesStatus.forEach { (id, device) ->
|
||||
// if more than 30s between two events, consider the update as stuck
|
||||
// We do not timeout on the Downloading step as it has it own timeout
|
||||
// We do not timeout on the Done step as it is the end of the update process
|
||||
if (!device.status.isError() &&
|
||||
!intArrayOf(FirmwareUpdateStatus.DONE.id, FirmwareUpdateStatus.DOWNLOADING.id).contains(device.status.id) &&
|
||||
System.currentTimeMillis() - device.time > 30 * 1000
|
||||
) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
id,
|
||||
FirmwareUpdateStatus.ERROR_TIMEOUT,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this only works for OTA trackers as the device id
|
||||
// only exists when the usb connection is created
|
||||
override fun onTrackerStatusChanged(
|
||||
tracker: Tracker,
|
||||
oldStatus: TrackerStatus,
|
||||
newStatus: TrackerStatus,
|
||||
) {
|
||||
val device = tracker.device
|
||||
if (device !is UDPDevice) return
|
||||
|
||||
if (oldStatus == TrackerStatus.DISCONNECTED && newStatus == TrackerStatus.OK) {
|
||||
val queuedDevice = watchRestartQueue.find { it.first.id == device.id }
|
||||
|
||||
if (queuedDevice != null) {
|
||||
queuedDevice.second() // we start the queued update task
|
||||
watchRestartQueue.remove(queuedDevice) // then we remove it from the queue
|
||||
return
|
||||
}
|
||||
|
||||
// We can only filter OTA method here as the device id is only provided when using Wi-Fi
|
||||
val deviceStatusKey =
|
||||
updatingDevicesStatus.keys.find { it.type == FirmwareUpdateMethod.OTA && it.id == device.id }
|
||||
?: return
|
||||
val updateStatus = updatingDevicesStatus[deviceStatusKey] ?: return
|
||||
// We check for the reconnection of the tracker, once the tracker reconnected we notify the user that the update is completed
|
||||
if (updateStatus.status == FirmwareUpdateStatus.REBOOTING) {
|
||||
onStatusChange(
|
||||
UpdateStatusEvent(
|
||||
updateStatus.deviceId,
|
||||
FirmwareUpdateStatus.DONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvisioningStatusChange(
|
||||
status: ProvisioningStatus,
|
||||
port: SerialPort?,
|
||||
) {
|
||||
fun update(s: FirmwareUpdateStatus) {
|
||||
val deviceStatusKey =
|
||||
updatingDevicesStatus.keys.find { it.type == FirmwareUpdateMethod.SERIAL && it.id == port?.portLocation }
|
||||
?: return
|
||||
val updateStatus = updatingDevicesStatus[deviceStatusKey] ?: return
|
||||
onStatusChange(UpdateStatusEvent(updateStatus.deviceId, s))
|
||||
}
|
||||
|
||||
when (status) {
|
||||
ProvisioningStatus.PROVISIONING -> update(FirmwareUpdateStatus.PROVISIONING)
|
||||
ProvisioningStatus.DONE -> update(FirmwareUpdateStatus.DONE)
|
||||
ProvisioningStatus.CONNECTION_ERROR, ProvisioningStatus.COULD_NOT_FIND_SERVER -> update(FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSerialDeviceReconnect(deviceHandle: Pair<UpdateDeviceId<*>, () -> Unit>) {
|
||||
deviceHandle.second()
|
||||
watchRestartQueue.remove(deviceHandle)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFirmware(url: String, expectedDigest: String): ByteArray {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
val chunk = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
val stream: InputStream = URL(url).openStream()
|
||||
while (stream.read(chunk).also { bytesRead = it } > 0) {
|
||||
outputStream.write(chunk, 0, bytesRead)
|
||||
}
|
||||
|
||||
val downloadedData = outputStream.toByteArray()
|
||||
|
||||
if (!verifyChecksum(downloadedData, expectedDigest)) {
|
||||
error("Checksum verification failed for $url")
|
||||
}
|
||||
|
||||
return downloadedData
|
||||
}
|
||||
|
||||
fun verifyChecksum(data: ByteArray, expectedDigest: String): Boolean {
|
||||
val parts = expectedDigest.split(":", limit = 2)
|
||||
if (parts.size != 2) {
|
||||
error("Invalid digest format. Expected 'algorithm:hash' got $expectedDigest")
|
||||
}
|
||||
|
||||
val algorithm = parts[0].uppercase().replace("-", "")
|
||||
val expectedHash = parts[1].lowercase()
|
||||
|
||||
val messageDigest = MessageDigest.getInstance(algorithm)
|
||||
val actualHash = messageDigest.digest(data).joinToString("") {
|
||||
"%02x".format(it)
|
||||
}
|
||||
|
||||
return actualHash == expectedHash
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
interface FirmwareUpdateListener {
|
||||
fun onUpdateStatusChange(event: UpdateStatusEvent<*>)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
enum class FirmwareUpdateMethod(val id: Byte) {
|
||||
NONE(solarxr_protocol.rpc.FirmwareUpdateMethod.NONE),
|
||||
OTA(solarxr_protocol.rpc.FirmwareUpdateMethod.OTAFirmwareUpdate),
|
||||
SERIAL(solarxr_protocol.rpc.FirmwareUpdateMethod.SerialFirmwareUpdate),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun getById(id: Byte): FirmwareUpdateMethod? = byId[id]
|
||||
}
|
||||
}
|
||||
|
||||
private val byId = FirmwareUpdateMethod.entries.associateBy { it.id }
|
||||
@@ -1,29 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
enum class FirmwareUpdateStatus(val id: Int) {
|
||||
DOWNLOADING(solarxr_protocol.rpc.FirmwareUpdateStatus.DOWNLOADING),
|
||||
AUTHENTICATING(solarxr_protocol.rpc.FirmwareUpdateStatus.AUTHENTICATING),
|
||||
UPLOADING(solarxr_protocol.rpc.FirmwareUpdateStatus.UPLOADING),
|
||||
SYNCING_WITH_MCU(solarxr_protocol.rpc.FirmwareUpdateStatus.SYNCING_WITH_MCU),
|
||||
REBOOTING(solarxr_protocol.rpc.FirmwareUpdateStatus.REBOOTING),
|
||||
NEED_MANUAL_REBOOT(solarxr_protocol.rpc.FirmwareUpdateStatus.NEED_MANUAL_REBOOT),
|
||||
PROVISIONING(solarxr_protocol.rpc.FirmwareUpdateStatus.PROVISIONING),
|
||||
DONE(solarxr_protocol.rpc.FirmwareUpdateStatus.DONE),
|
||||
ERROR_DEVICE_NOT_FOUND(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_DEVICE_NOT_FOUND),
|
||||
ERROR_TIMEOUT(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_TIMEOUT),
|
||||
ERROR_DOWNLOAD_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_DOWNLOAD_FAILED),
|
||||
ERROR_AUTHENTICATION_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED),
|
||||
ERROR_UPLOAD_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_UPLOAD_FAILED),
|
||||
ERROR_PROVISIONING_FAILED(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_PROVISIONING_FAILED),
|
||||
ERROR_UNSUPPORTED_METHOD(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_UNSUPPORTED_METHOD),
|
||||
ERROR_UNKNOWN(solarxr_protocol.rpc.FirmwareUpdateStatus.ERROR_UNKNOWN),
|
||||
;
|
||||
|
||||
fun isError(): Boolean = id in ERROR_DEVICE_NOT_FOUND.id..ERROR_UNKNOWN.id
|
||||
|
||||
companion object {
|
||||
fun getById(id: Int): FirmwareUpdateStatus? = byId[id]
|
||||
}
|
||||
}
|
||||
|
||||
private val byId = FirmwareUpdateStatus.entries.associateBy { it.id }
|
||||
@@ -1,205 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
import io.eiren.util.logging.LogManager
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import kotlin.math.min
|
||||
|
||||
class OTAUpdateTask(
|
||||
private val firmware: ByteArray,
|
||||
private val deviceId: UpdateDeviceId<Int>,
|
||||
private val deviceIp: InetAddress,
|
||||
private val statusCallback: Consumer<UpdateStatusEvent<Int>>,
|
||||
) {
|
||||
private val receiveBuffer: ByteArray = ByteArray(38)
|
||||
var socketServer: ServerSocket? = null
|
||||
var uploadSocket: Socket? = null
|
||||
var authSocket: DatagramSocket? = null
|
||||
var canceled: Boolean = false
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class)
|
||||
private fun bytesToMd5(bytes: ByteArray): String {
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
md5.update(bytes)
|
||||
val digest = md5.digest()
|
||||
val md5str = StringBuilder()
|
||||
for (b in digest) {
|
||||
md5str.append(String.format("%02x", b))
|
||||
}
|
||||
return md5str.toString()
|
||||
}
|
||||
|
||||
private fun authenticate(localPort: Int): Boolean {
|
||||
try {
|
||||
DatagramSocket().use { socket ->
|
||||
authSocket = socket
|
||||
statusCallback.accept(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.AUTHENTICATING))
|
||||
LogManager.info("[OTAUpdate] Sending OTA invitation to: $deviceIp")
|
||||
|
||||
val fileMd5 = bytesToMd5(firmware)
|
||||
val message = "$FLASH $localPort ${firmware.size} $fileMd5\n"
|
||||
|
||||
socket.send(DatagramPacket(message.toByteArray(), message.length, deviceIp, PORT))
|
||||
socket.soTimeout = 10000
|
||||
|
||||
val authPacket = DatagramPacket(receiveBuffer, receiveBuffer.size)
|
||||
socket.receive(authPacket)
|
||||
|
||||
val data = String(authPacket.data, 0, authPacket.length)
|
||||
|
||||
// if we received OK directly from the MCU, we do not need to authenticate
|
||||
if (data == "OK") return true
|
||||
|
||||
val args = data.split(" ")
|
||||
|
||||
// The expected auth payload should look like "AUTH AUTH_TOKEN"
|
||||
// if we have less than those two args it means that we are in an invalid state
|
||||
if (args.size != 2 || args[0] != "AUTH") return false
|
||||
|
||||
LogManager.info("[OTAUpdate] Authenticating...")
|
||||
|
||||
val authToken = args[1]
|
||||
val signature = bytesToMd5(UUID.randomUUID().toString().toByteArray())
|
||||
val hashedPassword = bytesToMd5(PASSWORD.toByteArray())
|
||||
val resultText = "$hashedPassword:$authToken:$signature"
|
||||
val payload = bytesToMd5(resultText.toByteArray())
|
||||
|
||||
val authMessage = "$AUTH $signature $payload\n"
|
||||
|
||||
socket.soTimeout = 10000
|
||||
socket.send(
|
||||
DatagramPacket(
|
||||
authMessage.toByteArray(),
|
||||
authMessage.length,
|
||||
deviceIp,
|
||||
PORT,
|
||||
),
|
||||
)
|
||||
|
||||
val authResponsePacket = DatagramPacket(receiveBuffer, receiveBuffer.size)
|
||||
socket.receive(authResponsePacket)
|
||||
|
||||
val authResponse = String(authResponsePacket.data, 0, authResponsePacket.length)
|
||||
|
||||
return authResponse == "OK"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("OTA Authentication exception", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun upload(serverSocket: ServerSocket): Boolean {
|
||||
var connection: Socket? = null
|
||||
try {
|
||||
LogManager.info("[OTAUpdate] Starting on: ${serverSocket.localPort}")
|
||||
LogManager.info("[OTAUpdate] Waiting for device...")
|
||||
|
||||
connection = serverSocket.accept()
|
||||
this.uploadSocket = connection
|
||||
connection.setSoTimeout(1000)
|
||||
val dos = DataOutputStream(connection.getOutputStream())
|
||||
val dis = DataInputStream(connection.getInputStream())
|
||||
|
||||
LogManager.info("[OTAUpdate] Upload size: ${firmware.size} bytes")
|
||||
var offset = 0
|
||||
val chunkSize = 2048
|
||||
while (offset != firmware.size && !canceled) {
|
||||
statusCallback.accept(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.UPLOADING,
|
||||
((offset.toDouble() / firmware.size) * 100).toInt(),
|
||||
),
|
||||
)
|
||||
|
||||
val chunkLen = min(chunkSize, (firmware.size - offset))
|
||||
dos.write(firmware, offset, chunkLen)
|
||||
dos.flush()
|
||||
offset += chunkLen
|
||||
|
||||
// Those skipped bytes are the size written to the MCU. We do not really need that information,
|
||||
// so we simply skip it.
|
||||
// The reason those bytes are skipped here is to not have to skip all of them when checking
|
||||
// for the OK response. Saving time
|
||||
val bytesSkipped = dis.skipBytes(4)
|
||||
// Replicate behaviour of .skipNBytes()
|
||||
if (bytesSkipped != 4) {
|
||||
throw IOException("Unexpected number of bytes skipped: $bytesSkipped")
|
||||
}
|
||||
}
|
||||
if (canceled) return false
|
||||
|
||||
LogManager.info("[OTAUpdate] Waiting for result...")
|
||||
// We set the timeout of the connection bigger as it can take some time for the MCU
|
||||
// to confirm that everything is ok
|
||||
connection.setSoTimeout(10000)
|
||||
val responseBytes = dis.readBytes()
|
||||
val response = String(responseBytes)
|
||||
|
||||
return response.contains("OK")
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("Unable to upload the firmware using ota", e)
|
||||
return false
|
||||
} finally {
|
||||
connection?.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun run() {
|
||||
ServerSocket(0).use { serverSocket ->
|
||||
socketServer = serverSocket
|
||||
if (!authenticate(serverSocket.localPort)) {
|
||||
statusCallback.accept(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_AUTHENTICATION_FAILED,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!upload(serverSocket)) {
|
||||
statusCallback.accept(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.ERROR_UPLOAD_FAILED,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
statusCallback.accept(
|
||||
UpdateStatusEvent(
|
||||
deviceId,
|
||||
FirmwareUpdateStatus.REBOOTING,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
canceled = true
|
||||
socketServer?.close()
|
||||
authSocket?.close()
|
||||
uploadSocket?.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FLASH = 0
|
||||
private const val PORT = 8266
|
||||
private const val PASSWORD = "SlimeVR-OTA"
|
||||
private const val AUTH = 200
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
import dev.llelievr.espflashkotlin.FlasherSerialInterface
|
||||
|
||||
interface SerialFlashingHandler : FlasherSerialInterface
|
||||
@@ -1,66 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.serial.SerialListener
|
||||
import dev.slimevr.serial.SerialPort
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
interface SerialRebootListener {
|
||||
fun onSerialDeviceReconnect(deviceHandle: Pair<UpdateDeviceId<*>, () -> Unit>)
|
||||
}
|
||||
|
||||
/**
|
||||
* This class watch for a serial device to disconnect then reconnect.
|
||||
* This is used to watch the user progress through the firmware update process
|
||||
*/
|
||||
class SerialRebootHandler(
|
||||
private val watchRestartQueue: MutableList<Pair<UpdateDeviceId<*>, () -> Unit>>,
|
||||
private val server: VRServer,
|
||||
// Could be moved to a list of listeners later
|
||||
private val serialRebootListener: SerialRebootListener,
|
||||
) : SerialListener {
|
||||
|
||||
private var currentPort: SerialPort? = null
|
||||
private val disconnectedDevices: MutableList<SerialPort> = CopyOnWriteArrayList()
|
||||
|
||||
override fun onSerialConnected(port: SerialPort) {
|
||||
currentPort = port
|
||||
}
|
||||
|
||||
override fun onSerialDisconnected() {
|
||||
currentPort = null
|
||||
}
|
||||
|
||||
override fun onSerialLog(str: String, ignored: Boolean) {
|
||||
if (str.contains("starting up...")) {
|
||||
val foundPort = watchRestartQueue.find { it.first.id == currentPort?.portLocation }
|
||||
if (foundPort != null) {
|
||||
disconnectedDevices.remove(currentPort)
|
||||
serialRebootListener.onSerialDeviceReconnect(foundPort)
|
||||
// once the restart detected we close the connection
|
||||
if (server.serialHandler.isConnected) {
|
||||
server.serialHandler.closeSerial()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewSerialDevice(port: SerialPort) {
|
||||
val foundPort = watchRestartQueue.find { it.first.id == port.portLocation }
|
||||
if (foundPort != null && disconnectedDevices.contains(port)) {
|
||||
disconnectedDevices.remove(port)
|
||||
serialRebootListener.onSerialDeviceReconnect(foundPort)
|
||||
// once the restart detected we close the connection
|
||||
if (server.serialHandler.isConnected) {
|
||||
server.serialHandler.closeSerial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSerialDeviceDeleted(port: SerialPort) {
|
||||
val foundPort = watchRestartQueue.find { it.first.id == port.portLocation }
|
||||
if (foundPort != null) {
|
||||
disconnectedDevices.add(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
data class UpdateDeviceId<T>(
|
||||
val type: FirmwareUpdateMethod,
|
||||
val id: T,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as UpdateDeviceId<*>
|
||||
|
||||
if (type != other.type) return false
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = type.hashCode()
|
||||
result = 31 * result + (id?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package dev.slimevr.firmware
|
||||
|
||||
data class UpdateStatusEvent<T>(
|
||||
val deviceId: UpdateDeviceId<T>,
|
||||
val status: FirmwareUpdateStatus,
|
||||
val progress: Int = 0,
|
||||
val time: Long = System.currentTimeMillis(),
|
||||
)
|
||||
@@ -1,217 +0,0 @@
|
||||
package dev.slimevr.games.vrchat
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.tracking.processor.config.SkeletonConfigToggles
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import dev.slimevr.tracking.trackers.TrackerUtils
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.math.*
|
||||
|
||||
enum class VRCTrackerModel(val value: Int, val id: Int) {
|
||||
UNKNOWN(-1, solarxr_protocol.rpc.VRCTrackerModel.UNKNOWN),
|
||||
SPHERE(0, solarxr_protocol.rpc.VRCTrackerModel.SPHERE),
|
||||
SYSTEM(1, solarxr_protocol.rpc.VRCTrackerModel.SYSTEM),
|
||||
BOX(2, solarxr_protocol.rpc.VRCTrackerModel.BOX),
|
||||
AXIS(3, solarxr_protocol.rpc.VRCTrackerModel.AXIS),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private val byValue = VRCTrackerModel.entries.associateBy { it.value }
|
||||
|
||||
fun getByValue(value: Int): VRCTrackerModel? = byValue[value]
|
||||
}
|
||||
}
|
||||
|
||||
enum class VRCSpineMode(val value: Int, val id: Int) {
|
||||
UNKNOWN(-1, solarxr_protocol.rpc.VRCSpineMode.UNKNOWN),
|
||||
LOCK_HIP(0, solarxr_protocol.rpc.VRCSpineMode.LOCK_HIP),
|
||||
LOCK_HEAD(1, solarxr_protocol.rpc.VRCSpineMode.LOCK_HEAD),
|
||||
LOCK_BOTH(2, solarxr_protocol.rpc.VRCSpineMode.LOCK_BOTH),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private val byValue = VRCSpineMode.entries.associateBy { it.value }
|
||||
|
||||
fun getByValue(value: Int): VRCSpineMode? = byValue[value]
|
||||
}
|
||||
}
|
||||
|
||||
enum class VRCAvatarMeasurementType(val value: Int, val id: Int) {
|
||||
UNKNOWN(-1, solarxr_protocol.rpc.VRCAvatarMeasurementType.UNKNOWN),
|
||||
ARM_SPAN(0, solarxr_protocol.rpc.VRCAvatarMeasurementType.ARM_SPAN),
|
||||
HEIGHT(1, solarxr_protocol.rpc.VRCAvatarMeasurementType.HEIGHT),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private val byValue = VRCAvatarMeasurementType.entries.associateBy { it.value }
|
||||
|
||||
fun getByValue(value: Int): VRCAvatarMeasurementType? = byValue[value]
|
||||
}
|
||||
}
|
||||
|
||||
data class VRCConfigValues(
|
||||
val legacyMode: Boolean,
|
||||
val shoulderTrackingDisabled: Boolean,
|
||||
val shoulderWidthCompensation: Boolean,
|
||||
val userHeight: Double,
|
||||
val calibrationRange: Double,
|
||||
val calibrationVisuals: Boolean,
|
||||
val trackerModel: VRCTrackerModel,
|
||||
val spineMode: VRCSpineMode,
|
||||
val avatarMeasurementType: VRCAvatarMeasurementType,
|
||||
)
|
||||
|
||||
data class VRCConfigRecommendedValues(
|
||||
val legacyMode: Boolean,
|
||||
val shoulderTrackingDisabled: Boolean,
|
||||
val shoulderWidthCompensation: Boolean,
|
||||
val userHeight: Double,
|
||||
val calibrationRange: Double,
|
||||
val calibrationVisuals: Boolean,
|
||||
val trackerModel: VRCTrackerModel,
|
||||
val spineMode: Array<VRCSpineMode>,
|
||||
val avatarMeasurementType: VRCAvatarMeasurementType,
|
||||
)
|
||||
|
||||
data class VRCConfigValidity(
|
||||
val legacyModeOk: Boolean,
|
||||
val shoulderTrackingOk: Boolean,
|
||||
val shoulderWidthCompensationOk: Boolean,
|
||||
val userHeightOk: Boolean,
|
||||
val calibrationRangeOk: Boolean,
|
||||
val calibrationVisualsOk: Boolean,
|
||||
val trackerModelOk: Boolean,
|
||||
val spineModeOk: Boolean,
|
||||
val avatarMeasurementTypeOk: Boolean,
|
||||
)
|
||||
|
||||
abstract class VRCConfigHandler {
|
||||
abstract val isSupported: Boolean
|
||||
abstract fun initHandler(onChange: (config: VRCConfigValues) -> Unit)
|
||||
}
|
||||
|
||||
class VRCConfigHandlerStub : VRCConfigHandler() {
|
||||
override val isSupported: Boolean
|
||||
get() = false
|
||||
|
||||
override fun initHandler(onChange: (config: VRCConfigValues) -> Unit) {}
|
||||
}
|
||||
|
||||
interface VRCConfigListener {
|
||||
fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List<String>)
|
||||
}
|
||||
|
||||
class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHandler) {
|
||||
|
||||
private val listeners: MutableList<VRCConfigListener> = CopyOnWriteArrayList()
|
||||
var currentValues: VRCConfigValues? = null
|
||||
var currentValidity: VRCConfigValidity? = null
|
||||
|
||||
val isSupported: Boolean
|
||||
get() = handler.isSupported
|
||||
|
||||
init {
|
||||
handler.initHandler(::onChange)
|
||||
}
|
||||
|
||||
fun toggleMuteWarning(key: String) {
|
||||
val keys = VRCConfigValidity::class.java.declaredFields.asSequence().map { p -> p.name }
|
||||
if (!keys.contains(key)) return
|
||||
|
||||
if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) {
|
||||
server.configManager.vrConfig.vrcConfig.mutedWarnings.add(key)
|
||||
} else {
|
||||
server.configManager.vrConfig.vrcConfig.mutedWarnings.remove(key)
|
||||
}
|
||||
|
||||
server.configManager.saveConfig()
|
||||
|
||||
val recommended = recommendedValues()
|
||||
val validity = currentValidity ?: return
|
||||
val values = currentValues ?: return
|
||||
listeners.forEach {
|
||||
it.onChange(
|
||||
validity,
|
||||
values,
|
||||
recommended,
|
||||
server.configManager.vrConfig.vrcConfig.mutedWarnings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* shoulderTrackingDisabled should be true if:
|
||||
* The user isn't tracking their whole arms from their controllers:
|
||||
* forceArmsFromHMD is enabled || the user doesn't have hand trackers with position || the user doesn't have lower arms trackers || the user doesn't have upper arm trackers
|
||||
* And the user isn't tracking their arms from their HMD or doesn't have both shoulders:
|
||||
* (forceArmsFromHMD is disabled && user has hand trackers with position) || user is missing a shoulder tracker
|
||||
*/
|
||||
fun recommendedValues(): VRCConfigRecommendedValues {
|
||||
val forceArmsFromHMD = server.humanPoseManager.getToggle(SkeletonConfigToggles.FORCE_ARMS_FROM_HMD)
|
||||
|
||||
val hasLeftHandWithPosition = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_HAND)?.hasPosition ?: false
|
||||
val hasRightHandWithPosition = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_HAND)?.hasPosition ?: false
|
||||
|
||||
val isMissingAnArmTracker = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_LOWER_ARM) == null ||
|
||||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_LOWER_ARM) == null ||
|
||||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_UPPER_ARM) == null ||
|
||||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_UPPER_ARM) == null
|
||||
val isMissingAShoulderTracker = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_SHOULDER) == null ||
|
||||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_SHOULDER) == null
|
||||
|
||||
return VRCConfigRecommendedValues(
|
||||
legacyMode = false,
|
||||
shoulderTrackingDisabled =
|
||||
((forceArmsFromHMD || !hasLeftHandWithPosition || !hasRightHandWithPosition) || isMissingAnArmTracker) && // Not tracking shoulders from hands
|
||||
((!forceArmsFromHMD && hasLeftHandWithPosition && hasRightHandWithPosition) || isMissingAShoulderTracker), // Not tracking shoulders from HMD
|
||||
userHeight = server.humanPoseManager.realUserHeight.toDouble(),
|
||||
calibrationRange = 0.2,
|
||||
trackerModel = VRCTrackerModel.AXIS,
|
||||
spineMode = arrayOf(VRCSpineMode.LOCK_HIP, VRCSpineMode.LOCK_HEAD),
|
||||
calibrationVisuals = true,
|
||||
avatarMeasurementType = VRCAvatarMeasurementType.HEIGHT,
|
||||
shoulderWidthCompensation = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun addListener(listener: VRCConfigListener) {
|
||||
listeners.add(listener)
|
||||
val values = currentValues ?: return
|
||||
val recommended = recommendedValues()
|
||||
val validity = checkValidity(values, recommended)
|
||||
listener.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings)
|
||||
}
|
||||
|
||||
fun removeListener(listener: VRCConfigListener) {
|
||||
listeners.removeIf { l -> l === listener }
|
||||
}
|
||||
|
||||
fun checkValidity(values: VRCConfigValues, recommended: VRCConfigRecommendedValues): VRCConfigValidity = VRCConfigValidity(
|
||||
legacyModeOk = values.legacyMode == recommended.legacyMode,
|
||||
shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled,
|
||||
spineModeOk = recommended.spineMode.contains(values.spineMode),
|
||||
trackerModelOk = values.trackerModel == recommended.trackerModel,
|
||||
calibrationRangeOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1,
|
||||
userHeightOk = abs(server.humanPoseManager.realUserHeight - values.userHeight) < 0.1,
|
||||
calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals,
|
||||
avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
|
||||
shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation,
|
||||
)
|
||||
|
||||
fun forceUpdate() {
|
||||
val values = currentValues
|
||||
if (values != null) {
|
||||
this.onChange(values)
|
||||
}
|
||||
}
|
||||
|
||||
fun onChange(values: VRCConfigValues) {
|
||||
val recommended = recommendedValues()
|
||||
val validity = checkValidity(values, recommended)
|
||||
currentValidity = validity
|
||||
currentValues = values
|
||||
listeners.forEach {
|
||||
it.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package dev.slimevr.guards
|
||||
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class ServerGuards {
|
||||
|
||||
var canDoMounting: Boolean = false
|
||||
var canDoYawReset: Boolean = false
|
||||
var canDoUserHeightCalibration: Boolean = false
|
||||
|
||||
private val timer = Timer()
|
||||
private var mountingTimeoutTask: TimerTask? = null
|
||||
|
||||
fun onFullReset() {
|
||||
canDoMounting = true
|
||||
canDoYawReset = true
|
||||
mountingTimeoutTask?.cancel()
|
||||
mountingTimeoutTask = timer.schedule(MOUNTING_RESET_TIMEOUT) {
|
||||
canDoMounting = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MOUNTING_RESET_TIMEOUT = 2 * 60 * 1000L
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package dev.slimevr.math
|
||||
|
||||
import com.jme3.math.FastMath
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* An angle between [-PI, PI).
|
||||
*/
|
||||
@JvmInline
|
||||
value class Angle(private val rad: Float) {
|
||||
|
||||
fun toRad() = rad
|
||||
|
||||
fun toDeg() = rad * FastMath.RAD_TO_DEG
|
||||
|
||||
operator fun unaryPlus() = this
|
||||
|
||||
operator fun unaryMinus() = Angle(normalize(-rad))
|
||||
|
||||
operator fun plus(other: Angle) = Angle(normalize(rad + other.rad))
|
||||
|
||||
operator fun minus(other: Angle) = Angle(normalize(rad - other.rad))
|
||||
|
||||
operator fun times(scale: Float) = Angle(normalize(rad * scale))
|
||||
|
||||
operator fun div(scale: Float) = Angle(normalize(rad / scale))
|
||||
|
||||
operator fun compareTo(other: Angle) = rad.compareTo(other.rad)
|
||||
|
||||
override fun toString() = "${toDeg()} deg"
|
||||
|
||||
companion object {
|
||||
val ZERO = Angle(0.0f)
|
||||
|
||||
fun ofRad(rad: Float) = Angle(normalize(rad))
|
||||
|
||||
fun ofDeg(deg: Float) = Angle(normalize(deg * FastMath.DEG_TO_RAD))
|
||||
|
||||
// Angle between two vectors
|
||||
fun absBetween(a: Vector3, b: Vector3) = Angle(normalize(a.angleTo(b)))
|
||||
|
||||
// Angle between two rotations in rotation space
|
||||
fun absBetween(a: Quaternion, b: Quaternion) = Angle(normalize(a.angleToR(b)))
|
||||
|
||||
/**
|
||||
* Normalizes an angle to [-PI, PI)
|
||||
*/
|
||||
private fun normalize(rad: Float): Float {
|
||||
// Normalize to [0, 2*PI)
|
||||
val r =
|
||||
if (rad < 0.0f || rad >= FastMath.TWO_PI) {
|
||||
rad - floor(rad * FastMath.INV_TWO_PI) * FastMath.TWO_PI
|
||||
} else {
|
||||
rad
|
||||
}
|
||||
|
||||
// Normalize to [-PI, PI)
|
||||
return if (r > FastMath.PI) {
|
||||
r - FastMath.TWO_PI
|
||||
} else {
|
||||
r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package dev.slimevr.math
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
* Averages angles by summing vectors.
|
||||
*
|
||||
* See https://www.themathdoctors.org/averaging-angles/
|
||||
*/
|
||||
class AngleAverage {
|
||||
|
||||
private var sumX = 0.0f
|
||||
private var sumY = 0.0f
|
||||
|
||||
/**
|
||||
* Adds another angle to the average.
|
||||
*/
|
||||
fun add(angle: Angle, weight: Float = 1.0f) {
|
||||
sumX += cos(angle.toRad()) * weight
|
||||
sumY += sin(angle.toRad()) * weight
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average angle.
|
||||
*/
|
||||
fun toAngle(): Angle = if (isEmpty()) {
|
||||
Angle.ZERO
|
||||
} else {
|
||||
Angle.ofRad(atan2(sumY, sumX))
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there are any angles to average.
|
||||
*/
|
||||
fun isEmpty() = sumX == 0.0f && sumY == 0.0f
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package dev.slimevr.math
|
||||
|
||||
import kotlin.math.*
|
||||
|
||||
class AngleErrors {
|
||||
|
||||
private var sumSqrErrors = 0.0f
|
||||
|
||||
fun add(error: Angle) {
|
||||
sumSqrErrors += error.toRad() * error.toRad()
|
||||
}
|
||||
|
||||
fun toL2Norm() = Angle.ofRad(sqrt(sumSqrErrors))
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package dev.slimevr.osc;
|
||||
|
||||
import com.illposed.osc.transport.OSCPortIn;
|
||||
import com.illposed.osc.transport.OSCPortOut;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
|
||||
public interface OSCHandler {
|
||||
|
||||
public void refreshSettings(boolean refreshRouterSettings);
|
||||
|
||||
public void updateOscReceiver(int portIn, String[] args);
|
||||
|
||||
public void updateOscSender(int portOut, String address);
|
||||
|
||||
public void update();
|
||||
|
||||
public OSCPortOut getOscSender();
|
||||
|
||||
public int getPortOut();
|
||||
|
||||
public InetAddress getAddress();
|
||||
|
||||
public OSCPortIn getOscReceiver();
|
||||
|
||||
public int getPortIn();
|
||||
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package dev.slimevr.osc;
|
||||
|
||||
import com.illposed.osc.*;
|
||||
import com.illposed.osc.messageselector.OSCPatternAddressMessageSelector;
|
||||
import com.illposed.osc.transport.OSCPortIn;
|
||||
import com.illposed.osc.transport.OSCPortOut;
|
||||
import dev.slimevr.config.OSCConfig;
|
||||
import io.eiren.util.collections.FastList;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
|
||||
public class OSCRouter {
|
||||
private OSCPortIn oscReceiver;
|
||||
private OSCPortOut oscSender;
|
||||
private final OSCConfig config;
|
||||
private final FastList<OSCHandler> oscHandlers;
|
||||
private int lastPortIn;
|
||||
private int lastPortOut;
|
||||
private InetAddress lastAddress;
|
||||
private long timeAtLastError;
|
||||
|
||||
public OSCRouter(
|
||||
OSCConfig oscConfig,
|
||||
FastList<OSCHandler> oscHandlers
|
||||
) {
|
||||
this.config = oscConfig;
|
||||
this.oscHandlers = oscHandlers;
|
||||
|
||||
refreshSettings(false);
|
||||
}
|
||||
|
||||
public void refreshSettings(boolean refreshHandlersSettings) {
|
||||
if (refreshHandlersSettings) {
|
||||
for (OSCHandler oscHandler : oscHandlers) {
|
||||
oscHandler.refreshSettings(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Stops listening and closes OSC port
|
||||
boolean wasListening = oscReceiver != null && oscReceiver.isListening();
|
||||
if (wasListening) {
|
||||
oscReceiver.stopListening();
|
||||
}
|
||||
oscReceiver = null;
|
||||
boolean wasConnected = oscSender != null && oscSender.isConnected();
|
||||
if (wasConnected) {
|
||||
try {
|
||||
oscSender.close();
|
||||
} catch (IOException e) {
|
||||
LogManager.severe("[OSCRouter] Error closing the OSC sender: " + e);
|
||||
}
|
||||
}
|
||||
oscSender = null;
|
||||
|
||||
if (config.getEnabled()) {
|
||||
// Instantiates the OSC receiver
|
||||
int portIn = config.getPortIn();
|
||||
// Check if another OSC receiver with same port exists
|
||||
for (OSCHandler oscHandler : oscHandlers) {
|
||||
if (oscHandler.getPortIn() == portIn) {
|
||||
if (oscHandler.getOscReceiver().isListening()) {
|
||||
oscReceiver = oscHandler.getOscReceiver();
|
||||
LogManager.info("[OSCRouter] Listening to port " + portIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Else, create our own OSC receiver
|
||||
if (oscReceiver == null) {
|
||||
try {
|
||||
oscReceiver = new OSCPortIn(portIn);
|
||||
if (lastPortIn != portIn || !wasListening) {
|
||||
LogManager.info("[OSCRouter] Listening to port " + portIn);
|
||||
}
|
||||
lastPortIn = portIn;
|
||||
} catch (IOException e) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[OSCRouter] Error listening to the port "
|
||||
+ config.getPortIn()
|
||||
+ ": "
|
||||
+ e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the OSC sender
|
||||
int portOut = config.getPortOut();
|
||||
InetAddress address;
|
||||
try {
|
||||
address = InetAddress.getByName(config.getAddress());
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Check if another OSC sender with same port and address exists
|
||||
for (OSCHandler oscHandler : oscHandlers) {
|
||||
if (oscHandler.getPortOut() == portOut && oscHandler.getAddress() == address) {
|
||||
if (oscHandler.getOscSender().isConnected()) {
|
||||
oscSender = oscHandler.getOscSender();
|
||||
LogManager
|
||||
.info(
|
||||
"[OSCRouter] Sending to port "
|
||||
+ portOut
|
||||
+ " at address "
|
||||
+ address.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Else, create our own OSC sender
|
||||
if (oscSender == null) {
|
||||
try {
|
||||
oscSender = new OSCPortOut(new InetSocketAddress(address, portOut));
|
||||
if ((lastPortOut != portOut && lastAddress != address) || !wasConnected) {
|
||||
LogManager
|
||||
.info(
|
||||
"[OSCRouter] Sending to port "
|
||||
+ portOut
|
||||
+ " at address "
|
||||
+ address.toString()
|
||||
);
|
||||
}
|
||||
lastPortOut = portOut;
|
||||
lastAddress = address;
|
||||
|
||||
oscSender.connect();
|
||||
} catch (IOException e) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[OSCRouter] Error connecting to port "
|
||||
+ config.getPortOut()
|
||||
+ " at the address "
|
||||
+ config.getAddress()
|
||||
+ ": "
|
||||
+ e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Starts listening to messages
|
||||
if (oscReceiver != null) {
|
||||
OSCMessageListener listener = this::handleReceivedMessage;
|
||||
// Listens for any message ("//" is a wildcard)
|
||||
MessageSelector selector = new OSCPatternAddressMessageSelector("//");
|
||||
oscReceiver.getDispatcher().addListener(selector, listener);
|
||||
if (!oscReceiver.isListening())
|
||||
oscReceiver.startListening();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleReceivedMessage(OSCMessageEvent event) {
|
||||
if (oscSender != null && oscSender.isConnected()) {
|
||||
OSCMessage oscMessage = new OSCMessage(
|
||||
event.getMessage().getAddress(),
|
||||
event.getMessage().getArguments()
|
||||
);
|
||||
try {
|
||||
oscSender.send(oscMessage);
|
||||
} catch (IOException | OSCSerializeException e) {
|
||||
// Avoid spamming AsynchronousCloseException too many
|
||||
// times per second
|
||||
if (System.currentTimeMillis() - timeAtLastError > 100) {
|
||||
timeAtLastError = System.currentTimeMillis();
|
||||
LogManager
|
||||
.warning(
|
||||
"[OSCRouter] Error sending OSC packet: "
|
||||
+ e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
package dev.slimevr.osc
|
||||
|
||||
import com.jme3.math.FastMath
|
||||
import dev.slimevr.tracking.processor.TransformNode
|
||||
import io.github.axisangles.ktmath.EulerAngles
|
||||
import io.github.axisangles.ktmath.EulerOrder
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
|
||||
/**
|
||||
* TODO make this class use Bone.kt
|
||||
*/
|
||||
class UnityArmature(localRot: Boolean) {
|
||||
|
||||
// Head
|
||||
private val headNode = TransformNode(localRotation = localRot)
|
||||
private val neckTailNode = TransformNode(localRotation = localRot)
|
||||
private val neckHeadNode = TransformNode(localRotation = localRot)
|
||||
|
||||
// Spine
|
||||
private val upperChestNode = TransformNode(localRotation = localRot)
|
||||
private val chestNode = TransformNode(localRotation = localRot)
|
||||
private val spineTailNode = TransformNode(localRotation = localRot)
|
||||
private val spineHeadNode = TransformNode(localRotation = localRot)
|
||||
private val hipsNode = TransformNode(localRotation = localRot)
|
||||
private val leftHipNode = TransformNode(localRotation = localRot)
|
||||
private val rightHipNode = TransformNode(localRotation = localRot)
|
||||
|
||||
// Legs
|
||||
private val leftKneeNode = TransformNode(localRotation = localRot)
|
||||
private val leftAnkleNode = TransformNode(localRotation = localRot)
|
||||
private val leftFootNode = TransformNode(localRotation = localRot)
|
||||
private val rightKneeNode = TransformNode(localRotation = localRot)
|
||||
private val rightAnkleNode = TransformNode(localRotation = localRot)
|
||||
private val rightFootNode = TransformNode(localRotation = localRot)
|
||||
|
||||
// Arms
|
||||
private val leftShoulderHeadNode = TransformNode(localRotation = localRot)
|
||||
private val rightShoulderHeadNode = TransformNode(localRotation = localRot)
|
||||
private val leftShoulderTailNode = TransformNode(localRotation = localRot)
|
||||
private val rightShoulderTailNode = TransformNode(localRotation = localRot)
|
||||
private val leftElbowNode = TransformNode(localRotation = localRot)
|
||||
private val rightElbowNode = TransformNode(localRotation = localRot)
|
||||
private val leftWristNode = TransformNode(localRotation = localRot)
|
||||
private val rightWristNode = TransformNode(localRotation = localRot)
|
||||
private val leftHandNode = TransformNode(localRotation = !localRot)
|
||||
private val rightHandNode = TransformNode(localRotation = !localRot)
|
||||
|
||||
// Fingers
|
||||
val leftThumbProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val leftThumbProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val leftThumbIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val leftThumbDistalNode = TransformNode(localRotation = localRot)
|
||||
val leftIndexProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val leftIndexProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val leftIndexIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val leftIndexDistalNode = TransformNode(localRotation = localRot)
|
||||
val leftMiddleProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val leftMiddleProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val leftMiddleIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val leftMiddleDistalNode = TransformNode(localRotation = localRot)
|
||||
val leftRingProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val leftRingProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val leftRingIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val leftRingDistalNode = TransformNode(localRotation = localRot)
|
||||
val leftLittleProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val leftLittleProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val leftLittleIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val leftLittleDistalNode = TransformNode(localRotation = localRot)
|
||||
val rightThumbProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val rightThumbProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val rightThumbIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val rightThumbDistalNode = TransformNode(localRotation = localRot)
|
||||
val rightIndexProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val rightIndexProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val rightIndexIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val rightIndexDistalNode = TransformNode(localRotation = localRot)
|
||||
val rightMiddleProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val rightMiddleProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val rightMiddleIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val rightMiddleDistalNode = TransformNode(localRotation = localRot)
|
||||
val rightRingProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val rightRingProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val rightRingIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val rightRingDistalNode = TransformNode(localRotation = localRot)
|
||||
val rightLittleProximalHeadNode = TransformNode(localRotation = localRot)
|
||||
val rightLittleProximalTailNode = TransformNode(localRotation = localRot)
|
||||
val rightLittleIntermediateNode = TransformNode(localRotation = localRot)
|
||||
val rightLittleDistalNode = TransformNode(localRotation = localRot)
|
||||
|
||||
private var rootPosition = Vector3.NULL
|
||||
private var rootRotation = Quaternion.IDENTITY
|
||||
|
||||
init {
|
||||
// Attach nodes
|
||||
// Spine
|
||||
hipsNode.attachChild(spineHeadNode)
|
||||
spineHeadNode.attachChild(spineTailNode)
|
||||
spineTailNode.attachChild(chestNode)
|
||||
chestNode.attachChild(upperChestNode)
|
||||
upperChestNode.attachChild(neckHeadNode)
|
||||
neckHeadNode.attachChild(neckTailNode)
|
||||
neckTailNode.attachChild(headNode)
|
||||
|
||||
// Legs
|
||||
hipsNode.attachChild(leftHipNode)
|
||||
hipsNode.attachChild(rightHipNode)
|
||||
leftHipNode.attachChild(leftKneeNode)
|
||||
rightHipNode.attachChild(rightKneeNode)
|
||||
leftKneeNode.attachChild(leftAnkleNode)
|
||||
rightKneeNode.attachChild(rightAnkleNode)
|
||||
leftAnkleNode.attachChild(leftFootNode)
|
||||
rightAnkleNode.attachChild(rightFootNode)
|
||||
|
||||
// Arms
|
||||
upperChestNode.attachChild(leftShoulderHeadNode)
|
||||
upperChestNode.attachChild(rightShoulderHeadNode)
|
||||
leftShoulderHeadNode.attachChild(leftShoulderTailNode)
|
||||
rightShoulderHeadNode.attachChild(rightShoulderTailNode)
|
||||
leftShoulderTailNode.attachChild(leftElbowNode)
|
||||
rightShoulderTailNode.attachChild(rightElbowNode)
|
||||
leftElbowNode.attachChild(leftWristNode)
|
||||
rightElbowNode.attachChild(rightWristNode)
|
||||
leftWristNode.attachChild(leftHandNode)
|
||||
rightWristNode.attachChild(rightHandNode)
|
||||
|
||||
// Fingers
|
||||
leftHandNode.attachChild(leftThumbProximalHeadNode)
|
||||
leftThumbProximalHeadNode.attachChild(leftThumbProximalTailNode)
|
||||
leftThumbProximalTailNode.attachChild(leftThumbIntermediateNode)
|
||||
leftThumbIntermediateNode.attachChild(leftThumbDistalNode)
|
||||
leftHandNode.attachChild(leftIndexProximalHeadNode)
|
||||
leftIndexProximalHeadNode.attachChild(leftIndexProximalTailNode)
|
||||
leftIndexProximalTailNode.attachChild(leftIndexIntermediateNode)
|
||||
leftIndexIntermediateNode.attachChild(leftIndexDistalNode)
|
||||
leftHandNode.attachChild(leftMiddleProximalHeadNode)
|
||||
leftMiddleProximalHeadNode.attachChild(leftMiddleProximalTailNode)
|
||||
leftMiddleProximalTailNode.attachChild(leftMiddleIntermediateNode)
|
||||
leftMiddleIntermediateNode.attachChild(leftMiddleDistalNode)
|
||||
leftHandNode.attachChild(leftRingProximalHeadNode)
|
||||
leftRingProximalHeadNode.attachChild(leftRingProximalTailNode)
|
||||
leftRingProximalTailNode.attachChild(leftRingIntermediateNode)
|
||||
leftRingIntermediateNode.attachChild(leftRingDistalNode)
|
||||
leftHandNode.attachChild(leftLittleProximalHeadNode)
|
||||
leftLittleProximalHeadNode.attachChild(leftLittleProximalTailNode)
|
||||
leftLittleProximalTailNode.attachChild(leftLittleIntermediateNode)
|
||||
leftLittleIntermediateNode.attachChild(leftLittleDistalNode)
|
||||
rightHandNode.attachChild(rightThumbProximalHeadNode)
|
||||
rightThumbProximalHeadNode.attachChild(rightThumbProximalTailNode)
|
||||
rightThumbProximalTailNode.attachChild(rightThumbIntermediateNode)
|
||||
rightThumbIntermediateNode.attachChild(rightThumbDistalNode)
|
||||
rightHandNode.attachChild(rightIndexProximalHeadNode)
|
||||
rightIndexProximalHeadNode.attachChild(rightIndexProximalTailNode)
|
||||
rightIndexProximalTailNode.attachChild(rightIndexIntermediateNode)
|
||||
rightIndexIntermediateNode.attachChild(rightIndexDistalNode)
|
||||
rightHandNode.attachChild(rightMiddleProximalHeadNode)
|
||||
rightMiddleProximalHeadNode.attachChild(rightMiddleProximalTailNode)
|
||||
rightMiddleProximalTailNode.attachChild(rightMiddleIntermediateNode)
|
||||
rightMiddleIntermediateNode.attachChild(rightMiddleDistalNode)
|
||||
rightHandNode.attachChild(rightRingProximalHeadNode)
|
||||
rightRingProximalHeadNode.attachChild(rightRingProximalTailNode)
|
||||
rightRingProximalTailNode.attachChild(rightRingIntermediateNode)
|
||||
rightRingIntermediateNode.attachChild(rightRingDistalNode)
|
||||
rightHandNode.attachChild(rightLittleProximalHeadNode)
|
||||
rightLittleProximalHeadNode.attachChild(rightLittleProximalTailNode)
|
||||
rightLittleProximalTailNode.attachChild(rightLittleIntermediateNode)
|
||||
rightLittleIntermediateNode.attachChild(rightLittleDistalNode)
|
||||
}
|
||||
|
||||
fun update() {
|
||||
// Set the upper chest node's rotation to the chest's
|
||||
upperChestNode.localTransform.rotation = chestNode.localTransform.rotation
|
||||
// Update the root node
|
||||
hipsNode.update()
|
||||
}
|
||||
|
||||
fun setRootPose(globalPos: Vector3, globalRot: Quaternion) {
|
||||
rootPosition = globalPos
|
||||
rootRotation = globalRot
|
||||
}
|
||||
|
||||
fun setGlobalRotationForBone(unityBone: UnityBone, globalRot: Quaternion) {
|
||||
val node = getHeadNodeOfBone(unityBone)
|
||||
if (node != null) {
|
||||
node.localTransform.rotation = if (UnityBone.isLeftArmBone(unityBone)) {
|
||||
globalRot * LEFT_SHOULDER_OFFSET
|
||||
} else if (UnityBone.isRightArmBone(unityBone)) {
|
||||
globalRot * RIGHT_SHOULDER_OFFSET
|
||||
} else {
|
||||
globalRot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setLocalRotationForBone(unityBone: UnityBone, localRot: Quaternion) {
|
||||
val node = getHeadNodeOfBone(unityBone)
|
||||
if (node != null) {
|
||||
if (unityBone == UnityBone.HIPS) {
|
||||
node.worldTransform.rotation = localRot
|
||||
} else {
|
||||
node.localTransform.rotation = if (UnityBone.isLeftStartOfArmOrFingerBone(unityBone)) {
|
||||
localRot * RIGHT_SHOULDER_OFFSET
|
||||
} else if (UnityBone.isRightStartOfArmOrFingerBone(unityBone)) {
|
||||
localRot * LEFT_SHOULDER_OFFSET
|
||||
} else {
|
||||
localRot
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getGlobalTranslationForBone(unityBone: UnityBone): Vector3 {
|
||||
val node = getHeadNodeOfBone(unityBone)
|
||||
return if (node != null) {
|
||||
if (unityBone == UnityBone.HIPS) {
|
||||
val hipsAverage = (
|
||||
leftHipNode.worldTransform.translation +
|
||||
rightHipNode.worldTransform.translation
|
||||
) *
|
||||
0.5f
|
||||
node.worldTransform.translation * 2f - hipsAverage + rootPosition
|
||||
} else {
|
||||
node.worldTransform.translation + rootPosition
|
||||
}
|
||||
} else {
|
||||
Vector3.NULL
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocalTranslationForBone(unityBone: UnityBone): Vector3 {
|
||||
val node = getHeadNodeOfBone(unityBone)
|
||||
return if (node != null) {
|
||||
if (unityBone == UnityBone.HIPS) {
|
||||
val hipsAverage = (
|
||||
leftHipNode.worldTransform.translation +
|
||||
rightHipNode.worldTransform.translation
|
||||
) *
|
||||
0.5f
|
||||
node.worldTransform.translation * 2f - hipsAverage + rootPosition
|
||||
} else {
|
||||
node.localTransform.translation
|
||||
}
|
||||
} else {
|
||||
Vector3.NULL
|
||||
}
|
||||
}
|
||||
|
||||
fun getGlobalRotationForBone(unityBone: UnityBone?): Quaternion {
|
||||
val node = getHeadNodeOfBone(unityBone)
|
||||
return if (node != null) {
|
||||
node.worldTransform.rotation * rootRotation
|
||||
} else {
|
||||
Quaternion.IDENTITY
|
||||
}
|
||||
}
|
||||
|
||||
fun getLocalRotationForBone(unityBone: UnityBone): Quaternion {
|
||||
val node = getHeadNodeOfBone(unityBone)
|
||||
return if (node != null) {
|
||||
if (unityBone == UnityBone.HIPS) {
|
||||
node.worldTransform.rotation * rootRotation
|
||||
} else {
|
||||
node.parent!!.worldTransform.rotation.inv() * node.worldTransform.rotation
|
||||
}
|
||||
} else {
|
||||
Quaternion.IDENTITY
|
||||
}
|
||||
}
|
||||
|
||||
fun getHeadNodeOfBone(unityBone: UnityBone?): TransformNode? = if (unityBone == null) {
|
||||
null
|
||||
} else {
|
||||
when (unityBone) {
|
||||
UnityBone.HEAD -> neckTailNode
|
||||
UnityBone.NECK -> neckHeadNode
|
||||
UnityBone.UPPER_CHEST -> chestNode
|
||||
UnityBone.CHEST -> spineTailNode
|
||||
UnityBone.SPINE -> spineHeadNode
|
||||
UnityBone.HIPS -> hipsNode
|
||||
UnityBone.LEFT_UPPER_LEG -> leftHipNode
|
||||
UnityBone.RIGHT_UPPER_LEG -> rightHipNode
|
||||
UnityBone.LEFT_LOWER_LEG -> leftKneeNode
|
||||
UnityBone.RIGHT_LOWER_LEG -> rightKneeNode
|
||||
UnityBone.LEFT_FOOT -> leftAnkleNode
|
||||
UnityBone.RIGHT_FOOT -> rightAnkleNode
|
||||
UnityBone.LEFT_SHOULDER -> leftShoulderHeadNode
|
||||
UnityBone.RIGHT_SHOULDER -> rightShoulderHeadNode
|
||||
UnityBone.LEFT_UPPER_ARM -> leftShoulderTailNode
|
||||
UnityBone.RIGHT_UPPER_ARM -> rightShoulderTailNode
|
||||
UnityBone.LEFT_LOWER_ARM -> leftElbowNode
|
||||
UnityBone.RIGHT_LOWER_ARM -> rightElbowNode
|
||||
UnityBone.LEFT_HAND -> leftWristNode
|
||||
UnityBone.RIGHT_HAND -> rightWristNode
|
||||
UnityBone.LEFT_THUMB_PROXIMAL -> leftThumbProximalHeadNode
|
||||
UnityBone.LEFT_THUMB_INTERMEDIATE -> leftThumbProximalTailNode
|
||||
UnityBone.LEFT_THUMB_DISTAL -> leftThumbIntermediateNode
|
||||
UnityBone.LEFT_INDEX_PROXIMAL -> leftIndexProximalHeadNode
|
||||
UnityBone.LEFT_INDEX_INTERMEDIATE -> leftIndexProximalTailNode
|
||||
UnityBone.LEFT_INDEX_DISTAL -> leftIndexIntermediateNode
|
||||
UnityBone.LEFT_MIDDLE_PROXIMAL -> leftMiddleProximalHeadNode
|
||||
UnityBone.LEFT_MIDDLE_INTERMEDIATE -> leftMiddleProximalTailNode
|
||||
UnityBone.LEFT_MIDDLE_DISTAL -> leftMiddleIntermediateNode
|
||||
UnityBone.LEFT_RING_PROXIMAL -> leftRingProximalHeadNode
|
||||
UnityBone.LEFT_RING_INTERMEDIATE -> leftRingProximalTailNode
|
||||
UnityBone.LEFT_RING_DISTAL -> leftRingIntermediateNode
|
||||
UnityBone.LEFT_LITTLE_PROXIMAL -> leftLittleProximalHeadNode
|
||||
UnityBone.LEFT_LITTLE_INTERMEDIATE -> leftLittleProximalTailNode
|
||||
UnityBone.LEFT_LITTLE_DISTAL -> leftLittleIntermediateNode
|
||||
UnityBone.RIGHT_THUMB_PROXIMAL -> rightThumbProximalHeadNode
|
||||
UnityBone.RIGHT_THUMB_INTERMEDIATE -> rightThumbProximalTailNode
|
||||
UnityBone.RIGHT_THUMB_DISTAL -> rightThumbIntermediateNode
|
||||
UnityBone.RIGHT_INDEX_PROXIMAL -> rightIndexProximalHeadNode
|
||||
UnityBone.RIGHT_INDEX_INTERMEDIATE -> rightIndexProximalTailNode
|
||||
UnityBone.RIGHT_INDEX_DISTAL -> rightIndexIntermediateNode
|
||||
UnityBone.RIGHT_MIDDLE_PROXIMAL -> rightMiddleProximalHeadNode
|
||||
UnityBone.RIGHT_MIDDLE_INTERMEDIATE -> rightMiddleProximalTailNode
|
||||
UnityBone.RIGHT_MIDDLE_DISTAL -> rightMiddleIntermediateNode
|
||||
UnityBone.RIGHT_RING_PROXIMAL -> rightRingProximalHeadNode
|
||||
UnityBone.RIGHT_RING_INTERMEDIATE -> rightRingProximalTailNode
|
||||
UnityBone.RIGHT_RING_DISTAL -> rightRingIntermediateNode
|
||||
UnityBone.RIGHT_LITTLE_PROXIMAL -> rightLittleProximalHeadNode
|
||||
UnityBone.RIGHT_LITTLE_INTERMEDIATE -> rightLittleProximalTailNode
|
||||
UnityBone.RIGHT_LITTLE_DISTAL -> rightLittleIntermediateNode
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LEFT_SHOULDER_OFFSET = EulerAngles(EulerOrder.YZX, 0f, 0f, FastMath.HALF_PI).toQuaternion()
|
||||
private val RIGHT_SHOULDER_OFFSET = EulerAngles(EulerOrder.YZX, 0f, 0f, -FastMath.HALF_PI).toQuaternion()
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
package dev.slimevr.osc
|
||||
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Unity HumanBodyBones from:
|
||||
* https://docs.unity3d.com/ScriptReference/HumanBodyBones.html
|
||||
*/
|
||||
@Serializable
|
||||
enum class UnityBone(
|
||||
val stringVal: String,
|
||||
val boneType: BoneType?,
|
||||
val trackerPosition: TrackerPosition?,
|
||||
) {
|
||||
@SerialName("hips")
|
||||
HIPS("Hips", BoneType.HIP, TrackerPosition.HIP),
|
||||
|
||||
@SerialName("leftUpperLeg")
|
||||
LEFT_UPPER_LEG("LeftUpperLeg", BoneType.LEFT_UPPER_LEG, TrackerPosition.LEFT_UPPER_LEG),
|
||||
|
||||
@SerialName("rightUpperLeg")
|
||||
RIGHT_UPPER_LEG("RightUpperLeg", BoneType.RIGHT_UPPER_LEG, TrackerPosition.RIGHT_UPPER_LEG),
|
||||
|
||||
@SerialName("leftLowerLeg")
|
||||
LEFT_LOWER_LEG("LeftLowerLeg", BoneType.LEFT_LOWER_LEG, TrackerPosition.LEFT_LOWER_LEG),
|
||||
|
||||
@SerialName("rightLowerLeg")
|
||||
RIGHT_LOWER_LEG("RightLowerLeg", BoneType.RIGHT_LOWER_LEG, TrackerPosition.RIGHT_LOWER_LEG),
|
||||
|
||||
@SerialName("leftFoot")
|
||||
LEFT_FOOT("LeftFoot", BoneType.LEFT_FOOT, TrackerPosition.LEFT_FOOT),
|
||||
|
||||
@SerialName("rightFoot")
|
||||
RIGHT_FOOT("RightFoot", BoneType.RIGHT_FOOT, TrackerPosition.RIGHT_FOOT),
|
||||
|
||||
@SerialName("spine")
|
||||
SPINE("Spine", BoneType.WAIST, TrackerPosition.WAIST),
|
||||
|
||||
@SerialName("chest")
|
||||
CHEST("Chest", BoneType.CHEST, TrackerPosition.CHEST),
|
||||
|
||||
@SerialName("upperChest")
|
||||
UPPER_CHEST("UpperChest", BoneType.CHEST, TrackerPosition.CHEST),
|
||||
|
||||
@SerialName("neck")
|
||||
NECK("Neck", BoneType.NECK, TrackerPosition.NECK),
|
||||
|
||||
@SerialName("head")
|
||||
HEAD("Head", BoneType.HEAD, TrackerPosition.HEAD),
|
||||
|
||||
@SerialName("leftShoulder")
|
||||
LEFT_SHOULDER("LeftShoulder", BoneType.LEFT_SHOULDER, TrackerPosition.LEFT_SHOULDER),
|
||||
|
||||
@SerialName("rightShoulder")
|
||||
RIGHT_SHOULDER("RightShoulder", BoneType.RIGHT_SHOULDER, TrackerPosition.RIGHT_SHOULDER),
|
||||
|
||||
@SerialName("leftUpperArm")
|
||||
LEFT_UPPER_ARM("LeftUpperArm", BoneType.LEFT_UPPER_ARM, TrackerPosition.LEFT_UPPER_ARM),
|
||||
|
||||
@SerialName("rightUpperArm")
|
||||
RIGHT_UPPER_ARM("RightUpperArm", BoneType.RIGHT_UPPER_ARM, TrackerPosition.RIGHT_UPPER_ARM),
|
||||
|
||||
@SerialName("leftLowerArm")
|
||||
LEFT_LOWER_ARM("LeftLowerArm", BoneType.LEFT_LOWER_ARM, TrackerPosition.LEFT_LOWER_ARM),
|
||||
|
||||
@SerialName("rightLowerArm")
|
||||
RIGHT_LOWER_ARM("RightLowerArm", BoneType.RIGHT_LOWER_ARM, TrackerPosition.RIGHT_LOWER_ARM),
|
||||
|
||||
@SerialName("leftHand")
|
||||
LEFT_HAND("LeftHand", BoneType.LEFT_HAND, TrackerPosition.LEFT_HAND),
|
||||
|
||||
@SerialName("rightHand")
|
||||
RIGHT_HAND("RightHand", BoneType.RIGHT_HAND, TrackerPosition.RIGHT_HAND),
|
||||
|
||||
@SerialName("leftToes")
|
||||
LEFT_TOES("LeftToes", null, null),
|
||||
|
||||
@SerialName("rightToes")
|
||||
RIGHT_TOES("RightToes", null, null),
|
||||
|
||||
@SerialName("leftEye")
|
||||
LEFT_EYE("LeftEye", null, null),
|
||||
|
||||
@SerialName("rightEye")
|
||||
RIGHT_EYE("RightEye", null, null),
|
||||
|
||||
@SerialName("jaw")
|
||||
JAW("Jaw", null, null),
|
||||
|
||||
@SerialName("leftThumbMetacarpal")
|
||||
LEFT_THUMB_PROXIMAL("LeftThumbProximal", BoneType.LEFT_THUMB_METACARPAL, TrackerPosition.LEFT_THUMB_METACARPAL),
|
||||
|
||||
@SerialName("leftThumbProximal")
|
||||
LEFT_THUMB_INTERMEDIATE("LeftThumbIntermediate", BoneType.LEFT_THUMB_PROXIMAL, TrackerPosition.LEFT_THUMB_PROXIMAL),
|
||||
|
||||
@SerialName("leftThumbDistal")
|
||||
LEFT_THUMB_DISTAL("LeftThumbDistal", BoneType.LEFT_THUMB_DISTAL, TrackerPosition.LEFT_THUMB_DISTAL),
|
||||
|
||||
@SerialName("leftIndexProximal")
|
||||
LEFT_INDEX_PROXIMAL("LeftIndexProximal", BoneType.LEFT_INDEX_PROXIMAL, TrackerPosition.LEFT_INDEX_PROXIMAL),
|
||||
|
||||
@SerialName("leftIndexIntermediate")
|
||||
LEFT_INDEX_INTERMEDIATE("LeftIndexIntermediate", BoneType.LEFT_INDEX_INTERMEDIATE, TrackerPosition.LEFT_INDEX_INTERMEDIATE),
|
||||
|
||||
@SerialName("leftIndexDistal")
|
||||
LEFT_INDEX_DISTAL("LeftIndexDistal", BoneType.LEFT_INDEX_DISTAL, TrackerPosition.LEFT_INDEX_DISTAL),
|
||||
|
||||
@SerialName("leftMiddleProximal")
|
||||
LEFT_MIDDLE_PROXIMAL("LeftMiddleProximal", BoneType.LEFT_MIDDLE_PROXIMAL, TrackerPosition.LEFT_MIDDLE_PROXIMAL),
|
||||
|
||||
@SerialName("leftMiddleIntermediate")
|
||||
LEFT_MIDDLE_INTERMEDIATE("LeftMiddleIntermediate", BoneType.LEFT_MIDDLE_INTERMEDIATE, TrackerPosition.LEFT_MIDDLE_INTERMEDIATE),
|
||||
|
||||
@SerialName("leftMiddleDistal")
|
||||
LEFT_MIDDLE_DISTAL("LeftMiddleDistal", BoneType.LEFT_MIDDLE_DISTAL, TrackerPosition.LEFT_MIDDLE_DISTAL),
|
||||
|
||||
@SerialName("leftRingProximal")
|
||||
LEFT_RING_PROXIMAL("LeftRingProximal", BoneType.LEFT_RING_PROXIMAL, TrackerPosition.LEFT_RING_PROXIMAL),
|
||||
|
||||
@SerialName("leftRingIntermediate")
|
||||
LEFT_RING_INTERMEDIATE("LeftRingIntermediate", BoneType.LEFT_RING_INTERMEDIATE, TrackerPosition.LEFT_RING_INTERMEDIATE),
|
||||
|
||||
@SerialName("leftRingDistal")
|
||||
LEFT_RING_DISTAL("LeftRingDistal", BoneType.LEFT_RING_DISTAL, TrackerPosition.LEFT_RING_DISTAL),
|
||||
|
||||
@SerialName("leftLittleProximal")
|
||||
LEFT_LITTLE_PROXIMAL("LeftLittleProximal", BoneType.LEFT_LITTLE_PROXIMAL, TrackerPosition.LEFT_LITTLE_PROXIMAL),
|
||||
|
||||
@SerialName("leftLittleIntermediate")
|
||||
LEFT_LITTLE_INTERMEDIATE("LeftLittleIntermediate", BoneType.LEFT_LITTLE_INTERMEDIATE, TrackerPosition.LEFT_LITTLE_INTERMEDIATE),
|
||||
|
||||
@SerialName("leftLittleDistal")
|
||||
LEFT_LITTLE_DISTAL("LeftLittleDistal", BoneType.LEFT_LITTLE_DISTAL, TrackerPosition.LEFT_LITTLE_DISTAL),
|
||||
|
||||
@SerialName("rightThumbMetacarpal")
|
||||
RIGHT_THUMB_PROXIMAL("RightThumbProximal", BoneType.RIGHT_THUMB_METACARPAL, TrackerPosition.RIGHT_THUMB_METACARPAL),
|
||||
|
||||
@SerialName("rightThumbProximal")
|
||||
RIGHT_THUMB_INTERMEDIATE("RightThumbIntermediate", BoneType.RIGHT_THUMB_PROXIMAL, TrackerPosition.RIGHT_THUMB_PROXIMAL),
|
||||
|
||||
@SerialName("rightThumbDistal")
|
||||
RIGHT_THUMB_DISTAL("RightThumbDistal", BoneType.RIGHT_THUMB_DISTAL, TrackerPosition.RIGHT_THUMB_DISTAL),
|
||||
|
||||
@SerialName("rightIndexProximal")
|
||||
RIGHT_INDEX_PROXIMAL("RightIndexProximal", BoneType.RIGHT_INDEX_PROXIMAL, TrackerPosition.RIGHT_INDEX_PROXIMAL),
|
||||
|
||||
@SerialName("rightIndexIntermediate")
|
||||
RIGHT_INDEX_INTERMEDIATE("RightIndexIntermediate", BoneType.RIGHT_INDEX_INTERMEDIATE, TrackerPosition.RIGHT_INDEX_INTERMEDIATE),
|
||||
|
||||
@SerialName("rightIndexDistal")
|
||||
RIGHT_INDEX_DISTAL("RightIndexDistal", BoneType.RIGHT_INDEX_DISTAL, TrackerPosition.RIGHT_INDEX_DISTAL),
|
||||
|
||||
@SerialName("rightMiddleProximal")
|
||||
RIGHT_MIDDLE_PROXIMAL("RightMiddleProximal", BoneType.RIGHT_MIDDLE_PROXIMAL, TrackerPosition.RIGHT_MIDDLE_PROXIMAL),
|
||||
|
||||
@SerialName("rightMiddleIntermediate")
|
||||
RIGHT_MIDDLE_INTERMEDIATE("RightMiddleIntermediate", BoneType.RIGHT_MIDDLE_INTERMEDIATE, TrackerPosition.RIGHT_MIDDLE_INTERMEDIATE),
|
||||
|
||||
@SerialName("rightMiddleDistal")
|
||||
RIGHT_MIDDLE_DISTAL("RightMiddleDistal", BoneType.RIGHT_MIDDLE_DISTAL, TrackerPosition.RIGHT_MIDDLE_DISTAL),
|
||||
|
||||
@SerialName("rightRingProximal")
|
||||
RIGHT_RING_PROXIMAL("RightRingProximal", BoneType.RIGHT_RING_PROXIMAL, TrackerPosition.RIGHT_RING_PROXIMAL),
|
||||
|
||||
@SerialName("rightRingIntermediate")
|
||||
RIGHT_RING_INTERMEDIATE("RightRingIntermediate", BoneType.RIGHT_RING_INTERMEDIATE, TrackerPosition.RIGHT_RING_INTERMEDIATE),
|
||||
|
||||
@SerialName("rightRingDistal")
|
||||
RIGHT_RING_DISTAL("RightRingDistal", BoneType.RIGHT_RING_DISTAL, TrackerPosition.RIGHT_RING_DISTAL),
|
||||
|
||||
@SerialName("rightLittleProximal")
|
||||
RIGHT_LITTLE_PROXIMAL("RightLittleProximal", BoneType.RIGHT_LITTLE_PROXIMAL, TrackerPosition.RIGHT_LITTLE_PROXIMAL),
|
||||
|
||||
@SerialName("rightLittleIntermediate")
|
||||
RIGHT_LITTLE_INTERMEDIATE("RightLittleIntermediate", BoneType.RIGHT_LITTLE_INTERMEDIATE, TrackerPosition.RIGHT_LITTLE_INTERMEDIATE),
|
||||
|
||||
@SerialName("rightLittleDistal")
|
||||
RIGHT_LITTLE_DISTAL("RightLittleDistal", BoneType.RIGHT_LITTLE_DISTAL, TrackerPosition.RIGHT_LITTLE_DISTAL),
|
||||
|
||||
LAST_BONE("LastBone", null, null),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private val byStringVal: Map<String, UnityBone> = values().associateBy { it.stringVal.lowercase() }
|
||||
|
||||
@JvmStatic
|
||||
fun getByStringVal(stringVal: String): UnityBone? = byStringVal[stringVal.lowercase()]
|
||||
|
||||
/**
|
||||
* Returns the bone on the opposite limb, or the original bone if
|
||||
* it not a limb bone.
|
||||
*/
|
||||
fun tryGetOppositeArmBone(bone: UnityBone): UnityBone = when (bone) {
|
||||
LEFT_SHOULDER -> RIGHT_SHOULDER
|
||||
LEFT_UPPER_ARM -> RIGHT_UPPER_ARM
|
||||
LEFT_LOWER_ARM -> RIGHT_LOWER_ARM
|
||||
LEFT_HAND -> RIGHT_HAND
|
||||
RIGHT_SHOULDER -> LEFT_SHOULDER
|
||||
RIGHT_UPPER_ARM -> LEFT_UPPER_ARM
|
||||
RIGHT_LOWER_ARM -> LEFT_LOWER_ARM
|
||||
RIGHT_HAND -> LEFT_HAND
|
||||
LEFT_UPPER_LEG -> RIGHT_UPPER_LEG
|
||||
LEFT_LOWER_LEG -> RIGHT_LOWER_LEG
|
||||
LEFT_FOOT -> RIGHT_FOOT
|
||||
RIGHT_UPPER_LEG -> LEFT_UPPER_LEG
|
||||
RIGHT_LOWER_LEG -> LEFT_LOWER_LEG
|
||||
RIGHT_FOOT -> LEFT_FOOT
|
||||
LEFT_THUMB_PROXIMAL -> RIGHT_THUMB_PROXIMAL
|
||||
LEFT_THUMB_INTERMEDIATE -> RIGHT_THUMB_INTERMEDIATE
|
||||
LEFT_THUMB_DISTAL -> RIGHT_THUMB_DISTAL
|
||||
LEFT_INDEX_PROXIMAL -> RIGHT_INDEX_PROXIMAL
|
||||
LEFT_INDEX_INTERMEDIATE -> RIGHT_INDEX_INTERMEDIATE
|
||||
LEFT_INDEX_DISTAL -> RIGHT_INDEX_DISTAL
|
||||
LEFT_MIDDLE_PROXIMAL -> RIGHT_MIDDLE_PROXIMAL
|
||||
LEFT_MIDDLE_INTERMEDIATE -> RIGHT_MIDDLE_INTERMEDIATE
|
||||
LEFT_MIDDLE_DISTAL -> RIGHT_MIDDLE_DISTAL
|
||||
LEFT_RING_PROXIMAL -> RIGHT_RING_PROXIMAL
|
||||
LEFT_RING_INTERMEDIATE -> RIGHT_RING_INTERMEDIATE
|
||||
LEFT_RING_DISTAL -> RIGHT_RING_DISTAL
|
||||
LEFT_LITTLE_PROXIMAL -> RIGHT_LITTLE_PROXIMAL
|
||||
LEFT_LITTLE_INTERMEDIATE -> RIGHT_LITTLE_INTERMEDIATE
|
||||
LEFT_LITTLE_DISTAL -> RIGHT_LITTLE_DISTAL
|
||||
RIGHT_THUMB_PROXIMAL -> LEFT_THUMB_PROXIMAL
|
||||
RIGHT_THUMB_INTERMEDIATE -> LEFT_THUMB_INTERMEDIATE
|
||||
RIGHT_THUMB_DISTAL -> LEFT_THUMB_DISTAL
|
||||
RIGHT_INDEX_PROXIMAL -> LEFT_INDEX_PROXIMAL
|
||||
RIGHT_INDEX_INTERMEDIATE -> LEFT_INDEX_INTERMEDIATE
|
||||
RIGHT_INDEX_DISTAL -> LEFT_INDEX_DISTAL
|
||||
RIGHT_MIDDLE_PROXIMAL -> LEFT_MIDDLE_PROXIMAL
|
||||
RIGHT_MIDDLE_INTERMEDIATE -> LEFT_MIDDLE_INTERMEDIATE
|
||||
RIGHT_MIDDLE_DISTAL -> LEFT_MIDDLE_DISTAL
|
||||
RIGHT_RING_PROXIMAL -> LEFT_RING_PROXIMAL
|
||||
RIGHT_RING_INTERMEDIATE -> LEFT_RING_INTERMEDIATE
|
||||
RIGHT_RING_DISTAL -> LEFT_RING_DISTAL
|
||||
RIGHT_LITTLE_PROXIMAL -> LEFT_LITTLE_PROXIMAL
|
||||
RIGHT_LITTLE_INTERMEDIATE -> LEFT_LITTLE_INTERMEDIATE
|
||||
RIGHT_LITTLE_DISTAL -> LEFT_LITTLE_DISTAL
|
||||
else -> bone
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the bone is part of the left arm (incl. fingers, excl. shoulder)
|
||||
*/
|
||||
fun isLeftArmBone(bone: UnityBone): Boolean = bone == LEFT_UPPER_ARM ||
|
||||
bone == LEFT_LOWER_ARM ||
|
||||
bone == LEFT_HAND ||
|
||||
bone == LEFT_THUMB_PROXIMAL ||
|
||||
bone == LEFT_THUMB_INTERMEDIATE ||
|
||||
bone == LEFT_THUMB_DISTAL ||
|
||||
bone == LEFT_INDEX_PROXIMAL ||
|
||||
bone == LEFT_INDEX_INTERMEDIATE ||
|
||||
bone == LEFT_INDEX_DISTAL ||
|
||||
bone == LEFT_MIDDLE_PROXIMAL ||
|
||||
bone == LEFT_MIDDLE_INTERMEDIATE ||
|
||||
bone == LEFT_MIDDLE_DISTAL ||
|
||||
bone == LEFT_RING_PROXIMAL ||
|
||||
bone == LEFT_RING_INTERMEDIATE ||
|
||||
bone == LEFT_RING_DISTAL ||
|
||||
bone == LEFT_LITTLE_PROXIMAL ||
|
||||
bone == LEFT_LITTLE_INTERMEDIATE ||
|
||||
bone == LEFT_LITTLE_DISTAL
|
||||
|
||||
/**
|
||||
* Returns true if the bone is part of the right arm (incl. fingers, excl. shoulder)
|
||||
*/
|
||||
fun isRightArmBone(bone: UnityBone): Boolean = bone == RIGHT_UPPER_ARM ||
|
||||
bone == RIGHT_LOWER_ARM ||
|
||||
bone == RIGHT_HAND ||
|
||||
bone == RIGHT_THUMB_PROXIMAL ||
|
||||
bone == RIGHT_THUMB_INTERMEDIATE ||
|
||||
bone == RIGHT_THUMB_DISTAL ||
|
||||
bone == RIGHT_INDEX_PROXIMAL ||
|
||||
bone == RIGHT_INDEX_INTERMEDIATE ||
|
||||
bone == RIGHT_INDEX_DISTAL ||
|
||||
bone == RIGHT_MIDDLE_PROXIMAL ||
|
||||
bone == RIGHT_MIDDLE_INTERMEDIATE ||
|
||||
bone == RIGHT_MIDDLE_DISTAL ||
|
||||
bone == RIGHT_RING_PROXIMAL ||
|
||||
bone == RIGHT_RING_INTERMEDIATE ||
|
||||
bone == RIGHT_RING_DISTAL ||
|
||||
bone == RIGHT_LITTLE_PROXIMAL ||
|
||||
bone == RIGHT_LITTLE_INTERMEDIATE ||
|
||||
bone == RIGHT_LITTLE_DISTAL
|
||||
|
||||
/**
|
||||
* Returns true if the bone is the left upper arm or proximal left finger bone
|
||||
*/
|
||||
fun isLeftStartOfArmOrFingerBone(bone: UnityBone): Boolean = bone == LEFT_UPPER_ARM ||
|
||||
bone == LEFT_THUMB_PROXIMAL ||
|
||||
bone == LEFT_INDEX_PROXIMAL ||
|
||||
bone == LEFT_MIDDLE_PROXIMAL ||
|
||||
bone == LEFT_RING_PROXIMAL ||
|
||||
bone == LEFT_LITTLE_PROXIMAL
|
||||
|
||||
/**
|
||||
* Returns true if the bone is the right upper arm or proximal right finger bone
|
||||
*/
|
||||
fun isRightStartOfArmOrFingerBone(bone: UnityBone): Boolean = bone == RIGHT_UPPER_ARM ||
|
||||
bone == RIGHT_THUMB_PROXIMAL ||
|
||||
bone == RIGHT_INDEX_PROXIMAL ||
|
||||
bone == RIGHT_MIDDLE_PROXIMAL ||
|
||||
bone == RIGHT_RING_PROXIMAL ||
|
||||
bone == RIGHT_LITTLE_PROXIMAL
|
||||
|
||||
/**
|
||||
* Returns true if the bone is part of the left fingers
|
||||
*/
|
||||
fun isLeftFingerBone(bone: UnityBone): Boolean = bone == LEFT_THUMB_PROXIMAL ||
|
||||
bone == LEFT_THUMB_INTERMEDIATE ||
|
||||
bone == LEFT_THUMB_DISTAL ||
|
||||
bone == LEFT_INDEX_PROXIMAL ||
|
||||
bone == LEFT_INDEX_INTERMEDIATE ||
|
||||
bone == LEFT_INDEX_DISTAL ||
|
||||
bone == LEFT_MIDDLE_PROXIMAL ||
|
||||
bone == LEFT_MIDDLE_INTERMEDIATE ||
|
||||
bone == LEFT_MIDDLE_DISTAL ||
|
||||
bone == LEFT_RING_PROXIMAL ||
|
||||
bone == LEFT_RING_INTERMEDIATE ||
|
||||
bone == LEFT_RING_DISTAL ||
|
||||
bone == LEFT_LITTLE_PROXIMAL ||
|
||||
bone == LEFT_LITTLE_INTERMEDIATE ||
|
||||
bone == LEFT_LITTLE_DISTAL
|
||||
|
||||
/**
|
||||
* Returns true if the bone part of the right fingers
|
||||
*/
|
||||
fun isRightFingerBone(bone: UnityBone): Boolean = bone == RIGHT_THUMB_PROXIMAL ||
|
||||
bone == RIGHT_THUMB_INTERMEDIATE ||
|
||||
bone == RIGHT_THUMB_DISTAL ||
|
||||
bone == RIGHT_INDEX_PROXIMAL ||
|
||||
bone == RIGHT_INDEX_INTERMEDIATE ||
|
||||
bone == RIGHT_INDEX_DISTAL ||
|
||||
bone == RIGHT_MIDDLE_PROXIMAL ||
|
||||
bone == RIGHT_MIDDLE_INTERMEDIATE ||
|
||||
bone == RIGHT_MIDDLE_DISTAL ||
|
||||
bone == RIGHT_RING_PROXIMAL ||
|
||||
bone == RIGHT_RING_INTERMEDIATE ||
|
||||
bone == RIGHT_RING_DISTAL ||
|
||||
bone == RIGHT_LITTLE_PROXIMAL ||
|
||||
bone == RIGHT_LITTLE_INTERMEDIATE ||
|
||||
bone == RIGHT_LITTLE_DISTAL
|
||||
}
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
package dev.slimevr.osc
|
||||
|
||||
import com.illposed.osc.OSCBundle
|
||||
import com.illposed.osc.OSCMessage
|
||||
import com.illposed.osc.OSCMessageEvent
|
||||
import com.illposed.osc.OSCMessageListener
|
||||
import com.illposed.osc.OSCSerializeException
|
||||
import com.illposed.osc.messageselector.OSCPatternAddressMessageSelector
|
||||
import com.illposed.osc.transport.OSCPortIn
|
||||
import com.illposed.osc.transport.OSCPortOut
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.VRServer.Companion.currentLocalTrackerId
|
||||
import dev.slimevr.VRServer.Companion.getNextLocalTrackerId
|
||||
import dev.slimevr.config.VMCConfig
|
||||
import dev.slimevr.osc.UnityBone.Companion.getByStringVal
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import dev.slimevr.tracking.trackers.Device
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import io.github.axisangles.ktmath.Vector3.Companion.NULL
|
||||
import io.github.axisangles.ktmath.Vector3.Companion.POS_Y
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
/**
|
||||
* VMC documentation: https://protocol.vmc.info/english
|
||||
*
|
||||
*
|
||||
* Notes: VMC uses local rotation from hip (unlike SlimeVR, which uses rotations
|
||||
* from head). VMC works with Unity's coordinate system, which means
|
||||
* Quaternions' z and w components and Vectors' z components need to be inverse
|
||||
*/
|
||||
class VMCHandler(
|
||||
private val server: VRServer,
|
||||
private val humanPoseManager: HumanPoseManager,
|
||||
private val config: VMCConfig,
|
||||
) : OSCHandler {
|
||||
private var oscReceiver: OSCPortIn? = null
|
||||
private var oscSender: OSCPortOut? = null
|
||||
private val computedTrackers: MutableList<Tracker> = FastList()
|
||||
private val oscArgs = FastList<Any?>()
|
||||
private val startTime = System.currentTimeMillis()
|
||||
private val byTrackerNameTracker: MutableMap<String, Tracker> = HashMap()
|
||||
private var yawOffset = IDENTITY
|
||||
private var inputUnityArmature: UnityArmature? = null
|
||||
private var outputUnityArmature: UnityArmature? = null
|
||||
private var vrmHeight = 0f
|
||||
private var trackerDevice: Device? = null
|
||||
private var timeAtLastError: Long = 0
|
||||
private var timeAtLastSend: Long = 0
|
||||
private var anchorHip = false
|
||||
private var mirrorTracking = false
|
||||
private var lastPortIn = 0
|
||||
private var lastPortOut = 0
|
||||
private var lastAddress: InetAddress? = null
|
||||
|
||||
init {
|
||||
refreshSettings(false)
|
||||
}
|
||||
|
||||
override fun refreshSettings(refreshRouterSettings: Boolean) {
|
||||
anchorHip = config.anchorHip
|
||||
mirrorTracking = config.mirrorTracking
|
||||
|
||||
updateOscReceiver(
|
||||
config.portIn,
|
||||
arrayOf(
|
||||
"/VMC/Ext/Bone/Pos",
|
||||
"/VMC/Ext/Hmd/Pos",
|
||||
"/VMC/Ext/Con/Pos",
|
||||
"/VMC/Ext/Tra/Pos",
|
||||
"/VMC/Ext/Root/Pos",
|
||||
),
|
||||
)
|
||||
updateOscSender(config.portOut, config.address)
|
||||
|
||||
if (config.enabled) {
|
||||
// Load VRM data
|
||||
if (outputUnityArmature != null && config.vrmJson != null) {
|
||||
val vrmReader = VRMReader(config.vrmJson!!)
|
||||
for (unityBone in UnityBone.entries) {
|
||||
val node = outputUnityArmature!!.getHeadNodeOfBone(unityBone)
|
||||
if (node != null) {
|
||||
val offset = if (unityBone == UnityBone.HIPS) {
|
||||
// For the hip bone, add average of upper leg offsets (which are negative). The hip bone's offset is global.
|
||||
vrmReader.getOffsetForBone(UnityBone.HIPS) +
|
||||
((vrmReader.getOffsetForBone(UnityBone.LEFT_UPPER_LEG) + vrmReader.getOffsetForBone(UnityBone.RIGHT_UPPER_LEG)) / 2f)
|
||||
} else {
|
||||
vrmReader.getOffsetForBone(unityBone)
|
||||
}
|
||||
|
||||
node.localTransform.translation = offset
|
||||
}
|
||||
}
|
||||
// Make sure to account for the upper legs because of the hip.
|
||||
vrmHeight = (
|
||||
vrmReader.getOffsetForBone(UnityBone.HIPS) +
|
||||
((vrmReader.getOffsetForBone(UnityBone.LEFT_UPPER_LEG) + vrmReader.getOffsetForBone(UnityBone.RIGHT_UPPER_LEG))) +
|
||||
vrmReader.getOffsetForBone(UnityBone.SPINE) +
|
||||
vrmReader.getOffsetForBone(UnityBone.CHEST) +
|
||||
vrmReader.getOffsetForBone(UnityBone.UPPER_CHEST) +
|
||||
vrmReader.getOffsetForBone(UnityBone.NECK) +
|
||||
vrmReader.getOffsetForBone(UnityBone.HEAD)
|
||||
).y
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshRouterSettings) server.oSCRouter.refreshSettings(false)
|
||||
}
|
||||
|
||||
override fun updateOscReceiver(portIn: Int, args: Array<String>) {
|
||||
// Stops listening and closes OSC port
|
||||
val wasListening = oscReceiver != null && oscReceiver!!.isListening
|
||||
if (wasListening) {
|
||||
oscReceiver!!.stopListening()
|
||||
}
|
||||
|
||||
if (config.enabled) {
|
||||
// Instantiates the OSC receiver
|
||||
try {
|
||||
oscReceiver = OSCPortIn(portIn)
|
||||
if (lastPortIn != portIn || !wasListening) {
|
||||
LogManager.info("[VMCHandler] Listening to port $portIn")
|
||||
}
|
||||
lastPortIn = portIn
|
||||
} catch (e: IOException) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[VMCHandler] Error listening to the port $portIn: $e",
|
||||
)
|
||||
}
|
||||
|
||||
// Starts listening for VMC messages
|
||||
if (oscReceiver != null) {
|
||||
val listener = OSCMessageListener { event: OSCMessageEvent -> this.handleReceivedMessage(event) }
|
||||
|
||||
for (address in args) {
|
||||
oscReceiver!!
|
||||
.dispatcher
|
||||
.addListener(OSCPatternAddressMessageSelector(address), listener)
|
||||
}
|
||||
|
||||
oscReceiver!!.startListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateOscSender(portOut: Int, ip: String) {
|
||||
// Stop sending
|
||||
val wasConnected = oscSender != null && oscSender!!.isConnected
|
||||
if (wasConnected) {
|
||||
try {
|
||||
oscSender!!.close()
|
||||
} catch (e: IOException) {
|
||||
LogManager.severe("[VMCHandler] Error closing the OSC sender: $e")
|
||||
}
|
||||
}
|
||||
|
||||
if (config.enabled) {
|
||||
// Instantiate the OSC sender
|
||||
try {
|
||||
val addr = InetAddress.getByName(ip)
|
||||
oscSender = OSCPortOut(InetSocketAddress(addr, portOut))
|
||||
if ((lastPortOut != portOut && lastAddress != addr) || !wasConnected) {
|
||||
LogManager
|
||||
.info(
|
||||
"[VMCHandler] Sending to port $portOut at address $ip",
|
||||
)
|
||||
}
|
||||
lastPortOut = portOut
|
||||
lastAddress = addr
|
||||
|
||||
oscSender!!.connect()
|
||||
outputUnityArmature = UnityArmature(false)
|
||||
} catch (e: IOException) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[VMCHandler] Error connecting to port $portOut at the address $ip: $e",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceivedMessage(event: OSCMessageEvent) {
|
||||
when (event.message.address) {
|
||||
// Is bone (rotation)
|
||||
"/VMC/Ext/Bone/Pos" -> {
|
||||
var trackerPosition: TrackerPosition? = null
|
||||
val bone = getByStringVal(event.message.arguments[0].toString())
|
||||
if (bone != null) trackerPosition = bone.trackerPosition
|
||||
|
||||
// If received bone is part of SlimeVR's skeleton
|
||||
if (trackerPosition != null) {
|
||||
handleReceivedTracker(
|
||||
"VMC-Bone-" + event.message.arguments[0],
|
||||
trackerPosition,
|
||||
null,
|
||||
Quaternion(
|
||||
-(event.message.arguments[7] as Float),
|
||||
event.message.arguments[4] as Float,
|
||||
event.message.arguments[5] as Float,
|
||||
-(event.message.arguments[6] as Float),
|
||||
),
|
||||
true,
|
||||
getByStringVal(
|
||||
event.message.arguments[0].toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Is tracker (position + rotation)
|
||||
"/VMC/Ext/Hmd/Pos", "/VMC/Ext/Con/Pos", "/VMC/Ext/Tra/Pos" ->
|
||||
handleReceivedTracker(
|
||||
"VMC-Tracker-" + event.message.arguments[0],
|
||||
null,
|
||||
Vector3(
|
||||
event.message.arguments[1] as Float,
|
||||
event.message.arguments[2] as Float,
|
||||
-(event.message.arguments[3] as Float),
|
||||
),
|
||||
Quaternion(
|
||||
-(event.message.arguments[7] as Float),
|
||||
event.message.arguments[4] as Float,
|
||||
event.message.arguments[5] as Float,
|
||||
-(event.message.arguments[6] as Float),
|
||||
),
|
||||
false,
|
||||
null,
|
||||
)
|
||||
|
||||
// Is VMC tracking root (offsets all rotations)
|
||||
"/VMC/Ext/Root/Pos" -> {
|
||||
if (inputUnityArmature != null) {
|
||||
inputUnityArmature!!
|
||||
.setRootPose(
|
||||
Vector3(
|
||||
event.message.arguments[1] as Float,
|
||||
event.message.arguments[2] as Float,
|
||||
-(event.message.arguments[3] as Float),
|
||||
),
|
||||
Quaternion(
|
||||
-(event.message.arguments[7] as Float),
|
||||
event.message.arguments[4] as Float,
|
||||
event.message.arguments[5] as Float,
|
||||
-(event.message.arguments[6] as Float),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceivedTracker(
|
||||
name: String,
|
||||
trackerPosition: TrackerPosition?,
|
||||
position: Vector3?,
|
||||
rotation: Quaternion,
|
||||
localRotation: Boolean,
|
||||
unityBone: UnityBone?,
|
||||
) {
|
||||
// Create device if it doesn't exist
|
||||
var rot = rotation
|
||||
if (trackerDevice == null) {
|
||||
trackerDevice = server.deviceManager.createDevice("VMC receiver", "1.0", "VMC")
|
||||
server.deviceManager.addDevice(trackerDevice!!)
|
||||
}
|
||||
|
||||
// Try to get tracker
|
||||
var tracker = byTrackerNameTracker[name]
|
||||
|
||||
// Create tracker if trying to get it returned null
|
||||
if (tracker == null) {
|
||||
tracker = Tracker(
|
||||
trackerDevice,
|
||||
getNextLocalTrackerId(),
|
||||
name,
|
||||
"VMC Tracker #$currentLocalTrackerId",
|
||||
trackerPosition,
|
||||
hasPosition = position != null,
|
||||
hasRotation = true,
|
||||
userEditable = true,
|
||||
isComputed = position != null,
|
||||
usesTimeout = true,
|
||||
allowReset = position != null,
|
||||
)
|
||||
trackerDevice!!.trackers[trackerDevice!!.trackers.size] = tracker
|
||||
byTrackerNameTracker[name] = tracker
|
||||
server.registerTracker(tracker)
|
||||
}
|
||||
tracker.status = TrackerStatus.OK
|
||||
|
||||
// Set position
|
||||
if (position != null) {
|
||||
tracker.position = position
|
||||
}
|
||||
|
||||
// Set rotation
|
||||
if (localRotation) {
|
||||
// Instantiate unityHierarchy if not done
|
||||
if (inputUnityArmature == null) inputUnityArmature = UnityArmature(true)
|
||||
inputUnityArmature!!.setLocalRotationForBone(unityBone!!, rot)
|
||||
rot = inputUnityArmature!!.getGlobalRotationForBone(unityBone)
|
||||
rot = yawOffset.times(rot)
|
||||
}
|
||||
tracker.setRotation(rot)
|
||||
|
||||
tracker.dataTick()
|
||||
}
|
||||
|
||||
override fun update() {
|
||||
// Update unity hierarchy
|
||||
if (inputUnityArmature != null) inputUnityArmature!!.update()
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - timeAtLastSend > 3) { // 200hz to not crash VSF
|
||||
timeAtLastSend = currentTime
|
||||
// Send OSC data
|
||||
if (oscSender != null && oscSender!!.isConnected) {
|
||||
// Create new OSC Bundle
|
||||
val oscBundle = OSCBundle()
|
||||
|
||||
// Add our relative time
|
||||
oscArgs.clear()
|
||||
oscArgs.add((System.currentTimeMillis() - startTime) / 1000f)
|
||||
oscBundle.addPacket(OSCMessage("/VMC/Ext/T", oscArgs.clone()))
|
||||
|
||||
if (humanPoseManager.isSkeletonPresent) {
|
||||
// Indicate tracking is available
|
||||
oscArgs.clear()
|
||||
oscArgs.add(1)
|
||||
oscBundle.addPacket(OSCMessage("/VMC/Ext/OK", oscArgs.clone()))
|
||||
|
||||
oscArgs.clear()
|
||||
oscArgs.add("root")
|
||||
addTransformToArgs(NULL, IDENTITY)
|
||||
oscBundle.addPacket(OSCMessage("/VMC/Ext/Root/Pos", oscArgs.clone()))
|
||||
|
||||
for (unityBone in UnityBone.entries) {
|
||||
// Get opposite bone if tracking must be mirrored
|
||||
val boneType = (if (mirrorTracking) UnityBone.tryGetOppositeArmBone(unityBone) else unityBone).boneType
|
||||
|
||||
if (boneType == null) continue
|
||||
|
||||
// Get SlimeVR bone
|
||||
val bone = humanPoseManager.getBone(boneType)
|
||||
|
||||
// Update unity hierarchy from bone's global rotation
|
||||
val boneRotation = if (mirrorTracking) {
|
||||
// Mirror tracking horizontally
|
||||
val rotBuf = bone.getGlobalRotation() * bone.rotationOffset.inv()
|
||||
Quaternion(rotBuf.w, rotBuf.x, -rotBuf.y, -rotBuf.z)
|
||||
} else {
|
||||
bone.getGlobalRotation() * bone.rotationOffset.inv()
|
||||
}
|
||||
outputUnityArmature?.setGlobalRotationForBone(unityBone, boneRotation)
|
||||
}
|
||||
|
||||
if (!anchorHip) {
|
||||
// Anchor from head
|
||||
outputUnityArmature?.let { unityArmature ->
|
||||
// Scale the SlimeVR neck position with the VRM model
|
||||
// We're only getting the height up to the neck because we don't want to factor the neck's length into the scaling
|
||||
val slimevrScaledRootPos = humanPoseManager.getBone(BoneType.NECK).getTailPosition() *
|
||||
(vrmHeight / humanPoseManager.userNeckHeightFromConfig)
|
||||
|
||||
// Get the VRM head and hip positions
|
||||
val vrmHeadPos = unityArmature.getHeadNodeOfBone(UnityBone.HEAD)!!.parent!!.worldTransform.translation
|
||||
val vrmHipPos = unityArmature.getHeadNodeOfBone(UnityBone.HIPS)!!.worldTransform.translation
|
||||
|
||||
// Calculate the new VRM hip position by subtracting the difference head-hip distance from the SlimeVR head
|
||||
val calculatedVrmHipPos = slimevrScaledRootPos - (vrmHeadPos - vrmHipPos)
|
||||
|
||||
// Set the VRM's hip position
|
||||
unityArmature.getHeadNodeOfBone(UnityBone.HIPS)?.localTransform?.translation = calculatedVrmHipPos
|
||||
}
|
||||
}
|
||||
|
||||
// Update Unity skeleton
|
||||
outputUnityArmature?.update()
|
||||
|
||||
// Add Unity humanoid bones transforms
|
||||
for (unityBone in UnityBone.entries) {
|
||||
// Don't send bones for which we don't have an equivalent
|
||||
// Don't send fingers if we don't have any tracker for them
|
||||
// Don't send arm bones if we're tracking from the controller
|
||||
if (unityBone.boneType != null &&
|
||||
(!UnityBone.isLeftFingerBone(unityBone) || humanPoseManager.skeleton.hasLeftFingerTracker || (mirrorTracking && humanPoseManager.skeleton.hasRightFingerTracker)) &&
|
||||
(!UnityBone.isRightFingerBone(unityBone) || humanPoseManager.skeleton.hasRightFingerTracker || (mirrorTracking && humanPoseManager.skeleton.hasLeftFingerTracker)) &&
|
||||
!(humanPoseManager.isTrackingLeftArmFromController && (UnityBone.isLeftArmBone(unityBone) || unityBone == UnityBone.LEFT_SHOULDER)) &&
|
||||
!(humanPoseManager.isTrackingRightArmFromController && (UnityBone.isRightArmBone(unityBone) || unityBone == UnityBone.RIGHT_SHOULDER))
|
||||
) {
|
||||
oscArgs.clear()
|
||||
oscArgs.add(unityBone.stringVal)
|
||||
outputUnityArmature?.let {
|
||||
addTransformToArgs(
|
||||
it.getLocalTranslationForBone(unityBone),
|
||||
it.getLocalRotationForBone(unityBone),
|
||||
)
|
||||
}
|
||||
|
||||
oscBundle.addPacket(OSCMessage("/VMC/Ext/Bone/Pos", oscArgs.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (tracker in computedTrackers) {
|
||||
if (!tracker.status.reset) {
|
||||
oscArgs.clear()
|
||||
|
||||
val name = tracker.name
|
||||
oscArgs.add(name)
|
||||
|
||||
addTransformToArgs(
|
||||
tracker.position,
|
||||
tracker.getRotation(),
|
||||
)
|
||||
|
||||
var address: String
|
||||
val role = tracker.trackerPosition
|
||||
address = if (role == TrackerPosition.HEAD) {
|
||||
"/VMC/Ext/Hmd/Pos"
|
||||
} else if (role == TrackerPosition.LEFT_HAND || role == TrackerPosition.RIGHT_HAND) {
|
||||
"/VMC/Ext/Con/Pos"
|
||||
} else {
|
||||
"/VMC/Ext/Tra/Pos"
|
||||
}
|
||||
oscBundle
|
||||
.addPacket(
|
||||
OSCMessage(
|
||||
address,
|
||||
oscArgs.clone(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Send OSC packets as bundle
|
||||
try {
|
||||
oscSender!!.send(oscBundle)
|
||||
} catch (e: IOException) {
|
||||
// Avoid spamming AsynchronousCloseException too many
|
||||
// times per second
|
||||
if (System.currentTimeMillis() - timeAtLastError > 100) {
|
||||
timeAtLastError = System.currentTimeMillis()
|
||||
LogManager
|
||||
.warning(
|
||||
"[VMCHandler] Error sending OSC packets: " +
|
||||
e,
|
||||
)
|
||||
}
|
||||
} catch (e: OSCSerializeException) {
|
||||
if (System.currentTimeMillis() - timeAtLastError > 100) {
|
||||
timeAtLastError = System.currentTimeMillis()
|
||||
LogManager
|
||||
.warning(
|
||||
"[VMCHandler] Error sending OSC packets: " +
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Quaternion to shift the received VMC tracking rotations' yaw
|
||||
*
|
||||
* @param reference the head's rotation
|
||||
*/
|
||||
fun alignVMCTracking(reference: Quaternion) {
|
||||
yawOffset = reference.project(POS_Y).unit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a computed tracker to the list of trackers to send.
|
||||
*
|
||||
* @param computedTracker the computed tracker
|
||||
*/
|
||||
fun addComputedTracker(computedTracker: Tracker) {
|
||||
computedTrackers.add(computedTracker)
|
||||
}
|
||||
|
||||
private fun addTransformToArgs(pos: Vector3, rot: Quaternion) {
|
||||
oscArgs.add(pos.x)
|
||||
oscArgs.add(pos.y)
|
||||
oscArgs.add(-pos.z)
|
||||
oscArgs.add(rot.x)
|
||||
oscArgs.add(rot.y)
|
||||
oscArgs.add(-rot.z)
|
||||
oscArgs.add(-rot.w)
|
||||
}
|
||||
|
||||
override fun getOscSender(): OSCPortOut = oscSender!!
|
||||
|
||||
override fun getPortOut(): Int = lastPortOut
|
||||
|
||||
override fun getAddress(): InetAddress = lastAddress!!
|
||||
|
||||
override fun getOscReceiver(): OSCPortIn = oscReceiver!!
|
||||
|
||||
override fun getPortIn(): Int = lastPortIn
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
package dev.slimevr.osc
|
||||
|
||||
import com.illposed.osc.OSCBundle
|
||||
import com.illposed.osc.OSCMessage
|
||||
import com.illposed.osc.OSCMessageEvent
|
||||
import com.illposed.osc.OSCMessageListener
|
||||
import com.illposed.osc.OSCSerializeException
|
||||
import com.illposed.osc.messageselector.OSCPatternAddressMessageSelector
|
||||
import com.illposed.osc.transport.OSCPortIn
|
||||
import com.illposed.osc.transport.OSCPortOut
|
||||
import com.jme3.math.FastMath
|
||||
import com.jme3.system.NanoTimer
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.config.VRCOSCConfig
|
||||
import dev.slimevr.protocol.rpc.setup.RPCUtil
|
||||
import dev.slimevr.tracking.trackers.Device
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import io.github.axisangles.ktmath.EulerAngles
|
||||
import io.github.axisangles.ktmath.EulerOrder
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import java.io.IOException
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
private const val OFFSET_SLERP_FACTOR = 0.5f // Guessed from eyeing VRChat
|
||||
|
||||
/**
|
||||
* VRChat OSCTracker documentation: https://docs.vrchat.com/docs/osc-trackers
|
||||
*/
|
||||
class VRCOSCHandler(
|
||||
private val server: VRServer,
|
||||
private val config: VRCOSCConfig,
|
||||
private val computedTrackers: List<Tracker>,
|
||||
) : OSCHandler {
|
||||
private val localIp = RPCUtil.getLocalIp()
|
||||
private val loopbackIp = InetAddress.getLoopbackAddress().hostAddress
|
||||
private val vrsystemTrackersAddresses = arrayOf(
|
||||
"/tracking/vrsystem/head/pose",
|
||||
"/tracking/vrsystem/leftwrist/pose",
|
||||
"/tracking/vrsystem/rightwrist/pose",
|
||||
)
|
||||
private val oscTrackersAddresses = arrayOf(
|
||||
"/tracking/trackers/*/position",
|
||||
"/tracking/trackers/*/rotation",
|
||||
)
|
||||
private var oscReceiver: OSCPortIn? = null
|
||||
private var oscSender: OSCPortOut? = null
|
||||
private var oscQuerySender: OSCPortOut? = null
|
||||
private var oscMessage: OSCMessage? = null
|
||||
private var headTracker: Tracker? = null
|
||||
private var oscTrackersDevice: Device? = null
|
||||
private var vrsystemTrackersDevice: Device? = null
|
||||
private val oscArgs = FastList<Float?>(3)
|
||||
private val trackersEnabled: BooleanArray = BooleanArray(computedTrackers.size)
|
||||
private var oscPortIn = 0
|
||||
private var oscPortOut = 0
|
||||
private var oscIp: InetAddress? = null
|
||||
private var oscQuerySenderState = false
|
||||
private var oscQueryPortOut = 0
|
||||
private var oscQueryIp: String? = null
|
||||
private var timeAtLastError: Long = 0
|
||||
private var receivingPositionOffset = Vector3.NULL
|
||||
private var postReceivingPositionOffset = Vector3.NULL
|
||||
private var receivingRotationOffset = Quaternion.IDENTITY
|
||||
private var receivingRotationOffsetGoal = Quaternion.IDENTITY
|
||||
private val postReceivingOffset = EulerAngles(EulerOrder.YXZ, 0f, FastMath.PI, 0f).toQuaternion()
|
||||
private var timeAtLastReceivedRotationOffset = System.currentTimeMillis()
|
||||
private var fpsTimer: NanoTimer? = null
|
||||
private var vrcOscQueryHandler: VRCOSCQueryHandler? = null
|
||||
|
||||
init {
|
||||
refreshSettings(false)
|
||||
}
|
||||
|
||||
override fun refreshSettings(refreshRouterSettings: Boolean) {
|
||||
// Sets which trackers are enabled and force head and hands to false
|
||||
for (i in computedTrackers.indices) {
|
||||
if (computedTrackers[i].trackerPosition != TrackerPosition.HEAD || computedTrackers[i].trackerPosition != TrackerPosition.LEFT_HAND || computedTrackers[i].trackerPosition != TrackerPosition.RIGHT_HAND) {
|
||||
trackersEnabled[i] = config
|
||||
.getOSCTrackerRole(
|
||||
computedTrackers[i].trackerPosition!!.trackerRole!!,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
trackersEnabled[i] = false
|
||||
}
|
||||
}
|
||||
|
||||
updateOscReceiver(config.portIn, vrsystemTrackersAddresses + oscTrackersAddresses)
|
||||
updateOscSender(config.portOut, config.address)
|
||||
|
||||
if (vrcOscQueryHandler == null && config.enabled && config.oscqueryEnabled) {
|
||||
try {
|
||||
vrcOscQueryHandler = VRCOSCQueryHandler(this)
|
||||
} catch (e: Throwable) {
|
||||
LogManager.severe("Unable to initialize OSCQuery: $e", e)
|
||||
}
|
||||
} else if (vrcOscQueryHandler != null && (!config.enabled || !config.oscqueryEnabled)) {
|
||||
vrcOscQueryHandler?.close()
|
||||
vrcOscQueryHandler = null
|
||||
}
|
||||
|
||||
if (refreshRouterSettings) {
|
||||
server.oSCRouter.refreshSettings(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an OSC Sender from OSCQuery
|
||||
*/
|
||||
fun addOSCQuerySender(oscPortOut: Int, oscIP: String) {
|
||||
val addr = InetAddress.getByName(oscIP)
|
||||
oscQuerySenderState = true
|
||||
oscQueryIp = oscIP
|
||||
oscQueryPortOut = oscPortOut
|
||||
if (oscPortOut != portOut || (oscIP != address.hostName && !(oscIP == localIp && address.hostName == loopbackIp))) {
|
||||
try {
|
||||
oscQuerySender = OSCPortOut(InetSocketAddress(addr, oscPortOut))
|
||||
oscQuerySender?.connect()
|
||||
LogManager.info("[VRCOSCHandler] OSCQuery sender sending to port $oscPortOut at address $oscIP")
|
||||
} catch (e: IOException) {
|
||||
LogManager.severe("[VRCOSCHandler] Error connecting to port $oscPortOut at the address $oscIP: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close/remove the osc query sender
|
||||
*/
|
||||
fun closeOscQuerySender(newState: Boolean) {
|
||||
oscQuerySender?.let {
|
||||
try {
|
||||
it.close()
|
||||
oscQuerySender = null
|
||||
oscQuerySenderState = newState
|
||||
} catch (e: IOException) {
|
||||
LogManager.severe("[VRCOSCHandler] Error closing the OSC sender: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateOscReceiver(portIn: Int, args: Array<String>) {
|
||||
// Stop listening
|
||||
val wasListening = oscReceiver != null && oscReceiver!!.isListening
|
||||
if (wasListening) {
|
||||
oscReceiver!!.stopListening()
|
||||
}
|
||||
|
||||
if (config.enabled) {
|
||||
// Instantiates the OSC receiver
|
||||
try {
|
||||
oscReceiver = OSCPortIn(portIn)
|
||||
if (oscPortIn != portIn || !wasListening) {
|
||||
LogManager.info("[VRCOSCHandler] Listening to port $portIn")
|
||||
}
|
||||
oscPortIn = portIn
|
||||
vrcOscQueryHandler?.updateOSCQuery(portIn.toUShort())
|
||||
} catch (e: IOException) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[VRCOSCHandler] Error listening to the port $portIn: $e",
|
||||
)
|
||||
}
|
||||
|
||||
// Starts listening for VRC or OSCTrackers messages
|
||||
oscReceiver?.let {
|
||||
val listener = OSCMessageListener { event: OSCMessageEvent ->
|
||||
handleReceivedMessage(event)
|
||||
}
|
||||
for (address in args) {
|
||||
it.dispatcher.addListener(
|
||||
OSCPatternAddressMessageSelector(address),
|
||||
listener,
|
||||
)
|
||||
}
|
||||
it.startListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateOscSender(portOut: Int, ip: String) {
|
||||
// Stop sending
|
||||
val wasConnected = oscSender != null && oscSender!!.isConnected
|
||||
if (wasConnected) {
|
||||
try {
|
||||
oscSender!!.close()
|
||||
} catch (e: IOException) {
|
||||
LogManager.severe("[VRCOSCHandler] Error closing the OSC sender: $e")
|
||||
}
|
||||
}
|
||||
|
||||
if (config.enabled) {
|
||||
// Instantiate the OSC sender
|
||||
try {
|
||||
val addr = InetAddress.getByName(ip)
|
||||
oscSender = OSCPortOut(InetSocketAddress(addr, portOut))
|
||||
if ((oscPortOut != portOut && oscIp != addr) || !wasConnected) {
|
||||
LogManager.info("[VRCOSCHandler] Sending to port $portOut at address $ip")
|
||||
}
|
||||
oscPortOut = portOut
|
||||
oscIp = addr
|
||||
oscSender?.connect()
|
||||
} catch (e: IOException) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[VRCOSCHandler] Error connecting to port $portOut at the address $ip: $e",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (oscQueryPortOut == portOut && (oscQueryIp == ip || (oscQueryIp == localIp && ip == loopbackIp))) {
|
||||
if (oscQuerySender != null) {
|
||||
// Close the oscQuerySender if it has the same port/ip
|
||||
closeOscQuerySender(true)
|
||||
}
|
||||
} else if (oscQuerySender == null && oscQuerySenderState) {
|
||||
// Instantiate the oscQuerySender if it could not be instantiated.
|
||||
addOSCQuerySender(oscQueryPortOut, oscQueryIp!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceivedMessage(event: OSCMessageEvent) {
|
||||
if (vrsystemTrackersAddresses.contains(event.message.address)) {
|
||||
// Receiving Head and Wrist pose data thanks to OSCQuery
|
||||
// Create device if it doesn't exist
|
||||
if (vrsystemTrackersDevice == null) {
|
||||
// Instantiate OSC Trackers device
|
||||
vrsystemTrackersDevice = server.deviceManager.createDevice("VRC VRSystem", null, "VRChat")
|
||||
server.deviceManager.addDevice(vrsystemTrackersDevice!!)
|
||||
}
|
||||
|
||||
// Look at xxx in "/tracking/vrsystem/xxx/pose" to know TrackerPosition
|
||||
var name = "VRChat "
|
||||
val trackerPosition = when (event.message.address.split('/')[3]) {
|
||||
"head" -> {
|
||||
name += "head"
|
||||
TrackerPosition.HEAD
|
||||
}
|
||||
|
||||
"leftwrist" -> {
|
||||
name += "left hand"
|
||||
TrackerPosition.LEFT_HAND
|
||||
}
|
||||
|
||||
"rightwrist" -> {
|
||||
name += "right hand"
|
||||
TrackerPosition.RIGHT_HAND
|
||||
}
|
||||
|
||||
else -> {
|
||||
LogManager.warning("[VRCOSCHandler] Received invalid body part in message \"${event.message.address}\"")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the tracker
|
||||
var tracker = vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal]
|
||||
|
||||
// Build the tracker if it doesn't exist
|
||||
if (tracker == null) {
|
||||
tracker = Tracker(
|
||||
device = vrsystemTrackersDevice,
|
||||
id = VRServer.getNextLocalTrackerId(),
|
||||
name = name,
|
||||
displayName = name,
|
||||
trackerNum = trackerPosition.ordinal,
|
||||
trackerPosition = trackerPosition,
|
||||
hasRotation = true,
|
||||
hasPosition = true,
|
||||
userEditable = true,
|
||||
isComputed = true,
|
||||
allowReset = trackerPosition != TrackerPosition.HEAD,
|
||||
usesTimeout = true,
|
||||
)
|
||||
vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] = tracker
|
||||
server.registerTracker(tracker)
|
||||
}
|
||||
|
||||
// Sets the tracker status to OK
|
||||
tracker.status = TrackerStatus.OK
|
||||
|
||||
// Update tracker position
|
||||
tracker.position = Vector3(
|
||||
event.message.arguments[0] as Float,
|
||||
event.message.arguments[1] as Float,
|
||||
-(event.message.arguments[2] as Float),
|
||||
)
|
||||
|
||||
// Update tracker rotation
|
||||
val (w, x, y, z) = EulerAngles(
|
||||
EulerOrder.YXZ,
|
||||
event.message.arguments[3] as Float * FastMath.DEG_TO_RAD,
|
||||
event.message.arguments[4] as Float * FastMath.DEG_TO_RAD,
|
||||
event.message.arguments[5] as Float * FastMath.DEG_TO_RAD,
|
||||
).toQuaternion()
|
||||
val rot = Quaternion(w, -x, -y, z)
|
||||
tracker.setRotation(rot)
|
||||
|
||||
tracker.dataTick()
|
||||
} else {
|
||||
// Receiving OSC Trackers data. This is not from VRChat.
|
||||
if (oscTrackersDevice == null) {
|
||||
// Instantiate OSC Trackers device
|
||||
oscTrackersDevice = server.deviceManager.createDevice("OSC Tracker", null, "OSC Trackers")
|
||||
server.deviceManager.addDevice(oscTrackersDevice!!)
|
||||
}
|
||||
|
||||
// Extract the xxx in "/tracking/trackers/xxx/..."
|
||||
val splitAddress = event.message.address.split('/')
|
||||
val trackerStringValue = splitAddress[3]
|
||||
val dataType = event.message.address.split('/')[4]
|
||||
if (trackerStringValue == "head") {
|
||||
// Head data
|
||||
if (dataType == "position") {
|
||||
// Position offset
|
||||
receivingPositionOffset = Vector3(
|
||||
event.message.arguments[0] as Float,
|
||||
event.message.arguments[1] as Float,
|
||||
-(event.message.arguments[2] as Float),
|
||||
)
|
||||
|
||||
headTracker?.let {
|
||||
if (it.hasPosition) {
|
||||
postReceivingPositionOffset = it.position
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Rotation offset
|
||||
val (w, x, y, z) = EulerAngles(EulerOrder.YXZ, event.message.arguments[0] as Float * FastMath.DEG_TO_RAD, event.message.arguments[1] as Float * FastMath.DEG_TO_RAD, event.message.arguments[2] as Float * FastMath.DEG_TO_RAD).toQuaternion()
|
||||
receivingRotationOffsetGoal = Quaternion(w, -x, -y, z).inv()
|
||||
|
||||
headTracker.let {
|
||||
receivingRotationOffsetGoal = if (it != null && it.hasRotation) {
|
||||
it.getRotation().project(Vector3.POS_Y).unit() * receivingRotationOffsetGoal
|
||||
} else {
|
||||
receivingRotationOffsetGoal
|
||||
}
|
||||
}
|
||||
|
||||
// If greater than 300ms, snap to rotation
|
||||
if (System.currentTimeMillis() - timeAtLastReceivedRotationOffset > 300) {
|
||||
receivingRotationOffset = receivingRotationOffsetGoal
|
||||
}
|
||||
|
||||
// Update time variable
|
||||
timeAtLastReceivedRotationOffset = System.currentTimeMillis()
|
||||
}
|
||||
} else {
|
||||
// Trackers data (1-8)
|
||||
val trackerId = trackerStringValue.toInt()
|
||||
var tracker = oscTrackersDevice!!.trackers[trackerId]
|
||||
|
||||
if (tracker == null) {
|
||||
tracker = Tracker(
|
||||
device = oscTrackersDevice,
|
||||
id = VRServer.getNextLocalTrackerId(),
|
||||
name = "OSC Tracker #$trackerId",
|
||||
displayName = "OSC Tracker #$trackerId",
|
||||
trackerNum = trackerId,
|
||||
trackerPosition = null,
|
||||
hasRotation = true,
|
||||
hasPosition = true,
|
||||
userEditable = true,
|
||||
isComputed = true,
|
||||
allowReset = true,
|
||||
usesTimeout = true,
|
||||
)
|
||||
oscTrackersDevice!!.trackers[trackerId] = tracker
|
||||
server.registerTracker(tracker)
|
||||
}
|
||||
|
||||
// Sets the tracker status to OK
|
||||
tracker.status = TrackerStatus.OK
|
||||
|
||||
if (dataType == "position") {
|
||||
// Update tracker position
|
||||
tracker.position = receivingRotationOffset.sandwich(
|
||||
Vector3(
|
||||
event.message.arguments[0] as Float,
|
||||
event.message.arguments[1] as Float,
|
||||
-(event.message.arguments[2] as Float),
|
||||
) -
|
||||
receivingPositionOffset,
|
||||
) +
|
||||
postReceivingPositionOffset
|
||||
} else {
|
||||
// Update tracker rotation
|
||||
val (w, x, y, z) = EulerAngles(
|
||||
EulerOrder.YXZ,
|
||||
event.message.arguments[0] as Float * FastMath.DEG_TO_RAD,
|
||||
event.message.arguments[1] as Float * FastMath.DEG_TO_RAD,
|
||||
event.message.arguments[2] as Float * FastMath.DEG_TO_RAD,
|
||||
).toQuaternion()
|
||||
val rot = Quaternion(w, -x, -y, z)
|
||||
tracker.setRotation(receivingRotationOffset * rot * postReceivingOffset)
|
||||
}
|
||||
|
||||
tracker.dataTick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun update() {
|
||||
// Gets timer from vrServer
|
||||
if (fpsTimer == null) {
|
||||
fpsTimer = VRServer.instance.fpsTimer
|
||||
}
|
||||
// Update received trackers' offset rotation slerp
|
||||
if (receivingRotationOffset != receivingRotationOffsetGoal) {
|
||||
receivingRotationOffset = receivingRotationOffset.interpR(receivingRotationOffsetGoal, OFFSET_SLERP_FACTOR * (fpsTimer?.timePerFrame ?: 1f))
|
||||
}
|
||||
|
||||
// Update current time
|
||||
val currentTime = System.currentTimeMillis().toFloat()
|
||||
|
||||
// Send OSC data
|
||||
if (oscSender != null && oscSender!!.isConnected) {
|
||||
// Create new bundle
|
||||
val bundle = OSCBundle()
|
||||
|
||||
for (i in computedTrackers.indices) {
|
||||
if (trackersEnabled[i]) {
|
||||
// Send regular trackers' positions
|
||||
val (x, y, z) = computedTrackers[i].position
|
||||
oscArgs.clear()
|
||||
oscArgs.add(x)
|
||||
oscArgs.add(y)
|
||||
oscArgs.add(-z)
|
||||
bundle.addPacket(
|
||||
OSCMessage(
|
||||
"/tracking/trackers/${getVRCOSCTrackersId(computedTrackers[i].trackerPosition)}/position",
|
||||
oscArgs.clone(),
|
||||
),
|
||||
)
|
||||
|
||||
// Send regular trackers' rotations
|
||||
val (w, x1, y1, z1) = computedTrackers[i].getRotation()
|
||||
// We flip the X and Y components of the quaternion because
|
||||
// we flip the z direction when communicating from
|
||||
// our right-handed API to VRChat's left-handed API.
|
||||
// X quaternion represents a rotation from y to z
|
||||
// Y quaternion represents a rotation from z to x
|
||||
// When we negate the z direction, X and Y quaternion
|
||||
// components must be negated.
|
||||
val (_, x2, y2, z2) = Quaternion(
|
||||
w,
|
||||
-x1,
|
||||
-y1,
|
||||
z1,
|
||||
).toEulerAngles(EulerOrder.YXZ)
|
||||
oscArgs.clear()
|
||||
oscArgs.add(x2 * FastMath.RAD_TO_DEG)
|
||||
oscArgs.add(y2 * FastMath.RAD_TO_DEG)
|
||||
oscArgs.add(z2 * FastMath.RAD_TO_DEG)
|
||||
bundle.addPacket(
|
||||
OSCMessage(
|
||||
"/tracking/trackers/${getVRCOSCTrackersId(computedTrackers[i].trackerPosition)}/rotation",
|
||||
oscArgs.clone(),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (computedTrackers[i].trackerPosition == TrackerPosition.HEAD) {
|
||||
// Send HMD position
|
||||
val (x, y, z) = computedTrackers[i].position
|
||||
oscArgs.clear()
|
||||
oscArgs.add(x)
|
||||
oscArgs.add(y)
|
||||
oscArgs.add(-z)
|
||||
bundle.addPacket(
|
||||
OSCMessage(
|
||||
"/tracking/trackers/head/position",
|
||||
oscArgs.clone(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
oscSender?.send(bundle)
|
||||
oscQuerySender?.send(bundle)
|
||||
} catch (e: IOException) {
|
||||
// Avoid spamming AsynchronousCloseException too many
|
||||
// times per second
|
||||
if (currentTime - timeAtLastError > 100) {
|
||||
timeAtLastError = System.currentTimeMillis()
|
||||
LogManager.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e")
|
||||
}
|
||||
} catch (e: OSCSerializeException) {
|
||||
if (currentTime - timeAtLastError > 100) {
|
||||
timeAtLastError = System.currentTimeMillis()
|
||||
LogManager.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVRCOSCTrackersId(trackerPosition: TrackerPosition?): Int {
|
||||
// Needs to range from 1-8.
|
||||
// Don't change as third party applications may rely
|
||||
// on this for mapping trackers to body parts.
|
||||
return when (trackerPosition) {
|
||||
TrackerPosition.HIP -> 1
|
||||
TrackerPosition.LEFT_FOOT -> 2
|
||||
TrackerPosition.RIGHT_FOOT -> 3
|
||||
TrackerPosition.LEFT_UPPER_LEG -> 4
|
||||
TrackerPosition.RIGHT_UPPER_LEG -> 5
|
||||
TrackerPosition.UPPER_CHEST -> 6
|
||||
TrackerPosition.LEFT_UPPER_ARM -> 7
|
||||
TrackerPosition.RIGHT_UPPER_ARM -> 8
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
fun setHeadTracker(headTracker: Tracker?) {
|
||||
this.headTracker = headTracker
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the expected HMD rotation upon reset to align the trackers in VRC
|
||||
*/
|
||||
fun yawAlign(headRot: Quaternion) {
|
||||
if (oscSender != null && oscSender!!.isConnected) {
|
||||
val (_, _, y, _) = headRot.toEulerAngles(EulerOrder.YXZ)
|
||||
oscArgs.clear()
|
||||
oscArgs.add(0f)
|
||||
oscArgs.add(-y * FastMath.RAD_TO_DEG)
|
||||
oscArgs.add(0f)
|
||||
oscMessage = OSCMessage(
|
||||
"/tracking/trackers/head/rotation",
|
||||
oscArgs,
|
||||
)
|
||||
try {
|
||||
oscSender?.send(oscMessage)
|
||||
oscQuerySender?.send(oscMessage)
|
||||
} catch (e: IOException) {
|
||||
LogManager
|
||||
.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e")
|
||||
} catch (e: OSCSerializeException) {
|
||||
LogManager
|
||||
.warning("[VRCOSCHandler] Error sending OSC message to VRChat: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOscSender(): OSCPortOut = oscSender!!
|
||||
|
||||
override fun getPortOut(): Int = oscPortOut
|
||||
|
||||
override fun getAddress(): InetAddress = oscIp!!
|
||||
|
||||
override fun getOscReceiver(): OSCPortIn = oscReceiver!!
|
||||
|
||||
override fun getPortIn(): Int = oscPortIn
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package dev.slimevr.osc
|
||||
|
||||
import OSCQueryNode
|
||||
import OSCQueryServer
|
||||
import ServiceInfo
|
||||
import dev.slimevr.protocol.rpc.setup.RPCUtil
|
||||
import io.eiren.util.logging.LogManager
|
||||
import randomFreePort
|
||||
import java.io.IOException
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
private const val serviceStartsWith = "VRChat-Client"
|
||||
private const val queryPath = "/tracking/vrsystem"
|
||||
|
||||
/**
|
||||
* Handler for OSCQuery for VRChat using our library
|
||||
* https://github.com/SlimeVR/oscquery-kt
|
||||
*/
|
||||
class VRCOSCQueryHandler(
|
||||
private val vrcOscHandler: VRCOSCHandler,
|
||||
) {
|
||||
private val oscQueryServer: OSCQueryServer
|
||||
|
||||
init {
|
||||
// Request data
|
||||
val localIp = RPCUtil.getLocalIp() ?: throw IllegalStateException("No local IP address found for OSCQuery to bind to")
|
||||
val httpPort = randomFreePort()
|
||||
oscQueryServer = OSCQueryServer(
|
||||
"SlimeVR-Server-$httpPort",
|
||||
OscTransport.UDP,
|
||||
localIp,
|
||||
vrcOscHandler.portIn.toUShort(),
|
||||
httpPort,
|
||||
)
|
||||
oscQueryServer.rootNode.addNode(OSCQueryNode(queryPath))
|
||||
oscQueryServer.init()
|
||||
LogManager.info("[VRCOSCQueryHandler] SlimeVR OSCQueryServer started at http://$localIp:$httpPort")
|
||||
|
||||
try {
|
||||
// Add service listener
|
||||
LogManager.info("[VRCOSCQueryHandler] Listening for VRChat OSCQuery")
|
||||
oscQueryServer.service.addServiceListener(
|
||||
"_osc._udp.local.",
|
||||
onServiceAdded = ::serviceAdded,
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
LogManager.warning("[VRCOSCQueryHandler] " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the OSC service's port
|
||||
*/
|
||||
fun updateOSCQuery(port: UShort) {
|
||||
if (oscQueryServer.oscPort != port) {
|
||||
thread(start = true) {
|
||||
oscQueryServer.updateOscService(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a service is added
|
||||
*/
|
||||
private fun serviceAdded(info: ServiceInfo) {
|
||||
// Check the service name
|
||||
if (!info.name.startsWith(serviceStartsWith)) return
|
||||
|
||||
// Get url from ServiceInfo
|
||||
val ip = info.inetAddresses[0].hostAddress
|
||||
val port = info.port
|
||||
|
||||
// create a new OSCHandler for this service
|
||||
vrcOscHandler.addOSCQuerySender(port, ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the OSCQueryServer and the associated OSC sender.
|
||||
*/
|
||||
fun close() {
|
||||
vrcOscHandler.closeOscQuerySender(false)
|
||||
thread(start = true) {
|
||||
oscQueryServer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package dev.slimevr.osc
|
||||
|
||||
import io.eiren.util.logging.LogManager
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.*
|
||||
|
||||
private val jsonIgnoreKeys = Json { ignoreUnknownKeys = true }
|
||||
class VRMReader(vrmJson: String) {
|
||||
|
||||
private val data: GLTF = jsonIgnoreKeys.decodeFromString(vrmJson)
|
||||
|
||||
fun getOffsetForBone(unityBone: UnityBone): Vector3 {
|
||||
val node = try {
|
||||
if (data.extensions.vrmV1 != null) {
|
||||
if (data.extensions.vrmV1.specVersion != "1.0") {
|
||||
LogManager.warning("[VRMReader] VRM version is not 1.0")
|
||||
}
|
||||
data.extensions.vrmV1.humanoid.humanBones.getValue(unityBone).node
|
||||
} else {
|
||||
data.extensions.vrmV0?.humanoid?.humanBones?.first {
|
||||
it.bone.equals(unityBone.stringVal, ignoreCase = true)
|
||||
}?.node
|
||||
}
|
||||
} catch (_: NoSuchElementException) {
|
||||
LogManager.warning("[VRMReader] Bone ${unityBone.stringVal} not found in JSON")
|
||||
null
|
||||
} ?: return Vector3.NULL
|
||||
|
||||
val translationNode = data.nodes[node].translation ?: return Vector3.NULL
|
||||
|
||||
return Vector3(translationNode[0].toFloat(), translationNode[1].toFloat(), translationNode[2].toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GLTF(
|
||||
val extensions: Extensions,
|
||||
val extensionsUsed: List<String>,
|
||||
val nodes: List<Node>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Extensions(
|
||||
@SerialName("VRM")
|
||||
val vrmV0: VRMV0? = null,
|
||||
@SerialName("VRMC_vrm")
|
||||
val vrmV1: VRMV1? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VRMV1(
|
||||
val specVersion: String,
|
||||
val humanoid: HumanoidV1,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HumanoidV1(
|
||||
val humanBones: Map<UnityBone, HumanBoneV1>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HumanBoneV1(
|
||||
val node: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VRMV0(
|
||||
val humanoid: HumanoidV0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HumanoidV0(
|
||||
val humanBones: List<HumanBoneV0>,
|
||||
val armStretch: Double,
|
||||
val legStretch: Double,
|
||||
val upperArmTwist: Double,
|
||||
val lowerArmTwist: Double,
|
||||
val upperLegTwist: Double,
|
||||
val lowerLegTwist: Double,
|
||||
val feetSpacing: Double,
|
||||
val hasTranslationDoF: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HumanBoneV0(
|
||||
val bone: String,
|
||||
val node: Int,
|
||||
val useDefaultValues: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Node(
|
||||
val translation: List<Double>? = null,
|
||||
val rotation: List<Double>? = null,
|
||||
val scale: List<Double>? = null,
|
||||
// GLTF says that there can be a matrix instead of translation,
|
||||
// rotation and scale, so we need to support that too in the future.
|
||||
// val matrix: List<List<Double>>,
|
||||
val children: List<Int> = emptyList(),
|
||||
)
|
||||
@@ -1,189 +0,0 @@
|
||||
package dev.slimevr.poseframeformat
|
||||
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrame
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrameData
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByDesignation
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
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.io.IOException
|
||||
|
||||
object PfrIO {
|
||||
@Throws(IOException::class)
|
||||
private fun writeVector3f(outputStream: DataOutputStream, vector: Vector3) {
|
||||
outputStream.writeFloat(vector.x)
|
||||
outputStream.writeFloat(vector.y)
|
||||
outputStream.writeFloat(vector.z)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeQuaternion(outputStream: DataOutputStream, quaternion: Quaternion) {
|
||||
outputStream.writeFloat(quaternion.x)
|
||||
outputStream.writeFloat(quaternion.y)
|
||||
outputStream.writeFloat(quaternion.z)
|
||||
outputStream.writeFloat(quaternion.w)
|
||||
}
|
||||
|
||||
fun writeFrame(outputStream: DataOutputStream, trackerFrame: TrackerFrame?) {
|
||||
if (trackerFrame == null) {
|
||||
outputStream.writeInt(0)
|
||||
return
|
||||
}
|
||||
|
||||
var dataFlags = trackerFrame.dataFlags
|
||||
|
||||
// Don't write destination strings anymore, replace with
|
||||
// the enum
|
||||
if (trackerFrame.hasData(TrackerFrameData.DESIGNATION_STRING)) {
|
||||
dataFlags = TrackerFrameData.TRACKER_POSITION_ENUM
|
||||
.add(TrackerFrameData.DESIGNATION_STRING.remove(dataFlags))
|
||||
}
|
||||
outputStream.writeInt(dataFlags)
|
||||
if (trackerFrame.hasData(TrackerFrameData.ROTATION)) {
|
||||
writeQuaternion(outputStream, trackerFrame.rotation!!)
|
||||
}
|
||||
if (trackerFrame.hasData(TrackerFrameData.POSITION)) {
|
||||
writeVector3f(outputStream, trackerFrame.position!!)
|
||||
}
|
||||
if (TrackerFrameData.TRACKER_POSITION_ENUM.check(dataFlags)) {
|
||||
// ID is offset by 1 for historical reasons
|
||||
outputStream.writeInt(trackerFrame.trackerPosition!!.id - 1)
|
||||
}
|
||||
if (trackerFrame.hasData(TrackerFrameData.ACCELERATION)) {
|
||||
writeVector3f(outputStream, trackerFrame.acceleration!!)
|
||||
}
|
||||
if (trackerFrame.hasData(TrackerFrameData.RAW_ROTATION)) {
|
||||
writeQuaternion(outputStream, trackerFrame.rawRotation!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeFrames(outputStream: DataOutputStream, frames: PoseFrames) {
|
||||
outputStream.writeInt(frames.frameHolders.size)
|
||||
for (tracker in frames.frameHolders) {
|
||||
outputStream.writeUTF(tracker.name)
|
||||
outputStream.writeInt(tracker.frames.size)
|
||||
for (i in 0 until tracker.frames.size) {
|
||||
writeFrame(outputStream, tracker.tryGetFrame(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tryWriteFrames(outputStream: DataOutputStream, frames: PoseFrames): Boolean = try {
|
||||
writeFrames(outputStream, frames)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("Error writing frame to stream.", e)
|
||||
false
|
||||
}
|
||||
|
||||
fun writeToFile(file: File, frames: PoseFrames) {
|
||||
DataOutputStream(
|
||||
BufferedOutputStream(FileOutputStream(file)),
|
||||
).use { writeFrames(it, frames) }
|
||||
}
|
||||
|
||||
fun tryWriteToFile(file: File, frames: PoseFrames): Boolean = try {
|
||||
writeToFile(file, frames)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("Error writing frames to file.", e)
|
||||
false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readVector3f(inputStream: DataInputStream): Vector3 = Vector3(
|
||||
inputStream.readFloat(),
|
||||
inputStream.readFloat(),
|
||||
inputStream.readFloat(),
|
||||
)
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readQuaternion(inputStream: DataInputStream): Quaternion {
|
||||
val x = inputStream.readFloat()
|
||||
val y = inputStream.readFloat()
|
||||
val z = inputStream.readFloat()
|
||||
val w = inputStream.readFloat()
|
||||
|
||||
return Quaternion(w, x, y, z)
|
||||
}
|
||||
|
||||
fun readFrame(inputStream: DataInputStream): TrackerFrame {
|
||||
val dataFlags = inputStream.readInt()
|
||||
|
||||
var designation: TrackerPosition? = null
|
||||
if (TrackerFrameData.DESIGNATION_STRING.check(dataFlags)) {
|
||||
designation = getByDesignation(inputStream.readUTF())
|
||||
}
|
||||
var rotation: Quaternion? = null
|
||||
if (TrackerFrameData.ROTATION.check(dataFlags)) {
|
||||
rotation = readQuaternion(inputStream)
|
||||
}
|
||||
var position: Vector3? = null
|
||||
if (TrackerFrameData.POSITION.check(dataFlags)) {
|
||||
position = readVector3f(inputStream)
|
||||
}
|
||||
if (TrackerFrameData.TRACKER_POSITION_ENUM.check(dataFlags)) {
|
||||
// ID is offset by 1 for historical reasons
|
||||
designation = TrackerPosition.getById(inputStream.readInt() + 1)
|
||||
}
|
||||
var acceleration: Vector3? = null
|
||||
if (TrackerFrameData.ACCELERATION.check(dataFlags)) {
|
||||
acceleration = readVector3f(inputStream)
|
||||
}
|
||||
var rawRotation: Quaternion? = null
|
||||
if (TrackerFrameData.RAW_ROTATION.check(dataFlags)) {
|
||||
rawRotation = readQuaternion(inputStream)
|
||||
}
|
||||
|
||||
return TrackerFrame(
|
||||
designation,
|
||||
rotation,
|
||||
position,
|
||||
acceleration,
|
||||
rawRotation,
|
||||
)
|
||||
}
|
||||
|
||||
fun readFrames(inputStream: DataInputStream): PoseFrames {
|
||||
val trackerCount = inputStream.readInt()
|
||||
val trackers = FastList<TrackerFrames>(trackerCount)
|
||||
for (i in 0 until trackerCount) {
|
||||
val name = inputStream.readUTF()
|
||||
val trackerFrameCount = inputStream.readInt()
|
||||
val trackerFrames = FastList<TrackerFrame?>(
|
||||
trackerFrameCount,
|
||||
)
|
||||
for (j in 0 until trackerFrameCount) {
|
||||
trackerFrames.add(readFrame(inputStream))
|
||||
}
|
||||
trackers.add(TrackerFrames(name, trackerFrames))
|
||||
}
|
||||
return PoseFrames(trackers)
|
||||
}
|
||||
|
||||
fun tryReadFrames(inputStream: DataInputStream): PoseFrames? = try {
|
||||
readFrames(inputStream)
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("Error reading frames from stream.", e)
|
||||
null
|
||||
}
|
||||
|
||||
fun readFromFile(file: File): PoseFrames = DataInputStream(BufferedInputStream(FileInputStream(file))).use { readFrames(it) }
|
||||
|
||||
fun tryReadFromFile(file: File): PoseFrames? = try {
|
||||
readFromFile(file)
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("Error reading frames from file.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package dev.slimevr.poseframeformat
|
||||
|
||||
import dev.slimevr.config.SkeletonConfig
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrame
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import io.eiren.util.logging.LogManager
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.EOFException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* PoseFrameStream File IO, designed to handle the internal PoseFrames format with
|
||||
* the new file format for streaming and storing additional debugging info
|
||||
*/
|
||||
object PfsIO {
|
||||
private fun writeRecordingDef(stream: DataOutputStream, frameInterval: Float) {
|
||||
stream.writeByte(PfsPackets.RECORDING_DEFINITION.id)
|
||||
stream.writeFloat(frameInterval)
|
||||
}
|
||||
|
||||
private fun writeTrackerDef(stream: DataOutputStream, id: Int, name: String) {
|
||||
stream.writeByte(PfsPackets.TRACKER_DEFINITION.id)
|
||||
stream.writeByte(id)
|
||||
stream.writeUTF(name)
|
||||
}
|
||||
|
||||
private fun writeTrackerFrame(stream: DataOutputStream, id: Int, frameIndex: Int, frame: TrackerFrame) {
|
||||
stream.writeByte(PfsPackets.TRACKER_FRAME.id)
|
||||
stream.writeByte(id)
|
||||
stream.writeInt(frameIndex)
|
||||
// Write frame data (same format as PFR)
|
||||
PfrIO.writeFrame(stream, frame)
|
||||
}
|
||||
|
||||
private fun writeBodyProportions(stream: DataOutputStream, skeletonConfig: SkeletonConfig) {
|
||||
stream.writeByte(PfsPackets.PROPORTIONS_CONFIG.id)
|
||||
// HMD height
|
||||
stream.writeFloat(skeletonConfig.hmdHeight)
|
||||
// Floor height
|
||||
stream.writeFloat(skeletonConfig.floorHeight)
|
||||
// Write config map
|
||||
stream.writeShort(skeletonConfig.offsets.size)
|
||||
for ((key, value) in skeletonConfig.offsets) {
|
||||
stream.writeUTF(key)
|
||||
stream.writeFloat(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeFrames(stream: DataOutputStream, frames: PoseFrames) {
|
||||
// Give trackers IDs (max 255)
|
||||
val trackers = frames.frameHolders.mapIndexed { i, t -> i to t }
|
||||
|
||||
// Write recording definition
|
||||
writeRecordingDef(stream, frames.frameInterval)
|
||||
|
||||
// Write tracker definitions
|
||||
for (tracker in trackers) {
|
||||
writeTrackerDef(stream, tracker.first, tracker.second.name)
|
||||
}
|
||||
|
||||
// Write tracker frames
|
||||
for (i in 0 until frames.maxFrameCount) {
|
||||
for (tracker in trackers) {
|
||||
// If the tracker has a frame at the index
|
||||
val frame = tracker.second.tryGetFrame(i)
|
||||
if (frame != null) {
|
||||
writeTrackerFrame(stream, tracker.first, i, frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tryWriteFrames(stream: DataOutputStream, frames: PoseFrames): Boolean = try {
|
||||
writeFrames(stream, frames)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("[PfsIO] Error writing frame to stream.", e)
|
||||
false
|
||||
}
|
||||
|
||||
fun writeToFile(file: File, frames: PoseFrames) {
|
||||
DataOutputStream(
|
||||
BufferedOutputStream(FileOutputStream(file)),
|
||||
).use { writeFrames(it, frames) }
|
||||
}
|
||||
|
||||
fun tryWriteToFile(file: File, frames: PoseFrames): Boolean = try {
|
||||
writeToFile(file, frames)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("[PfsIO] Error writing frames to file.", e)
|
||||
false
|
||||
}
|
||||
|
||||
fun readFrame(stream: DataInputStream, poseFrames: PoseFrames, trackers: MutableMap<Int, TrackerFrames>) {
|
||||
val packetId = stream.readUnsignedByte()
|
||||
val packetType = PfsPackets.byId[packetId]
|
||||
|
||||
when (packetType) {
|
||||
null -> {
|
||||
throw IOException("Encountered unknown packet ID ($packetId) while deserializing PFS stream.")
|
||||
}
|
||||
|
||||
PfsPackets.RECORDING_DEFINITION -> {
|
||||
// Unused, useful for debugging
|
||||
val frameInterval = stream.readFloat()
|
||||
poseFrames.frameInterval = frameInterval
|
||||
LogManager.debug("[PfsIO] Frame interval: $frameInterval s")
|
||||
}
|
||||
|
||||
PfsPackets.TRACKER_DEFINITION -> {
|
||||
val trackerId = stream.readUnsignedByte()
|
||||
val name = stream.readUTF()
|
||||
|
||||
// Get or make tracker and set its name
|
||||
trackers.getOrPut(trackerId) {
|
||||
TrackerFrames(name)
|
||||
}.name = name
|
||||
}
|
||||
|
||||
PfsPackets.TRACKER_FRAME -> {
|
||||
val trackerId = stream.readUnsignedByte()
|
||||
val tracker = trackers.getOrPut(trackerId) {
|
||||
// If tracker doesn't exist yet, make one
|
||||
TrackerFrames()
|
||||
}
|
||||
val frameNum = stream.readInt()
|
||||
val frame = PfrIO.readFrame(stream)
|
||||
|
||||
tracker.frames.add(frameNum, frame)
|
||||
}
|
||||
|
||||
PfsPackets.PROPORTIONS_CONFIG -> {
|
||||
// Unused, useful for debugging
|
||||
|
||||
val hmdHeight = stream.readFloat()
|
||||
val floorHeight = stream.readFloat()
|
||||
LogManager.debug("[PfsIO] HMD height: $hmdHeight, Floor height: $floorHeight")
|
||||
|
||||
// Currently just prints JSON format config to console
|
||||
val configCount = stream.readUnsignedShort()
|
||||
val sb = StringBuilder("[PfsIO] Body proportion configs ($configCount): {")
|
||||
for (i in 0 until configCount) {
|
||||
if (i > 0) {
|
||||
sb.append(", ")
|
||||
}
|
||||
sb.append(stream.readUTF())
|
||||
sb.append(": ")
|
||||
sb.append(stream.readFloat())
|
||||
}
|
||||
sb.append('}')
|
||||
|
||||
LogManager.debug(sb.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readFrames(stream: DataInputStream): PoseFrames {
|
||||
val poseFrames = PoseFrames()
|
||||
val trackers = mutableMapOf<Int, TrackerFrames>()
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
readFrame(stream, poseFrames, trackers)
|
||||
} catch (_: EOFException) {
|
||||
// Reached end of stream, stop reading and return the recording
|
||||
// LogManager.debug("[PfsIO] Reached end of PFS stream.", e)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
poseFrames.frameHolders.addAll(trackers.values)
|
||||
return poseFrames
|
||||
}
|
||||
|
||||
fun tryReadFrames(stream: DataInputStream): PoseFrames? = try {
|
||||
readFrames(stream)
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("[PfsIO] Error reading frames from stream.", e)
|
||||
null
|
||||
}
|
||||
|
||||
fun readFromFile(file: File): PoseFrames = DataInputStream(BufferedInputStream(FileInputStream(file))).use { readFrames(it) }
|
||||
|
||||
fun tryReadFromFile(file: File): PoseFrames? = try {
|
||||
readFromFile(file)
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("[PfsIO] Error reading frames from file.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package dev.slimevr.poseframeformat
|
||||
|
||||
/**
|
||||
* Packet ID ([UByte]),
|
||||
* Packet data (see [PfsPackets], implemented in [PfsIO])
|
||||
*/
|
||||
enum class PfsPackets(val id: Int) {
|
||||
/**
|
||||
* Frame interval ([Float] seconds)
|
||||
*/
|
||||
RECORDING_DEFINITION(0),
|
||||
|
||||
/**
|
||||
* Tracker ID ([UByte]),
|
||||
* Tracker name (UTF-8 [String])
|
||||
*/
|
||||
TRACKER_DEFINITION(1),
|
||||
|
||||
/**
|
||||
* Tracker ID ([UByte]),
|
||||
* Frame number ([UInt]),
|
||||
* PFR frame data (see [PfrIO.writeFrame] & [PfrIO.readFrame])
|
||||
*/
|
||||
TRACKER_FRAME(2),
|
||||
|
||||
/**
|
||||
* Hmd height ([Float]),
|
||||
* Floor height ([Float]),
|
||||
* Body proportion configs (Count ([UShort]) x (Key (UTF-8 [String]), Value ([Float]))
|
||||
*/
|
||||
PROPORTIONS_CONFIG(3),
|
||||
;
|
||||
|
||||
val byteId = id.toUByte()
|
||||
|
||||
companion object {
|
||||
val byId = entries.associateBy { it.id }
|
||||
val byByteId = entries.associateBy { it.byteId }
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package dev.slimevr.poseframeformat
|
||||
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrame
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import io.eiren.util.collections.FastList
|
||||
|
||||
class PoseFrames : Iterable<Array<TrackerFrame?>> {
|
||||
val frameHolders: FastList<TrackerFrames>
|
||||
|
||||
/**
|
||||
* Frame interval in seconds
|
||||
*/
|
||||
var frameInterval: Float = 0.02f
|
||||
|
||||
/**
|
||||
* Creates a [PoseFrames] object with the provided list of
|
||||
* [TrackerFrames]s as the internal [TrackerFrames] list.
|
||||
*
|
||||
* @see [FastList]
|
||||
* @see [TrackerFrames]
|
||||
*/
|
||||
constructor(frameHolders: FastList<TrackerFrames>) {
|
||||
this.frameHolders = frameHolders
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [PoseFrames] object with the specified initial tracker
|
||||
* capacity.
|
||||
*
|
||||
* @see [PoseFrames]
|
||||
*/
|
||||
constructor(initialCapacity: Int = 5) {
|
||||
frameHolders = FastList(initialCapacity)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The [TrackerFrames] associated with [position] at frame
|
||||
* index [index].
|
||||
*/
|
||||
fun getTrackerForPosition(position: TrackerPosition, index: Int = 0): TrackerFrames? {
|
||||
for (tracker in frameHolders) {
|
||||
if (tracker.tryGetFrame(index)?.trackerPosition == position) return tracker
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// region Data Utilities
|
||||
|
||||
/**
|
||||
* @return The maximum Y value of the [TrackerFrames] associated with the
|
||||
* [TrackerPosition.HEAD] tracker position on the first frame, otherwise `0f` if
|
||||
* no [TrackerFrames] is associated with [TrackerPosition.HEAD] or if there are no
|
||||
* valid positions.
|
||||
* @see [getMaxHeight]
|
||||
*/
|
||||
val maxHmdHeight: Float
|
||||
get() {
|
||||
return getMaxHeight(
|
||||
getTrackerForPosition(TrackerPosition.HEAD)
|
||||
?: return 0f,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The maximum Y value of the [trackerFrames], otherwise `0f` if
|
||||
* there are no valid positions.
|
||||
* @see [TrackerPosition]
|
||||
*/
|
||||
fun getMaxHeight(trackerFrames: TrackerFrames): Float {
|
||||
var maxHeight = 0f
|
||||
for (frame in trackerFrames.frames) {
|
||||
val framePosition = frame?.tryGetPosition() ?: continue
|
||||
|
||||
if (framePosition.y > maxHeight) {
|
||||
maxHeight = framePosition.y
|
||||
}
|
||||
}
|
||||
|
||||
return maxHeight
|
||||
}
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* @return The maximum number of [TrackerFrame]s contained within each
|
||||
* [TrackerFrames] in the internal [TrackerFrames] list.
|
||||
* @see [TrackerFrames.frames]
|
||||
* @see [List.size]
|
||||
*/
|
||||
val maxFrameCount: Int
|
||||
get() {
|
||||
return frameHolders.maxOfOrNull { tracker -> tracker.frames.size } ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the provided array buffer, get the [TrackerFrame]s contained
|
||||
* within each [TrackerFrames] in the internal
|
||||
* [TrackerFrames] list at the specified index.
|
||||
*
|
||||
* @return The number of frames written to the buffer.
|
||||
* @see [TrackerFrames.tryGetFrame]
|
||||
*/
|
||||
fun getFrames(frameIndex: Int, buffer: Array<TrackerFrame?>): Int {
|
||||
var frameCount = 0
|
||||
for (tracker in frameHolders) {
|
||||
if (tracker == null) {
|
||||
continue
|
||||
}
|
||||
val frame = tracker.tryGetFrame(frameIndex) ?: continue
|
||||
buffer[frameCount++] = frame
|
||||
}
|
||||
return frameCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the provided [List] buffer, get the [TrackerFrame]s
|
||||
* contained within each [TrackerFrames] in the internal
|
||||
* [TrackerFrames] list at the specified index.
|
||||
*
|
||||
* @return The number of frames written to the buffer.
|
||||
* @see [TrackerFrames.tryGetFrame]
|
||||
*/
|
||||
fun getFrames(frameIndex: Int, buffer: MutableList<TrackerFrame?>): Int {
|
||||
var frameCount = 0
|
||||
for (tracker in frameHolders) {
|
||||
if (tracker == null) {
|
||||
continue
|
||||
}
|
||||
val frame = tracker.tryGetFrame(frameIndex) ?: continue
|
||||
buffer[frameCount++] = frame
|
||||
}
|
||||
return frameCount
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The [TrackerFrame]s contained within each
|
||||
* [TrackerFrames] in the internal [TrackerFrames] list at
|
||||
* the specified index.
|
||||
* @see [TrackerFrames.tryGetFrame]
|
||||
*/
|
||||
fun getFrames(frameIndex: Int): Array<TrackerFrame?> {
|
||||
val trackerFrames = arrayOfNulls<TrackerFrame>(frameHolders.size)
|
||||
getFrames(frameIndex, trackerFrames)
|
||||
return trackerFrames
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<Array<TrackerFrame?>> = PoseFrameIterator(this)
|
||||
|
||||
inner class PoseFrameIterator(private val poseFrame: PoseFrames) : Iterator<Array<TrackerFrame?>> {
|
||||
private val trackerFrameBuffer: Array<TrackerFrame?> = arrayOfNulls(poseFrame.frameHolders.size)
|
||||
private val maxCursor = poseFrame.maxFrameCount
|
||||
private var cursor = 0
|
||||
|
||||
override fun hasNext(): Boolean = frameHolders.isNotEmpty() && cursor < maxCursor
|
||||
|
||||
override fun next(): Array<TrackerFrame?> {
|
||||
if (!hasNext()) {
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
poseFrame.getFrames(cursor++, trackerFrameBuffer)
|
||||
return trackerFrameBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package dev.slimevr.poseframeformat
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.util.TickReducer
|
||||
import dev.slimevr.util.ann.VRServerThread
|
||||
import io.eiren.util.collections.FastList
|
||||
import io.eiren.util.logging.LogManager
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import java.util.function.Consumer
|
||||
|
||||
class PoseRecorder(private val server: VRServer) {
|
||||
inner class RecordingProgress(val frame: Int, val totalFrames: Int)
|
||||
|
||||
private var poseFrame: PoseFrames? = null
|
||||
private var numFrames = -1
|
||||
private var frameCursor = 0
|
||||
|
||||
// Default 50 TPS
|
||||
private val ticker = TickReducer({ onTick() }, 0.02f)
|
||||
|
||||
private var recordingFuture: CompletableFuture<PoseFrames>? = null
|
||||
private var frameCallback: Consumer<RecordingProgress>? = null
|
||||
var trackers = FastList<Pair<Tracker, TrackerFrames>>()
|
||||
|
||||
init {
|
||||
server.addOnTick {
|
||||
if (numFrames > 0) {
|
||||
ticker.tick(server.fpsTimer.timePerFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure it's synchronized since this is the server thread interacting with
|
||||
// an unknown outside thread controlling this class
|
||||
@Synchronized
|
||||
@VRServerThread
|
||||
fun onTick() {
|
||||
if (frameCursor >= numFrames) {
|
||||
// If done and hasn't yet, send finished recording
|
||||
stopFrameRecording()
|
||||
return
|
||||
}
|
||||
|
||||
// A stopped recording will be accounted for by an empty "trackers" list
|
||||
val cursor = frameCursor++
|
||||
for (tracker in trackers) {
|
||||
// Add a frame for each tracker
|
||||
tracker.right.addFrameFromTracker(cursor, tracker.left)
|
||||
}
|
||||
|
||||
// Send the number of finished frames
|
||||
frameCallback?.accept(RecordingProgress(frameCursor, numFrames))
|
||||
// If done, send finished recording
|
||||
if (frameCursor >= numFrames) {
|
||||
stopFrameRecording()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun startFrameRecording(
|
||||
numFrames: Int,
|
||||
interval: Float,
|
||||
trackers: List<Tracker?> = server.allTrackers,
|
||||
frameCallback: Consumer<RecordingProgress>? = null,
|
||||
): Future<PoseFrames> {
|
||||
require(numFrames >= 1) { "numFrames must at least have a value of 1." }
|
||||
require(interval > 0) { "interval must be greater than 0." }
|
||||
require(trackers.isNotEmpty()) { "trackers must have at least one entry." }
|
||||
|
||||
cancelFrameRecording()
|
||||
val poseFrame = PoseFrames(trackers.size)
|
||||
poseFrame.frameInterval = interval
|
||||
|
||||
// Update tracker list
|
||||
this.trackers.ensureCapacity(trackers.size)
|
||||
for (tracker in trackers) {
|
||||
// Ignore null and internal trackers
|
||||
if (tracker == null || tracker.isInternal) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a tracker recording
|
||||
val trackerFrames = TrackerFrames(tracker, numFrames)
|
||||
poseFrame.frameHolders.add(trackerFrames)
|
||||
|
||||
// Pair tracker with recording
|
||||
this.trackers.add(Pair.of(tracker, trackerFrames))
|
||||
}
|
||||
require(this.trackers.isNotEmpty()) { "trackers must have at least one valid tracker." }
|
||||
|
||||
// Ticking setup
|
||||
ticker.interval = interval
|
||||
ticker.reset()
|
||||
|
||||
val recordingFuture = CompletableFuture<PoseFrames>()
|
||||
this.recordingFuture = recordingFuture
|
||||
this.frameCallback = frameCallback
|
||||
|
||||
// Recording setup
|
||||
this.poseFrame = poseFrame
|
||||
frameCursor = 0
|
||||
this.numFrames = numFrames
|
||||
|
||||
LogManager.info(
|
||||
"[PoseRecorder] Recording $numFrames samples at a $interval s frame interval",
|
||||
)
|
||||
|
||||
return recordingFuture
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun internalStopFrameRecording(cancel: Boolean) {
|
||||
val currentRecording = recordingFuture
|
||||
if (currentRecording != null && !currentRecording.isDone) {
|
||||
val currentFrames = poseFrame
|
||||
if (cancel || currentFrames == null) {
|
||||
// If it's supposed to be cancelled or there's actually no recording,
|
||||
// then cancel the recording and return nothing
|
||||
currentRecording.cancel(true)
|
||||
} else {
|
||||
// Stop the recording, returning the frames recorded
|
||||
currentRecording.complete(currentFrames)
|
||||
}
|
||||
}
|
||||
numFrames = -1
|
||||
frameCursor = 0
|
||||
trackers.clear()
|
||||
poseFrame = null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopFrameRecording() {
|
||||
internalStopFrameRecording(false)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun cancelFrameRecording() {
|
||||
internalStopFrameRecording(true)
|
||||
}
|
||||
|
||||
val isReadyToRecord: Boolean
|
||||
get() = server.trackersCount > 0
|
||||
|
||||
val isRecording: Boolean
|
||||
get() = numFrames > frameCursor
|
||||
|
||||
fun hasRecording(): Boolean = recordingFuture != null
|
||||
|
||||
val framesAsync: Future<PoseFrames>?
|
||||
get() = recordingFuture
|
||||
|
||||
@get:Throws(ExecutionException::class, InterruptedException::class)
|
||||
val frames: PoseFrames?
|
||||
get() {
|
||||
return recordingFuture?.get()
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package dev.slimevr.poseframeformat.player
|
||||
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
|
||||
class PlayerTracker(val trackerFrames: TrackerFrames, val tracker: Tracker, private var internalCursor: Int = 0, private var internalScale: Float = 1f) {
|
||||
|
||||
var cursor: Int
|
||||
get() = internalCursor
|
||||
set(value) {
|
||||
val limitedCursor = limitCursor(value)
|
||||
internalCursor = limitedCursor
|
||||
setTrackerStateFromIndex(limitedCursor)
|
||||
}
|
||||
|
||||
var scale: Float
|
||||
get() = internalScale
|
||||
set(value) {
|
||||
internalScale = value
|
||||
setTrackerStateFromIndex()
|
||||
}
|
||||
|
||||
init {
|
||||
setTrackerStateFromIndex(limitCursor())
|
||||
}
|
||||
|
||||
fun limitCursor(cursor: Int): Int {
|
||||
return if (cursor < 0 || trackerFrames.frames.isEmpty()) {
|
||||
return 0
|
||||
} else if (cursor >= trackerFrames.frames.size) {
|
||||
return trackerFrames.frames.size - 1
|
||||
} else {
|
||||
cursor
|
||||
}
|
||||
}
|
||||
|
||||
fun limitCursor(): Int {
|
||||
val limitedCursor = limitCursor(internalCursor)
|
||||
internalCursor = limitedCursor
|
||||
return limitedCursor
|
||||
}
|
||||
|
||||
private fun setTrackerStateFromIndex(index: Int = internalCursor) {
|
||||
val frame = trackerFrames.tryGetFrame(index) ?: return
|
||||
|
||||
/*
|
||||
* TODO: No way to set adjusted rotation manually? That might be nice to have...
|
||||
* for now we'll stick with just setting the final rotation as raw and not
|
||||
* enabling any adjustments
|
||||
*/
|
||||
|
||||
val trackerPosition = frame.tryGetTrackerPosition()
|
||||
if (trackerPosition != null) {
|
||||
tracker.trackerPosition = trackerPosition
|
||||
}
|
||||
|
||||
val rotation = frame.tryGetRotation()
|
||||
if (rotation != null) {
|
||||
tracker.setRotation(rotation)
|
||||
}
|
||||
|
||||
val position = frame.tryGetPosition()
|
||||
if (position != null) {
|
||||
tracker.position = position * internalScale
|
||||
}
|
||||
|
||||
val acceleration = frame.tryGetAcceleration()
|
||||
if (acceleration != null) {
|
||||
tracker.setAcceleration(acceleration * internalScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package dev.slimevr.poseframeformat.player
|
||||
|
||||
import dev.slimevr.poseframeformat.PoseFrames
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrame
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrames
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
|
||||
class TrackerFramesPlayer(vararg val frameHolders: TrackerFrames) {
|
||||
|
||||
val playerTrackers: Array<PlayerTracker> = frameHolders.map { trackerFrames ->
|
||||
PlayerTracker(
|
||||
trackerFrames,
|
||||
trackerFrames.toTracker(),
|
||||
)
|
||||
}.toTypedArray()
|
||||
|
||||
val trackers: Array<Tracker> =
|
||||
playerTrackers.map { playerTracker -> playerTracker.tracker }.toTypedArray()
|
||||
|
||||
/**
|
||||
* @return The maximum number of [TrackerFrame]s contained within each
|
||||
* [TrackerFrames] in the internal [TrackerFrames] array.
|
||||
* @see [TrackerFrames.frames]
|
||||
* @see [List.size]
|
||||
*/
|
||||
val maxFrameCount: Int
|
||||
get() {
|
||||
return frameHolders.maxOfOrNull { tracker -> tracker.frames.size } ?: 0
|
||||
}
|
||||
|
||||
constructor(poseFrames: PoseFrames) : this(frameHolders = poseFrames.frameHolders.toTypedArray())
|
||||
|
||||
fun setCursors(index: Int) {
|
||||
for (playerTracker in playerTrackers) {
|
||||
playerTracker.cursor = index
|
||||
}
|
||||
}
|
||||
|
||||
fun setScales(scale: Float) {
|
||||
for (playerTracker in playerTrackers) {
|
||||
playerTracker.scale = scale
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package dev.slimevr.poseframeformat.trackerdata
|
||||
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerPosition
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import io.github.axisangles.ktmath.Vector3
|
||||
|
||||
data class TrackerFrame(
|
||||
val trackerPosition: TrackerPosition? = null,
|
||||
val rotation: Quaternion? = null,
|
||||
val position: Vector3? = null,
|
||||
val acceleration: Vector3? = null,
|
||||
val rawRotation: Quaternion? = null,
|
||||
) {
|
||||
val dataFlags: Int
|
||||
|
||||
val name: String
|
||||
get() = "TrackerFrame:/${trackerPosition?.designation ?: "null"}"
|
||||
|
||||
init {
|
||||
var initDataFlags = 0
|
||||
|
||||
if (trackerPosition != null) {
|
||||
initDataFlags = TrackerFrameData.TRACKER_POSITION_ENUM.add(initDataFlags)
|
||||
}
|
||||
if (rotation != null) {
|
||||
initDataFlags = TrackerFrameData.ROTATION.add(initDataFlags)
|
||||
}
|
||||
if (position != null) {
|
||||
initDataFlags = TrackerFrameData.POSITION.add(initDataFlags)
|
||||
}
|
||||
if (acceleration != null) {
|
||||
initDataFlags = TrackerFrameData.ACCELERATION.add(initDataFlags)
|
||||
}
|
||||
if (rawRotation != null) {
|
||||
initDataFlags = TrackerFrameData.RAW_ROTATION.add(initDataFlags)
|
||||
}
|
||||
|
||||
dataFlags = initDataFlags
|
||||
}
|
||||
|
||||
fun hasData(flag: TrackerFrameData): Boolean = flag.check(dataFlags)
|
||||
|
||||
// region Tracker Try Getters
|
||||
fun tryGetTrackerPosition(): TrackerPosition? = if (hasData(TrackerFrameData.TRACKER_POSITION_ENUM) || hasData(TrackerFrameData.DESIGNATION_STRING)) {
|
||||
trackerPosition
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun tryGetRotation(): Quaternion? = if (hasData(TrackerFrameData.ROTATION)) {
|
||||
rotation
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun tryGetRawRotation(): Quaternion? = if (hasData(TrackerFrameData.RAW_ROTATION)) {
|
||||
rawRotation
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun tryGetPosition(): Vector3? = if (hasData(TrackerFrameData.POSITION)) {
|
||||
position
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun tryGetAcceleration(): Vector3? = if (hasData(TrackerFrameData.ACCELERATION)) {
|
||||
acceleration
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun hasRotation(): Boolean = hasData(TrackerFrameData.ROTATION)
|
||||
|
||||
fun hasPosition(): Boolean = hasData(TrackerFrameData.POSITION)
|
||||
|
||||
fun hasAcceleration(): Boolean = hasData(TrackerFrameData.ACCELERATION)
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
val empty = TrackerFrame()
|
||||
|
||||
fun fromTracker(tracker: Tracker): TrackerFrame? {
|
||||
// If the tracker is not ready
|
||||
if (tracker.status != TrackerStatus.OK && tracker.status != TrackerStatus.BUSY && tracker.status != TrackerStatus.OCCLUDED) {
|
||||
return null
|
||||
}
|
||||
|
||||
val trackerPosition = tracker.trackerPosition
|
||||
|
||||
// If tracker has no data at all, there's no point in writing a frame
|
||||
// Note: This includes rawRotation because of `!tracker.hasRotation`
|
||||
if (trackerPosition == null && !tracker.hasRotation && !tracker.hasPosition && !tracker.hasAcceleration) {
|
||||
return null
|
||||
}
|
||||
|
||||
val rotation: Quaternion? = if (tracker.hasRotation) tracker.getRotation() else null
|
||||
val position: Vector3? = if (tracker.hasPosition) tracker.position else null
|
||||
val acceleration: Vector3? = if (tracker.hasAcceleration) tracker.getAcceleration() else null
|
||||
|
||||
var rawRotation: Quaternion? = if (tracker.hasAdjustedRotation) tracker.getRawRotation() else null
|
||||
// If the rawRotation is the same as rotation, there's no point in saving it, set it back to null
|
||||
if (rawRotation == rotation) rawRotation = null
|
||||
|
||||
return TrackerFrame(
|
||||
trackerPosition,
|
||||
rotation,
|
||||
position,
|
||||
acceleration,
|
||||
rawRotation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package dev.slimevr.poseframeformat.trackerdata
|
||||
|
||||
enum class TrackerFrameData(val id: Int) {
|
||||
DESIGNATION_STRING(0),
|
||||
ROTATION(1),
|
||||
POSITION(2),
|
||||
TRACKER_POSITION_ENUM(3),
|
||||
ACCELERATION(4),
|
||||
RAW_ROTATION(5),
|
||||
;
|
||||
|
||||
val flag: Int = 1 shl id
|
||||
|
||||
/*
|
||||
* Inline is fine for these, there's no negative to inlining them as they'll never
|
||||
* change, so any warning about it can be safely ignored
|
||||
*/
|
||||
inline fun check(dataFlags: Int): Boolean = dataFlags and flag != 0
|
||||
|
||||
inline fun add(dataFlags: Int): Int = dataFlags or flag
|
||||
|
||||
inline fun remove(dataFlags: Int): Int = dataFlags xor flag
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package dev.slimevr.poseframeformat.trackerdata
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.poseframeformat.trackerdata.TrackerFrame.Companion.fromTracker
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus
|
||||
import io.eiren.util.collections.FastList
|
||||
|
||||
data class TrackerFrames(var name: String = "", val frames: FastList<TrackerFrame?>) {
|
||||
|
||||
constructor(name: String = "", initialCapacity: Int = 5) : this(name, FastList<TrackerFrame?>(initialCapacity))
|
||||
constructor(baseTracker: Tracker, frames: FastList<TrackerFrame?>) : this(baseTracker.name, frames)
|
||||
constructor(baseTracker: Tracker, initialCapacity: Int = 5) : this(baseTracker, FastList<TrackerFrame?>(initialCapacity))
|
||||
|
||||
fun addFrameFromTracker(index: Int, tracker: Tracker): TrackerFrame? {
|
||||
val trackerFrame = fromTracker(tracker)
|
||||
frames.add(index, trackerFrame)
|
||||
return trackerFrame
|
||||
}
|
||||
|
||||
fun addFrameFromTracker(tracker: Tracker): TrackerFrame? {
|
||||
val trackerFrame = fromTracker(tracker)
|
||||
frames.add(trackerFrame)
|
||||
return trackerFrame
|
||||
}
|
||||
|
||||
fun tryGetFrame(index: Int): TrackerFrame? = if (index < 0 || index >= frames.size) null else frames[index]
|
||||
|
||||
fun tryGetFirstNotNullFrame(): TrackerFrame? = frames.firstOrNull { frame -> frame != null }
|
||||
|
||||
fun toTracker(): Tracker {
|
||||
val firstFrame = tryGetFirstNotNullFrame() ?: TrackerFrame.empty
|
||||
val tracker = Tracker(
|
||||
device = null,
|
||||
id = VRServer.getNextLocalTrackerId(),
|
||||
name = name,
|
||||
trackerPosition = firstFrame.tryGetTrackerPosition(),
|
||||
hasPosition = firstFrame.hasPosition(),
|
||||
hasRotation = firstFrame.hasRotation(),
|
||||
hasAcceleration = firstFrame.hasAcceleration(),
|
||||
// Make sure this is false!! Otherwise HumanSkeleton ignores it
|
||||
isInternal = false,
|
||||
isComputed = true,
|
||||
trackRotDirection = false,
|
||||
)
|
||||
|
||||
tracker.status = TrackerStatus.OK
|
||||
|
||||
return tracker
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import com.jme3.math.FastMath
|
||||
import dev.slimevr.tracking.processor.Bone
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import io.github.axisangles.ktmath.EulerOrder
|
||||
import io.github.axisangles.ktmath.Quaternion
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
class BVHFileStream : PoseDataStream {
|
||||
var bvhSettings: BVHSettings = BVHSettings.BLENDER
|
||||
|
||||
private val writer: BufferedWriter
|
||||
private var frameCount: Long = 0
|
||||
private var frameCountOffset: Long = 0
|
||||
|
||||
constructor(outputStream: OutputStream, bvhSettings: BVHSettings = BVHSettings.BLENDER) : super(outputStream) {
|
||||
this.bvhSettings = bvhSettings
|
||||
writer = BufferedWriter(OutputStreamWriter(outputStream), 4096)
|
||||
}
|
||||
|
||||
constructor(file: File, bvhSettings: BVHSettings = BVHSettings.BLENDER) : super(file) {
|
||||
this.bvhSettings = bvhSettings
|
||||
writer = BufferedWriter(OutputStreamWriter(outputStream), 4096)
|
||||
}
|
||||
|
||||
constructor(file: String, bvhSettings: BVHSettings = BVHSettings.BLENDER) : super(file) {
|
||||
this.bvhSettings = bvhSettings
|
||||
writer = BufferedWriter(OutputStreamWriter(outputStream), 4096)
|
||||
}
|
||||
|
||||
private fun getBufferedFrameCount(frameCount: Long): String {
|
||||
val frameString = frameCount.toString()
|
||||
val bufferCount = LONG_MAX_VALUE_DIGITS - frameString.length
|
||||
|
||||
return if (bufferCount > 0) frameString + StringUtils.repeat(' ', bufferCount) else frameString
|
||||
}
|
||||
|
||||
private fun internalNavigateSkeleton(
|
||||
bone: Bone,
|
||||
header: (bone: Bone, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) -> Unit,
|
||||
footer: (distance: Int) -> Unit,
|
||||
lastBone: Bone? = null,
|
||||
invertParentRot: Quaternion = Quaternion.IDENTITY,
|
||||
distance: Int = 0,
|
||||
isParent: Boolean = false,
|
||||
) {
|
||||
val parent = bone.parent
|
||||
// If we're visiting the parents or at root, continue to the next parent
|
||||
val visitParent = (isParent || lastBone == null) && parent != null
|
||||
|
||||
val children = bone.children
|
||||
val childCount = children.size - (if (isParent) 1 else 0)
|
||||
|
||||
val hasBranch = visitParent || childCount > 0
|
||||
|
||||
header(bone, lastBone, invertParentRot, distance, hasBranch, isParent)
|
||||
|
||||
if (hasBranch) {
|
||||
// Cache this inverted rotation to reduce computation for each branch
|
||||
val thisInvertRot = bone.getGlobalRotation().inv()
|
||||
|
||||
if (visitParent) {
|
||||
internalNavigateSkeleton(parent, header, footer, bone, thisInvertRot, distance + 1, true)
|
||||
}
|
||||
|
||||
for (child in children) {
|
||||
// If we're a parent, ignore the child
|
||||
if (isParent && child == lastBone) continue
|
||||
internalNavigateSkeleton(child, header, footer, bone, thisInvertRot, distance + 1, false)
|
||||
}
|
||||
}
|
||||
|
||||
footer(distance)
|
||||
}
|
||||
|
||||
private fun navigateSkeleton(
|
||||
root: Bone,
|
||||
header: (bone: Bone, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) -> Unit,
|
||||
footer: (distance: Int) -> Unit = {},
|
||||
) {
|
||||
internalNavigateSkeleton(root, header, footer)
|
||||
}
|
||||
|
||||
private fun writeBoneDefHeader(bone: Bone?, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) {
|
||||
val indentLevel = StringUtils.repeat("\t", distance)
|
||||
val nextIndentLevel = indentLevel + "\t"
|
||||
|
||||
// Handle ends
|
||||
if (bone == null) {
|
||||
writer.write("${indentLevel}End Site\n")
|
||||
} else {
|
||||
writer
|
||||
.write("${indentLevel}${if (distance > 0) "JOINT" else "ROOT"} ${bone.boneType}\n")
|
||||
}
|
||||
writer.write("$indentLevel{\n")
|
||||
|
||||
// Ignore the root and endpoint offsets
|
||||
if (bone != null && lastBone != null) {
|
||||
writer.write(
|
||||
"${nextIndentLevel}OFFSET 0.0 ${(if (isParent) lastBone.length else -lastBone.length) * bvhSettings.offsetScale} 0.0\n",
|
||||
)
|
||||
} else {
|
||||
writer.write("${nextIndentLevel}OFFSET 0.0 0.0 0.0\n")
|
||||
}
|
||||
|
||||
// Define channels
|
||||
if (bone != null) {
|
||||
// Only give position for root
|
||||
if (lastBone != null) {
|
||||
writer.write("${nextIndentLevel}CHANNELS 3 Zrotation Xrotation Yrotation\n")
|
||||
} else {
|
||||
writer.write(
|
||||
"${nextIndentLevel}CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n",
|
||||
)
|
||||
}
|
||||
|
||||
// Write an empty end bone if there are no branches
|
||||
// We use null for convenience and treat it as an end node (no bone)
|
||||
if (!hasBranch) {
|
||||
val endDistance = distance + 1
|
||||
writeBoneDefHeader(null, bone, Quaternion.IDENTITY, endDistance, false, false)
|
||||
writeBoneDefFooter(endDistance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeBoneDefFooter(level: Int) {
|
||||
// Closing bracket
|
||||
writer.write("${StringUtils.repeat("\t", level)}}\n")
|
||||
}
|
||||
|
||||
private fun writeSkeletonDef(rootBone: Bone) {
|
||||
navigateSkeleton(rootBone, ::writeBoneDefHeader, ::writeBoneDefFooter)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeHeader(skeleton: HumanSkeleton, streamer: PoseStreamer) {
|
||||
writer.write("HIERARCHY\n")
|
||||
writeSkeletonDef(skeleton.getBone(bvhSettings.rootBone))
|
||||
|
||||
writer.write("MOTION\n")
|
||||
writer.write("Frames: ")
|
||||
|
||||
// Get frame offset for finishing writing the file
|
||||
if (outputStream is FileOutputStream) {
|
||||
// Flush buffer to get proper offset
|
||||
writer.flush()
|
||||
frameCountOffset = outputStream.channel.position()
|
||||
}
|
||||
|
||||
writer.write(getBufferedFrameCount(frameCount) + "\n")
|
||||
|
||||
// Frame time in seconds
|
||||
writer.write("Frame Time: ${streamer.frameInterval}\n")
|
||||
}
|
||||
|
||||
private fun writeBoneRot(bone: Bone, lastBone: Bone?, invertParentRot: Quaternion, distance: Int, hasBranch: Boolean, isParent: Boolean) {
|
||||
val rot = invertParentRot * bone.getGlobalRotation()
|
||||
val angles = rot.toEulerAngles(EulerOrder.ZXY)
|
||||
|
||||
// Output in order of roll (Z), pitch (X), yaw (Y) (extrinsic)
|
||||
// Assume spacing is needed at the start (we start with position with no following space)
|
||||
writer
|
||||
.write(" ${angles.z * FastMath.RAD_TO_DEG} ${angles.x * FastMath.RAD_TO_DEG} ${angles.y * FastMath.RAD_TO_DEG}")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeFrame(skeleton: HumanSkeleton) {
|
||||
val rootBone = skeleton.getBone(bvhSettings.rootBone)
|
||||
|
||||
val rootPos = rootBone.getPosition()
|
||||
|
||||
// Write root position
|
||||
val positionScale = bvhSettings.positionScale
|
||||
writer
|
||||
.write("${rootPos.x * positionScale} ${rootPos.y * positionScale} ${rootPos.z * positionScale}")
|
||||
|
||||
navigateSkeleton(rootBone, ::writeBoneRot)
|
||||
writer.newLine()
|
||||
|
||||
frameCount++
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeFooter(skeleton: HumanSkeleton) {
|
||||
// Write the final frame count for files
|
||||
if (outputStream is FileOutputStream) {
|
||||
// Flush before anything else
|
||||
writer.flush()
|
||||
// Seek to the count offset
|
||||
outputStream.channel.position(frameCountOffset)
|
||||
// Overwrite the count with a new value
|
||||
writer.write(frameCount.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
writer.close()
|
||||
super.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LONG_MAX_VALUE_DIGITS = Long.MAX_VALUE.toString().length
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import io.eiren.util.logging.LogManager
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
|
||||
class BVHRecorder(server: VRServer) {
|
||||
private val poseStreamer: ServerPoseStreamer = ServerPoseStreamer(server)
|
||||
private var poseDataStream: PoseDataStream? = null
|
||||
|
||||
val isRecording: Boolean
|
||||
get() = poseDataStream != null
|
||||
|
||||
fun startRecording(path: Path) {
|
||||
val filePath = path.toFile()
|
||||
|
||||
val file = if (filePath.isDirectory()) {
|
||||
getBvhFile(filePath) ?: return
|
||||
} else {
|
||||
filePath
|
||||
}
|
||||
|
||||
try {
|
||||
val stream = BVHFileStream(file)
|
||||
poseDataStream = stream
|
||||
// 100 FPS
|
||||
poseStreamer.setOutput(stream, 1f / 100f)
|
||||
} catch (_: IOException) {
|
||||
LogManager.severe("[BVH] Failed to create the recording file \"${file.path}\".")
|
||||
}
|
||||
}
|
||||
|
||||
fun endRecording() {
|
||||
try {
|
||||
val stream = poseDataStream
|
||||
if (stream != null) {
|
||||
poseStreamer.closeOutput(stream)
|
||||
}
|
||||
} catch (e1: Exception) {
|
||||
LogManager.severe("[BVH] Exception while closing poseDataStream", e1)
|
||||
} finally {
|
||||
poseDataStream = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBvhFile(bvhSaveDir: File): File? {
|
||||
if (bvhSaveDir.isDirectory() || bvhSaveDir.mkdirs()) {
|
||||
var saveRecording: File?
|
||||
var recordingIndex = 1
|
||||
do {
|
||||
saveRecording =
|
||||
File(bvhSaveDir, "BVH-Recording${recordingIndex++}.bvh")
|
||||
} while (saveRecording.exists())
|
||||
|
||||
return saveRecording
|
||||
} else {
|
||||
LogManager
|
||||
.severe(
|
||||
"[BVH] Failed to create the recording directory \"${bvhSaveDir.path}\".",
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import dev.slimevr.tracking.processor.BoneType
|
||||
|
||||
class BVHSettings {
|
||||
var offsetScale: Float = 100f
|
||||
private set
|
||||
var positionScale: Float = 100f
|
||||
private set
|
||||
var rootBone: BoneType = BoneType.HIP
|
||||
private set
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(source: BVHSettings) {
|
||||
this.offsetScale = source.offsetScale
|
||||
this.positionScale = source.positionScale
|
||||
}
|
||||
|
||||
fun setOffsetScale(offsetScale: Float): BVHSettings {
|
||||
this.offsetScale = offsetScale
|
||||
return this
|
||||
}
|
||||
|
||||
fun setPositionScale(positionScale: Float): BVHSettings {
|
||||
this.positionScale = positionScale
|
||||
return this
|
||||
}
|
||||
|
||||
fun setRootBone(rootBone: BoneType): BVHSettings {
|
||||
this.rootBone = rootBone
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DEFAULT: BVHSettings = BVHSettings()
|
||||
val BLENDER: BVHSettings = BVHSettings(DEFAULT)
|
||||
.setOffsetScale(1f)
|
||||
.setPositionScale(1f)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.lang.AutoCloseable
|
||||
|
||||
abstract class PoseDataStream protected constructor(protected val outputStream: OutputStream) : AutoCloseable {
|
||||
var isClosed: Boolean = false
|
||||
protected set
|
||||
|
||||
protected constructor(file: File) : this(FileOutputStream(file))
|
||||
protected constructor(file: String) : this(FileOutputStream(file))
|
||||
|
||||
@Throws(IOException::class)
|
||||
open fun writeHeader(skeleton: HumanSkeleton, streamer: PoseStreamer) {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
abstract fun writeFrame(skeleton: HumanSkeleton)
|
||||
|
||||
@Throws(IOException::class)
|
||||
open fun writeFooter(skeleton: HumanSkeleton) {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
outputStream.close()
|
||||
this.isClosed = true
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import dev.slimevr.poseframeformat.PfrIO.readFromFile
|
||||
import dev.slimevr.poseframeformat.PoseFrames
|
||||
import dev.slimevr.poseframeformat.player.TrackerFramesPlayer
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
import java.io.File
|
||||
|
||||
class PoseFrameStreamer : PoseStreamer {
|
||||
|
||||
val player: TrackerFramesPlayer
|
||||
val hpm: HumanPoseManager
|
||||
|
||||
private constructor(
|
||||
player: TrackerFramesPlayer,
|
||||
hpm: HumanPoseManager,
|
||||
) : super(hpm.skeleton) {
|
||||
this.player = player
|
||||
this.hpm = hpm
|
||||
}
|
||||
|
||||
constructor(player: TrackerFramesPlayer) : this(player, HumanPoseManager(player.trackers.toList()))
|
||||
constructor(poseFrames: PoseFrames) : this(TrackerFramesPlayer(poseFrames))
|
||||
constructor(file: File) : this(readFromFile(file))
|
||||
constructor(path: String) : this(File(path))
|
||||
|
||||
@Synchronized
|
||||
fun streamAllFrames() {
|
||||
for (i in 0 until player.maxFrameCount) {
|
||||
player.setCursors(i)
|
||||
hpm.update()
|
||||
captureFrame()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import io.eiren.util.logging.LogManager
|
||||
import java.io.IOException
|
||||
|
||||
open class PoseStreamer(skeleton: HumanSkeleton) {
|
||||
// 60 FPS
|
||||
private var intervalInternal: Float = 1f / 60f
|
||||
private var stream: PoseDataStream? = null
|
||||
|
||||
var skeleton: HumanSkeleton = skeleton
|
||||
protected set
|
||||
|
||||
@Synchronized
|
||||
fun captureFrame() {
|
||||
// Make sure the stream is open before trying to write
|
||||
val stream = stream
|
||||
if (stream == null || stream.isClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
stream.writeFrame(skeleton)
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions without crashing the program
|
||||
LogManager.severe("[PoseStreamer] Exception while saving frame", e)
|
||||
}
|
||||
}
|
||||
|
||||
open var frameInterval: Float
|
||||
get() = intervalInternal
|
||||
set(interval) {
|
||||
require(interval > 0f) { "interval must be a value greater than 0" }
|
||||
this.intervalInternal = interval
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun setOutput(poseFileStream: PoseDataStream, interval: Float) {
|
||||
this.frameInterval = interval
|
||||
this.output = poseFileStream
|
||||
}
|
||||
|
||||
@set:Throws(IOException::class)
|
||||
@set:Synchronized
|
||||
open var output: PoseDataStream?
|
||||
get() = stream
|
||||
set(stream) {
|
||||
requireNotNull(stream) { "stream must not be null" }
|
||||
stream.writeHeader(skeleton, this)
|
||||
this.stream = stream
|
||||
}
|
||||
|
||||
val hasOutput
|
||||
get() = output?.isClosed == false
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun closeOutput() {
|
||||
val stream = this.stream
|
||||
if (stream != null) {
|
||||
closeOutput(stream)
|
||||
this.stream = null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun closeOutput(stream: PoseDataStream) {
|
||||
stream.writeFooter(skeleton)
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package dev.slimevr.posestreamer
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
|
||||
import dev.slimevr.util.ann.VRServerThread
|
||||
|
||||
class ServerPoseStreamer(val server: VRServer) : TickPoseStreamer(server.humanPoseManager.skeleton) {
|
||||
|
||||
init {
|
||||
// Register callbacks/events
|
||||
server.addSkeletonUpdatedCallback { skeleton: HumanSkeleton? ->
|
||||
this.onSkeletonUpdated(
|
||||
skeleton,
|
||||
)
|
||||
}
|
||||
server.addOnTick { this.tick() }
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
fun onSkeletonUpdated(skeleton: HumanSkeleton?) {
|
||||
if (skeleton != null) {
|
||||
this.skeleton = skeleton
|
||||
}
|
||||
}
|
||||
|
||||
@VRServerThread
|
||||
fun tick() {
|
||||
super.tick(server.fpsTimer.timePerFrame)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user