Камеры и движение

Управление позой и проекцией камеры, анимация, тряска, таймлайн и проверка видимого пространства.

Анимация камеры

Плавный полёт камеры из текущей позы в заданную: позиция, точка взгляда, вектор up и FOV доезжают за dur секунд по выбранной кривой. Включается одним вызовом, обновляется одним вызовом раз в кадр. Пока анимация не запущена, она не стоит ни одного такта — движок ничего не делает «за кулисами».

Кривые (CameraAnimationType)

Имена кривых лежат прямо в namespace движка — пишите их без префиксов:

Значение Поведение
LINEAR Постоянная скорость от старта до финиша
SMOOTH Плавный разгон и торможение (по умолчанию)
EASE Ещё сильнее «задерживается» на старте и в конце

Методы Camera

Метод Описание
void animateTo(const Vector3 &pos, const Vector3 &tgt, float dur = 1.0f, CameraAnimationType type = SMOOTH) Полёт к новой позиции и точке взгляда за dur секунд
void animateShot(const Vector3 &pos, const Vector3 &tgt, float fov, float dur = 1.0f, CameraAnimationType type = SMOOTH) Кинематографичный кадр одним полётом: позиция + цель + FOV
void animatePos(const Vector3 &pos, float dur = 1.0f) Полёт к позиции; точка взгляда смещается вместе с камерой
void animateTarget(const Vector3 &tgt, float dur = 1.0f) Камера на месте, плавно переводит взгляд на новую точку
void animateFOV(float fov, float dur = 1.0f) Плавный зум углом обзора (FOV-удар)
void stopAnim() Остановить полёт; камера замирает в текущей позе
bool isAnimating() true, пока полёт идёт
float animProgress() Прогресс после сглаживания: 0.0 — старт, 1.0 — прибытие. Удобно для триггеров «камера доехала наполовину»
void updateAnim(float dt) Продвинуть анимацию и тряску на dt секунд. Вызывать раз в кадр
Важно

Движок сам не обновляет анимацию камеры. Вызывайте cam.updateAnim(dt) один раз за кадр до рендера — одновременно с ней продвигается и тряска (cam.shake(...)).

Запуск новой анимации отменяет предыдущую — активной всегда максимум одна. Параметры текущего полёта (стартовая поза, дельты, время) скрыты: управлять ходом извне можно только через методы Camera, а класс CameraAnimation — внутренний узел движка (pip3d::detail). Наружу смотрит только перечисление кривых CameraAnimationType (LINEAR / SMOOTH / EASE) — его и передаёте в animate* и в ключи CameraTimeline.

Пример

Camera &cam = r.getCamera();

// кинематографичный облёт: за 2 секунды к точке с новым FOV
cam.animateShot(Vector3(8, 3, 8), Vector3(0, 1, 0), 50.0f, 2.0f, EASE);

// игровой цикл
while (true)
{
    const float dt = /* секунды с прошлого кадра */;

    cam.updateAnim(dt);          // полёт + тряска, раз в кадр
    if (!cam.isAnimating())
        startGameplay();         // камера доехала — отдать управление игроку

    // ... рендер кадра ...
}

Для полёта по цепочке ключевых кадров (вступительный ролик, осмотр локации) есть CameraTimeline.


Камера

Камера от первого/третьего лица: позиция, точка взгляда, FOV, ленивые кэши матриц и готовые перемещения. Поля position, target, up, fov, nearPlane, farPlane открыты и меняются напрямую — после прямых правок вызовите cam.markDirty(), все методы камеры делают это сами. Пока камера стоит на месте, matrices не пересчитываются ни разу.

Весь API камеры работает в градусах — никаких конвертаций на вашей стороне.

Оси

Метод Описание
const Vector3 &forward() Нормированный взгляд (кэшируется)
const Vector3 &right() Нормированный вправо (кэшируется)
const Vector3 &upVec() Текущий up
float yaw() / float pitch() Углы взгляда в градусах

Внутри камера полностью аффинная: никаких матриц 4×4 — три строки базиса + спроецированные на экран коэффициенты пересчитываются лениво, только когда камера сдвинулась.

Перемещение

