diff --git a/Basis/Packages/com.basis.framework/BasisUI/BasisMenuMover.cs b/Basis/Packages/com.basis.framework/BasisUI/BasisMenuMover.cs
index c93dcf16d0..a49f7f0b92 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/BasisMenuMover.cs
+++ b/Basis/Packages/com.basis.framework/BasisUI/BasisMenuMover.cs
@@ -65,6 +65,8 @@ public struct RootModeOffset
private bool _hasLocalMoveEvent;
private const float MIN_Z_SCALE = 0.01f;
+ // MIN_TMP_RENDER_SCALE is empirical: TMP rendered block glyphs on the main menu below roughly 0.05328 world scale during OSC tiny-avatar testing.
+ private const float MIN_TMP_RENDER_SCALE = 0.055f;
// --- PlaySpaceStable state (from v1) ---
private bool _stableHasAnchor;
@@ -254,8 +256,7 @@ private void SetRootOffset(RootModeOffset offset)
offsetScale.z = Mathf.Max(MIN_Z_SCALE, offsetScale.z);
GroupOffset.localScale = offsetScale;
- // Root is avatar-compensated
- transform.localScale = Vector3.one * playerHeight;
+ transform.localScale = Vector3.one * GetRenderSafeMenuScale(playerHeight);
}
private void SetEyeOffset(float scaleFactor)
@@ -269,7 +270,7 @@ private void SetEyeOffset(float scaleFactor)
offsetScale.z = Mathf.Max(MIN_Z_SCALE, offsetScale.z);
GroupOffset.localScale = offsetScale;
- transform.localScale = Vector3.one * playerHeight;
+ transform.localScale = Vector3.one * GetRenderSafeMenuScale(playerHeight);
}
///
@@ -290,8 +291,18 @@ private void ApplyScaleOnly()
offsetScale.z = Mathf.Max(MIN_Z_SCALE, offsetScale.z);
GroupOffset.localScale = offsetScale;
- // 2) Root scale (avatar-to-default compensation)
- transform.localScale = Vector3.one * BasisHeightDriver.AvatarToDefaultRatioScaledWithAvatarScale;
+ transform.localScale = Vector3.one * GetRenderSafeMenuScale(BasisHeightDriver.AvatarToDefaultRatioScaledWithAvatarScale);
+ }
+
+ private static float GetRenderSafeMenuScale(float avatarRelativeScale)
+ {
+ // TextMeshPro renders block glyphs at extremely small world scales; keep menu rendering stable without clamping avatar scale.
+ if (float.IsNaN(avatarRelativeScale) || float.IsInfinity(avatarRelativeScale) || avatarRelativeScale <= 0f)
+ {
+ return MIN_TMP_RENDER_SCALE;
+ }
+
+ return Mathf.Max(avatarRelativeScale, MIN_TMP_RENDER_SCALE);
}
private void UpdateUILocation()
diff --git a/Basis/Packages/com.basis.framework/BasisUI/BasisSettingsDefaults.cs b/Basis/Packages/com.basis.framework/BasisUI/BasisSettingsDefaults.cs
index 370af71689..7e9b0d3990 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/BasisSettingsDefaults.cs
+++ b/Basis/Packages/com.basis.framework/BasisUI/BasisSettingsDefaults.cs
@@ -47,6 +47,12 @@ public static class BasisSettingsDefaults
///
public static BasisSettingsBinding DisableAnimationsInFBT = new("disableanimationsinfbt", new BasisPlatformDefault(false));
+ ///
+ /// When enabled, avatar scale affects locomotion blend velocity so small avatars
+ /// animate faster and large avatars animate slower without changing movement speed.
+ ///
+ public static BasisSettingsBinding ScaleAffectsLocomotionSpeed = new("scaleaffectslocomotionspeed", new BasisPlatformDefault(false));
+
///
/// Master switch for full-body tracking. When disabled, hip/chest/foot/knee
/// trackers are ignored and the avatar falls back to head + hands + procedural
@@ -1242,6 +1248,7 @@ public static void LoadAll()
EnableEyeTracking.LoadBindingValue();
FootIKEnabled.LoadBindingValue();
DisableAnimationsInFBT.LoadBindingValue();
+ ScaleAffectsLocomotionSpeed.LoadBindingValue();
LocalHeadBlendShapes.LoadBindingValue();
// Rendering / Graphics
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ar.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ar.json
index 097a272e7b..cae948acd4 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ar.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ar.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "الأفاتار" },
{ "key": "menu.individualPlayer.avatar.description", "value": "مفاتيح الإظهار والتفاعل." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "خطأ في تحميل الأفاتار" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "مطابقة ارتفاع العين" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "غيّر مقياس أفاتارك ليطابق مقياس جسم هذا اللاعب الحالي ({0:0.###} م كارتفاع عين مستهدف)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "ارتفاع عين أفاتار هذا اللاعب الحالي غير متاح بعد." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "إخفاء الأفاتار" },
{ "key": "menu.individualPlayer.showAvatar", "value": "إظهار الأفاتار" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "تفعيل/تعطيل عرض أفاتار هذا اللاعب على جهازك." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "تشغيل عظام عيون أفاتارك من بيانات تتبع العين. النظر الطبيعي للعين يستمر عند التعطيل." },
{ "key": "settings.bodyTracking.footIk.description", "value": "يفعّل وضع القدم الإجرائي عند الوقوف ساكناً بدون متتبعات قدم." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "يكبح حركات القفز والهبوط وانخفاض الورك عند الهبوط أثناء معايرة متتبعات الجسم الكامل." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "يؤثر المقياس في سرعة التنقل" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "يضبط سرعة رسوم التنقل المتحركة وفقًا لمقياس الأفاتار دون تغيير سرعة الحركة الفعلية." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "يفعّل تصادم الكبسولات الافتراضية بين المرفقين والصدر لمنع اختراق الذراع للجسم." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "حماية المرفق" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/bn.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/bn.json
index dc00ef1b63..5e9a35af9d 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/bn.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/bn.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "অবতার" },
{ "key": "menu.individualPlayer.avatar.description", "value": "দৃশ্যমানতা এবং ইন্টারঅ্যাকশন টগল।" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "অবতার লোড ত্রুটি" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "চোখের উচ্চতা মেলান" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "এই খেলোয়াড়ের বর্তমান শরীরের স্কেলের সাথে আপনার অবতার স্কেল করুন ({0:0.###} মি লক্ষ্য চোখের উচ্চতা)।" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "এই খেলোয়াড়ের বর্তমান অবতার চোখের উচ্চতা এখনো উপলভ্য নয়।" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "অবতার লুকান" },
{ "key": "menu.individualPlayer.showAvatar", "value": "অবতার দেখান" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "আপনার ক্লায়েন্টে এই খেলোয়াড়ের অবতারের রেন্ডারিং টগল করে।" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "চোখ ট্র্যাকিং ডেটা থেকে আপনার অবতারের চোখের হাড় চালান। অক্ষম থাকলে স্বাভাবিক চোখের লুক চলতে থাকে।" },
{ "key": "settings.bodyTracking.footIk.description", "value": "পা ট্র্যাকার ছাড়া স্থির দাঁড়িয়ে থাকার সময় পদ্ধতিগত পা স্থাপনা সক্ষম করে।" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "সম্পূর্ণ-শরীর ট্র্যাকার ক্যালিব্রেট থাকাকালীন জাম্প এবং ল্যান্ডিং অ্যানিমেশন এবং ল্যান্ডিং হিপ ডিপ দমন করে।" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "স্কেল লোকোমোশনের গতিকে প্রভাবিত করে" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "শারীরিক চলাচলের গতি না বদলে, অবতারের স্কেল অনুযায়ী লোকোমোশন অ্যানিমেশনের গতি সমন্বয় করে।" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "শরীরের ভিতর দিয়ে বাহু ক্লিপিং প্রতিরোধ করতে কনুই এবং বুকের মধ্যে ভার্চুয়াল ক্যাপসুল সংঘর্ষ সক্ষম করে।" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "কনুই রক্ষা করুন" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/de.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/de.json
index 2fadba1565..d4a89c7b85 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/de.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/de.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Sichtbarkeits- und Interaktions-Schalter." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Avatar-Ladefehler" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Augenhöhe anpassen" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Skaliert deinen Avatar auf die aktuelle Körpergröße dieses Spielers ({0:0.###} m Ziel-Augenhöhe)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "Die aktuelle Avatar-Augenhöhe dieses Spielers ist noch nicht verfügbar." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Avatar ausblenden" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Avatar anzeigen" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Schaltet das Rendern des Avatars dieses Spielers auf deinem Client um." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Steuert die Augenknochen deines Avatars über Eye-Tracking-Daten. Der natürliche Augenblick läuft weiter, wenn deaktiviert." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Aktiviert prozedurales Fußplatzieren beim ruhigen Stehen ohne Fuß-Tracker." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Unterdrückt Sprung- und Lande-Animationen sowie das Hüftabsenken bei der Landung, solange Full-Body-Tracker kalibriert sind." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "Skalierung beeinflusst Fortbewegungsgeschwindigkeit" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Passt die Geschwindigkeit der Geh-/Laufanimation an die Avatar-Skalierung an, ohne die tatsächliche Bewegungsgeschwindigkeit zu verändern." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Aktiviert die virtuelle Kapsel-Kollision zwischen Ellenbogen und Brust, um zu verhindern, dass Arme durch den Körper schneiden." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Ellenbogen schützen" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/en.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/en.json
index f2567c137d..7cd0c3463f 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/en.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/en.json
@@ -186,6 +186,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Visibility and interaction toggles." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Avatar Load Error" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Match Eye Height" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Scale your avatar to match this player's current body scale ({0:0.###} m target eye height)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "This player's current avatar eye height is not available yet." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Hide Avatar" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Show Avatar" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Toggles rendering of this player's avatar on your client." },
@@ -686,6 +689,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Drive your avatar's eye bones from eye tracking data. The natural eye look keeps running when disabled." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Enables procedural foot placement when standing still without foot trackers." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Suppresses jump and landing animations and the landing hip dip while full-body trackers are calibrated." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "Scale Affects Locomotion Speed" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Adjusts locomotion animation velocity by avatar scale without changing physical movement speed." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Enables virtual capsule collision between elbows and chest to prevent arm clipping through the body." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Protect Elbow" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es-MX.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es-MX.json
index 2397a1182f..4cfcc10f30 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es-MX.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es-MX.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Visibilidad y opciones de interacción." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Error al cargar el avatar" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Igualar altura de ojos" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Escala tu avatar para igualar la escala corporal actual de este jugador ({0:0.###} m de altura de ojos objetivo)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "La altura de ojos actual del avatar de este jugador aún no está disponible." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Ocultar avatar" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Mostrar avatar" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Activa o desactiva el renderizado del avatar de este jugador en tu cliente." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Controla los huesos de los ojos de tu avatar a partir de datos de rastreo ocular. La mirada natural sigue funcionando cuando está desactivado." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Activa la colocación procedural de los pies al estar quieto sin trackers de pies." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Suprime las animaciones de salto y aterrizaje y la caída de cadera al aterrizar mientras los trackers de cuerpo completo estén calibrados." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "La escala afecta la velocidad de locomoción" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Ajusta la velocidad de la animación de locomoción según la escala del avatar sin cambiar la velocidad de movimiento físico." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Activa la colisión de cápsulas virtuales entre los codos y el pecho para evitar que los brazos atraviesen el cuerpo." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Proteger codo" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es.json
index 4836d8437e..fdaa910c1f 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/es.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Visibilidad y opciones de interacción." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Error al cargar el avatar" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Igualar altura de ojos" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Escala tu avatar para igualar la escala corporal actual de este jugador ({0:0.###} m de altura de ojos objetivo)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "La altura de ojos actual del avatar de este jugador aún no está disponible." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Ocultar avatar" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Mostrar avatar" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Activa o desactiva el renderizado del avatar de este jugador en tu cliente." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Acciona los huesos de los ojos de tu avatar a partir de los datos de seguimiento ocular. La mirada natural sigue funcionando cuando está desactivado." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Activa la colocación procedural de los pies cuando estás quieto sin trackers de pies." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Suprime las animaciones de salto y aterrizaje y la flexión de cadera al aterrizar mientras los trackers de cuerpo completo estén calibrados." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "La escala afecta la velocidad de locomoción" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Ajusta la velocidad de la animación de locomoción según la escala del avatar sin cambiar la velocidad física de movimiento." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Activa la colisión por cápsula virtual entre los codos y el pecho para evitar que los brazos atraviesen el cuerpo." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Proteger codo" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/fr.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/fr.json
index 7acf646cfa..084e55cd26 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/fr.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/fr.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Options de visibilité et d'interaction." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Erreur de chargement d'avatar" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Adapter la hauteur des yeux" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Redimensionne votre avatar pour correspondre à l'échelle corporelle actuelle de ce joueur ({0:0.###} m de hauteur des yeux cible)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "La hauteur actuelle des yeux de l'avatar de ce joueur n'est pas encore disponible." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Masquer l'avatar" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Afficher l'avatar" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Bascule le rendu de l'avatar de ce joueur sur votre client." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Pilote les os des yeux de votre avatar à partir des données de suivi oculaire. Le regard naturel des yeux continue de fonctionner lorsqu'il est désactivé." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Active le placement procédural des pieds en position immobile sans trackers de pieds." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Supprime les animations de saut et d'atterrissage ainsi que la flexion des hanches à l'atterrissage tant que les trackers corps complet sont calibrés." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "L'échelle affecte la vitesse de locomotion" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Ajuste la vitesse de l'animation de locomotion selon l'échelle de l'avatar sans modifier la vitesse de déplacement physique." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Active la collision par capsule virtuelle entre les coudes et le torse pour empêcher les bras de traverser le corps." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Protéger le coude" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/hi.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/hi.json
index e6a23d6777..e1217a7300 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/hi.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/hi.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "अवतार" },
{ "key": "menu.individualPlayer.avatar.description", "value": "दृश्यता और इंटरैक्शन टॉगल।" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "अवतार लोड त्रुटि" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "आँख की ऊँचाई मिलाएँ" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "अपने अवतार को इस खिलाड़ी के मौजूदा शरीर स्केल से मिलाएँ ({0:0.###} मी लक्ष्य आँख ऊँचाई)।" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "इस खिलाड़ी के अवतार की मौजूदा आँख ऊँचाई अभी उपलब्ध नहीं है।" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "अवतार छिपाएँ" },
{ "key": "menu.individualPlayer.showAvatar", "value": "अवतार दिखाएँ" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "आपके क्लाइंट पर इस खिलाड़ी के अवतार के रेंडरिंग को टॉगल करता है।" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "आई ट्रैकिंग डेटा से अपने अवतार की आँख की हड्डियों को चलाएँ। अक्षम होने पर प्राकृतिक आँख देखना चलता रहता है।" },
{ "key": "settings.bodyTracking.footIk.description", "value": "बिना पैर ट्रैकर्स के स्थिर खड़े होने पर प्रोसीजरल पैर प्लेसमेंट सक्षम करता है।" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "जब फुल-बॉडी ट्रैकर्स कैलिब्रेट किए गए हों तो जंप और लैंडिंग एनिमेशन और लैंडिंग कूल्हा डिप को दबाता है।" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "स्केल लोकोमोशन की गति को प्रभावित करता है" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "भौतिक गति की रफ़्तार बदले बिना अवतार के स्केल के अनुसार लोकोमोशन एनीमेशन की गति समायोजित करता है।" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "हाथों को शरीर से क्लिप होने से रोकने के लिए कोहनियों और छाती के बीच वर्चुअल कैप्सूल कोलिज़न सक्षम करता है।" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "कोहनी की सुरक्षा करें" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/it.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/it.json
index 35301e1b2a..34bd6fb27d 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/it.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/it.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Opzioni di visibilità e interazione." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Errore caricamento avatar" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Abbina altezza occhi" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Ridimensiona il tuo avatar per corrispondere alla scala corporea attuale di questo giocatore ({0:0.###} m altezza occhi target)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "L'altezza occhi attuale dell'avatar di questo giocatore non è ancora disponibile." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Nascondi avatar" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Mostra avatar" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Attiva o disattiva il rendering dell'avatar di questo giocatore sul tuo client." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Pilota le ossa degli occhi del tuo avatar con i dati di tracciamento oculare. Lo sguardo naturale degli occhi continua a funzionare quando disabilitato." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Abilita il posizionamento procedurale dei piedi quando si è fermi senza tracker per i piedi." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Sopprime le animazioni di salto e atterraggio e l'abbassamento delle anche all'atterraggio mentre i tracker corporei completi sono calibrati." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "La scala influisce sulla velocità di locomozione" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Regola la velocità dell'animazione di locomozione in base alla scala dell'avatar senza modificare la velocità di movimento fisica." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Abilita la collisione tra capsule virtuali tra gomiti e torace per evitare che le braccia attraversino il corpo." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Proteggi gomito" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ja.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ja.json
index dbe148e9f1..27ec6c7052 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ja.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ja.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "アバター" },
{ "key": "menu.individualPlayer.avatar.description", "value": "表示とインタラクションの切替。" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "アバター読み込みエラー" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "目の高さを合わせる" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "このプレイヤーの現在の体のスケールに合わせて自分のアバターを拡大縮小します(目標の目の高さ {0:0.###} m)。" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "このプレイヤーの現在のアバターの目の高さはまだ利用できません。" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "アバターを非表示" },
{ "key": "menu.individualPlayer.showAvatar", "value": "アバターを表示" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "自分のクライアントでこのプレイヤーのアバター描画を切替。" },
@@ -583,6 +586,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "視線トラッキングデータからアバターの目ボーンを駆動します。無効化中も自然な視線の動きは継続します。" },
{ "key": "settings.bodyTracking.footIk.description", "value": "足トラッカーなしで静止しているとき、足のプロシージャル配置を有効にします。" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "フルボディトラッカーがキャリブレーション済みの間、ジャンプと着地のアニメーション、着地時の腰沈み込みを抑制します。" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "スケールが移動アニメーション速度に影響する" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "アバターのスケールに応じて歩行・移動アニメーションの速度を調整しますが、物理的な移動速度は変更しません。" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "肘と胸の間に仮想カプセルコリジョンを有効化し、腕が体を貫通するのを防ぎます。" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "肘を保護" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/nl.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/nl.json
index 1f01d4a214..d10e79310e 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/nl.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/nl.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Schakelaars voor zichtbaarheid en interactie." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Avatar Laadfout" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Ooghoogte Afstemmen" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Schaal je avatar zodat deze overeenkomt met de huidige lichaamsschaal van deze speler ({0:0.###} m doel-ooghoogte)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "De huidige avatar-ooghoogte van deze speler is nog niet beschikbaar." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Avatar Verbergen" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Avatar Tonen" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Schakelt de weergave van de avatar van deze speler in jouw client." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Stuur de oogbeenderen van je avatar aan vanuit eye tracking-gegevens. De natuurlijke oogblik blijft draaien wanneer uitgeschakeld." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Schakelt procedurele voetplaatsing in wanneer je stilstaat zonder voettrackers." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Onderdrukt sprong- en landingsanimaties en de heupdip bij landing terwijl full-body trackers gekalibreerd zijn." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "Schaal beïnvloedt locomotiesnelheid" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Past de snelheid van de locomotie-animatie aan op basis van de avatarschaal zonder de fysieke bewegingssnelheid te veranderen." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Schakelt virtuele capsule-botsing tussen ellebogen en borst in om te voorkomen dat armen door het lichaam clippen." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Elleboog Beschermen" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/pt.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/pt.json
index 8462c1d844..b01290d070 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/pt.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/pt.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Avatar" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Alternar visibilidade e interação." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Erro ao Carregar Avatar" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Igualar Altura dos Olhos" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Redimensiona seu avatar para corresponder à escala corporal atual deste jogador ({0:0.###} m de altura dos olhos alvo)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "A altura atual dos olhos do avatar deste jogador ainda não está disponível." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Ocultar Avatar" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Mostrar Avatar" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Alterna a renderização do avatar deste jogador no seu cliente." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Comanda os ossos dos olhos do seu avatar a partir de dados de rastreamento ocular. O olhar natural continua funcionando quando desativado." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Habilita o posicionamento procedural dos pés ao ficar parado sem trackers de pé." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Suprime as animações de pulo e aterrissagem e o agachamento do quadril ao aterrissar enquanto trackers de corpo completo estiverem calibrados." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "A escala afeta a velocidade de locomoção" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Ajusta a velocidade da animação de locomoção de acordo com a escala do avatar sem alterar a velocidade física de movimento." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Habilita a colisão de cápsula virtual entre cotovelos e peito para evitar que os braços atravessem o corpo." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Proteger Cotovelo" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ru.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ru.json
index 2bd7e2e5a8..6de5a9b0bd 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ru.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ru.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "Аватар" },
{ "key": "menu.individualPlayer.avatar.description", "value": "Переключатели видимости и взаимодействия." },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "Ошибка загрузки аватара" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "Сравнять высоту глаз" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "Масштабирует ваш аватар под текущий масштаб тела этого игрока ({0:0.###} м целевой высоты глаз)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "Текущая высота глаз аватара этого игрока пока недоступна." },
{ "key": "menu.individualPlayer.hideAvatar", "value": "Скрыть аватар" },
{ "key": "menu.individualPlayer.showAvatar", "value": "Показать аватар" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "Включает/выключает отображение аватара этого игрока на вашем клиенте." },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "Управляет костями глаз вашего аватара по данным отслеживания глаз. Естественный взгляд продолжает работать при отключении." },
{ "key": "settings.bodyTracking.footIk.description", "value": "Включает процедурное размещение ступней в неподвижном состоянии без трекеров ступней." },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "Подавляет анимации прыжка и приземления, а также провисание бёдер при приземлении, пока откалиброваны трекеры полного отслеживания тела." },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "Масштаб влияет на скорость передвижения" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "Подстраивает скорость анимации передвижения под масштаб аватара, не изменяя фактическую скорость движения." },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "Включает виртуальные столкновения капсул между локтями и грудью, чтобы предотвратить прохождение рук сквозь тело." },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "Защитить локоть" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ur.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ur.json
index 872ffa5b62..5f27088ce4 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ur.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/ur.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "اواتار" },
{ "key": "menu.individualPlayer.avatar.description", "value": "ظاہریت اور تعامل کے ٹوگلز۔" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "اواتار لوڈ خرابی" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "آنکھوں کی اونچائی ملائیں" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "اپنے اواتار کو اس کھلاڑی کے موجودہ جسمانی اسکیل سے ملانے کے لیے اسکیل کریں ({0:0.###} م ہدف آنکھوں کی اونچائی)." },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "اس کھلاڑی کے اواتار کی موجودہ آنکھوں کی اونچائی ابھی دستیاب نہیں ہے۔" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "اواتار چھپائیں" },
{ "key": "menu.individualPlayer.showAvatar", "value": "اواتار دکھائیں" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "آپ کے کلائنٹ پر اس کھلاڑی کے اواتار کی رینڈرنگ کو ٹوگل کرتا ہے۔" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "آنکھ کی ٹریکنگ ڈیٹا سے اپنے اواتار کی آنکھ کی ہڈیاں چلائیں۔ غیر فعال ہونے پر قدرتی آنکھ کا منظر چلتا رہتا ہے۔" },
{ "key": "settings.bodyTracking.footIk.description", "value": "بغیر پاؤں ٹریکرز کے ساکن کھڑے ہونے پر طریقہ کار سے پاؤں کی پلیسمنٹ کو فعال کرتا ہے۔" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "فل باڈی ٹریکرز کیلیبریٹ ہونے کے دوران چھلانگ اور لینڈنگ اینیمیشنز اور لینڈنگ ہپ ڈپ کو دباتا ہے۔" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "پیمانہ نقل و حرکت کی رفتار کو متاثر کرتا ہے" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "اوتار کے پیمانے کے مطابق حرکت کی اینیمیشن کی رفتار کو ایڈجسٹ کرتا ہے، جبکہ جسمانی حرکت کی رفتار تبدیل نہیں ہوتی۔" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "بازو کو جسم سے گزرنے سے روکنے کے لیے کہنیوں اور سینے کے درمیان ورچوئل کیپسول تصادم کو فعال کرتا ہے۔" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "کہنی کی حفاظت" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-CN.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-CN.json
index 0c16b85492..d0a27488e5 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-CN.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-CN.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "虚拟形象" },
{ "key": "menu.individualPlayer.avatar.description", "value": "可见性和交互切换。" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "虚拟形象加载错误" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "匹配眼高" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "将你的虚拟形象缩放到匹配该玩家当前身体比例(目标眼高 {0:0.###} 米)。" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "该玩家当前虚拟形象眼高尚不可用。" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "隐藏虚拟形象" },
{ "key": "menu.individualPlayer.showAvatar", "value": "显示虚拟形象" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "切换在你的客户端上是否渲染该玩家的虚拟形象。" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "通过眼部追踪数据驱动化身的眼部骨骼。禁用时自然眼神望向继续运行。" },
{ "key": "settings.bodyTracking.footIk.description", "value": "在没有脚部追踪器静止站立时启用程序化脚部放置。" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "在全身追踪器已校准时抑制跳跃和落地动画以及落地时的髋部下沉。" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "缩放会影响移动动画速度" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "会根据头像缩放调整移动动画速度,但不会改变实际移动速度。" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "启用肘部与胸部之间的虚拟胶囊体碰撞,防止手臂穿过身体。" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "保护肘部" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hans.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hans.json
index b8f97d5019..610f9e37b2 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hans.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hans.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "虚拟形象" },
{ "key": "menu.individualPlayer.avatar.description", "value": "可见性和交互切换。" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "虚拟形象加载错误" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "匹配眼高" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "将你的虚拟形象缩放到匹配该玩家当前身体比例(目标眼高 {0:0.###} 米)。" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "该玩家当前虚拟形象眼高尚不可用。" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "隐藏虚拟形象" },
{ "key": "menu.individualPlayer.showAvatar", "value": "显示虚拟形象" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "切换在你的客户端上是否渲染该玩家的虚拟形象。" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "通过眼部追踪数据驱动化身的眼部骨骼。禁用时自然眼神望向继续运行。" },
{ "key": "settings.bodyTracking.footIk.description", "value": "在没有脚部追踪器静止站立时启用程序化脚部放置。" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "在全身追踪器已校准时抑制跳跃和落地动画以及落地时的髋部下沉。" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "缩放会影响移动动画速度" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "会根据头像缩放调整移动动画速度,但不会改变实际移动速度。" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "启用肘部与胸部之间的虚拟胶囊体碰撞,防止手臂穿过身体。" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "保护肘部" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hant.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hant.json
index 1200b31e49..0878e15ac0 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hant.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh-Hant.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "虚拟形象" },
{ "key": "menu.individualPlayer.avatar.description", "value": "可见性和互动开关。" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "虚拟形象加载错误" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "匹配眼高" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "將你的虛擬形象縮放到匹配該玩家目前身體比例(目標眼高 {0:0.###} 公尺)。" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "該玩家目前虛擬形象眼高尚不可用。" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "隐藏虚拟形象" },
{ "key": "menu.individualPlayer.showAvatar", "value": "显示虚拟形象" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "在你的客户端上切换该玩家虚拟形象的渲染。" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "透過眼部追蹤資料驅動虛擬人的眼部骨骼。停用時自然眼神望向繼續執行。" },
{ "key": "settings.bodyTracking.footIk.description", "value": "在沒有腳部追蹤器靜止站立時啟用程序化腳部放置。" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "在全身追蹤器已校準時抑制跳躍和落地動畫以及落地時的髖部下沉。" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "縮放會影響移動動畫速度" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "會依照頭像縮放調整移動動畫速度,但不會改變實際移動速度。" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "啟用肘部與胸部之間的虛擬膠囊體碰撞,防止手臂穿過身體。" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "保護肘部" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh.json b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh.json
index b5ace579f3..b9f126a6b1 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh.json
+++ b/Basis/Packages/com.basis.framework/BasisUI/Localization/Languages/zh.json
@@ -158,6 +158,9 @@
{ "key": "menu.individualPlayer.avatar", "value": "虚拟形象" },
{ "key": "menu.individualPlayer.avatar.description", "value": "可见性和互动开关。" },
{ "key": "menu.individualPlayer.avatarLoadError", "value": "虚拟形象加载错误" },
+ { "key": "menu.individualPlayer.matchEyeHeight", "value": "匹配眼高" },
+ { "key": "menu.individualPlayer.matchEyeHeight.description", "value": "將你的虛擬形象縮放到匹配該玩家目前身體比例(目標眼高 {0:0.###} 公尺)。" },
+ { "key": "menu.individualPlayer.matchEyeHeight.unavailable", "value": "該玩家目前虛擬形象眼高尚不可用。" },
{ "key": "menu.individualPlayer.hideAvatar", "value": "隐藏虚拟形象" },
{ "key": "menu.individualPlayer.showAvatar", "value": "显示虚拟形象" },
{ "key": "menu.individualPlayer.toggleAvatar.description", "value": "在你的客户端上切换该玩家虚拟形象的渲染。" },
@@ -582,6 +585,8 @@
{ "key": "settings.bodyTracking.eyeTracking.description", "value": "通过眼部追踪数据驱动化身的眼部骨骼。禁用时自然眼神望向继续运行。" },
{ "key": "settings.bodyTracking.footIk.description", "value": "在没有脚部追踪器静止站立时启用程序化脚部放置。" },
{ "key": "settings.bodyTracking.disableAnimFbt.description", "value": "在全身追踪器已校准时抑制跳跃和落地动画以及落地时的髋部下沉。" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion", "value": "缩放会影响移动动画速度" },
+ { "key": "settings.bodyTracking.scaleAffectsLocomotion.description", "value": "会根据头像缩放调整移动动画速度,但不会改变实际移动速度。" },
{ "key": "settings.bodyTracking.collisionsEnabled.description", "value": "启用肘部与胸部之间的虚拟胶囊体碰撞,防止手臂穿过身体。" },
{ "key": "settings.bodyTracking.protectElbow.title", "value": "保护肘部" },
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/IndividualPlayerProvider.cs b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/IndividualPlayerProvider.cs
index 1cde3b1a7b..0b39844e45 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/IndividualPlayerProvider.cs
+++ b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/IndividualPlayerProvider.cs
@@ -48,6 +48,26 @@ public static void AddToMenu()
private static readonly Dictionary s_volumeBeforeMute = new Dictionary();
private const float UnmuteFallbackVolume = 1.0f;
+ private static bool TryGetMatchedEyeHeightOverrideMeters(BasisRemotePlayer target, out float eyeHeightMeters)
+ {
+ eyeHeightMeters = 0f;
+ if (target?.NetworkReceiver == null || BasisLocalPlayer.Instance?.LocalAvatarDriver == null)
+ {
+ return false;
+ }
+
+ target.NetworkReceiver.GetLatestNetworkPose(out _, out _, out var networkScale);
+ float localCalibrationScale = BasisLocalPlayer.Instance.LocalAvatarDriver.ScaleAvatarModification.DuringCalibrationScale.y;
+ if (float.IsNaN(localCalibrationScale) || float.IsInfinity(localCalibrationScale) || localCalibrationScale <= 0f)
+ {
+ localCalibrationScale = 1f;
+ }
+
+ float matchedApplyScale = networkScale.y / localCalibrationScale;
+ eyeHeightMeters = BasisHeightDriver.AvatarEyeHeight * matchedApplyScale;
+ return !float.IsNaN(eyeHeightMeters) && !float.IsInfinity(eyeHeightMeters) && eyeHeightMeters > 0f;
+ }
+
// ===== Shared player action helpers (used by this panel and UserListProvider rows) =====
///
@@ -791,6 +811,35 @@ void RefreshDirectConnLabel()
avatarErrorField.SetDescription(remotePlayer.AvatarLoadErrorMessage);
}
+ PanelButton matchEyeHeightBtn = PanelButton.CreateNew(avatarGroup.ContentParent);
+ matchEyeHeightBtn.Descriptor.SetTitle(BasisLocalization.Get("menu.individualPlayer.matchEyeHeight"));
+ if (TryGetMatchedEyeHeightOverrideMeters(remotePlayer, out float initialRemoteEyeHeight))
+ {
+ matchEyeHeightBtn.Descriptor.SetDescription(BasisLocalization.Get("menu.individualPlayer.matchEyeHeight.description", initialRemoteEyeHeight));
+ }
+ else
+ {
+ matchEyeHeightBtn.Descriptor.SetDescription(BasisLocalization.Get("menu.individualPlayer.matchEyeHeight.unavailable"));
+ }
+
+ matchEyeHeightBtn.OnClicked += () =>
+ {
+ if (!SMModuleCalibration.ApplyCustomScale)
+ {
+ BasisSettingsDefaults.CustomScale.SetValue(true);
+ SMModuleCalibration.ApplyCustomScale = true;
+ }
+
+ if (!TryGetMatchedEyeHeightOverrideMeters(remotePlayer, out float remoteEyeHeight))
+ {
+ BasisDebug.LogWarning("Cannot match eye height because the selected remote avatar eye height is unavailable.", BasisDebug.LogTag.Avatar);
+ return;
+ }
+
+ BasisHeightDriver.ApplyRuntimeOscEyeHeightOverride(remoteEyeHeight);
+ matchEyeHeightBtn.Descriptor.SetDescription(BasisLocalization.Get("menu.individualPlayer.matchEyeHeight.description", remoteEyeHeight));
+ };
+
// Performance filter result — tells the local user why a specific remote
// avatar was hard-blocked and/or what the trim pass removed from it,
// and offers a per-player bypass toggle so they can see this one avatar
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProvider.cs b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProvider.cs
index 089c5ca06b..f171911108 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProvider.cs
+++ b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProvider.cs
@@ -1182,7 +1182,7 @@ public static PanelTabPage GraphicsTab(PanelTabGroup tabGroup)
// before per-pixel quality knobs.
PanelSlider sliderAvatarRange = PanelSlider.CreateEntryAndBind(
qualityGroup,
- PanelSlider.SliderSettings.Distance(BasisLocalization.Get("settings.general.avatarRange"), 100),
+ PanelSlider.SliderSettings.Distance(BasisLocalization.Get("settings.general.avatarRange"), 1000),
BasisSettingsDefaults.AvatarRange);
PanelToggle toggleLimitAvatars = PanelToggle.CreateNewEntry(qualityGroup);
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProviderParts/SettingsProviderIK.cs b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProviderParts/SettingsProviderIK.cs
index 2bd226f8f2..9b4d73fb7b 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProviderParts/SettingsProviderIK.cs
+++ b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/SettingsProviderParts/SettingsProviderIK.cs
@@ -25,6 +25,7 @@ public static class SettingsProviderIK
private static PanelToggle _uiEuroPos;
private static PanelToggle _uiEuroRot;
private static PanelSlider _uiCalibSphereScale;
+ private static PanelSlider _avatarScaleSlider;
private static PanelElementDescriptor _boneEuroEditorGroup;
private struct BoneBindings
@@ -104,6 +105,7 @@ public static PanelTabPage IKTab(PanelTabGroup tabGroup)
ikParent,
PanelSlider.SliderSettings.Advanced("Avatar Height Scale", 0.1f, 5f, false, 2, ValueDisplayMode.Meters),
BasisSettingsDefaults.SelectedScale);
+ _avatarScaleSlider = avatarScaleSlider;
if (avatarScaleSlider != null)
{
@@ -187,6 +189,11 @@ public static PanelTabPage IKTab(PanelTabGroup tabGroup)
disableAnimInFBTToggle.Descriptor.SetTitle(BasisLocalization.Get("settings.bodyTracking.disableAnimFbt"));
disableAnimInFBTToggle.AssignBinding(BasisSettingsDefaults.DisableAnimationsInFBT);
disableAnimInFBTToggle.Descriptor.SetDescription(BasisLocalization.Get("settings.bodyTracking.disableAnimFbt.description"));
+
+ var scaleAffectsLocomotionToggle = PanelToggle.CreateNewEntry(trackingParent);
+ scaleAffectsLocomotionToggle.Descriptor.SetTitle(BasisLocalization.Get("settings.bodyTracking.scaleAffectsLocomotion"));
+ scaleAffectsLocomotionToggle.AssignBinding(BasisSettingsDefaults.ScaleAffectsLocomotionSpeed);
+ scaleAffectsLocomotionToggle.Descriptor.SetDescription(BasisLocalization.Get("settings.bodyTracking.scaleAffectsLocomotion.description"));
});
// ============== Body Collision ==============
@@ -659,6 +666,17 @@ public static PanelTabPage IKTab(PanelTabGroup tabGroup)
return tabPage;
}
+ public static void SetAvatarScaleSliderValueWithoutNotify(float value)
+ {
+ if (_avatarScaleSlider == null)
+ {
+ return;
+ }
+
+ _avatarScaleSlider.SetValueWithoutNotify(value);
+ }
+
+
// ------------------
// Debug Info
// ------------------
diff --git a/Basis/Packages/com.basis.framework/Drivers/Common/BasisHeightDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Common/BasisHeightDriver.cs
index 13d7c7e17f..4b948c0f64 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Common/BasisHeightDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Common/BasisHeightDriver.cs
@@ -1,3 +1,4 @@
+using Basis.BasisUI;
using Basis.Scripts.BasisSdk.Players;
using Basis.Scripts.Device_Management;
using Basis.Scripts.TransformBinders.BoneControl;
@@ -47,11 +48,24 @@ public static class BasisHeightDriver
public static float PlayerToDefaultRatioScaled = 1f;
public static float AvatarToDefaultRatioScaled = 1f;
+ public static bool HasRuntimeOscEyeHeightOverride = false;
+ public static float RuntimeOscEyeHeightMeters = FallbackHeightInMeters;
public static float DeviceScale = 1f;
public static void ApplyScaleAndHeight()
{
RevaluateUnscaledHeight(SMModuleCalibration.HeightMode);
+ if (HasRuntimeOscEyeHeightOverride)
+ {
+ if (SMModuleCalibration.ApplyCustomScale)
+ {
+ ApplyRuntimeOscEyeHeightOverride(RuntimeOscEyeHeightMeters);
+ return;
+ }
+
+ ClearRuntimeOscEyeHeightOverride();
+ }
+
ApplyScale(SMModuleCalibration.ApplyCustomScale, SMModuleCalibration.SelectedScale);
ChooseHeightToUse(SMModuleCalibration.HeightMode);
ScheduleHeightChangeCallback(HeightModeChange.OnApplyHeightAndScale);
@@ -101,6 +115,38 @@ public enum HeightModeChange
OnApplyHeightAndScale
}
+ public static bool ApplyRuntimeOscEyeHeightOverride(float eyeHeightMeters)
+ {
+ eyeHeightMeters = SanitizePositive(eyeHeightMeters, FallbackHeightInMeters);
+
+ float unscaledAvatarEyeHeight = SanitizePositive(AvatarEyeHeight, FallbackHeightInMeters);
+ float scaleFactor = eyeHeightMeters / unscaledAvatarEyeHeight;
+
+ HasRuntimeOscEyeHeightOverride = true;
+ RuntimeOscEyeHeightMeters = eyeHeightMeters;
+ SMModuleCalibration.SelectedScale = eyeHeightMeters;
+ BasisSettingsDefaults.SelectedScale.SetValueWithoutNotify(eyeHeightMeters);
+ SettingsProviderIK.SetAvatarScaleSliderValueWithoutNotify(eyeHeightMeters);
+ ScaledToMatchValue = scaleFactor;
+
+ ApplyAvatarScale(scaleFactor);
+ RefreshScaledHeightState(HeightModeChange.OnApplyHeightAndScale);
+ return true;
+ }
+
+ public static void ClearRuntimeOscEyeHeightOverride()
+ {
+ HasRuntimeOscEyeHeightOverride = false;
+ RuntimeOscEyeHeightMeters = FallbackHeightInMeters;
+ }
+
+ public static void RefreshScaledHeightState(HeightModeChange mode)
+ {
+ RevaluateUnscaledHeight(SMModuleCalibration.HeightMode);
+ ChooseHeightToUse(SMModuleCalibration.HeightMode);
+ ScheduleHeightChangeCallback(mode);
+ }
+
///
/// Applies a scale factor to the local avatar and updates cached bone offsets.
///
@@ -156,6 +202,8 @@ public static void CapturePlayerHeight()
public static void CaptureAvatarHeightDuringTpose()
{
+ ClearRuntimeOscEyeHeightOverride();
+
var player = BasisLocalPlayer.Instance;
if (player == null)
{
diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAnimatorDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAnimatorDriver.cs
index f70b209573..3bd9557ca4 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAnimatorDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalAnimatorDriver.cs
@@ -76,6 +76,10 @@ public class BasisLocalAnimatorDriver
/// Default: 30.
public float AngularDampingFactor = 30;
+ private const float MinimumLocomotionScale = 0.05f;
+ private const float MaximumLocomotionScale = 20f;
+ private const float LocomotionScaleSpeedExponent = 0.5f;
+
///
/// Last raw (pre-damped) velocity sample used for smoothing.
///
@@ -250,7 +254,9 @@ public void SimulateAnimator(float DeltaTime)
basisAnimatorVariableApply.BasisAnimatorVariables.Velocity = dampenedVelocity;
bool isMoving = dampenedVelocity.sqrMagnitude > StationaryVelocityThreshold;
basisAnimatorVariableApply.BasisAnimatorVariables.isMoving = isMoving;
- basisAnimatorVariableApply.BasisAnimatorVariables.AnimationsCurrentSpeed = 1;
+ basisAnimatorVariableApply.BasisAnimatorVariables.AnimationsCurrentSpeed = Basis.BasisUI.BasisSettingsDefaults.ScaleAffectsLocomotionSpeed.RawValue && isMoving
+ ? GetScaleAdjustedLocomotionSpeed()
+ : 1f;
if (HasHipsInput && isMoving == false)
{
@@ -291,6 +297,12 @@ public void SimulateAnimator(float DeltaTime)
previousHipsRotation = BasisLocalBoneDriver.HipsControl.OutgoingWorldData.rotation;
}
+ private static float GetScaleAdjustedLocomotionSpeed()
+ {
+ float avatarScale = Mathf.Clamp(BasisHeightDriver.ScaledToMatchValue, MinimumLocomotionScale, MaximumLocomotionScale);
+ return Mathf.Exp(-Mathf.Log(avatarScale) * LocomotionScaleSpeedExponent);
+ }
+
///
/// Event handler that sets the animator's jump state immediately after a jump begins.
///
diff --git a/Basis/Packages/com.basis.framework/Drivers/Remote/BasisRemoteBoneDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Remote/BasisRemoteBoneDriver.cs
index 261effb978..73384d5e06 100644
--- a/Basis/Packages/com.basis.framework/Drivers/Remote/BasisRemoteBoneDriver.cs
+++ b/Basis/Packages/com.basis.framework/Drivers/Remote/BasisRemoteBoneDriver.cs
@@ -2,6 +2,7 @@
using Basis.Scripts.Common;
using Basis.Scripts.Drivers;
using Basis.Scripts.Networking.NetworkedAvatar;
+using Basis.Scripts.UI.NamePlate;
using System;
using System.Collections.Generic;
using Unity.Burst;
@@ -106,6 +107,8 @@ public struct RemoteFrameOutput
/// Vertical delta between hips and mouth in scaled TPose space (used for UI placement).
///
public float HeightAvatarHipCoord;
+ /// Per-avatar visual nameplate scale multiplier derived from current avatar height.
+ public float NamePlateScaleMultiplier;
}
///
@@ -117,6 +120,13 @@ public struct RemoteFrameOutput
[BurstCompile(FloatMode = FloatMode.Fast, FloatPrecision = FloatPrecision.Low)]
public struct BasisRemoteBoneJob : IJobParallelFor
{
+ private const float NamePlateMinimumHeightOffset = 0.4f;
+ private const float NamePlateShrinkStartHeight = 0.5f;
+ private const float NamePlateMinimumScaleMultiplier = 0.5f;
+ private const float NamePlateGrowStartHeight = 2f;
+ // Cap tall-avatar growth so the nameplate stays readable without becoming unreasonably large.
+ private const float MaxNamePlateScaleMultiplier = 3f;
+
/// Authoring-time TPose and offset data (unscaled).
[ReadOnly] public NativeArray Authoring;
@@ -184,6 +194,8 @@ public void Execute(int i)
float3 difference = SafeDivide(nowScale, a.TposeScale);
+ float rawNamePlateHeight = difference.y * 1.2f;
+
Out[i] = new RemoteFrameOutput
{
pos_Head = headP,
@@ -202,11 +214,30 @@ public void Execute(int i)
rot_Mouth = headR,
- // Used for vertical offsetting of the nameplate UI
- HeightAvatarHipCoord = difference.y * 1.2f,
+ // Keep tiny avatars readable without changing developer-branch behavior for avatars at or above 2m.
+ HeightAvatarHipCoord = rawNamePlateHeight < NamePlateGrowStartHeight ? math.max(rawNamePlateHeight, NamePlateMinimumHeightOffset) : rawNamePlateHeight,
+ NamePlateScaleMultiplier = ComputeNamePlateScaleMultiplier(rawNamePlateHeight),
};
MouthPositions[i] = mouthP;
}
+
+ private static float ComputeNamePlateScaleMultiplier(float namePlateHeight)
+ {
+ if (namePlateHeight < NamePlateShrinkStartHeight)
+ {
+ float t = math.saturate(namePlateHeight / NamePlateShrinkStartHeight);
+ return math.lerp(NamePlateMinimumScaleMultiplier, 1f, t);
+ }
+
+ if (namePlateHeight > NamePlateGrowStartHeight)
+ {
+ // Preserve linear growth above the tall-avatar threshold, but stop before giant avatars overwhelm the UI.
+ return math.min(1f + (namePlateHeight - NamePlateGrowStartHeight), MaxNamePlateScaleMultiplier);
+ }
+
+ return 1f;
+ }
+
private readonly float3 SafeDivide(float3 numerator, float3 denominator)
{
const float eps = 1e-6f;
@@ -309,6 +340,8 @@ public struct MappedNameplateApplyJob : IJobParallelForTransform
{
/// Camera world position used to bill-board the plate (yaw-only).
public float3 CameraPosition;
+ /// User-configured base local scale before per-avatar height compensation.
+ public float NamePlateBaseScale;
/// Input pose data (per-avatar) for nameplate placement.
[ReadOnly] public NativeArray NamePlateIn;
@@ -319,7 +352,6 @@ public void Execute(int jobIndex, TransformAccess tx)
var data = NamePlateIn[jobIndex];
float3 hips = data.pos_Hips;
- // y = hips.y + diff * 1.8
float3 nameplatePos = new float3(hips.x, hips.y + data.HeightAvatarHipCoord, hips.z);
// Face the camera (yaw only) with zero-distance guard.
@@ -329,6 +361,8 @@ public void Execute(int jobIndex, TransformAccess tx)
quaternion rot = quaternion.RotateY(yaw);
tx.SetPositionAndRotation(nameplatePos, rot);
+ float localScale = NamePlateBaseScale * data.NamePlateScaleMultiplier;
+ tx.localScale = new Vector3(localScale, localScale, localScale);
}
}
@@ -1229,6 +1263,7 @@ public static JobHandle Schedule(int maxBatchSize = 64)
var nameplateJob = new MappedNameplateApplyJob
{
CameraPosition = CameraPosition,
+ NamePlateBaseScale = BasisRemoteNamePlateDriver.BaseNamePlateLocalScale * BasisRemoteNamePlateDriver.NamePlateSize,
NamePlateIn = sOut.AsDeferredJobArray(),
}.Schedule(sNamePlate, simAndScale);
diff --git a/Basis/Packages/com.basis.framework/Networking/Compression/BasisUnityBitPackerExtensions.cs b/Basis/Packages/com.basis.framework/Networking/Compression/BasisUnityBitPackerExtensions.cs
index e41f50760d..a8edca992f 100644
--- a/Basis/Packages/com.basis.framework/Networking/Compression/BasisUnityBitPackerExtensions.cs
+++ b/Basis/Packages/com.basis.framework/Networking/Compression/BasisUnityBitPackerExtensions.cs
@@ -160,11 +160,8 @@ public static void WritePosition(UnityEngine.Vector3 position, ref byte[] buffer
}
public static void CompressScale(float scale, ref LocalAvatarSyncMessage message, ref int offset)
{
-
- float clamped = math.clamp(scale, BasisAvatarBitPacking.MinScale, BasisAvatarBitPacking.MaxScale);
- float normalized = (clamped - BasisAvatarBitPacking.MinScale) / BasisAvatarBitPacking.ComputedRange;
-
- ushort compressed = (ushort)(normalized * BasisAvatarBitPacking.UShortRangeDifference);
+ // Scale needs wide dynamic range and small-avatar precision; Posit16 gives better relative precision than the old linear ushort.
+ ushort compressed = EncodePositiveScalePosit16(scale);
WriteUShort(compressed, ref message.array, ref offset);
}
///
@@ -176,8 +173,140 @@ public static void CompressScale(float scale, ref LocalAvatarSyncMessage message
///
public static float DecompressScale(ushort value)
{
- float normalized = (float)value / BasisAvatarBitPacking.UShortRangeDifference;
- return normalized * (BasisAvatarBitPacking.MaxScale - BasisAvatarBitPacking.MinScale) + BasisAvatarBitPacking.MinScale;
+ return DecodePositiveScalePosit16(value);
+ }
+
+ private const int PositScaleBits = 16;
+ private const int PositScaleExponentBits = 1;
+ private const int PositScaleUseedShift = 1 << PositScaleExponentBits;
+ private static readonly float MaxRepresentablePositiveScale = DecodePositiveScalePosit16(0x7FFF);
+
+ private static ushort EncodePositiveScalePosit16(float scale)
+ {
+ if (!math.isfinite(scale) || scale <= 0f)
+ {
+ return 0;
+ }
+
+ scale = math.min(scale, MaxRepresentablePositiveScale);
+
+ const int remainingBits = PositScaleBits - 1;
+ int k = (int)math.floor(math.log2(scale) / PositScaleUseedShift);
+ float useedPower = math.exp2(k * PositScaleUseedShift);
+ float normalized = scale / useedPower;
+
+ int exponent = 0;
+ if (normalized >= 2f)
+ {
+ exponent = 1;
+ normalized *= 0.5f;
+ }
+
+ float fraction = math.clamp(normalized - 1f, 0f, 1f);
+ uint payload = 0;
+ int bit = remainingBits - 1;
+
+ if (k >= 0)
+ {
+ int ones = k + 1;
+ for (int i = 0; i < ones && bit >= 0; i++)
+ {
+ payload |= 1u << bit--;
+ }
+
+ if (bit >= 0)
+ {
+ bit--;
+ }
+ }
+ else
+ {
+ int zeros = -k;
+ bit -= zeros;
+ if (bit >= 0)
+ {
+ payload |= 1u << bit--;
+ }
+ }
+
+ if (bit >= 0)
+ {
+ if (exponent != 0)
+ {
+ payload |= 1u << bit;
+ }
+
+ bit--;
+ }
+
+ while (bit >= 0)
+ {
+ fraction *= 2f;
+ if (fraction >= 1f)
+ {
+ payload |= 1u << bit;
+ fraction -= 1f;
+ }
+
+ bit--;
+ }
+
+ ushort truncated = (ushort)payload;
+ ushort roundedUp = truncated < 0x7FFF ? (ushort)(truncated + 1) : truncated;
+ float lower = DecodePositiveScalePosit16(truncated);
+ float upper = DecodePositiveScalePosit16(roundedUp);
+ return math.abs(upper - scale) < math.abs(scale - lower) ? roundedUp : truncated;
+ }
+
+ private static float DecodePositiveScalePosit16(ushort value)
+ {
+ if (value == 0)
+ {
+ return 0f;
+ }
+
+ if ((value & 0x8000) != 0)
+ {
+ value = (ushort)(~value + 1);
+ }
+
+ uint raw = value;
+ int bit = PositScaleBits - 2;
+ bool regimeSign = ((raw >> bit) & 1u) != 0;
+ int runLength = 0;
+ while (bit >= 0 && (((raw >> bit) & 1u) != 0) == regimeSign)
+ {
+ runLength++;
+ bit--;
+ }
+
+ if (bit >= 0)
+ {
+ bit--;
+ }
+
+ int k = regimeSign ? runLength - 1 : -runLength;
+ int exponent = 0;
+ if (bit >= 0)
+ {
+ exponent = (int)((raw >> bit) & 1u);
+ bit--;
+ }
+
+ float fraction = 1f;
+ float fractionBit = 0.5f;
+ while (bit >= 0)
+ {
+ if (((raw >> bit) & 1u) != 0)
+ {
+ fraction += fractionBit;
+ }
+
+ fractionBit *= 0.5f;
+ bit--;
+ }
+
+ return math.exp2((k * PositScaleUseedShift) + exponent) * fraction;
}
///
diff --git a/Basis/Packages/com.basis.framework/Settings/SMModuleCalibration.cs b/Basis/Packages/com.basis.framework/Settings/SMModuleCalibration.cs
index 81eb0f00b7..f38e0f85d3 100644
--- a/Basis/Packages/com.basis.framework/Settings/SMModuleCalibration.cs
+++ b/Basis/Packages/com.basis.framework/Settings/SMModuleCalibration.cs
@@ -268,6 +268,7 @@ public override void ValidSettingsChange(string matchedSettingName, string optio
{
if (!Mathf.Approximately(old, parsed))
{
+ BasisHeightDriver.ClearRuntimeOscEyeHeightOverride();
SelectedScale = parsed;
_dirty = true;
}
diff --git a/Basis/Packages/com.basis.framework/UI/BasisPointRaycaster.cs b/Basis/Packages/com.basis.framework/UI/BasisPointRaycaster.cs
index 7cdf7e6deb..82cc7448c3 100644
--- a/Basis/Packages/com.basis.framework/UI/BasisPointRaycaster.cs
+++ b/Basis/Packages/com.basis.framework/UI/BasisPointRaycaster.cs
@@ -1,4 +1,5 @@
using Basis.Scripts.BasisSdk.Interactions;
+using Basis.Scripts.BasisSdk.Players;
using Basis.Scripts.Device_Management.Devices;
using Basis.Scripts.Drivers;
using System.Collections.Generic;
@@ -10,7 +11,9 @@ namespace Basis.Scripts.UI
{
public class BasisPointRaycaster : BaseRaycaster
{
+ private const float MaxDistanceScaleThresholdMeters = 100f;
public float MaxDistance = 120;
+ public float EffectiveMaxDistance = 120;
public bool UseWorldPosition = true;
///
@@ -122,11 +125,30 @@ public void Initialize(BasisInput basisInput)
BasisInput = basisInput;
PhysicHits = new RaycastHit[BasisPlayerInteract.k_MaxPhysicHitCount];
PhysicBackcastHits = new RaycastHit[4]; // We don't need as many backcast hits.
+ RefreshEffectiveMaxDistance();
+ BasisLocalPlayer.OnPlayersHeightChangedNextFrame -= OnPlayersHeightChangedNextFrame;
+ BasisLocalPlayer.OnPlayersHeightChangedNextFrame += OnPlayersHeightChangedNextFrame;
// Create the ray with the adjusted starting position and direction
UpdateRay();
}
+ protected override void OnDestroy()
+ {
+ base.OnDestroy();
+ BasisLocalPlayer.OnPlayersHeightChangedNextFrame -= OnPlayersHeightChangedNextFrame;
+ }
+
+ private void OnPlayersHeightChangedNextFrame(BasisHeightDriver.HeightModeChange _)
+ {
+ RefreshEffectiveMaxDistance();
+ }
+
+ private void RefreshEffectiveMaxDistance()
+ {
+ EffectiveMaxDistance = MaxDistance + Mathf.Max(0f, BasisHeightDriver.SelectedScaledAvatarHeight - MaxDistanceScaleThresholdMeters);
+ }
+
public void UpdateRay()
{
ray = GetUpdatedRay();
@@ -153,7 +175,7 @@ public void UpdateRaycast()
PhysicHitCount = Physics.RaycastNonAlloc(
ray,
PhysicHits,
- MaxDistance,
+ EffectiveMaxDistance,
BasisPlayerInteract.Mask,
BasisPlayerInteract.TriggerInteraction);
@@ -243,7 +265,7 @@ private void UpdateClosestHitPreferOverlayUI()
private void DoBackcastFixup()
{
- float backcastDistance = ClosestRayCastHit.distance > 0 ? ClosestRayCastHit.distance : MaxDistance;
+ float backcastDistance = ClosestRayCastHit.distance > 0 ? ClosestRayCastHit.distance : EffectiveMaxDistance;
Ray backcastRay = new Ray(ray.origin + ray.direction * backcastDistance, -ray.direction);
int backcastHitCount = Physics.RaycastNonAlloc(
@@ -469,7 +491,7 @@ private void OnDrawGizmosSelected()
return;
Gizmos.color = Color.cyan;
- Gizmos.DrawLine(ray.origin, ray.origin + ray.direction * MaxDistance);
+ Gizmos.DrawLine(ray.origin, ray.origin + ray.direction * EffectiveMaxDistance);
if (_mode == ControlMode.Placement && CurrentPlacement.HasHit)
{
diff --git a/Basis/Packages/com.basis.framework/UI/BasisUIRaycast.cs b/Basis/Packages/com.basis.framework/UI/BasisUIRaycast.cs
index 7f62091998..355e693f6d 100644
--- a/Basis/Packages/com.basis.framework/UI/BasisUIRaycast.cs
+++ b/Basis/Packages/com.basis.framework/UI/BasisUIRaycast.cs
@@ -544,7 +544,7 @@ public bool ProcessSortedHitsResults(Canvas canvas, bool hitSomething, List 0;
}
- validHit &= hitData.distance < BasisPointRaycaster.MaxDistance;
+ validHit &= hitData.distance < BasisPointRaycaster.EffectiveMaxDistance;
if (validHit)
{
@@ -612,7 +612,7 @@ public void SortedRaycastGraphics(Canvas canvas, Camera eventCamera, ref List Muscles(bitstream, varies by quality) -> Scale (2) -> Rotation (16)
+ // Position (12) -> bone rotations (bitstream, varies by quality) -> Posit16 scale (2) -> rotation (7) -> hips tail
public byte DataQualityLevel; // 0=Low, 1=Medium, 2=High
public byte[] array; // payload bytes (length must match ConvertToSize(quality))
diff --git a/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/Components/Acquisition/OSCAcquisition.cs b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/Components/Acquisition/OSCAcquisition.cs
index ff425e6d9b..bb29afa0c0 100644
--- a/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/Components/Acquisition/OSCAcquisition.cs
+++ b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/Components/Acquisition/OSCAcquisition.cs
@@ -34,6 +34,7 @@ internal void OnAvatarReady(bool isWearer)
if (_alreadyInitialized) return;
+ BasisOscService.EnsureInitialized();
_acquisitionServer = OSCAcquisitionServer.SceneInstance;
_acquisitionServer.SendWakeUpMessage(FakeWakeUpMessage);
BasisOscService.RegisterAddressReceiver(_oscOwnerId, OnOscAddressUpdated);
diff --git a/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscAvatarScaling.cs b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscAvatarScaling.cs
new file mode 100644
index 0000000000..9ac9c34dc6
--- /dev/null
+++ b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscAvatarScaling.cs
@@ -0,0 +1,157 @@
+using Basis.Scripts.BasisSdk.Players;
+using HVR.Basis.Comms.OSC;
+using UnityEngine;
+
+namespace HVR.Basis.Comms
+{
+ public static class BasisOscAvatarScaling
+ {
+ private static bool _initialized;
+
+ public const string EyeHeightAddress = "/avatar/eyeheight";
+ public const string EyeHeightMinAddress = "/avatar/eyeheightmin";
+ public const string EyeHeightMaxAddress = "/avatar/eyeheightmax";
+ public const string EyeHeightScalingAllowedAddress = "/avatar/eyeheightscalingallowed";
+
+ // OSC accepts extreme float inputs; OscMaximumEyeHeightMeters is a sanity ceiling, separate from supported/user-selectable limits.
+ public const float OscMaximumEyeHeightMeters = 10000f;
+ public const float SupportedMinimumEyeHeightMeters = 0.1f;
+ public const float SupportedMaximumEyeHeightMeters = 100f;
+ public const float UserSelectableMinimumEyeHeightMeters = 0.1f;
+ public const float UserSelectableMaximumEyeHeightMeters = 5f;
+
+ public static bool EyeHeightScalingAllowed => SMModuleCalibration.ApplyCustomScale;
+
+ public static void EnsureInitialized()
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ _initialized = true;
+ BasisOscService.MessageReceived -= OnOscMessageReceived;
+ BasisOscService.MessageReceived += OnOscMessageReceived;
+ BasisLocalPlayer.OnLocalAvatarChanged -= OnLocalAvatarChanged;
+ BasisLocalPlayer.OnLocalAvatarChanged += OnLocalAvatarChanged;
+ OnLocalAvatarChanged();
+ }
+
+ public static bool IsAvatarScalingAddress(string path)
+ {
+ return path == EyeHeightAddress ||
+ path == EyeHeightMinAddress ||
+ path == EyeHeightMaxAddress ||
+ path == EyeHeightScalingAllowedAddress;
+ }
+
+ public static bool TryHandleWrite(OscMessage message)
+ {
+ if (message == null || !IsAvatarScalingAddress(message.Path))
+ {
+ return false;
+ }
+
+ if (message.Path != EyeHeightAddress)
+ {
+ return true;
+ }
+
+ OscData[] arguments = message.Arguments;
+ if (arguments == null || arguments.Length == 0 || arguments[0].Kind != OscDataKind.Float32)
+ {
+ return true;
+ }
+
+ ApplyEyeHeight(arguments[0].FloatValue);
+ return true;
+ }
+
+ public static void ClearRuntimeOverride()
+ {
+ BasisHeightDriver.ClearRuntimeOscEyeHeightOverride();
+ }
+
+ public static void ApplyEyeHeight(float requestedEyeHeightMeters)
+ {
+ if (!EyeHeightScalingAllowed)
+ {
+ BasisDebug.LogWarning("Ignoring OSC avatar eye height because Custom Scale is disabled.", BasisDebug.LogTag.LocalNetwork);
+ PublishCurrentState();
+ return;
+ }
+
+ if (float.IsNaN(requestedEyeHeightMeters) || float.IsInfinity(requestedEyeHeightMeters) || requestedEyeHeightMeters <= 0f)
+ {
+ BasisDebug.LogWarning($"Ignoring invalid OSC avatar eye height {requestedEyeHeightMeters}.", BasisDebug.LogTag.LocalNetwork);
+ PublishCurrentState();
+ return;
+ }
+ BasisLocalPlayer localPlayer = BasisLocalPlayer.Instance;
+ if (localPlayer == null || localPlayer.LocalAvatarDriver == null || localPlayer.LocalBoneDriver == null)
+ {
+ BasisDebug.LogWarning("Ignoring OSC avatar eye height because the local avatar is not ready.", BasisDebug.LogTag.LocalNetwork);
+ return;
+ }
+
+ float eyeHeightMeters = Mathf.Min(requestedEyeHeightMeters, OscMaximumEyeHeightMeters);
+ if (!Mathf.Approximately(eyeHeightMeters, requestedEyeHeightMeters))
+ {
+ BasisDebug.LogWarning($"Clamped OSC avatar eye height from {requestedEyeHeightMeters}m to {eyeHeightMeters}m.", BasisDebug.LogTag.LocalNetwork);
+ }
+
+ if (eyeHeightMeters < SupportedMinimumEyeHeightMeters || eyeHeightMeters > SupportedMaximumEyeHeightMeters)
+ {
+ BasisDebug.LogWarning(
+ $"OSC avatar eye height {eyeHeightMeters}m is outside the supported {SupportedMinimumEyeHeightMeters}m..{SupportedMaximumEyeHeightMeters}m range.",
+ BasisDebug.LogTag.LocalNetwork);
+ }
+
+ BasisHeightDriver.ApplyRuntimeOscEyeHeightOverride(eyeHeightMeters);
+ PublishCurrentState();
+ }
+
+ public static void PublishCurrentState()
+ {
+ BasisOscService.PublishValue(EyeHeightAddress, OscData.Float32(GetCurrentEyeHeightMeters()));
+ BasisOscService.PublishValue(EyeHeightMinAddress, OscData.Float32(UserSelectableMinimumEyeHeightMeters));
+ BasisOscService.PublishValue(EyeHeightMaxAddress, OscData.Float32(UserSelectableMaximumEyeHeightMeters));
+ BasisOscService.PublishValue(EyeHeightScalingAllowedAddress, OscData.Boolean(EyeHeightScalingAllowed));
+ }
+
+ private static float GetCurrentEyeHeightMeters()
+ {
+ float selected = BasisHeightDriver.SelectedScaledAvatarHeight;
+ if (!float.IsNaN(selected) && !float.IsInfinity(selected) && selected > 0f)
+ {
+ return selected;
+ }
+
+ float fallback = BasisHeightDriver.AvatarEyeHeight * BasisHeightDriver.AppliedUpScale;
+ if (!float.IsNaN(fallback) && !float.IsInfinity(fallback) && fallback > 0f)
+ {
+ return fallback;
+ }
+
+ return BasisHeightDriver.FallbackHeightInMeters;
+ }
+
+ private static void OnLocalAvatarChanged()
+ {
+ ClearRuntimeOverride();
+ BasisLocalPlayer.OnPlayersHeightChangedNextFrame -= OnPlayerHeightChanged;
+ BasisLocalPlayer.OnPlayersHeightChangedNextFrame += OnPlayerHeightChanged;
+ PublishCurrentState();
+ }
+
+ private static void OnPlayerHeightChanged(BasisHeightDriver.HeightModeChange mode)
+ {
+ PublishCurrentState();
+ }
+
+ private static void OnOscMessageReceived(OscMessage message)
+ {
+ TryHandleWrite(message);
+ }
+ }
+}
diff --git a/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscAvatarScaling.cs.meta b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscAvatarScaling.cs.meta
new file mode 100644
index 0000000000..d6ee4f8517
--- /dev/null
+++ b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscAvatarScaling.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b41d2e05ce3e47c9b939c6e0c58600b3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscService.cs b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscService.cs
index 0c3755208b..97275540a2 100644
--- a/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscService.cs
+++ b/Basis/Packages/dev.hai-vr.basis.comms/Scripts/Systems/Runtime/OSC/BasisOscService.cs
@@ -83,6 +83,7 @@ public DispatcherState(
public static void EnsureInitialized()
{
_ = OSCAcquisitionServer.SceneInstance;
+ BasisOscAvatarScaling.EnsureInitialized();
}
public static void RegisterReceiver(EntityId ownerId, Action handler)
@@ -653,6 +654,13 @@ private static bool TryResolveAddressValue(SimpleOSC.OSCMessage message, out int
addressId = 0;
value = 0f;
+ // Avatar scaling uses a dedicated OSC message handler and must not be remapped into
+ // the generic float address-id pipeline used for avatar parameters/acquisition.
+ if (BasisOscAvatarScaling.IsAvatarScalingAddress(message.path))
+ {
+ return false;
+ }
+
object[] arguments = message.arguments;
if (arguments == null || arguments.Length == 0 || !(arguments[0] is float floatValue))
{
diff --git a/Basis/Packages/dev.hai-vr.basis.comms/Tests/Runtime/OscBridgeTests.cs b/Basis/Packages/dev.hai-vr.basis.comms/Tests/Runtime/OscBridgeTests.cs
index 864291d916..88842a1457 100644
--- a/Basis/Packages/dev.hai-vr.basis.comms/Tests/Runtime/OscBridgeTests.cs
+++ b/Basis/Packages/dev.hai-vr.basis.comms/Tests/Runtime/OscBridgeTests.cs
@@ -7,6 +7,7 @@
using Basis.Network.Core;
using Basis.Shims;
using Basis.Scripts.BasisSdk;
+using Basis.Scripts.Networking.Compression;
using Cilbox;
using HVR.Basis.Comms;
using HVR.Basis.Comms.OSC;
@@ -633,6 +634,117 @@ public void BasisOsc_LocalAvatarPublishValue_SubmitsFloatIntoVixxyVariableStore(
}
}
+ [Test]
+ public void BasisOscAvatarScaling_PublishesVrChatScalingEndpoints()
+ {
+ DestroySceneInstance();
+ bool previousApplyCustomScale = SMModuleCalibration.ApplyCustomScale;
+ SMModuleCalibration.ApplyCustomScale = true;
+
+ try
+ {
+ BasisOscAvatarScaling.PublishCurrentState();
+
+ object root = GetQueryRoot();
+ object eyeHeight = ResolveNode(root, "avatar", "eyeheight");
+ object eyeHeightMin = ResolveNode(root, "avatar", "eyeheightmin");
+ object eyeHeightMax = ResolveNode(root, "avatar", "eyeheightmax");
+ object scalingAllowed = ResolveNode(root, "avatar", "eyeheightscalingallowed");
+
+ Assert.That(eyeHeight, Is.Not.Null);
+ Assert.That(eyeHeightMin, Is.Not.Null);
+ Assert.That(eyeHeightMax, Is.Not.Null);
+ Assert.That(scalingAllowed, Is.Not.Null);
+
+ Assert.That((string)eyeHeight.GetType().GetField("FULL_PATH").GetValue(eyeHeight), Is.EqualTo("/avatar/eyeheight"));
+ Assert.That((string)eyeHeight.GetType().GetField("TYPE").GetValue(eyeHeight), Is.EqualTo(",f"));
+ Assert.That((string)eyeHeightMin.GetType().GetField("TYPE").GetValue(eyeHeightMin), Is.EqualTo(",f"));
+ Assert.That((string)eyeHeightMax.GetType().GetField("TYPE").GetValue(eyeHeightMax), Is.EqualTo(",f"));
+ Assert.That((string)scalingAllowed.GetType().GetField("TYPE").GetValue(scalingAllowed), Is.EqualTo(",T"));
+
+ IList minValues = (IList)eyeHeightMin.GetType().GetField("VALUE").GetValue(eyeHeightMin);
+ IList maxValues = (IList)eyeHeightMax.GetType().GetField("VALUE").GetValue(eyeHeightMax);
+ IList allowedValues = (IList)scalingAllowed.GetType().GetField("VALUE").GetValue(scalingAllowed);
+
+ Assert.That(minValues[0], Is.EqualTo(BasisOscAvatarScaling.UserSelectableMinimumEyeHeightMeters));
+ Assert.That(maxValues[0], Is.EqualTo(BasisOscAvatarScaling.UserSelectableMaximumEyeHeightMeters));
+ Assert.That(allowedValues[0], Is.EqualTo(true));
+ }
+ finally
+ {
+ SMModuleCalibration.ApplyCustomScale = previousApplyCustomScale;
+ DestroySceneInstance();
+ }
+ }
+
+ [Test]
+ public void BasisOscService_AvatarScalingAddress_UsesMessageReceiverOnly()
+ {
+ MethodInfo submitRawMessages = typeof(BasisOscService).GetMethod("SubmitRawMessages", BindingFlags.NonPublic | BindingFlags.Static);
+ Assert.That(submitRawMessages, Is.Not.Null);
+
+ GameObject owner = new GameObject("AvatarScalingMessageReceiverOwner");
+ EntityId ownerId = owner.GetEntityId();
+ string receivedPath = null;
+ bool addressReceiverCalled = false;
+
+ try
+ {
+ BasisOscService.RegisterReceiver(ownerId, message =>
+ {
+ receivedPath = message.Path;
+ });
+ BasisOscService.RegisterAddressReceiver(ownerId, (address, value) => addressReceiverCalled = true);
+ BasisOscService.UpdateSubscriptions(ownerId, true, Array.Empty(), Array.Empty());
+
+ submitRawMessages.Invoke(null, new object[]
+ {
+ new List
+ {
+ new SimpleOSC.OSCMessage
+ {
+ path = BasisOscAvatarScaling.EyeHeightAddress,
+ arguments = new object[] { 2f }
+ }
+ }
+ });
+
+ Assert.That(receivedPath, Is.EqualTo(BasisOscAvatarScaling.EyeHeightAddress));
+ Assert.That(addressReceiverCalled, Is.False);
+ }
+ finally
+ {
+ BasisOscService.UnregisterReceiver(ownerId);
+ BasisOscService.UnregisterAddressReceiver(ownerId);
+ BasisOscService.ClearSubscriptions(ownerId);
+ Object.DestroyImmediate(owner);
+ DestroySceneInstance();
+ }
+ }
+
+ [Test]
+ public void AvatarScalePosit16_RoundTripsTinyAndDefaultScales()
+ {
+ byte[] payload = new byte[2];
+ SerializableBasis.LocalAvatarSyncMessage message = new SerializableBasis.LocalAvatarSyncMessage(payload);
+
+ AssertScaleRoundTrip(message, payload, 1f, 1e-6f);
+ AssertScaleRoundTrip(message, payload, 0.005f, 0.00001f);
+ AssertScaleRoundTrip(message, payload, 0.0005f, 0.000001f);
+ }
+
+ private static void AssertScaleRoundTrip(SerializableBasis.LocalAvatarSyncMessage message, byte[] payload, float scale, float tolerance)
+ {
+ Array.Clear(payload, 0, payload.Length);
+ int offset = 0;
+ BasisUnityBitPackerExtensionsUnsafe.CompressScale(scale, ref message, ref offset);
+
+ offset = 0;
+ Assert.That(BasisUnityBitPackerExtensionsUnsafe.TryReadUShort(ref payload, ref offset, out ushort encoded), Is.True);
+ float decoded = BasisUnityBitPackerExtensionsUnsafe.DecompressScale(encoded);
+ Assert.That(decoded, Is.EqualTo(scale).Within(tolerance));
+ }
+
[Test]
public void BasisOsc_PublishValue_ReturnsResolvedRelativeAddress()
{