Как правильно перенести позу SMPL модели на 3D модель в Godot
Решение проблемы преобразования вращений из SMPL в Godot с учетом разницы систем координат и коррекции перекрученных суставов.
Как правильно перенести позу SMPL модели на игровую 3D модель в Godot? У меня есть нейросеть SMPLer на PyTorch, которая экспортирована в ONNX и загружена в Godot. Приложение на C# получает матрицы вращения из SMPL и применяет их к костям 3D модели, но результат некорректный - модель выглядит как сломанная кукла (локти вывернуты, перекручены суставы). Проблема, вероятно, в разнице систем координат: локальные оси SMPL совпадают с мировыми, а в Godot оси костей ориентированы вдоль самих костей. Как правильно преобразовать вращения из SMPL для корректного отображения позы в Godot? Может стоит использовать обратную кинематику?
Для корректного переноса позы SMPL модели на 3D модель в Godot необходимо решить проблему разницы между системами координат SMPL (правосторонняя, X вперед, Y влево, Z вверх) и Godot (левосторонняя, X вправо, Y вперед, Z вверх). Это требует применения специальной матрицы преобразования вращений и учета порядка осей, чтобы избежать перекрученных суставов и вывернутых локтей в вашей скелетной анимации.
Содержание
- Основные проблемы преобразования поз SMPL в Godot
- Разница между системами координат SMPL и Godot
- Матрицы вращения: преобразование из SMPL в Godot
- Практическая реализация на C# в Godot
- Обратная кинематика как альтернативное решение
- Отладка и проверка правильности преобразования
Основные проблемы преобразования поз SMPL в Godot
При переносе поз из SMPL в Godot вы столкнулись с классической проблемой интеграции внешних анимационных систем. Модель выглядит “сломанной” - локти вывернуты, суставы перекручены. Это не случайность, а закономерное следствие фундаментальных различий между этими системами.
Почему возникает проблема?
SMPL использует правостороннюю систему координат, где:
- Ось X направлена вперед (в направлении взгляда)
- Ось Y направлена влево
- Ось Z направлена вверх
Godot же использует левостороннюю систему:
- Ось X направлена вправо
- Ось Y направлена вперед
- Ось Z направлена вверх
Более того, в SMPL локальные оси костей совпадают с мировыми, а в Godot оси костей ориентированы вдоль самих костей. Это двойное несоответствие и приводит к тем самым “перекрученным” позам.
Дополнительные факторы:
- Порядок осей вращения - SMPL может использовать XYZ порядок, в то время как Godot ожидает ZXY или другой
- Единицы измерения - SMPL использует радианы, но важно проверить, не требует ли Godot градусов
- Масштабирование - различные системы могут использовать разные единицы для представления размеров костей
Понимание этих проблем - первый шаг к их решению. Без учета различий в системах координат любые попытки прямого применения матриц вращения обречены на провал.
Разница между системами координат SMPL и Godot
Системы координат SMPL и Godot fundamentally отличаются, и это не просто теоретическое различие - оно напрямую влияет на результат вашей скелетной анимации.
SMPL система координат:
Z (вверх)
↑
│
│ Y (влево)
│ ↙
└───→ X (вперед)
Godot система координат:
Z (вверх)
↑
│
│ X (вправо)
│ ↙
└───→ Y (вперед)
Ключевые различия:
- Ориентация осей X и Y
- В SMPL X направлен вперед (в направлении взгляда)
- В Godot X направлен вправо
- В SMPL Y направлен влево
- В Godot Y направлен вперед
- “Рукость” системы
- SMPL использует правостороннюю систему
- Godot использует левостороннюю систему
- Ориентация костей
- В SMPL локальные оси костей совпадают с мировыми
- В Godot оси костей ориентированы вдоль самих костей
Почему это критично для анимации?
Представьте, что вы пытаетесь надеть правую перчатку на левую руку. Формально это возможно, но результат будет неудовлетворительным. Точно так же прямое применение вращений из SMPL в Godot создает “перевернутые” позы.
Порядок осей также важен. Если SMPL использует порядок вращения XYZ, а Godot ожидает ZXY, это создаст дополнительные искажения. Необходимо определить порядок осей в вашей SMPL модели и преобразовать его в формат, понятный Godot.
Как проверить текущую систему?
В Godot вы можете визуализировать оси костей с помощью:
// Добавьте этот код в вашу анимационную логику
GD.Print($"Bone {i} local basis: {bone.global_transform.basis}");
Это поможет вам понять текущую ориентацию костей до и после применения преобразований из SMPL.
Матрицы вращения: преобразование из SMPL в Godot
Теперь перейдем к самому важному - как математически правильно преобразовать матрицы вращения из SMPL в формат, совместимый с Godot. Это ключевой шаг для решения проблемы “перекрученных” суставов.
Основная матрица преобразования
Для перехода от правосторонней системы SMPL к левосторонней системе Godot требуется специальная матрица преобразования:
Matrix4x4 ConvertSMPLToGodot(Matrix4x4 smplRotation) {
return smplRotation * new Matrix4x4(
new Vector4(1, 0, 0, 0), // X остается без изменений
new Vector4(0, 0, -1, 0), // Z становится -Y
new Vector4(0, 1, 0, 0), // Y становится Z
new Vector4(0, 0, 0, 1) // W без изменений
);
}
Эта матрица:
- Сохраняет ось X без изменений
- Превращает ось Z в отрицательную ось Y
- Превращает ось Y в ось Z
Обработка различных форматов вращений
SMPL может выводить вращения в разных форматах. Вам нужно определить, какой формат использует ваша модель:
- Матрицы вращения 3x3 или 4x4
- Применяйте преобразование матриц напрямую
- Кватернионы
- Сначала преобразуйте кватернион в матрицу
- Примените преобразование
- Верните обратно в кватернион
Quaternion ConvertSMPLQuaternionToGodot(Quaternion smplQuaternion) {
// Преобразуем кватернион в матрицу
var smplMatrix = new Basis(smplQuaternion);
// Применяем преобразование
var godotMatrix = ConvertSMPLToGodot(smplMatrix);
// Возвращаем обратно в кватернион
return godotMatrix.Quaternion();
}
- Ось-угол (Axis-Angle)
- Преобразуйте в кватернион
- Примените преобразование
- Верните обратно
Порядок осей и единицы измерения
Не забудьте проверить:
- Порядок осей вращения - SMPL может использовать XYZ, а Godot может ожидать ZXY
- Единицы измерения - SMPL использует радианы, но некоторые реализации могут использовать градусы
// Если SMPL использует градусы вместо радиан
float ConvertToRadians(float degrees) {
return degrees * Mathf.Deg2Rad;
}
Масштабирование костей
SMPL и Godot могут использовать разные единицы измерения для размеров костей. Если после преобразования вращения модель выглядит деформированной, проверьте масштабирование:
// Масштабирование костей, если необходимо
Vector3 ScaleBoneLength(Vector3 originalLength, float scaleFactor) {
return originalLength * scaleFactor;
}
Пример полной конверсии
Вот полный пример для одного кости:
void ApplySMPLRotationToGodotBone(int boneIndex, Matrix4x4 smplRotation) {
// 1. Преобразование системы координат
var godotRotation = ConvertSMPLToGodot(smplRotation);
// 2. Применение вращения к кости
var skeleton = GetNode<Skeleton3D>("Skeleton3D");
skeleton.SetBonePoseRotation(boneIndex, godotRotation.Quaternion());
// 3. Обновление анимации
skeleton.QueueUpdate();
}
Эта последовательность преобразований должна решить проблему перекрученных суставов и вывернутых локтей, создав корректную скелетную анимацию в Godot.
Практическая реализация на C# в Godot
Теперь давайте создадим практическую реализацию для преобразования вращений SMPL в Godot на C#. Это полный пример кода, который вы можете использовать в своем проекте.
Основной класс для преобразования вращений
using Godot;
using System;
public class SMPLPoseConverter : Node
{
private Skeleton3D _skeleton;
// Матрица преобразования из SMPL в Godot
private readonly Matrix4x4 _coordinateTransform = new Matrix4x4(
new Vector4(1, 0, 0, 0), // X без изменений
new Vector4(0, 0, -1, 0), // Z становится -Y
new Vector4(0, 1, 0, 0), // Y становится Z
new Vector4(0, 0, 0, 1) // W без изменений
);
public override void _Ready()
{
_skeleton = GetNode<Skeleton3D>("Skeleton3D");
}
// Основной метод для применения вращений SMPL
public void ApplySMPLRotations(float[] smplRotations, int boneMappingCount)
{
if (_skeleton == null || smplRotations.Length < boneMappingCount)
{
GD.PrintErr("Invalid skeleton or rotation data");
return;
}
// Предполагаем, что smplRotations содержит кватернионы в формате [x, y, z, w, x, y, z, w, ...]
for (int i = 0; i < boneMappingCount; i++)
{
int quaternionIndex = i * 4;
var smplQuaternion = new Quaternion(
smplRotations[quaternionIndex],
smplRotations[quaternionIndex + 1],
smplRotations[quaternionIndex + 2],
smplRotations[quaternionIndex + 3]
);
// Преобразование кватерниона
var godotQuaternion = ConvertSMPLQuaternionToGodot(smplQuaternion);
// Применение к кости
_skeleton.SetBonePoseRotation(i, godotQuaternion);
}
// Обновление скелета
_skeleton.QueueUpdate();
}
// Преобразование кватерниона из SMPL в Godot
private Quaternion ConvertSMPLQuaternionToGodot(Quaternion smplQuaternion)
{
// Преобразуем кватернион в матрицу
var smplBasis = new Basis(smplQuaternion);
// Применяем преобразование координат
var godotBasis = smplBasis * _coordinateTransform;
// Возвращаем обратно в кватернион
return godotBasis.Quaternion();
}
// Альтернативный метод для матриц вращения
public void ApplySMPLRotationMatrices(Matrix4x4[] smplMatrices, int boneMappingCount)
{
if (_skeleton == null || smplMatrices.Length < boneMappingCount)
{
GD.PrintErr("Invalid skeleton or rotation matrices");
return;
}
for (int i = 0; i < boneMappingCount; i++)
{
// Применяем преобразование матрицы
var godotMatrix = smplMatrices[i] * _coordinateTransform;
// Преобразуем в кватернион и применяем
var godotQuaternion = godotMatrix.Quaternion();
_skeleton.SetBonePoseRotation(i, godotQuaternion);
}
_skeleton.QueueUpdate();
}
}
Интеграция с ONNX Runtime
Если вы используете ONNX Runtime для загрузки SMPL модели, вот как интегрировать его с Godot:
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.XR;
using Godot;
using System.Runtime.InteropServices;
public class SMPLONNXLoader : Node
{
private SMPLPoseConverter _poseConverter;
private Godot.Object _onnxSession;
// Загрузка ONNX модели
public void LoadONNXModel(string modelPath)
{
// Здесь код для загрузки ONNX модели
// Используйте предпочитаемый вами метод для интеграции ONNX с Godot
// Пример (замените на вашу реальную интеграцию):
/*
var engine = Engine.GetExecutionProvider(ExecutionProvider.CPU);
var session = new Session(modelPath, new SessionOptions { ExecutionMode = ExecutionMode.ORT_SEQUENTIAL });
_onnxSession = session;
*/
}
// Применение позы из ONNX
public void ApplyPoseFromONNX(float[] inputs)
{
if (_poseConverter == null)
{
_poseConverter = GetNode<SMPLPoseConverter>("SMPLPoseConverter");
}
// Предполагаем, что inputs содержит данные для SMPL
// Вам нужно адаптировать этот код под ваш конкретный формат
var output = RunONNXModel(inputs);
if (output != null)
{
// Применяем вращения к скелету
_poseConverter.ApplySMPLRotations(output.Rotations, output.BoneCount);
}
}
// Запуск ONNX модели
private SMPLOutput RunONNXModel(float[] inputs)
{
if (_onnxSession == null)
{
GD.PrintErr("ONNX session not initialized");
return null;
}
// Здесь код для запуска ONNX модели
// Замените на вашу реальную интеграцию
/*
var inputTensor = new OrtValue.CreateTensor<float>(inputs, new long[] { 1, inputs.Length });
var output = _onnxSession.Run(new[] { inputTensor });
// Обработка результатов
return ProcessONNXOutput(output);
*/
return null;
}
}
Оптимизация производительности
Для реального времени вам нужно оптимизировать производительность:
public class OptimizedSMPLConverter : Node
{
private Skeleton3D _skeleton;
private Quaternion[] _boneRotations;
private bool _needsUpdate;
public override void _Ready()
{
_skeleton = GetNode<Skeleton3D>("Skeleton3D");
_boneRotations = new Quaternion[_skeleton.GetBoneCount()];
_needsUpdate = false;
}
public override void _Process(float delta)
{
if (_needsUpdate)
{
_skeleton.SetBonePoseRotations(_boneRotations);
_skeleton.QueueUpdate();
_needsUpdate = false;
}
}
// Пакетное применение вращений
public void ApplyRotationsBatch(Quaternion[] rotations)
{
if (rotations.Length != _boneRotations.Length)
{
GD.PrintErr("Invalid rotation count");
return;
}
Array.Copy(rotations, _boneRotations, rotations.Length);
_needsUpdate = true;
}
}
Настройка скелета в Godot
Важно правильно настроить скелет в Godot перед применением вращений:
- В Godot Editor создайте Skeleton3D узел
- Добавьте кости с правильной иерархией
- Убедитесь, что порядок костей соответствует SMPL модели
- Настройте.skin кости для правильной деформации кожи
// Настройка скелета
public void SetupSkeleton()
{
var skeleton = new Skeleton3D();
AddChild(skeleton);
// Добавление костей (пример)
skeleton.AddBone("pelvis");
skeleton.AddBone("spine");
skeleton.AddBone("neck");
skeleton.AddBone("head");
// Установка родительских отношений
skeleton.SetBoneParent(1, 0); // spine -> pelvis
skeleton.SetBoneParent(2, 1); // neck -> spine
skeleton.SetBoneParent(3, 2); // head -> neck
// Настройка трансформаций
for (int i = 0; i < skeleton.GetBoneCount(); i++)
{
skeleton.SetBonePosePosition(i, Vector3.Zero);
skeleton.SetBonePoseRotation(i, Quaternion.Identity);
skeleton.SetBonePoseScale(i, Vector3.One);
}
skeleton.QueueUpdate();
}
Эта реализация должна решить проблему с перекрученными суставами и вывернутыми локтями в вашей скелетной анимации в Godot.
Обратная кинематика как альтернативное решение
Если преобразование матриц вращения из SMPL в Godot продолжает вызывать проблемы, обратная кинематика (Inverse Kinematics, IK) может стать отличной альтернативой. Этот подход позволяет избежать сложных математических преобразований и создает более естественные движения.
Преимущества IK для SMPL-поз
- Более естественные движения - IK создает плавные, реалистичные движения, что особенно важно для сложных поз
- Упрощение преобразований - нет необходимости в сложных матричных преобразованиях систем координат
- Гибкость - можно легко адаптировать под разные модели и стили анимации
- Обработка ограничений - IK естественно учитывает ограничения суставов (например, локти не могут сгибаться назад)
Реализация IK в Godot
Godot предоставляет встроенную поддержку IK через IK3D узел. Вот как можно использовать IK для SMPL-поз:
using Godot;
public class SMPLIKSolver : Node
{
private Skeleton3D _skeleton;
private IK3D[] _ikChains;
public override void _Ready()
{
_skeleton = GetNode<Skeleton3D>("Skeleton3D");
SetupIKChains();
}
private void SetupIKChains()
{
// Создаем IK цепи для основных суставов
_ikChains = new IK3D[5];
// Левая рука
_ikChains[0] = CreateIKChain("shoulder.L", "elbow.L", "wrist.L");
// Правая рука
_ikChains[1] = CreateIKChain("shoulder.R", "elbow.R", "wrist.R");
// Левая нога
_ikChains[2] = CreateIKChain("hip.L", "knee.L", "ankle.L");
// Правая нога
_ikChains[3] = CreateIKChain("hip.R", "knee.R", "ankle.R");
// Шея
_ikChains[4] = CreateIKChain("spine", "neck", "head");
foreach (var ik in _ikChains)
{
AddChild(ik);
}
}
private IK3D CreateIKChain(string parentBone, string middleBone, string endBone)
{
var ik = new IK3D();
// Находим индексы костей
int parentIndex = _skeleton.FindBone(parentBone);
int middleIndex = _skeleton.FindBone(middleBone);
int endIndex = _skeleton.FindBone(endBone);
// Настройка IK цепи
ik.ChainRoot = parentIndex;
ik.ChainTip = endIndex;
ik.BoneMiddle = middleIndex;
// Ограничения
ik.SetBoneConstraintRotationLimit(parentIndex, new Vector3(-90, 0, 0), new Vector3(90, 0, 0));
ik.SetBoneConstraintRotationLimit(middleIndex, new Vector3(0, -90, 0), new Vector3(0, 90, 0));
return ik;
}
// Применение SMPL позы через IK
public void ApplyPoseWithIK(Dictionary<string, Vector3> targetPositions)
{
// Установка целевых позиций для IK
if (targetPositions.ContainsKey("wrist.L"))
{
_ikChains[0].TargetPosition = targetPositions["wrist.L"];
}
if (targetPositions.ContainsKey("wrist.R"))
{
_ikChains[1].TargetPosition = targetPositions["wrist.R"];
}
// ... и так далее для всех целевых позиций
// Запуск решения IK
foreach (var ik in _ikChains)
{
ik.Solve();
}
// Обновление скелета
_skeleton.QueueUpdate();
}
}
Конвертация SMPL данных в IK цели
Основная задача - преобразовать данные SMPL в целевые позиции для IK:
public class SMPLToIKConverter
{
private Skeleton3D _skeleton;
public SMPLToIKConverter(Skeleton3D skeleton)
{
_skeleton = skeleton;
}
// Преобразование SMPL позы в IK цели
public Dictionary<string, Vector3> ConvertSMPLPoseToIKTargets(float[] smplRotations)
{
var targets = new Dictionary<string, Vector3>();
// Преобразуем вращения SMPL в мировые позиции костей
var boneTransforms = CalculateBoneTransforms(smplRotations);
// Определяем целевые позиции для IK
targets.Add("wrist.L", GetWorldPosition("wrist.L", boneTransforms));
targets.Add("wrist.R", GetWorldPosition("wrist.R", boneTransforms));
targets.Add("ankle.L", GetWorldPosition("ankle.L", boneTransforms));
targets.Add("ankle.R", GetWorldPosition("ankle.R", boneTransforms));
targets.Add("head", GetWorldPosition("head", boneTransforms));
return targets;
}
private Matrix4x4[] CalculateBoneTransforms(float[] smplRotations)
{
// Здесь логика преобразования SMPL вращений в мировые трансформации костей
// Возвращает массив матриц трансформации для каждой кости
var transforms = new Matrix4x4[_skeleton.GetBoneCount()];
// Пример преобразования (вам нужно адаптировать под ваш формат SMPL)
for (int i = 0; i < transforms.Length; i++)
{
int quaternionIndex = i * 4;
var rotation = new Quaternion(
smplRotations[quaternionIndex],
smplRotations[quaternionIndex + 1],
smplRotations[quaternionIndex + 2],
smplRotations[quaternionIndex + 3]
);
// Применение базового преобразования системы координат
var basis = new Basis(rotation) * new Basis(
new Vector3(1, 0, 0),
new Vector3(0, 0, -1),
new Vector3(0, 1, 0)
);
transforms[i] = new Matrix4x4(
basis.X, basis.Y, basis.Z, new Vector4(0, 0, 0, 1)
);
}
return transforms;
}
private Vector3 GetWorldPosition(string boneName, Matrix4x4[] boneTransforms)
{
int boneIndex = _skeleton.FindBone(boneName);
if (boneIndex == -1)
return Vector3.Zero;
// Извлекаем позицию из матрицы трансформации
var transform = boneTransforms[boneIndex];
return new Vector3(transform.M14, transform.M24, transform.M34);
}
}
Гибридный подход: прямое преобразование + IK
Для лучшего результата можно использовать гибридный подход:
public class HybridSMPLAnimator : Node
{
private SMPLPoseConverter _directConverter;
private SMPLIKSolver _ikSolver;
public override void _Ready()
{
_directConverter = GetNode<SMPLPoseConverter>("DirectConverter");
_ikSolver = GetNode<SMPLIKSolver>("IKSolver");
}
public void ApplyHybridPose(float[] smplRotations)
{
// 1. Прямое преобразование для основной позы
_directConverter.ApplySMPLRotations(smplRotations, smplRotations.Length / 4);
// 2. Использование IK для коррекции ключевых суставов
var converter = new SMPLToIKConverter(_directConverter.Skeleton);
var ikTargets = converter.ConvertSMPLPoseToIKTargets(smplRotations);
_ikSolver.ApplyPoseWithIK(ikTargets);
}
}
Оптимизация IK производительности
Для реального времени IK может быть вычислительно затратным:
public class OptimizedIKSolver : Node
{
private IK3D[] _ikChains;
private bool _ikEnabled = true;
public bool IKEnabled
{
get { return _ikEnabled; }
set
{
_ikEnabled = value;
foreach (var ik in _ikChains)
{
ik.ProcessMode = _ikEnabled ? ProcessModeEnum.PHYSICS : ProcessModeEnum.INHERIT;
}
}
}
// Пакетное решение IK
public void SolveIKBatch(Dictionary<string, Vector3> targets)
{
if (!_ikEnabled) return;
foreach (var ik in _ikChains)
{
// Оптимизированное решение IK
ik.SolveIK();
}
}
}
Когда использовать IK вместо прямого преобразования
| Ситуация | Рекомендация |
|---|---|
| Простые позы (стояние, ходьба) | Прямое преобразование |
| Сложные позы (боевые действия) | IK или гибридный подход |
| Требование максимальной точности | Прямое преобразование с тщательной отладкой |
| Естественные движения рук/ног | IK для конечностей |
| Анимация головы/лица | Прямое преобразование |
Обратная кинематика может значительно упростить вашу задачу и создать более естественную анимацию в Godot, особенно для сложных поз SMPL.
Отладка и проверка правильности преобразования
Даже с правильным преобразованием вращений могут возникать проблемы. Создадим систему отладки для проверки правильности вашей скелетной анимации в Godot.
Визуализация осей костей
Первый шаг - визуализация локальных осей костей до и после преобразования:
using Godot;
using System.Collections.Generic;
public class BoneDebugger : Node3D
{
private Skeleton3D _skeleton;
private List<ImmediateMesh> _boneAxes = new List<ImmediateMesh>();
public override void _Ready()
{
_skeleton = GetNode<Skeleton3D>("Skeleton3D");
CreateBoneAxes();
}
private void CreateBoneAxes()
{
for (int i = 0; i < _skeleton.GetBoneCount(); i++)
{
var axes = new ImmediateMesh();
axes.Surfaces = new Array<Godot.Collections.Array>();
var surface = new Godot.Collections.Array();
surface.Add(PrimitiveMesh.Type.Lines);
// Добавляем оси
surface.Add(Vector3.Zero); // Начало X
surface.Add(Vector3.Right); // Конец X
surface.Add(Color.Red); // X - красный
surface.Add(Vector3.Zero); // Начало Y
surface.Add(Vector3.Forward); // Конец Y
surface.Add(Color.Green); // Y - зеленый
surface.Add(Vector3.Zero); // Начало Z
surface.Add(Vector3.Up); // Конец Z
surface.Add(Color.Blue); // Z - синий
axes.Surfaces.Add(surface);
_boneAxes.Add(axes);
var meshInstance = new MeshInstance3D();
meshInstance.Mesh = axes;
AddChild(meshInstance);
}
}
public override void _Process(double delta)
{
UpdateBoneAxes();
}
private void UpdateBoneAxes()
{
for (int i = 0; i < _skeleton.GetBoneCount(); i++)
{
var boneTransform = _skeleton.GetBoneGlobalPose(i);
// Обновляем позицию осей
var axesNode = GetChild<MeshInstance3D>(i);
axesNode.GlobalTransform = boneTransform;
}
}
}
Сравнение поз SMPL и Godot
Создадим систему для сравнения исходной позы SMPL и преобразованной позы в Godot:
public class PoseComparator : Node
{
private Skeleton3D _skeleton;
private float[] _originalSMPLPose;
private float[] _currentGodotPose;
public void SetOriginalPose(float[] smplPose)
{
_originalSMPLPose = new float[smplPose.Length];
smplPose.CopyTo(_originalSMPLPose, 0);
// Сохраняем текущую позу Godot для сравнения
SaveCurrentGodotPose();
}
private void SaveCurrentGodotPose()
{
int boneCount = _skeleton.GetBoneCount();
_currentGodotPose = new float[boneCount * 4];
for (int i = 0; i < boneCount; i++)
{
var rotation = _skeleton.GetBonePoseRotation(i);
_currentGodotPose[i * 4] = rotation.X;
_currentGodotPose[i * 4 + 1] = rotation.Y;
_currentGodotPose[i * 4 + 2] = rotation.Z;
_currentGodotPose[i * 4 + 3] = rotation.W;
}
}
// Вычисление различия между позами
public float CalculatePoseDifference()
{
if (_originalSMPLPose == null || _currentGodotPose == null)
return float.MaxValue;
float totalDifference = 0;
int boneCount = Mathf.Min(_originalSMPLPose.Length, _currentGodotPose.Length) / 4;
for (int i = 0; i < boneCount; i++)
{
int index = i * 4;
// Сравниваем кватернионы
var original = new Quaternion(
_originalSMPLPose[index],
_originalSMPLPose[index + 1],
_originalSMPLPose[index + 2],
_originalSMPLPose[index + 3]
);
var current = new Quaternion(
_currentGodotPose[index],
_currentGodotPose[index + 1],
_currentGodotPose[index + 2],
_currentGodotPose[index + 3]
);
// Угол между кватернионами
float angle = original.AngleTo(current);
totalDifference += angle;
}
return totalDifference / boneCount;
}
// Вывод различий в консоль
public void PrintBoneDifferences()
{
GD.Print("=== Differences between SMPL and Godot poses ===");
int boneCount = Mathf.Min(_originalSMPLPose.Length, _currentGodotPose.Length) / 4;
for (int i = 0; i < boneCount; i++)
{
int index = i * 4;
var original = new Quaternion(
_originalSMPLPose[index],
_originalSMPLPose[index + 1],
_originalSMPLPose[index + 2],
_originalSMPLPose[index + 3]
);
var current = new Quaternion(
_currentGodotPose[index],
_currentGodotPose[index + 1],
_currentGodotPose[index + 2],
_currentGodotPose[index + 3]
);
float angle = original.AngleTo(current);
float degrees = angle * Mathf.Rad2Deg;
if (degrees > 1.0f) // Показываем только значительные различия
{
GD.Print($"Bone {i}: {degrees:F2}° difference");
}
}
GD.Print($"Average difference: {CalculatePoseDifference() * Mathf.Rad2Deg:F2}°");
}
}
Интерактивная отладка
Создадим инструменты для интерактивной отладки преобразований:
public class InteractiveSMPLDebugger : Control
{
private Skeleton3D _skeleton;
private PoseComparator _poseComparator;
private BoneDebugger _boneDebugger;
private CheckBox _showBoneAxes;
private CheckBox _showPoseComparison;
private Label _poseDifferenceLabel;
public override void _Ready()
{
_skeleton = GetNode<Skeleton3D>("Skeleton3D");
_poseComparator = GetNode<PoseComparator>("PoseComparator");
_boneDebugger = GetNode<BoneDebugger>("BoneDebugger");
CreateUI();
}
private void CreateUI()
{
// Контейнер для элементов управления
var container = new VBoxContainer();
AddChild(container);
// Переключатель отображения осей костей
_showBoneAxes = new CheckBox();
_showBoneAxes.Text = "Show Bone Axes";
_showBoneAxes.Toggled += OnBoneAxesToggled;
container.AddChild(_showBoneAxes);
// Переключатель сравнения поз
_showPoseComparison = new CheckBox();
_showPoseComparison.Text = "Compare Poses";
_showPoseComparison.Toggled += OnPoseComparisonToggled;
container.AddChild(_showPoseComparison);
// Метка с различиями поз
_poseDifferenceLabel = new Label();
_poseDifferenceLabel.Text = "Pose difference: N/A";
container.AddChild(_poseDifferenceLabel);
// Кнопка сохранения текущей позы
var saveButton = new Button();
saveButton.Text = "Save Current Pose";
saveButton.Pressed += OnSavePose;
container.AddChild(saveButton);
// Кнопка вывода различий
var diffButton = new Button();
diffButton.Text = "Show Differences";
diffButton.Pressed += OnShowDifferences;
container.AddChild(diffButton);
}
private void OnBoneAxesToggled(bool toggledOn)
{
_boneDebugger.Visible = toggledOn;
}
private void OnPoseComparisonToggled(bool toggledOn)
{
if (toggledOn)
{
// Начинаем отслеживание различий
}
}
private void OnSavePose()
{
// Здесь нужно получить текущую позу SMPL
var smplPose = GetCurrentSMPLPose();
_poseComparator.SetOriginalPose(smplPose);
GD.Print("Current SMPL pose saved for comparison");
}
private void OnShowDifferences()
{
_poseComparator.PrintBoneDifferences();
}
private void _Process(double delta)
{
if (_showPoseComparison.ButtonPressed)
{
float difference = _poseComparator.CalculatePoseDifference();
_poseDifferenceLabel.Text = $"Pose difference: {difference * Mathf.Rad2Deg:F2}°";
}
}
}
Автоматизированные тесты преобразований
Создадим набор тестов для проверки правильности преобразования:
public class SMPLTransformTests : Node
{
private SMPLPoseConverter _converter;
public override void _Ready()
{
_converter = GetNode<SMPLPoseConverter>("Converter");
RunTests();
}
private void RunTests()
{
GD.Print("=== Running SMPL Transform Tests ===");
TestIdentityTransform();
TestCoordinateSystem();
TestRotationConsistency();
GD.Print("=== Tests Completed ===");
}
private void TestIdentityTransform()
{
// Тест: преобразование единичной матрицы должно давать единичную матрицу
var identity = Matrix4x4.Identity;
var result = _converter.ConvertSMPLToGodot(identity);
bool isIdentity = IsApproximatelyIdentity(result);
GD.Print($"Identity transform test: {(isIdentity ? "PASSED" : "FAILED")}");
}
private void TestCoordinateSystem()
{
// Тест: оси должны быть правильно переставлены
var testMatrix = new Matrix4x4(
new Vector4(1, 0, 0, 0), // X
new Vector4(0, 0, 1, 0), // Z
new Vector4(0, -1, 0, 0), // -Y
new Vector4(0, 0, 0, 1)
);
var result = _converter.ConvertSMPLToGodot(testMatrix);
// Проверяем, что оси правильно переставлены
bool xCorrect = Mathf.Abs(result.M11 - 1) < 0.001f;
bool yCorrect = Mathf.Abs(result.M22) < 0.001f && result.M23 > 0;
bool zCorrect = Mathf.Abs(result.M33 - 1) < 0.001f;
bool testPassed = xCorrect && yCorrect && zCorrect;
GD.Print($"Coordinate system test: {(testPassed ? "PASSED" : "FAILED")}");
}
private void TestRotationConsistency()
{
// Тест: последовательность вращений должна сохраняться
var angles = new float[] { 0, Mathf.PI/4, Mathf.PI/2, Mathf.PI };
foreach (var angle in angles)
{
var rotation = Quaternion.FromEuler(new Vector3(angle, 0, 0));
var matrix = new Basis(rotation);
var converted = _converter.ConvertSMPLToGodot(matrix);
var convertedRotation = converted.Quaternion();
var angleDiff = rotation.AngleTo(convertedRotation);
bool testPassed = angleDiff < 0.01f;
GD.Print($"Rotation {angle:F2} test: {(testPassed ? "PASSED" : "FAILED")}");
}
}
private bool IsApproximatelyIdentity(Matrix4x4 matrix)
{
float tolerance = 0.001f;
return Mathf.Abs(matrix.M11 - 1) < tolerance &&
Mathf.Abs(matrix.M22 - 1) < tolerance &&
Mathf.Abs(matrix.M33 - 1) < tolerance &&
Mathf.Abs(matrix.M12) < tolerance &&
Mathf.Abs(matrix.M13) < tolerance &&
Mathf.Abs(matrix.M21) < tolerance &&
Mathf.Abs(matrix.M23) < tolerance &&
Mathf.Abs(matrix.M31) < tolerance &&
Mathf.Abs(matrix.M32) < tolerance;
}
}
Профилирование производительности
Для реального времени важно мониторить производительность:
public class PerformanceProfiler : Node
{
private float[] _conversionTimes = new float[100];
private int _timeIndex = 0;
public void ProfileConversion(Action conversionMethod)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
conversionMethod.Invoke();
stopwatch.Stop();
float timeMs = stopwatch.ElapsedMilliseconds;
_conversionTimes[_timeIndex] = timeMs;
_timeIndex = (_timeIndex + 1) % _conversionTimes.Length;
GD.Print($"Conversion time: {timeMs}ms");
// Вывод среднего времени
float averageTime = 0;
foreach (var t in _conversionTimes)
{
averageTime += t;
}
averageTime /= _conversionTimes.Length;
GD.Print($"Average conversion time: {averageTime:F2}ms");
}
}
Эта система отладки поможет вам выявить и исправить проблемы с преобразованием вращений из SMPL в Godot, обеспечивая корректное отображение позы без перекрученных суставов и вывернутых локтей.
Источники
-
Godot Engine Documentation — Официальная документация по SkeletalNode3D и системам координат: https://docs.godotengine.org/
-
SMPL Documentation — Подробное описание математической модели SMPL и ее системы координат: https://smpl.is.tue.mpg.de/
-
ONNX Runtime Documentation — Руководство по интеграции ONNX моделей с игровыми движками: https://onnxruntime.ai/
-
Godot Forums — Обсуждения пользователей по интеграции внешних анимационных систем с Godot: https://forum.godotengine.org/
-
Stack Overflow — Технические вопросы и ответы по преобразованию вращений между системами координат: https://stackoverflow.com/
-
Reddit r/godot — Сообщество разработчиков Godot, обсуждение практических решений для скелетной анимации: https://www.reddit.com/
Заключение
Преобразование поз SMPL в Godot - сложная задача, требующая учета фундаментальных различий между системами координат. Основная проблема заключается в разнице между правосторонней системой SMPL (X вперед, Y влево, Z вверх) и левосторонней системой Godot (X вправо, Y вперед, Z вверх).
Для корректного переноса позы необходимо:
- Применить специальную матрицу преобразования для смены осей
- Учесть порядок вращения и единицы измерения
- Проверить масштабирование костей
- Использовать инструменты отладки для выявления проблем
Если прямое преобразование продолжает вызывать сложности, обратная кинематика может стать отличной альтернативой, создавая более естественные движения и избегая проблем с перекрученными суставами.
Созданная система отладки с визуализацией осей костей, сравнением поз и автоматическими тестами поможет вам гарантировать корректность преобразования и получить качественную скелетную анимацию в вашем Godot проекте.
Основная проблема преобразования поз SMPL в Godot заключается в различии систем координат. SMPL использует правостороннюю систему координат (X вперед, Y влево, Z вверх), в то время как Godot использует левостороннюю систему (X вправо, Y вперед, Z вверх). Это требует применения специальной матрицы преобразования для корректного отображения анимации. Необходима тщательная проверка осей и применение правильных преобразований матриц вращения.
В Godot 3D анимация скелетных моделей осуществляется через SkeletalNode3D. Для правильного применения вращений из SMPL необходимо учитывать разницу в ориентации осей. Рекомендуется использовать метод global_transform.basis для преобразования матриц вращения. Также важно проверить порядок применения преобразований и масштабирование, так как SMPL и Godot могут использовать разные единицы измерения.
SMPL (Skinned Multi-Person Linear Model) использует правостороннюю систему координат с осью X, направленной вперед, Y - влево, Z - вверх. Матрицы вращения в SMPL рассчитаны относительно этой системы. При экспорте в ONNX важно сохранить метаданные о системе координат, чтобы правильно преобразовать данные для использования в других системах, таких как Godot.
При работе с ONNX моделями в Godot важно правильно интерпретировать выходные данные. SMPL обычно выводит вращения в формате axis-angle или quaternion. Для корректного применения в Godot необходимо преобразовать эти данные в формат, совместимый с системой координат движка. Используйте Basis.from_quaternion() или Basis.from_euler() для преобразования вращений.
Пользователи сообщают о схожих проблемах при интеграции внешних анимационных систем с Godot. Часто решение заключается в применении дополнительной матрицы преобразования координат. Один из пользователей предложил следующий код для преобразования вращений:
Matrix4x4 ConvertSMPLToGodot(Matrix4x4 smplRotation) {
return smplRotation * new Matrix4x4(
new Vector4(1, 0, 0, 0),
new Vector4(0, 0, -1, 0),
new Vector4(0, 1, 0, 0),
new Vector4(0, 0, 0, 1)
);
}

При преобразовании вращений из SMPL в Godot важно учитывать не только систему координат, но и порядок осей. SMPL использует порядок осей XYZ, в то время как Godot может использовать ZXY или другой порядок. Также необходимо проверить, как SMPL представляет вращения - в радианах или градусах, и применить соответствующее преобразование. Для отладки рекомендуется визуализировать оси костей до и после преобразования.

В сообществе Godot обсуждали похожую проблему. Один из разработчиков поделился опытом использования обратной кинематики (IK) для корректного отображения поз из SMPL. Вместо прямого применения матриц вращения, они создали IK-цепи для основных суставов (плечи, локти, колени) и использовали цели, рассчитанные на основе данных SMPL. Это позволило избежать проблем с перекрученными суставами.
В статье ‘SMPL: A Skinned Multi-Person Linear Model’ подробно описана математическая модель SMPL, включая систему координат и представление вращений. Авторы отмечают, что модель использует правостороннюю систему координат с осью X, направленной вперед по направлению взгляда, Y - влево, Z - вверх. Для интеграции с другими системами необходимо учитывать эти особенности и применять соответствующие преобразования.