Метод Описание
void move(float fwd, float right, float up) Сдвиг по своим осям (летит вместе с наклоном)
void moveFlat(float fwd, float right, float up = 0) Ходьба по yaw: наклон камеры не поднимает/не опускает
void moveWorld(float dx, float dy, float dz) Сдвиг в мировых осях (тележка, платформа): направление взгляда не меняется
void setPose(const Vector3 &pos, const Vector3 &tgt, const Vector3 &up = {0,1,0}) Поставить и навести камеру одним вызовом (up нормализуется сам)

Движение внутри кадра можно комбинировать: move/moveFlat/moveWorld — просто три разных способа задать дельту, каждая сама сбрасывает кэш вида.

Вращение

Метод Описание
void rotate(float yawDeg, float pitchDeg) Поворот, pitch зажимается ±89° (kPitchLimitDeg)
void setOrientation(float yawDeg, float pitchDeg) Задать углы абсолютом (yaw нормализуется, pitch зажимается)
void lookAt(const Vector3 &tgt) Повернуть взгляд в точку
void lookAt(const Vector3 &tgt, const Vector3 &up) То же + свой up
void orbit(const Vector3 &center, float radius, float azimuthDeg, float elevationDeg) Поставить камеру на сферу вокруг center, смотреть в центр (углы в градусах, как весь API камеры)

Проекция

Метод Описание
void setFrustum(float fovDeg = 60, float near = 0.1, float far = 100) FOV + плоскости отсечения одним вызовом
void setFov(float fovDeg) Только FOV (1…179°)

Тряска

Метод Описание
void shake(float amount) Добавить силу тряски 0…1 (суммируется, гаснет сама)
void shakeExplosion() Взрыв — сильная тряска
void shakeDamage() Получен урон — средняя
void shakeLanding() Приземление — средняя
void shakeStep() Шаг — лёгкая
void clearShake() Мгновенно погасить тряску

Тряска сдвигает и наклоняет картинку, не трогая position/target. Продвигается тем же cam.updateAnim(dt).

Как работает тряска

Модель trauma: каждый импульс (shake*) суммируется в общий уровень 0…1, картинка колышется с силой trauma² (слабые удары остаются деликатными, сильные — страшными), уровень сам гаснет со скоростью decay в секунду. Пресеты уже сбалансированы под экшен; сила любого импульса в долях единицы задаётся вручную через cam.shake(amount).

Тряска не портит игровые координаты: смещаются только матрицы вида, position/target остаются чистыми. Пока тряска не активна, она не стоит ни такта: updateAnim в покое делает одну проверку флага.

Пикинг и кэш

Метод Описание
bool pickRay(float screenX, float screenY, const Viewport &vp, Vector3 &origin, Vector3 &dir) Экранная точка → луч в мир (тач/мышь). false при нулевом viewport
void markDirty() Сбросить кэш после прямых правок полей
bool isViewDirty() / bool isProjDirty() Для движковых проходов: нужен ли пересчёт

Пример

Camera &cam = r.getCamera();

cam.setFrustum(60.0f, 0.1f, 200.0f);
cam.setPose(Vector3(0, 2, -8), Vector3(0, 1, 0));   // поставили и навели

// игровой цикл
while (true)
{
    const float dt = /* секунды с прошлого кадра */;

    if (btnLeft)  cam.rotate(-90.0f * dt, 0);
    if (btnFwd)   cam.moveFlat(4.0f * dt, 0);
    if (wasHit)   cam.shakeDamage();

    cam.updateAnim(dt);   // полёт + тряска
    // ... рендер: r.draw(...) внутри бандов ...
}

Готовые контроллеры

Класс Описание
FreeCam Камера от первого лица: handleJoystick(x, y, dt) — поворот, handleButtons(fwd, back, left, right, up, down, dt) — движение, handleDPad, handleRotateButtons. Настройки: rotSpeed, moveSpeed (°/с и ед./с), stickDeadzone, fpsMode (ходьба по yaw без полёта)
OrbitCam Облёт точки: поля center, radius, azimuth, elevation (градусы), тот же handleJoystick + handleButtons(zoomIn, zoomOut, dt) для зума
CameraBuilder Однострочная настройка: CameraBuilder().at(pos).lookAt(tgt).frustum(60).build()

Для роликов из ключевых кадров — CameraTimeline.


Фрустум

