Анимация камеры
Плавный полёт камеры из текущей позы в заданную: позиция, точка взгляда, вектор 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 ¢er, 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 ¢er, 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(); // ролик закончился именно в этом кадре