/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "alcachofa/camera.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/script.h"
#include "common/system.h"
#include "math/vector4d.h"
using namespace Common;
using namespace Math;
namespace Alcachofa {
void Camera::resetRotationAndScale() {
_cur._scale = 1;
_cur._rotation = 0;
_cur._usedCenter.z() = 0;
}
void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
float scaleFactor = 1 - bgScale * kInvBaseScale;
_roomMin = Vector2d(
g_system->getWidth() / 2 * scaleFactor,
g_system->getHeight() / 2 * scaleFactor);
_roomMax = _roomMin + Vector2d(
bgSize.x * bgScale * kInvBaseScale,
bgSize.y * bgScale * kInvBaseScale);
_roomScale = bgScale;
}
void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
_cur._isFollowingTarget = target != nullptr;
_followTarget = target;
_lastUpdateTime = g_engine->getMillis();
_catchUp = catchUp;
if (target == nullptr)
_isChanging = false;
}
void Camera::setPosition(Vector2d v) {
setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
}
void Camera::setPosition(Vector3d v) {
_cur._usedCenter = v;
setFollow(nullptr);
}
void Camera::backup(uint slot) {
assert(slot < kStateBackupCount);
_backups[slot] = _cur;
}
void Camera::restore(uint slot) {
assert(slot < kStateBackupCount);
auto backupState = _backups[slot];
_backups[slot] = _cur;
_cur = backupState;
}
static Matrix4 scale2DMatrix(float scale) {
Matrix4 m;
m(0, 0) = scale;
m(1, 1) = scale;
return m;
}
void Camera::setupMatricesAround(Vector3d center) {
Matrix4 matTemp;
matTemp.buildAroundZ(_cur._rotation);
_mat3Dto2D.setToIdentity();
_mat3Dto2D.translate(-center);
_mat3Dto2D = matTemp * _mat3Dto2D;
_mat3Dto2D = scale2DMatrix(_cur._scale) * _mat3Dto2D;
_mat2Dto3D.setToIdentity();
_mat2Dto3D.translate(center);
matTemp.buildAroundZ(-_cur._rotation);
matTemp = matTemp * scale2DMatrix(1 / _cur._scale);
_mat2Dto3D = _mat2Dto3D * matTemp;
}
void minmax(Vector3d &min, Vector3d &max, Vector3d val) {
min.set(
MIN(min.x(), val.x()),
MIN(min.y(), val.y()),
MIN(min.z(), val.z()));
max.set(
MAX(max.x(), val.x()),
MAX(max.y(), val.y()),
MAX(max.z(), val.z()));
}
Vector3d Camera::setAppliedCenter(Vector3d center) {
setupMatricesAround(center);
if (g_engine->game().shouldClipCamera()) {
const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
Vector3d min, max;
min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
minmax(min, max, transform2Dto3D(Vector3d(screenW, 0, _roomScale)));
minmax(min, max, transform2Dto3D(Vector3d(screenW, screenH, _roomScale)));
minmax(min, max, transform2Dto3D(Vector3d(0, screenH, _roomScale)));
center.x() += MAX(0.0f, _roomMin.getX() - min.x());
center.y() += MAX(0.0f, _roomMin.getY() - min.y());
center.x() -= MAX(0.0f, max.x() - _roomMax.getX());
center.y() -= MAX(0.0f, max.y() - _roomMax.getY());
setupMatricesAround(center);
}
return _appliedCenter = center;
}
Vector3d Camera::transform2Dto3D(Vector3d v2d) const {
// if this looks like normal 3D math to *someone* please contact.
Vector4d vh;
vh.w() = 1.0f;
vh.z() = v2d.z() - _cur._usedCenter.z();
vh.y() = (v2d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
vh.x() = (v2d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
vh = _mat2Dto3D * vh;
return Vector3d(vh.x(), vh.y(), 0.0f);
}
Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
// I swear there is a better way than this. This is stupid. But it is original.
float depthScale = v3d.z() * kInvBaseScale;
Vector4d vh;
vh.x() = v3d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
vh.y() = v3d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
vh.z() = v3d.z();
vh.w() = 1.0f;
vh = _mat3Dto2D * vh;
return Vector3d(
g_system->getWidth() * 0.5f + vh.x() * kBaseScale / vh.z(),
g_system->getHeight() * 0.5f + vh.y() * kBaseScale / vh.z(),
_cur._scale * kBaseScale / vh.z());
}
Point Camera::transform3Dto2D(Point p3d) const {
auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
return { (int16)v2d.x(), (int16)v2d.y() };
}
void Camera::update() {
// original would be some smoothing of delta times, let's not.
uint32 now = g_engine->getMillis();
float deltaTime = (now - _lastUpdateTime) / 1000.0f;
deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
_lastUpdateTime = now;
if (_catchUp) {
for (int i = 0; i < 4; i++)
updateFollowing(50.0f);
_catchUp = false;
} else
updateFollowing(deltaTime);
setAppliedCenter(_cur._usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
}
void Camera::updateFollowing(float deltaTime) {
if (!_cur._isFollowingTarget || _followTarget == nullptr)
return;
const float resolutionFactor = g_system->getWidth() * 0.00125f;
const float acceleration = 460 * resolutionFactor;
const float baseDeadZoneSize = 25 * resolutionFactor;
const float minSpeed = 20 * resolutionFactor;
const float maxSpeed = this->_cur._maxSpeedFactor * resolutionFactor;
const float depthScale = _followTarget->graphic()->depthScale();
const auto characterPolygon = _followTarget->shape()->at(0);
const float halfHeight = ABS(characterPolygon._points[0].y - characterPolygon._points[2].y) / 2.0f;
Vector3d targetCenter = setAppliedCenter({
_shake.getX() + _followTarget->position().x,
_shake.getY() + _followTarget->position().y - depthScale * 85,
_cur._usedCenter.z() });
targetCenter.y() -= halfHeight;
float distanceToTarget = as2D(_cur._usedCenter - targetCenter).getMagnitude();
float moveDistance = _followTarget->stepSizeFactor() * _cur._speed * deltaTime;
float deadZoneSize = baseDeadZoneSize / _cur._scale;
if (_followTarget->isWalking() && depthScale > 0.8f)
deadZoneSize = (baseDeadZoneSize + (depthScale - 0.8f) * 200) / _cur._scale;
bool isFarAway = false;
if (ABS(targetCenter.x() - _cur._usedCenter.x()) > deadZoneSize ||
ABS(targetCenter.y() - _cur._usedCenter.y()) > deadZoneSize) {
isFarAway = true;
_cur._isBraking = false;
_isChanging = true;
}
if (_cur._isBraking) {
_cur._speed -= acceleration * 0.9f * deltaTime;
_cur._speed = MAX(_cur._speed, minSpeed);
}
if (_isChanging && !_cur._isBraking) {
_cur._speed += acceleration * deltaTime;
_cur._speed = MIN(_cur._speed, maxSpeed);
if (!isFarAway)
_cur._isBraking = true;
}
if (_isChanging) {
if (distanceToTarget <= moveDistance) {
_cur._usedCenter = targetCenter;
_isChanging = false;
_cur._isBraking = false;
} else {
Vector3d deltaCenter = targetCenter - _cur._usedCenter;
deltaCenter.z() = 0.0f;
_cur._usedCenter += deltaCenter * moveDistance / distanceToTarget;
}
}
}
static void syncMatrix(Serializer &s, Matrix4 &m) {
float *data = m.getData();
for (int i = 0; i < 16; i++)
s.syncAsFloatLE(data[i]);
}
static void syncVector(Serializer &s, Vector3d &v) {
s.syncAsFloatLE(v.x());
s.syncAsFloatLE(v.y());
s.syncAsFloatLE(v.z());
}
void Camera::State::syncGame(Serializer &s) {
syncVector(s, _usedCenter);
s.syncAsFloatLE(_scale);
s.syncAsFloatLE(_speed);
s.syncAsFloatLE(_maxSpeedFactor);
float rotationDegs = _rotation.getDegrees();
s.syncAsFloatLE(rotationDegs);
_rotation.setDegrees(rotationDegs);
s.syncAsByte(_isBraking);
s.syncAsByte(_isFollowingTarget);
}
void Camera::syncGame(Serializer &s) {
syncMatrix(s, _mat3Dto2D);
syncMatrix(s, _mat2Dto3D);
syncVector(s, _appliedCenter);
s.syncAsUint32LE(_lastUpdateTime);
s.syncAsByte(_isChanging);
_cur.syncGame(s);
for (uint i = 0; i < kStateBackupCount; i++)
_backups[i].syncGame(s);
// originally the follow object is also searched for before changing the room
// so that would practically mean only the main characters could be reasonably found
// instead we fall back to global search
String name;
if (_followTarget != nullptr)
name = _followTarget->name();
s.syncString(name);
if (s.isLoading()) {
if (name.empty())
_followTarget = nullptr;
else {
_followTarget = dynamic_cast(g_engine->world().getObjectByName(name.c_str()));
if (_followTarget == nullptr)
_followTarget = dynamic_cast(g_engine->world().getObjectByNameFromAnyRoom(name.c_str()));
if (_followTarget == nullptr)
warning("Camera follow target from savestate was not found: %s", name.c_str());
}
}
}
struct CamLerpTask : public Task {
CamLerpTask(Process &process, uint32 duration = 0, EasingType easingType = EasingType::Linear)
: Task(process)
, _camera(g_engine->camera())
, _duration(duration)
, _easingType(easingType) {}
TaskReturn run() override {
TASK_BEGIN;
_startTime = g_engine->getMillis();
while (g_engine->getMillis() - _startTime < _duration) {
update(ease((g_engine->getMillis() - _startTime) / (float)_duration, _easingType));
_camera._isChanging = true;
TASK_YIELD(1);
}
update(1.0f);
TASK_END;
}
void debugPrint() override {
uint32 remaining = g_engine->getMillis() - _startTime <= _duration
? _duration - (g_engine->getMillis() - _startTime)
: 0;
g_engine->console().debugPrintf("%s camera with %ums remaining\n", taskName(), remaining);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
s.syncAsUint32LE(_startTime);
s.syncAsUint32LE(_duration);
syncEnum(s, _easingType);
}
protected:
virtual void update(float t) = 0;
Camera &_camera;
uint32 _startTime = 0, _duration;
EasingType _easingType;
};
struct CamLerpPosTask final : public CamLerpTask {
CamLerpPosTask(Process &process, Vector3d targetPos, int32 duration, EasingType easingType)
: CamLerpTask(process, duration, easingType)
, _fromPos(_camera._appliedCenter)
, _deltaPos(targetPos - _camera._appliedCenter) {}
CamLerpPosTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
syncVector(s, _fromPos);
syncVector(s, _deltaPos);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera.setPosition(_fromPos + _deltaPos * t);
}
Vector3d _fromPos, _deltaPos;
};
DECLARE_TASK(CamLerpPosTask)
struct CamLerpScaleTask final : public CamLerpTask {
CamLerpScaleTask(Process &process, float targetScale, int32 duration, EasingType easingType)
: CamLerpTask(process, duration, easingType)
, _fromScale(_camera._cur._scale)
, _deltaScale(targetScale - _camera._cur._scale) {}
CamLerpScaleTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
s.syncAsFloatLE(_fromScale);
s.syncAsFloatLE(_deltaScale);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera._cur._scale = _fromScale + _deltaScale * t;
}
float _fromScale = 0, _deltaScale = 0;
};
DECLARE_TASK(CamLerpScaleTask)
struct CamLerpPosScaleTask final : public CamLerpTask {
CamLerpPosScaleTask(Process &process,
Vector3d targetPos, float targetScale,
int32 duration,
EasingType moveEasingType, EasingType scaleEasingType)
: CamLerpTask(process, duration, EasingType::Linear) // linear as we need different ones per component
, _fromPos(_camera._appliedCenter)
, _deltaPos(targetPos - _camera._appliedCenter)
, _fromScale(_camera._cur._scale)
, _deltaScale(targetScale - _camera._cur._scale)
, _moveEasingType(moveEasingType)
, _scaleEasingType(scaleEasingType) {}
CamLerpPosScaleTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
syncVector(s, _fromPos);
syncVector(s, _deltaPos);
s.syncAsFloatLE(_fromScale);
s.syncAsFloatLE(_deltaScale);
syncEnum(s, _moveEasingType);
syncEnum(s, _scaleEasingType);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
_camera._cur._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
}
Vector3d _fromPos, _deltaPos;
float _fromScale = 0, _deltaScale = 0;
EasingType _moveEasingType = {}, _scaleEasingType = {};
};
DECLARE_TASK(CamLerpPosScaleTask)
struct CamLerpRotationTask final : public CamLerpTask {
CamLerpRotationTask(Process &process, float targetRotation, int32 duration, EasingType easingType)
: CamLerpTask(process, duration, easingType)
, _fromRotation(_camera._cur._rotation.getDegrees())
, _deltaRotation(targetRotation - _camera._cur._rotation.getDegrees()) {}
CamLerpRotationTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
s.syncAsFloatLE(_fromRotation);
s.syncAsFloatLE(_deltaRotation);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
}
float _fromRotation = 0, _deltaRotation = 0;
};
DECLARE_TASK(CamLerpRotationTask)
static void syncVector(Serializer &s, Vector2d &v) {
float *data = v.getData();
s.syncAsFloatLE(data[0]);
s.syncAsFloatLE(data[1]);
}
struct CamShakeTask final : public CamLerpTask {
CamShakeTask(Process &process, Vector2d amplitude, Vector2d frequency, int32 duration)
: CamLerpTask(process, duration, EasingType::Linear)
, _amplitude(amplitude)
, _frequency(frequency) {}
CamShakeTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
syncVector(s, _amplitude);
syncVector(s, _frequency);
}
const char *taskName() const override;
protected:
void update(float t) override {
const Vector2d phase = _frequency * t * (float)M_PI * 2.0f;
const float amplTimeFactor = 1.0f / expf(t * 5.0f); // a curve starting at 1, depreciating towards 0
_camera.shake() = {
sinf(phase.getX()) * _amplitude.getX() * amplTimeFactor,
sinf(phase.getY()) * _amplitude.getY() * amplTimeFactor
};
}
Vector2d _amplitude, _frequency;
};
DECLARE_TASK(CamShakeTask)
struct CamWaitToStopTask final : public Task {
CamWaitToStopTask(Process &process)
: Task(process)
, _camera(g_engine->camera()) {}
CamWaitToStopTask(Process &process, Serializer &s)
: Task(process)
, _camera(g_engine->camera()) {
syncGame(s);
}
TaskReturn run() override {
return _camera._isChanging
? TaskReturn::yield()
: TaskReturn::finish(1);
}
void debugPrint() override {
g_engine->console().debugPrintf("Wait for camera to stop moving\n");
}
const char *taskName() const override;
private:
Camera &_camera;
};
DECLARE_TASK(CamWaitToStopTask)
struct CamSetInactiveAttributeTask final : public Task {
enum Attribute {
kPosZ,
kScale,
kRotation
};
CamSetInactiveAttributeTask(Process &process, Attribute attribute, float value, int32 delay)
: Task(process)
, _camera(g_engine->camera())
, _attribute(attribute)
, _value(value)
, _delay(delay) {}
CamSetInactiveAttributeTask(Process &process, Serializer &s)
: Task(process)
, _camera(g_engine->camera()) {
syncGame(s);
}
TaskReturn run() override {
if (_delay > 0) {
uint32 delay = (uint32)_delay;
_delay = 0;
return TaskReturn::waitFor(new DelayTask(process(), delay));
}
auto &state = _camera._backups[0];
switch (_attribute) {
case kPosZ:
state._usedCenter.z() = _value;
break;
case kScale:
state._scale = _value;
break;
case kRotation:
state._rotation = _value;
break;
default:
g_engine->game().unknownCamSetInactiveAttribute((int)_attribute);
break;
}
return TaskReturn::finish(0);
}
void debugPrint() override {
const char *attributeName;
switch (_attribute) {
case kPosZ:
attributeName = "PosZ";
break;
case kScale:
attributeName = "Scale";
break;
case kRotation:
attributeName = "Rotation";
break;
default:
attributeName = "";
break;
}
g_engine->console().debugPrintf("Set inactive camera %s to %f after %dms\n", attributeName, _value, _delay);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncEnum(s, _attribute);
s.syncAsFloatLE(_value);
s.syncAsSint32LE(_delay);
}
const char *taskName() const override;
private:
Camera &_camera;
Attribute _attribute = {};
float _value = 0;
int32 _delay = 0;
};
DECLARE_TASK(CamSetInactiveAttributeTask)
Task *Camera::lerpPos(Process &process,
Vector2d targetPos,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new DelayTask(process, duration); // lerpPos does not handle inactive players
}
Vector3d targetPos3d(targetPos.getX(), targetPos.getY(), _appliedCenter.z());
return new CamLerpPosTask(process, targetPos3d, duration, easingType);
}
Task *Camera::lerpPos(Process &process,
Vector3d targetPos,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new DelayTask(process, duration); // lerpPos does not handle inactive players
}
setFollow(nullptr); // 3D position lerping is the only task that resets following
return new CamLerpPosTask(process, targetPos, duration, easingType);
}
Task *Camera::lerpPosZ(Process &process,
float targetPosZ,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kPosZ, targetPosZ, duration);
}
Vector3d targetPos(_appliedCenter.x(), _appliedCenter.y(), targetPosZ);
return new CamLerpPosTask(process, targetPos, duration, easingType);
}
Task *Camera::lerpScale(Process &process,
float targetScale,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kScale, targetScale, duration);
}
return new CamLerpScaleTask(process, targetScale, duration, easingType);
}
Task *Camera::lerpRotation(Process &process,
float targetRotation,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kRotation, targetRotation, duration);
}
return new CamLerpRotationTask(process, targetRotation, duration, easingType);
}
Task *Camera::lerpPosScale(Process &process,
Vector3d targetPos, float targetScale,
int32 duration,
EasingType moveEasingType, EasingType scaleEasingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kScale, targetScale, duration);
}
return new CamLerpPosScaleTask(process, targetPos, targetScale, duration, moveEasingType, scaleEasingType);
}
Task *Camera::waitToStop(Process &process) {
return new CamWaitToStopTask(process);
}
Task *Camera::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration) {
if (!process.isActiveForPlayer()) {
return new DelayTask(process, (uint32)duration);
}
return new CamShakeTask(process, amplitude, frequency, duration);
}
} // namespace Alcachofa