Усечённая пирамида видимости из 6 плоскостей: то, что камера реально видит. Движок сам заполняет фрустум каждый кадр (аналитически, без матриц) и отсекает им инстансы — руками вызывать обычно нужно только для собственных отсечений (маркеры, зоны, звук).

FrustumPlane — одна плоскость

Метод Описание
float distanceToPoint(const Vector3 &p) Знаковое расстояние: >= 0 — внутри полупространства

Frustum (он же CameraFrustum)

Плоскости индексируются PLANE_LEFT / RIGHT / TOP / BOTTOM / NEAR / FAR; стороны идут первыми — именно они чаще всего отсекают, и ранние выходы testSphere срабатывают раньше.

Метод Описание
bool testSphere(const Vector3 &center, float radius) Ложь = сфера целиком снаружи, объект можно пропустить. Цена: 2–6 точечных произведений с ранними выходами
bool testAABB(const Vector3 &min, const Vector3 &max) Ложь = бокс целиком снаружи. Для вытянутых объектов отсекает точнее сферы
const FrustumPlane &plane(int i) Прочитать плоскость для своих запросов (например, высота над горизонтом)
FrustumPlane &plane(int i) Записать плоскость: можно собрать свой фрустум (прожектор, триггер-зона) и гонять по нему testSphere/testAABB

Пример

Frustum &fr = r.getFrustum();  // актуален на текущий кадр

for (Enemy &e : enemies)
{
    // не тратим ИИ и отрисовку на то, чего камера не видит
    if (!fr.testSphere(e.pos, e.radius))
        continue;
    e.update(dt);
}
Не кэшируйте фрустум

Движок пересобирает его при каждом движении камеры, поэтому берите свежий через r.getFrustum().


Тряска камеры

Вся тряска живёт внутри Camera и включается одним вызовом из таблицы Тряска: cam.shake(amount) — ручной импульс, shakeExplosion()/shakeDamage()/shakeLanding()/shakeStep() — готовые пресеты. Гаснет сама, мгновенно гасится cam.clearShake(), состояние читается cam.isShaking().

Игре не нужно ничего из CameraShake напрямую — это внутренний узел камеры (pip3d::detail-стиля доступа у него нет, но дотянуться до него из игры всё равно некуда: состояние лежит в приватной части Camera). Если готовые пресеты не подошли — комбинируйте ручные импульсы cam.shake(0.0…1.0).


Камерные ролики

Проигрывание цепочки ключевых кадров: вступительный облёт, осмотр локации, кинематографичные переброски. Каждый кадр — полёт камеры к следующему ключу по кривой из раздела 1.

CameraKeyframe

CameraKeyframe k(pos, tgt, fovDegrees, dur, EASE, up);
Поле Смысл
position, target, up, fov Полная поза камеры в этом ключе
duration Время перехода к следующему ключу. 0 = мгновенная склейка (резкий кадр)
type Кривая перехода: LINEAR / SMOOTH / EASE

CameraTimeline

Метод Описание
void setTrack(const CameraKeyframe *keys, int n, bool looped = false) Задать дорожку ключей (2+ для ролика; 1 ключ = мгновенная установка позы)
void start(Camera &cam) Начать с первого ключа
bool update(Camera &cam, float dt) Продвинуть ролик (сам тикает анимацию камеры). true — не-луповая дорожка только что закончилась
bool isPlaying() Идёт ли ролик
void stop() Остановить (перезапуск — start())
Время жизни ключей

setTrack хранит указатель на массив — массив должен жить, пока играет ролик (глобальный, статический или член сцены).

Не тикайте камеру дважды: update() сам вызывает cam.updateAnim(dt) — в кадре с роликом не вызывайте его вручную.

Пример

static CameraKeyframe intro[] = {
    {Vector3(0, 2, -14), Vector3(0, 1, 0), 60.0f, 2.0f, EASE},
    {Vector3(9, 4, -6),  Vector3(0, 1, 0), 55.0f, 1.5f, SMOOTH},
    {Vector3(0, 12, 0),  Vector3(0, 0, 0), 70.0f, 2.5f, EASE},
};

CameraTimeline introTl;
introTl.setTrack(intro, 3, false);
introTl.start(cam);

// в цикле:
if (introTl.update(cam, dt))
    startGameplay();   // ролик закончился именно в этом кадре