Процедурный локомоушен в unreal 5
На днях увлекся работой с процедурным , решил сделать небольшой разбор урока от Lincoln Margison - на примере упрощенного процедурного локомоушена
Мой телеграмм канал где я рассказываю об анимации , unreal и не только
Идем в Control rig персонажа
Идем Construction event - создаем первую позу дефолную
Создаем массив имен FootNames ( сюда вписываем имена наших костей) , по циклу их перебераем и берем из них трансформы , переводим их в мировые координаты и запихиваем в ноду OutputWorldFootTargets
Таким образом мы сделаем дефолтную позу от которой и будем работать
Добавляем переменную FootTransitionValue - она нам пригодиться позже
Создаем Функцию
checkFootOutRange - проверка выходит ли наша ногу за дистанцию шага
Берем массив имен FootNames , по циклу их перебераем
и теперь берем ноду OutputWorldFootTargets и применяем к ней AT ( это аналог Get) index берем из For Each ( каждому имени в массиве соотвествует свой индекс , и с помощью индекса мы достаем из ноды трансформы привязанные к нашим костям)
Дальше Мы из AT тянем from world и что бы перевести данные из мировых координат в координаты рига ( это нужно что бы потом их сравнивать внутри одной системы координат)
Создаем ноду Get transform Bone и сюда пихаем из Element в Name ( что мы делаем мы берем имя из FootNames - с помощью цикла перебираем эти имена и подаем в ноду Get transform Bone - где он по именам находит кости и снимает с них трансформы
Дальше мы сравниваем значения из OutputWorldFootTargets и трансформы костей
Напомню данные OutputWorldFootTargets у нас статичные и служать как бы отправной точкой -и в мировых координатах они будут на одном месте , данные же трансформов с костей- будут постоянно меняться в мировых координатах
И если значения больше чем ( тут мы создаем новую переменную - которая по сути будет длинной нашего шага ) то мы берем FootTransitionValue и ставим 1
в переменную FootTransitionValue так же подаем наш индекс из Foor loop
значение 1.0 — это сигнал или флаг. Он говорит остальной части анимационной системы: "Эта нога растянулась слишком далеко, пора запускать анимацию нового шага!"
Создаем функцию DecreaseTransitionValue
Это функция DecreaseTransitionValue — она приводит значение 1 которую мы установили к 0 - по заданному периоду времени ( как бы сбрасывает счетчик)
Что тут происходит по шагам:
- Get FootTransitionValue → For Each Берём массив FootTransitionValue (по одному числу на каждую ногу) и проходишься по всем индексам.
- At достаем значение каждой ноги по ее идексу .
- Subtract (A − B) Вычитаем из значения Delta Time (B приходит из ноды Delta Time). Смысл: каждый кадр наша 1 уменьшается по delta time.
- Set AT
- Записываем обратно уменьшенное значение по тому же индексу.
И так каждый кадр значение постепенно будет идти к 0
Тем самым после шага наша нога сможет сделать еще шаг при соблюдении других условий
Создаем функцию MoveFeet
Запускается цикл For Each по всем ногам (foot_L, foot_R).
Для каждой ноги проверяется её FootTransitionValue. Если значение больше нуля, значит, ногу нужно двигать.
- С помощью Spring Interpolate вычисляется новое, плавное положение для IK-цели, которая "догоняет" реальное положение кости из анимации.
- Мы берем положение стопы из OutputWorldFootTargets. - говорим прими положение текущие ног и интерполируем переход из одного положение в другой нодой Spring Interpolate
- Результат (новое положение, поворот и масштаб) записывается в массив OutputWorldFootTargets с помощью ноды Set AT , index берем из for loop
Итог этого шага: Переменная OutputWorldFootTargets теперь содержит самые свежие, рассчитанные на этот кадр, целевые координаты для левой и правой ног
Теперь соберем это все вместе и добавим fullbody ik
- Forwards Solve: Это главная точка входа, которая запускается на каждом кадре обновления анимации.
- checkFootOutOfRange / DecreaseTransitionValue: Напомню в одной функции мы проверяем не ушел ли персонаж от дефолтной позы на указанное значение -если ушел то ставим единицу в FootTransitionValue , во второй мы постепенное снижаем значение 1 до 0 в функции FootTransitionValue
- MoveFeet: Это вызов той самой большой функции, которую мы уже разобрали. Её единственная задача — рассчитать новые целевые координаты для ног и записать их в переменную OutputWorldFootTargets.
- Full Body IK: Это мощный встроенный в Unreal Engine решатель (солвер).В его входы Effectors подаются рассчитанные нами целевые трансформации. Мы говорим ему: "Цель для кости foot_l находится в этих координатах. Цель для foot_r — в этих".Получив эти цели, узел Full Body IK автоматически изменяет позу всего скелета персонажа (сгибает колени, бедра, наклоняет таз), чтобы стопы модели точно встали в указанные нами точки.
Важно данные для FullBody мы берем из OutputWorldFootTargets - туда мы записали мировые координаты ног - нужно их перевести обратно в пространство рига с помощью ноды FromWorld
Значения мы так же берем по индекс с помощью ноды AT и просто ставим или 1 или 0
Создаем функцию CalculateVelocity
создаем ноду PreviousWorldTransform
Узел достает из памяти позицию и ориентацию персонажа, которые были у него в предыдущем кадре. ( это мы назначим позже)
WorldDelta - здесь мы будем хранить - разницу между прошлым и текущим кадром
Velocity - здесь мы будем хранить скорость нашего персонажа
Дла того что бы нам узнать нашу делту , нам нужно взять текущие положение персонажа и вычесть из него прошлое
И делать мы это будем необычным вычитанием - а вот так :
Get PreviousWorldTransform: Узел достает из памяти позицию и ориентацию персонажа, которые были у него в предыдущем кадре.
Inverse: Этот узел "обращает" трансформацию.- уводит в минус
To World & Multiply: Мы берем текущую трансформацию персонажа и умножаем ее на миносовую(inverse) предыдущую. В 3D-математике операция ТекущаяПозиция * Обратная(ПрошлаяПозиция) дает в результате трансформацию смещения (дельту) — то есть, само движение, которое произошло между кадрами.
Set WorldDelta: Результат этого вычисления (насколько персонаж сдвинулся и повернулся за 1 кадр) сохраняется в переменную WorldDelta.
Теперь берем нашу переменную WorldDelta Отсюда мы берем Расстояние — вектор смещения, который мы рассчитали на прошлом шаге.
Delta Time: Этот узел дает нам Время — сколько секунд прошло за последний кадр. Например, при 60 FPS это будет примерно 0.01667 секунды.
Divide (1.0 / Delta Time): Здесь происходит деление единицы на время кадра. — фактически, это множитель, показывающий, сколько таких "кадров" поместилось бы в одной секунде.
Scale: Этот узел умножает наше Расстояние (WorldDelta.Translation) на результат из узла Divide. и мы благодоря это получает скорость персонажа в секунду
Пример 1/0.01667( это наша Delta time) = 59,9880023995 * на расстояние за кадр(World delta которую мы высчитываливыше) = расстояние за секунду времени - который пройдет персона
Итог этого шага: Мы преобразовали нестабильное "смещение за кадр" в стабильную, фреймрейт-независимую скорость в юнитах/секунду. Теперь неважно, 30 FPS в игре или 120 — вычисленная скорость всегда будет отражать реальную скорость движения персонажа.
Spring Interpolate: Вычисленная "сырая" скорость может быть немного дерганой. Чтобы сделать ее более плавной, ее прогоняют через уже знакомый нам Spring Interpolate. Он сглаживает резкие изменения скорости от кадра к кадру.
Set Velocity: Новая, плавная скорость сохраняется в переменную Velocity,
Set PreviousWorldTransform: Это критически важный узел, который "замыкает цикл". Он берет текущую позицию персонажа(To World берет данные с 000 координатх рига и переносить их в те координаты где сейчас персонаж находится в мире ) и сохраняет ее
И так наш персонаж смещается и постояннно перезаписывает свое положение в мире в переменную PreviousWorldTransform: и сравнивает с данными из этой переменной как далеко он прошел
Возвращаемся в MoveFeet
И добавим несколько нод для Get Transform bone
- Get Transform - BoneЭто базовая позиция ноги, взятая прямо из анимации в текущем кадре. Назовем ее Точка А.
- Get VelocityЭто вектор скорости всего персонажа, который мы вычисляли ранее. Он показывает, куда и как быстро движется персонаж.
- Clamp (Length)Это ключевой узел в этом блоке. Он берет вектор скорости и ограничивает его длину.Maximum Length задается переменной Get MaxStepDistance (например, 50 см).Как это работает: Если персонаж движется очень быстро, его вектор скорости может быть очень длинным (например, 200 см/с). Этот узел "укорачивает" этот вектор до максимальной длины шага (50 см), сохраняя при этом его направление. Если персонаж движется медленно (вектор короче 50 см), узел ничего не меняет.Результат: Мы получаем вектор прогнозируемого шага. Он всегда указывает в сторону движения персонажа, но его длина никогда не превышает разумную длину одного шага. .
- Add (Сложение векторов)Этот узел просто складывает два вектора: Точка А + Вектор Б.Он берет базовую позицию ноги из анимации и "смещает" её вперед в направлении движения на расстояние, которое мы рассчитали на прошлом шаге.Результат: Мы получаем финальную Целевую Точку Приземления.
- To World и Spring Interpolate Новая вычисленная позиция (Translation) объединяется с оригинальным вращением (Rotation) из анимации.И эта итоговая трансформация подается в качестве Target для уже знакомого нам узла Spring Interpolate.
Итог
- Берет позицию ноги из текущей позы.
- Смотрит, куда движется персонаж.
- Прогнозирует "вектор шага" в этом направлении, ограничивая его максимальной длиной.
- Смещает анимированную позицию ноги вдоль этого вектора.
- И уже в эту новую, "умную" точку плавно перемещает ногу с помощью пружинной интерполяции.
Делаем рассинхрон для ног
Идем в фуекцию checkFootOutRange и добавляем вот такие ноды
- Get FootTransitionValue → At Если условие True, система первым делом получает текущее значение FootTransitionValue для нужной ноги. Это значение показывает, насколько нога "готова" к движению (0 = стоит на месте, 1 = активно движется).
- Maximum ( первый) Этот узел просто выбирает большее из двух значений. Вход А: Текущее FootTransitionValue. Вход B: 0.0.Это защита, которая гарантирует, что значение не будет отрицательным. В
- Add Ключевой узел. Система берет значение и прибавляет к нему 0.2.Вместо того чтобы мгновенно дать команду "ШАГАТЬ!" (установив значение в 1.0), система постепенно увеличивает готовность ноги к шагу. Каждый кадр, пока нога отстает, ее "желание" сделать шаг нарастает: 0 → 0.2 → 0.4...
- Set At Новое, увеличенное значение записывается обратно в массив FootTransitionValue для текущей ноги, чтобы другие части системы могли его использовать.
Эта логика создает динамическую систему, которая работает в паре с другой функцией, постоянно уменьшающей FootTransitionValue до нуля.
- checkFootOutOfRange увеличивает значение, когда нога отстает, что бы сделать задержку перед шагом ".
- DecreaseTransitionValue (другая функция) уменьшает значение, когда нога в порядке.
получается сначала мы отмечаем вышла наша нога за границу цикла или нет - если вышла - делаем задержку перед шагом ( это нужно что бы нога не дрожала), если персонаж перестал смещать тело вперед обнуляем таймер и не делаем шаг
Вот такая последовательность нод у нас получается
Настраиваем FullBody для головы
1. Get Transform – Bone (Global Space)
Берём глобальный трансформ кости head. То есть получаем позицию и ротацию головы относительно Rig Hierarchy (глобального для Control Rig), но ещё не в «чистом» world.
2. To World
Эта нода переводит полученный трансформ из Rig Global Space в World Space (мировые координаты Unreal).
3. Spring Interpolate
Эта нода добавляет пружинную интерполяцию (с демпфированием) для сглаживания движения.
- Вход Target → это цель (куда должна двигаться кость).
- Strength и Critical Damping → задают, как сильно и плавно будет происходить смещение.
- Результат → плавное значение в world space, как если бы на голову навесили физическую пружину.
То есть, если кость резко дёрнется (например, IK подвинул тело), пружина сгладит это движение.
4. From World
После интерполяции результат нужно вернуть обратно в систему координат Control Rig (Rig Space). Почему?
- Full Body IK и большинство операций внутри Control Rig ждут данные в пространстве рига, а не в world.
- Если оставить в world, IK просто "не поймёт" координаты. Поэтому мы снова делаем преобразование: World → Rig Space.
5. Зачем такая цепочка?
- To World → нужно, чтобы "вынести" координаты кости в чистый мир, где работает Spring Interpolate.
- Spring Interpolate → сглаживает движение в world space.
- From World → возвращает результат обратно в rig space, чтобы кость правильно участвовала в IK и в остальной логике Control Rig.
- 6 так же для ротейтов делается отдельно Spring Interpolate ( можно поставить другие значения)
Итоговый эффект
Когда персонаж будет бежать, резко останавливаться или поворачиваться, его голова не будет двигаться как единое целое с позвоночником. Вместо этого она будет:
- Слегка отставать от резких движений.
- Плавно "догонять" основную анимацию.
- Совершать небольшие вторичные колебания после остановки.