Процедурная анимация с использованием ПИД-регуляторов зависит от частоты кадров и нервничает.C#

Место общения программистов C#
Ответить
Anonymous
 Процедурная анимация с использованием ПИД-регуляторов зависит от частоты кадров и нервничает.

Сообщение Anonymous »

Это будет долго.
Контекст
Я разрабатываю шутер от первого лица. Для анимации оружия я использую ПИД-регуляторы для реализации процедурно анимированного раскачивания оружия. Моя цель — создать ощущение, что оружие имеет массу и инерцию, когда игрок движется, вместо того, чтобы быть привязанным к родительской трансформации и двигаться без каких-либо задержек или раскачиваний, что было бы очень роботизировано и неудовлетворительно.
Я хочу добиться чего-то вроде этого:
Изображение

Класс регулятора реализован следующим образом:

Код: Выделить всё

namespace Animation
{
[Serializable]
public class PidController
{
public float P, I, D;

private float _integral;
private float _previousError;
private bool _firstUpdate = true;

public float Calculate(float targetValue, float currentValue, float deltaTime)
{
var error = targetValue - currentValue;
var proportional = P * error;

_integral += error * deltaTime;
var integralTerm = I * _integral;

var derivative = 0f;

if (!_firstUpdate && deltaTime > 0f)
{
derivative = (error - _previousError) / deltaTime;
}

var derivativeTerm = D * derivative;

_previousError = error;
_firstUpdate = false;

return proportional + integralTerm + derivativeTerm;
}

public void Reset()
{
_integral = 0;
_previousError = 0;
_firstUpdate = true;
}
}
}
Затем на каждой итерации (что именно это означает, продолжайте читать) я сохраняю последнее вращение моего объекта-персонажа, а затем в следующей итерации я вычисляю угловую скорость на основе разницы между текущим и предыдущим вращением. Это прекрасно работает.
Используя это, я затем рассчитываю три вида раскачивания (вертикальное, горизонтальное и вращательное). Давайте сосредоточимся на горизонтальном, потому что все они по сути одинаковы. dt — разница времени между предыдущей и текущей итерациями.

Код: Выделить всё

private float UpdateHorizontalSway(float velocityY, float dt)
{
var target = velocityY * horizontalSwayScale;

var yRotation = Mathf.DeltaAngle(0, swayTransform.localEulerAngles.y);

var action = horizontalSwayPidController.Calculate(target, yRotation, dt);
_horizontalSwayVelocity += action * dt;
_horizontalSwayVelocity *= 1f - horizontalSwayDamping;

return yRotation + _horizontalSwayVelocity * dt;
}
Я использую возвращаемое значение для обновления текущего вращения:

Код: Выделить всё

var newYRotation = UpdateHorizontalSway(angularVelocity.y, dt) - linearVelocity.x * movementFactor;
var newXRotation = UpdateVerticalSway(angularVelocity.x, dt) + linearVelocity.y * movementFactor;
var newZRotation = UpdateRotationalSway(angularVelocity.y, dt);

swayTransform.localRotation = Quaternion.Euler(newXRotation, newYRotation, newZRotation);

