Update VRM reader to the new VMC 1.0 spec (#1481)

This commit is contained in:
Uriel
2025-06-25 23:24:24 -04:00
committed by GitHub
parent add1e4eb5d
commit dd06e2b28a
3 changed files with 190 additions and 22 deletions

View File

@@ -70,11 +70,10 @@ export function VMCSettings() {
);
if (values.vmc.vrmJson !== undefined) {
if (values.vmc.vrmJson.length > 0) {
vmcOsc.vrmJson = await parseVRMFile(values.vmc.vrmJson[0]);
if (vmcOsc.vrmJson) {
setModelName(
JSON.parse(vmcOsc.vrmJson)?.extensions?.VRM?.meta?.title || ''
);
const file = await parseVRMFile(values.vmc.vrmJson[0]);
if (file) {
vmcOsc.vrmJson = file.json;
setModelName(file.name);
}
} else {
vmcOsc.vrmJson = '';
@@ -114,7 +113,7 @@ export function VMCSettings() {
}
const vrmJson = settings.vmcOsc.vrmJson?.toString();
if (vrmJson) {
setModelName(JSON.parse(vrmJson)?.extensions?.VRM?.meta?.title || '');
setModelName(getVRMName(vrmJson) || '');
}
formData.vmc.anchorHip = settings.vmcOsc.anchorHip;
@@ -299,7 +298,9 @@ export function VMCSettings() {
const gltfHeaderStart = 0;
const gltfHeaderEnd = 20;
async function parseVRMFile(vrm: File): Promise<string | null> {
async function parseVRMFile(
vrm: File
): Promise<{ json: string; name: string } | null> {
const headerView = new DataView(
await vrm.slice(gltfHeaderStart, gltfHeaderEnd).arrayBuffer()
);
@@ -337,7 +338,36 @@ async function parseVRMFile(vrm: File): Promise<string | null> {
return null;
}
return vrm
const json = await vrm
.slice(gltfHeaderEnd, gltfHeaderEnd + jsonLength, 'application/json')
.text();
const name = getVRMName(json);
if (name === null) return null;
return { json, name };
}
function getVRMName(json: string): string | null {
try {
const data = JSON.parse(json);
if (typeof data?.extensions?.VRMC_vrm?.specVersion === 'string') {
const name = data.extensions.VRMC_vrm.meta.name;
if (typeof name !== 'string') {
error(
`The name of the VRM model is not a string, instead it is a ${typeof name}`
);
return null;
}
return name;
} else {
return data?.extensions?.VRM?.meta?.title || '';
}
} catch (e) {
error(e);
return null;
}
}

View File

@@ -2,72 +2,184 @@ 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),
;

View File

@@ -4,7 +4,6 @@ import io.eiren.util.logging.LogManager
import io.github.axisangles.ktmath.Vector3
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.util.*
@@ -14,14 +13,23 @@ class VRMReader(vrmJson: String) {
private val data: GLTF = jsonIgnoreKeys.decodeFromString(vrmJson)
fun getOffsetForBone(unityBone: UnityBone): Vector3 {
val bone = try {
data.extensions.vrm.humanoid.humanBones.first { it.bone.equals(unityBone.stringVal, ignoreCase = true) }
} catch (e: NoSuchElementException) {
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")
return Vector3.NULL
}
null
} ?: return Vector3.NULL
val translationNode = data.nodes[bone.node].translation ?: return Vector3.NULL
val translationNode = data.nodes[node].translation ?: return Vector3.NULL
return Vector3(translationNode[0].toFloat(), translationNode[1].toFloat(), translationNode[2].toFloat())
}
@@ -37,17 +45,35 @@ data class GLTF(
@Serializable
data class Extensions(
@SerialName("VRM")
val vrm: VRM,
val vrmV0: VRMV0? = null,
@SerialName("VRMC_vrm")
val vrmV1: VRMV1? = null,
)
@Serializable
data class VRM(
val humanoid: Humanoid,
data class VRMV1(
val specVersion: String,
val humanoid: HumanoidV1,
)
@Serializable
data class Humanoid(
val humanBones: List<HumanBone>,
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,
@@ -59,7 +85,7 @@ data class Humanoid(
)
@Serializable
data class HumanBone(
data class HumanBoneV0(
val bone: String,
val node: Int,
val useDefaultValues: Boolean,