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() {