Remember();
И затем, не забывая, я обновляю _lastRotation.
Подход 1: обновляемые вычисления
В приведенном выше разделе уже содержатся некоторые детали реализации, связанные с этим подходом, но я хотел показать фактический код. При таком подходе я запускаю симуляцию один раз для каждого кадра и напрямую обновляю вращение. Моя частота кадров очень стабильна, поскольку сцена, которую я использую для тестирования (и вся игра), еще не содержит абсолютно никакого контента. При стабильной частоте кадров в редакторе и входных значениях для масштабирования, демпфирования и настройки ПИД-регуляторов я получаю красивую анимацию покачивания.
Когда я создаю игру и запускаю сборку, игра, очевидно, в конечном итоге работает с гораздо более высоким счетчиком FPS, чем редактор, поскольку предположительно гораздо меньше накладных расходов, поэтому скорость, с которой выполняется симуляция, также значительно увеличивается, и именно здесь начинаются проблемы.
Симуляция теперь ведет себя совершенно по-другому. Амплитуда движений, а также скорость их корректировки значительно больше, чем в редакторе. ChatGPT и Gemini в некоторой степени согласны с тем, что это связано с тем, что ПИД-регулятор из-за своей зависимости от времени сильно зависит от частоты дискретизации, но на данный момент мое понимание математики становится недостаточным, поэтому я не знаю, как это исправить. Выборка с фиксированной частотой, но обновление с динамической частотой приведет к чрезмерной корректировке системы и приведет к преувеличенному движению и, опять же, джиттеру. Что я считаю решающим, так это то, что система немедленно реагирует на свои исправления, поэтому вы не можете рассчитывать исправления с другой скоростью, чем та, с которой система реагирует на эти исправления.
Здесь вы можете увидеть этот подход в действии. Пока писал это, я заметил, что на этих записях также наблюдается заметное дрожание, и при перемещении мыши с относительно постоянной скоростью я также могу заметить это во время «игры», но в этом случае оно менее серьезное, чем в подходе 2, хотя на записях это может не проявляться.
В редакторе Unity (~150 кадров в секунду)
Изображение

В сборке (~700 кадров в секунду)
Изображение

Подход 2: расчеты с фиксированной скоростью в thread
Мой следующий подход заключался в запуске симуляции в FixUpdate, но в итоге это привело к невероятному джиттеру, поскольку FixUpdate обычно работает с гораздо меньшей частотой, чем Update, поэтому визуальным элементам передаются только новые скорректированные значения вращения каждые пару кадров, что приводит к заиканию анимации. Я очень быстро отказался от этого.
На этом этапе я предполагаю, что симуляцию необходимо запускать по крайней мере с тем же интервалом или чаще, чем обновление. Поэтому я переместил всю логику в отдельный поток и вместо того, чтобы напрямую обновлять вращение объекта, я сохраняю его в переменной-члене, которая затем используется в Update для обновления вращения сетки.
Симуляция теперь выполняется примерно 300 раз в секунду, тогда как игра в редакторе Unity работает со скоростью около 150 кадров в секунду, поэтому, насколько я понимаю, все должно быть хорошо. Однако меня все еще трясет, и у меня нет идей, как это исправить.
Изображение

Я написал небольшой инструмент, который строит временную диаграмму, показывающую, когда запускалось моделирование и когда выполнялось обновление, и оно выглядит следующим образом. Синие — вызовы обновления, черные — итерации моделирования. Вы можете ясно видеть, что оба выполняются через равные промежутки времени. Почему меня все еще трясет и как это исправить?
Изображение

Последнее средство
Моей последней идеей было бы ограничить частоту кадров в игре до 60 FPS. Это частота кадров, которой мне в любом случае нужно будет достичь, чтобы игроки получали удовольствие от игры, и она будет охватывать 95% игроков, поскольку у большинства геймеров (я полагаю?) нет мониторов с частотой обновления выше этой, но я уже вижу, как люди жалуются на это. Ограничение частоты кадров до чего-то высокого, например 144 кадров в секунду, не решит мою проблему, потому что это все равно приведет к тому, что анимация будет вести себя по-другому в системах, которые не могут достичь такой высокой частоты кадров (а их будет большинство). Также будет исключена вертикальная синхронизация, поскольку она (по крайней мере, в Unity) несовместима с максимальной частотой кадров.
Моя лучшая идея — принять этот факт и настроить все анимации на 60 кадров в секунду, принимая во внимание, что некоторые игроки будут воспринимать их по-другому, но это действительно неудовлетворительно, и мне очень хочется понять, как я могу это исправить.
Любые предложения приветствуются.>

Подробнее здесь: https://stackoverflow.com/questions/798 ... nd-jittery
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»