From 16f1120cef21c0528fa9fbfc939db9c2fd15bae9 Mon Sep 17 00:00:00 2001 From: fifth_light Date: Fri, 22 Aug 2025 23:50:09 +0800 Subject: [PATCH 1/8] Add JavaOSC dependency --- MODULE.bazel | 1 + maven_install.json | 110 ++++++++++++++++++++++++++++++++++++++++++++- mod/BUILD.bazel | 2 + 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index fb4be9c7..853006b1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -48,6 +48,7 @@ maven.install( "org.antlr:antlr4-runtime:4.13.1", "io.github.douira:glsl-transformer:2.0.1", "org.anarres:jcpp:1.4.14", + "com.illposed.osc:javaosc-core:0.8", # test dependencies "org.jetbrains.kotlin:kotlin-test-junit5:2.1.10", diff --git a/maven_install.json b/maven_install.json index 658f79c4..69ffe270 100755 --- a/maven_install.json +++ b/maven_install.json @@ -1,7 +1,7 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": 668299150, - "__RESOLVED_ARTIFACTS_HASH": -119811499, + "__INPUT_ARTIFACTS_HASH": -1347659811, + "__RESOLVED_ARTIFACTS_HASH": 907829391, "conflict_resolution": { "com.google.code.gson:gson:2.8.9": "com.google.code.gson:gson:2.10.1", "com.google.errorprone:error_prone_annotations:2.23.0": "com.google.errorprone:error_prone_annotations:2.24.1", @@ -179,6 +179,12 @@ }, "version": "72.1" }, + "com.illposed.osc:javaosc-core": { + "shasums": { + "jar": "ecf3ff99ef2123870e516b42e1ecbe2438931ea80f3a9005581de3e09c4f11ad" + }, + "version": "0.8" + }, "com.squareup.okhttp:okhttp": { "shasums": { "jar": "88ac9fd1bb51f82bcc664cc1eb9c225c90dc4389d660231b4cc737bebfe7d0aa" @@ -269,6 +275,12 @@ }, "version": "4.13.2" }, + "log4j:log4j": { + "shasums": { + "jar": "1d31696445697720527091754369082a6651bd49781b6005deb94e56753406f9" + }, + "version": "1.2.17" + }, "net.bytebuddy:byte-buddy": { "shasums": { "jar": "e99761a526df0fefbbd3fe14436b0f953000cdfa5151dc63c0b18d37d9c46f1c" @@ -971,6 +983,18 @@ }, "version": "1.7.25" }, + "org.slf4j:slf4j-ext": { + "shasums": { + "jar": "9df4ea4d390eb559f153716fa77658c26540f4f83635973b8c0e0da70d6fa944" + }, + "version": "1.7.25" + }, + "org.slf4j:slf4j-log4j12": { + "shasums": { + "jar": "ddb343954deb6f046f862606c534178730c02ed23d0b7f6ca1012c1e3fa74273" + }, + "version": "1.7.25" + }, "org.vineflower:vineflower": { "shasums": { "jar": "a615d07ddbbcd489369674f40e42df639c32be95410890b38f173d5c1e2ea39c" @@ -1100,6 +1124,12 @@ "org.checkerframework:checker-qual", "org.ow2.asm:asm" ], + "com.illposed.osc:javaosc-core": [ + "log4j:log4j", + "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12" + ], "com.squareup.okhttp:okhttp": [ "com.squareup.okio:okio" ], @@ -1477,6 +1507,13 @@ "org.ow2.asm:asm", "org.ow2.asm:asm-analysis", "org.ow2.asm:asm-tree" + ], + "org.slf4j:slf4j-ext": [ + "org.slf4j:slf4j-api" + ], + "org.slf4j:slf4j-log4j12": [ + "log4j:log4j", + "org.slf4j:slf4j-api" ] }, "packages": { @@ -1885,6 +1922,16 @@ "com.ibm.icu.text", "com.ibm.icu.util" ], + "com.illposed.osc:javaosc-core": [ + "com.illposed.osc", + "com.illposed.osc.argument", + "com.illposed.osc.argument.handler", + "com.illposed.osc.messageselector", + "com.illposed.osc.transport", + "com.illposed.osc.transport.channel", + "com.illposed.osc.transport.tcp", + "com.illposed.osc.transport.udp" + ], "com.squareup.okhttp:okhttp": [ "com.squareup.okhttp", "com.squareup.okhttp.internal", @@ -2093,6 +2140,29 @@ "org.junit.runners.parameterized", "org.junit.validator" ], + "log4j:log4j": [ + "org.apache.log4j", + "org.apache.log4j.chainsaw", + "org.apache.log4j.config", + "org.apache.log4j.helpers", + "org.apache.log4j.jdbc", + "org.apache.log4j.jmx", + "org.apache.log4j.lf5", + "org.apache.log4j.lf5.util", + "org.apache.log4j.lf5.viewer", + "org.apache.log4j.lf5.viewer.categoryexplorer", + "org.apache.log4j.lf5.viewer.configure", + "org.apache.log4j.net", + "org.apache.log4j.nt", + "org.apache.log4j.or", + "org.apache.log4j.or.jms", + "org.apache.log4j.or.sax", + "org.apache.log4j.pattern", + "org.apache.log4j.rewrite", + "org.apache.log4j.spi", + "org.apache.log4j.varia", + "org.apache.log4j.xml" + ], "net.bytebuddy:byte-buddy": [ "net.bytebuddy", "net.bytebuddy.agent.builder", @@ -3180,6 +3250,18 @@ "org.slf4j.helpers", "org.slf4j.spi" ], + "org.slf4j:slf4j-ext": [ + "org.slf4j", + "org.slf4j.agent", + "org.slf4j.cal10n", + "org.slf4j.ext", + "org.slf4j.instrumentation", + "org.slf4j.profiler" + ], + "org.slf4j:slf4j-log4j12": [ + "org.apache.log4j", + "org.slf4j.impl" + ], "org.vineflower:vineflower": [ "net.fabricmc.fernflower.api", "org.jetbrains.java.decompiler.api", @@ -3248,6 +3330,7 @@ "com.google.truth:truth", "com.h2database:h2", "com.ibm.icu:icu4j", + "com.illposed.osc:javaosc-core", "com.squareup.okhttp:okhttp", "com.squareup.okio:okio", "com.sun.jersey:jersey-client", @@ -3263,6 +3346,7 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", + "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3380,6 +3464,8 @@ "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://maven.fabricmc.net/": [ @@ -3411,6 +3497,7 @@ "com.google.truth:truth", "com.h2database:h2", "com.ibm.icu:icu4j", + "com.illposed.osc:javaosc-core", "com.squareup.okhttp:okhttp", "com.squareup.okio:okio", "com.sun.jersey:jersey-client", @@ -3426,6 +3513,7 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", + "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3543,6 +3631,8 @@ "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://maven.terraformersmc.com/": [ @@ -3574,6 +3664,7 @@ "com.google.truth:truth", "com.h2database:h2", "com.ibm.icu:icu4j", + "com.illposed.osc:javaosc-core", "com.squareup.okhttp:okhttp", "com.squareup.okio:okio", "com.sun.jersey:jersey-client", @@ -3589,6 +3680,7 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", + "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3706,6 +3798,8 @@ "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://maven.nucleoid.xyz/": [ @@ -3737,6 +3831,7 @@ "com.google.truth:truth", "com.h2database:h2", "com.ibm.icu:icu4j", + "com.illposed.osc:javaosc-core", "com.squareup.okhttp:okhttp", "com.squareup.okio:okio", "com.sun.jersey:jersey-client", @@ -3752,6 +3847,7 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", + "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3869,6 +3965,8 @@ "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://masa.dy.fi/maven/": [ @@ -3900,6 +3998,7 @@ "com.google.truth:truth", "com.h2database:h2", "com.ibm.icu:icu4j", + "com.illposed.osc:javaosc-core", "com.squareup.okhttp:okhttp", "com.squareup.okio:okio", "com.sun.jersey:jersey-client", @@ -3915,6 +4014,7 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", + "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -4032,6 +4132,8 @@ "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://api.modrinth.com/maven/": [ @@ -4063,6 +4165,7 @@ "com.google.truth:truth", "com.h2database:h2", "com.ibm.icu:icu4j", + "com.illposed.osc:javaosc-core", "com.squareup.okhttp:okhttp", "com.squareup.okio:okio", "com.sun.jersey:jersey-client", @@ -4078,6 +4181,7 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", + "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -4195,6 +4299,8 @@ "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", + "org.slf4j:slf4j-ext", + "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ] }, diff --git a/mod/BUILD.bazel b/mod/BUILD.bazel index 945059d1..de019f72 100644 --- a/mod/BUILD.bazel +++ b/mod/BUILD.bazel @@ -172,6 +172,7 @@ merge_jij( visibility = ["//visibility:public"], deps = { "@maven//:com_h2database_h2": "com_h2database_h2:2.3.232", + "@maven//:com_illposed_osc_javaosc_core": "javaosc-core:0.8", "//blazerod:blazerod_without_jij": "blazerod:=", "//blazerod/model/model-base": "blazerod-model-base:=", "//blazerod/model/model-formats": "blazerod-model-formats:=", @@ -239,6 +240,7 @@ java_binary( "//game:remapped_iris", "//game:remapped_sodium", "@maven//:com_h2database_h2", + "@maven//:com_illposed_osc_javaosc_core", "@maven//:io_github_douira_glsl_transformer", "@maven//:io_github_llamalad7_mixinextras_fabric", "@maven//:net_fabricmc_fabric_language_kotlin", From 756e53c4441ed56a4d5907a215c526d19657cbce Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sat, 23 Aug 2025 18:46:32 +0800 Subject: [PATCH 2/8] Add more expression and humanoid tags --- .../fifthlight/blazerod/model/Expression.kt | 31 ++++++- .../fifthlight/blazerod/model/HumanoidTag.kt | 88 ++++++++++++++++++- .../blazerod/model/gltf/GltfLoader.kt | 8 +- .../blazerod/model/pmx/PmxLoader.kt | 3 +- .../armorstand/state/ModelController.kt | 2 +- 5 files changed, 117 insertions(+), 15 deletions(-) diff --git a/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/Expression.kt b/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/Expression.kt index b55fe1d6..6b9270e7 100644 --- a/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/Expression.kt +++ b/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/Expression.kt @@ -90,6 +90,29 @@ sealed class Expression { vrm0Name = "LookRight", vrm1Name = "lookRight", ); + + companion object { + private val vrm0NameMap = entries + .mapNotNull { it.takeIf { it.vrm0Name != null } } + .associateBy { it.vrm0Name!!.lowercase() } + + private val vrm1NameMap = entries + .mapNotNull { it.takeIf { it.vrm1Name != null } } + .associateBy { it.vrm1Name!!.lowercase() } + + private val pmxJapaneseMap = entries + .mapNotNull { it.takeIf { it.pmxJapanese != null } } + .associateBy { it.pmxJapanese!!.lowercase() } + + private val pmxEnglishMap = entries + .mapNotNull { it.takeIf { it.pmxEnglish != null } } + .associateBy { it.pmxEnglish!!.lowercase() } + + fun fromVrm0Name(name: String): Tag? = vrm0NameMap[name.lowercase()] + fun fromVrm1Name(name: String): Tag? = vrm1NameMap[name.lowercase()] + fun fromPmxJapanese(name: String): Tag? = pmxJapaneseMap[name.lowercase()] + fun fromPmxEnglish(name: String): Tag? = pmxEnglishMap[name.lowercase()] + } } data class Target( @@ -97,19 +120,19 @@ sealed class Expression { override val tag: Tag? = null, val isBinary: Boolean = false, val bindings: List, - ): Expression() { + ) : Expression() { sealed class Binding { data class NodeMorphTarget( val nodeId: NodeId, val index: Int, val weight: Float, - ): Binding() + ) : Binding() data class MeshMorphTarget( val meshId: MeshId, val index: Int, val weight: Float, - ): Binding() + ) : Binding() } } @@ -117,7 +140,7 @@ sealed class Expression { override val name: String? = null, override val tag: Tag? = null, val targets: List, - ): Expression() { + ) : Expression() { data class TargetItem( val target: Target, val influence: Float, diff --git a/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt b/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt index 815ac59e..62894ef3 100644 --- a/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt +++ b/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt @@ -1,12 +1,96 @@ package top.fifthlight.blazerod.model +// VRM spec: https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/humanoid.md#list-of-humanoid-bones +// PMX reference: https://vrstormlab.com/dancexr/features/bones enum class HumanoidTag( val vrmName: String? = null, val pmxEnglish: String? = null, val pmxJapanese: String? = null, ) { - CENTER(vrmName = "hips", pmxEnglish = "Root", pmxJapanese = "センター"), - HEAD(vrmName = "head", pmxEnglish = "Head", pmxJapanese = "頭"); + // Torso + HIPS(vrmName = "hips", pmxEnglish = "Center", pmxJapanese = "センター"), + SPINE(vrmName = "spine", pmxEnglish = "Torso", pmxJapanese = "上半身"), + CHEST(vrmName = "chest", pmxEnglish = "Torso2", pmxJapanese = "上半身2"), + UPPER_CHEST(vrmName = "upperChest"), + NECK(vrmName = "neck", pmxEnglish = "Neck", pmxJapanese = "首"), + + // Head + HEAD(vrmName = "head", pmxEnglish = "Head", pmxJapanese = "頭"), + LEFT_EYE(vrmName = "leftEye", pmxEnglish = "LeftEye", pmxJapanese = "左目"), + RIGHT_EYE(vrmName = "rightEye", pmxEnglish = "RightEye", pmxJapanese = "右目"), + JAW(vrmName = "jaw"), + + // Leg + LEFT_UPPER_LEG(vrmName = "leftUpperLeg", pmxEnglish = "LeftLeg", pmxJapanese = "左足"), + LEFT_LOWER_LEG(vrmName = "leftLowerLeg", pmxEnglish = "LeftKnee", pmxJapanese = "左ひざ"), + LEFT_FOOT(vrmName = "leftFoot", pmxEnglish = "LeftAnkle", pmxJapanese = "左足首"), + LEFT_TOES(vrmName = "leftToes", pmxEnglish = "LeftToe", pmxJapanese = "左つま先"), + RIGHT_UPPER_LEG(vrmName = "rightUpperLeg", pmxEnglish = "RightLeg", pmxJapanese = "右足"), + RIGHT_LOWER_LEG(vrmName = "rightLowerLeg", pmxEnglish = "RightKnee", pmxJapanese = "右ひざ"), + RIGHT_FOOT(vrmName = "rightFoot", pmxEnglish = "RightAnkle", pmxJapanese = "右足首"), + RIGHT_TOES(vrmName = "rightToes", pmxEnglish = "RightToe", pmxJapanese = "右つま先"), + + // Arm + LEFT_SHOULDER(vrmName = "leftShoulder", pmxEnglish = "LeftShoulder", pmxJapanese = "左肩"), + LEFT_UPPER_ARM(vrmName = "leftUpperArm", pmxEnglish = "LeftArm", pmxJapanese = "左腕"), + LEFT_LOWER_ARM(vrmName = "leftLowerArm", pmxEnglish = "LeftElbow", pmxJapanese = "左ひじ"), + LEFT_HAND(vrmName = "leftHand", pmxEnglish = "LeftWrist", pmxJapanese = "左手首"), + RIGHT_SHOULDER(vrmName = "rightShoulder", pmxEnglish = "RightShoulder", pmxJapanese = "右肩"), + RIGHT_UPPER_ARM(vrmName = "rightUpperArm", pmxEnglish = "RightArm", pmxJapanese = "右腕"), + RIGHT_LOWER_ARM(vrmName = "rightLowerArm", pmxEnglish = "RightElbow", pmxJapanese = "右ひじ"), + RIGHT_HAND(vrmName = "rightHand", pmxEnglish = "RightWrist", pmxJapanese = "右手首"), + + // Finger + LEFT_THUMB_METACARPAL(vrmName = "leftThumbMetacarpal", pmxEnglish = "LeftThumb1", pmxJapanese = "左親指1"), + LEFT_THUMB_PROXIMAL(vrmName = "leftThumbProximal", pmxEnglish = "LeftThumb2", pmxJapanese = "左親指2"), + LEFT_THUMB_DISTAL(vrmName = "leftThumbDistal", pmxEnglish = "LeftThumb3", pmxJapanese = "左親指3"), + LEFT_INDEX_PROXIMAL(vrmName = "leftIndexProximal", pmxEnglish = "LeftIndexFinger1", pmxJapanese = "左人指1"), + LEFT_INDEX_INTERMEDIATE( + vrmName = "leftIndexIntermediate", + pmxEnglish = "LeftIndexFinger2", + pmxJapanese = "左人指2" + ), + LEFT_INDEX_DISTAL(vrmName = "leftIndexDistal", pmxEnglish = "LeftIndexFinger3", pmxJapanese = "左人指3"), + LEFT_MIDDLE_PROXIMAL(vrmName = "leftMiddleProximal", pmxEnglish = "LeftMiddleFinger1", pmxJapanese = "左中指1"), + LEFT_MIDDLE_INTERMEDIATE( + vrmName = "leftMiddleIntermediate", + pmxEnglish = "LeftMiddleFinger2", + pmxJapanese = "左中指2" + ), + LEFT_MIDDLE_DISTAL(vrmName = "leftMiddleDistal", pmxEnglish = "LeftMiddleFinger3", pmxJapanese = "左中指3"), + LEFT_RING_PROXIMAL(vrmName = "leftRingProximal", pmxEnglish = "LeftRingFinger1", pmxJapanese = "左薬指1"), + LEFT_RING_INTERMEDIATE(vrmName = "leftRingIntermediate", pmxEnglish = "LeftRingFinger2", pmxJapanese = "左薬指2"), + LEFT_RING_DISTAL(vrmName = "leftRingDistal", pmxEnglish = "LeftRingFinger3", pmxJapanese = "左薬指3"), + LEFT_LITTLE_PROXIMAL(vrmName = "leftLittleProximal", pmxEnglish = "LeftPinky1", pmxJapanese = "左小指1"), + LEFT_LITTLE_INTERMEDIATE(vrmName = "leftLittleIntermediate", pmxEnglish = "LeftPinky2", pmxJapanese = "左小指2"), + LEFT_LITTLE_DISTAL(vrmName = "leftLittleDistal", pmxEnglish = "LeftPinky3", pmxJapanese = "左小指3"), + RIGHT_THUMB_METACARPAL(vrmName = "rightThumbMetacarpal", pmxEnglish = "RightThumb1", pmxJapanese = "右親指1"), + RIGHT_THUMB_PROXIMAL(vrmName = "rightThumbProximal", pmxEnglish = "RightThumb2", pmxJapanese = "右親指2"), + RIGHT_THUMB_DISTAL(vrmName = "rightThumbDistal", pmxEnglish = "RightThumb3", pmxJapanese = "右親指3"), + RIGHT_INDEX_PROXIMAL(vrmName = "rightIndexProximal", pmxEnglish = "RightIndexFinger1", pmxJapanese = "右人指1"), + RIGHT_INDEX_INTERMEDIATE( + vrmName = "rightIndexIntermediate", + pmxEnglish = "RightIndexFinger2", + pmxJapanese = "右人指2" + ), + RIGHT_INDEX_DISTAL(vrmName = "rightIndexDistal", pmxEnglish = "RightIndexFinger3", pmxJapanese = "右人指3"), + RIGHT_MIDDLE_PROXIMAL(vrmName = "rightMiddleProximal", pmxEnglish = "RightMiddleFinger1", pmxJapanese = "右中指1"), + RIGHT_MIDDLE_INTERMEDIATE( + vrmName = "rightMiddleIntermediate", + pmxEnglish = "RightMiddleFinger2", + pmxJapanese = "右中指2" + ), + RIGHT_MIDDLE_DISTAL(vrmName = "rightMiddleDistal", pmxEnglish = "RightMiddleFinger3", pmxJapanese = "右中指3"), + RIGHT_RING_PROXIMAL(vrmName = "rightRingProximal", pmxEnglish = "RightRingFinger1", pmxJapanese = "右薬指1"), + RIGHT_RING_INTERMEDIATE( + vrmName = "rightRingIntermediate", + pmxEnglish = "RightRingFinger2", + pmxJapanese = "右薬指2" + ), + RIGHT_RING_DISTAL(vrmName = "rightRingDistal", pmxEnglish = "RightRingFinger3", pmxJapanese = "右薬指3"), + RIGHT_LITTLE_PROXIMAL(vrmName = "rightLittleProximal", pmxEnglish = "RightPinky1", pmxJapanese = "右小指1"), + RIGHT_LITTLE_INTERMEDIATE(vrmName = "rightLittleIntermediate", pmxEnglish = "RightPinky2", pmxJapanese = "右小指2"), + RIGHT_LITTLE_DISTAL(vrmName = "rightLittleDistal", pmxEnglish = "RightPinky3", pmxJapanese = "右小指3"); companion object { private val vrmNameMap = entries.associateBy { it.vrmName } diff --git a/blazerod/model/model-gltf/src/main/kotlin/top/fifthlight/blazerod/model/gltf/GltfLoader.kt b/blazerod/model/model-gltf/src/main/kotlin/top/fifthlight/blazerod/model/gltf/GltfLoader.kt index 4f7f3a09..4edc2f6d 100644 --- a/blazerod/model/model-gltf/src/main/kotlin/top/fifthlight/blazerod/model/gltf/GltfLoader.kt +++ b/blazerod/model/model-gltf/src/main/kotlin/top/fifthlight/blazerod/model/gltf/GltfLoader.kt @@ -565,9 +565,7 @@ internal class GltfLoader( expressions = vrmV0.map { expression -> Expression.Target( name = expression.name, - tag = Expression.Tag.entries.firstOrNull { - it.vrm0Name.equals(expression.presetName, ignoreCase = true) - }, + tag = expression.presetName?.let(Expression.Tag::fromVrm0Name), bindings = expression.binds?.map { bind -> Expression.Target.Binding.MeshMorphTarget( meshId = MeshId(uuid, bind.mesh), @@ -587,9 +585,7 @@ internal class GltfLoader( Expression.Target( name = name, isBinary = value.isBinary ?: false, - tag = Expression.Tag.entries.firstOrNull { - it.vrm1Name.equals(name, ignoreCase = true) - }, + tag = Expression.Tag.fromVrm1Name(name), bindings = value.morphTargetBinds?.map { bind -> Expression.Target.Binding.NodeMorphTarget( nodeId = NodeId(uuid, bind.node), diff --git a/blazerod/model/model-pmx/src/main/kotlin/top/fifthlight/blazerod/model/pmx/PmxLoader.kt b/blazerod/model/model-pmx/src/main/kotlin/top/fifthlight/blazerod/model/pmx/PmxLoader.kt index 39a79c16..ba799101 100644 --- a/blazerod/model/model-pmx/src/main/kotlin/top/fifthlight/blazerod/model/pmx/PmxLoader.kt +++ b/blazerod/model/model-pmx/src/main/kotlin/top/fifthlight/blazerod/model/pmx/PmxLoader.kt @@ -739,8 +739,7 @@ class PmxLoader : ModelFileLoader { val nameLocal = loadString(buffer) val nameUniversal = loadString(buffer) val expressionTag = - Expression.Tag.entries.firstOrNull { it.pmxJapanese.equals(nameLocal, ignoreCase = true) } - ?: Expression.Tag.entries.firstOrNull { it.pmxEnglish.equals(nameUniversal, ignoreCase = true) } + Expression.Tag.fromPmxJapanese(nameLocal) ?: Expression.Tag.fromPmxEnglish(nameUniversal) val panelType = buffer.get().toInt() .let { type -> PmxMorphPanelType.entries.firstOrNull { it.value == type } } ?: throw PmxLoadException("Unknown panel type") diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt index 9baa4e71..726b267c 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt @@ -115,7 +115,7 @@ sealed class ModelController { private var blinkProgress: Float = 0f constructor(scene: RenderScene) : this( - center = scene.getBone(HumanoidTag.CENTER), + center = scene.getBone(HumanoidTag.HIPS), head = scene.getBone(HumanoidTag.HEAD), blinkExpression = scene.getExpression(Expression.Tag.BLINK), ) From 1ecb14c9aced26333232392610eac3f9d3a18d2d Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sat, 23 Aug 2025 18:59:17 +0800 Subject: [PATCH 3/8] Add VMC marionette client --- MODULE.bazel | 2 +- maven_install.json | 75 +------- mod/BUILD.bazel | 3 +- .../armorstand/vmc/VmcMarionetteClient.kt | 120 +++++++++++++ .../armorstand/vmc/VmcMarionetteState.kt | 161 ++++++++++++++++++ 5 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt create mode 100644 mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt diff --git a/MODULE.bazel b/MODULE.bazel index 853006b1..1097ec7d 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -48,7 +48,7 @@ maven.install( "org.antlr:antlr4-runtime:4.13.1", "io.github.douira:glsl-transformer:2.0.1", "org.anarres:jcpp:1.4.14", - "com.illposed.osc:javaosc-core:0.8", + "com.illposed.osc:javaosc-core:0.9", # test dependencies "org.jetbrains.kotlin:kotlin-test-junit5:2.1.10", diff --git a/maven_install.json b/maven_install.json index 69ffe270..fd21e1de 100755 --- a/maven_install.json +++ b/maven_install.json @@ -1,7 +1,7 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": -1347659811, - "__RESOLVED_ARTIFACTS_HASH": 907829391, + "__INPUT_ARTIFACTS_HASH": -570601380, + "__RESOLVED_ARTIFACTS_HASH": -144996448, "conflict_resolution": { "com.google.code.gson:gson:2.8.9": "com.google.code.gson:gson:2.10.1", "com.google.errorprone:error_prone_annotations:2.23.0": "com.google.errorprone:error_prone_annotations:2.24.1", @@ -181,9 +181,9 @@ }, "com.illposed.osc:javaosc-core": { "shasums": { - "jar": "ecf3ff99ef2123870e516b42e1ecbe2438931ea80f3a9005581de3e09c4f11ad" + "jar": "9d2b8011ca8046cd8685911ae5d1b989c4fe424cbf372303f6af41e62ae770ec" }, - "version": "0.8" + "version": "0.9" }, "com.squareup.okhttp:okhttp": { "shasums": { @@ -275,12 +275,6 @@ }, "version": "4.13.2" }, - "log4j:log4j": { - "shasums": { - "jar": "1d31696445697720527091754369082a6651bd49781b6005deb94e56753406f9" - }, - "version": "1.2.17" - }, "net.bytebuddy:byte-buddy": { "shasums": { "jar": "e99761a526df0fefbbd3fe14436b0f953000cdfa5151dc63c0b18d37d9c46f1c" @@ -979,21 +973,15 @@ }, "org.slf4j:slf4j-api": { "shasums": { - "jar": "18c4a0095d5c1da6b817592e767bb23d29dd2f560ad74df75ff3961dbde25b79" + "jar": "e7c2a48e8515ba1f49fa637d57b4e2f590b3f5bd97407ac699c3aa5efb1204a9" }, - "version": "1.7.25" + "version": "2.0.13" }, "org.slf4j:slf4j-ext": { "shasums": { - "jar": "9df4ea4d390eb559f153716fa77658c26540f4f83635973b8c0e0da70d6fa944" - }, - "version": "1.7.25" - }, - "org.slf4j:slf4j-log4j12": { - "shasums": { - "jar": "ddb343954deb6f046f862606c534178730c02ed23d0b7f6ca1012c1e3fa74273" + "jar": "9963ed4a45df1ad7d197f6940e99b8063edab645370d427bea4901bad5846c95" }, - "version": "1.7.25" + "version": "2.0.13" }, "org.vineflower:vineflower": { "shasums": { @@ -1125,10 +1113,8 @@ "org.ow2.asm:asm" ], "com.illposed.osc:javaosc-core": [ - "log4j:log4j", "org.slf4j:slf4j-api", - "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12" + "org.slf4j:slf4j-ext" ], "com.squareup.okhttp:okhttp": [ "com.squareup.okio:okio" @@ -1510,10 +1496,6 @@ ], "org.slf4j:slf4j-ext": [ "org.slf4j:slf4j-api" - ], - "org.slf4j:slf4j-log4j12": [ - "log4j:log4j", - "org.slf4j:slf4j-api" ] }, "packages": { @@ -2140,29 +2122,6 @@ "org.junit.runners.parameterized", "org.junit.validator" ], - "log4j:log4j": [ - "org.apache.log4j", - "org.apache.log4j.chainsaw", - "org.apache.log4j.config", - "org.apache.log4j.helpers", - "org.apache.log4j.jdbc", - "org.apache.log4j.jmx", - "org.apache.log4j.lf5", - "org.apache.log4j.lf5.util", - "org.apache.log4j.lf5.viewer", - "org.apache.log4j.lf5.viewer.categoryexplorer", - "org.apache.log4j.lf5.viewer.configure", - "org.apache.log4j.net", - "org.apache.log4j.nt", - "org.apache.log4j.or", - "org.apache.log4j.or.jms", - "org.apache.log4j.or.sax", - "org.apache.log4j.pattern", - "org.apache.log4j.rewrite", - "org.apache.log4j.spi", - "org.apache.log4j.varia", - "org.apache.log4j.xml" - ], "net.bytebuddy:byte-buddy": [ "net.bytebuddy", "net.bytebuddy.agent.builder", @@ -3258,10 +3217,6 @@ "org.slf4j.instrumentation", "org.slf4j.profiler" ], - "org.slf4j:slf4j-log4j12": [ - "org.apache.log4j", - "org.slf4j.impl" - ], "org.vineflower:vineflower": [ "net.fabricmc.fernflower.api", "org.jetbrains.java.decompiler.api", @@ -3346,7 +3301,6 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", - "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3465,7 +3419,6 @@ "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://maven.fabricmc.net/": [ @@ -3513,7 +3466,6 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", - "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3632,7 +3584,6 @@ "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://maven.terraformersmc.com/": [ @@ -3680,7 +3631,6 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", - "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3799,7 +3749,6 @@ "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://maven.nucleoid.xyz/": [ @@ -3847,7 +3796,6 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", - "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -3966,7 +3914,6 @@ "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://masa.dy.fi/maven/": [ @@ -4014,7 +3961,6 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", - "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -4133,7 +4079,6 @@ "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ], "https://api.modrinth.com/maven/": [ @@ -4181,7 +4126,6 @@ "javax.ws.rs:jsr311-api", "joda-time:joda-time", "junit:junit", - "log4j:log4j", "net.bytebuddy:byte-buddy", "net.bytebuddy:byte-buddy-agent", "net.fabricmc.fabric-api:fabric-api", @@ -4300,7 +4244,6 @@ "org.ow2.asm:asm-util", "org.slf4j:slf4j-api", "org.slf4j:slf4j-ext", - "org.slf4j:slf4j-log4j12", "org.vineflower:vineflower" ] }, diff --git a/mod/BUILD.bazel b/mod/BUILD.bazel index de019f72..5b7b7243 100644 --- a/mod/BUILD.bazel +++ b/mod/BUILD.bazel @@ -132,6 +132,7 @@ kt_jvm_library( "//:kotlin_serialization", "//blazerod:blazerod_without_jij", "@maven//:com_h2database_h2", + "@maven//:com_illposed_osc_javaosc_core", "@maven//:io_github_llamalad7_mixinextras_fabric", "@maven//:net_fabricmc_fabric_language_kotlin", "@maven//:net_fabricmc_fabric_loader", @@ -172,7 +173,7 @@ merge_jij( visibility = ["//visibility:public"], deps = { "@maven//:com_h2database_h2": "com_h2database_h2:2.3.232", - "@maven//:com_illposed_osc_javaosc_core": "javaosc-core:0.8", + "@maven//:com_illposed_osc_javaosc_core": "javaosc-core:0.9", "//blazerod:blazerod_without_jij": "blazerod:=", "//blazerod/model/model-base": "blazerod-model-base:=", "//blazerod/model/model-formats": "blazerod-model-formats:=", diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt new file mode 100644 index 00000000..2bdb2dbe --- /dev/null +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt @@ -0,0 +1,120 @@ +package top.fifthlight.armorstand.vmc + +import com.illposed.osc.OSCBundle +import com.illposed.osc.OSCMessage +import com.illposed.osc.OSCPacket +import com.illposed.osc.transport.Transport +import kotlinx.coroutines.* +import net.minecraft.client.MinecraftClient +import org.slf4j.LoggerFactory +import top.fifthlight.armorstand.util.ThreadExecutorDispatcher +import top.fifthlight.blazerod.model.Expression +import top.fifthlight.blazerod.model.HumanoidTag + +class VmcMarionetteClient( + private val transport: Transport, +) : AutoCloseable { + companion object { + private val logger = LoggerFactory.getLogger(VmcMarionetteClient::class.java) + } + + private val scope = CoroutineScope(ThreadExecutorDispatcher(MinecraftClient.getInstance()) + SupervisorJob()) + + @Volatile + private var closed = false + + private val stateHolder: VmcMarionetteStateHolder = VmcMarionetteStateHolder() + + fun getState() = stateHolder.read() + + private fun handlePacket(message: OSCPacket) { + when (message) { + is OSCBundle -> { + message.packets.forEach(::handlePacket) + } + + is OSCMessage -> handleMessage(message) + } + } + + private val pendingBlendValues = mutableMapOf() + private fun handleMessage(message: OSCMessage) { + when (message.address) { + "/VMC/Ext/Root/Pos" -> { + val name = (message.arguments.getOrNull(0) as? String) ?: return + if (name != "root") { + return + } + val posX = (message.arguments.getOrNull(1) as? Float) ?: return + val posY = (message.arguments.getOrNull(2) as? Float) ?: return + val posZ = (message.arguments.getOrNull(3) as? Float) ?: return + val rotX = (message.arguments.getOrNull(4) as? Float) ?: return + val rotY = (message.arguments.getOrNull(5) as? Float) ?: return + val rotZ = (message.arguments.getOrNull(6) as? Float) ?: return + val rotW = (message.arguments.getOrNull(7) as? Float) ?: return + stateHolder.write { + setRootTransform(posX, posY, posZ, rotX, rotY, rotZ, rotW) + } + } + + "/VMC/Ext/Bone/Pos" -> { + val name = (message.arguments.getOrNull(0) as? String) ?: return + val tag = HumanoidTag.fromVrmName(name.replaceFirstChar { it.lowercase() }) ?: return + val posX = (message.arguments.getOrNull(1) as? Float) ?: return + val posY = (message.arguments.getOrNull(2) as? Float) ?: return + val posZ = (message.arguments.getOrNull(3) as? Float) ?: return + val rotX = (message.arguments.getOrNull(4) as? Float) ?: return + val rotY = (message.arguments.getOrNull(5) as? Float) ?: return + val rotZ = (message.arguments.getOrNull(6) as? Float) ?: return + val rotW = (message.arguments.getOrNull(7) as? Float) ?: return + stateHolder.write { + setBoneTransform(tag, posX, posY, posZ, rotX, rotY, rotZ, rotW) + } + } + + "/VMC/Ext/Blend/Val" -> { + val tag = (message.arguments.getOrNull(0) as? String) ?: return + val expressionTag = Expression.Tag.fromVrm0Name(tag) ?: Expression.Tag.fromVrm1Name(tag) ?: return + val value = (message.arguments.getOrNull(1) as? Float) ?: return + pendingBlendValues[expressionTag] = value + } + + "/VMC/Ext/Blend/Apply" -> { + stateHolder.write { + pendingBlendValues.forEach { + setBlendShape(it.key, it.value) + } + } + pendingBlendValues.clear() + } + } + } + + init { + scope.launch(Dispatchers.IO) { + while (true) { + val message = try { + transport.receive() + } catch (ex: Exception) { + if (!scope.isActive) { + // throw CancellationException + ensureActive() + } + logger.warn("Failed to receive OSC message", ex) + continue + } + handlePacket(message) + ensureActive() + } + } + } + + override fun close() { + if (closed) { + return + } + closed = true + scope.cancel() + transport.close() + } +} diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt new file mode 100644 index 00000000..d2564216 --- /dev/null +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt @@ -0,0 +1,161 @@ +package top.fifthlight.armorstand.vmc + +import kotlinx.atomicfu.locks.withLock +import org.joml.Quaternionf +import org.joml.Quaternionfc +import org.joml.Vector3f +import org.joml.Vector3fc +import top.fifthlight.blazerod.model.Expression +import top.fifthlight.blazerod.model.HumanoidTag +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.ReentrantLock + +sealed class VmcMarionetteStateView { + abstract val rootTransform: RootTransform? + abstract val boneTransforms: Map + abstract val blendShapes: Map + + abstract class RootTransform { + abstract val position: Vector3fc + abstract val rotation: Quaternionfc + } + + abstract class BoneTransform { + abstract val position: Vector3fc + abstract val rotation: Quaternionfc + } + + abstract fun snapshot(): VmcMarionetteStateView +} + +class ImmutableVmcMarionetteStateView( + rootTransform: VmcMarionetteStateView.RootTransform? = null, + boneTransforms: Map = mapOf(), + blendShapes: Map = mapOf(), +) : VmcMarionetteStateView() { + override val rootTransform = rootTransform?.let { RootTransform(it) } + override val boneTransforms = boneTransforms.mapValues { BoneTransform(it.value) } + override val blendShapes = blendShapes.toMap() + + class RootTransform( + position: Vector3fc, + rotation: Quaternionfc, + ) : VmcMarionetteStateView.RootTransform() { + constructor(transform: VmcMarionetteStateView.RootTransform) : this( + position = transform.position, + rotation = transform.rotation, + ) + + override val position: Vector3fc = Vector3f(position) + override val rotation: Quaternionfc = Quaternionf(rotation) + } + + class BoneTransform( + position: Vector3fc, + rotation: Quaternionfc, + ) : VmcMarionetteStateView.BoneTransform() { + constructor(transform: VmcMarionetteStateView.BoneTransform) : this( + position = transform.position, + rotation = transform.rotation, + ) + + override val position: Vector3fc = Vector3f(position) + override val rotation: Quaternionfc = Quaternionf(rotation) + } + + override fun snapshot() = this +} + +interface VmcMarionetteStateWriter { + fun setRootTransform( + posX: Float, + posY: Float, + posZ: Float, + rotX: Float, + rotY: Float, + rotZ: Float, + rotW: Float, + ) + + fun setBoneTransform( + tag: HumanoidTag, + posX: Float, + posY: Float, + posZ: Float, + rotX: Float, + rotY: Float, + rotZ: Float, + rotW: Float, + ) + + fun setBlendShape(tag: Expression.Tag, weight: Float) +} + +sealed class MutableVmcMarionetteStateView : VmcMarionetteStateView(), VmcMarionetteStateWriter { + abstract override val rootTransform: RootTransform? + abstract override val boneTransforms: MutableMap + abstract override val blendShapes: MutableMap +} + +private class MutableVmcMarionetteStateViewImpl : MutableVmcMarionetteStateView() { + override var rootTransform: RootTransform? = null + override val boneTransforms: MutableMap = mutableMapOf() + override val blendShapes: MutableMap = mutableMapOf() + + class RootTransform : VmcMarionetteStateView.RootTransform() { + override var position = Vector3f() + override var rotation = Quaternionf() + } + + class BoneTransform : VmcMarionetteStateView.BoneTransform() { + override var position = Vector3f() + override var rotation = Quaternionf() + } + + override fun setRootTransform( + posX: Float, + posY: Float, + posZ: Float, + rotX: Float, + rotY: Float, + rotZ: Float, + rotW: Float, + ) { + val rootTransform = rootTransform ?: RootTransform().also { this.rootTransform = it } + rootTransform.position.set(posX, posY, posZ) + rootTransform.rotation.set(rotX, rotY, rotZ, rotW) + } + + override fun setBoneTransform( + tag: HumanoidTag, + posX: Float, + posY: Float, + posZ: Float, + rotX: Float, + rotY: Float, + rotZ: Float, + rotW: Float, + ) { + val boneTransforms = boneTransforms.getOrPut(tag) { BoneTransform() } + boneTransforms.position.set(posX, posY, posZ) + boneTransforms.rotation.set(rotX, rotY, rotZ, rotW) + } + + override fun setBlendShape(tag: Expression.Tag, weight: Float) { + blendShapes[tag] = weight + } + + override fun snapshot() = ImmutableVmcMarionetteStateView(rootTransform, boneTransforms, blendShapes) +} + +class VmcMarionetteStateHolder { + private val writeLock = ReentrantLock() + private val writeBuffer = MutableVmcMarionetteStateViewImpl() + private val readBuffer = AtomicReference(writeBuffer.snapshot()) + + fun read(): VmcMarionetteStateView = readBuffer.get() + + fun write(block: VmcMarionetteStateWriter.() -> Unit) = writeLock.withLock { + block(writeBuffer) + } +} From 949d112fab33f5ff6d9dbc86d0dc082ee99fea77 Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sat, 23 Aug 2025 20:28:43 +0800 Subject: [PATCH 4/8] Fix double buffering of VmcMarionetteStateHolder --- .../kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt index d2564216..27a8f948 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteState.kt @@ -157,5 +157,6 @@ class VmcMarionetteStateHolder { fun write(block: VmcMarionetteStateWriter.() -> Unit) = writeLock.withLock { block(writeBuffer) + readBuffer.set(writeBuffer.snapshot()) } } From ca5417a0b755462c866d5fbadac5eefd61cdcbdc Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sat, 23 Aug 2025 22:56:16 +0800 Subject: [PATCH 5/8] Add UI for OSC/VMC --- .../armorstand/config/GlobalConfig.kt | 1 + .../ui/model/VmcConfigScreenModel.kt | 39 +++++ .../armorstand/ui/screen/ConfigScreen.kt | 5 + .../armorstand/ui/screen/VmcConfigScreen.kt | 140 ++++++++++++++++++ .../ui/state/VmcConfigScreenState.kt | 8 + .../armorstand/vmc/VmcMarionetteClient.kt | 30 ++-- .../armorstand/vmc/VmcMarionetteManager.kt | 61 ++++++++ .../assets/armorstand/lang/en_us.json | 10 +- .../assets/armorstand/lang/zh_cn.json | 10 +- 9 files changed, 290 insertions(+), 14 deletions(-) create mode 100644 mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/VmcConfigScreenModel.kt create mode 100644 mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/VmcConfigScreen.kt create mode 100644 mod/src/client/kotlin/top/fifthlight/armorstand/ui/state/VmcConfigScreenState.kt create mode 100644 mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt index 2a40edfc..83ad96ac 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/config/GlobalConfig.kt @@ -31,6 +31,7 @@ data class GlobalConfig( val thirdPersonDistanceScale: Float = 1f, val invertHeadDirection: Boolean = false, val renderer: RendererKey = RendererKey.VERTEX_SHADER_TRANSFORM, + val vmcUdpPort: Int = 9000, ) { companion object { private val logger = LoggerFactory.getLogger(GlobalConfig::class.java) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/VmcConfigScreenModel.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/VmcConfigScreenModel.kt new file mode 100644 index 00000000..53a00988 --- /dev/null +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/model/VmcConfigScreenModel.kt @@ -0,0 +1,39 @@ +package top.fifthlight.armorstand.ui.model + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import top.fifthlight.armorstand.config.ConfigHolder +import top.fifthlight.armorstand.ui.state.VmcConfigScreenState +import top.fifthlight.armorstand.vmc.VmcMarionetteManager + +class VmcConfigScreenModel(scope: CoroutineScope) : ViewModel(scope) { + private val _uiState = MutableStateFlow(VmcConfigScreenState()) + val uiState = _uiState.asStateFlow() + + init { + scope.apply { + launch { + ConfigHolder.config.collect { + _uiState.value = _uiState.value.copy( + portNumber = it.vmcUdpPort, + ) + } + } + launch { + VmcMarionetteManager.state.collect { + _uiState.value = _uiState.value.copy(state = it) + } + } + } + } + + fun startVmcClient() { + VmcMarionetteManager.start(uiState.value.portNumber) + } + + fun stopVmcClient() { + VmcMarionetteManager.stop() + } +} \ No newline at end of file diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt index 96537e74..423f2a26 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/screen/ConfigScreen.kt @@ -242,6 +242,10 @@ class ConfigScreen(parent: Screen? = null) : ArmorStandScreen( + title = Text.translatable("armorstand.vmc"), + parent = parent, + viewModelFactory = ::VmcConfigScreenModel, +) { + private val topBar by lazy { + TextWidget(width, 32, title, currentClient.textRenderer) + } + private val closeButton = ButtonWidget.builder(ScreenTexts.BACK) { close() }.build() + + private val statusLabel by lazy { + TextWidget(Text.empty(), currentClient.textRenderer).apply { + scope.launch { + viewModel.uiState.collect { + message = when (it.state) { + is VmcMarionetteManager.State.Running -> Text.translatable( + "armorstand.vmc.running", + it.state.port + ) + + is VmcMarionetteManager.State.Stopped -> Text.translatable("armorstand.vmc.stopped") + + is VmcMarionetteManager.State.Failed -> Text.translatable( + "armorstand.vmc.failed", + it.state.exception.message + ) + } + } + } + } + } + + private val portLabel by lazy { + TextWidget(Text.translatable("armorstand.vmc.port"), currentClient.textRenderer) + } + + private val portField by lazy { + val textContent = MutableStateFlow(viewModel.uiState.value.portNumber.toString()) + scope.launch { + textContent.collect { portStr -> + portStr.toIntOrNull()?.let { port -> + ConfigHolder.update { + copy(vmcUdpPort = port) + } + } + } + } + textField( + text = textContent, + onChanged = { + textContent.value = it + }, + ) + } + + private val startButton = ButtonWidget.builder(Text.translatable("armorstand.vmc.start")) { + when (viewModel.uiState.value.state) { + VmcMarionetteManager.State.Stopped, is VmcMarionetteManager.State.Failed -> viewModel.startVmcClient() + is VmcMarionetteManager.State.Running -> viewModel.stopVmcClient() + } + }.build().apply { + scope.launch { + viewModel.uiState.collect { + message = when (it.state) { + VmcMarionetteManager.State.Stopped, is VmcMarionetteManager.State.Failed -> + Text.translatable("armorstand.vmc.start") + + is VmcMarionetteManager.State.Running -> Text.translatable("armorstand.vmc.stop") + } + } + } + } + + override fun init() { + val rootLayout = BorderLayout( + width = width, + height = height, + direction = BorderLayout.Direction.VERTICAL, + ) + rootLayout.setFirstElement(topBar) { topBar, width, height -> topBar.width = width } + rootLayout.setCenterElement( + widget = LinearLayout( + width = 192, + direction = LinearLayout.Direction.VERTICAL, + padding = Insets(8), + gap = 8, + surface = Surface.listBackgroundWithSeparator(), + ).apply { + add(statusLabel, expand = true) + add( + widget = BorderLayout( + width = 192, + height = portField.height, + direction = BorderLayout.Direction.HORIZONTAL, + ).apply { + setFirstElement(portLabel, Positioner.create().marginRight(8)) + setCenterElement(portField) + }, + expand = true, + ) + add(startButton, expand = true) + pack() + addDrawable(this) + }, + positioner = Positioner.create().alignVerticalCenter().alignHorizontalCenter(), + onSizeChanged = { _, _, _ -> }, + ) + rootLayout.setSecondElement( + LinearLayout( + width = width, + height = 32, + direction = LinearLayout.Direction.HORIZONTAL, + align = LinearLayout.Align.CENTER, + gap = 8, + ).apply { + add(closeButton, Positioner.create().apply { alignVerticalCenter() }) + }) + rootLayout.refreshPositions() + rootLayout.forEachChild { addDrawableChild(it) } + } +} \ No newline at end of file diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/ui/state/VmcConfigScreenState.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/state/VmcConfigScreenState.kt new file mode 100644 index 00000000..4f329a1a --- /dev/null +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/ui/state/VmcConfigScreenState.kt @@ -0,0 +1,8 @@ +package top.fifthlight.armorstand.ui.state + +import top.fifthlight.armorstand.vmc.VmcMarionetteManager + +data class VmcConfigScreenState( + val state: VmcMarionetteManager.State = VmcMarionetteManager.State.Stopped, + val portNumber: Int = 9000, +) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt index 2bdb2dbe..de6a477a 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt @@ -91,20 +91,27 @@ class VmcMarionetteClient( } init { + transport.connect() + require(transport.isBlocking) { "Require blocking transport" } scope.launch(Dispatchers.IO) { - while (true) { - val message = try { - transport.receive() - } catch (ex: Exception) { - if (!scope.isActive) { - // throw CancellationException - ensureActive() + try { + while (true) { + val message = try { + transport.receive() + } catch (ex: Exception) { + if (!scope.isActive) { + // throw CancellationException + ensureActive() + } + logger.warn("Failed to receive OSC message", ex) + continue } - logger.warn("Failed to receive OSC message", ex) - continue + handlePacket(message) + ensureActive() } - handlePacket(message) - ensureActive() + } finally { + transport.disconnect() + transport.close() } } } @@ -115,6 +122,5 @@ class VmcMarionetteClient( } closed = true scope.cancel() - transport.close() } } diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt new file mode 100644 index 00000000..3380db5d --- /dev/null +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt @@ -0,0 +1,61 @@ +package top.fifthlight.armorstand.vmc + +import com.illposed.osc.transport.udp.UDPTransport +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.slf4j.LoggerFactory +import java.net.Inet4Address +import java.net.InetSocketAddress + +object VmcMarionetteManager { + private val logger = LoggerFactory.getLogger(VmcMarionetteManager::class.java) + + sealed class State { + data object Stopped : State() + + data class Running(val port: Int) : State() + + data class Failed(val exception: Exception) : State() + } + + private val _state = MutableStateFlow(State.Stopped) + val state = _state.asStateFlow() + + private var client: VmcMarionetteClient? = null + + fun getState() = client?.getState() + + fun start(port: Int) = synchronized(this) { + val state = state.value + if (state is State.Running) { + return@synchronized + } + try { + require(port in 0..65535) { "Invalid UDP port number: $port" } + // We never send message + val transport = UDPTransport( + InetSocketAddress(Inet4Address.getLoopbackAddress(), port), + InetSocketAddress(Inet4Address.getLoopbackAddress(), port), + ) + val client = VmcMarionetteClient(transport) + this.client = client + _state.value = State.Running(port) + } catch (ex: Exception) { + logger.warn("Failed to start VMC client", ex) + _state.value = State.Failed(ex) + } + } + + fun stop() = synchronized(this) { + if (state.value !is State.Running) { + return + } + try { + client?.close() + client = null + _state.value = State.Stopped + } catch (ex: Exception) { + _state.value = State.Failed(ex) + } + } +} \ No newline at end of file diff --git a/mod/src/client/resources/assets/armorstand/lang/en_us.json b/mod/src/client/resources/assets/armorstand/lang/en_us.json index 74875602..962fb38b 100644 --- a/mod/src/client/resources/assets/armorstand/lang/en_us.json +++ b/mod/src/client/resources/assets/armorstand/lang/en_us.json @@ -8,6 +8,7 @@ "armorstand.config.invert_head_direction": "Invert head direction", "armorstand.config.open_model_directory": "Open Model Folder", "armorstand.config.renderer_select": "Select renderer", + "armorstand.config.vmc": "OSC/VMC", "armorstand.config.sort.name.asc": "Sort: A-Z", "armorstand.config.sort.name.desc": "Sort: Z-A", "armorstand.config.sort.last_changed.asc": "Sort: First changed", @@ -93,5 +94,12 @@ "armorstand.renderer.speed.fast": "Fast", "armorstand.renderer.shader_compatible": "Shader compatible", "armorstand.renderer.is_available": "Available", - "armorstand.renderer.is_current": "Current" + "armorstand.renderer.is_current": "Current", + "armorstand.vmc": "OSC/VMC", + "armorstand.vmc.running": "Running, port: %s", + "armorstand.vmc.stopped": "Not running", + "armorstand.vmc.failed": "Start failed: %s", + "armorstand.vmc.start": "Start", + "armorstand.vmc.stop": "Stop", + "armorstand.vmc.port": "Port:" } \ No newline at end of file diff --git a/mod/src/client/resources/assets/armorstand/lang/zh_cn.json b/mod/src/client/resources/assets/armorstand/lang/zh_cn.json index b4302196..42681a47 100644 --- a/mod/src/client/resources/assets/armorstand/lang/zh_cn.json +++ b/mod/src/client/resources/assets/armorstand/lang/zh_cn.json @@ -8,6 +8,7 @@ "armorstand.config.invert_head_direction": "反转抬头方向", "armorstand.config.open_model_directory": "打开模型文件夹", "armorstand.config.renderer_select": "选择渲染器", + "armorstand.config.vmc": "OSC/VMC", "armorstand.config.sort.name.asc": "排序:名称顺序", "armorstand.config.sort.name.desc": "排序:名称逆序", "armorstand.config.sort.last_changed.asc": "排序:时间顺序", @@ -93,5 +94,12 @@ "armorstand.renderer.speed.fast": "快", "armorstand.renderer.shader_compatible": "光影兼容", "armorstand.renderer.is_available": "可用性", - "armorstand.renderer.is_current": "当前使用" + "armorstand.renderer.is_current": "当前使用", + "armorstand.vmc": "OSC/VMC", + "armorstand.vmc.running": "运行中,端口:%s", + "armorstand.vmc.stopped": "未运行", + "armorstand.vmc.failed": "启动失败: %s", + "armorstand.vmc.start": "启动", + "armorstand.vmc.stop": "停止", + "armorstand.vmc.port": "端口:" } \ No newline at end of file From 831e4209afe7d34aa7834cdcf635c6eae2b68f37 Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sun, 24 Aug 2025 00:27:42 +0800 Subject: [PATCH 6/8] Add ModelController integration --- .../armorstand/state/ModelController.kt | 31 +++++++++++++++++++ .../armorstand/state/ModelInstanceManager.kt | 8 ++--- .../armorstand/vmc/VmcMarionetteClient.kt | 9 ++++-- .../armorstand/vmc/VmcMarionetteManager.kt | 6 ++-- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt index 726b267c..b20f798d 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt @@ -9,6 +9,7 @@ import top.fifthlight.armorstand.config.ConfigHolder import top.fifthlight.armorstand.extension.internal.PlayerEntityRenderStateExtInternal import top.fifthlight.armorstand.ui.model.AnimationViewModel import top.fifthlight.armorstand.util.toRadian +import top.fifthlight.armorstand.vmc.VmcMarionetteManager import top.fifthlight.blazerod.animation.AnimationItem import top.fifthlight.blazerod.animation.Timeline import top.fifthlight.blazerod.model.* @@ -28,6 +29,10 @@ sealed class ModelController { fun update(instance: ModelInstance, func: NodeTransform.Decomposed.() -> Unit) { instance.setTransformDecomposed(nodeIndex, TransformId.RELATIVE_ANIMATION, func) } + + fun updateAbsolute(instance: ModelInstance, func: NodeTransform.Decomposed.() -> Unit) { + instance.setTransformDecomposed(nodeIndex, TransformId.ABSOLUTE, func) + } } private sealed class ExpressionItem { @@ -333,4 +338,30 @@ sealed class ModelController { blinkExpression?.apply(instance, blinkProgress) } } + + class Vmc( + private val scene: RenderScene, + ) : ModelController() { + private val bones = mutableMapOf>() + private val expressions = mutableMapOf>() + + override fun apply(instance: ModelInstance) { + val state = VmcMarionetteManager.getState() ?: return + state.boneTransforms.forEach { (bone, value) -> + val item = bones.getOrPut(bone) { Optional.ofNullable(scene.getBone(bone)) } + item.ifPresent { + it.updateAbsolute(instance) { + translation.set(value.position) + rotation.set(value.rotation) + } + } + } + state.blendShapes.forEach { (tag, value) -> + val item = expressions.getOrPut(tag) { Optional.ofNullable(scene.getExpression(tag)) } + item.ifPresent { + it.apply(instance, value) + } + } + } + } } diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelInstanceManager.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelInstanceManager.kt index dc15c74e..6c44e41d 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelInstanceManager.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelInstanceManager.kt @@ -7,6 +7,7 @@ import net.fabricmc.loader.api.FabricLoader import net.minecraft.client.MinecraftClient import top.fifthlight.armorstand.ArmorStand import top.fifthlight.armorstand.config.ConfigHolder +import top.fifthlight.armorstand.vmc.VmcMarionetteManager import top.fifthlight.blazerod.animation.AnimationItem import top.fifthlight.blazerod.animation.AnimationLoader import top.fifthlight.blazerod.model.Metadata @@ -141,10 +142,7 @@ object ModelInstanceManager { return null } - val path = ClientModelPathManager.getPath(uuid) - if (path == null) { - return null - } + val path = ClientModelPathManager.getPath(uuid) ?: return null val lastAccessTime = if (isSelf) { -1 @@ -188,9 +186,11 @@ object ModelInstanceManager { lastAccessTime = lastAccessTime, instance = ModelInstance(scene), controller = run { + val vmcRunning = VmcMarionetteManager.state.value is VmcMarionetteManager.State.Running val animationSet = FullAnimationSet.from(cache.animationSet) val animation = cache.animations.firstOrNull() when { + isSelf && vmcRunning -> ModelController.Vmc(scene) animationSet != null -> ModelController.LiveSwitched(scene, animationSet) animation != null -> ModelController.Predefined(animation) else -> ModelController.LiveUpdated(scene) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt index de6a477a..b2f4171b 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteClient.kt @@ -4,6 +4,7 @@ import com.illposed.osc.OSCBundle import com.illposed.osc.OSCMessage import com.illposed.osc.OSCPacket import com.illposed.osc.transport.Transport +import com.illposed.osc.transport.udp.UDPTransport import kotlinx.coroutines.* import net.minecraft.client.MinecraftClient import org.slf4j.LoggerFactory @@ -91,7 +92,9 @@ class VmcMarionetteClient( } init { - transport.connect() + if (transport !is UDPTransport) { + transport.connect() + } require(transport.isBlocking) { "Require blocking transport" } scope.launch(Dispatchers.IO) { try { @@ -110,7 +113,9 @@ class VmcMarionetteClient( ensureActive() } } finally { - transport.disconnect() + if (transport !is UDPTransport) { + transport.disconnect() + } transport.close() } } diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt index 3380db5d..b9ba2b3d 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/vmc/VmcMarionetteManager.kt @@ -4,7 +4,7 @@ import com.illposed.osc.transport.udp.UDPTransport import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import org.slf4j.LoggerFactory -import java.net.Inet4Address +import java.net.InetAddress import java.net.InetSocketAddress object VmcMarionetteManager { @@ -34,8 +34,8 @@ object VmcMarionetteManager { require(port in 0..65535) { "Invalid UDP port number: $port" } // We never send message val transport = UDPTransport( - InetSocketAddress(Inet4Address.getLoopbackAddress(), port), - InetSocketAddress(Inet4Address.getLoopbackAddress(), port), + InetSocketAddress(InetAddress.getByAddress(byteArrayOf(0, 0, 0, 0)), port), + InetSocketAddress(InetAddress.getByAddress(byteArrayOf(0, 0, 0, 0)), 0), ) val client = VmcMarionetteClient(transport) this.client = client From 8fd5ec9611790a2d3cf279e349d8515d983b4a06 Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sun, 24 Aug 2025 00:37:56 +0800 Subject: [PATCH 7/8] Apply root transform data from OSC/VMC --- .../top/fifthlight/armorstand/state/ModelController.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt index b20f798d..e4a2ea9d 100644 --- a/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt +++ b/mod/src/client/kotlin/top/fifthlight/armorstand/state/ModelController.kt @@ -347,6 +347,12 @@ sealed class ModelController { override fun apply(instance: ModelInstance) { val state = VmcMarionetteManager.getState() ?: return + state.rootTransform?.let { + instance.setTransformDecomposed(scene.rootNode.nodeIndex, TransformId.ABSOLUTE) { + translation.set(it.position) + rotation.set(it.rotation) + } + } state.boneTransforms.forEach { (bone, value) -> val item = bones.getOrPut(bone) { Optional.ofNullable(scene.getBone(bone)) } item.ifPresent { From aaaf1bc2af3facb35a54bbdf4829ae2e48aedd29 Mon Sep 17 00:00:00 2001 From: fifth_light Date: Sun, 24 Aug 2025 12:14:27 +0800 Subject: [PATCH 8/8] Disable eye HumanoidTags --- .../kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt b/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt index 62894ef3..49bd3dd8 100644 --- a/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt +++ b/blazerod/model/model-base/src/main/kotlin/top/fifthlight/blazerod/model/HumanoidTag.kt @@ -16,8 +16,10 @@ enum class HumanoidTag( // Head HEAD(vrmName = "head", pmxEnglish = "Head", pmxJapanese = "頭"), - LEFT_EYE(vrmName = "leftEye", pmxEnglish = "LeftEye", pmxJapanese = "左目"), - RIGHT_EYE(vrmName = "rightEye", pmxEnglish = "RightEye", pmxJapanese = "右目"), + + // Disable them because we have no correct VRM gaze control + // LEFT_EYE(vrmName = "leftEye", pmxEnglish = "LeftEye", pmxJapanese = "左目"), + // RIGHT_EYE(vrmName = "rightEye", pmxEnglish = "RightEye", pmxJapanese = "右目"), JAW(vrmName = "jaw"), // Leg