Отслеживание тела: данные, аватар и трекеры ролей
Проверенный reader суставов, требования к поставщику и реальные ограничения модификатора.
Есть два разных сценария: готовый скелет от поставщика body tracking и отдельные трекеры на поясе/ногах. XRBodyTracker хранит суставы скелета. Трекеры ролей дают отдельные цели для вашей логики IK; сами по себе они не собираются в XRBodyTracker и не решают анимацию всего тела.
Сначала проверьте поставщика
Подготовьте запуск XR. Получите XRServer.GetTracker("/user/body_tracker") as XRBodyTracker. Если результат null, согласуйте с поставщиком поддержку, имя tracker и жизненный цикл сеанса. Создание new XRBodyTracker не включает tracking и не подключает устройство. Наличие классов SDK не подтверждает, что встроенный OpenXR-модуль публикует полный скелет на вашей платформе.
Проверяйте три уровня:
| Проверка | Что означает |
|---|---|
| Tracker найден | Объект зарегистрирован; это ещё не данные текущего кадра |
| HasTrackingData | Поставщик сообщает о данных тела в целом |
| GetJointFlags(joint) | Валидность позиции/ориентации именно этого сустава |
BodyFlags сообщает поддерживаемые области: UpperBodySupported, LowerBodySupported, HandsSupported. Это не замена флагам суставов. Устройство может передавать только верх тела или оценивать положение части суставов. VALID и TRACKED различаются; выбирайте, допускает ли ваша механика оценённые положения.
Подтверждён дефект конструктора: начальные JointFlags не инициализированы. В проверенном SDK есть 87 суставов; Joint.Max — граница, не сустав. Игровой код не должен включать HasTrackingData у пустого контейнера. Собственный поставщик должен сначала заполнить флаги и трансформы нужных суставов. При потере данных старые значения сохраняются и не становятся текущими только потому, что их можно прочитать.
Прочитайте сустав относительно Root
Сохраните BodyJointReader.cs. Пример принимает трансформы Root и выбранного сустава в одном пространстве поставщика, проверяет данные и возвращает копию относительно Root. Для Hips передайте Joint.Hips. Это не локальная pose кости относительно её непосредственного родителя и не мировой трансформ.
using Reckfell;
public static class BodyJointReader
{
// Provider transforms must share one space. Result is relative to Root, not a bone parent.
public static bool TryRelativeToRoot(XRBodyTracker tracker, XRBodyTracker.Joint joint,
out Transform3D relative)
{
relative = Transform3D.Identity;
if (tracker is null || !tracker.HasTrackingData
|| (long)joint < 0 || (long)joint >= (long)XRBodyTracker.Joint.Max) return false;
var root = XRBodyTracker.Joint.Root;
const XRBodyTracker.JointFlags needed =
XRBodyTracker.JointFlags.PositionValid | XRBodyTracker.JointFlags.OrientationValid;
if ((tracker.GetJointFlags(root) & needed) != needed
|| (tracker.GetJointFlags(joint) & needed) != needed) return false;
Transform3D rootTransform = tracker.GetJointTransform(root);
Transform3D target = tracker.GetJointTransform(joint);
float determinant = rootTransform.Basis.Determinant();
if (!rootTransform.IsFinite() || !target.IsFinite()
|| !float.IsFinite(determinant) || Mathf.Abs(determinant) < 0.000001f) return false;
Transform3D result = rootTransform.AffineInverse() * target;
if (!result.IsFinite()) return false;
relative = result;
return true;
}
}Пример требует PositionValid и OrientationValid, допускает оценённые положения без TRACKED и не оценивает точность. Вырожденный Root, NaN/Infinity и недопустимый индекс возвращают false без обращения к несуществующему суставу. BodyFlags намеренно проверяется отдельно: метаданные возможностей не подменяют пригодность конкретных данных. Для событий взаимодействия дополнительно нужен фокус приложения.
Подключите аватар
Нужны корректный скелет модели, skin weights и соглашения пространства от поставщика:
XROrigin3D
└── AvatarRoot (Entity3D)
└── Skeleton3D
└── XRBodyModifier3DУ XRBodyModifier3D задайте BodyTracker=/user/body_tracker до входа в дерево. Модификатор сопоставляет имена костей, например Root, Hips, Spine, Chest, LeftUpperArm, RightFoot. Несовпадающие кости пропускаются с предупреждением; он не переименовывает их и не исправляет веса модели.
BodyUpdate выбирает области сопоставления: UpperBody, LowerBody, Hands; по умолчанию включены все три. Root и дополнительные суставы не обязательно исключаются нулевой маской, поэтому для полного отключения используйте унаследованное Active=false. BodyTracker переключает источник без пересборки карты костей. Сопоставление зависит от Skeleton и BodyUpdate, поэтому смена одного имени не требует его обновления. Области меняйте через BodyUpdate.
Full меняет позиции и вращения. RotationOnly сохраняет позиции большинства костей, но Hips всё равно получает позиционное обновление — для приседания и подъёма. Нужны валидные Root и используемые родительские суставы. MotionScale скелета применяется к позициям данных; согласуйте его с моделью и WorldScale.
Размещение AvatarRoot в игровом мире — отдельная задача. Не считайте, что XRBodyModifier3D автоматически переносит модель к физическому Root или что XRBodyTracker всегда публикует positional pose default для XREntity3D. Поставщик должен явно определить оба соглашения. Этот раздел не подтверждает готовый ретаргетинг вашей модели.
Посмотрите результат модификации
Skeleton3D временно применяет модификаторы, затем восстанавливает базовую pose. Для диагностики читайте результат внутри ModificationProcessed. Сохраните BonePoseProbe.cs, прикрепите к обычному Entity и задайте Skeleton, Modifier и существующий индекс Bone. LastModified — копия результата события, не доказательство текущего tracking.
using Reckfell;
public partial class BonePoseProbe : Entity
{
[Export] public Skeleton3D Skeleton { get; set; } = null!;
[Export] public SkeletonModifier3D Modifier { get; set; } = null!;
[Export] public int Bone { get; set; }
public Transform3D LastModified { get; private set; } = Transform3D.Identity;
public override void _Ready()
{
if (Skeleton is null || Modifier is null || Modifier.GetSkeleton() != Skeleton
|| Bone < 0 || Bone >= Skeleton.GetBoneCount())
{
ReckfellApi.PushError("BonePoseProbe: assign a matching skeleton, modifier and bone index.");
return;
}
Modifier.ModificationProcessed += Capture;
}
private void Capture() => LastModified = Skeleton.GetBonePose(Bone);
public override void _ExitTree()
{
if (Modifier is not null && ReckfellObject.IsInstanceValid(Modifier))
Modifier.ModificationProcessed -= Capture;
}
}Исполнение подтвердило: Full обновляет позиции и вращения; RotationOnly сохраняет базовую позицию Spine, но продолжает обновлять позицию Hips. При HasTrackingData=false, невалидном Root или отсутствии tracker обновление пропускается — сохранённая pose руки здесь не воспроизводится.
BodyUpdate=0 оставил базовую pose основных суставов, но LowerChest из дополнительных суставов всё равно обновился. Для полного отключения проверен Active=false. Смена BodyTracker выбрала данные другого поставщика без пересопоставления костей.
Тест использовал диагностический скелет: Spine под Hips, остальные не-корневые кости под Root, все 87 имён присутствовали. Это проверка обработки трансформов, не настоящая анатомическая иерархия или оценка модели. Частично невалидные родительские суставы не проверены как корректный сценарий: используемые цепочки должны быть полностью согласованы и валидны.
Если используются трекеры ролей
Встроенный модуль содержит профиль /interaction_profiles/htc/vive_tracker_htcx, запрашивающий расширение XR_HTCX_vive_tracker_interaction. Поддержка конкретным runtime и устройством проверяется отдельно. Не используйте старую таблицу совместимости как гарантию.
Пример роли пояса: /user/vive_tracker_htcx/role/waist. В карте действий создайте Pose-действие с этим ToplevelPaths и привязкой /user/vive_tracker_htcx/role/waist/input/grip/pose в указанном профиле. Для ноги используется отдельная роль, например /user/vive_tracker_htcx/role/left_foot. Затем привяжите XREntity3D к точному имени роли и имени настроенной pose; используйте её как цель своей IK-логики. Сначала задаётся Tracker, затем Pose. Одного наличия Tracker в инспекторе недостаточно — нужны карта, данные и проверка флагов.
Список ролей и их координаты не определяют длины костей, калибровку, позу остальных суставов или столкновения тела. Для этого нужен отдельный solver; здесь он не реализован.
Диагностика и охват
Если tracker есть, но аватар неподвижен, проверьте HasTrackingData, валидность Root и родителей, Bone/BodyUpdate и имена костей. Если аватар смещён или растянут — пространство данных, размещение AvatarRoot и MotionScale. Если осталась старая поза — проверьте потерю tracking, не ориентируйтесь только на ненулевые координаты. Для рук есть отдельное руководство.
Выполнены 19 проверок состояния искусственного XRBodyTracker и 14 проверок настоящего Skeleton3D/XRBodyModifier3D с искусственными данными. Точный BonePoseProbe выполнен в движке. Дефект начальных JointFlags сохранён как отрицательный результат. Реальный поставщик, роль HTC, IK, настоящая Humanoid-иерархия, skin weights/деформация, смешивание анимации, экспорт и изображение не проверялись.