/* 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